真实攻击案例分析系列之Fantasm Finance攻击事件分析

  • 小驹
  • 更新于 2022-06-16 00:07
  • 阅读 1747

一句话对这个漏洞进行总结:合约中的漏洞是个典型的逻辑漏洞,漏洞主要在Pool合约中的mint方法中,在mint方法中调用calcMint方法计算了铸造xFTM时需要的最少FTM和最少FSM,而合约代码只对FSM进行了销毁,却没有考虑FTM的情况,导致即使用户不输入FTM,也能获得xFTM。

1. 事件简介

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

今天我们从技术层面分析Fantasm Finance被攻击的全过程。在分析攻击过程之前,对Fantasm Finance项目需要有下面的前置背景知识。

2. 背景知识

2.1 区分四种币:FTM,FSM,xFTM,WFTM

FTM :是Fantom公链的内置货币

FSM和xFTM :Fantasm Finance是一个Defi金融项目,FSM和xFTM都是Fantasm Finance发行的代币。在 Fantasm Finance,引入了一种去中心化的解决方案,通过部分抵押设计来扩大 FTM 代币的数量,其中 xFTM 的合成代币供应将部分由 FTM 支持,部分由 FSM 代币支持。XFTM 是一种分数算法合成代币,在 Fantom公链上与 1 FTM 的价值挂钩

WFTM :黑客利用的都是FTM,FSM合成xFTM时的漏洞,整体都是围绕这三个展开,但黑客为了获得FSM时,使用了WFTM,WFTM是FTM的ERC20格式,关于WFTM黑客将50个FTM换成50个WFTM后,又通过uniswap,将50个WFTM换成FSM,将FSM应用到漏洞中。WFTM只起到一个转换代币的作用,在漏洞利用中没有关键作用

项目官网: https://docs.fantasticprotocol.io/synthetic-tokens Untitled.png

根据介绍可得知,Fantasm Finance是做合成代币的,xFTM就是这个项目的合成代币,由FTM和FSM这两个币支持,xFTM价格与FTM挂钩。

2.2 如何保证xFTM与FTM的挂钩。

前面说了xFTM基本上与1FTM进行挂钩,那么是如何进行挂钩的呢?

在官方文档中,定义了CR,Collateral Ratio 质押比率。关于CR有下面几点需要理解:

参考https://docs.fantasticprotocol.io/mechanisms/collateral-ratio官方文档

  • Fantastic Protocol 使用抵押比率 (CR) 进行铸造和赎回过程。抵押比率(CR) 将有由治理组织设定。
  • CR在铸造和赎回过程中使用,它是一个分数,表示在铸造或者赎回FTM时,xFTM占用的百分比。
  • CR每小时以0.2%的幅度进行涨跌。如果60分钟的时间加权平均价格(TWAP)超过1.005倍的FTM,CR上调;如果60分钟的时间加权平均价格(TWAP)低过0.995倍的FTM,CR下调 初始的CR值为90%.

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

2.3 xFTM的铸造公式

那么铸造xFTM的公式是怎样的呢? Untitled1.png

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

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

3. 攻击过程分析

根据攻击者的地址:

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

攻击过程通过攻击合约进行, 攻击者创建的攻击合约地址为:0x944b58c9b3b49487005cead0ac5d71c857749e3e

根据攻击发生的时间,找到攻击时的交易记录如下: Untitled2.png

这4条交易记录与攻击者的攻击流程一致,因此将攻击流程划分为下面的4步:

  1. 创建攻击合约
  2. 调用攻击合约的getWFTM函数
  3. 调用攻击合约的0x671daed9函数
  4. 调用攻击合约的Collect函数

下面我们会按照攻击者的攻击流程进行攻击过程推演。**在这4步中,第1步,第2步,第3步的大部分都是做攻击准备,主要漏洞利用存在于3中的mint方法调用和4中的collect函数调用中。**

