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.
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.