本文深入分析了 Liquity 协议的核心机制,重点介绍了稳定池、清算和赎回的实现原理。通过对多个核心 Solidity 合约代码的解析,详细展示了协议如何处理债务抵消、抵押品分配以及在正常和恢复模式下的系统安全性维护。
在前一篇文章中,我开始从智能合约的角度解释 Liquity 协议,其中我介绍了它的原生 Token 和它的 BorrowerOperations.sol 等内容。
在本文中,我们将继续深入探讨该协议中三个最重要且最有趣的部分:
Stability Pool(稳定池),顾名思义,确保了协议保持稳定并能以最高效率运行。
当任何 Trove 被清算时,将从 Stability Pool 的余额中销毁与该 Trove 剩余债务相对应的 LUSD 数量,以偿还其债务。作为交换,来自该 Trove 的全部抵押品将转移到 Stability Pool 中。
从池中移除必要数量的 LUSD 以抵消债务,然后将锁定的以太坊按比例分配给池中的质押者。这些被称为“清算收益”。
包含 Stability Pool 操作的功能:进行存款,以及提取复利存款、累积的 ETH 和 LQTY 收益。
持有 LUSD Stability Pool 存款和清算中为存款人准备的 ETH 收益。
StabilityPool 的一些主要函数包括:
provideToSP(uint _amount, address _frontEndTag) — 允许稳定币持有者向 Stability Pool 存入 _amount 数量的 LUSD。withdrawFromSP(uint _amount) — 允许稳定币持有者从 Stability Pool 中提取 _amount 数量的 LUSD,最高可达其剩余稳定池存款的价值。withdrawETHGainToTrove(address _hint) — 将用户全部累积的 ETH 收益发送到用户活跃的 Trove,并根据债务吸收产生的累积损失更新其稳定池存款。registerFrontEnd(uint _kickbackRate) — 将一个地址注册为前端,并在 [0,1] 范围内设置其选择的回扣率。与 StabilityPool.sol 一起,这些合约为系统各自的部分持有以太币和/或 Token,并包含极少的逻辑:
持有活跃 Trove 的总以太币余额并记录总稳定币债务。
1function sendETH(address _account, uint _amount) external override {
2 _requireCallerIsBOorTroveMorSP();
3 ETH = ETH.sub(_amount);
4 emit ActivePoolETHBalanceUpdated(ETH);
5 emit EtherSent(_account, _amount);
6
7 (bool success, ) = _account.call{ value: _amount }("");
8 require(success, "ActivePool: sending ETH failed");
9}
10
11function increaseLUSDDebt(uint _amount) external override {
12 _requireCallerIsBOorTroveM();
13 LUSDDebt = LUSDDebt.add(_amount);
14 ActivePoolLUSDDebtUpdated(LUSDDebt);
15}
16
17function decreaseLUSDDebt(uint _amount) external override {
18 _requireCallerIsBOorTroveMorSP();
19 LUSDDebt = LUSDDebt.sub(_amount);
20 ActivePoolLUSDDebtUpdated(LUSDDebt);
21}
持有已清算 Trove 的总以太币余额,并记录待重新分配给活跃 Trove 的总稳定币债务。
如果一个 Trove 在 DefaultPool 中有待处理的以太币/债务“奖励”,那么在它下一次进行借款人操作、赎回或清算时,这些奖励将被应用到该 Trove。
1function sendETHToActivePool(uint _amount) external override {
2 _requireCallerIsTroveManager();
3 address activePool = activePoolAddress; // 缓存以节省一个 SLOAD
4 ETH = ETH.sub(_amount);
5 emit DefaultPoolETHBalanceUpdated(ETH);
6 emit EtherSent(activePool, _amount);
7
8 (bool success, ) = activePool.call{ value: _amount }("");
9 require(success, "DefaultPool: sending ETH failed");
10}
持有来自已完全赎回的 Trove 以及在恢复模式(Recovery Mode)下被清算且 ICR > MCR 的 Trove 的 ETH 盈余。当受到 BorrowerOperations.sol 的指示时,将盈余发送回所属的借款人。
1function accountSurplus(address _account, uint _amount) external override {
2 _requireCallerIsTroveManager();
3
4 uint newAmount = balances[_account].add(_amount);
5 balances[_account] = newAmount;
6
7 emit CollBalanceUpdated(_account, newAmount);
8}
9
10function claimColl(address _account) external override {
11 _requireCallerIsBorrowerOperations();
12 uint claimableColl = balances[_account];
13 require(claimableColl > 0, "CollSurplusPool: No collateral available to claim");
14
15 balances[_account] = 0;
16 emit CollBalanceUpdated(_account, 0);
17
18 ETH = ETH.sub(claimableColl);
19 emit EtherSent(_account, claimableColl);
20
21 (bool success, ) = _account.call{ value: claimableColl }("");
22 require(success, "CollSurplusPool: sending ETH failed");
23}
持有总 LUSD 清算准备金。当 Trove 被开启时,LUSD 被移入 GasPool;当 Trove 被清算或关闭时,LUSD 被移出。
Liquity 的清算机制包括用 Stability Pool 抵消清算债务,如果池中没有足够的 LUSD,则将剩余债务按抵押品数量排序重新分配给所有活跃的 Trove。
清算仅在两种情况下发生:
被清算的 Trove 的所有者被允许保留他们借出的 LUSD,但仍会损失资金,因为他们将无法取回抵押品(抵押品的价值总是高于贷款价格)。
一旦 Trove 低于 110% 的最低抵押率(Minimum Collateral Ratio),任何人都可以清算它。发起人会收到 Gas 补偿(200 LUSD + Trove 抵押品的 0.5%)作为此项服务的奖励。
Liquity 有两种不同的清算模式。在正常模式中:
1function _liquidateNormalMode(
2 IActivePool _activePool,
3 IDefaultPool _defaultPool,
4 address _borrower,
5 uint _LUSDInStabPool
6) internal returns (LiquidationValues memory singleLiquidation)
7{
8 [...]
9 (singleLiquidation.entireTroveDebt,
10 singleLiquidation.entireTroveColl,
11 vars.pendingDebtReward,
12 vars.pendingCollReward) = getEntireDebtAndColl(_borrower);
13
14 _movePendingTroveRewardsToActivePool(
15 _activePool, _defaultPool,
16 vars.pendingDebtReward, vars.pendingCollReward
17 );
18 _removeStake(_borrower);
19
20 [...]
21 (singleLiquidation.debtToOffset,
22 singleLiquidation.collToSendToSP,
23 singleLiquidation.debtToRedistribute,
24 singleLiquidation.collToRedistribute) =
25 _getOffsetAndRedistributionVals(
26 singleLiquidation.entireTroveDebt,
27 collToLiquidate,
28 _LUSDInStabPool
29 );
30
31 _closeTrove(_borrower, Status.closedByLiquidation);
32
33 [...]
34}
如果 Liquity 系统的总抵押率 (TCR) 低于关键系统抵押率 (CCR, 150%),则会触发恢复模式。在恢复模式期间,ICR 低于 TCR 的 Trove 也可以被清算,为系统提供额外的保护。
Redemption(赎回)是以面值将 LUSD 兑换为 ETH 的过程,就好像 1 LUSD 的价值恰好等于 $1。也就是说,通过 x LUSD,你可以换回价值 x 美元的 ETH。
用户可以随时无限制地将其 LUSD 赎回为 ETH。但是,可能会对赎回金额收取赎回费。
LUSD 赎回是 Liquity 最独特的功能之一。简单来说:赎回机制使 LUSD 持有者能够随时按面值将 LUSD 赎回为底层的 ETH 抵押品。
赎回的逻辑位于 TroveManager.sol 中,它开始搜索第一个 ICR 大于或等于 MCR (110%) 的 Trove。
1function redeemCollateral(
2 uint _LUSDamount,
3 address _firstRedemptionHint,
4 address _upperPartialRedemptionHint,
5 address _lowerPartialRedemptionHint,
6 uint _partialRedemptionHintNICR,
7 uint _maxIterations,
8 uint _maxFeePercentage
9)
10 external
11 override
12{
13 [...]
14
15 if (_isValidFirstRedemptionHint(
16 contractsCache.sortedTroves,
17 _firstRedemptionHint,
18 totals.price))
19 {
20 currentBorrower = _firstRedemptionHint;
21 } else {
22 currentBorrower = contractsCache.sortedTroves.getLast();
23 // 查找第一个 ICR >= MCR 的 Trove
24 while (currentBorrower != address(0)
25 && getCurrentICR(currentBorrower, totals.price) < MCR)
26 {
27 currentBorrower = contractsCache.sortedTroves.getPrev(
28 currentBorrower
29 );
30 }
31 }
32
33 [...]
34}
赎回机制按照 ICR 从低到高的顺序遍历排序后的 Trove 列表,从每个 Trove 进行赎回,直到覆盖全部 LUSD 金额。这种设计自然地首先针对风险最高的头寸,从而加强了系统的健康。修改这种遍历逻辑的 Liquity 分叉——例如通过跳过某些 Trove 或以不同方式限制迭代——经常会引发关于部分赎回的边界情况。有关 DeFi 借贷协议中相关的漏洞模式,请参阅我们关于 Oracle 操纵和重入攻击的指南。
在 Zealynx,我们专注于 DeFi 协议安全审计——从像 Liquity 这样的借贷系统到复杂的稳定币机制和清算逻辑。无论你是在构建基于 CDP 的协议还是分叉现有的协议,我们的团队都随时准备帮助你交付安全的代码。在聘请审计师之前,请查看我们的审计前清单以确保你的代码库已准备就绪。联系我们开始对话。
想通过更多像这样的深入分析保持领先吗?订阅我们的时事通讯,确保你不会错过未来的见解。
- 原文链接: zealynx.io/blogs/liquity...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!