// SPDX-License-Identifier: MIT pragma solidity 0.8.24; import { Test } from "forge-std/Test.sol"; import { ThresholdConsensusV3 } from "../../contracts/consensus/ThresholdConsensusV3.sol"; contract ThresholdConsensusV3Test is Test { address internal constant OWNER = address(0xA11CE); address internal constant ATTESTER_A = address(0xA); address internal constant ATTESTER_B = address(0xB); bytes32 internal constant SCHEMA = keccak256("schema"); bytes32 internal constant SUBJECT = keccak256("subject"); bytes32 internal constant SNAPSHOT = keccak256("snapshot"); bytes32 internal constant OUTCOME = keccak256("outcome"); ThresholdConsensusV3 internal consensus; function setUp() external { consensus = new ThresholdConsensusV3(OWNER, 2); vm.startPrank(OWNER); consensus.setAttester(ATTESTER_A, true); consensus.setAttester(ATTESTER_B, true); vm.stopPrank(); } function test_FinalizedClaimCannotBeUsedAfterExpiry() external { uint64 expiresAt = uint64(block.timestamp + 1 days); ThresholdConsensusV3.Claim memory claim = ThresholdConsensusV3.Claim({ schema: SCHEMA, subject: SUBJECT, nonce: 1, expiresAt: expiresAt, settlementSnapshot: SNAPSHOT, outcome: OUTCOME }); vm.prank(ATTESTER_A); consensus.attest(claim); vm.prank(ATTESTER_B); consensus.attest(claim); assertTrue(consensus.isFinalized(SCHEMA, SUBJECT, 1, SNAPSHOT, OUTCOME)); vm.warp(expiresAt); assertFalse(consensus.isFinalized(SCHEMA, SUBJECT, 1, SNAPSHOT, OUTCOME)); } /// @notice A removed attester's partial approval must not survive a rotation. /// @dev Before the fix, A could vote, governance could remove A/add C, and C's /// first vote would finalize the claim at threshold two using A's revoked vote. function test_attesterRotationInvalidatesPartialClaimAndRequiresFreshThreshold() external { address attesterC = address(0xC); uint64 expiresAt = uint64(block.timestamp + 1 days); ThresholdConsensusV3.Claim memory claim = ThresholdConsensusV3.Claim({ schema: SCHEMA, subject: SUBJECT, nonce: 2, expiresAt: expiresAt, settlementSnapshot: SNAPSHOT, outcome: OUTCOME }); vm.prank(ATTESTER_A); consensus.attest(claim); bytes32 key = consensus.claimKey(SCHEMA, SUBJECT, 2); assertTrue(consensus.voted(key, ATTESTER_A)); vm.startPrank(OWNER); consensus.setAttester(ATTESTER_A, false); consensus.setAttester(attesterC, true); vm.stopPrank(); assertTrue(consensus.voted(key, ATTESTER_A), "historical vote was lost"); vm.prank(attesterC); consensus.attest(claim); assertFalse(consensus.isFinalized(SCHEMA, SUBJECT, 2, SNAPSHOT, OUTCOME), "stale approval finalized"); vm.prank(ATTESTER_B); consensus.attest(claim); assertTrue(consensus.isFinalized(SCHEMA, SUBJECT, 2, SNAPSHOT, OUTCOME)); } function test_thresholdChangeInvalidatesPartialClaim() external { address attesterC = address(0xC); vm.startPrank(OWNER); consensus.setAttester(attesterC, true); vm.stopPrank(); uint64 expiresAt = uint64(block.timestamp + 1 days); ThresholdConsensusV3.Claim memory claim = ThresholdConsensusV3.Claim({ schema: SCHEMA, subject: SUBJECT, nonce: 3, expiresAt: expiresAt, settlementSnapshot: SNAPSHOT, outcome: OUTCOME }); vm.prank(ATTESTER_A); consensus.attest(claim); vm.prank(OWNER); consensus.setThreshold(3); vm.prank(ATTESTER_B); consensus.attest(claim); vm.prank(attesterC); consensus.attest(claim); assertFalse( consensus.isFinalized(SCHEMA, SUBJECT, 3, SNAPSHOT, OUTCOME), "old vote counted after threshold change" ); vm.prank(ATTESTER_A); consensus.attest(claim); assertTrue(consensus.isFinalized(SCHEMA, SUBJECT, 3, SNAPSHOT, OUTCOME)); } /// @dev The public getter is consumed by bond self-attestation guards, so /// a later attester rotation must not hide a vote in an already-final claim. function test_finalizedClaimRetainsHistoricalVoteForSelfAttestationGuard() external { uint64 expiresAt = uint64(block.timestamp + 1 days); ThresholdConsensusV3.Claim memory claim = ThresholdConsensusV3.Claim({ schema: SCHEMA, subject: SUBJECT, nonce: 4, expiresAt: expiresAt, settlementSnapshot: SNAPSHOT, outcome: OUTCOME }); bytes32 key = consensus.claimKey(SCHEMA, SUBJECT, 4); vm.prank(ATTESTER_A); consensus.attest(claim); vm.prank(ATTESTER_B); consensus.attest(claim); assertTrue(consensus.isFinalized(SCHEMA, SUBJECT, 4, SNAPSHOT, OUTCOME)); vm.prank(OWNER); consensus.setAttester(address(0xC), true); assertTrue(consensus.voted(key, ATTESTER_A)); assertTrue(consensus.voted(key, ATTESTER_B)); } }