DeFi清算漏洞

  • Dacian
  • 发布于 2025-01-24 19:17
  • 阅读 22

本文深入探讨了DeFi协议中清算机制的复杂性与潜在漏洞,强调了高效清算对于维护协议偿付能力的重要性。文章详细分析了多种可能导致清算失败或不公平的漏洞类型,例如缺乏清算激励、坏账处理不当、以及各种拒绝服务攻击。此外,还讨论了清算计算中的常见错误,以及在协议暂停或L2 sequencer出现问题时可能导致的不公平清算情况,并为智能合约开发者和审计人员提供了实用的启发式方法和建议,以识别和修复这些漏洞。

提示和高效的清算对于维持 DeFi 协议的偿付能力至关重要,但它也是最难且最复杂的代码,需要以安全且尤其是无需信任的方式实现。存在许多潜在的漏洞和错误,这些漏洞和错误可能对协议的偿付能力和用户信任产生灾难性影响,导致清算实现具有高“bug 密度”。智能合约开发者和审计人员应注意以下漏洞类别,并验证他们的清算代码是否容易受到攻击。

以下是两个常用术语及其定义

  • 可清算的(Liquidatable):collateral_value * loan_to_value_ratio < borrow_value

    - 剩余的抵押品足以覆盖借款

    - 如果及时清算,则不会产生坏账

  • 资不抵债(Insolvent):collateral_value < borrow_value

    - 剩余的抵押品不足以覆盖借款

    - 即使在清算后,也会给协议带来坏账

清算的目标是通过及时清算“可清算”的头寸来避免坏账,使其不会变为“资不抵债”。

没有清算激励

许多去中心化协议不使用受信任的清算人,而是允许任何地址执行“无需信任”的清算。为了激励及时和高效的无需信任的清算人,此类协议通常提供清算激励,通常以清算“奖励”或“报酬”的形式。

受激励的无需信任的清算人(通常以 MEV 机器人的形式)会及时清算任何可清算的头寸,如果清算激励大于执行清算的 gas 成本,从而持续地产生小的无风险利润。

启发式: 如果协议依赖于无需信任的清算人,它是否通过奖励或报酬来激励清算人?

没有激励去清算小额头寸

如果协议不强制执行最低存款和头寸规模,则可能会积累小额头寸,无需信任的清算人没有激励去清算这些头寸。从理论上讲,累积资不抵债的小额头寸对稳定币协议来说尤其有问题,因为它允许坏账累积,导致协议抵押不足。

除了对新头寸强制执行最小头寸规模外,对于允许以下操作的协议:

启发式: 该协议是否强制执行最小头寸规模?这是否在每个可以更改头寸规模的函数中强制执行,或者仅在开设新头寸时强制执行?

更多示例:[ 1, 2, 3, 4, 5]

可盈利的用户提取所有抵押品,从而消除清算激励

在永续合约等交易协议中,用户持有的多头/空头头寸的当前盈亏 (PNL) 在确定其抵押品总价值时会被计算在内。当用户的头寸有很大的正 PNL 时,该用户可能能够提取大部分甚至全部存入的抵押品,同时继续保持偿付能力

如果用户的 PNL 随后下降,该头寸将变得可清算。但是,由于用户已经提取了抵押品,除了剩余的正 PNL 之外,没有什么可以扣押并作为对清算人的激励。缺乏抵押品可能导致清算激励不足(甚至在尝试清算时出现恐慌性回滚),导致该头寸无法清算,随后变得资不抵债。

一个简单的缓解措施是始终确保持有未平仓交易头寸的用户必须存入最少数量的抵押品,无论他们可能拥有多么大的正 PNL。另一种缓解措施可能是“折扣”正 PNL,使其与实际存入的抵押品相比,不提供相同的“抵押权重”。

允许用户无限制地借用存入的抵押品也可能很危险,因为它可能导致相同的状态。

启发式: 可盈利的用户是否可以提取他们存入的抵押品?如果可以,如果市场逆转并且用户变得无利可图,会发生什么?

更多示例:[ 1]

没有处理坏账的机制

如果不能及时清算,则可清算的头寸可能会达到资不抵债的状态,即清算奖励和扣押的抵押品价值低于解决坏账和清算头寸所需的债务代币价值。

在这种情况下,无需信任的清算人没有激励去清算该头寸,从而允许坏账在协议中累积。 协议也可能没有考虑到此状态,从而导致清算交易出现恐慌性回滚,从而无法清算资不抵债的头寸。 可以通过以下方式缓解此问题:

  • 运营受信任的清算人,他们及时清算所有可清算的头寸

  • 设立一个“保险基金”,通常通过协议费用提供资金,该基金可以吸收坏账,从而使无需信任的清算人仍然可以从正常情况下无利可图的头寸中获利

  • 在协议用户(例如流动性提供者)之间分摊坏账

