We will first consider what the attacker wanted to get from the attack, and it seems to be the following:
- The attacker wanted to capture (and has captured) a certain amount of funds through this exploit.
- He wanted to gain (and has gained) majority voting power.
- Most notably, the attacker most likely intended for all of this to happen without being noticed (he did not achieve this).
Let us start from the last point, why do we suspect he didn’t want to be noticed?
We suspect this because in [0], one can notice that he deploys a contract, which is the Proposal #20 contract, 3 times. The attacker does this using the SELFDESTRUCT
opcode and a metamorphic contract factory [1] (the factory allows him to deploy different contracts at the same address). This implies, that the attacker wanted to give the impression, post-execution, that the originally intended bytecode had been executed, which was not the case.
The attacker now proposes the first instance of the Proposal contract which can self-destruct.
With the backdoored proposal awaiting the end of voting and execution, the first half of the preparation for the attack had been completed. What was now necessary for the attacker was to plant exactly those accounts, for which the malicious proposal would increase locked balances, and which would allow him to:
- Withdraw funds from the Vault slowly, instead of doing this in one go.
- Be able to withdraw a variable amount of funds.
- Give the impression that there is no one large TORN holder with a majority vote.
In [2] the attacker deploys a contract which is capable of deploying other contracts. These contracts immediately start locking 0 balances (with lockWithApproval
) in Governance. This is visible in [3], [4], [5], [6]. There is multiple reasons for doing this:
- Since he decided not to use deterministic addresses for the sub-contracts (this can be seen from decompiled bytecode), he needed to know the addresses of the deployed contracts in forward to prepare the malicious proposal code for the attack.
- He needs to
lockWithApproval
because if he intends to not be noticed, but still claim rewards, he needs to set the accumulated reward value for each account to a normal value because otherwise onegetReward()
call would rug the entire staking contract.
Now that everything had been prepared, the attacker replaces, as shown in the second contract creation transaction visible in [0], the Proposal contract, and executes it which successfully executes the attack and gives him majority voting power. Note that the attacker also executes the expected state changes, trying to hide the malicious logic from being noticed. All of this, is visible in [7].
In [8] and [9], the attacker starts withdrawing his first 10,000 TORN tokens. Initially, he does not rug the entire vault, because he most likely believes that he has not been noticed for now. Thanks to @Theo, the malicious execution is discovered and as many TORN holders as possible are alerted. About 260,000 TORN manages to be withdrawn from the vault (not by the hacker) before the full rug.
15 hours after the first withdrawal, the attacker discovers that the attack had been noticed and decides to rug the entire treasury. This can be found in [10].
After the Vault had been drained, Staking rewards remained. While the attacker was busy transferring tokens, he did not notice that rugging the Vault would lead to abnormally large reward values being assigned (accumulated). In the meantime, someone manages to exactly claim the entire staking balance as shown in [11].
Due to this, the attacker initially thought that he would need to update the rewards for each account and withdraw, because they have been assigned as such. He does this in [12], but then realizes that the rewards have already been claimed.
Finally, in [13], the attacker proposes a Proposal to set lockedBalance
(s) for all 101 addresses used in the attack back to 0. Some of these addresses, which are included in this proposal, already have a 0 balance, due to the withdrawals, but the attacker most likely did not have enough time to manually choose the addresses and decides to zero them all out instead.
This is the full description of the attack. The attacker currently has more than 700,000 TORN voting power, and he has deployed a contract with which he (as much as we have been able to verify, another thread will be posted on this topic) intends to revert all state changes made, view the contract here [14].
Links
- pcaversaccio has created an example repository showcasing how this attack can be executed.
Footnote
Transactions: [0], [1], [2], [3], [4], [5], [6], [7], [8], [9], [10], [11], [12], [13], [14]
Edit 1,2,3,4 (all): Moved current state to bottom, grammar, added Proposal 21 contract.
Edit 5: Add pcaver example link and demarcate footnotes.
Edit 6: Note that he also executed valid logic in proposal to hide malicious.