DeFi借贷概念 3 - 如何使用协议代币激励用户存款
这篇文章是借贷系列文章的第三篇,看看DeFi借贷协议如何通过奖励来激励参与者。
在以前的文章中,我们探讨了DeFi借贷的两个关键概念:借贷和清算。在这最后这篇文章中,我们将探讨协议如何使用奖励来激励存款人。
本文主要内容:
我们前面已经介绍了一些主要的借贷协议,如 AAVE、Compound 和 Maker,以及像 Euler 这样的小协议。这些协议在没有存款人存款资产的情况下是无法运作的。例如,AAVE 的 aToken
和 Compound 的cToken
都需要大量的流动性来实现借贷。AAVE的 USDT aToken 在撰写本文时就有 超过1.14亿 USDT。协议通过提供健康和稳定的金融生态系统以及包括奖励在内的金融激励措施来竞争储户的资产。
协议通常以自己的协议代币发放奖励,这大致代表了协议中的所有权份额。协议代币的持有者通常有权通过对提案进行投票来参与治理,但治理不在这篇文章的范围内--我们可以就此做一个完整的系列。
从存款人的角度来看,分配存款以赚取协议代币的策略通常被称为流动性挖矿。一些协议允许协议代币的持有者将其代币质押,以获得进一步的奖励。
在下面的章节中,我们将探讨在三个有趣的协议中如何分配协议代币作为奖励:Liquity、AAVE V2和Compound。最后,我们对这些概念进行了归纳,说明这三种实现方式具有相同的基本概念。
Liquity 实现了用稳定币 LUSD 支付的无息、以太币支持的贷款。贷款必须保持110%的固定最低抵押率。该协议是无治理和不可变的。协议代币LQTY使其持有者有权获得协议所赚取的费用份额。
回顾我们之前的文章,当用户的借款余额超过协议定义的阈值,有穿仓风险时,协议使第三方能够在清算中扣押抵押品。这些清算不是自动化进行的,而是委托给复杂的DeFi用户,他们开发智能合约和操作,以清算用户获利。
Liquity 采取了一种不同的方法。它不依靠第三方,而是与 LUSD 一起维持一个稳定池(Stability Pool)。当贷款需清算时,稳定池中的LUSD被用来偿还贷款。这些LUSD被销毁,以太币的抵押品被分配给向稳定池提供LUSD的用户。
Liquity 的清算
由于至少要满足 110% 抵押率,大多数清算对稳定池来说是有利可图的,因为参与者从抵押品中获得的ETH价值超过他们销毁 LUSD 的损失。更多关于Liquity清算的逻辑,请看他们的文档。
Liquity 通过发行协议代币(LQTY)的奖励来进一步激励对稳定池的参与。让我们来看看分配逻辑:
pragma solidity ^0.8.13;
function _getLQTYGainFromSnapshots(uint initialStake, Snapshots memory snapshots) internal view returns(uint) {
/*
* Grab the sum 'G' from the epoch at which the stake was made. The LQTY
gain may span up to one scale change.
* If it does, the second portion of the LQTY gain is scaled by 1e9.
* If the gain spans no scale change, the second portion will be 0.
*/
uint128 epochSnapshot = snapshots.epoch;
uint128 scaleSnapshot = snapshots.scale;
uint G_Snapshot = snapshots.G;
uint P_Snapshot = snapshots.P;
uint firstPortion =
epochToScaleToG[epochSnapshot][scaleSnapshot].sub(G_Snapshot);
uint secondPortion =
epochToScaleToG[epochSnapshot][scaleSnapshot.add(1)].div(SCALE_FACTOR);
uint LQTYGain =
initialStake.mul(firstPortion.add(secondPortion)).div(P_Snapshot).div(DECIMAL_PRECISION);
return LQTYGain;
}
该功能依赖于两个关键概念:
G
代表一个全局累积器,反映协议发出的LQTY
奖励的累积总和。P
是一个全局乘积累加器,跟踪复利存款利率随时间变化的总体乘积。它在LUSD
清算给稳定池参与者带来的损失时减少。以下是该功能的工作原理:
epoch
,scale
,G
和P
。这些值取自用户最后一次更新stake的时间。firstPortion
,用 G_Snapshot
值减去相应epoch 和 scale 的 G
值。这代表在同一 scale 内发生的 LQTY
收益。secondPortion
, 下一个 scale
的 G
值除以 SCALE_FACTOR
来计算secondPortion
。这代表在一个scale
变化间发生的 LQTY
增加。如果没有 scale
变化,secondPortion
将是 0
。firstPortion
和 secondPortion
相加,得到总的LQTYGain
( LQTY
收益)。LQTYGain
乘以用户的初始份额。P_Snapshot
(用户对乘积 P
的快照)。这就得到了用户的最终LQTY
奖励。从一些技术细节中抽象出来,用0来表示快照时的变量值,奖励公式可以总结为::
AAVE 协议代币授予治理权,允许持有者参与决策,例如增加新的代币。
有两种方法可以赚取 AAVE 的奖励:
balanceOf(address)
函数来查看用户的stkAAVE代币的余额。让我们来探讨一下如何计算质押的奖励:
pragma solidity ^0.8.13;
contract StakedTokenV2 {
// ...
IERC20 public immutable STAKED_TOKEN;
IERC20 public immutable REWARD_TOKEN;
// ...
mapping(address => uint256) public stakerRewardsToClaim;
// ...
function getTotalRewardsBalance(address staker) external view returns
(uint256) {
DistributionTypes.UserStakeInput[] memory userStakeInputs =
new DistributionTypes.UserStakeInput[](1);
userStakeInputs[0] = DistributionTypes.UserStakeInput({
underlyingAsset: address(this),
stakedByUser: balanceOf(staker),
totalStaked: totalSupply()
});
return stakerRewardsToClaim[staker].add(_getUnclaimedRewards(staker,
userStakeInputs));
}
}
正如你所看到的,一个用户最新的待定奖励总额并没有直接存储。stakerRewardsToClaim
映射只保存最后一个值,即检查点。为了了解总奖励余额是如何计算的,我们查看_getUnclaimedRewards
:
pragma solidity 0.8.13;
contract AaveDistributionManager {
// ...
struct AssetData {
uint128 emissionPerSecond;
uint128 lastUpdateTimestamp;
uint256 index;
mapping(address => uint256) users;
}
// ...
mapping(address => AssetData) public assets;
// ...
function _getUnclaimedRewards(address user,
DistributionTypes.UserStakeInput[] memory stakes)
internal
view
returns(uint256) {
uint256 accruedRewards = 0;
for (uint256 i = 0; i < stakes.length; i++) {
AssetData storage assetConfig = assets[stakes[i].underlyingAsset];
uint256 assetIndex =
_getAssetIndex(
assetConfig.index,
assetConfig.emissionPerSecond,
assetConfig.lastUpdateTimestamp,
stakes[i].totalStaked
);
accruedRewards = accruedRewards.add(
_getRewards(stakes[i].stakedByUser, assetIndex, assetConfig.users[user])
);
}
return accruedRewards;
}
}
未认领的奖励关键取决于assetIndex
。这是一个经典的基于快照的分配技术,我们在该系列的第一篇文章中讨论过。下面是它的计算方法:
pragma solidity 0.8.13;
contract AaveDistributionManager {
// ...
uint256 public immutable DISTRIBUTION_END;
// ...
uint8 public constant PRECISION = 18;
// ...
function _getAssetIndex(
uint256 currentIndex,
uint256 emissionPerSecond,
uint128 lastUpdateTimestamp,
uint256 totalBalance
) internal view returns(uint256) {
if (
emissionPerSecond == 0 |
totalBalance == 0 |
lastUpdateTimestamp == block.timestamp |
lastUpdateTimestamp >= DISTRIBUTION_END
) {
return currentIndex;
}
uint256 currentTimestamp =
block.timestamp > DISTRIBUTION_END ? DISTRIBUTION_END : block.timestamp;
uint256 timeDelta = currentTimestamp.sub(lastUpdateTimestamp);
return |
emissionPerSecond.mul(timeDelta).mul(10 ** uint256(PRECISION)).div(totalBalance).add(
currentIndex
);
}
}
assetIndex
部分工作方式如下:
如果以下任何一项为真,则返回currentIndex
:
lastUpdateTimestamp = block.timestamp
lastUpdateTimestamp >= DISTRIBUTION_END
emissionPerSecond = 0
totalSupply = 0
。
否则,进行以下计算:
timeDelta
:
block.timestamp <= DISTRIBUTION_END
:timeDelta = block.timestamp- lastUpdateTimestamp
timeDelta = DISTRIBUTION_END - lastUpdateTimestamp
。assetIndex = currentIndex + (emissionPerSecond * timeDelta) / (totalSupply)
。上面的逻辑通过根据时间差 和 释放率(emission rate) 增加适当的数量来更新指数。最后,我们可以回顾一下AAVE的奖励是如何实际分配的:
pragma solidity 0.8 .13;
contract AaveDistributionManager {
function _getRewards(
uint256 principalUserBalance,
uint256 reserveIndex,
uint256 userIndex
) internal pure returns(uint256) {
return
principalUserBalance.mul(reserveIndex.sub(userIndex)).div(10 ** uint256(PRECISION));
}
}
关键参数:
principalUserBalance
:用户的初始质押代币存款。类似于上面Liquity中的initialStake
。reserveIndex
: assetIndex
的当前值,代表奖励分配进度。userIndex
:用户质押时的reserveIndex
的值。最后的方程是这样的:
Compound 对其 COMP 代币协议的奖励与 AAVE 非常相似。我们将用 Compound 用来向存款者分配奖励的函数来说明。有一个用于借款人的类似函数,但它对借款金额而不是存款资金进行汇总。
contract ComptrollerG7 {
// ...
function distributeSupplierComp(address cToken, address supplier) internal {
CompMarketState storage supplyState = compSupplyState[cToken];
uint supplyIndex = supplyState.index;
uint supplierIndex = compSupplierIndex[cToken][supplier];
compSupplierIndex[cToken][supplier] = supplyIndex;
if (supplierIndex == 0 && supplyIndex >= compInitialIndex) {
supplierIndex = compInitialIndex;
}
Double memory deltaIndex = Double({
mantissa: sub_(supplyIndex,
supplierIndex)
});
uint supplierTokens = CToken(cToken).balanceOf(supplier);
uint supplierDelta = mul_(supplierTokens, deltaIndex);
uint supplierAccrued = add_(compAccrued[supplier], supplierDelta);
compAccrued[supplier] = supplierAccrued;
emit DistributedSupplierComp(CToken(cToken), supplier, supplierDelta,
supplyIndex);
}
}
关键参数:
supplierTokens
:用户的存款。supplyIndex
:存款指数的当前值。supplierIndex
:用户存入代币时的存款指数值。再一次,我们获得了一个熟悉的方程式:
上面的等式都可以简化为相同的逻辑:我们将用户所投的代币数量乘以一个累积的总和,这个累积的总和代表了在用户所投代币的时间段内,每个代币所获得的奖励数量。
实现方式不同,但在每种情况下,协议都是根据存款人的存款金额和存款时间来奖励他们的协议代币。
奖励累积的归纳
作者:Tal 研究员 @ smlXL, 感谢 Sam Ragsdale 和为本帖提供建议和反馈的smlXL团队成员。
本翻译由 DeCert.me 协助支持, DeCert.me 的口号是码一个未来
,帮助开发者构建可信的技能履历
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!