Root + Impact
Description
When contributeBonus() is rejected because the registry is in a blocked state (PROMOTION_REQUESTED, PRODUCTION, or CORRUPTED) or because block.timestamp >= expiry, it reverts with StakingClosed. Contributing a bonus is not "staking" — it is a donation that carries no claim rights. The StakingClosed error name implies the caller was trying to stake, which is misleading for integrators and off-chain error handling.
function contributeBonus(uint256 amount) external nonReentrant whenPoolNotPaused {
@> if (block.timestamp >= expiry) revert StakingClosed();
@> _assertDepositsAllowed(_observePoolState());
}
Risk
Likelihood: High — any bonus contribution attempt during PROMOTION_REQUESTED/PRODUCTION/CORRUPTED or after expiry triggers this path.
Impact: Low — no funds lost; off-chain error handling may incorrectly categorize bonus rejections as staking rejections.
Proof of Concept
File: L4-StakingClosed-Bonus.poc.t.sol
pragma solidity 0.8.26;
import {IAttackRegistry} from "@battlechain/interface/IAttackRegistry.sol";
import {IConfidencePool} from "src/interfaces/IConfidencePool.sol";
import {BaseConfidencePoolTest} from "test/helpers/BaseConfidencePoolTest.sol";
contract L4_StakingClosed_Bonus_POC is BaseConfidencePoolTest {
function testPOC_L4_stakingClosedForBonus_promotionRequested() external {
token.mint(alice, ONE);
vm.startPrank(alice);
token.approve(address(pool), ONE);
attackRegistry.setAgreementState(IAttackRegistry.ContractState.PROMOTION_REQUESTED);
vm.expectRevert(IConfidencePool.StakingClosed.selector);
pool.contributeBonus(ONE);
vm.stopPrank();
}
function testPOC_L4_stakingClosedForBonus_expired() external {
token.mint(alice, ONE);
vm.startPrank(alice);
token.approve(address(pool), ONE);
vm.warp(pool.expiry());
vm.expectRevert(IConfidencePool.StakingClosed.selector);
pool.contributeBonus(ONE);
vm.stopPrank();
}
function testPOC_L4_bonusUsesStakingClosed_notDepositsClosed() external {
attackRegistry.setAgreementState(IAttackRegistry.ContractState.PRODUCTION);
token.mint(alice, ONE);
vm.startPrank(alice);
token.approve(address(pool), ONE);
vm.expectRevert(IConfidencePool.StakingClosed.selector);
pool.stake(ONE);
vm.expectRevert(IConfidencePool.StakingClosed.selector);
pool.contributeBonus(ONE);
vm.stopPrank();
}
}
Run: forge test --match-path 'L4-StakingClosed-Bonus.poc.t.sol' -vv
Recommended Mitigation
Rename StakingClosed to DepositsClosed in _assertDepositsAllowed:
- revert StakingClosed();
+ revert DepositsClosed();