去中心化金融(DeFi)清算风险与漏洞解析

  • cyfrin
  • 发布于 15小时前
  • 阅读 79

本文深入探讨了去中心化金融(DeFi)清算过程中的关键漏洞及其可能的攻击方式,并提出了相应的缓解策略,确保协议的偿付能力和用户信任。文章涵盖清算激励不足、坏账管理、部分清算及清算机制失败等问题,也强调了清算奖励的计算和优先级等细节,提供了从开发和审计的角度进行防范的最佳实践。

DeFi 清算漏洞与缓解策略

发现 DeFi 清算代码中的关键漏洞、潜在利用方式及保护协议偿付能力和用户信任的最佳实践。

最初发表于 Dacian 的博客,题为 DeFi Liquidation Vulnerabilities,发布时间为 2025 年 1 月 23 日。

引言

高效的清算对去中心化金融(DeFi)的偿付能力至关重要,但在无信任系统中安全实施这一点充满挑战。高漏洞密度可能威胁到协议的稳定性和用户的信任。开发者和审计人员应评估这些关键漏洞类别,以降低风险。

让我们先来探讨两个常用术语及其定义

  • Liquidatable: 当 collateral_value * loan_to_value_ratio < borrow_value,意味着存在足够的抵押品来覆盖贷款,及时清算将防止坏账的发生。
  • Insolvent: 当 collateral_value < borrow_value,意味着剩余的抵押品不足,清算后仍然会导致坏账。

应及时进行清算,以防止 liquidatable 头寸变为 insolvent

无清算激励

去中心化协议通常依赖无信任的清算者而非指定实体。为了确保及时清算,它们提供激励,例如奖金或奖励。例如,MEV 机器人在奖励超过 gas 成本时会进行清算,使其成为一种稳定、低风险的获利机会。

指导原则: 如果协议依赖无信任的清算者,是否提供足够的激励?

对小头寸没有清算激励

没有最低存款和头寸规模要求,小额债务头寸可能会积累,因缺乏财务激励而未被清算。这对于稳定币协议尤其危险,因为坏账可能积累,导致抵押率不足。

缓解:

指导原则: 协议是否在所有修改头寸的功能中强制执行最低头寸规模,还是仅在创建新头寸时?

其他案例: [ 1, 2, 3, 4, 5]

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

在诸如永续合约的交易协议中,用户的未平仓多头/空头头寸在计算总抵押品价值时包括当前的盈利/亏损(PNL)。如果用户有很高的正 PNL,他们可能会提取大部分或全部已存抵押品,而看起来仍然是偿付能力的。

如果他们的 PNL 之后下降,头寸便变得可以清算。然而,清算者将没有可没收的抵押品,这可能会导致清算失败或交易回退,最终导致偿付能力不足。

缓解:

  • 要求持有未平仓头寸的用户必须保持最低抵押品余额,无论其 PNL 如何。
  • 折扣正 PNL,降低其与实际存入资产相比的抵押品权重。

此外,允许用户在没有限制的情况下借出存入的抵押品可能会产生类似风险,因此应谨慎管理。

指导原则: 盈利的用户可以提取他们存入的抵押品吗?如果可以,市场条件不利的情况下会发生什么?

其他案例: [ 1]

无机制管理坏账

如果一个可以清算的头寸没有得到迅速处理,它可能会变得无偿付能力,此时清算奖励和没收的抵押品的价值低于偿还坏账所需的债务代币。

在这种情况下,无信任清算者没有激励进行清算,导致坏账积累。一些协议可能无法正确处理此状态,导致清算交易失败或回退,使无偿付能力的头寸无法清算。

缓解:

  • 使用受信赖的清算者,确保所有可以清算的头寸得到及时处理。
  • 建立一个保险基金,由协议费用资助,以吸收坏账,使清算对无信任清算者保持盈利。
  • 将坏账分摊给协议参与者,比如流动性提供者。

指导原则: 协议是否有机制处理坏账?无偿付能力的头寸清算时会发生什么?

其他案例: [ 1, 2]

部分清算绕过坏账核算

考虑一下这段清算代码:

// 清算关闭头寸时的额外处理
if (!hasPosition) {
    int256 remainingMargin = vault.margin;

    // 将正的保证金记入仓库接收者
    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;

        // 无法由仓库覆盖的损失
        // 必须由清算者补偿
        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();

    // 省略 - 一大堆清算处理代码 //

    // 从帐户中删除此活跃市场
    // @audit 这个调用 `EnumerableSet::remove` 改变 `activeMarketIds` 的顺序
    tradingAccount.updateActiveMarkets(ctx.marketId, ctx.oldPositionSizeX18, SD_ZERO);
}

