☰
GLM 5.3 审计借贷协议健康因子与抵押清算:Compound 分叉盘算术下溢漏洞挖掘
2026/10/12 12:16:39 网站建设 项目流程

在去中心化借贷协议的历史长河中,Compound v2 的源码架构无疑是整个 DeFi 乐高世界被“分叉(Fork)”最多的代码资产之一。从各种小众公链上的初代借贷产品,到各类结合了新兴质押衍生品(LST/LRT)的结构化杠杆协议,其核心结算逻辑几乎都流淌着 Compound 经典双代币模型(cToken 与 Comptroller)的血脉。

然而,开源代码的繁荣背后潜藏着极其巨大的安全隐患。

很多分叉团队为了支持自身独特的代币经济学,往往会在不经意间对底层的核心数学库进行“微调”:

  • 将老旧的 Solidity 0.5.x / 0.6.x 代码强行升级到 Solidity 0.8.x,并在为了节省 Gas 的冲动下,大量包裹非安全的unchecked { ... }作用域;
  • 在支持非标准精度代币(如 6 位精度的 USDC/USDT、8 位精度的 WBTC 与 18 位精度的 WETH)时,粗暴地魔改利息累加因子(Borrow Index)与抵押物扣押计算公式;
  • 未能深刻理解抵押品清算过程中极端行情下的算术下溢(Arithmetic Underflow)与精度截断(Precision Truncation),导致健康因子计算产生荒谬的数值翻转。

这种由于多代币精度不对齐和代数运算顺序错误引发的漏洞,通常隐藏在数百行复杂的指数折算库中,传统的静态语法扫描工具几乎无法识别。

本文将手把手带你使用GLM 5.3智能合约审计模型,系统还原一起典型的 Compound 分叉盘精度截断漏洞,并拆解工业级的防御审计指南。


一、Compound 借贷流动性评估的核心数学骨架

在 Compound 的风险控制中,用户的借贷安全性由审计核心合约Comptroller中的getAccountLiquidity方法全权裁决:

$$\text{账户净流动性 (Liquidity)} = \sum \left( \text{有效抵押物价值} \times \text{抵押因子} \right) - \sum \left( \text{借出负债总价值} \right)$$

  1. 有效抵押物价值:将用户持有的 cToken 按照底层汇率(Exchange Rate)折算为原生资产,再乘以预言机喂价(Oracle Price);
  2. 负债总价值:用户未偿还的借款本息总和乘以对应代币价格;
  3. 安全边界:
    • 若 $\text{Liquidity} > 0$:账户健康,允许继续借款或提现;
    • 若 $\text{Liquidity} < 0$:账户发生赤字(Shortfall),触发抵押品被清算。

二、真实致命缺陷:多精度换算下的截断与下溢陷阱

让我们来看一段在分叉项目中极其常见的魔改版清算抵押物扣押函数(liquidateCalculateSeizeTokens):

// SPDX-License-Identifier: MIT pragma solidity 0.8.26; contract VulnerableCompoundForkMath { // 假设预言机统一返回 18 位精度的 USD 价格 // priceBorrowed: 被偿还负债代币的单价 (18 位精度) // priceCollateral: 被没收抵押物代币的单价 (18 位精度) // actualRepayAmount: 清算者偿还的负债代币真实数量 function calculateSeizeTokens( uint256 priceBorrowed, uint256 priceCollateral, uint256 exchangeRateCollateral, uint256 actualRepayAmount, uint8 collateralDecimals, uint8 borrowedDecimals ) public pure returns (uint256 seizeTokens) { // 开发者试图对不同代币的小数位差进行手动对齐 // 致命错误:运算顺序颠倒,先进行了小整数除法,引发严重的精度截断归零! uint256 decimalAdjustment = 10 ** (18 + collateralDecimals - borrowedDecimals); // 错误实现的抵押品扣押比率: // 原意为: (repayAmount * priceBorrowed * 1.05) / priceCollateral // 但开发者在中间提前除以了 priceCollateral,且未对大数进行前置放大: uint256 priceRatio = (priceBorrowed * 1e18) / priceCollateral; // 灾难发生:当抵押物单价极高(如 WBTC = $60,000),而借出代币单价极低(如 1 枚小币 = $0.05)时: // priceBorrowed * 1e18 / priceCollateral 在数值上由于向下截断,结果直接变成 0! // 导致计算出的 seizeTokens 瞬间被清零! uint256 seizeAmount = (actualRepayAmount * priceRatio) / decimalAdjustment; seizeTokens = (seizeAmount * 1e18) / exchangeRateCollateral; } }

2.1 精度截断引发的协议瘫痪与坏账穿仓

