这篇文章详细探讨了 Liquity V2 的去中心化借贷机制,介绍了多种新特性,包括用户设定利率、多抵押品系统以及流动性池的奖励分配等。文章涵盖了 Liquity V2 的架构、功能与安全性视角,对于想要了解去中心化借贷的用户来说,提供了深刻的见解和理论基础。
我们不仅是代码、经济审计和区块链工程的专家——我们热衷于拆解 Web3 概念,以推动行业的清晰度和采用率。
Liquity V1 是在代码的唯一治理下构建的不可变协议。自2021年4月推出以来,它便无缝运作,提供0%的利率,同时确保用户资金的安全。随着 Liquity V2 的发布,该平台大胆前行,引入了一个新特性:用户设定的利率,为借款人和稳定币持有者创造了一个更高效、更具吸引力的市场。
Liquity V2 是一个去中心化的抵押债务平台,允许用户锁定资产如 WETH 或 Liquid Staking Tokens (LSTs) 来发行其稳定币Token,称为 BOLD。BOLD 设计为保持1美元的价值,确保系统始终超额抵押,并且 BOLD 始终可以根据协议的抵押品兑换相应数量的抵押品。
该系统使用户能够通过存入 ERC20 代币作为抵押品来打开称为“Troves”的抵押债务头寸。只要抵押比例保持在最低要求之上,便可以根据抵押品借入 BOLD 代币。BOLD 可以在以太坊地址之间自由转移,并且可以被任何人兑换为减去费用后的等值抵押品。
CollateralRegistry
,一个 BoldToken
,以及为每个抵押“分支”部署的一系列核心系统合约。CollateralRegistry
负责将外部 ERC20 抵押代币映射到相应的 TroveManager
地址。它还处理各个抵押分支间的赎回路由。CollateralRegistry
– 追踪所有 LST 抵押品,并将分支特定的 TroveManagers 映射到这些抵押品。它计算赎回费用并将 BOLD 赎回定向到不同分支的相应 TroveManagers,按其未偿还债务的比例分配。BOLDToken
– 这是实现 ERC20 标准并包括 EIP-2612 许可功能的稳定币代币合约。该合约负责铸造、燃烧和转移 BOLD 代币。BorrowerOperations
– 包含借款人和经理与其 Troves 互动的核心功能,包括创建 Troves、添加或提取抵押品、发行和偿还 BOLD,以及调整利率。BorrowerOperations
的功能调用 TroveManager
更新 Trove 的状态并与各种池进行互动,将抵押品和 BOLD 在池之间或池与用户之间移动。它还指示 ActivePool
凭单利息。TroveManager
– 负责清算、赎回和计算单个 Troves 的利息。它保持每个 Trove 的状态,包括抵押品、债务和利率。然而,TroveManager
本身并不拥有任何价值(抵押品或 BOLD)。它在需要时调用各种池以移动抵押品或 BOLD。TroveNFT
– 为 Trove NFTs 实现基本的铸造和燃烧功能,并由 TroveManager
控制。它还实现了 tokenURI
功能,为每个 Trove 提供元数据,包括唯一图像。LiquityBase
– 包含 CollateralRegistry
、TroveManager
、BorrowerOperations
和 StabilityPool
使用的共享功能。StabilityPool
– 管理稳定池的操作,如存入、提款复利存款、清算获取的抵押品和 BOLD收益。它持有所有存款人在某个分支的稳定池 BOLD 存款、收益和清算抵押品。SortedTroves
– 一个双向链表,存储以 Trove 拥有者地址为索引的地址,按他们的年利率进行排序。它会根据利率自动插入和重新插入 Troves。该合约还处理批量 Troves 的插入和重新插入,建模为双向链表的切片。ActivePool
– 持有一个分支的抵押品余额并跟踪活动 Troves 的总 BOLD 债务。它铸造累计利息,分配给 StabilityPool
和收益路由器(目前为 DEX LP 激励的 MockInterestRouter
)。DefaultPool
– 持有被清算 Troves 的抵押品余额和 BOLD 债务,待分配给活动 Troves。如果一个活跃 Trove 在 DefaultPool
中有待分配的抵押品和债务“奖励”,则将在下一个借款人操作、赎回或清算期间应用到该 Trove。CollSurplusPool
– 追踪和持有来自清算 Troves 的抵押品盈余。当借款人申请时,它分配借款人累积的盈余。GasPool
– 管理 WETH Gas补偿。当打开 Trove 时,WETH 从借款人转移到 GasPool
,在 Trove 被清算或关闭时支付。MockInterestRouter
– 一个占位合约,当前接收铸造利息的 LP 收益分配。稍后将由真实的收益路由器替换,该路由器将收益引导至 DEX LP 激励。HintHelpers
– 一个辅助合约,提供读取功能以计算准确的提示,这些提示可以提供给借款人操作。MultiTroveGetter
– 一个辅助合约,提供读取功能以获取 Trove 数据结构数组,包含每个 Trove 的完整记录状态。PriceFeed
合约来给不同分支的抵押品定价。然而,核心功能在父合约之间共享。更多信息可以在 这里 阅读。
CollateralRegistry
和多个抵押分支组成。每个分支都可以独立配置自己的 最低抵押比例(MCR)、临界抵押比例(CCR)和 关闭抵押比例(SCR)。每个分支还有自己的 TroveManager
和 StabilityPool
,在某个分支里,Troves 仅接受单一类型的抵押品。一个分支中的清算仅对应于其相应的稳定池,清算时的任何收益都以该特定抵押品的形式支付给存款者。清算中的抵押品和债务的重分配也只适用于同一分支内的活动 Troves。CollateralRegistry
的构造函数可以接受最多10个 TroveManagers
及其各自的抵押类型。然而,目前仅接受 WETH 和两个 LST——rETH 和 wstETH,原生 ETH 不被接受。1constructor(IBoldToken _boldToken, IERC20Metadata[] memory _tokens, ITroveManager[] memory _troveManagers)
1/* 按它们各自选择的利率加权的单个记录的 Trove 债务总和。
2* 在个别 Trove 操作时更新。
3* "S" 在规范中。
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}
_mintAggInterest
中的 ActivePool
1function _mintAggInterest(IBoldToken _boldToken, uint256 _upfrontFee) internal returns (uint256 mintedAmount) {
2 mintedAmount = calcPendingAggInterest() + _upfrontFee;
3
4 // 将部分 BOLD 利息铸造成 SP,部分铸造给流动性提供者的路由器。
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 中被调用,该函数由所有改变状态的用户操作触发,包括借款人操作、清算、赎回和稳定池的存款/提款。如果用户的操作改变了 Trove 的债务,则通过待积分的总利息和净 Trove 债务变化更新聚合记录债务。
1function mintAggInterestAndAccountForTroveChange(TroveChange calldata _troveChange, address _batchAddress)
2 external
3{
4 _requireCallerIsBOorTroveM();
5
6 // 批量管理费用
7 if (_batchAddress != address(0)) {
8 _mintBatchManagementFeeAndAccountForChange(boldToken, _troveChange, _batchAddress);
9 }
10
11 // 这里做两步计算以避免从减少中溢出
12 uint256 newAggRecordedDebt = aggRecordedDebt; // 1 SLOAD
13 newAggRecordedDebt += _mintAggInterest(boldToken, _troveChange.upfrontFee); // 添加已铸造的聚合利息 + 前期费用
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 // 如果 TCR 超过关闭阈值且分支未关闭,则可赎回
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 // 收集并积累未担保部分
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 // 计算每个抵押的赎回金额,并根据相应的 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 // 从抵押比率最低的 Troves 开始循环直到将 _amount 的 BOLD 兑换为抵押品
11 if (_maxIterations == 0) _maxIterations = type(uint256).max;
12 while (singleRedemption.troveId != 0 && remainingBold > 0 && _maxIterations > 0) {
13 _maxIterations--;
14 // 保存当前 Trove 前面的 uint256
15 uint256 nextUserToCheck = sortedTrovesCached.getPrev(singleRedemption.troveId);
16 // 如果 ICR < 100%,跳过,以确保赎回总是改善当前 Troves 的 CR
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 // 如若过小,则使得 Trove 不可赎回,以防止未来(正常,顺序)赎回的破坏攻击
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 时被 铸造,并在 关闭 Trove 时被 燃烧。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成本,批量的处理方式类似于“共享 Troves”,系统跟踪批量的总债务和利率。利息和管理费用随时间累积,而每个 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}
当新批量的利率被更新时,将自动影响该批中的所有 Troves:
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
(https://github.com/liquity/bold/blob/a34960222df5061fa7c0213df5d20626adf3ecc4/contracts/src/TroveManager.sol#L1720)函数在管理单个 Troves 及其所属批量之间的关系中起着关键作用。此函数确保与 Trove 相关的债务和抵押品在相应的批量中准确反映。
onOpenTroveAndJoinBatch()
、onAdjustTroveInsideBatch()
、onRegisterBatchManager()
、onLowerBatchManagerAnnualFee()
、onSetBatchManagerAnnualInterestRate()
、onSetInterestBatchManager()
和 onRemoveFromBatch()
负责在它们属于一个批量时更新它们,并更新相关的批量经理变量。
在进行赎回时,对于属于批量的 Troves 进行特定处理:
1if (isTroveInBatch) {
2 _getLatestBatchData(_singleRedemption.batchAddress, _singleRedemption.batch);
3 // 我们知道 boldLot <= trove 整个债务,因此这个减法是安全的
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 // 生成批量管理费用
6 troveChange.batchAccruedManagementFee = batch.accruedManagementFee;
7 activePool.mintBatchManagementFeeAndAccountForChange(troveChange, batchAddress);
8}
当然,这并不是批量考虑的唯一情况,但它们提供了一个很好的例子。
BorrowerOperations
合约中的 shutdown
函数,该函数会关闭所有其他分支合约,如 TroveManager
和 ActivePool
。关闭的另一个场景是在预言机失败时(回退或返回0)。在这种情况下,继承自 MainnetPriceFeedBase
的合约通过其 _disableFeedAndShutDown
函数调用 BorrowerOperations
中的 shutdownFromOracleFailure
函数以触发关闭。
TroveManager
执行,并仅影响该分支,不经过其他分支。BorrowerOperations
1
2// 系统关闭抵押比例。在给定的抵押品的系统总抵押比例(TCR)低于 SCR 时,
3// 协议触发借款市场的关闭,永久禁用所有借款操作,仅允许关闭 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// 技术上并不是“借款人操作”,但似乎由于当前的关闭逻辑放在这里比较合适。
26function shutdownFromOracleFailure(address _failedOracleAddr) external {
27 _requireCallerIsPriceFeed();
28
29 // 为了避免反转即使系统已关闭,也不让外部函数调用获取价格时反转
30 if (hasBeenShutDown) return;
31
32 _applyShutdown();
33
34 emit ShutDownFromOracleFailure(_failedOracleAddr);
35}
36
37...
38
39function _applyShutdown() internal {
40 activePool.mintAggInterest();
41 hasBeenShutDown = true;
42 troveManager.shutdown();
43}
WETHPriceFeed
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}
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}
```- **恢复模式的移除**:之前的恢复模式逻辑已经被移除。只有当个人抵押品比率(ICR)低于最低抵押品比率(MCR)时,才会对借款进行清算。然而,当总抵押品比率(TCR)低于特定分支的临界抵押品比率(CCR)时,仍然适用借贷限制。这是因为某些操作可能会降低 ICR 进而使 TCR 低于 CCR。当 TCR 低于 CCR 时,借贷受到限制,并且在没有等值或更大价值的债务偿还的情况下,设定利率或提取抵押品等操作也受到限制。在 `BorrowerOperations` 中的 `_requireNewTCRisAboveCCR` 函数确保在借款操作期间 TCR 始终健康:
### `BorrowerOperations`
```solidity
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}
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 // 清算罚金与清算阈值之间的差异
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 claimColl(address _account) external override {
2 _requireCallerIsBorrowerOperations();
3 uint256 claimableColl = balances[_account];
4 require(claimableColl > 0, "CollSurplusPool: 没有可认领的抵押品");
5
6 ...
7
8 collToken.safeTransfer(_account, claimableColl);
9}
_getCollGasCompensation
:1// 返回从一个抵押品的抵押中提取并作为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}
_sendGasCompensation
函数用于从 ActivePool
或 GasPool
合约中提取Gas费用,这个合约的唯一目的就是为清算人预留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 _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 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 v1 中,这个问题通过 0.5% 的最低赎回费用得到了缓解,该费用匹配了 Chainlink 的 ETH-USD 预言机的 0.5% 更新阈值。在 v2 中,一些 LST-ETH 预言机的更新阈值较大(例如 Chainlink 的 RETH-ETH 为 2%),但由于这些数据源通常基于心跳而不是价格偏离而更新,因此预期抢跑问题将会减少。
赎回路由逻辑通过相同的比例减少每个分支的“外部”债务,分支的外部债务计算为:
outside_debt_i = bold_debt_i - bold_in_SP_i
这使得赎回者通过将资金存入其不希望赎回的分支的稳定池(SP)来暂时操控特定分支的外部债务。这种策略将赎回引向赎回者希望的分支,使他们能够针对潜在在外部市场中滑点更低的特定 Liquid Staking Tokens(LST)。
攻击者可以通过向不想要的分支的 SP 存入 BOLD,并在同一笔交易中从选择的分支赎回,来实现这一目标,可能资金通过闪电贷提供。
解决方案:目前没有解决措施,因为:
Liquity v2 中的赎回费用是路径依赖的。这意味着,在一笔交易中赎回大量的 BOLD 会产生比在多笔小额交易中赎回同样数量的 BOLD 更高的费用(假设交易之间系统状态没有发生变化)。因此,赎回者可能会受到激励,将赎回拆分为较小的部分,以最小化总费用。
示例:
说明此费用结构的示例可以在此 电子表格 找到。
解决方案:
认为没有必要修复以下原因:
当预言机故障触发分支关闭时,该分支的 PriceFeed 的 fetchPrice
函数默认为该分支 LST 的记录 lastGoodPrice。在关闭后,此价格将用于紧急赎回,即使真实市场价格可能不同,这可能导致潜在的扭曲:
解决方案状态:目前没有解决方案,原因如下:
Liquity v2 会监控市场预言机的答案是否过时,如果它们超过预设阈值,将触发相应分支的关闭。然而,在最后一次预言机更新与分支关闭之间,系统持续使用最近的(可能过时的)预言机价格。这可能导致价格差异,从而引发以下扭曲:
解决方案:为最小化这些风险,必须精心选择过时阈值参数。更易波动的价格反馈如 ETH-USD 和 STETH-USD 应该有比 LST-ETH 反馈更低的过时阈值,因为 USD 反馈更容易出现显著的价格偏差。所有过时阈值应大于推送预言机的更新心跳时间,以避免过早关闭。
总体而言,Liquity V2 为 DeFi 领域带来了新的创新,使其已经成功的前身在借贷协议中更具竞争力。在原有坚实的基础上,Liquity v2 凭借其新特性和改进,有望取得成功,为用户提供的好处无疑会引起广泛共鸣。我们期待 Liquity 团队的平稳和成功发布!
请联系我们的 Three Sigma,让我们经验丰富的专业团队自信地引导你穿越 Web3 领域。凭借我们在智能合约安全、经济模型和区块链工程方面的专业知识,我们将帮助你确保项目的未来。
今天就 联系我们,将你的 Web3 理想变为现实!
https://github.com/liquity/dev/blob/main/README.md
https://www.liquity.org/blog/liquity-v2-enhancing-the-borrowing-experience
https://www.liquity.org/blog/liquity-v2-why-user-set-interest-rates
- 原文链接: threesigma.xyz/blog/liqu...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!