由于 EnumerableSet 不保证 元素顺序且其 remove 函数使用交换和弹出 方法以提高效率,从列表中删除一个市场(而不是最后一个市场)会扰乱用户活跃市场的顺序。

恶意用户可以利用此点,通过打开多个头寸并触发此损坏,导致清算尝试因超出数组边界而失败。

缓解: 使用 EnumerableSet::values 遍历 activeMarketIds 的内存副本,而不是直接访问存储。

指导原则: 拥有多个开放头寸的用户能否被清算?测试套件中是否包含测试该场景?

攻击者通过抢跑交易阻止清算

如果可清算的用户能够在清算过程中修改关键变量,则可迫使交易回退。通过前置清算尝试,他们使自己变得不可能被清算。

阻止清算的方式示例:

指导原则: 是否存在可导致清算回退的用户控制变量?可清算用户能否通过前置清算进行利用?可清算用户可以采取哪些行动,是否应该允许他们这样做?

其他案例: [ 1, 2, 3, 4, 5, 6]

攻击者利用待处理动作阻止清算

考虑这段清算检查:

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

恶意用户可以通过启动与其余额相等的提现,使所有清算尝试失败,导致清算不可能。

缓解: 限制可清算用户执行某些操作,如存款、提现或交换。然而,这种方法可能会意外地影响在变得可清算之前已经有待处理提现的合法用户。

此外,攻击者还可能利用协议功能,在可清算状态下从即将发生的清算中获利。协议应该谨慎评估可清算用户应该被允许执行哪些操作。

指导原则: 是否存在需要多个交易才能完成的操作?如果是,当用户在这些操作仍在待处理状态时被清算时会发生什么?可清算用户可以采取哪些行动,是否应该允许这样做?

其他案例: [ 1, 2]

攻击者利用恶意 onERC721Received 回调阻止清算

