initialize and setExpiry never require the agreement's commitment window to cover the pool term, letting the sponsor narrow scope mid-term and void coverage of a still-published accountSeverity: Medium
The pool prices settlement on its locked scope, reserving a CORRUPTED outcome for when an in-scope account was the breach surface. This holds only while the agreement's own scope is immutable for at least as long as the pool is live. The agreement enforces immutability only for its commitment window (getCantChangeUntil), whose registration floor is MIN_COMMITMENT = 7 days, whereas the pool term floor is _MIN_EXPIRY_LEAD = 30 days.
Neither initialize() nor setExpiry() requires getCantChangeUntil() >= expiry. Once the commitment window lapses, the agreement owner calls removeAccounts() — which has no attack-state gate — to remove a covered account while the agreement is still UNDER_ATTACK, deleting its registry binding. The pool keeps publishing the account as covered but reads only the single stored agreement; if that account is later corrupted (including after rebinding to another agreement), the stored agreement never reports CORRUPTED, so flagOutcome(CORRUPTED) reverts InvalidOutcome. The pool mis-settles SURVIVED / EXPIRED and returns principal and bonus.
The remove-while-under-attack unbinding is exercised by the vendored test testUnregisterContractForExistingAgreement_HappyPath (lib/battlechain-safe-harbor-contracts/test/unit/AttackRegistryTest.t.sol).
Likelihood:
A pool outlives the agreement's commitment window whenever expiry exceeds the remaining getCantChangeUntil; the pool floor (30 days) exceeds the commitment floor (7 days) and the two are never checked against each other.
The agreement owner removes a covered account once the commitment window lapses; removeAccounts() is gated only on block.timestamp >= s_cantChangeUntil, so it executes while the agreement is still UNDER_ATTACK.
Impact:
Coverage of the removed account is voided while the pool keeps publishing it, so stakers and bonus contributors rely on coverage the protocol can no longer enforce.
Up to the entire principal and bonus is returned to stakers instead of routed to the whitehat or recoveryAddress, despite the sole published-scope account being breached during the term.
A per-agreement registry mock is used because the repo's default MockAttackRegistry holds a single global state.
Require the agreement's commitment window to cover the pool term wherever the term is set. The window can only be extended, never shortened, so the check is durable once passed.
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.