3.1 创建攻击合约。

创建的攻击合约地址为:0x944b58c9b3b49487005cead0ac5d71c857749e3e

Untitled3.png

3.2 调用攻击合约中的getWFTM函数

这一步做了啥? 攻击合约调用WFTM合约的deposit方法,将50个原生的FTM代币,换成了50个WFTM。(从WFTM的代码合约中deposit函数,可以看到WFTM与FTM的兑换是1:1进行的)

交易详情可以看到 Untitled4.png

从攻击合约的反编译的代码中也可以看出来,getWFTM函数只是调用了stor1(槽一中的变量,也就是wrappedFtm合约地址)的deposit方法,根据上图中的FTM转移过程,猜测合约的函数只是调用了WFTM合约的deposit方法,所以stor1变量应该是WFTM的合约地址。

def unknown527beca5() payable: 
  require ext_code.size(stor1)
  call stor1.deposit() with:
     value call.value wei
       gas gas_remaining wei
  if not ext_call.success:
      revert with ext_call.return_data[0 len return_data.size]
  require return_data.size >=′ 32
  require ext_call.return_data == ext_call.return_data[0]

WFTM代币的合约可以参考: https://ftmscan.com/address/0x21be370d5312f44cb42ce377bc9b8a0cef1a4c83#code

3.3 调用攻击合约的0x671daed9函数

调用该函数的交易hash为:0xa84d216a1915e154d868e66080c00a665b12dab1dae2862289f5236b70ec2ad9

该交易中涉及到的tokens的转移如下

  1. 攻击合约(0x94)将50 WFTM通过uniswapV2Pair合约换出5.721个FSM,如下图中的1所标识的。
  2. 攻击都将这5.721个FSM代币,发送给了Pool合约。如下图中2所标识的。
  3. Pool合约将收到的5.721个FSM代币进行了销毁。如下图中的3所标识的。
  4. 最后的一个是Pool合约将7.73个FSM转给了FSM代币的税务员(这个是代币兑换收取的费用)。与漏洞无关。 Untitled5.png

下面分析下0x671daed9函数的具体的调用过程,如下所示。

Untitled6.png

包括下面几个步骤,这些步骤中除了最后一步mint操作,其他的都是常规的正常操作。mint操作中存在逻辑漏洞,黑客在调用mint方法后,利用程序的逻辑错误,burn 了5.721个FSM,给UserInfo添加了2618个xfmt。

