instantPromote collapses confirmed in-scope vulnerability scenarios into a PRODUCTION registry state, preventing the moderator from expressing a CORRUPTED pool outcome and causing the pool to distribute the bonus as SURVIVEDWe know that ConfidencePool relies on the Battlechain Attack Registry to determine which pool outcomes are valid. Under the normal lifecycle, agreements transition through UNDER_ATTACK before eventually reaching either PRODUCTION or CORRUPTED, allowing the moderator to resolve the pool accordingly.
However, Battlechain also introduces an emergency lifecycle through instantPromote(). We can read more about it here: https://docs.battlechain.com/battlechain/how-to/instant-promotion
The interesting thing about instantPromote() is that it right away promotes the state to PRODUCTION during an emergency scenario. Well, let's talk about the emergency scenarios where this will be used (as also mentioned in the doc):
Copycat Discovery:
In this case, whitehat finds a vulnerability on Battlechain which can be both an in-scope vulnerability and an out-of-scope one. We are caring about the in-scope here.
Within a standard scenario, after a successful attack, the attackModerator will call AttackRegistry::markCorrupted, which will set the state to CORRUPTED. But here, they are choosing to instantly promote this to PRODUCTION in order to stop further disclosure of the vulnerability because it could make the mainnet contract vulnerable.
Protocol Emergency:
Here, the protocol discovers a critical issue and decides to patch it without whitehats or blackhats exploiting it.
So instantPromote is used to protect users after a confirmed in-scope vulnerability.
There's one more case under this one:
Battlechain frequently asks the whitehats not to publicly disclose a vulnerability that also exists on mainnet (check out the warning at the very last section of the Execute Attack page). Furthermore, it suggests contacting the protocol through their security contacts, which is obviously the right step.
Hence, instantPromote will be preferred here too for security purposes such that it can be patched up.
Therefore, the agreement exits attack mode through instantPromote() and immediately reaches the PRODUCTION registry state despite the emergency security event that triggered the promotion.
The problem is that ConfidencePool::flagOutcome() only permits the moderator to resolve the pool as CORRUPTED while the registry itself reports the CORRUPTED state.
Let's look at the flagOutcome function, especially line 347
As a result, even when the moderator's off-chain security assessment concludes that an in-scope vulnerability has already been confirmed and determines that the pool should no longer reward stakers, he is simply unable to express that decision because flagOutcome(CORRUPTED) always reverts with InvalidOutcome().
And, if the moderator refrains from flagging any outcome, the pool eventually reaches expiry, where ConfidencePool::claimExpired() mechanically interprets every PRODUCTION state as SURVIVED:
Consequently, the implementation collapses two semantically different agreement lifecycles into the same terminal registry state:
"a normal promotion after successfully completing the Safe Harbor period, and"
"an emergency promotion performed because of a security event through an in-scope vulnerability."
Since ConfidencePool observes only the terminal registry state, both cases become indistinguishable and always resolve as SURVIVED, permanently preventing the moderator from expressing the latter case through a CORRUPTED outcome.
BattleChain explicitly documents instantPromote() as an emergency workflow for copycat discoveries and protocol-discovered critical vulnerabilities.
The issue deterministically occurs whenever an associated Confidence Pool exists, and the agreement exits attack mode through instantPromote() before a CORRUPTED registry state is reached.
The moderator loses the ability to resolve the pool according to their off-chain security judgement despite the documentation treating the moderator as the authoritative decision-maker for scope-related outcomes.
The pool incorrectly resolves as SURVIVED, allowing stakers to claim the sponsored bonus even though the emergency promotion was triggered because a confirmed in-scope vulnerability had already terminated the intended confidence assessment.
Before running the PoC, we have to add a mock instantPromote function to the mock attack registry provided in the test suite. So, please add the following code in test/mocks/MockAttackRegistry.sol:
Now paste the following PoC in test/unit/ConfidencePool.t.sol:
Running the test:
Output:
One possible approach is to allow the moderator to express emergency security outcomes after an emergency promotion instead of permanently coupling CORRUPTED pool resolution to the registry's terminal state.
For example:
Alternatively, expose additional registry information indicating that the agreement reached PRODUCTION through an emergency instantPromote() so the moderator can distinguish ordinary successful promotions from emergency exits caused by confirmed security issues before resolving the pool.
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.