I understand that
SELFDESTRUCT and mispricing of opcodes allowed many empty accounts to be created cheaply as explained in Why were empty accounts allowed to be on the blockchain? The answer to that question also includes a transaction that created many empty accounts. How did that transaction/attack work? When a contract invokes
SELFDESTRUCT, it can't
SELFDESTRUCT again... or could it?
I understand that
Ancient history, but here goes :-)
There was one coordinating contract for the initial attack. A later version had a more complex algorithm to generate the empty account addresses, presumably to try to frustrate any future clean-up effort. This one just incrementally worked through the address space.
Every time the coordinating contract was called - and it was called 4750 times - it did the following:
(1) Create a child contract.
0000 PUSH32 0x6004600c60003960046000f3600035ff00000000000000000000000000000000 0021 PUSH1 0x00 0023 MSTORE 0024 PUSH1 0x20 0026 PUSH1 0x00 0028 PUSH1 0x00 002a CREATE
This creates a single copy of the following child contract on a new address:
;; Child contract (in full) PUSH1 0x00 CALLDATALOAD SELFDESTRUCT
The code for this child contract is embedded in the first
PUSH32 of the coordinating contract:
0000 PUSH32 0x6004600c60003960046000f3600035ff00000000000000000000000000000000
The first 12 bytes of this are the code for the constructor of the child contract; the next four bytes are the child contract code itself, as above.
(2) Load a counter from storage:
002b PUSH1 0x00 002d SLOAD 002e DUP1
This keeps track of the empty/bloat accounts being created and allows subsequent calls to the coordinating contract to continue from where the previous one left off.
(3) Loop over the rest of the contract: each loop iteration calls the child contract 40 times (i.e. the following code is duplicated 40 times)
0030 PUSH1 0x01 0032 ADD 0033 DUP1 0034 PUSH1 0x00 0036 MSTORE 0037 PUSH1 0x00 0039 DUP1 003a PUSH1 0x20 003c DUP2 003d DUP1 003e DUP8 003f PUSH1 0x06 0041 CALL 0042 POP
Here it is adding one to the counter, duplicating it, putting one copy back into memory and then calling the child contract with the other copy of the counter as the call data.
The child contract interprets the data it is sent (the counter) as an address, and it when it
SELFDESTRUCTs it sends its (zero) value to that address creating an empty bloat account.
The key point here is that the child contract doesn't actually self-destruct until the end of the execution of the current transaction, so it is able to be called many times over within the one transaction. The description in the yellow paper is "Halt execution and register account for later deletion" (emphasis mine); it will be deleted at the termination of the current transaction. It is sufficient for the child contract to be recreated only on each invocation of the coordinating contract; it can then be called many times from within it.
(4) Check remaining gas, and if there is sufficient then go back and loop over all forty calls again, continuing to increment the address counter.
0328 GAS 0329 PUSH2 0x6000 032c LT 032d PUSH3 0x00002f 0331 JUMPI
Thus, each call to the coordinating contract was able to create thousands of bloat accounts, depending on the amount of gas initially supplied.
(5) Finally, store the address counter ready for the next invocation.
0332 PUSH1 0x00 0334 SSTORE
The problem was, of course, that creating an account via SELFDESTRUCT did not use nearly as much gas as it ought to have done. This was addressed in EIP150: long-term gas cost changes.