该函数中操作的步骤如下(在mint之前的操作都是攻击准备工作,从mint函数开始才是真正的漏洞利用过程):

  • approve:调用的是WFTM合约的approve方法。目的是将WFTM代币中,攻击合约(0x94)授权Router合约可处理的代币数量为0xFFFFFFFFFFFFFFFF
  • balanceOf:调用的是WFTM合约的balanceOf方法,查了攻击合约的WFTM的余额,也就是50 WFTM。
  • getAmountsOut:调用 UniswapV2Router02 的getAmountsOut方法,计算50个WFTM可以换出5.72个FSM代币。
  • swapExactTokensForTokens:调用UniswapV2Router02的swapExactTokensForTokens方法,完成兑换5.72个FSM代币的过程
  • approve:调用Pool合约的approve访求,在fsm token合约中,攻击合约(0x94)授权Pool合约的地址的取款权限为0xFFFFFFFFFFFFFFFF
  • mint:调用Pool合约的mint方法,该方法中存在逻辑漏洞,方法中计算出5.72个FSM可以mint出2618个xFTM,只接就burn掉5.72个FSM,却没有验证用户输入的FTM是否正确(正确的做法应该是计算出所需要的fsm和 ftm后,将fsm销毁,将ftm收回到Pool合约)。这就导致用户可以用很少的FSM就兑换出大量的xFTM(由背景知识中,可知大约1个xFTM=CR*FTM+(1-CR)FSM,CR通常为90%,逻辑代码就忽略了将占总交易价值90%左右的FTM收回到Pool合约)。
  1. approve 使用的参数:

    "from":{
    "address":"0x944b58c9b3b49487005cead0ac5d71c857749e3e" // 攻击合约地址
    "balance":"0"
    }
    "to":{
    "address":"0x21be370d5312f44cb42ce377bc9b8a0cef1a4c83" //WFTM 合约地址
    "balance":"658429842400380886695645688"
    }
    "value":"0"
    "input":{
    "spender":"0xf491e7b69e4244ad4002bc14e878a34207e38c29" // UniswapV2Router02
    "amount":"115792089237316195423570985008687907853269984665640564039457584007913129639935"
    }
    "output":{
    "0":true
    }
    
    底层调用的_approve方法
    "input":{
    "owner":"0x944b58c9b3b49487005cead0ac5d71c857749e3e" // 攻击合约地址
    "spender":"0xf491e7b69e4244ad4002bc14e878a34207e38c29" // UniswapV2Router02
    "amount":"115792089237316195423570985008687907853269984665640564039457584007913129639935"
    }

    WFTM代币中,攻击合约(0x94)授权Router合约可处理的代币数量为0xFFFFFFFFFFFFFFFF(也就是10进制的115792089237316195423570985008687907853269984665640564039457584007913129639935)

  2. balanceOf 调用的是WFTM合约的balanceOf方法。查看了下攻击合约的balanceOf,现在攻击合约有50个WFTM

    "from":{
    "address":"0x944b58c9b3b49487005cead0ac5d71c857749e3e"  //发起者是攻击合约地址
    "balance":"0"
    }
    "to":{
    "address":"0x21be370d5312f44cb42ce377bc9b8a0cef1a4c83" //接收者是WFTM 合约地址
    "balance":"658429842400380886695645688"
    }
    "input":{
    "account":"0x944b58c9b3b49487005cead0ac5d71c857749e3e"
    }
    "output":{
    "0":"50000000000000000000" // 查询的结果是攻击合约在WFTM中的余额为50WFTM
    }
  3. getAmountsOut 调用了Router合约的getAmountsOut方法,计算出输入50个WFTM可以兑换出5.72个fsm。(返回的值为50和5.7)

    "from":{
    "address":"0x944b58c9b3b49487005cead0ac5d71c857749e3e" //发起者是攻击合约地址
    "balance":"0"
    }
    "to":{
    "address":"0xf491e7b69e4244ad4002bc14e878a34207e38c29" // UniswapV2Router02
    "balance":"0"
    }
    "[INPUT]":"0xd06ca61f000000000000000000000000000000000000000000000002b5e3af16b18800000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000200000000000000000000000021be370d5312f44cb42ce377bc9b8a0cef1a4c83000000000000000000000000aa621d2002b5a6275ef62d7a065a865167914801" // getAmountsOut方法
    "output":{
    "amounts":[
    0:"50000000000000000000"
    1:"5720527256067865356"
    ]
  4. swapExactTokensForTokens 调用Router合约的swapExactTokensForTokens方法,兑换5.72个fsm.

    "from":{
    "address":"0x944b58c9b3b49487005cead0ac5d71c857749e3e" //发起者是攻击合约地址
    "balance":"0"
    }
    "to":{
    "address":"0xf491e7b69e4244ad4002bc14e878a34207e38c29" // UniswapV2Router02
    "balance":"0"
    }
    "value":"0"
    "input":{
    "amountIn":"50000000000000000000"
    "amountOutMin":"5663321983507186702"
    "path":[
    0:"0x21be370d5312f44cb42ce377bc9b8a0cef1a4c83"
    1:"0xaa621d2002b5a6275ef62d7a065a865167914801"
    ]
    "to":"0x944b58c9b3b49487005cead0ac5d71c857749e3e"
    "deadline":"1646833795"
    }
    "output":{
    "amounts":[
    0:"50000000000000000000"
    1:"5720527256067865356" 
    ]
    }
  5. approve 在fsm token合约中,攻击合约(0x94)授权Pool合约的地址的取款权限为0xFFFFFFFFFFFFFFFF

    "from":{
    "address":"0x944b58c9b3b49487005cead0ac5d71c857749e3e" //发起者是攻击合约地址
    "balance":"0"
    }
    "to":{
    "address":"0xaa621d2002b5a6275ef62d7a065a865167914801"  // FSM Token合约地址
    "balance":"0"
    }
    "value":"0"
    "input":{
    "spender":"0x880672ab1d46d987e5d663fc7476cd8df3c9f937" // Pool合约地址
    "amount":"115792089237316195423570985008687907853269984665640564039457584007913129639935"
    }
    "output":{
    "0":true
    }
  6. mint

我们先比较下正常调用mint函数攻击时调用mint函数参数的区别 ,首先我们可以随意找一个正常调用的交易,比如 0xfc618528f6c0d6ff84702358ee0768c552e43ddf13c8c124461c13cf9a94ce11 这个交易。

  • 正常调用时的函数:

正常调用时,uint256 _ftmIn = msg.value; 来自于 msg.value的_ftmln都是有fantom的原生币FTM的转入的,在这个交易中是20个FTM。在随后的操作中WethUtils.wrap(_ftmIn);中,将20个原生的FTM充值到Pool合约中。

  • 攻击时的函数:

uint256 _ftmIn = msg.value; 来自于 msg.value的_ftmln没有原生币FTM转入。在随后的操作中WethUtils.wrap(_ftmIn);中,将0个原生的FTM充值到Pool合约中。

在攻击时,调用Pool合约的mint方法,mint方法的功能是计算能铸造的xFTM的数量,并保存到userInfo[_minter].xftmBalance中(第4步的Collect函数会从userInfo[_minter].xftmBalance中读取能铸造的xFT数量并铸造)。mint方法输入的参数为_fantasmIn为5.72个fsm代币,和_minXftmOut为0。

"from":{
    "address":"0x944b58c9b3b49487005cead0ac5d71c857749e3e" //发起者是攻击合约地址
    "balance":"0"
}
"to":{
    "address":"0x880672ab1d46d987e5d663fc7476cd8df3c9f937" // Pool合约地址
    "balance":"0"
}
"value":"0"
"input":{
    "_fantasmIn":"5720527256067865356"  // 输入值为fsm:5.72个,
    "_minXftmOut":"0"
}
"[OUTPUT]":"0x"

min函数中调用了calcMint函数,calcMint功能为根据输入的ftm和fsm计算出,可以铸造出来的xFTM的数量(_xftmOut),需要的最少的ftm数量(_minFtmIn),需要的最少的fsm的数量(_minFantasmIn),税费(_fee),通过函数返回可以看到5.72个fsm可以兑换2618个xftm。

calcMint函数的定义及调用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的数量,手续费
"input":{
    "_ftmIn":"0" // 输入的ftm为0
    "_fantasmIn":"5720527256067865356" // 输入的fsm为5.72个
}
"output":{
    "_xftmOut":"2618992620259886970084" // 可以铸造的xFTM为2618个
    "_minFtmIn":"2576962648420209746893" // 需要的最少的FTM为2576个
    "_minFantasmIn":"5720527256067865356" // 需要最少的fsm为5.72个
    "_fee":"7730887945260629240" // 需要收取的手续费为7.73个WFTM.
}

重点来了,到现在为至,一切正常,clacMint也给出了最少需要的FTM是2576个,但是遗憾的是,后面并没有验证用户是否真正的传入了2576个FTM,导致在只输入FSM的情况下,并不需要补充FTM,也就是如果FSM的占比为10%,那么就能用价值1u的FSM铸造价值10u的xFTM。。

如下面代码所示,这个漏洞可以说非常遗憾,明明calcMint函数都算出来需要的最少的FTM了,却愣是没有校验…………

Untitled7.png

3.4 调用collect函数

该函数位于Pool合约中,主要功能是根据userInfo结构体来铸造代币,如果userInfo[_sender].xftmBalance存在的话,就会铸造xFTM

调用collect函数。交易hash为: 0xa84d216a1915e154d868e66080c00a665b12dab1dae2862289f5236b70ec2ad9

通过函数的输入和输出,可以看出来,攻击者合约调用了Pool合约的collect方法。

"from":{
    "address":"0x944b58c9b3b49487005cead0ac5d71c857749e3e" //攻击者合约地址
    "balance":"0"
}
"to":{
    "address":"0x880672ab1d46d987e5d663fc7476cd8df3c9f937" //Pool合约地址
    "balance":"0"
}
"value":"0"
"[INPUT]":"0xe5225381" //collect() 方法
"[OUTPUT]":"0x"

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

调用关系为:攻击合约→Pool合约的Collect()→调用XFTM的_mint方法,从而完成了xFTM的铸造。 Untitled8.png

从上面的截图中,可以看到token的转移情况,直接从0x0地址给0x94攻击者转移了2618个xFTM代币

从Pool合约的源代码中,也可以看到调用的过程

Untitled9.png

从下面的函数代码可以看出来,能mint多少xFTM,是由_fantasmAmount决定的,而_fantasmAmount参数直接来源于userInfo[_sender].xftmBalance

/**
     * @notice collect all minting and redemption
     */
    function collect() external nonReentrant {
        address _sender = msg.sender;
        require(userInfo[_sender].lastAction < block.number, "Pool::collect: <minimum_delay");

        bool _sendXftm = false;
        bool _sendFantasm = false;
        bool _sendFtm = false;
        uint256 _xftmAmount;
        uint256 _fantasmAmount;  //这里是参数定义。
        uint256 _ftmAmount;

        // Use Checks-Effects-Interactions pattern
        if (userInfo[_sender].xftmBalance > 0) {
            _xftmAmount = userInfo[_sender].xftmBalance;
            userInfo[_sender].xftmBalance = 0;
            unclaimedXftm = unclaimedXftm - _xftmAmount;
            _sendXftm = true;
        }

        if (userInfo[_sender].fantasmBalance > 0) {
            _fantasmAmount = userInfo[_sender].fantasmBalance;   //取出调用者的FST的余额
            userInfo[_sender].fantasmBalance = 0;
            unclaimedFantasm = unclaimedFantasm - _fantasmAmount;
            _sendFantasm = true;
        }

        if (userInfo[_sender].ftmBalance > 0) {
            _ftmAmount = userInfo[_sender].ftmBalance;
            userInfo[_sender].ftmBalance = 0;
            unclaimedFtm = unclaimedFtm - _ftmAmount;
            _sendFtm = true;
        }

        if (_sendXftm) {
            xftm.mint(_sender, _xftmAmount);
        }

        if (_sendFantasm) {
            fantasm.mint(_sender, _fantasmAmount);   // 这里是第384行,调用铸造方法._fantasmAmount参数对应着要铸造的数量,这里_fantasmAmount应该为2618
        }

4.总结

一句话对这个漏洞进行总结:合约中的漏洞是个典型的逻辑漏洞,漏洞主要在Pool合约中的mint方法中,在mint方法中调用calcMint方法计算了铸造xFTM时需要的最少FTM和最少FSM,而合约代码只对FSM进行了销毁,却没有考虑FTM的情况,导致即使用户不输入FTM,也能获得xFTM。

5.参考

https://dashboard.tenderly.co https://www.tofreedom.me/fantasm-finance

点赞 0
收藏 2
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

1 条评论

请先 登录 后评论
小驹
小驹
区块链安全分析,欢迎私信沟通交流