FoundrySolidityLayer 2
7.25 ETH
Submission Details
Impact: medium
Likelihood: low

Bonus Contributions Unnecessarily Blocked During `PROMOTION_REQUESTED`, Preventing Sponsor Rewards at Critical Moment

Author Revealed upon completion

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 {
...
// Wrong: reuses stake() gate, blocks bonus at PROMOTION_REQUESTED
_assertDepositsAllowed(_observePoolState());
...
}
function _assertDepositsAllowed(IAttackRegistry.ContractState state) private pure {
if (
state == IAttackRegistry.ContractState.PROMOTION_REQUESTED // blocks contributeBonus too
|| 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();
// Observe state for side effects (scope lock etc.)
// but do NOT apply staking gate — contributor receives nothing back
IAttackRegistry.ContractState state = _observePoolState();
// Only block when already terminal — no point contributing to resolved pool
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.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!