本文是对 Yield Variable Rate 智能合约协议的安全审查报告,由 Christos Pap 完成。报告详细记录了在审查过程中发现的漏洞、问题和代码改进建议,包括高、中、低风险等级的问题,并提出了相应的修复或缓解措施。主要涉及合约代码的精度损失、不准确的退款逻辑以及与EIP3156标准兼容性等问题。
Yield Variable Rate 智能合约协议的安全审查由 Christos Pap 完成。\ 此安全审查报告包括在安全审查期间发现的所有漏洞、问题和代码改进。
“审计是一项受时间、资源和专业知识约束的工作,训练有素的专家使用自动化和手动技术相结合的方法来评估智能 合约,以尽可能多地发现漏洞。审计可以显示漏洞的存在,但不能证明其不存在。”
Christos Pap 是一位独立的安全性研究员,专长是 Ethereum 智能合约安全。他目前在 Spearbit 担任初级安全性研究员,并且是 yAcademy 奖学金计划的校友。此外,他还是电气和计算机工程专业的本科生,并且在进攻性安全方面拥有丰富的经验,曾担任渗透测试员并持有 OSCP 认证。你可以在 Twitter 上与他联系,@christos_eth。
| 严重程度 | 影响:高 | 影响:中等 | 影响:低 |
|---|---|---|---|
| 可能性:高 | 危急 | 高 | 中等 |
| 可能性:中等 | 高 | 中等 | 低 |
| 可能性:低 | 中等 | 低 | 低 |
| 项目名称 | Yield 协议 |
| 仓库 | https://github.com/yieldprotocol/vault-v2 |
| Commit hash | 1d1602a06fda352f463b6f126c8a90e05e221541 |
| 文档 | https://docs.yieldprotocol.com/ |
| 方法 | 手动审查 |
| 严重程度 | 计数 |
|---|---|
| 危急风险 | 0 |
| 高风险 | 2 |
| 中等风险 | 1 |
| 低风险 | 3 |
| 信息 | 5 |
| 文件 | nSLOC |
|---|---|
| 合约 (6) | |
| src/variable/VRCauldron.sol | 297 |
| src/variable/VRLadle.sol | 210 |
| src/variable/VRRouter.sol | 18 |
| src/variable/VRWitch.sol | 57 |
| src/variable/VYToken.sol | 154 |
| src/oracles/VariableInterestRateOracle.sol | 149 |
| 接口 (2) | |
| src/variable/interfaces/IVRCauldron.sol | 22 |
| src/variable/interfaces/IVRWitch.sol | 78 |
| 总计 (8) | 985 |
| 编号 | 标题 | 严重程度 | 解决方案 |
|---|---|---|---|
| [H-01] | VariableInterestRateOracle:get 函数中的精度损失会影响 Yield Variable Rate 协议的利率 |
高 | 已修复 |
| [H-02] | 不准确的退款逻辑导致底层 Join 合约中的错误会计 | 高 | 已修复 |
| [M-01] | VyToken 中的 Name、Symbol 和 Decimals 将具有默认值 |
中等 | 已修复 |
| [L-01] | 如果现货预言机出现故障,包括清算在内的大部分 Yield Variable Rate Protocol 可能会被冻结 |
低 | 已确认 |
| [L-02] | 偏离 EIP3156 标准可能会影响可组合性 |
低 | - |
| [L-03] | 攻击者可以强制发出带有错误 holder 参数的 Redeemed 事件 |
低 | 已确认 |
| [I-01] | 没有津贴抢跑缓解措施 | 信息 | 已确认 |
| [I-02] | TransferHelper 库在执行转账之前不验证 token 代码大小 |
信息 | 已确认 |
| [I-03] | 函数参数中缺少输入验证 | 信息 | 已确认 |
| [I-04] | 注释和 NatSpec 文档中存在印刷错误和缺少数据 | 信息 | 已修复 |
| [I-05] | 函数排序不遵循 Solidity 风格指南 | 信息 | 已确认 |
VariableInterestRateOracle:get 函数中的精度损失会影响 Yield Variable Rate 协议的利率上下文: VariableInterestRateOracle.sol#L200-L204, VariableInterestRateOracle.sol#L206-L212, VariableInterestRateOracle.sol#L194-L196
描述: 代码中使用的利率计算公式基于 AAVE 使用的公式。
如果 utilizationRate <= rateParameters.optimalUsageRate,则利率计算如下:
interestRate = rateParameters.baseVariableBorrowRate +
utilizationRate.wmul(rateParameters.slope1).wdiv(rateParameters.optimalUsageRate
但是,由于 wmul 和 wdiv 函数一起使用,因此计算执行如下:
interestRate = rateParameters + utilizationRate * rateParameters.slope1 / 1e18 * 1e18 / rateParameters.optimalUsageRate。
这会导致精度损失,因为在乘法 (* 1e18) 之前 执行除法 (/ 1e18)。
当 utilizationRate > rateParameters.optimalUsageRate 时,也会发生类似的问题。
建议的缓解措施: 为了避免 get 函数中的精度损失,建议按如下方式调整计算:
interestRate = rateParameters.baseVariableBorrowRate + utilizationRate * rateParameters.slope1 / rateParameters.optimalUsageRate;
Yield 团队: 我们现在已经删除了 wmul 和 wdiv 函数的用法。
Christos Pap: 已验证。
上下文: VRLadle.sol#L381, Join.sol#L44
描述: VRLadle 合约中的 repay 函数可供用户用来偿还金库中的所有债务。 根据函数注释,剩余的基础货币将返回给 msg.sender。
但是,repay 函数中的退款逻辑存在缺陷。 在减少底层 Join 中的 storedBalance 时,会向用户退款,从而导致 Join 合约中的会计中断,因为当退还剩余资金时,storedBalance 将会越来越小。
攻击者可能会通过向 Join 合约发送大量资金,然后调用 repay 函数来利用此问题,从而导致 storedBalance 显着减少。
Join 合约的 exit 函数:
/// @dev Transfer `amount` `asset` to `user`
function exit(address user, uint128 amount) external virtual override auth returns (uint128) {
return _exit(user, amount);
}
/// @dev Transfer `amount` `asset` to `user`
function _exit(address user, uint128 amount) internal virtual returns (uint128) {
IERC20 token = IERC20(asset);
storedBalance -= amount;
token.safeTransfer(user, amount);
return amount;
}
建议的缓解措施: 为了解决此问题,建议向 Join 合约添加一个新函数,该函数检索未入账金额。 应调用此函数而不是 exit 函数,以确保维护正确的会计记录。
Yield 团队: 发现得好。 我认为 Join 需要一个 skim 函数,该函数允许获取实际余额和存储余额之间的差额,类似于此。
Christos Pap: 已验证。 该问题已通过引入 skim 函数来修复,该函数检索未入账金额。
Name、Symbol 和 Decimals 将在 VyToken 中具有默认值上下文: VYToken.sol#L41
描述: yield-utils-v2 ERC20 实现未将 decimals、string 和 symbol 声明为不可变的。 由于 VYToken 合约旨在可升级,因此 ERC20 实现的构造函数代码不会在初始化期间执行,从而导致默认值。 因此,VYToken 具有 0 位小数,这与底层 token 不同。
由于基于代理的可升级性系统的要求,在可升级合约中不能使用任何 constructors。 不可变将起作用,因为它们不存储在存储中,并且编译器会将它们放置在部署中的字节码中。
如果我们运行以下代码片段,我们可以看到 VYToken 具有 0 位小数,这与预期行为不同。
function testDecimals() public {
console.log("underlying is:", vyToken.underlying());
console.log(vyToken.decimals());
console.log("Name is", vyToken.name());
}
建议的缓解措施: 建议还在 yield-utils-v2 ERC20 token 中将 decimals、string 和 symbol 标记为不可变的。 或者,可以使用 initialize 函数 来设置这些值。
Yield 团队: 通过使用 initialize() 函数来设置 decimals、string 和 symbol 变量来修复。
Christos Pap: 已验证。
Yield Variable Rate Protocol 可能会被冻结上下文: VRCauldron.sol#L139, ChainlinkMultiOracle.sol#L16
描述: VRCauldron 合约中的 setSpotOracle 函数将 ChainlinkMultiOracle 合约 的实例设置为 IOracle。 但是,ChainlinkMultiOracle 合约 使用单个预言机来获取最新的价格提要。
根据 OpenZeppelin 的 智能合约安全指南 #3:价格预言机的危险,
虽然目前没有允许或禁止合约读取价格的白名单机制,但强大的多重签名可以加强这些访问控制。 换句话说,多重签名可以随意立即阻止对价格提要的访问。 因此,为了防止拒绝服务的情况发生,建议使用 Solidity 的 try/catch 结构 以防御性方法查询 ChainLink 价格提要。 这样,如果对价格提要的调用失败,则调用者合约仍然可以控制并可以安全且明确地处理任何错误。
在极端情况下,Chainlink 已将预言机脱机,例如在 UST 崩溃期间,它暂停了 UST/ETH 价格预言机,以防止协议接收到不准确的数据。
建议的缓解措施: 为了防止访问 Chainlink 提要的可能性被拒绝,建议实施一项保护措施,例如备用预言机或可以在需要时采取的替代方法。
Yield 团队: 在极端情况下,可以执行提案以更改 spotOracle。 由于在极端情况下存在风险,我们将坚持使用当前的缓解方法。
Christos Pap: 已确认。
EIP3156 标准可能会影响可组合性上下文: VYToken.sol#L248, VYToken.sol#L181-L195
描述: VYToken 与 EIP3156 标准不完全兼容。
根据 Lender Specification,指出:
在回调之后,flashLoan 函数必须从接收者处获取金额 + 费用 token,或者如果未成功,则恢复。
但是,由于 VYToken 合约中的自定义 _burn 函数,如果合约中有一些资金,则金额从合约中获取。
function _burn(address holder, uint256 principalAmount) internal override returns (bool) {
// 第一步是使用锁定在此合约中的任何 token
uint256 available = _balanceOf[address(this)];
if (available >= principalAmount) {
return super._burn(address(this), principalAmount);
} else {
if (available > 0) super._burn(address(this), available);
unchecked {
_decreaseAllowance(holder, principalAmount - available);
}
unchecked {
return super._burn(holder, principalAmount - available);
}
}
}
建议的缓解措施: 为了使 VYToken 合约完全兼容 EIP3156 标准,建议删除 custom _burn 函数并相应地修改 VYToken。
Yield 团队: 嗨,你说得对,这与书面标准有所偏差。 但是,当我在编写它时,我打算允许这样做(与 4626 中的相同)。
这意味着如果借款人批准还款,那么它应该始终有效。 但是,如果贷方也有其他还款方式,并且借款人决定使用它,则不应有任何障碍。
感谢你提出这个问题,我将修改 3156 中的措辞,而不是在此处更改代码。
Christos Pap: 已验证。
上下文: VYToken.sol#L111, VYToken.sol#L143
描述: 通过使用 VyToken 合约中存在的任何 token,custom _burn 函数允许用户将 vyToken 转移到合约以启用 burn,从而可能节省 approve 或 permit 的成本。
但是,此功能可能会被攻击者利用,因为合约中的 withdraw 和 redeem 函数允许用户将 holder 地址作为参数输入。 攻击者可以将 token 直接发送到合约,然后在 withdraw /redeem 函数中传递任何地址作为 holder 参数,这将强制该地址在 Redeemed 事件中发出。
建议的缓解措施: 在当前设置下,很难缓解此问题。 一种可能的解决方案是在 withdaw/redeem 函数中检查批准。
Yield 团队: 我们的用户被指示严格使用前端。 因此,我们不会对此进行更改。
Christos Pap: 已确认。
上下文: VYToken.sol#L16
描述: VYToken 合约继承自 ERC20Permit,后者又继承自 ERC20 合约。 这些合约是 yield-utils-v2 GitHub 仓库的一部分。
这些合约均未提供针对 allowance front-running attack 的保护。 当 token 所有者授权另一个帐户代表他们转移特定数量的 token 时,可能会发生此攻击。 如果 token 所有者决定更改津贴金额,则消费方可以通过抢跑津贴更改交易来花费所有津贴。
建议 为了缓解此问题,你可以考虑使用 OpenZeppelin ERC20 实现。 此实现包括 [increaseAllowance](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/8d633cb7d169f2f8#### [i-03] 函数参数中缺少输入验证
背景: VRCauldron.sol#L114,VRCauldron.sol#L149,VYToken.sol#L66
描述: Variable Rate Yield 项目的一些函数缺少阈值检查,这可能在某些情况下导致意外行为。
建议的缓解措施: 为了解决这些问题,建议采取以下缓解措施:
setDebtLimits 中,你可以添加一个 require 语句,要求 min < max 或 min <= max。setSpotOracle 函数中,可以添加一个 require 语句,要求 ratio <= 1000000。setFlashFeeFactor 函数中,可以添加一个阈值作为 fee 值的上限。Yield 团队: 我们依靠成熟的治理流程来防止上述问题。
Christos Pap: 已知悉。
背景: VRCauldron.sol#L110,VRLadle.sol#L220,VRLadle.sol#L111,VariableInterestRateOracle.sol#L12,VYToken.sol#L20,VYToken.sol#L21,VRLadle.sol#L366
描述: 在审计期间,发现注释和 NatSpec 文档中存在多个拼写错误和数据缺失。请参阅“建议的缓解步骤”部分以获取详细列表。
建议的缓解措施:
address -> address 的类型转换是不必要的。implemnting 应该为 implementing。addToken 也可以用于移除 token,因此可以考虑将 TokenAdded 事件重命名为 TokenStatusChanged。NatSpec 文档。Point 事件未在 VYToken 中使用。可以考虑将其删除。repay 函数中的 comment 是错误的。考虑将其替换为:The surplus base will be returned to the refundTo address, if refundTo is different than address(0)(如果 refundTo 与 address(0) 不同,剩余的基础将被返回到 refundTo 地址)。Yield 团队: 已修复。
Christos Pap: 已验证。
背景: VRWitch.sol#L15,VRCauldron.sol#L13,VRLadle.sol#L21,VYToken.sol#L16
描述: Solidity 中推荐的函数顺序,如 Solidity 风格指南 中所述,如下所示:constructor(),receive(),fallback(),external,public,internal 和 private。但是,这种排序并没有通过 Variable Rate 代码库强制执行。
建议的缓解措施: 建议遵循 Solidity 风格指南 中概述的 Solidity 中推荐的函数顺序。
Yield 团队: 我们将坚持我们当前的风格。
Christos Pap: 已知悉。
nonReentrant() modifier 的保护。如果支持任何 ERC777 token,则可能会引入重入(相同函数或跨函数)。
协议团队回应:
Token 以个案方式支持。到目前为止,我们尚未支持任何 erc777 token,也没有立即计划这样做。如果我们要这样做,我们将在当时调查后果。
- 原文链接: github.com/christos-eth/...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!