After a terminal observation fixes riskWindowEnd as payout endpoint T, a registry rewind to an active-risk state can still set riskWindowStart > T and reset the global bonus moments to timestamps beyond T.
The risk markers should be chronological and monotonic: an active-risk observation opens the window, and a later terminal observation closes it. A terminal-first observation deliberately leaves riskWindowStart == 0, preserving the protocol's “no observable risk” treatment.
_observePoolState() enforces each marker's one-time initialization independently. It does not prevent _markRiskWindowStart() after riskWindowEnd is already nonzero. Therefore, after PRODUCTION or CORRUPTED seals the end, a trusted registry migration or rewind to UNDER_ATTACK/PROMOTION_REQUESTED lets any caller invoke pokeRiskWindow() and retroactively open the window.
_markRiskWindowStart() then overwrites sumStakeTime and sumStakeTimeSq for all existing principal using this late start. Capping the start at expiry does not ensure it is at or before the already-sealed end. Resolution still uses the older riskWindowEnd as T, so the k=2 score treats post-terminal entries as amount × (T - entry)^2. The square makes entries farther after T gain more weight, allowing a post-rewind depositor to capture nearly all bonus. The manufactured nonzero start can also activate the post-grace auto-CORRUPTED path and permanently disable withdrawals.
Likelihood:
The issue occurs when an unresolved pool first observes a terminal registry state and the trusted registry is later replaced, migrated, or reset to report an active-risk state.
The normal BattleChain lifecycle is forward-only, so the required rewind is an exceptional infrastructure event rather than an action a staker can cause alone.
Once the rewind occurs, exploitation is permissionless: any account can seal the inverted window, and UNDER_ATTACK permits the later stake used to distort the bonus.
Impact:
Accounting no longer represents time inside the risk interval. A later post-terminal deposit can receive more k=2 weight than existing capital and drain nearly all staker bonus.
A retroactively nonzero riskWindowStart changes outcome behavior: it permanently closes withdrawals and can turn a no-risk expiry into bad-faith auto-CORRUPTED after the grace period, exposing the full principal and bonus to recovery.
Create test/audit/CP008RegistryRewindRiskWindowInversion.t.sol with the following contents:
Run the PoC from the repository root:
Execute forge test --offline --match-path test/audit/CP008RegistryRewindRiskWindowInversion.t.sol -vv.
Confirm that the test passes with one test passed and zero failures. Bob receives 121/122 of the bonus despite depositing ten days after Alice's rewritten entry and eleven days after T.
Once riskWindowEnd has been sealed, preserve the terminal-first no-risk state and never allow the start marker or global moments to be created afterward.
Add an invariant test asserting riskWindowStart == 0 || riskWindowEnd == 0 || riskWindowStart <= riskWindowEnd across arbitrary registry-state sequences. Deposit and withdrawal gates should also treat riskWindowEnd != 0 as terminal history so a registry rewind cannot reopen fund movement after T.
Impact - Medium Bonus pool only, principal always returns under SURVIVED/EXPIRED, but the quadratic advantage is unbounded and a tiny late stake can take nearly all of it. Likelihood - Low It needs a DAO registry migration to land inside the window where a pool is unresolved with riskWindowEnd already sealed, and the attacker still has to be watching. No attacker action can create the precondition.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
View preliminary resultsAppeals are being carefully reviewed by our judges.
The contest is complete and the rewards are being distributed.