Compound V3 奖励系统(COMP)详解
目录
- 一、核心机制
- 二、追踪指数(供应与借款)
- 三、最低门槛保护 baseMinForRewards
- 四、速率变量(immutable)
- 五、用户级奖励追踪
- 六、奖励累积公式 accrueInternal
- 七、领取机制 claim
- 八、COMP 代币的怪癖
- 九、完整流程
- 十、总结
- 十一、动手练习项目:MasterChef 式奖励分发 RewardsDistributor
一、核心机制
Compound V3 按参与度成比例地给出借人和借款人分发 COMP 代币。系统紧密借鉴了 MasterChef 质押算法——追踪”自开天辟地以来每单位资产累积赚了多少奖励”。
这和上一篇的利率指数是同一类思想:用一个全局累加器代替逐账户更新,O(1) 省 Gas。
二、追踪指数(供应与借款)
协议维护 trackingSupplyIndex 和 trackingBorrowIndex 作为奖励累加器,衡量”一个假想单位的 USDC 自开始以来赚了多少奖励”。
当总供应或总借款增加时,新赚的奖励要分摊到更多单位上,每单位收益下降——这正是 MasterChef 的精髓:奖励速率固定,参与的人越多,人均越少。
三、最低门槛保护 baseMinForRewards
Compound 在总借款或总出借低于某阈值时不发奖励。
具体地,baseMinForRewards = 1,000,000 USDC(6 位小数下是 1e12)。
为什么?防止累加器溢出。当质押量很小时,“奖励速率 ÷ 质押量”会让累加器增长过快,有数值溢出风险。设最低门槛保证分母足够大。
四、速率变量(immutable)
两个固定参数控制奖励速度:
- baseTrackingSupplySpeed:2.979166666666e-03(每单位时间)
- baseTrackingBorrowSpeed:4.414467592592e-03(每单位时间)
都用 trackingIndexScale(1e15)做定点精度。它们代表”每单位时间的奖励”,乘以经过的时间得到参与资产累积的奖励。注意借款速率 > 供应速率——协议当时更想激励借款。
五、用户级奖励追踪
每个用户维护:
- baseTrackingIndex:用户上次状态变更交易时记录的指数值;
- baseTrackingAccrued:累积已赚奖励 = (当前指数 − 用户存的指数) × 用户余额。
新增累积奖励 = (当前指数 − 用户的指数) × 用户余额
AccrualDescaleFactor:归一化不同小数精度资产的缩放因子。ETH(18 位)的奖励除以 1e12 来匹配 USDC 的 6 位精度,实现统一追踪。
六、奖励累积公式 accrueInternal
核心更新逻辑:
若 totalSupplyBase ≥ baseMinForRewards:
trackingSupplyIndex += (baseTrackingSupplySpeed × 经过时间) / totalSupplyBase
若 totalBorrowBase ≥ baseMinForRewards:
trackingBorrowIndex += (baseTrackingBorrowSpeed × 经过时间) / totalBorrowBase
注意分母是 totalSupplyBase/totalBorrowBase——参与越多,指数涨得越慢,人均奖励越低。低于最低门槛时指数不增长(防溢出)。
七、领取机制 claim
用户在独立的 CometRewards 合约调 claim()(不在主 Comet 合约里)。函数流程:
- 检查
shouldAccrue标志(若 true,先确保累积发生); - 从用户的 Comet 结构体读当前
baseTrackingAccrued; - 减去
rewardsClaimed(追踪之前已领的,类似 MasterChef 的 reward debt); - 把差额转给领取者。
为什么独立合约:COMP 总量固定 1000 万枚(已全部铸造),CometRewards 需要治理定期充值。历史提案显示大约每年补充一次。
八、COMP 代币的怪癖
COMP 代币偏离标准 ERC20:
COMP 不像大多数 ERC20 那样用 uint256 存余额,而是用 uint96。
转账或授权超过 uint96 最大值(约 7.9e28)会 revert,给单个地址持有量设了上限。这是早期为投票打包优化设计的遗留。
九、完整流程
- 用户在 Comet 存款/借款;
accrueInternal()更新追踪指数(若达最低门槛);updateBasePrincipal()按指数差算用户新增累积奖励;- 用户调
claim()领取 COMP,从累积总额扣除已领部分; - CometRewards 合约从治理充值的储备转出代币。
这套架构确保奖励随参与度反比缩放,同时用最低门槛闸门防止数值不稳定。
十、总结
- COMP 奖励用 MasterChef 式追踪指数:每单位资产的累计奖励,参与越多人均越少;
- 供应、借款各一个 trackingIndex,速率是 immutable,借款速率更高;
- baseMinForRewards(100 万 USDC)防累加器溢出;
- 用户存 baseTrackingIndex + baseTrackingAccrued,按指数差算奖励;
- 在独立 CometRewards 合约 claim,治理定期充值 COMP;
- COMP 用 uint96 存余额,是非标准遗留。
十一、动手练习项目:MasterChef 式奖励分发 RewardsDistributor
项目目标
亲手实现 MasterChef 式追踪指数奖励系统:固定速率、参与反比、最低门槛、独立合约 claim。部署到 Sepolia,验证两个用户按存款比例和时长分到奖励。
合约要求
1. StakingComet.sol(简化的存款合约,复用指数记账)
trackingSupplyIndex、baseTrackingSupplySpeed(immutable)、baseMinForRewards、trackingIndexScale = 1e15;mapping(address => uint) principal、mapping(address => uint) baseTrackingIndex、mapping(address => uint) baseTrackingAccrued;accrue():若totalSupply >= baseMinForRewards,trackingSupplyIndex += speed * dt / totalSupply;_updateUser(address):baseTrackingAccrued[u] += (trackingSupplyIndex - baseTrackingIndex[u]) * principal[u] / SCALE,再把baseTrackingIndex[u] = trackingSupplyIndex;supply/withdraw:进出前先 accrue + _updateUser。
2. RewardsClaimer.sol(独立领取合约)
- 持有 RewardToken(你自部署的 COMP 替身)储备;
mapping(address => uint) rewardsClaimed;claim(address comet, address to):读 comet 的 baseTrackingAccrued[to],减 rewardsClaimed[to],转差额,更新 rewardsClaimed;topUp(uint amount):治理充值储备。
测试要求(Foundry,重点 vm.warp)
test_ProportionalRewards:A 存 100、B 存 300,推进时间后 B 的累积奖励是 A 的 3 倍;test_NoRewardsBelowMin:总供应 < baseMinForRewards 时,推进时间 trackingIndex 不增长、无奖励;test_TimeWeighted:A 先存、晚些 B 才存,A 累积更多(参与时间长);test_ClaimTransfers:claim 后 to 收到正确 COMP,rewardsClaimed 更新;test_DoubleClaimNoDouble:连续 claim 两次,第二次几乎为 0(reward debt 机制);test_ParticipationDilutes:新用户大额存入后,老用户的后续每秒奖励变少(指数增速下降);test_TopUpRequired:储备耗尽时 claim revert,topUp 后恢复。
Sepolia 部署与验证步骤
- 部署 RewardToken、StakingComet、RewardsClaimer(充值 RewardToken);
- 两个账户按不同金额 supply(总额超过 baseMinForRewards);
- 隔一段时间读各自 baseTrackingAccrued,验证比例;
- 在 RewardsClaimer 调 claim 领取,Etherscan 确认到账;
- 让一个大户加入,观察老用户后续奖励速率下降。
进阶挑战(可选)
- 同时实现 borrow 侧的 trackingBorrowIndex,让借款人也赚奖励,验证供应/借款独立累积;
- 用 uint96 存奖励余额复刻 COMP 怪癖,测试超过上限 revert;
- 写注释回答:为什么需要 baseMinForRewards?用具体数字说明小质押量如何导致累加器快速增长甚至溢出。