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

Sponsor Can Inflate Their Bonus Share by Pausing the Pool During the Staking Window

Author Revealed upon completion

Description

  • The pool sponsor can pause and unpause the pool at any time via pause() and unpause(), both gated to onlyOwner. During a pause, stake() and contributeBonus() revert, but withdraw() is unaffected. This is documented behavior in README.md.

  • The sponsor, who can also be a staker, can pause the pool during the active staking window to prevent new stakers from joining. Because the bonus share formula divides each staker's score by globalScore (derived from snapshotTotalStaked at resolution time), fewer stakers in the pool means a larger bonus share for each existing staker — including the sponsor. The sponsor gains bonus tokens at the expense of stakers who were blocked from entering.

// @> pause() has no restriction on when during the pool lifecycle it can be called
function pause() external onlyOwner whenPoolNotPaused {
_pause();
}
// @> stake() is blocked during pause — future stakers cannot enter
function stake(uint256 amount) external nonReentrant whenPoolNotPaused {
...
}
// @> bonus share is computed at resolution using snapshotTotalStaked
// fewer stakers → smaller globalScore → larger share per remaining staker
uint256 globalScore = plus > minus ? plus - minus : 0;
return Math.mulDiv(userScore, snapshotTotalBonus, globalScore); // @> inflated for existing stakers

Risk

Likelihood:

  • The attack requires the sponsor to act in good faith. The sponsor is an explicitly trusted actor per DESIGN.md §10 and README.md. No permissionless attacker can call pause().

  • The sponsor must also be a staker (or collude with an existing staker) to benefit. A sponsor who only contributes bonus and does not stake gains nothing from this.

Impact:

  • Existing stakers (including the sponsor-as-staker) receive a disproportionately large share of the bonus pool. In the PoC, the sponsor gains 1,500 tokens above their fair share.

  • Stakers who were blocked during the pause lose the opportunity to participate. They lose no deposited funds — they never deposited — but the bonus pool is distributed among fewer participants than intended.

  • No deposited principal is at risk. The bonus pool is redistributed within the contract, not extracted to an external address.


Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
// Test: testSponsorPauseInflatesBonusShareByBlockingFutureStakers
// Added to test/unit/ConfidencePool.k2Bonus.t.sol
// Setup:
// bonusPool = 9,000 tokens (contributed by sponsor)
// Expected stakers: Sponsor (3,000), Alice (3,000), Bob (3,000)
// Fair share per staker = 3,000 tokens
// Attack:
// 1. Sponsor stakes 3,000 tokens
// 2. Alice stakes 3,000 tokens
// 3. Sponsor calls pause()
// 4. Bob calls stake(3,000) → reverts: PoolPaused
// 5. Sponsor calls unpause()
// 6. Pool resolves SURVIVED
// Result (from forge test output):
// Fair share (tokens, 1e18): 3000000000000000000000
// Actual sponsor bonus (tokens, 1e18): 4500000000000000000000
// Sponsor gain from pause manipulation (1e18): 1500000000000000000000

Test output confirmed:

[PASS] testSponsorPauseInflatesBonusShareByBlockingFutureStakers() (gas: 753130)

Recommended Mitigation

The most targeted fix is to extend the staking window by the total cumulative pause duration, so that stakers blocked during a pause can still join after unpause. This preserves the emergency-pause capability while neutralizing the bonus inflation side effect.

+ uint256 public totalPausedDuration;
+ uint256 private _pauseStartedAt;
function pause() external onlyOwner whenPoolNotPaused {
+ _pauseStartedAt = block.timestamp;
_pause();
}
function unpause() external onlyOwner whenPoolPaused {
+ totalPausedDuration += block.timestamp - _pauseStartedAt;
_unpause();
}
// In stake(), replace the expiry check:
- if (block.timestamp >= expiry) revert StakingClosed();
+ if (block.timestamp >= expiry + totalPausedDuration) revert StakingClosed();
efccweb3@EFCCWEB3:~/2026-07-bc-confidence-pools$ forge test --match-test testSponsorPauseInflatesBonusShareByBlockingFutureStakers -vvv
[⠢] Compiling...
[⠔] Compiling 1 files with Solc 0.8.26
[⠑] Solc 0.8.26 finished in 44.34s
Compiler run successful!
Ran 1 test for test/unit/ConfidencePool.k2Bonus.t.sol:ConfidencePoolK2BonusTest
[PASS] testSponsorPauseInflatesBonusShareByBlockingFutureStakers() (gas: 753130)
Logs:
Fair share (tokens, 1e18): 3000000000000000000000
Actual sponsor bonus (tokens, 1e18): 4500000000000000000000
Sponsor gain from pause manipulation (tokens, 1e18): 1500000000000000000000
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 34.68ms (5.75ms CPU time)
Ran 1 test suite in 211.08ms (34.68ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Support

FAQs

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

Give us feedback!