On May 13th, tornado cash governance was hacked.
Let’s understand how it was possible and what was the vulnerability. This post won’t be about the statistics but more about the technical route of the attack.
TL;DR — The attacker mainly used CREATE, CREATE2, and selfdestruct to exploit the governance. They proposed a contract identical to the previously passed proposal, but this proposal has a selfdestruct function that went unnoticed. After getting accepted, the hacker deletes the proposal contract and deploys a malicious contract at the same address. As this address was already accepted by the governance, they got full control of the governance contract.
First of all, governance works in a way where members submit their proposals, and the other members vote for approval or rejection of the proposal. To participate in the governance, you need to lock the specific tokens(TORN tokens in this case).
After voting or creating a proposal, the tokens are completely locked until the execution or rejection of the proposal. The proposal must be submitted as a verified smart contract, and if approved by the DAO, the code executes via delegatecall in the governance contract.
Everything seems good till now unless we get to know the main vulnerability used by the attacker was
selfdestruct() along with CREATE2 opcode.
Let’s break this down into several steps.
- The hacker locks their TORN tokens to participate in the governance.
2. Deploys a proposal contract, which was claimed to be the same as an earlier proposal that has been accepted by the governance. But it was not the truth, attacker introduced a change in the contract which is the inclusion of a function
emergencyStop which was supposed to execute the
selfdestruct function and delete the contract. We can use the term Trojan horse for this.
3. When this proposal got passed by the voters, the attacker uses
selfdestruct and then deploys a new malicious contract on the same address.
4. And now that the logic of the passed proposal has been changed to whatever they wanted, they first allocated the majority of votes to themselves and then drained all the funds they could.
But the question is how was it possible? What was the main point of failure in this attack?
Let’s understand step 2 mentioned above. How could they deploy a new contract with a different code to the same address?
To answer this question, the most basic concept we should know is the process of address generation of a smart contract while using CREATE and CREATE2 opcodes.
When we use CREATE opcode to deploy a contract, the address generated for that contract depends on the address of the creator and the nonce of the creator.
This is how the address is generated using the sender’s address and nonce:
address of contract = last 20 bytes of sha3(rlp(sender,nonce))
Did you know? Smart contract addresses also have a nonce. But it’s different from an EOA nonce because it only increases when the contract deploys another contract, and not every time a contract calls some other contract functions that we call “internal transaction”. And after EIP 161, the nonce starts from 1 instead of 0 in a newly deployed contract. Keep this in mind as we move forward.
In CREATE2 there is no need for nonce. The address generation depends on 4 parameters.
- 0xFF — A constant that is used to prevent collisions with CREATE
- sender’s address — deployer contract’s address
- salt — arbitrary data in bytes format sent by the creator
- creation code— creation code of the contract to be deployed
We can compute the address even before the deployment. Here is a small practical you can check.
The solidity code to generate the address looks something like this.
address of contract = address( uint160( uint256( keccak256( abi.encodePacked( bytes1(0xFF), address(this), salt, keccak256(creation code) ) ) ) ) );
We can now say that in order to deploy a contract on the same address as before, we need to meet these 2 conditions.
- The deployer address should be the same in both CREATE and CREATE2.
- In CREATE the nonce should be the same, whereas in CREATE2 the contract creation code should be the same.
Now let’s see how the hacker combined the potentials of
selfdestruct, CREATE, and CREATE2 in this case. I will again write the steps but this time in a more understanding way.
- The attacker deploys a contract. let’s say the “Deployer” contract.
- From this Deployer Contract they deploy another contract but with CREATE2, let’s name this “Sender” contract.
- From the Sender contract, they deploy the proposal contract using the CREATE opcode.
As of now, the flow looks something like this:
4. As soon as the governance voted and passed the proposal, the attacker used selfdestruct in both the Proposal and Sender contracts and again deployed the Sender contract at the same address. And then similar to before they used the Sender contract to deploy the malicious contract at the address of the previously destructed proposal contract.
Here comes the crucial part about CREATE and CREATE2.
CREATE depends on the sender’s address and the nonce, every time the contract deploys another contract, the nonce increases by 1.
And CREATE2 depends on the sender’s address, salt, and creation code. This means the nonce needs not to be the same but the contract creation code should be the same.
It’s very much clear that to deploy the malicious contract on the same address as the proposal contract’s address, we can’t use CREATE2 because the code has changed so as the creation code.
That means we need to go with the default deployment method which is CREATE.
But here we got another problem, i.e. after deploying the proposal contract the nonce of the Sender contract would have been increased by 1, and while deploying the malicious contract, a different contract address will be generated.
That’s the reason why the hacker destructed the Sender contract as well, selfdestruct also resets the address nonce to 0. So that after deploying the Sender contract using CREATE2 the nonce will be again 1, and the address is already the same. And this leads to the deployment of the malicious contract at the same address where the accepted proposal contract was deployed before.
This way the attacker got full control. With this takeover, they unlocked the locked TORN tokens and 10,000 governance tokens were assigned to each address controlled by the attacker for a total of 1.2 million votes.
With this many votes, the attacker had complete control over the Tornado Cash governance system since only about 70,000 legitimate votes existed. They took out around a million dollar worth of assets. Also used Tornado Cash itself to get the ETHs out.
I hope it was easy to understand, in case there is some confusion about anything you can reach out to us HERE.