create
address = hash(sender, nonce)create2
address = hash(0xFF, sender, salt, bytecode)- The addresses of contracts created with the
create
andcreate2
methodology can be pre-computed.
- A person from TornadoCash deploys their
TornadoCashDAO
contract. Responsible for approving and executing proposals. - Attacker deploys contract
DeployerUsingCreate2
. A contract that will deploy another contact calledDeployerUsingCreate
usingcreate2
. The address ofDeployerUsingCreate
will always be fixed at0xXYZ
becausecreate2
is based on the bytecode of the contract being deployed. - Attacker calls
DeployerUsingCreate
contract with normalcreate
to deployProposal
contract at address be0xABC
. The contract address of a contract being deployed withcreate
depends on the sender and nonce. It can also be pre-computed just like create2, except using sender and nonce rather than the bytecode. - The people at TornadoCash governance voted that the
Proposal
contract at address0xABC
was not malicious.selfdestruct
was somehow hidden in the code. TheTornadoCashDAO
then contract callsapprove
forProposal
. The contract address0xABC
associated withProposal
can now callexecute
whenever they want, which is a delegate call to the DAO. However,Proposal
doesn't actually contain any directly malicious code. The attacker needs to deploy a malicious contract at the same address ofProposal
now. - Attacker selfdestructs
Proposal
andDeployerUsingCreate
contracts. Self destructingDeployerUsingCreate
allows for the nonce to be reset, if the attacker can manage to deployDeployerUsingCreate
to the same address. - Attacker redeploys
DeployerUsingCreate
contract withDeployerUsingCreate2
usingcreate2
.DeployerUsingCreate
will have the same address of0xXYZ
because it's bytecode is the same. - Attacker deploys
Attack
contract withDeployerUsingCreate
usingcreate
, which also gives it the same address ofProposal
of0xABC
. This is because the sender is the same,DeployerUsingCreate
, and the nonce is 0, the same as before. Effectively, the attacker had to find a clever way to reset the nonce to 0 in order to make another contract at the same address. - Attacker runs
executeProposal
insideAttack
, which invokes a delegatecall insideTornadoCash
contract from it'sexecute
function, allowing the malicious code fromAttack
to be run in the context ofTornadoCash
.TornadoCash
thought this contract wasProposal
, since it's address is the same, and is already verified, but it's actuallyAttack
.
- Deploy
TornadoCashDAO
with an Address 'A', representing an entity associated with TornadoCash DAO. - Attacker deploys
DeployerUsingCreate2
contract with address 'B', representing the attackers address. - Attacker calls
deploy
function fromDeployerUsingCreate2
to deployDeployerUsingCreate
, attacker retrieves address ofDeployerUsingCreate
. - Attacker calls
deployProposal
function fromDeployerUsingCreate
to deploy the innocentProposal
contract, attacker retrieves address ofProposal
. - TornadoCash DAO votes to approve the
Proposal
contract, and thenTornadoCashDAO
contract callsapprove
forProposal
. - Attacker calls
emergencyStop
andkill
forProposal
andDeployer
, self destructing both. - Attacker calls
deploy
function fromDeployerUsingCreate2
to deployDeployerUsingCreate
again. The address is the same as before. - Attacker calls
deployAttack
function fromDeployerUsingCreate
to deploy the maliciousAttack
contract. The address is the same asProposal
. TornadoCashDAO
callsexecute
to bring in the proposal's update. It uses a delegatecall to call theexecuteProposal
function inAttack
. TheexecuteProposal
function is malicious inAttack
, and changes the owner to theAttack
contract.
- Assume
selfdestruct
is malicious even if coming from a trusted source. Carefully analyze the code and exactly why it was added. - Be wary of delegatecalls as always.
- A contract can be re-created at the same address after having been destroyed. But it is possible for that newly created contract to have a different deployed bytecode even though the creation bytecode has been the same. Iif the constructor can query external state that might have changed between the two creations and incorporate that into the deployed bytecode before it is stored.