In ConfidencePool.sol, the setPoolScope() function allows the owner (the pool sponsor) to unilaterally change the smart contracts that stakers are underwriting. The protection variable scopeLocked is not toggled to true upon the first user deposit. Instead, it remains completely unlocked until the underlying BattleChain global registry transitions out of NEW_DEPLOYMENT or NOT_DEPLOYED. This architectural inconsistency (especially compared to expiryLocked, which correctly locks upon the first stake) leaves the pool's entire risk profile completely mutable even while holding user funds.
A Confidence Pool's scope explicitly defines its risk profile. If a contract inside the scope gets hacked, the stakers lose their deposited principal to the attacker bounty.
Because setPoolScope remains unlocked even after users have deposited liquidity, a malicious sponsor can execute a deterministic, atomic "Bait-and-Switch" rugpull:
The Bait: The sponsor deploys the pool with a highly secure, audited contract in the scope (SAFE_CONTRACT). They may also optionally contribute a large bonus using contributeBonus() to artificially inflate the yield and attract liquidity.
The Hook: Users review the safe scope, accept the low risk profile, and deposit massive amounts of stakeToken.
The Switch and Trap: In a single batched sequence (or closely timed transactions), the sponsor calls setPoolScope([VULNERABLE_CONTRACT]) to silently swap the underwritten contracts, followed immediately by calling requestPromotion() on the global attack registry.
The Slaughter: Stakers observing the scope change will immediately attempt to withdraw their funds to escape the new risk profile. However, because the global registry has transitioned to PROMOTION_REQUESTED, the withdraw() function evaluates state != NEW_DEPLOYMENT and intentionally reverts with WithdrawsDisabled.
The stakers are permanently trapped underwriting a maliciously vulnerable contract against their consent. The sponsor can then exploit their own vulnerable contract by a separate attacker identity, trigger the CORRUPTED resolution state, and legally drain the stakers' liquidity via the attacker bounty.
High: The attack vector relies entirely on a single actor (the pool sponsor). However, Confidence Pools are heavily permissionless for creators and any owner of a valid agreement can deploy one. the protocol docs and readme does not explicitly declare them trusted and must assume that some pool sponsors are highly adversarial and motivated to rugpull their underwriters.
High: This vulnerability results in a total loss of principal for any stakers inside the pool. It breaks the fundamental guarantee that underwriters have the right to assess the risk of the contracts they are insuring before being locked into a binding slashing condition.
Add the test below to the ConfidencePool.scope.t.sol test file and run the test.
Enforce scope immutability upon the very first deposit, exactly identically to how the expiry timestamp is protected.
Update the stake() function in ConfidencePool.sol to permanently lock the scope the moment user funds are committed:
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.
The contest is complete and the rewards are being distributed.