Analysis on the principle of EGD price manipulation attack incident--phalcon+etherscan

Other related content can be found on: Personal homepage

EGD attack related information

Happens on BSC

EGD-Finance code analysis and attack process explanation

All calls to the EGD_Finance contract are made through the proxy contract.

Click "Read as Proxy" on the blockchain browser to view the final contract.

EGD FinanceThe main function implemented in the contract is to pledge USDT for a period of time, and you can withdraw the reward EGD token , which is equivalent to a bank deposit. After depositing for a period of time, you can withdraw the interest.

The following staking steps and redemption reward steps are all transaction steps actually initiated by the attacker.

Pledge steps

Address 0xbc5e8602c4fba28d0efdbf3c6a52be455d9558f5 | BscScan calls stake()the function of the attack contract to perform the corresponding mortgage operation. This address should also be the address of the attacker, who created the attack contract.

The specific transactions are as follows: BNB Smart Chain Transaction Hash (Txhash) Details | BscScan

We can see the specific call information of this transaction on Phalcon:

image-20231219160115062

Here is further analysis:

EGD_Finance | Address 0x93c175439726797dcee24d08e4ac9164e88e7aee | The function in BscScan bond()should just fill in the following inviter. It should be the same as web2. The inviter of each address is related to the pledge income.

function bond(address invitor) external {        
        require(userInfo[msg.sender].invitor == address(0), 'have invitor');
        require(userInfo[invitor].invitor != address(0) || invitor == fund, 'wrong invitor');
        userInfo[msg.sender].invitor = invitor;
        userInfo[invitor].refer ++;

    }

The next swapETHForExactTokens()call is a very common token exchange operation in Defi. Looking at the source code through the address, it is consistent with the corresponding function of uniswap_v2

It can be seen from the name that an uncertain amount of ETH is exchanged for a certain number of tokens. It can be determined that the exchange is USDT.

uniswap parameter list

function swapETHForExactTokens(
    uint amountOut, // 交易获得的代币数量
    address[] calldata path, // 交易路径列表
    address to, // 交易获得的 token 发送到的地址
    uint deadline // 过期时间
) external virtual override payable ensure(deadline) returns (
    uint[] memory amounts // 交易期望数量列表
){
    ...
}

PancakeSwap: Router v2 | Address 0x10ed43c718714eb63d5aa57b78b54704e256024e | BscScan

    function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline)
        external
        virtual
        override
        payable
        ensure(deadline)
        returns (uint[] memory amounts)
    {
    	//检查是否为WETH进行交换
        require(path[0] == WETH, 'PancakeRouter: INVALID_PATH');
        // 从library中获知得到amountOut数量的USDT,需要多少ETH
        amounts = PancakeLibrary.getAmountsIn(factory, amountOut, path);
        //发给pancake的ETH必须大于所需数量
        require(amounts[0] <= msg.value, 'PancakeRouter: EXCESSIVE_INPUT_AMOUNT');
        // 将 WETH 换成 ETH(对应phalcon的操作)
        IWETH(WETH).deposit{value: amounts[0]}();
        // 将 amounts[0] 数量的 path[0] 代币从用户账户中转移到 path[0], path[1] 的流动池
        assert(IWETH(WETH).transfer(PancakeLibrary.pairFor(factory, path[0], path[1]), amounts[0]));
        // 按 path 列表执行交易集合,不细究了,之后再详细看uniswap-qwq
        _swap(amounts, path, to);
        // 返回多余的ETH
        if (msg.value > amounts[0]) TransferHelper.safeTransferETH(msg.sender, msg.value - amounts[0]);
    }

Finally, stake()the function of the agent contract is called and 100 USDT are pledged. It can be seen from the call record in Phalcon that the specific function code will not be analyzed. It mainly records a lot of relevant information for subsequent reward calculations. It can be seen that EGD_Finance | Address 0x93c175439726797dcee24d08e4ac9164e88e7aee | BscScan

image-20231219162437653

Steps to redeem rewards

Address 0xee0221d76504aec40f63ad7e36855eebf5ea5edd | BscScan The attacker calls harvest()the function of the attack contract to redeem the corresponding reward. The transaction analysis of phalcon is shown in the figure below:

image-20231219162808682

First, the contract is called calculateAll()函数to calculate the user's pledge reward and how much total income the user can get.

    function calculateReward(address addr, uint slot) public view returns (uint){
        UserSlot memory info = userSlot[addr][slot];
        if (info.leftQuota == 0) {
            return 0;
        }
        uint totalRew = (block.timestamp - info.claimTime) * info.rates;
        if (totalRew >= info.leftQuota) {
            totalRew = info.leftQuota;
        }
        return totalRew;
    }

Then the user started the flash loan operation.

First, 200 USDT PancakeSwapwas borrowed through (0x16b9a82891338f9ba80e2d6970fdda79d1eb0dae) call , and then the optimistic transfer and callback attack contract were visible in the previous flash loan analysis article.swap()函数pancakeCall()函数