如果在清算期间将 NFT(ERC721"推送"到攻击者控制的地址,攻击者可以设置其合约在 onERC721Received 回调中回退,从而阻止清算完成。

缓解: 使用“拉”机制,要求 NFT 所有者手动以单独交易的方式检索自己的代币。

此攻击也可以发生在带有ERC20 代币的情况下,包含转移Hook,可能会干扰清算结算。

指导原则: 如果清算依赖于代币转账的“推送”机制,攻击者是否可以利用回调使交易回退?

攻击者利用收益池在清算期间逃避抵押品扣押

某些多抵押协议允许用户将抵押品存入产生收益的池或农场,以最大化资金效率。这些协议必须在以下情况下正确核算存入的抵押品和产生的收益

  • 计算防止清算所需的最低抵押品
  • 在清算时扣押抵押品和产生的收益

如果仅实现第一个,攻击者可以通过:

  1. 针对存入的抵押品进行借贷
  2. 允许清算发生
  3. 从池或农场中提取其抵押品及收益

缓解: 智能合约审计人员应验证所有用于抵押的工具在清算时是否得以核算,并且在发生清算时持有抵押品的任何合约是否得到了正确通知。

指导原则: 协议是否包括抵押品的收益生成特性?如果有,清算代码是否与这些特性完全整合?用户是否可以以清算系统不识别的方式掩盖其存入的抵押品?

其他案例: [ 1]

如果坏账超过保险基金,则清算失败

在一个保险基金覆盖坏账的协议中,如果债务超过基金的可用余额,清算交易将失败,除非协议对此场景有具体处理。这可能导致大额无偿付能力头寸无限期淤塞,阻止清算,直至足够的费用累积以补充保险基金。

指导原则: 如果无偿付能力头寸的坏账超过保险基金的余额,会发生什么?

其他案例: [ 1]

由于固定的清算奖金,清算失败

考虑这段代码,试图通过向清算者提供征收抵押品的额外清算奖金来保证固定的 10% 清算奖金:

uint256 tokenAmountFromDebtCovered = getTokenAmountFromUsd(collateral, debtToCover);
// 清算者始终获得 10% 奖金
uint256 bonusCollateral = (tokenAmountFromDebtCovered * LIQUIDATION_BONUS) / LIQUIDATION_PRECISION;
_redeemCollateral(collateral, tokenAmountFromDebtCovered + bonusCollateral, user, msg.sender);

当借款人的抵押品比例低于 110% 时,他们便变为不足抵押,处于清算之中。然而,如果固定的清算奖金要求的抵押品超过剩余的抗压,交易将失败,导致清算无法进行。

缓解: 检查借款人是否拥有足够的抵押品来覆盖奖金,如果没有,灵活调整奖金以不超过可用最大金额。

指导原则: 如果没有足够的抵押品来完全支付清算奖金,会发生什么?

其他案例: [ 1, 2]

对非 18 次方抵押品清算失败

多抵押协议支持多种资产,其中一些资产并不遵循标准的 ERC20 18 次方精度。为处理此情况,协议通常会:

  • 对内部计算和存储使用 18 次方
  • 在转移资产时适用原生代币的精度
  • 在用户面对的功能输入中接受原生代币的小数点数量

虽然这种方式在一致性应用时有效,但拥有多个开发者的大型协议可能会引入不一致。审计员应验证当抵押品或债务代币具有不同小数精度时,清算是否能正常工作。

指导原则: 当代币有不同的小数精度时,清算是否能够正确运行?

由于多个 nonReentrancy 修饰符而导致清算失败

在复杂协议中,清算逻辑通常涉及可选地调用多个合约。审计员应确保没有执行路径触发同一合约内的两个 nonReentrant 函数,因为这将导致交易失败。

指导原则: 是否存在触发同一合约内多个 nonReentrant 修饰符的清算路径?

由于零值代币转账而导致清算失败

清算代码通常涉及计算各种代币金额,例如清算者奖励和费用,随后进行多次代币转账。如果协议未检查零值转移,处理零值转移的代币可能导致清算失败

指导原则: 协议在代币转账前是否执行零值检查?如果没有,协议是否支持在零值转移时出现回退的代币?

其他案例: [ 1]

清算因代币黑名单而回退

一些代币(如 USDC)具有黑名单,允许代币管理员冻结某些地址,使所有转账尝试发送到该地址时均因回退而失败。许多清算机制采用“推送”模型,代币自动发送到指定地址。如果协议支持黑名单中的代币,且清算尝试向被屏蔽地址发送资金,交易将失败,使清算变为不可能。

缓解: 切换到“拉”模型,用户必须手动申请他们的代币,而不是自动发送给他们。

指导原则: 协议是否使用“推送”清算机制,并且支持带有黑名单代币?如果是,当清算尝试向被阻止地址发送代币时会发生什么?

其他案例: [ 1, 2, 3]

仅有一个借款人时无法清算

考虑以下清算逻辑:

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

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

这段代码阻止当只有一个借款人时进行清算,这是一个设计缺陷。单一借款人仍应在其头寸成为可清算时受到清算。

指导原则: 如果仅剩一个借款人,他们仍然会被清算吗?

其他案例: [ 1]

不正确的清算计算

清算涉及多项计算,包括抵押品估值、坏账评估、清算者奖励和费用的确定。即便是轻微的计算错误也可能产生严重后果。

清算者奖励计算不正确

清算通常涉及债务和抵押代币,而它们的小数精度可能不同。在处理这些差异时的错误可能导致:

  • 奖励过低,抑制清算者参与
  • 奖励过高,在协议的支出下过度补偿清算者

考虑以下简化的代码:

function executeLiquidate(State storage state, LiquidateParams calldata params)
    external returns (uint256 liquidatorProfitCollateralToken) {

    // @audit debtPosition = USDC 使用 6 个小数
    DebtPosition storage debtPosition = state.getDebtPosition(params.debtPositionId);

    // @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 个小数进行计算的
            // 这是不正确的,必须使用 debtInCollateralToken 使用 18 个小数进行计算
            Math.mulDivUp(debtPosition.futureValue, state.feeConfig.liquidationRewardPercent, PERCENT)
            // @audit 应该是:
         // Math.mulDivUp(debtInCollateralToken, ...)

        );

这个清算函数以抵押代币(WETH,18 次方)分配奖励,但错误地使用债务代币(USDC,6 次方)计算奖励。因此,清算者的支付会显著低于预期,降低了他们参与的激励。

清算奖励应该进行比例缩放——如果借款人在一个账户中从三个贷方借同样的数量,他们的清算奖励应与从三个贷方分别借款的奖励相当。

清算奖励计算中的错误可能有许多来源。由于没有通用的指导方针,审计员必须仔细检查具体实现,以识别潜在的计算错误。

其他案例: [ 1, 2, 3, 4, 5, 6, 7, 8, 9]

未能优先考虑清算奖励

在清算过程中,可能需要将多项费用分配给不同的实体。如果可用的抵押品(或在坏账情况下的保险基金)不足以支付所有费用,协议应优先支付清算者的奖励。这确保清算者保持激励,尤其在依赖无信任清算者的系统中,他们在维持协议偿付能力方面发挥着关键作用。

指导原则: 如果没有足够的抵押品来支付所有清算费用,会发生什么?清算者的奖励是否获得优先权,特别是在依赖无信任清算者的协议中?

协议清算费用计算不正确

某些协议在清算过程中施加“协议费用”,由清算者或清算用户支付。如果计算错误且设定过高,可能使清算变得无利可图,从而抑制清算者的参与,使坏账得以积累。

考虑这段协议清算费用计算代码:

function _transferAssetsToLiquidator(address position, AssetData[] calldata assetData) internal {
    // 将头寸资产转移给清算者并累计协议清算费用
    uint256 assetDataLength = assetData.length;
    for (uint256 i; i < 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排序器重新上线后,用户是否可以在价格数据更新之前在宽限期内存入抵押?协议是否提供宽限期以防止立即清算,还是用户在价格数据恢复后立即被清算?

由于协议暂停期间利息积累导致不公平清算

如果一个协议允许暂停,但在暂停期间阻止用户偿还贷款,借款利息也应停止积累。否则,用户在恢复时可能因突然的利息积累而面临瞬时清算。

指导原则: 在协议暂停期间,即使用户无法偿还,借款利息是否持续累积?

清算仍在进行期间暂停偿还导致的不公平清算

协议不应允许在清算仍然启用的情况下暂停还款的情况。这将不公正地清算那些本意偿还却被协议本身阻止的借款人。

缓解措施: 协议应实施一个在解除清算暂停后的宽限期,使用户有时间偿还贷款或增加抵押后再被清算。

指导原则: 协议是否可能进入用户无法偿还但仍面临清算的状态?在解除暂停后,是否有偿还和抵押存入的宽限期,还是用户立即被清算?

额外案例: [ 1, 2, 3, 4, 5, 6]

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

在判断用户是否可清算之前,协议必须先更新任何累计费用,例如杠杆交易中的贷款利息或资金费用。未能做到这一点可能导致清算延迟或不正确。

审计人员应密切关注检查清算状态的view函数,但不应修改状态,确保其正确计算最新的应付费用。此外,所有相关费用必须在执行清算之前更新。

指导原则: 协议是否在判断用户是否可清算之前刷新所有利息、收益、资金费用和PNL?

清算期间正PNL、收益和奖励被损失

杠杆交易中的一个边缘案例发生在:

  1. 一位交易者存入抵押$C并针对资产$A开启一个杠杆多头头寸。
  2. $A的价格上涨,为交易者带来了大量未实现收益。
  3. $C的价格进一步下跌,使得该头寸虽然总体上处于盈利状态,却变得可清算。
  4. 另外,累计的借款或资金费用超过未实现收益,触发清算。

如果发生这种情况,交易者的正PNL、已赚收益和其他奖励应该在清算之前计入他们的账户。否则,这些资产将被损失,使得清算显得不公平。

指导原则: 协议是否确保在清算之前所有未实现的收益、收益和奖励都被实现?如果没有,它们在清算后是否会损失?

额外案例: [ 1, 2, 3]

清算期间未施加交换费用

一些协议在将一种资产转换为另一种时征收交换费用。如果这些费用适用于常规交换,但在清算者用扣押的抵押交换债务代币时则不收取费用,则协议及其保险基金可能获取的代币数量少于预期。

指导原则: 协议是否通常在清算期间收取交换费用并执行交换?如果是,是否会对清算交换施加交换费用?如果没有,协议是否明确记录了这一例外存在的原因?

通过操纵预言机更新进行盈利的自清算

攻击者可以通过以下步骤利用用户触发的预言机更新执行盈利的自清算:

  1. 使用闪电贷获得大量抵押代币。
  2. 存入抵押并借取最大可借债务(最大杠杆)。
  3. 触发预言机价格更新。
  4. 清算自己的头寸。

如果预言机价格更新导致全额抵押恢复,而归还的债务代币少于借入的债务代币,则该攻击变得有利可图。

缓解措施:

  • 对借款和清算费用施加费用以减少盈利诱因。
  • 在清算发生之前,设立冷却期。
  • 对高波动的抵押资产限制杠杆。
  • 使用小价格偏差且不能被用户触发的预言机。

自清算可能构成重大风险,特别是如果用户能够故意让自己被清算以利用协议。

指导原则: 用户是否可以故意变得可清算并触发自清算?预言机价格更新可以被操纵以从协议中提取价值吗?

清算使借款人面临较低的健康得分

清算,无论是完全还是部分,应通过减少未来清算的风险来改善借款人的财务状况。然而,在支持多种抵押类型和部分清算的协议中,细微的错误可能会导致借款人在清算后处于更糟糕的状态,从而增加进一步清算的可能性。

考虑这个清算函数:

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

该清算功能允许清算人选择作为赔偿的抵押物类型。这很冒险,因为不同类型的抵押物具有不同的:

  • 拨款因子,决定能借多少
  • 风险轮廓,其中一些资产(例如USDC)是稳定的,而其他资产(例如ETH或投机性质的ERC20代币)则高度波动

清算人可以首先没收借款人最稳定、高拨款因子的抵押物。这使得借款人面临:

  1. 更多波动的抵押,被更大的价格波动影响
  2. 更低的拨款因子,使他们更有可能再次被清算

这可能导致级联清算,首先的清算削弱了借款人的头寸,触发了快速连续的进一步清算。这使得交易者披着不健康且风险更高的抵押组合

缓解措施:

  • 在清算前后计算借款人的健康得分
  • 如果清算后的健康得分等于或低于之前的得分,则撤销交易

常见的 健康得分计算 :

  • 抵押与债务比例: collateral_value / borrow_value
  • 借款能力: (collateral_value * loan_to_value_ratio) / borrow_value

指导原则: 清算是否可能将借款人置于更糟糕的财务状态?清算人是否可以选择性地没收抵押物,使借款人面临更波动和风险更高的投资组合?

额外案例: [ 1, 2, 3, 4]

抵押优先级顺序的腐败

允许各种抵押类型的协议可以通过建立优先清算顺序来减少上述风险,首先针对更波动、更高风险的抵押。然而,在设计修改此顺序的函数时需谨慎,因为这些函数可能会无意中破坏它,导致不当清算。

指导原则: 更改抵押清算优先级的函数是否可能导致腐败?

由于借款人替换导致偿还归属不正确

某些协议提供了一种可选的“替换”清算方法。这允许可清算头寸被用来满足订单薄上的订单,实质上是将一个财务不稳定的借款人替换为一个稳定的借款人。

其他协议允许用户“购买”可清算头寸,基本上承担它,前提是他们有足够的抵押以确保其偿付能力。这实现了相同的结果:将债务从原来的不健康借款者转移到新的健康借款者。

在这两种情况下,借款人的地址更新为新借款人,但其他数据(例如头寸的id和债务)通常保持不变。考虑这两笔交易同时发生时可能造成的冲突:

交易1 (TX1): 原始可清算借款人尝试偿还他们的头寸,提供其id作为输入。

交易2 (TX2): 一笔替换清算交易尝试将头寸转移到一个健康的借款人。

如果TX2在TX1之前执行,则新借款人在原借款人偿还之前获得该头寸。原借款人有效地偿还了新借款人的债务!在允许用户“购买”头寸的协议中,恶意行为者可以通过先行偿还交易进行利用。他们可以购买可清算头寸,然后让原借款人为他们刚刚获得的头寸偿还债务。

一个可能的解决方案是要求偿还交易包括借款人的地址作为输入,并在当前借款人的地址与之不匹配时撤销交易。

指导原则: 能否将可清算头寸从不稳定的用户转移给稳定的用户?如果可以,若不稳定用户在转移的同时尝试偿还,会有什么后果?

额外案例: [ 1]

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

许多平台在启动贷款时使用的贷款价值比率(LTV)高于触发清算的LTV。这种差异的存在是为了保护借款人,防止他们在建立新头寸后立即被清算。

消除借款与清算LTV之间的差距使用户能够开启危险接近清算的头寸。这增加了清算的可能性,危及平台的稳定性,负面影响用户体验。

指导原则: 用户是否能在开新头寸后不久便面临清算?现有头寸调整后呢?

额外案例: [ 1, 2, 3, 4]

在清算拍卖进行时借款人产生利息

某些协议采用“拍卖”系统,具体时间内对可清算头寸进行拍卖。在这些情况下,债务利息应在拍卖开始时立即暂停。清算位置在拍卖期间不应产生更多利息

指导原则: 在债务被拍卖时,借款人是否继续产生利息?

清算和交换中没有滑点

理想情况下,在清算过程中,清算人应能够定义最低可接受的奖励(代币、份额等)。这对于使用清算交换的协议至关重要,因为这些交换容易受到MEV利用,可能会导致清算人获得低于预期的奖励。

指导原则: 清算人是否能够设置滑点参数?如果交换是清算的一部分,是否可能导致清算人(或协议或被清算用户)获得的代币少于预期?

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

0 条评论

请先 登录 后评论
cyfrin
cyfrin
Securing the blockchain and its users. Industry-leading smart contract audits, tools, and education.