Summary
contributeBonus() reuses the same _assertDepositsAllowed() gate as stake(), which blocks all deposits during PROMOTION_REQUESTED state. This restriction is correct for staking but wrong for bonus contributions, which are pure one-way donations with zero benefit to the contributor. Sponsors cannot increase staker rewards precisely when it matters most.
Root Cause
function contributeBonus(uint256 amount) external nonReentrant whenPoolNotPaused {
...
_assertDepositsAllowed(_observePoolState());
...
}
function _assertDepositsAllowed(IAttackRegistry.ContractState state) private pure {
if (
state == IAttackRegistry.ContractState.PROMOTION_REQUESTED
|| state == IAttackRegistry.ContractState.PRODUCTION
|| state == IAttackRegistry.ContractState.CORRUPTED
) {
revert StakingClosed();
}
}
Code snippet-
https://github.com/CodeHawks-Contests/2026-07-bc-confidence-pools/blob/58e8ba4ce3f3277866e4926f3140e597f9554a1e/src/ConfidencePool.sol#L271
Impact
Design docs mention no restriction on bonus contributions. Sponsor/User cannot add bonus during PROMOTION_REQUESTED even intentionally. No security benefit from this block contributor gains nothing.
Fix
Separate the bonus gate from the staking gate, only block terminal states:
function contributeBonus(uint256 amount) external nonReentrant whenPoolNotPaused {
if (amount == 0) revert InvalidAmount();
if (outcome != PoolStates.Outcome.UNRESOLVED) revert OutcomeAlreadySet();
if (block.timestamp >= expiry) revert StakingClosed();
IAttackRegistry.ContractState state = _observePoolState();
if (
state == IAttackRegistry.ContractState.PRODUCTION ||
state == IAttackRegistry.ContractState.CORRUPTED
) {
revert StakingClosed();
}
uint256 balanceBefore = stakeToken.balanceOf(address(this));
stakeToken.safeTransferFrom(msg.sender, address(this), amount);
uint256 balanceAfter = stakeToken.balanceOf(address(this));
uint256 received = balanceAfter > balanceBefore ? balanceAfter - balanceBefore : 0;
if (received == 0) revert NoTokensReceived();
totalBonus += received;
emit BonusContributed(msg.sender, received);
}
This allows bonus contributions during PROMOTION_REQUESTED while correctly blocking them once the pool reaches a terminal state.