After pancakecall()函数中another flash loan was initiated, 424456 USDT were borrowed from Pancake LPs(0xa361433e409adac1f87cdf133127585f8a93c67d) , and then called back again . This is the reward that the user had pledged before redeeming himself.swap()函数pancakeCall()函数claimAllReward()函数

It should be obvious to guess why the rewards are redeemed here. It is because the large amount of flash loans affects the calculation method of rewards. There should be a loophole in the function that calculates pledge rewards.

Now go to the PancakeSwap: Router v2 | Address 0x10ed43c718714eb63d5aa57b78b54704e256024e | BscScan project claimAllReward()to look at the specific source code and make detailed comments:

 function claimAllReward() external {
 		//判断是否存在对应的质押
        require(userInfo[msg.sender].userStakeList.length > 0, 'no stake');
        require(!black[msg.sender],'black');
        //获取质押时的,一系列质押记录,包括金额、时间戳等等
        uint[] storage list = userInfo[msg.sender].userStakeList;
        uint rew;
        uint outAmount;
        uint range = list.length;
        //计算对应的奖励
        for (uint i = 0; i < range; i++) {
            UserSlot storage info = userSlot[msg.sender][list[i - outAmount]];
            require(info.totalQuota != 0, 'wrong index');
            //不能超过一个最大奖励
            uint quota = (block.timestamp - info.claimTime) * info.rates;
            if (quota >= info.leftQuota) {
                quota = info.leftQuota;
            }
            //关键步骤,计算对应的奖励,仔细看一下getEGDPrice()函数
            //根据EGD的价格,来确定奖励多少EGD
            rew += quota * 1e18 / getEGDPrice();
            //下面是一些计算账户剩下最大奖励,以及账户余额(+利息)等操作
            info.claimTime = block.timestamp;
            info.leftQuota -= quota;
            info.claimedQuota += quota;
            if (info.leftQuota == 0) {
                userInfo[msg.sender].totalAmount -= info.totalQuota;
                delete userSlot[msg.sender][list[i - outAmount]];
                list[i - outAmount] = list[list.length - 1];
                list.pop();
                outAmount ++;
            }
        }
        //更新相应的质押列表
        userInfo[msg.sender].userStakeList = list;
        //发送响应的奖励
        EGD.transfer(msg.sender, rew);
        userInfo[msg.sender].totalClaimed += rew;
        emit Claim(msg.sender,rew);
    }
    function getEGDPrice() public view returns (uint){
    	//可在phalcon上看到行营的记录
        uint balance1 = EGD.balanceOf(pair);
        uint balance2 = U.balanceOf(pair);
        //EGD的价格仅仅是根据两种代币的实时数量(流动性)来进行计算,可以被攻击者操纵
        return (balance2 * 1e18 / balance1);
    }
    function initialize() public initializer {
        __Context_init_unchained();
        __Ownable_init_unchained();
        rate = [200, 180, 160, 140];
        startTime = block.timestamp;
        referRate = [6, 3, 1, 1, 1, 1, 1, 1, 2, 3];
        rateList = [547,493,438,383];
        dailyStakeLimit = 1000000 ether;
        wallet = 0xC8D45fF624F698FA4E745F02518f451ec4549AE8;
        fund = 0x9Ce3Aded1422A8c507DC64Ce1a0C759cf7A4289F;
        EGD = IERC20(0x202b233735bF743FA31abb8f71e641970161bF98);
        U = IERC20(0x55d398326f99059fF775485246999027B3197955);
        router = IPancakeRouter02(0x10ED43C718714eb63d5aA57B78B54704E256024E);
        pair = IPancakeFactory(router.factory()).getPair(address(EGD),address(U));
    }

The price of EGD is calculated based on the number of two tokens on one address. We initialize()函数get the pair address in . The pair address is calculated based on the router address. The router is a proxy contract. On the blockchain browser, we can Seeing that the address of pair is 0xa361433e409adac1f87cdf133127585f8a93c67d, which is a contract that provides liquidity for pancake, it looks familiar.

At this point we must have discovered why the attack was successful?

  • The user first Pancake LPsborrowed a large amount of USDT from 0xa361... through flash loan, which led to Pancake LPsthe pairing of USDT and EGD, and the price of EGD became very cheap.
  • At this time pancakeCall()回调函数, the user is redeeming the reward. The reward is Pancake LPscalculated based on the number of the two tokens in the EDG price. As a result, the price of EDG is very cheap. This is rewthe calculation formula seen, and the user receives an excess reward.

The following is a brief introduction to subsequent calls on phalcon:

First Pancake LPsreturn the flash loan borrowed above; then conduct the corresponding k-value verification (make sure repayment = original amount + handling fee)

Then PancakeSwap: WBNB-BSC-USD 2perform the corresponding approve authorization on the borrowed flash loan.

Call swapExactTokensForTokensSupportingFeeOnTransferTokens function to exchange all the EGD obtained into USDT

PancakeSwap: WBNB-BSC-USD 2The borrowed flash loan is then returned and the corresponding k value is verified.

In the end, the attacker profited 36044 USDT

Guess you like

Origin blog.csdn.net/m0_53689197/article/details/135101484