本文为eBTC协议的第二次检验与改进,重点介绍了Fork测试与收益分配机制的覆盖,特别是收益故事的实现和验证。文章详细探讨了在Fork测试中遇到的技术挑战和解决方案,包括对mock合约的使用不当引发的问题,以及如何确保收益分配公式的正确性。整篇文章具有较强的技术深度,适合对区块链及DeFi机制有一定了解的读者。
Jun 05, 2024
2
在我们与 eBTC protocol 的第二次合作中,Recon团队增加了他们系统的分叉测试覆盖率,并添加了我们称之为 Yield Story 的收益分配机制的覆盖。
注意: 此次回顾也可通过视频形式 此处 查看。
为了从高层次理解eBTC,我们参考了 Antonio 在第一次合作中的 经验教训 中提供的描述:
感谢你关注Recon!免费订阅以接收新帖子并支持我的工作。
订阅
eBTC 是一种抵押加密资产,软性挂钩比特币价格,建立在以太坊网络之上。它仅由 Lido Staked ETH (stETH) 支持,并建立在 Liquity 的 LUSD 模型之上,以维持其挂钩稳定性。用户通过将 stETH 存入协议,创造抵押债务头寸 (CDP),这使他们能够铸造 eBTC,随后可以在去中心化金融 (DeFi) 中使用,并兑换回 stETH。
在我们与 eBTC 的首次合作中,尽管进行了一些分叉集成测试,但由于时间限制,仍然存在覆盖的空缺,而这些问题在此次第二次合作中得到了处理。
随着整个 eBTC 系统在本次合作期间的部署,我们能够进行端到端的分叉测试,但这引入了与最初覆盖减少相关的挑战,这些挑战通过以下要点中描述的方法得以解决。
在第一阶段的分叉模糊测试中,系统设置中仅使用了其主网部署实现的依赖合约。这使得为本地测试开发的测试套件进行最小重配置成为可能。
然而,由于最初使用 mocks 将一些集成合约包装为更简单的状态值操控接口,当它们被实际实现合约替代时,mock 接口和实现接口之间的差异引入了意外行为。
在预分叉模糊测试套件中,团队实现了 stETH
抵押代币的 mock StETHMock
,它定义了以下 submit
函数:
function submit(uint256 _sharesAmount) external {
_mintShares(msg.sender, _sharesAmount);
}
然而,主网上实际部署的 stETH
合约的 submit
函数有以下实现:
function submit(address _referral) external payable returns (uint256) {
return _submit(_referral);
}
函数签名的差异意味着当用之前的 mock 接口包装函数调用时,会静默还原。这最终导致分叉测试中的覆盖率低于本地测试设置。
为避免此类错误,建议在 mock 和实现合约之间进行差异模糊测试,这有助于提高对所使用的 mock 与实际实现一致性的信心。
在参与的整个端到端分叉模糊测试阶段,团队专注于确保现有的模糊测试套件仍然能够达到与本地和半分叉套件相同的覆盖率。
模糊测试套件的一个关键组成部分是允许模糊器模拟在 eBTC 系统之外的链上操作,这些操作对系统行为有副作用,从而创建出系统状态如何演变的更真实模拟。
在我们的例子中,eBTC 集成了 Lido 的 stETH,作为其收益积累机制的一部分。由于 stETH 是一种再基代币,因此它受到供应变化的影响,持有者的股份在赚取质押奖励时增加,而在发生削减罚款时减少。stETH 供应的这种变化对 eBTC 系统的影响,根本上是相应于 stETH/eBTC 之间的汇率变化,由 eBTC 系统中使用的预言机提供的价格决定。
因此,我们希望允许模糊器以一种现实地模拟再基事件的方式操控预言机返回的价格,因此团队实现了一个函数,当模糊器调用时,直接操控预言机返回的值,以实现再基事件所需的效果,从而操控 stETH 的数量。
实现此目的的最简单方法是操控预言机合约存储槽中保存的价格对应的状态变量值,使用 HEVM store 作弊代码:
function setPrice(uint256 newPrice) public override {
_before(bytes32(0));
// 将当前价格设置为零以触发预言机回退到最后良好价格
hevm.store(
address(priceFeedMock),
0x0000000000000000000000000000000000000000000000000000000000000002,
bytes32(0)
);
// 加载最后良好价格
uint256 oldPrice = uint256(
hevm.load(
address(priceFeedMock),
0x0000000000000000000000000000000000000000000000000000000000000001
)
);
// 计算新价格
newPrice = between(
newPrice,
(oldPrice * 1e18) / MAX_PRICE_CHANGE_PERCENT,
(oldPrice * MAX_PRICE_CHANGE_PERCENT) / 1e18
);
// 通过雕刻最后良好价格设置新价格
hevm.store(
address(priceFeedMock),
0x0000000000000000000000000000000000000000000000000000000000000001,
bytes32(newPrice)
);
cdpManager.syncGlobalAccountingAndGracePeriod();
_after(bytes32(0));
}
使用之前的价格来计算新价格,确保模糊器不会通过利用不切实际的价格变化引入错误的积极结果,这在实际抉择事件中不会发生。
最终,这使团队获得了与本地测试相同程度的覆盖率,并对不断发展的主网链状态进行了分叉测试(Recon 也提供此作为 服务)。
在上述核心功能的分叉测试结束并显示出符合预期的行为后,团队转向测试与 eBTC 收益分配机制相关的属性,我们称之为 Yield Story。
eBTC 通过一种利润收益共享机制分配收益,其中在再基事件中系统中 stETH 赚取的收益的一部分作为费用发送到协议,余下部分分配给协议参与者。利润收益共享(PYS)公式通过使用用户股份计算每个用户应得的金额和从每个用户扣除的金额来实现这一点。
因此,作为收益故事的一部分,我们的目标是证明这个公式实际上是正确的,而不是仅仅在应该证明它的不可变性中复制它的实现。因此,在不可变性中的相应实现使用百分比而不是股份来计算收益分配,以与协议实现相一致的方式进行比较,确保计算出的收益计算相等。
// 作为一个协议,我们期望从时刻到时刻的收益增长在正收益的情况下与 PYS 相等
function invariant_PYS_04(CdpManager cdpManager, Vars memory vars) internal view returns (bool) {
...
if (vars.yieldStEthIndexAfter > vars.yieldStEthIndexBefore) {
uint256 yieldGrowthPercent = (vars.yieldStEthIndexAfter - vars.yieldStEthIndexBefore) * 1e18 / vars.yieldStEthIndexBefore;
// 协议的可索取费用应按 PYS 的比例,期望同步系统抵押股份价值的预期增长
uint256 feesTakenExpected = (vars.yieldProtocolValueBefore * yieldGrowthPercent / 1e18)
* cdpManager.stakingRewardSplit() / cdpManager.MAX_REWARD_SPLIT();
uint256 feesTakenActual = vars.feeRecipientCollSharesAfter - vars.feeRecipientCollSharesBefore;
require((vars.yieldProtocolCollSharesBefore - vars.yieldProtocolCollSharesAfter) == feesTakenActual, "!费用应同步");
feesTakenExpected = collateral.getSharesByPooledEth(feesTakenExpected);
collateral.getSharesByPooledEth(feesTakenActual);
return _assertApproximateEq(feesTakenExpected, feesTakenActual, 1e6);
}
...
}
此外,由于收益将通过任何同步收益增长的函数进行更新,因此在调用同步收益增长之间的任意时间段内,没有很好的方法来测试收益是否正确累积。相反,利用收益因 stETH 的再基事件而累积的事实,我们检查在每个再基事件中收益是否被正确累积。
实施的机制通过以下图表描述:
我们证明了系统内各个再基事件中的收益增长与 PYS 公式在任何给定时间段内的预期收益增长相同。
这一验证在 invariant_PYS_04 中实现,检查每个再基事件的收益增长是否等于 PYS 公式返回的预期值。
这利用了为了证明某些特性,而不是尽力证明最终结果 (整体收益增量),如果我们可以证明对于贡献于收益增量的每个单独事件特性成立,那么我们可以通过归纳得出累积结果也成立。这在实现上不仅更简单,还因为需要更少的测试案例,使我们能够了解系统在多个再基事件中的行为与无限多次的行为相同。
分叉模糊测试对协议行为提供了更大的解析度,但可能会引入自身的复杂性到测试套件中。
应谨慎调整测试套件,以确保覆盖率可与预分叉测试水平相媲美。
如上例所示,并没有固定的方法来实现这一点,这需要运行模糊器以查看可能实际上导致覆盖率下降的因素。
感谢你关注Recon!免费订阅以接收与模糊测试相关的新文章和教程。
- 原文链接: getrecon.substack.com/p/...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!