FoundrySolidityLayer 2
7.25 ETH
View results
Submission Details
Impact: medium
Likelihood: medium
Invalid

Same-Block Risk-Window Open and Close Triggers Amount-Weighted Bonus Fallback Bypassing k=2 Time-Weighted Logic

## Description
Normally, the confidence pool distributes the bonus pool using a quadratic $k=2$ time-weighted formula to reward early stakers who stayed at risk for longer durations and penalize late large depositors.
​
If the risk window is opened and closed in the same block, the global score calculates to 0, which triggers a fallback that distributes the entire bonus pool based solely on deposit amount, bypassing the time-weighted penalty entirely.
​
```solidity
// In ConfidencePool.sol: _bonusShare()
uint256 T = outcomeFlaggedAt;
​
// Underflow guards on both subtractions: globally the sum of squares is nonneg, but
// truncation/rounding pathologies could push individual terms over.
uint256 userPlus = T * T * userEligible + userSumStakeTimeSq[u];
uint256 userMinus = 2 * T * userSumStakeTime[u];
uint256 userScore = userPlus > userMinus ? userPlus - userMinus : 0;
​
uint256 plus = T * T * snapshotTotalStaked + snapshotSumStakeTimeSq;
uint256 minus = 2 * T * snapshotSumStakeTime;
uint256 globalScore = plus > minus ? plus - minus : 0;
​
@> if (globalScore == 0) {
// No time elapsed in the risk window for anyone → fallback to amount-weighted.
if (snapshotTotalStaked == 0) return 0;
return Math.mulDiv(userEligible, snapshotTotalBonus, snapshotTotalStaked);
}
```
​
## Risk
Likelihood:
- The registry state transitions from `UNDER_ATTACK` to `PRODUCTION` or `CORRUPTED` within the same block.
- A moderator resolves the pool outcome in the same block as the registry transition, leading to a same-block collapse.
​
Impact:
- The quadratic time-weighted penalty is bypassed.
- A late depositor with a large stake receives the vast majority of the bonus pool, diluting the rewards of early stakers.
​
## Proof of Concept
Here is a test showing how a late large staker (Bob) can capture ~99.9% of the bonus pool despite entering 10 days after early staker (Alice). Place this test in your test suite:
​
```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
​
import {BaseConfidencePoolTest} from "test/helpers/BaseConfidencePoolTest.sol";
import {PoolStates} from "src/libraries/PoolStates.sol";
import {IAttackRegistry} from "@battlechain/interface/IAttackRegistry.sol";
​
contract M01SameBlockFallbackExploit is BaseConfidencePoolTest {
function testExploit_SameBlockFallback() public {
// Alice stakes 1 token early
_stake(alice, ONE);
vm.warp(block.timestamp + 10 days);
​
// Bob stakes 1000 tokens late
_stake(bob, 1000 * ONE);
_contributeBonus(carol, 100 * ONE);
​
// Attack executes in same block
attackRegistry.setAgreementState(IAttackRegistry.ContractState.UNDER_ATTACK);
pool.pokeRiskWindow();
​
attackRegistry.setAgreementState(IAttackRegistry.ContractState.PRODUCTION);
vm.prank(moderator);
pool.flagOutcome(PoolStates.Outcome.SURVIVED, false, address(0));
​
vm.prank(alice);
pool.claimSurvived();
vm.prank(bob);
pool.claimSurvived();
​
uint256 aliceBonus = token.balanceOf(alice) - ONE;
uint256 bobBonus = token.balanceOf(bob) - 1000 * ONE;
​
// Bob receives ~99.9% of the bonus pool
assertGt(bobBonus, 99 * ONE);
assertLt(aliceBonus, 0.1 * ONE);
}
}
```
​
## Recommended Mitigation
Ensure a minimum duration for the risk window, or enforce equal (per capita) distribution rather than amount-weighted distribution when `globalScore == 0` to prevent same-block timing games.
​
```diff
uint256 plus = T * T * snapshotTotalStaked + snapshotSumStakeTimeSq;
uint256 minus = 2 * T * snapshotSumStakeTime;
uint256 globalScore = plus > minus ? plus - minus : 0;
if (globalScore == 0) {
- // No time elapsed in the risk window for anyone → fallback to amount-weighted.
- if (snapshotTotalStaked == 0) return 0;
- return Math.mulDiv(userEligible, snapshotTotalBonus, snapshotTotalStaked);
+ // If no time elapsed, distribute equally per-capita or revert to prevent same-block exploits
+ revert RiskWindowTooShort();
}
```
Updates

Lead Judging Commences

inallhonesty Lead Judge
3 months ago
inallhonesty Lead Judge 2 months ago
Submission Judgement Published
Invalidated
Reason: Design choice
Assigned finding tags:

Known Design #7

inallhonesty Lead Judge 2 months ago
Submission Judgement Published
Invalidated
Reason: Design choice
Assigned finding tags:

Known Design #7

Support

FAQs

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

Give us feedback!