本文介绍了 Liquity V2,这是一个去中心化的抵押债务平台,允许用户锁定 WETH 或 LST 等资产以发行稳定币 BOLD。V2 版本引入了用户设定的利率、多抵押品系统、改进的赎回路由和清算机制等新功能,旨在提高 DeFi 借贷市场的效率和竞争力。文章还讨论了 Liquity V2 中的安全考虑因素,包括防止预言机抢跑交易、操纵赎回路由以及管理预言机故障等。
探索 Liquity V2 如何通过新功能支持去中心化借贷。了解 DeFi 中加密货币抵押债务和稳定币 BOLD。
随着 Liquity V2 的发布,Liquity 协议迈出了大胆的一步,引入了一个新功能:用户设定的利率,为借款人和稳定币持有者创造了一个更高效、更具吸引力的市场。Liquity V2 是一个去中心化的抵押债务平台,用于去中心化借贷,允许用户锁定 WETH 或流动性质押代币 (LST) 等资产,以发行其稳定币代币 BOLD。BOLD 旨在通过确保系统始终过度抵押,并且 BOLD 始终可以兑换为相应数量的协议抵押品,从而保持 1 美元的价值。请参阅我们的经济审计以了解更多信息。该系统使用户能够通过存入 ERC20 代币 作为抵押品来开设抵押债务头寸,称为 Trove。只要抵押率保持在最低要求之上,就可以根据抵押品借入 BOLD 代币。BOLD 可以在以太坊地址之间自由转移,并且可以被任何人兑换为价值一美元的抵押品,减去费用。对于协议团队,这需要进行机制设计审查。
CollateralRegistry
、一个 BoldToken
和一组核心系统合约,这些合约是为每个抵押品“分支”部署的。CollateralRegistry
负责将外部 ERC20 抵押代币映射到相应的 TroveManager
地址。它还处理跨各种抵押品分支的赎回路由。CollateralRegistry
– 跟踪所有 LST 抵押品,并将特定于分支的 TroveManager 映射到这些抵押品。它计算赎回费用,并将 BOLD 赎回定向到不同分支的相应 TroveManager,与其未偿债务成正比。BOLDToken
– 这是实现 ERC20 标准并包含 EIP-2612 许可功能的稳定币代币合约。该合约负责铸造、销毁和转移 BOLD 代币。BorrowerOperations
函数调用 TroveManager
来更新 Trove 的状态并与各种池交互,在池之间或池与用户之间移动抵押品和 BOLD。它还指示 ActivePool
铸造利息。TroveManager
– 负责清算、赎回和计算各个 Trove 的利息。它保持每个 Trove 的状态,包括抵押品、债务和利率。但是,TroveManager
本身不持有任何价值(抵押品或 BOLD)。它在需要时调用各种池来移动抵押品或 BOLD。TroveNFT
– 在 TroveManager
的控制下实现 Trove NFT 的基本铸造和销毁功能。它还实现了 tokenURI
功能,该功能为每个 Trove 提供元数据,包括唯一图像。LiquityBase
– 包含 CollateralRegistry
、TroveManager
、BorrowerOperations
和 StabilityPool
使用的共享函数。StabilityPool
– 管理 Stability Pool 操作,例如存款、取款复利存款、抵押品以及来自清算的 BOLD 收益。它持有 Stability Pool BOLD 存款、收益和分支上所有存款人的清算抵押品。SortedTroves
– 一个双向链表,用于存储 Trove 所有者的地址,按其年利率排序。它根据利率自动在正确的位置插入和重新插入 Trove。该合约还处理插入或重新插入成批的 Trove,这些 Trove 被建模为双向链表的切片。ActivePool
– 持有分支的抵押品余额,并跟踪活跃 Trove 的 BOLD 总债务。它铸造总利息,在 StabilityPool
和收益路由器(目前,用于 DEX LP 激励的 MockInterestRouter
)之间分配。DefaultPool
– 持有已清算 Trove 的抵押品余额和 BOLD 债务,等待重新分配给活跃 Trove。如果活跃 Trove 在 DefaultPool
中有待处理的抵押品和债务“奖励”,则会在其下次借款人操作、赎回或清算期间将其应用于该 Trove。CollSurplusPool
– 跟踪和持有来自已清算 Trove 的抵押品盈余。它在索赔时分配借款人累积的盈余。GasPool
– 管理 WETH 气体补偿。当 Trove 打开时,WETH 会从借款人转移到 GasPool
,然后在 Trove 被清算或关闭时支付。MockInterestRouter
– 一个占位符合约,目前接收铸造利息的 LP 收益分成。稍后它将被一个真正的收益路由器取代,该路由器将收益导向 DEX LP 激励。HintHelpers
– 一个辅助合约,提供只读功能来计算准确的提示,这些提示可以提供给借款人操作。MultiTroveGetter
– 一个辅助合约,提供只读功能来获取 Trove 数据结构的数组,其中包含每个 Trove 的完整记录状态。PriceFeed
合约为不同分支上的抵押品定价。但是,核心功能在父合约之间共享。更多信息可以在此处阅读。
MainnetPriceFeedBase
:从 Chainlink(或 Redstone)oracle 获取价格并处理 oracle 故障。CompositePriceFeed
:结合来自 LST-ETH 和 ETH-USD oracle 的价格来计算综合 LST-USD 价格。WETHPriceFeed
:获取 WETH 抵押品的 ETH-USD 价格。WSTETHPriceFeed
:使用 STETH-USD 和 WSTETH-STETH 汇率计算 WSTETH-USD 价格。RETHPriceFeed
:使用 CompositePriceFeed
获取 RETH 抵押品的 RETH-ETH 汇率。TroveManager
和 StabilityPool
,分支中的 Trove 只接受一种类型的抵押品。分支中的清算仅针对其对应的 Stability Pool 进行抵消,并且存款人的任何清算收益都以该特定抵押品支付。来自清算的抵押品和债务的重新分配也仅适用于同一分支内的活跃 Trove。CollateralRegistry
构造函数最多可以接受 10 个 TroveManager
及其各自的抵押品类型。但是,目前只有 WETH 和两个 LST(rETH 和 wstETH)可用作抵押品,而原生 ETH 不被接受。1constructor(IBoldToken _boldToken, IERC20Metadata[] memory _tokens, ITroveManager[] memory _troveManagers)
1/* Sum of individual recorded Trove debts weighted by their respective chosen interest rates.
2* Updated at individual Trove operations.
3* "S" in the spec.
4*/
5uint256 public aggWeightedDebtSum;
1function calcPendingAggInterest() public view returns (uint256) {
2 if (shutdownTime != 0) return 0;
3 return Math.ceilDiv(aggWeightedDebtSum * (block.timestamp - lastAggUpdateTime), ONE_YEAR * DECIMAL_PRECISION);
4}
ActivePool
中的 _mintAggInterest
内部1function _mintAggInterest(IBoldToken _boldToken, uint256 _upfrontFee) internal returns (uint256 mintedAmount) {
2 mintedAmount = calcPendingAggInterest() + _upfrontFee;
3
4 // Mint part of the BOLD interest to the SP and part to the router for LPs.
5 if (mintedAmount > 0) {
6 uint256 spYield = SP_YIELD_SPLIT * mintedAmount / DECIMAL_PRECISION;
7 uint256 remainderToLPs = mintedAmount - spYield;
8
9 _boldToken.mint(address(interestRouter), remainderToLPs);
10 _boldToken.mint(address(stabilityPool), spYield);
11
12 stabilityPool.triggerBoldRewards(spYield);
13 }
14
15 lastAggUpdateTime = block.timestamp;
16}
此函数在 mintAggInterestAndAccountForTroveChange 内部调用,该函数由所有状态更改用户操作触发,包括借款人操作、清算、赎回和 Stability Pool 存款/取款。如果用户的操作改变了 Trove 的债务,则总累积债务会通过总未决利息和 Trove 债务净变化来更新。
1function mintAggInterestAndAccountForTroveChange(TroveChange calldata _troveChange, address _batchAddress)
2 external
3{
4 _requireCallerIsBOorTroveM();
5
6 // Batch management fees
7 if (_batchAddress != address(0)) {
8 _mintBatchManagementFeeAndAccountForChange(boldToken, _troveChange, _batchAddress);
9 }
10
11 // Do the arithmetic in 2 steps here to avoid overflow from the decrease
12 uint256 newAggRecordedDebt = aggRecordedDebt; // 1 SLOAD
13 newAggRecordedDebt += _mintAggInterest(boldToken, _troveChange.upfrontFee); // adds minted agg. interest + upfront fee
14 newAggRecordedDebt += _troveChange.appliedRedistBoldDebtGain;
15 newAggRecordedDebt += _troveChange.debtIncrease;
16 newAggRecordedDebt -= _troveChange.debtDecrease;
17 aggRecordedDebt = newAggRecordedDebt; // 1 SSTORE
18
19 uint256 newAggWeightedDebtSum = aggWeightedDebtSum; // 1 SLOAD
20 newAggWeightedDebtSum += _troveChange.newWeightedRecordedDebt;
21 newAggWeightedDebtSum -= _troveChange.oldWeightedRecordedDebt;
22 aggWeightedDebtSum = newAggWeightedDebtSum; // 1 SSTORE
23}
1function getUnbackedPortionPriceAndRedeemability() external returns (uint256, uint256, bool) {
2 uint256 totalDebt = getEntireSystemDebt();
3 uint256 spSize = stabilityPool.getTotalBoldDeposits();
4 uint256 unbackedPortion = totalDebt > spSize ? totalDebt - spSize : 0;
5
6 (uint256 price,) = priceFeed.fetchPrice();
7 // It's redeemable if the TCR is above the shutdown threshold, and branch has not been shut down
8 bool redeemable = _getTCR(price) >= SCR && shutdownTime == 0;
9
10 return (unbackedPortion, price, redeemable);
11}
1function redeemCollateral(uint256 _boldAmount, uint256 _maxIterationsPerCollateral, uint256 _maxFeePercentage)
2 external
3{
4 ...
5
6 // Gather and accumulate unbacked portions
7 for (uint256 index = 0; index < totals.numCollaterals; index++) {
8 ITroveManager troveManager = getTroveManager(index);
9 (uint256 unbackedPortion, uint256 price, bool redeemable) =
10 troveManager.getUnbackedPortionPriceAndRedeemability();
11 prices[index] = price;
12 if (redeemable) {
13 totals.unbacked += unbackedPortion;
14 unbackedPortions[index] = unbackedPortion;
15 }
16 }
17
18 ...
19
20 // Compute redemption amount for each collateral and redeem against the corresponding TroveManager
21 for (uint256 index = 0; index < totals.numCollaterals; index++) {
22 //uint256 unbackedPortion = unbackedPortions[index];
23 if (unbackedPortions[index] > 0) {
24 uint256 redeemAmount = _boldAmount * unbackedPortions[index] / totals.unbacked;
25 if (redeemAmount > 0) {
26 ITroveManager troveManager = getTroveManager(index);
27 uint256 redeemedAmount = troveManager.redeemCollateral(
28 msg.sender, redeemAmount, prices[index], redemptionRate, _maxIterationsPerCollateral
29 );
30 totals.redeemedAmount += redeemedAmount;
31 }
32 }
33 }
34
35 ...
36}
TroveManager
中的 redeemCollateral
内部执行1function redeemCollateral(
2 address _redeemer,
3 uint256 _boldamount,
4 uint256 _price,
5 uint256 _redemptionRate,
6 uint256 _maxIterations
7) external override returns (uint256 _redemeedAmount) {
8 ...
9
10 // Loop through the Troves starting from the one with lowest collateral ratio until _amount of Bold is exchanged for collateral
11 if (_maxIterations == 0) _maxIterations = type(uint256).max;
12 while (singleRedemption.troveId != 0 && remainingBold > 0 && _maxIterations > 0) {
13 _maxIterations--;
14 // Save the uint256 of the Trove preceding the current one
15 uint256 nextUserToCheck = sortedTrovesCached.getPrev(singleRedemption.troveId);
16 // Skip if ICR < 100%, to make sure that redemptions always improve the CR of hit Troves
17 if (getCurrentICR(singleRedemption.troveId, _price) < _100pct) {
18 singleRedemption.troveId = nextUserToCheck;
19 continue;
20 }
21
22 ...
23 }
24
25 ...
26}
TroveManager
中的函数 _redeemCollateralFromTrove
(在 redeemCollateral
中调用)负责将 Trove 标记为不可赎回。1function _redeemCollateralFromTrove(
2 IDefaultPool _defaultPool,
3 SingleRedemptionValues memory _singleRedemption,
4 uint256 _maxBoldamount,
5 uint256 _price,
6 uint256 _redemptionRate
7) internal {
8 ...
9
10 uint256 newDebt = _applySingleRedemption(_defaultPool, _singleRedemption, isTroveInBatch);
11
12 // Make Trove unredeemable if it's tiny, in order to prevent griefing future (normal, sequential) redemptions
13 if (newDebt < MIN_DEBT) {
14 Troves[_singleRedemption.troveId].status = Status.unredeemable;
15 if (isTroveInBatch) {
16 sortedTroves.removeFromBatch(_singleRedemption.troveId);
17 } else {
18 sortedTroves.remove(_singleRedemption.troveId);
19 }
20 }
21}
TroveManager
中打开 Trove 时,铸造 NFT,当关闭 Trove 时,销毁 NFT。BorrowerOperations
中的 setInterestIndividualDelegate
函数完成的。以下函数用于检查特定委托是否有资格更改利率:1function _requireSenderIsOwnerOrInterestManager(uint256 _troveId, address _owner) internal view {
2 if (msg.sender != _owner && msg.sender != interestIndividualDelegateOf[_troveId].account) {
3 revert NotOwnerNorInterestManager();
4 }
5}
Batch
数据结构。当批量管理器更新利率时,整个批量会根据新的利率重新插入到列表中的适当位置。为了简化 gas 成本,批量被视为“共享 Trove”,系统会跟踪批量的总债务和利率。利息和管理费会随着时间的推移而累积,每个 Trove 的重新分配收益都会单独跟踪。1struct Batch {
2 uint256 debt;
3 uint256 coll;
4 uint64 arrayIndex;
5 uint64 lastDebtUpdateTime;
6 uint64 lastInterestRateAdjTime;
7 uint256 annualInterestRate;
8 uint256 annualManagementFee;
9 uint256 totalDebtShares;
10}
当批量的利率更新时,它会自动影响该批量中的所有 trove:
1function onSetBatchManagerAnnualInterestRate(
2 address _batchAddress,
3 uint256 _newColl,
4 uint256 _newDebt,
5 uint256 _newAnnualInterestRate
6) external {
7 batches[_batchAddress].coll = _newColl;
8 batches[_batchAddress].debt = _newDebt;
9 batches[_batchAddress].annualInterestRate = _newAnnualInterestRate;
10 batches[_batchAddress].lastDebtUpdateTime = uint64(block.timestamp);
11 batches[_batchAddress].lastInterestRateAdjTime = uint64(block.timestamp);
12}
_updateBatchShares
函数在管理个人 trove 与它们所属的批量之间的关系方面起着关键作用。此函数确保与 trove 关联的债务和抵押品准确地反映在相应的批量中。
onOpenTroveAndJoinBatch()
、onAdjustTroveInsideBatch()
、onRegisterBatchManager()
、onLowerBatchManagerAnnualFee()
、onSetBatchManagerAnnualInterestRate()
、onSetInterestBatchManager()
、onRemoveFromBatch()
负责在 trove 属于批量时更新 trove,并更新批量管理器相关的变量。
在执行赎回时,会针对属于批量的 Trove 进行特定处理:
1if (isTroveInBatch) {
2 _getLatestBatchData(_singleRedemption.batchAddress, _singleRedemption.batch);
3 // We know boldLot <= trove entire debt, so this subtraction is safe
4 uint256 newAmountForWeightedDebt = _singleRedemption.batch.entireDebtWithoutRedistribution
5 + _singleRedemption.trove.redistBoldDebtGain - _singleRedemption.boldLot;
6 _singleRedemption.oldWeightedRecordedDebt = _singleRedemption.batch.weightedRecordedDebt;
7 _singleRedemption.newWeightedRecordedDebt =
8 newAmountForWeightedDebt * _singleRedemption.batch.annualInterestRate;
9}
10...
清算机制还考虑了 trove 是否属于批量:
1if (isTroveInBatch) {
2 singleLiquidation.oldWeightedRecordedDebt =
3 batch.weightedRecordedDebt + (trove.entireDebt - trove.redistBoldDebtGain) * batch.annualInterestRate;
4 singleLiquidation.newWeightedRecordedDebt = batch.entireDebtWithoutRedistribution * batch.annualInterestRate;
5 // Mint batch management fee
6 troveChange.batchAccruedManagementFee = batch.accruedManagementFee;
7 activePool.mintBatchManagementFeeAndAccountForChange(troveChange, batchAddress);
8}
当然,这些并不是唯一考虑批量的情况,但它们可以作为一个很好的例子。
BorrowerOperations
合约中的 shutdown
函数,然后关闭所有其他分支合约,例如 TroveManager
和 ActivePool
。关闭的另一种情况是当预言机发生故障时(revert 或返回 0)。在这种情况下,继承自 MainnetPriceFeedBase
的合约将通过调用 BorrowerOperations
中的 shutdownFromOracleFailure
函数来触发关闭,通过它们的 _disableFeedAndShutDown
函数。
TroveManager
执行,并且仅影响该分支,而不会跨其他分支路由。BorrowerOperations
1
2// Shutdown system collateral ratio. If the system's total collateral ratio (TCR) for a given collateral falls below the SCR,
3// the protocol triggers the shutdown of the borrow market and permanently disables all borrowing operations except for closing Troves.
4uint256 public immutable SCR;
5
6...
7
8function shutdown() external {
9 if (hasBeenShutDown) revert IsShutDown();
10
11 uint256 totalColl = getEntireSystemColl();
12 uint256 totalDebt = getEntireSystemDebt();
13 (uint256 price,) = priceFeed.fetchPrice();
14
15 uint256 TCR = LiquityMath._computeCR(totalColl, totalDebt, price);
16 if (TCR >= SCR) revert TCRNotBelowSCR();
17
18 _applyShutdown();
19
20 emit ShutDown(TCR);
21}
22
23...
24
25// Not technically a "Borrower op", but seems best placed here given current shutdown logic.
26function shutdownFromOracleFailure(address _failedOracleAddr) external {
27 _requireCallerIsPriceFeed();
28
29 // No-op rather than revert here, so that the outer function call which fetches the price does not revert
30 // if the system is already shut down.
31 if (hasBeenShutDown) return;
32
33 _applyShutdown();
34
35 emit ShutDownFromOracleFailure(_failedOracleAddr);
36}
37
38...
39
40function _applyShutdown() internal {
41 activePool.mintAggInterest();
42 hasBeenShutDown = true;
43 troveManager.shutdown();
44}
1
2// 关闭系统抵押品比率。如果给定抵押品的系统总抵押品比率 (TCR) 低于 SCR,
3// 该协议会触发借贷市场的关闭,并永久禁用除关闭 Trove 之外的所有借贷操作。
4uint256 public immutable SCR;
5
6...
7
8function shutdown() external {
9 if (hasBeenShutDown) revert IsShutDown();
10
11 uint256 totalColl = getEntireSystemColl();
12 uint256 totalDebt = getEntireSystemDebt();
13 (uint256 price,) = priceFeed.fetchPrice();
14
15 uint256 TCR = LiquityMath._computeCR(totalColl, totalDebt, price);
16 if (TCR >= SCR) revert TCRNotBelowSCR();
17
18 _applyShutdown();
19
20 emit ShutDown(TCR);
21}
22
23...
24
25// 从技术上讲,这并不是一个“Borrower op”,但鉴于当前的关闭逻辑,它似乎是最佳位置。
26function shutdownFromOracleFailure(address _failedOracleAddr) external {
27 _requireCallerIsPriceFeed();
28
29 // 这里使用 No-op 而不是 revert,以便获取价格的外部函数调用不会 revert
30 // 如果系统已经关闭。
31 if (hasBeenShutDown) return;
32
33 _applyShutdown();
34
35 emit ShutDownFromOracleFailure(_failedOracleAddr);
36}
37
38...
39
40function _applyShutdown() internal {
41 activePool.mintAggInterest();
42 hasBeenShutDown = true;
43 troveManager.shutdown();
44}
WETHPriceFeed
1function _fetchPrice() internal override returns (uint256, bool) {
2 (uint256 ethUsdPrice, bool ethUsdOracleDown) = _getOracleAnswer(ethUsdOracle);
3
4 // If the Chainlink response was invalid in this transaction, return the last good ETH-USD price calculated
5 if (ethUsdOracleDown) return (_disableFeedAndShutDown(address(ethUsdOracle.aggregator)), true);
6
7 lastGoodPrice = ethUsdPrice;
8
9 return (ethUsdPrice, false);
10}
11
12...
13
14function _disableFeedAndShutDown(address _failedOracleAddr) internal returns (uint256) {
15 // Shut down the branch
16 borrowerOperations.shutdownFromOracleFailure(_failedOracleAddr);
17
18 priceFeedDisabled = true;
19 return lastGoodPrice;
20}
1function _fetchPrice() internal override returns (uint256, bool) {
2 (uint256 ethUsdPrice, bool ethUsdOracleDown) = _getOracleAnswer(ethUsdOracle);
3
4 // 如果此交易中 Chainlink 响应无效,则返回上次计算的良好 ETH-USD 价格
5 if (ethUsdOracleDown) return (_disableFeedAndShutDown(address(ethUsdOracle.aggregator)), true);
6
7 lastGoodPrice = ethUsdPrice;
8
9 return (ethUsdPrice, false);
10}
11
12...
13
14function _disableFeedAndShutDown(address _failedOracleAddr) internal returns (uint256) {
15 // 关闭分支
16 borrowerOperations.shutdownFromOracleFailure(_failedOracleAddr);
17
18 priceFeedDisabled = true;
19 return lastGoodPrice;
20}
TroveManager
1function shutdown() external {
2 _requireCallerIsBorrowerOperations();
3 shutdownTime = block.timestamp;
4 activePool.setShutdownFlag();
5}
6
7...
8
9function _requireIsShutDown() internal view {
10 if (shutdownTime == 0) {
11 revert NotShutDown();
12 }
13}
14
15...
16
17function _requireIsNotShutDown() internal view {
18 if (hasBeenShutDown) {
19 revert IsShutDown();
20 }
21}
22
23...
24
25function urgentRedemption(uint256 _boldAmount, uint256[] calldata _troveIds, uint256 _minCollateral) external {
26 _requireIsShutDown();
27 ...
28}
1function shutdown() external {
2 _requireCallerIsBorrowerOperations();
3 shutdownTime = block.timestamp;
4 activePool.setShutdownFlag();
5}
6
7...
8
9function _requireIsShutDown() internal view {
10 if (shutdownTime == 0) {
11 revert NotShutDown();
12 }
13}
14
15...
16
17function _requireIsNotShutDown() internal view {
18 if (hasBeenShutDown) {
19 revert IsShutDown();
20 }
21}
22
23...
24
25function urgentRedemption(uint256 _boldAmount, uint256[] calldata _troveIds, uint256 _minCollateral) external {
26 _requireIsShutDown();
27 ...
28}
ActivePool
1function setShutdownFlag() external {
2 _requireCallerIsTroveManager();
3 shutdownTime = block.timestamp;
4}
5
6...
7
8function hasBeenShutDown() external view returns (bool) {
9 return shutdownTime != 0;
10}
11
12...
13
14function calcPendingAggInterest() public view returns (uint256) {
15 if (shutdownTime != 0) return 0;
16 ...
17}
1function setShutdownFlag() external {
2 _requireCallerIsTroveManager();
3 shutdownTime = block.timestamp;
4}
5
6...
7
8function hasBeenShutDown() external view returns (bool) {
9 return shutdownTime != 0;
10}
11
12...
13
14function calcPendingAggInterest() public view returns (uint256) {
15 if (shutdownTime != 0) return 0;
16 ...
17}
BorrowerOperations
中的 _requireNewTCRisAboveCCR
函数可确保在借款操作期间 TCR 保持健康:BorrowerOperations
1function _requireValidAdjustmentInCurrentMode(
2 TroveChange memory _troveChange,
3 LocalVariables_adjustTrove memory _vars
4) internal view {
5 /*
6 * Below Critical Threshold, it is not permitted:
7 *
8 * - Borrowing
9 * - Collateral withdrawal except accompanied by a debt repayment of at least the same value
10 *
11 * In Normal Mode, ensure:
12 *
13 * - The adjustment won't pull the TCR below CCR
14 *
15 * In Both cases:
16 * - The new ICR is above MCR
17 */
18 _requireICRisAboveMCR(_vars.newICR);
19
20 if (_vars.isBelowCriticalThreshold) {
21 _requireNoBorrowing(_troveChange.debtIncrease);
22 _requireDebtRepaymentGeCollWithdrawal(_troveChange, _vars.price);
23 } else {
24 // if Normal Mode
25 uint256 newTCR = _getNewTCRFromTroveChange(_troveChange, _vars.price);
26 _requireNewTCRisAboveCCR(newTCR);
27 }
28}
29
30...
31
32function _requireNewTCRisAboveCCR(uint256 _newTCR) internal view {
33 if (_newTCR < CCR) {
34 revert TCRBelowCCR();
35 }
36}
1function _requireValidAdjustmentInCurrentMode(
2 TroveChange memory _troveChange,
3 LocalVariables_adjustTrove memory _vars
4) internal view {
5 /*
6 * 低于临界阈值,不允许:
7 *
8 * - 借款
9 * - 抵押品提取,除非伴随至少相同价值的债务偿还
10 *
11 * 在正常模式下,确保:
12 *
13 * - 调整不会将 TCR 拉低至 CCR 以下
14 *
15 * 在这两种情况下:
16 * - 新的 ICR 高于 MCR
17 */
18 _requireICRisAboveMCR(_vars.newICR);
19
20 if (_vars.isBelowCriticalThreshold) {
21 _requireNoBorrowing(_troveChange.debtIncrease);
22 _requireDebtRepaymentGeCollWithdrawal(_troveChange, _vars.price);
23 } else {
24 // 如果是正常模式
25 uint256 newTCR = _getNewTCRFromTroveChange(_troveChange, _vars.price);
26 _requireNewTCRisAboveCCR(newTCR);
27 }
28}
29
30...
31
32function _requireNewTCRisAboveCCR(uint256 _newTCR) internal view {
33 if (_newTCR < CCR) {
34 revert TCRBelowCCR();
35 }
36}
TroveManager
1function _batchLiquidateTroves(
2 IDefaultPool _defaultPool,
3 uint256 _price,
4 uint256 _boldInStabPool,
5 uint256[] memory _troveArray,
6 LiquidationValues memory totals,
7 TroveChange memory troveChange
8) internal {
9 ....
10
11 uint256 ICR = getCurrentICR(troveId, _price);
12
13 if (ICR < MCR) {
14 ....
15 }
16}
1function _batchLiquidateTroves(
2 IDefaultPool _defaultPool,
3 uint256 _price,
4 uint256 _boldInStabPool,
5 uint256[] memory _troveArray,
6 LiquidationValues memory totals,
7 TroveChange memory troveChange
8) internal {
9 ....
10
11 uint256 ICR = getCurrentICR(troveId, _price);
12
13 if (ICR < MCR) {
14 ....
15 }
16}
TroveManager
中的 _liquidate
将剩余的抵押品转移到 CollSurplusPool
:1function _liquidate(
2 IDefaultPool _defaultPool,
3 uint256 _troveId,
4 uint256 _boldInStabPool,
5 uint256 _price,
6 LatestTroveData memory trove,
7 LiquidationValues memory singleLiquidation
8) internal {
9 ...
10
11 // Differencen between liquidation penalty and liquidation threshold
12 if (singleLiquidation.collSurplus > 0) {
13 collSurplusPool.accountSurplus(owner, singleLiquidation.collSurplus);
14 }
15
16 ...
17}
1function _liquidate(
2 IDefaultPool _defaultPool,
3 uint256 _troveId,
4 uint256 _boldInStabPool,
5 uint256 _price,
6 LatestTroveData memory trove,
7 LiquidationValues memory singleLiquidation
8) internal {
9 ...
10
11 // 清算处罚和清算阈值之间的差异
12 if (singleLiquidation.collSurplus > 0) {
13 collSurplusPool.accountSurplus(owner, singleLiquidation.collSurplus);
14 }
15
16 ...
17}
借款人可以在 CollSurplusPool 调用 claimColl 来领取其剩余的抵押品:
1function accountSurplus(address _account, uint256 _amount) external override {
2 _requireCallerIsTroveManager();
3
4 uint256 newAmount = balances[_account] + _amount;
5 ...
6}
1function accountSurplus(address _account, uint256 _amount) external override {
2 _requireCallerIsTroveManager();
3
4 uint256 newAmount = balances[_account] + _amount;
5 ...
6}
1function claimColl(address _account) external override {
2 _requireCallerIsBorrowerOperations();
3 uint256 claimableColl = balances[_account];
4 require(claimableColl > 0, "CollSurplusPool: No collateral available to claim");
5
6 ...
7
8 collToken.safeTransfer(_account, claimableColl);
9}
1function claimColl(address _account) external override {
2 _requireCallerIsBorrowerOperations();
3 uint256 claimableColl = balances[_account];
4 require(claimableColl > 0, "CollSurplusPool: No collateral available to claim");
5
6 ...
7
8 collToken.safeTransfer(_account, claimableColl);
9}
_getCollGasCompensation
负责计算补偿金额:1// Return the amount of Coll to be drawn from a trove's collateral and sent as gas compensation.
2function _getCollGasCompensation(uint256 _entireColl) internal pure returns (uint256) {
3 return LiquityMath._min(_entireColl / COLL_GAS_COMPENSATION_DIVISOR, COLL_GAS_COMPENSATION_CAP);
4}
1// 返回要从 trove 的抵押品中提取并作为 gas 补偿发送的 Coll 金额。
2function _getCollGasCompensation(uint256 _entireColl) internal pure returns (uint256) {
3 return LiquityMath._min(_entireColl / COLL_GAS_COMPENSATION_DIVISOR, COLL_GAS_COMPENSATION_CAP);
4}
1function _liquidate(
2 IDefaultPool _defaultPool,
3 uint256 _troveId,
4 uint256 _boldInStabPool,
5 uint256 _price,
6 LatestTroveData memory trove,
7 LiquidationValues memory singleLiquidation
8) internal {
9 ...
10
11 singleLiquidation.collGasCompensation = _getCollGasCompensation(trove.entireColl);
12 uint256 collToLiquidate = trove.entireColl - singleLiquidation.collGasCompensation;
13
14 ...
15}
1function _liquidate(
2 IDefaultPool _defaultPool,
3 uint256 _troveId,
4 uint256 _boldInStabPool,
5 uint256 _price,
6 LatestTroveData memory trove,
7 LiquidationValues memory singleLiquidation
8) internal {
9 ...
10
11 singleLiquidation.collGasCompensation = _getCollGasCompensation(trove.entireColl);
12 uint256 collToLiquidate = trove.entireColl - singleLiquidation.collGasCompensation;
13
14 ...
15}
_sendGasCompensation
函数用于从 ActivePool
或 GasPool
合约中提取 gas,GasPool 合约的唯一目的是持有 gas 储备以支付给清算人。
1function _sendGasCompensation(IActivePool _activePool, address _liquidator, uint256 _eth, uint256 _coll) internal {
2 if (_eth > 0) {
3 WETH.transferFrom(gasPoolAddress, _liquidator, _eth);
4 }
5
6 if (_coll > 0) {
7 _activePool.sendColl(_liquidator, _coll);
8 }
9}
1function _sendGasCompensation(IActivePool _activePool, address _liquidator, uint256 _eth, uint256 _coll) internal {
2 if (_eth > 0) {
3 WETH.transferFrom(gasPoolAddress, _liquidator, _eth);
4 }
5
6 if (_coll > 0) {
7 _activePool.sendColl(_liquidator, _coll);
8 }
9}
1function _getYieldToKeepOrSend(uint256 _currentYieldGain, bool _doClaim) internal pure returns (uint256, uint256) {
2 uint256 yieldToKeep;
3 uint256 yieldToSend;
4
5 if (_doClaim) {
6 yieldToKeep = 0;
7 yieldToSend = _currentYieldGain;
8 } else {
9 yieldToKeep = _currentYieldGain;
10 yieldToSend = 0;
11 }
12
13 return (yieldToKeep, yieldToSend);
14}
1function _getYieldToKeepOrSend(uint256 _currentYieldGain, bool _doClaim) internal pure returns (uint256, uint256) {
2 uint256 yieldToKeep;
3 uint256 yieldToSend;
4
5 if (_doClaim) {
6 yieldToKeep = 0;
7 yieldToSend = _currentYieldGain;
8 } else {
9 yieldToKeep = _currentYieldGain;
10 yieldToSend = 0;
11 }
12
13 return (yieldToKeep, yieldToSend);
14}
1function getDepositorYieldGain(address _depositor) public view override returns (uint256) {
2 uint256 initialDeposit = deposits[_depositor].initialValue;
3
4 if (initialDeposit == 0) return 0;
5
6 Snapshots memory snapshots = depositSnapshots[_depositor];
7
8 uint256 yieldGain = _getYieldGainFromSnapshots(initialDeposit, snapshots);
9 return yieldGain;
10}
1function getDepositorYieldGain(address _depositor) public view override returns (uint256) {
2 uint256 initialDeposit = deposits[_depositor].initialValue;
3
4 if (initialDeposit == 0) return 0;
5
6 Snapshots memory snapshots = depositSnapshots[_depositor];
7
8 uint256 yieldGain = _getYieldGainFromSnapshots(initialDeposit, snapshots);
9 return yieldGain;
10}
1function withdrawFromSP(uint256 _amount, bool _doClaim) external override {
2 ...
3
4 uint256 currentCollGain = getDepositorCollGain(msg.sender);
5 uint256 currentYieldGain = getDepositorYieldGain(msg.sender);
6 uint256 compoundedBoldDeposit = getCompoundedBoldDeposit(msg.sender);
7 uint256 boldToWithdraw = LiquityMath._min(_amount, compoundedBoldDeposit);
8 (uint256 keptYieldGain, uint256 yieldGainToSend) = _getYieldToKeepOrSend(currentYieldGain, _doClaim);
9 ...
10
11 _updateDepositAndSnapshots(msg.sender, newDeposit, newStashedColl);
12 _decreaseYieldGainsOwed(currentYieldGain);
13 _updateTotalBoldDeposits(keptYieldGain, boldToWithdraw);
14 _sendBoldtoDepositor(msg.sender, boldToWithdraw + yieldGainToSend);
15 _sendCollGainToDepositor(collToSend);
16}
1function withdrawFromSP(uint256 _amount, bool _doClaim) external override {
2 ...
3
4 uint256 currentCollGain = getDepositorCollGain(msg.sender);
5 uint256 currentYieldGain = getDepositorYieldGain(msg.sender);
6 uint256 compoundedBoldDeposit = getCompoundedBoldDeposit(msg.sender);
7 uint256 boldToWithdraw = LiquityMath._min(_amount, compoundedBoldDeposit);
8 (uint256 keptYieldGain, uint256 yieldGainToSend) = _getYieldToKeepOrSend(currentYieldGain, _doClaim);
9 ...
10
11 _updateDepositAndSnapshots(msg.sender, newDeposit, newStashedColl);
12 _decreaseYieldGainsOwed(currentYieldGain);
13 _updateTotalBoldDeposits(keptYieldGain, boldToWithdraw);
14 _sendBoldtoDepositor(msg.sender, boldToWithdraw + yieldGainToSend);
15 _sendCollGainToDepositor(collToSend);
16}
在 Liquity V2 去中心化借贷中,推送预言机用于抵押品定价,这可能容易受到攻击。推送预言机用于抵押品定价,这可能容易受到抢跑交易攻击。在这种攻击中,攻击者可以观察到来自预言机的即将到来的价格上涨在 mempool 中,在更新被处理之前执行赎回交易,然后在价格上涨被验证后以更高的价格出售赎回的抵押品。这允许攻击者提取超出典型套利收益的利润。这些漏洞最好通过 L1 & L2 协议审计 来解决。
在 Liquity v1 中,这个问题通过 0.5% 的最低赎回费来缓解,与 Chainlink 的 ETH-USD 预言机更新阈值 0.5% 相匹配。在 v2 中,一些 LST-ETH 预言机具有更大的更新阈值(例如,Chainlink 的 RETH-ETH 为 2%),但由于这些 feed 的稳定性,预计抢跑交易的问题会减少,这些 feed 通常基于心跳而不是价格偏差进行更新。
赎回路由逻辑通过相同的百分比减少每个分支的“外部”债务,其中分支的外部债务计算为:
outside_debt_i = bold_debt_i - bold_in_SP_i
这允许赎回人通过存入他们不希望赎回的分支的 Stability Pool (SP) 来暂时操纵某些分支的外部债务。这种策略将赎回导向赎回人想要的分支,允许他们以可能在外部市场中具有较低滑点的特定 Liquid Staking Tokens (LST) 为目标。
攻击者可以通过将 BOLD 存入不需要的分支的 SP 中,并在同一交易中从选择的分支赎回来实现此目的,这可以通过闪电贷来提供资金。这就是事件与紧急响应 计划的重要性。
解决方案: 目前,没有修复措施,因为:
Liquity v2 中的赎回费用是路径依赖的。这意味着与将相同金额分成多个较小的交易赎回相比,在一个交易中赎回大量 BOLD 会产生更高的费用(假设交易之间系统状态没有变化)。因此,赎回人可能会被激励将赎回分成更小的块,以最大程度地减少总费用。
示例:
可以在此 电子表格 中找到说明此费用结构的示例。
解决方案:
出于以下原因,认为没有必要进行修复:
- 原文链接: threesigma.xyz/blog/lend...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!