FoundrySolidityLayer 2
7.25 ETH
Submission Details
Impact: medium
Likelihood: medium

`sweepUnclaimedBonus` strips the whitehat's bonus after a SURVIVED to good-faith CORRUPTED re-flag

Author Revealed upon completion

Root + Impact

Description

  • Describe the normal behavior in one or more sentences

  • Explain the specific issue or problem in one or more sentences

## Description
`sweepUnclaimedBonus` reserves the principal still owed to stakers, then sweeps the surplus to `recoveryAddress`. When `riskWindowStart == 0`, `_bonusShare` returns 0 for every staker, so the bonus is treated as unowed and the bonus-reserve line is skipped:
```solidity
uint256 reserved;
if (totalEligibleStake != 0) {
reserved = totalEligibleStake;
if (riskWindowStart != 0) {
reserved += snapshotTotalBonus - claimedBonus;
}
}
```
With the bonus unreserved, the full bonus `B` is swept and `totalBonus` is decremented to 0:
```solidity
if (totalEligibleStake == 0 || riskWindowStart == 0) {
totalBonus -= amount <= totalBonus ? amount : totalBonus;
}
```
This branch intentionally does not set `claimsStarted`:
```solidity
// Intentionally does NOT set claimsStarted. A direct-transfer donation of as little as 1
// wei would otherwise let anyone flip the flag post-flagOutcome and block the moderator's
// documented pre-claim re-flag window. Genuine reliance only comes from claim entrypoints.
stakeToken.safeTransfer(recoveryAddress, amount);
```
Because `claimsStarted` stays false, the re-flag gate in `flagOutcome` still passes:
```solidity
if (outcome != PoolStates.Outcome.UNRESOLVED && claimsStarted) revert OutcomeAlreadySet();
```
A moderator who now issues a good-faith CORRUPTED correction re-snapshots the live `totalBonus`, which is 0, and prices the bounty off principal only:
```solidity
snapshotTotalBonus = totalBonus;
```
```solidity
bountyEntitlement = willBeGoodFaithCorrupted ? snapshotTotalStaked + snapshotTotalBonus : 0;
```
The whitehat's `claimAttackerBounty` pays `snapshotTotalStaked` alone. The bonus `B` already left the pool to `recoveryAddress` and can never be recovered by the whitehat. DESIGN.md promises the good-faith CORRUPTED whitehat the whole pool, stake plus bonus. The bounty is not gated on `riskWindowStart`, unlike `_bonusShare`, so this re-pricing is silent.
## Example
Pool holds principal S = 5000e18 from active stakers and accumulated bonus B = 1000e18. The active-risk window was never observed on-chain, so `riskWindowStart == 0`.
1. The registry reaches a terminal state and the moderator first flags SURVIVED, judging the breach out of the pool's scope
2. The sponsor front-runs the moderator's pending correction and calls `sweepUnclaimedBonus`
3. `reserved` covers only the S principal, because the `riskWindowStart != 0` bonus-reserve line is skipped
4. `amount` equals the full bonus B = 1000e18, which transfers to the sponsor-controlled `recoveryAddress`
5. `totalBonus` is decremented to 0, and `claimsStarted` stays false
6. The moderator recognizes the breach was in scope and re-flags good-faith CORRUPTED naming the whitehat
7. The gate in `flagOutcome` passes because `claimsStarted` is false
8. `snapshotTotalBonus` re-reads `totalBonus` as 0, so `bountyEntitlement = 5000e18 + 0`
9. The whitehat calls `claimAttackerBounty` and receives 5000e18, not 6000e18
10. B = 1000e18 is permanently stranded at `recoveryAddress`

Risk

Likelihood:

  • Reason 1 // Describe WHEN this will occur (avoid using "if" statements)

  • Reason 2

Impact:

  • Impact 1

  • Impact 2

Proof of Concept

Recommended Mitigation

Latch finality when accounted bonus actually leaves the pool. A donation never decrements `totalBonus`, so the guard below keeps the 1 wei non-latch intact while closing the re-flag window once real bonus value moves.
```diff
if (totalEligibleStake == 0 || riskWindowStart == 0) {
- totalBonus -= amount <= totalBonus ? amount : totalBonus;
+ uint256 dec = amount <= totalBonus ? amount : totalBonus;
+ totalBonus -= dec;
+ // Accounted bonus (not a mere donation) left the pool: lock the outcome so a later
+ // good-faith CORRUPTED re-flag cannot re-price the whitehat's bounty to exclude it.
+ if (dec != 0 && !claimsStarted) claimsStarted = true;
}
```

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!