启发式: 该协议是否实施处理坏账的机制?清算资不抵债的头寸会发生什么?

更多示例:[ 1, 2]

部分清算绕过坏账核算

考虑以下清算代码:

// position 被清算关闭时的附加处理
if (!hasPosition) {
    int256 remainingMargin = vault.margin;

    // 将正的保证金记入 vault 接收者的账户
    if (remainingMargin > 0) {
        if (vault.recipient != address(0)) {
            vault.margin = 0;

            sentMarginAmount = uint256(remainingMargin);

            ERC20(pairStatus.quotePool.token).safeTransfer(
                vault.recipient, sentMarginAmount);
        }
    }
    // 否则确保清算人支付坏账
    else if (remainingMargin < 0) {
        vault.margin = 0;

        // vault 无法承担的任何损失
        // 必须由清算人赔偿
        ERC20(pairStatus.quotePool.token).safeTransferFrom(
            msg.sender, address(this), uint256(-remainingMargin));
    }
}

如果清算交易是完全清算,从而关闭了该头寸,则此代码可确保清算人承担与该头寸相关的任何坏账。 但是,由于此检查仅在头寸被完全清算时才会发生,因此部分清算人可以通过不清算整个头寸来绕过此检查。

这允许做市商绕过坏账核算,允许坏账在协议中累积。 一种可能的缓解措施是确保在对资不抵债的头寸进行部分清算时,该头寸的相应数量的坏账由保险基金或坏账社会化机制承担。

启发式: 在对资不抵债的头寸进行部分清算期间是否考虑了坏账?

没有部分清算会阻止巨鲸清算

在无需信任的清算人提供解决借款人坏账所需的债务代币的协议中,应支持部分清算,以允许清算人清算大型可清算头寸的一部分。 如果不支持部分清算,这将无法清算巨鲸开设的大型可清算头寸,因为个体清算人可能没有足够的代币来解决巨额债务。

可以使用闪电贷来清算大型头寸,但前提是贷款规模不超过当前的市场流动性 - 这不能保证始终成立。

启发式: 协议是否支持部分清算? 如果不支持,如何清算巨鲸用户? 是否有其他保障措施,例如对最大头寸规模的上限? 如果有,这些附加保障措施在确保可以清算尽可能大的头寸方面有多有效?

清算拒绝服务

如果攻击者可以永久性地导致清算回滚或阻止自己被清算,这代表了许多协议偿付能力的关键危险,因为它允许坏账在系统中累积。 在对真实协议的审计中发现了几个已知的攻击路径:

攻击者使用许多小头寸来阻止清算

考虑以下清算代码,该代码会循环遍历用户的所有当前头寸:

function _removePosition(uint256 positionId) internal {
    address trader = userPositions[positionId].owner;
    positionIDs[trader].removeItem(positionId);
}

// @audit 由 `_removePosition` 调用
function removeItem(uint256[] storage items, uint256 item) internal {
    uint256 index = getItemIndex(items, item);

    removeItemByIndex(items, index);
}

// @audit 由 `removeItem` 调用
function getItemIndex(uint256[] memory items, uint256 item) internal pure returns (uint256) {
    uint256 index = type(uint256).max;

    // @audit 大型 items.length 的 OOG 回滚
    for (uint256 i = 0; i < items.length; i++) {
        if (items[i] == item) {
            index = i;
            break;
        }
    }

    return index;
}

恶意用户可以利用此 for 循环,通过开设许多小头寸并允许最后一个头寸变得可清算,使自己无法被清算; 每当清算人尝试清算最后那个头寸时,清算将由于 gas 耗尽而回滚。 可以通过以下方式缓解此问题:

  • 强制执行最小头寸规模以防止许多“尘埃”头寸

  • 使用 mapping 或其他数据结构来防止循环遍历每个头寸

启发式: 协议是否循环遍历用户可以向其添加项目的无限制列表? 是否强制执行最小头寸规模?

更多示例:[ 1, 2, 3]

攻击者使用多个头寸来阻止清算

在某些用户可以拥有多个未平仓头寸的协议中,他们的健康评分会在所有头寸中一起考虑,以确定该用户是否需要进行清算。 在这些协议中,当发生清算时,用户的所有未平仓头寸都会在同一交易中被清算。

考虑以下代码:

// 加载正在清算的账户的未平仓市场
ctx.amountOfOpenPositions = tradingAccount.activeMarketsIds.length();

