前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >真实攻击案例分析系列之Fantasm Finance攻击事件分析

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

作者头像
Tiny熊
发布2022-11-07 10:03:20
4500
发布2022-11-07 10:03:20
举报

本文作者:小驹[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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-06-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 深入浅出区块链技术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 事件简介
  • 2. 背景知识
    • 2.1 区分四种币:FTM,FSM,xFTM,WFTM
      • 2.2 如何保证 xFTM 与 FTM 的挂钩。
        • 2.3 xFTM 的铸造公式
        • 3. 攻击过程分析
          • 3.1 创建攻击合约。
            • 3.2 调用攻击合约中的 getWFTM 函数
              • 3.3 调用攻击合约的 0x671daed9 函数
                • 3.4 调用 collect 函数
                • 4.总结
                • 5.参考
                  • 参考资料
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档