claimExpired() uses expiry + MODERATOR_CORRUPTED_GRACE as the cutoff for moderator review. The problem is that the registry can stay UNDER_ATTACK for the entire 180 days, and flagOutcome() rejects both SURVIVED and CORRUPTED while it is in that state. By the time CORRUPTED is finally created, the deadline may already be gone before the moderator could do anything.
The factory requires the sponsor to be the agreement owner and lets it choose the pool’s recovery address. The pinned AttackRegistry also makes the agreement owner the attack moderator by default and allows that role to be transferred while the agreement is still nonterminal.
While the registry is still UNDER_ATTACK, the sponsor can transfer the attack-moderator role to its own executor. After the deadline, one call to that executor runs:
Those three calls happen in one transaction. The first creates CORRUPTED, the second immediately finalizes the pool as bad-faith CORRUPTED, and the third sends the full pool balance to the sponsor’s recovery address.
So the contract counts 180 days of moderator “inaction” while the moderator is not allowed to act at all.
The upstream corruption does not need to be fake. A real agreement-level breach may still need the pool moderator to choose SURVIVED because the breach was outside the pool’s narrower scope, or good-faith CORRUPTED so a whitehat receives the bounty. This transaction removes both choices.
The timeline is:
Likelihood:
The pool remains unresolved for 180 days after expiry. Any caller can resolve it as EXPIRED while the registry remains UNDER_ATTACK, so this is a long public race.
The agreement remains in an active-risk state, and the sponsor retains or delegates the agreement’s attack-moderator role.
A genuine late agreement corruption is sufficient. The attack does not depend on forging a fake upstream incident.
Impact:
claimCorrupted() transfers the pool’s entire token balance to the sponsor-selected recovery address. The PoC loses 100e18 of staker principal plus a 50e18 bonus.
The independent moderator gets no transaction or block between creation of CORRUPTED and permanent finalization.
Both alternatives are lost: out-of-scope SURVIVED and good-faith CORRUPTED with a whitehat bounty.
This test proves both moderator outcomes are unavailable before the terminal transition, then executes markCorrupted() → claimExpired() → claimCorrupted() atomically and confirms the sponsor’s recovery address receives the full 150e18 pool balance.
Start the grace period when the pool first observes CORRUPTED, not at pool expiry. The first observation must be stored without reverting; otherwise, the new timestamp is rolled back with the revert.
Impact - High The mechanical branch always picks bad-faith, so the whole corpus lands at recoveryAddress and claimsStarted makes it permanent in the same call. Both alternatives it forecloses send the money elsewhere: DESIGN.md #8 lets the moderator flag SURVIVED for an out-of-scope breach, returning everything to stakers, and good-faith CORRUPTED reserves the pool for a named whitehat. Losing the classification decides who gets paid. Likelihood - Low The pool has to sit unresolved through the whole window past expiry, which is the hard part, since claimExpired stays permissionless throughout and would settle it as EXPIRED against the live active-risk state. Any staker with principal waiting has reason to make that call. Once corruption does land late, the moderator's first legal moment to classify and the fallback's first to finalize arrive together, so ordering alone decides it.
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.