本文是对 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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!