// 循环遍历未平仓市场
for (uint256 j = 0; j < ctx.amountOfOpenPositions; j++) {
    // 将当前活动的市场 ID 加载到工作数据中
    // @audit 假设活动市场的顺序恒定
    ctx.marketId = tradingAccount.activeMarketsIds.at(j).toUint128();

    // snip - 一堆清算处理代码 //

    // 从账户中删除此活动市场
    // @audit 这会调用 `EnumerableSet::remove`,从而更改 `activeMarketIds` 的顺序
    tradingAccount.updateActiveMarkets(ctx.marketId, ctx.oldPositionSizeX18, SD_ZERO);

由于 EnumerableSet 不提任何保证元素的顺序是否被保留,并且它的 remove 函数出于性能原因而使用 交换和弹出 方法,因此当删除活动的市场(如果该活动市场不是该用户的最后一个活动市场)时,用户的活动市场的顺序将被破坏。

恶意用户可以利用这一点,通过开设多个头寸并触发此破坏,从而使他们的账户无法被清算,从而导致任何清算尝试都以 panic: array out-of-bounds access 回滚。

防止此问题的简单方法是通过调用 EnumerableSet::values 循环遍历 activeMarketIds 的内存副本,而不是直接循环遍历存储。

启发式: 可以清算具有多个未平仓头寸的用户吗? 测试套件是否包含此场景的测试?

攻击者使用抢先交易来阻止清算

可清算的用户是否可以更改清算交易期间使用的变量,从而导致此更改导致清算回滚? 如果是这样,用户可以通过抢先交易任何清算交易来进行所需的更改,从而使自己无法被清算,从而迫使该交易随后回滚。 一些示例包括通过以下方式阻止清算:

启发式: 是否存在任何用户控制的变量会导致清算回滚? 如果是这样,可清算的用户是否可以抢先交易清算交易来更改此变量,从而迫使清算回滚? 可清算的用户可以执行哪些操作,他们应该能够执行这些操作吗?

更多示例:[ 1, 2, 3, 4, 5, 6]

攻击者使用待处理的操作来阻止清算

考虑清算期间的此检查:

require(balance - (withdrawalPendingAmount + depositPendingAmount) > 0);

恶意用户可以通过创建等于其余额的待处理提款来滥用此功能,从而迫使所有后续清算尝试都回滚,从而使自己无法被清算。 一个简单的缓解措施是阻止需要进行清算的用户执行许多协议功能,例如存款、提款和交换,尽管这仍然留下了一个边缘情况,即无辜用户有待处理的提款,然后需要进行清算。

攻击者也可能能够在使用协议功能时处于可清算状态,从而从后续的清算中获利 - 协议应仔细评估可清算的用户可以执行哪些操作(如果有)。

启发式: 是否有任何操作需要多次交易才能在多个区块上完成? 如果是这样,当用户在这些操作处于“待处理”状态时被清算会发生什么? 可清算的用户可以执行哪些操作,他们应该能够执行这些操作吗?

更多示例:[ 1, 2]

攻击者使用恶意的 onERC721Received 回调来阻止清算

如果在清算期间将 NFT ERC721 代币“推送”到攻击者控制的地址,则攻击者可以在该地址配置其部署的合约以在 onERC721Received 回调函数中回滚,从而使他们无法被清算。

一个简单的缓解措施是实施一个“拉取”功能,借助该功能,ERC721 代币所有者可以在单独的交易中检索其 NFT。

如果用于清算结算的 ERC20 代币包含转移Hook,则可能发生相同的攻击。

启发式: 如果清算使用“推送”来发送代币,攻击者是否可以利用回调来强制清算回滚?

攻击者使用 Yield Vault 阻止清算期间的抵押品扣押

多抵押品协议可能允许用户将其抵押品存入金库或农场以产生收益,从而实现用户存入抵押品的最高资本效率。 此类协议必须确保在以下情况下正确地核算存入收益金库的抵押品和产生的收益

  • 计算避免清算所需的最低抵押品

  • 在清算期间扣押抵押品和产生的收益

如果实现了第一个但没有实现第二个,则攻击者可以通过以下方式耗尽协议:

  • 根据他们存入的抵押品获取贷款

  • 允许清算贷款

  • 从金库/农场中提取其抵押品和收益

智能合约审计人员应仔细检查所有可以用作抵押品的工具是否在清算期间被核算,并且是否通知已存入或注册抵押品的任何其他合约已清算。

启发式: 是否有任何功能允许用户使用其存入的抵押品执行有趣的操作,例如赚取收益? 如果是这样,清算代码是否知道并集成了其他功能? 用户是否可以将他们的存入的抵押品“隐藏”在清算代码不知道的地方?

更多示例:[ 1]

当坏账大于保险基金时,清算会回滚

对于使用保险基金来支付坏账的协议,如果坏账大于保险基金,则清算将回滚,除非该协议对此边缘情况有特殊的处理。 此类协议可能会进入并无限期地保持一种状态,在这种状态下,直到保险基金累积足够的费用来支付坏账,否则无法清算大型资不抵债的头寸。

启发式: 当清算资不抵债的头寸造成的坏账大于保险基金中的数额时会发生什么?

更多示例:[ 1]

由于固定的清算红利,清算因资金不足而回滚

考虑以下旨在始终以额外扣押的抵押品的形式为清算人提供固定的 10% 清算红利的代码:

uint256 tokenAmountFromDebtCovered = getTokenAmountFromUsd(collateral, debtToCover);
// 清算人总是获得 10% 的红利
uint256 bonusCollateral = (tokenAmountFromDebtCovered * LIQUIDATION_BONUS) / LIQUIDATION_PRECISION;
_redeemCollateral(collateral, tokenAmountFromDebtCovered + bonusCollateral, user, msg.sender);

当用户的抵押率 < 110% 时,固定的清算红利会导致清算回滚,因为没有足够的抵押品来支付红利,即使在 < 110% 时,该协议中的用户被认为抵押不足并且需要进行清算。 一个简单的缓解措施是检查借款人是否有足够的抵押品来提供红利,如果没有,则将红利限制为最大可能金额。

启发式: 如果没有足够的抵押品来支付清算红利会发生什么?

更多示例:[ 1, 2]

非 18 位小数的抵押品清算回滚

多抵押品协议可以支持各种各样的抵押品,其中一些抵押品不使用标准的 ERC20 18 位小数精度。 协议通常采用一种策略,该策略使用:

  • 18 位小数用于所有内部计算和存储

  • 转移代币时使用原生代币小数

  • 用户调用的外部函数输入中使用原生代币小数

当持续使用此策略时,效果很好,但是在有多个开发人员的大型协议中,很容易出现不一致; 审计人员应始终验证当抵押代币或债务代币没有 18 位小数时,清算是否正常工作

启发式: 当代币使用不同的小数精度时,清算是否正常工作?

由于多个 nonReentrancy 修饰符,清算回滚

在较大的协议中,清算代码可能非常复杂,涉及对多个其他合约的可选调用。 智能合约审计人员应仔细验证是否存在由于在同一合约上调用了两个具有 nonReentrant 修饰符的函数而导致清算回滚的执行路径。

启发式: 是否有任何清算执行路径会碰到同一合约中的多个 nonReentrant 修饰符?

清算因零值代币转账而回滚

清算代码通常涉及计算多个代币金额,例如清算人奖励和相关费用,然后进行多次代币转账。 如果在代币转账之前没有零值检查,则这可能会导致清算因在零值转移时回滚的代币而回滚

启发式: 协议在代币转账之前是否进行零值检查? 如果没有,它是否支持在零代币转账时回滚的代币?

更多示例:[ 1]

清算因代币拒绝列表而回滚

一些代币(例如 USDC)实施“拒绝列表”,允许代币管理员冻结用户资金,从而导致所有转移尝试都因拒绝列表上的地址而回滚。 许多清算实现使用“推送”机制,该机制在清算交易期间将代币金额发送到不同的地址。 如果协议支持使用拒绝列表的代币,并且在清算期间发送代币的任何地址都在拒绝列表上,则清算将由于拒绝列表而回滚,从而无法清算该头寸。

由于风险较低,许多协议选择简单地承认此风险,但是一种稍微复杂的缓解措施是允许用户声明代币(“拉取”),而不是将它们发送出去(“推送”)。

启发式: 协议在清算期间是否使用“推送”并支持带有拒绝列表的代币? 如果是这样,当在清算期间将代币发送到被阻止的用户时会发生什么?

更多示例:[ 1, 2, 3]

只有一位借款人时无法清算

考虑以下清算逻辑:

// 获取借款人的数量
uint256 troveCount = troveManager.getTroveOwnersCount();

// 仅在借款人超过 1 个时才处理清算
while (trovesRemaining > 0 && troveCount > 1) {

如果只有一位借款人,则此代码无法清算。 这是一种设计缺陷,因为即使只有一位借款人,如果他们的头寸变得可清算,也应该对其进行清算。

启发式: 如果只有一位借款人,是否可以清算该用户?

更多示例:[ 1]

不正确的清算计算

在清算期间,需要进行许多计算,例如抵押品的价值、坏账的数量、清算人奖励和费用的计算。 细微的错误可能会滑入这些计算中,从而产生灾难性的影响:

清算人奖励的计算不正确

清算通常涉及处理使用不同小数精度的债务和抵押代币。 处理债务和抵押代币之间的精度差异时出现的错误可能导致计算出的清算人奖励:

  • 太小,因此没有进行清算的动力

  • 太``` // @audit assignedCollateral = WETH 使用 18 位小数 uint256 assignedCollateral = state.getDebtPositionAssignedCollateral(debtPosition);

// @audit debtPosition.futureValue = USDC 使用 6 位小数 // debtInCollateralToken = WETH 使用 18 位小数 uint256 debtInCollateralToken = state.debtTokenAmountToCollateralTokenAmount(debtPosition.futureValue);

if (assignedCollateral > debtInCollateralToken) { uint256 liquidatorReward = Math.min( assignedCollateral - debtInCollateralToken, // @audit liquidatorReward 使用 debtPosition.futureValue 计算,使用 // 6 位小数而不是使用 18 位小数的 debtInCollateralToken,即使 // 清算奖励以使用 18 位小数的 WETH 抵押品支付 Math.mulDivUp(debtPosition.futureValue, state.feeConfig.liquidationRewardPercent, PERCENT) // @audit 应该是: // Math.mulDivUp(debtInCollateralToken, ...)

);

此清算函数使用抵押代币(具有 18 位小数的 WETH)支付清算奖励,但它使用债务代币头寸(具有 6 位小数的 USDC)计算支付的清算奖励。因此,[清算人将不会受到激励,因为他们预期的奖励将大大减少](https://solodit.cyfrin.io/issues/h-04-users-wont-liquidate-positions-because-the-logic-used-to-calculate-the-liquidators-profit-is-incorrect-code4rena-size-size-git)。

[清算奖励应线性缩放](https://solodit.cyfrin.io/issues/m-1-liquidation-bonus-scales-exponentially-instead-of-linearly-sherlock-wagmileverage-v2-git),这样,如果总借款金额相同,则使用一个帐户向 3 个贷方借款的清算奖励应与使用 3 个单独帐户向 3 个贷方借款的清算奖励大致相同。

清算奖励的计算不正确可能是由于奖励计算中的许多不同错误造成的;没有明确的启发式方法,因此审计员需要仔细检查特定实现中的各种错误。更多示例:\[ [1](https://solodit.cyfrin.io/issues/h-09-kerosene-collateral-is-not-being-moved-on-liquidation-exposing-liquidators-to-loss-code4rena-dyad-dyad-git), [2](https://solodit.cyfrin.io/issues/m-01-liquidation-bonus-logic-is-wrong-code4rena-dyad-dyad-git), [3](https://solodit.cyfrin.io/issues/m-04-liquidated-nfts-can-earn-more-than-liquidation-incentive-zachobront-none-fungify-markdown), [4](https://solodit.cyfrin.io/issues/h-21-incorrect-liquidation-reward-computation-causes-excess-liquidator-rewards-to-be-given-code4rena-tapioca-dao-tapioca-dao-git), [5](https://solodit.cyfrin.io/issues/m-01-liquidation-bonus-logic-is-wrong-code4rena-dyad-dyad-git), [6](https://solodit.cyfrin.io/issues/liquidation-seizeassets-computation-rounding-issue-cantina-none-morpho-pdf), [7](https://solodit.cyfrin.io/issues/calculating-liquidationreward-without-considering-collateral-decimals-cantina-none-cryptex-pdf), [8](https://solodit.cyfrin.io/issues/h-2-users-can-seize-more-assets-during-liquidation-by-using-typeuintmax-sherlock-sentiment-v2-git), [9](https://solodit.cyfrin.io/issues/m-5-inconsistent-in-the-liquidation-fee-leads-to-unfairness-in-liquidation-process-sherlock-symmio-v084-update-contest-git)\]

### 未能优先考虑清算奖励

在清算期间,可能需要向不同的实体支付一些相关的费用。如果没有足够的抵押品(或者在坏账的情况下没有保险基金)来支付所有费用,则[协议应优先支付清算奖励](https://www.youtube.com/watch?v=AD2IF8ovE-w&t=2313s),以激励及时清算。

**启发式:** 如果没有足够的抵押品来支付清算中的所有费用,会发生什么?清算人奖励是否优先考虑 - 尤其是在依赖于无信任清算人的协议中?

### 协议清算费用计算不正确

一些协议收取“协议费”,清算人或被清算的用户在清算期间支付该费用。如果此费用的计算不正确,导致其大于应有的费用,则可能导致许多清算无利可图,从而消除了清算的动机,导致协议中累积坏账。考虑以下协议清算费用计算代码:

```solidity
function _transferAssetsToLiquidator(address position, AssetData[] calldata assetData) internal {
    // 将头寸资产转移给清算人并累积协议清算费用
    uint256 assetDataLength = assetData.length;
    for (uint256 i; i &lt; assetDataLength; ++i) {
        // 确保 assetData[i] 在头寸资产列表中
        if (Position(payable(position)).hasAsset(assetData[i].asset) == false) {
            revert PositionManager_SeizeInvalidAsset(position, assetData[i].asset);
        }
        // 计算费用金额
        // [ROUND] 清算费用向下取整,有利于清算人
        // @audit 清算人费用从没收的抵押品金额计算
        //        导致许多清算无利可图
        uint256 fee = liquidationFee.mulDiv(assetData[i].amt, 1e18);

        // 将费用金额转移给协议
        Position(payable(position)).transfer(owner(), assetData[i].asset, fee);
        // 将差额转移给清算人
        Position(payable(position)).transfer(msg.sender, assetData[i].asset, assetData[i].amt - fee);
    }
}

在这里,协议清算费用是根据没收的抵押品总额的百分比计算的,这使得许多清算无利可图;在这种情况下,协议清算费用占没收抵押品的 30%,大大降低了清算许多可清算头寸的动机。可能的缓解措施包括:

  • 没有协议清算费用或只有少量固定费用

  • 将协议清算费用计算为清算人利润的百分比,而不是原始没收的抵押品金额

协议清算费用的计算也可能以另一种方式不正确地实施,即清算人支付的费用少于完成可清算头寸清算所需的费用

启发式: 协议清算费用是否计算为清算人利润的百分比?如果不是,协议费用是否会使清算无利可图,从而降低清算人的积极性?

更多示例:[ 1, 2, 3, 4, 5]

最低抵押品要求中未计算清算费用

在开立新头寸或计算头寸是否具有偿付能力时,最低抵押品要求计算应包括清算费用(如果该头寸随后将被清算)。

如果最低抵押品要求计算中未包括清算费用,那么当头寸被清算时,可能不存在足够的抵押品,并且协议可能会在清算时恢复或以任何清算费用的形式产生坏账。然而,这是有争议的,一些协议可能选择不实施此功能,因为这被认为对用户不公平

启发式: 最低抵押品要求中是否包括清算费用?如果不是,协议是否已明确记录了原因?

未将已赚取的收益添加到抵押品价值中导致不公平清算

一些协议支持存款抵押品赚取收益的机制,以最大限度地提高资本效率。在评估用户的抵押品时,如果已赚取的收益未计入总抵押品价值,则用户可能会受到不公平的清算。

启发式: 已赚取的收益是否计入用户的总抵押品价值?如果不是,用户是否会受到不公平的清算?如果是,已赚取的收益会发生什么 - 会丢失吗?

未将正 PNL 添加到抵押品价值中导致不公平清算

交易协议中的清算机制在计算交易者抵押品的总价值时,应考虑未平仓杠杆头寸的当前盈利能力:

  • 如果头寸具有负 PNL,则应从抵押品价值中扣除负 PNL,从而导致清算更快发生 - 这应始终在所有协议中实施

  • 如果头寸具有正 PNL,则应将正 PNL 添加到抵押品价值中,从而延迟清算 - 尽管某些协议可能有充分的理由不这样做,但应为用户明确记录这一点

在清算期间不考虑未平仓 PNL 的杠杆交易协议可以不公平地清算具有大量正 PNL 的交易者

启发式: 在确定用户是否可以被清算时,是否考虑了未平仓用户 PNL?如果不是,用户是否会受到不公平的清算?如果是,未平仓 PNL 会发生什么 - 会丢失吗?

L2 排序器宽限期后不公平清算

Chainlink 的官方文档建议在 L2 排序器恢复在线后实施宽限期,并且仅在该宽限期到期后才获取价格数据。如果在该宽限期内阻止了诸如存入额外抵押品之类的交易,那么一旦宽限期到期并且开始获取新的价格数据,用户可能会立即受到不公平的清算。

协议应考虑是否允许在宽限期内存入额外抵押品,以便允许用户在 L2 排序器恢复在线后但在宽限期到期并且新的价格数据可用之前保护其未平仓头寸。

启发式: 一旦 L2 排序器恢复在线,用户是否可以在价格数据恢复之前的宽限期内存入额外抵押品?协议是否实施宽限期以允许这样做,还是在 L2 排序器恢复在线后立即清算用户?

暂停期间借款利息累积导致不公平清算

如果协议支持暂停并且用户在协议暂停期间无法偿还贷款,那么借款利息不应在暂停期间累积,否则由于暂停期间的利息累积,用户可以在协议取消暂停时立即受到不公平的清算。

启发式: 在协议暂停并且用户无法偿还时,借款利息是否累积?

启用清算时暂停还款导致不公平清算

理想情况下,协议不应进入还款暂停但清算已启用的状态,因为这将导致借款人受到不公平的清算,他们想要还款但受到协议管理员的阻止。理想情况下,在取消暂停清算后也应该有一个宽限期,以允许在协议暂停期间变得可清算的用户进行还款和抵押品存款。

启发式: 协议是否可以进入用户无法还款但需要进行清算的状态?取消暂停后是否有宽限期,或者在暂停期间变得可清算的用户是否立即被清算?

更多示例:[ 1, 2, 3, 4, 5, 6]

由于 isLiquidatable 未刷新利息/资金费用导致延迟清算

每当协议检查用户是否可清算时,它必须始终首先刷新任何费用,例如贷款的总利息或杠杆交易头寸的总资金费用,然后再确定用户是否可清算。

智能合约审计员应特别注意 view 函数,这些函数不会改变状态,但需要在确定账户是否可清算之前计算最新的应付费用。当发生清算时,在清算用户之前也需要更新所有这些费用。

启发式: 协议是否始终在确定用户是否可清算之前刷新所有利息、收益、资金费用、PNL 等?

账户清算时正 PNL、收益和奖励丢失

在杠杆交易协议中,可能会出现以下有趣的边缘情况:

  • 交易者存入抵押品 $C 并使用它来开立资产 $A 的多头杠杆交易头寸

  • $A 的市场价值增加,使得交易者具有显着的正未实现利润

  • $C 的市场价值甚至进一步降低,因此即使交易者在其交易中获利,其整体头寸也是可清算的

  • 或者,累积的借款/资金费用大于头寸的利润,并且整体头寸变得可清算

当发生这种边缘情况时,交易者的正 PNL 应在清算期间记入账户,否则它将丢失。对于借款人/交易者可以赚取的收益和其他奖励也是如此;所有奖励应在清算前累积

启发式: 在清算之前是否实现了所有未平仓利润(例如收益、奖励和正 PNL)并将其纳入清算计算中?如果不是,则清算后是否丢失?

更多示例:[ 1, 2, 3]

清算时不收取交换费用

当从一种资产内部交换到另一种资产时,协议可能会实施交换费用。如果实施了内部交换费用,那么当清算人提供债务代币以接收作为清算一部分没收的抵押代币时,也可能需要收取这些费用。未能收取清算期间的交换费用导致协议和潜在的保险基金累积的代币少于应有的代币。

启发式: 协议是否通常收取交换费用并在清算期间执行交换?如果是,它是否在清算期间收取交换费用?如果不是,它是否明确记录了这种差异的原因?

使用预言机更新三明治进行有利可图的自我清算

攻击者可以通过使用攻击合约来利用用户触发的预言机更新进行有利可图的自我清算

  • 闪电贷大量抵押代币

  • 存入抵押品并借入最大金额的债务代币(最大杠杆)

  • 触发预言机价格更新

  • 清算自己

当预言机价格更新导致收回整个抵押余额,同时偿还的债务代币少于借入的债务代币时,攻击是有利可图的。有助于降低此攻击的盈利能力的简单缓解措施是:

  • 收取借款和清算费用

  • 实施一个冷却期,在此期间不能清算账户

更高级的缓解措施包括限制波动性抵押资产的杠杆以及选择具有更小价格偏差更新(无法由用户触发)的预言机。

自我清算可能是一种危险的攻击媒介,尤其是在用户可以让自已变得可清算时

启发式: 用户是否可以让自己变得可清算并进行自我清算?用户是否可以利用预言机价格更新从协议中提取价值?

清算使借款人的健康评分降低

清算(无论是完全清算还是部分清算)应始终使被清算的借款人处于更“健康”的状态,在清算后,他们将来不太可能被清算。但是,在支持多种抵押类型和部分清算的高级协议中,可能存在细微的错误,这些错误会使借款人在清算后处于不健康的状态,从而使他们将来更有可能被清算。

考虑以下清算函数:

function liquidatePartiallyFromTokens(
    uint256 _nftId,
    uint256 _nftIdLiquidator,
    address _paybackToken,
    address _receiveToken, //@audit 清算人可以选择抵押品
    uint256 _shareAmountToPay
)

此清算函数允许清算人选择没收哪些抵押品并获得补偿以进行清算。为什么这很危险?因为不同的抵押品具有不同的:

  • 借款系数,使用户能够针对特定抵押品借入更多或更少

  • 风险概况,因为一些抵押品可能非常稳定(USDC),而其他抵押品可能更具波动性(ETH,尤其是投机性 ERC20 代币)

清算人可以通过选择首先清算用户更稳定,借款系数更高的抵押品来滥用此功能。清算后,这使用户的抵押品篮子不太健康,因为:

  • 他们的剩余抵押品在价格变动方面更具波动性

  • 他们的借款系数降低,因为他们剩下风险更高,波动性更高的抵押品,这些抵押品的借款系数降低

这两个结果都使用户更有可能在未来被清算,并且可能使用户受到级联清算,其中第一个清算交易使他们立即受到第二次清算的影响,依此类推,因为清算使交易者拥有不健康和风险更高的抵押品篮子

一种潜在的缓解措施是在清算交易期间:

两种简单的健康评分实现是:

  • collateral_value / borrow_value(抵押品:债务比率)

  • collateral_value * loan_to_value_ratio / borrow_value(借款能力)

启发式: 清算会否使借款人处于不健康的状态?清算人是否可以选择没收哪些抵押品,从而使借款人拥有波动性更高的抵押品篮子并降低借款系数?

更多示例:[ 1, 2, 3, 4]

抵押品优先级顺序损坏

在支持多种抵押品的协议中,先前漏洞的另一种缓解措施是强制执行抵押品清算优先级顺序,其中首先清算风险更高,波动性更大的抵押品。但是,在实施更改抵押品优先级顺序的函数(以防止损坏抵押品优先级顺序,从而导致抵押品清算顺序不正确)时必须小心。

启发式: 抵押品清算优先级顺序是否会被更改它的函数损坏?

借款人替换导致不正确的还款归属

一些协议支持可选的“替换”清算技术,其中可清算的头寸可用于“填充”来自订单簿的订单,从而有效地用健康的借款人替换不健康的借款人。

其他协议允许用户“购买”可清算头寸,只要他们有足够的抵押品来使该头寸具有偿付能力,就可以有效地接管该头寸。这实现了相同的有效最终状态,即将债务头寸从原始的不健康借款人转移到不同的健康借款人。

在这两种情况下,借款人的地址都更改为新的借款人,但大多数其他字段,包括头寸的“id”和债务,保持不变。考虑以下两个并发启动的交易会发生什么:

  • TX1 - 可清算的原始借款人尝试偿还其可清算头寸,并传入其头寸的“id”作为输入

  • TX2 - 替换清算交易尝试将可清算的头寸转移给更健康的借款人

如果在 TX1 之前执行 TX2,则可清算的头寸在执行原始借款人的还款交易之前由新的借款人获得 - 原始借款人实际上偿还了其他人的债务!在用户可以“购买”可清算头寸的协议中,MEV 攻击者可以利用这一点来抢先执行还款交易,购买可清算的头寸,然后让原始借款人偿还他们刚刚获得的头寸上的债务!

一种潜在的缓解措施是让还款交易将借款人的地址指定为输入的一部分,如果当前借款人的地址不匹配,则恢复。

启发式: 可清算的头寸是否可以从不健康的用户转移到健康的用户?如果是这样,如果不健康的用户尝试与转移同时还款,会发生什么?

更多示例:[ 1]

借贷和清算贷款价值比之间没有差距

大多数协议需要更高的贷款价值比 (LTV) 才能开立新的借款,但使用较低的 LTV 比率来确定头寸是否可清算;此设计旨在防止借款人在开立新的借款后不久就被清算。

如果借贷和清算 LTV 比率之间没有差距,那么借款人可以在清算边缘开立新头寸,这会增加后续清算的可能性,威胁到协议的稳定性并产生不良的用户体验。

启发式: 用户是否会在开立新头寸后不久就被清算?修改现有头寸后呢?

更多示例:[ 1, 2, 3, 4]

清算拍卖运行期间借款人累积利息

一些协议使用“拍卖”机制,其中可清算的头寸在一段时间内进行拍卖。在这种情况下,一旦将债务提交拍卖,就应暂停债务的利息支付;被清算的头寸不应在清算拍卖过程中继续累积额外利息

启发式: 借款人在债务被拍卖时是否继续累积利息?

清算和交换没有滑点

理想情况下,在执行清算时,清算人应该能够指定他们愿意收到的奖励(以代币、股份或其他工具的形式)的最低金额。这对于在清算期间执行交换的协议尤其重要,其中这些交换可以通过 MEV 加以利用,导致清算人收到的奖励少于他们预期收到的奖励

启发式: 清算人是否可以指定滑点参数?如果在清算期间发生交换,这是否会导致清算人(或协议或被清算的用户)收到的代币少于预期?

补充资源

21

1



>- 原文链接: [dacian.me/defi-liquidati...](https://dacian.me/defi-liquidation-vulnerabilities)
>- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Dacian
Dacian
in your storage