在上述有缺陷的代码中:

  • 当借款人借入低价垃圾代币、抵押高价值资产(如 WBTC)发生资不抵债时;
  • 外部清算机器人尝试替其还款清算,调用calculateSeizeTokens计算其应当分得的抵押物;
  • 由于priceRatio计算过程中分子被分母无情碾压截断为0,清算者拿到的seizeTokens变成了0;
  • 清算者付出了还款成本,却分不到任何抵押物回报,导致全网没有任何理性套利者愿意替该用户执行清算!
  • 坏账随着市场暴跌无限制失控放大,最终彻底击穿借贷池本金,导致全体无辜存款人资金遭遇永久挤兑封锁。

三、利用 GLM 5.3 执行上下文二阶交叉审计

面对此类晦涩的代数公式,我们向针对以太坊 DeFi 协议微调过的GLM 5.3模型输入如下审计提示词链:

你是一名负责以太坊借贷协议数学安全的资深审计专家。 请深度审查以下来自 Compound v2 分叉合约的清算公式: 1. 检查是否存在【先除后乘(Division Before Multiplication)】引发的精度截断到 0 的风险; 2. 分析当借入代币价格远低于抵押品价格时(如 10^-4 倍),中间变量是否存在算术下溢或计算归零; 3. 推演清算者在极端行情下无法获得抵押物份额导致协议坏账的攻击路径; 4. 给出基于定点数大数优先相乘(Multiplication Before Division)的工业级加固代码。

3.1 GLM 5.3 的核心审计输出

GLM 5.3 展现出了极高的代数洞察力,精准指出了痛点:

“漏洞根因在于priceRatio的独立计算违背了 EVM 定点数运算的基本准则——必须严格保持‘先乘后除’以维持最大有效精度。在被清算资产与抵押品价格跨越多个数量级时,(priceBorrowed * 1e18) / priceCollateral发生了致命的向下整数截断(Integer Truncation to Zero)。这使得清算奖励被置零,打破了借贷协议依靠清算罚金维系流动性的博弈平衡。该漏洞属于 P0 级坏账死锁风险。”


四、工业级防御:定点数先乘后除与全量精度对齐

在 Solidity 中处理不同精度的代币折算,绝对不能把中间比率拆开计算,必须将所有的分子在进入除法之前统一乘以大数(Scale Factor),并将其合并为一个单一的大分数:

// SPDX-License-Identifier: MIT pragma solidity 0.8.26; contract SecureCompoundForkMath { uint256 private constant EXP_SCALE = 1e18; uint256 private constant LIQUIDATION_INCENTIVE = 105e16; // 1.05 代表 5% 清算奖励 error TruncationToZeroDetected(); /// @notice 安全计算清算应扣押的 cToken 份额 function calculateSeizeTokensSafe( uint256 priceBorrowed, uint256 priceCollateral, uint256 exchangeRateCollateral, uint256 actualRepayAmount, uint8 collateralDecimals, uint8 borrowedDecimals ) public pure returns (uint256 seizeTokens) { // 1. 严格统一分子:先乘后除,将实际还款额、清算奖励因子、借出代币价格连乘 // 同时乘以前置的小数位补偿因子,确保整个乘积在大数域中进行 uint256 numerator = actualRepayAmount * LIQUIDATION_INCENTIVE * priceBorrowed; // 2. 统一分母:将抵押物价格与基础精度连乘 uint256 denominator = priceCollateral * EXP_SCALE; // 3. 补充代币精度差额的幂次补偿 if (collateralDecimals >= borrowedDecimals) { numerator = numerator * (10 ** (collateralDecimals - borrowedDecimals)); } else { denominator = denominator * (10 ** (borrowedDecimals - collateralDecimals)); } // 4. 执行单次终极除法,将有效精度损失降至极限物理底线 uint256 seizeAmount = numerator / denominator; if (seizeAmount == 0 && actualRepayAmount > 0) { revert TruncationToZeroDetected(); } // 5. 按照抵押品汇率折算为最终待扣押的 cToken 数量 seizeTokens = (seizeAmount * EXP_SCALE) / exchangeRateCollateral; } }

五、安全审计员复核 CheckList

在审查任何借贷协议的数学库时,审计员必须强制核对以下四条红线:

  1. 先乘后除铁律(Multiplication Before Division):所有的代数公式中,除法指令/必须且只能出现在整个计算表达式的绝对最末端,严禁在中间步骤中保存未放大的除法商数;
  2. 多代币精度单元测试必须包含极限边界:单元测试用例必须覆盖“18 位抵押品 + 6 位负债代币”以及“6 位抵押品 + 18 位负债代币”的双向场景,并且输入单价必须包含从 $$0.000001$ 到 $$100,000$ 的极端价格落差;
  3. 非零扣押断言(Non-zero Seize Check):在清算函数的出口,必须对算出的抵押物份额进行显式非零断言require(seizeTokens > 0),绝不允许清算者在偿还债务后得到空响应;
  4. 警惕 unchecked 滥用:在 Solidity 0.8+ 中,除非有极其完备的形式化验证证明该减法绝对不可发生下溢,否则严禁在流动性评估主流程中使用unchecked块强行跳过编译器的默认溢出防护。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询