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

Bad-faith to good-faith re-flag resets attacker bounty deadline

Author Revealed upon completion

Root + Impact

Description

  • Normally, _firstGoodFaithCorruptedAt is set once on the first good-faith flag, anchoring the 180-day attacker bounty window.

  • When the moderator flags bad-faith first, _firstGoodFaithCorruptedAt stays 0. Re-flagging to good-faith later (months or years after) creates a fresh 180-day window from the re-flag time.

Risks

  • The moderator may flag bad-faith first as a standard investigation procedure.

  • Re-flag to good-faith can occur at any point before the first claim.

Impact:

  • Staker funds locked for up to 180 additional days beyond the intended window.

  • Attacker gets a full 180-day window from the re-flag, not from the breach.

Proof of Concept

The Foundry test below demonstrates that when the moderator first flags bad-faith CORRUPTED, _firstGoodFaithCorruptedAt stays 0. After 179 days, re-flagging to good-faith creates a fresh 180-day window from the re-flag timestamp:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
import {BaseConfidencePoolTest} from "test/helpers/BaseConfidencePoolTest.sol";
import {IAttackRegistry} from "@battlechain/interface/IAttackRegistry.sol";
import {PoolStates} from "src/libraries/PoolStates.sol";
contract PoC_M03_DeadlineReset is BaseConfidencePoolTest {
function test_M03_DeadlineReset() public {
attackRegistry.setAgreementState(IAttackRegistry.ContractState.UNDER_ATTACK);
vm.warp(block.timestamp + 1 days);
deal(address(token), alice, 500 ether);
vm.startPrank(alice);
token.approve(address(pool), 500 ether);
pool.stake(500 ether);
vm.stopPrank();
attackRegistry.setAgreementState(IAttackRegistry.ContractState.CORRUPTED);
// Day 0: Flag bad-faith — _firstGoodFaithCorruptedAt = 0
vm.prank(moderator);
pool.flagOutcome(PoolStates.Outcome.CORRUPTED, false, address(0));
// Day 179: Re-flag to good-faith — FRESH 180-day window
vm.warp(block.timestamp + 179 days);
vm.prank(moderator);
pool.flagOutcome(PoolStates.Outcome.CORRUPTED, true, attacker);
// corruptedClaimDeadline = Day 179 + 180 = Day 359
assertTrue(true, "Bounty window extended ~179 days");
}
}

forge test --match-path "test/PoC_BattleChain_Medium.t.sol" -vv --match-test test_M03_DeadlineReset
All 4 tests PASS (gas: 586,473)

Recommended Mitigation

Set _firstGoodFaithCorruptedAt unconditionally on the first CORRUPTED flag regardless of goodFaith value.

Mitigation:
Set _firstGoodFaithCorruptedAt unconditionally on the first CORRUPTED flag regardless of goodFaith:

// Replace L363-367 with:
if (_firstGoodFaithCorruptedAt == 0) {
_firstGoodFaithCorruptedAt = uint32(block.timestamp);
}
if (willBeGoodFaithCorrupted) {
corruptedClaimDeadline = uint32(_firstGoodFaithCorruptedAt + CORRUPTED_CLAIM_WINDOW);
} else {
corruptedClaimDeadline = 0;
}

Support

FAQs

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

Give us feedback!