code4rena-Kinetiq
高 缓冲区达到最大限额,取消资金大于合约的资金
质押协议允许用户质押代币并赚取奖励。当用户希望提取他们的代币时,他们会调用 queueWithdrawal()
来启动提取流程。管理者可以通过调用 cancelWithdrawal()
来取消待处理的提取请求,这会将 KHYPE 代币返还给用户。为了正确完成取消流程,管理者必须调用 redelegateWithdrawnHYPE()
来更新协议状态,包括缓冲区和其他操作。
然而,存在一个限制:一旦达到缓冲区阈值,代币会从 EVM 层移动到 L1 spot 余额,但redelegateWithdrawnHYPE()
要求 address(this).balance >= cancelledWithdrawalAmount
。由于这些转移的资产在合约层面不再可访问,但仍然计入已取消的提取金额中,因此该条件在数学上变得不可能满足。尽管有概念验证(POC),但让我举个例子:
质押管理器合约的目标缓冲区为 3 HYPE。Alice、Bob、Sage 和 Tony 各质押了 1 HYPE,这自动达到了目标缓冲区,剩余部分将进入 L1 spot 余额。从这一点可以看出,余额不再是 4 HYPE,而是在调用 _distributeStake()
中的以下代码后大约为 3 HYPE:
(bool success,) = payable(L1_HYPE_CONTRACT).call{value: amount}(""); |
随后,Alice、Bob、Sage 和 Tony 都发起了提取请求。管理者看到后调用了这 4 个待处理提取请求的 cancelWithdrawal()
,根据 hypeAmount,取消金额为 4 HYPE。在通过重新委托完成状态更新时,管理者调用了 redelegateWithdrawnHYPE()
。由于 address(this).balance >= cancelledWithdrawalAmount,
这将回滚。为什么呢?因为 cancelAmount 是 4 HYPE,而质押合约的 HYPE 余额只有 3 HYPE,因为其余部分已经发送到了 L1 合约。
影响
已取消的资产无法重新委托,这将影响整个协议的会计核算。
缓冲区管理崩溃。
高 质押函数没有限制谁调用
只要向StakingManger
发送HYPE(使用call,transfer)都会立即触发receive的函数进行质押
receive() external payable { |
中 撤回函数中缺少暂停修饰符
StakingManager::confirmWithdrawal()
函数未包含 whenWithdrawalNotPaused 修饰符
,尽管该函数明确属于“提现操作”。而根据 pauseWithdrawal() 函数的 Natspec 文档说明,其用途是:
/** |
当前 confirmWithdrawal
函数定义如下:
function confirmWithdrawal(uint256 withdrawalId) external nonReentrant whenNotPaused { |
✅ 问题在于:缺少 whenWithdrawalNotPaused
修饰符,导致即使在协议执行 pauseWithdrawal()
后,用户依然可以继续执行提现确认。
这与“暂停提现”在设计上的初衷相悖 —— 按照预期,当协议暂停提现时,所有提现相关流程都应停止,以应对协议风险、紧急状态或攻击场景。
中 取消提款时,没有更新另一个排队
存在漏洞StakingManager
合约,如果提款在 L1 上仍处于等待处理状态时被取消。这引入了一种可能性,即用户可以为同一质押触发多个 L1 提款作,可能会提取比最初存入的更多的 HYPE 并破坏协议会计。
详细说明
当用户将提款排队时,StakingManager
会对 L1作进行排队,以从验证者那里提取 HYPE。至关重要的是,如果稍后在调用 processL1Withdrawals
之前取消了此提款,则提款请求将在 L2(StakingManager) 上删除,但相应的 L1 取消委托给 staker 的作仍保留在队列中。这可能导致 L2 状态和 L1 状态之间不同步。
为了了解如何作,我们来检查 cancelWithdrawal
函数中的相关代码:
function cancelWithdrawal(address user, uint256 withdrawalId) external onlyRole(MANAGER_ROLE) whenNotPaused { |
请注意,此函数仅通过删除提现请求并将 kHYPE 代币返回给用户来更新 L2 状态。但是,它没有一种机制来知道 undelegate 请求是否仍处于排队状态。
因此,当作员稍后使用processL1Operations
处理 L1作队列时,将执行所有作,包括来自已取消提款的作。
然后,可以按如下方式演示此漏洞:
用户质押 1 ETH,获得 1 kHYPE。
用户排队退出(L1 Undelegate 命令 #1 已添加到队列)。
提款由于某种原因被经理取消(但 L1 命令 #1 仍在队列中),但重要的是,在所有待处理的提款(包括已取消的提款)通过 processL1Withdrawals 处理之前。
用户立即使用他从取消中获得的相同 kHYPE 将另一次提款排队(L1 命令 #2 添加到队列中)。
命令 #1 和命令 #2 最终都通过调用 processL1Operations 进行处理。
结果:当只有 1 个 ETH 被质押时,2 个 ETH 从验证者那里提取,造成了会计不一致。
建议的缓解步骤
一种可能的缓解措施是从 _pendingWithdrawals
数组中删除已取消的提款取消委派(如果存在),从而将其从处理中删除。
中 排队处理优先级不正确
在同一笔交易中,既有提款又有存款
中 余额检查不正确
资金已经不在合约里了,那么在去使用address(this)检查余额会发生错误
资金的多层转移,一定要看使用的检查条件是否合理