高 缓冲区达到最大限额,取消资金大于合约的资金

质押协议允许用户质押代币并赚取奖励。当用户希望提取他们的代币时,他们会调用 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 {
// 直接调用质押函数
stake();
}

中 撤回函数中缺少暂停修饰符

StakingManager::confirmWithdrawal() 函数未包含 whenWithdrawalNotPaused 修饰符,尽管该函数明确属于“提现操作”。而根据 pauseWithdrawal() 函数的 Natspec 文档说明,其用途是:

/**
* @notice 暂停所有提现操作
*/
function pauseWithdrawal() external onlyRole(MANAGER_ROLE) {
withdrawalPaused = true;
emit WithdrawalPaused(msg.sender);
}

当前 confirmWithdrawal 函数定义如下:

function confirmWithdrawal(uint256 withdrawalId) external nonReentrant whenNotPaused {
uint256 amount = _processConfirmation(msg.sender, withdrawalId);
require(amount > 0, "No valid withdrawal request");
// ...
}

✅ 问题在于:缺少 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 {
WithdrawalRequest storage request = _withdrawalRequests[user][withdrawalId];
require(request.hypeAmount > 0, "No such withdrawal request");

uint256 hypeAmount = request.hypeAmount;
uint256 kHYPEAmount = request.kHYPEAmount;
uint256 kHYPEFee = request.kHYPEFee;

// Clear the withdrawal request
delete _withdrawalRequests[user][withdrawalId];
totalQueuedWithdrawals -= hypeAmount;

// Return kHYPE tokens to user (including fees)
kHYPE.transfer(user, kHYPEAmount + kHYPEFee);

// Track cancelled amount for future redelegation
_cancelledWithdrawalAmount += hypeAmount;

emit WithdrawalCancelled(user, withdrawalId, hypeAmount, _cancelledWithdrawalAmount);
}

请注意,此函数仅通过删除提现请求并将 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)检查余额会发生错误

资金的多层转移,一定要看使用的检查条件是否合理