// SPDX-License-Identifier: MIT pragma solidity ^0.8.15; import "forge-std/Test.sol"; import "../libraries/DisputeTypes.sol"; import { IDisputeGame } from "../dispute/IDisputeGame.sol"; import { IBondManager } from "../dispute/IBondManager.sol"; import { DisputeGameFactory } from "../dispute/DisputeGameFactory.sol"; import { BondManager } from "../dispute/BondManager.sol"; contract BondManager_Test is Test { DisputeGameFactory factory; BondManager bm; // DisputeGameFactory events event DisputeGameCreated( address indexed disputeProxy, GameType indexed gameType, Claim indexed rootClaim ); // BondManager events event BondPosted(bytes32 bondId, address owner, uint256 expiration, uint256 amount); event BondSeized(bytes32 bondId, address owner, address seizer, uint256 amount); event BondReclaimed(bytes32 bondId, address claiment, uint256 amount); function setUp() public { factory = new DisputeGameFactory(address(this)); bm = new BondManager(factory); } /** * ------------------------------------------- * Test Bond Posting * ------------------------------------------- */ /** * @notice Tests that posting a bond succeeds. */ function testFuzz_post_succeeds( bytes32 bondId, address owner, uint256 minClaimHold, uint256 amount ) public { vm.assume(owner != address(0)); vm.assume(owner != address(bm)); vm.assume(owner != address(this)); // Create2Deployer vm.assume(owner != address(0x4e59b44847b379578588920cA78FbF26c0B4956C)); vm.assume(amount != 0); unchecked { vm.assume(block.timestamp + minClaimHold > minClaimHold); } vm.deal(address(this), amount); vm.expectEmit(true, true, true, true); uint256 expiration = block.timestamp + minClaimHold; emit BondPosted(bondId, owner, expiration, amount); bm.post{ value: amount }(bondId, owner, minClaimHold); // Validate the bond ( address newFetchedOwner, uint256 fetchedExpiration, bytes32 fetchedBondId, uint256 bondAmount ) = bm.bonds(bondId); assertEq(newFetchedOwner, owner); assertEq(fetchedExpiration, block.timestamp + minClaimHold); assertEq(fetchedBondId, bondId); assertEq(bondAmount, amount); } /** * @notice Tests that posting a bond with the same id twice reverts. */ function testFuzz_post_duplicates_reverts( bytes32 bondId, address owner, uint256 minClaimHold, uint256 amount ) public { vm.assume(owner != address(0)); amount = amount / 2; vm.assume(amount != 0); unchecked { vm.assume(block.timestamp + minClaimHold > minClaimHold); } vm.deal(address(this), amount); bm.post{ value: amount }(bondId, owner, minClaimHold); vm.deal(address(this), amount); vm.expectRevert("BondManager: BondId already posted."); bm.post{ value: amount }(bondId, owner, minClaimHold); } /** * @notice Posting with the zero address as the owner fails. */ function testFuzz_post_zeroAddress_reverts( bytes32 bondId, uint256 minClaimHold, uint256 amount ) public { address owner = address(0); vm.deal(address(this), amount); vm.expectRevert("BondManager: Owner cannot be the zero address."); bm.post{ value: amount }(bondId, owner, minClaimHold); } /** * @notice Posting zero value bonds should revert. */ function testFuzz_post_zeroAddress_reverts( bytes32 bondId, address owner, uint256 minClaimHold ) public { vm.assume(owner != address(0)); uint256 amount = 0; vm.deal(address(this), amount); vm.expectRevert("BondManager: Value must be non-zero."); bm.post{ value: amount }(bondId, owner, minClaimHold); } /** * ------------------------------------------- * Test Bond Seizing * ------------------------------------------- */ /** * @notice Non-existing bonds shouldn't be seizable. */ function testFuzz_seize_missingBond_reverts(bytes32 bondId) public { vm.expectRevert("BondManager: The bond does not exist."); bm.seize(bondId); } /** * @notice Bonds that expired cannot be seized. */ function testFuzz_seize_expired_reverts( bytes32 bondId, address owner, uint256 minClaimHold, uint256 amount ) public { vm.assume(owner != address(0)); vm.assume(owner != address(bm)); vm.assume(owner != address(this)); vm.assume(amount != 0); unchecked { vm.assume(block.timestamp + minClaimHold + 1 > minClaimHold); } vm.deal(address(this), amount); bm.post{ value: amount }(bondId, owner, minClaimHold); vm.warp(block.timestamp + minClaimHold + 1); vm.expectRevert("BondManager: Bond expired."); bm.seize(bondId); } /** * @notice Bonds cannot be seized by unauthorized parties. */ function testFuzz_seize_unauthorized_reverts( bytes32 bondId, address owner, uint256 minClaimHold, uint256 amount ) public { vm.assume(owner != address(0)); vm.assume(owner != address(bm)); vm.assume(owner != address(this)); vm.assume(amount != 0); unchecked { vm.assume(block.timestamp + minClaimHold > minClaimHold); } vm.deal(address(this), amount); bm.post{ value: amount }(bondId, owner, minClaimHold); MockAttestationDisputeGame game = new MockAttestationDisputeGame(); vm.prank(address(game)); vm.expectRevert("BondManager: Unauthorized seizure."); bm.seize(bondId); } /** * @notice Seizing a bond should succeed if the game resolves. */ function testFuzz_seize_succeeds( bytes32 bondId, uint256 minClaimHold, bytes calldata extraData ) public { unchecked { vm.assume(block.timestamp + minClaimHold > minClaimHold); } vm.deal(address(this), 1 ether); bm.post{ value: 1 ether }(bondId, address(0xba5ed), minClaimHold); // Create a mock dispute game in the factory IDisputeGame proxy; Claim rootClaim; bytes memory ed = extraData; { rootClaim = Claim.wrap(bytes32("")); MockAttestationDisputeGame implementation = new MockAttestationDisputeGame(); GameType gt = GameType.ATTESTATION; factory.setImplementation(gt, IDisputeGame(address(implementation))); vm.expectEmit(false, true, true, false); emit DisputeGameCreated(address(0), gt, rootClaim); proxy = factory.create(gt, rootClaim, extraData); assertEq(address(factory.games(gt, rootClaim, extraData)), address(proxy)); } // Update the game fields MockAttestationDisputeGame spawned = MockAttestationDisputeGame(payable(address(proxy))); spawned.setBondManager(bm); spawned.setRootClaim(rootClaim); spawned.setGameStatus(GameStatus.CHALLENGER_WINS); spawned.setBondId(bondId); spawned.setExtraData(ed); // Seize the bond by calling resolve vm.expectEmit(true, true, true, true); emit BondSeized(bondId, address(0xba5ed), address(spawned), 1 ether); spawned.resolve(); assertEq(address(spawned).balance, 1 ether); // Validate that the bond was deleted (address newFetchedOwner, , , ) = bm.bonds(bondId); assertEq(newFetchedOwner, address(0)); } /** * ------------------------------------------- * Test Bond Split and Seizing * ------------------------------------------- */ /** * @notice Seizing and splitting a bond should succeed if the game resolves. */ function testFuzz_seizeAndSplit_succeeds( bytes32 bondId, uint256 minClaimHold, bytes calldata extraData ) public { unchecked { vm.assume(block.timestamp + minClaimHold > minClaimHold); } vm.deal(address(this), 1 ether); bm.post{ value: 1 ether }(bondId, address(0xba5ed), minClaimHold); // Create a mock dispute game in the factory IDisputeGame proxy; Claim rootClaim; bytes memory ed = extraData; { rootClaim = Claim.wrap(bytes32("")); MockAttestationDisputeGame implementation = new MockAttestationDisputeGame(); GameType gt = GameType.ATTESTATION; factory.setImplementation(gt, IDisputeGame(address(implementation))); vm.expectEmit(false, true, true, false); emit DisputeGameCreated(address(0), gt, rootClaim); proxy = factory.create(gt, rootClaim, extraData); assertEq(address(factory.games(gt, rootClaim, extraData)), address(proxy)); } // Update the game fields MockAttestationDisputeGame spawned = MockAttestationDisputeGame(payable(address(proxy))); spawned.setBondManager(bm); spawned.setRootClaim(rootClaim); spawned.setGameStatus(GameStatus.CHALLENGER_WINS); spawned.setBondId(bondId); spawned.setExtraData(ed); // Seize the bond by calling resolve vm.expectEmit(true, true, true, true); emit BondSeized(bondId, address(0xba5ed), address(spawned), 1 ether); spawned.splitResolve(); assertEq(address(spawned).balance, 0); address[] memory challengers = spawned.getChallengers(); uint256 proportionalAmount = 1 ether / challengers.length; for (uint256 i = 0; i < challengers.length; i++) { assertEq(address(challengers[i]).balance, proportionalAmount); } // Validate that the bond was deleted (address newFetchedOwner, , , ) = bm.bonds(bondId); assertEq(newFetchedOwner, address(0)); } /** * ------------------------------------------- * Test Bond Reclaiming * ------------------------------------------- */ /** * @notice Bonds can be reclaimed after the specified amount of time. */ function testFuzz_reclaim_succeeds( bytes32 bondId, address owner, uint256 minClaimHold, uint256 amount ) public { vm.assume(owner != address(0)); vm.assume(owner.code.length == 0); vm.assume(amount != 0); unchecked { vm.assume(block.timestamp + minClaimHold > minClaimHold); } assumeNoPrecompiles(owner); // Post the bond vm.deal(address(this), amount); bm.post{ value: amount }(bondId, owner, minClaimHold); // We can't claim if the block.timestamp is less than the bond expiration. (, uint256 expiration, , ) = bm.bonds(bondId); if (expiration > block.timestamp) { vm.prank(owner); vm.expectRevert("BondManager: Bond isn't claimable yet."); bm.reclaim(bondId); } // Past expiration, the owner can reclaim vm.warp(expiration); vm.prank(owner); bm.reclaim(bondId); assertEq(owner.balance, amount); } } /** * @title MockAttestationDisputeGame * @dev A mock dispute game for testing bond seizures. */ contract MockAttestationDisputeGame is IDisputeGame { GameStatus internal gameStatus; BondManager bm; Claim internal rc; bytes internal ed; bytes32 internal bondId; address[] internal challengers; function getChallengers() public view returns (address[] memory) { return challengers; } function setBondId(bytes32 bid) external { bondId = bid; } function setBondManager(BondManager _bm) external { bm = _bm; } function setGameStatus(GameStatus _gs) external { gameStatus = _gs; } function setRootClaim(Claim _rc) external { rc = _rc; } function setExtraData(bytes memory _ed) external { ed = _ed; } receive() external payable {} fallback() external payable {} function splitResolve() public { challengers = [address(1), address(2)]; bm.seizeAndSplit(bondId, challengers); } /** * ------------------------------------------- * Initializable Functions * ------------------------------------------- */ function initialize() external { /* noop */ } /** * ------------------------------------------- * IVersioned Functions * ------------------------------------------- */ function version() external pure returns (string memory _version) { return "0.1.0"; } /** * ------------------------------------------- * IDisputeGame Functions * ------------------------------------------- */ function createdAt() external pure override returns (Timestamp _createdAt) { return Timestamp.wrap(uint64(0)); } function status() external view override returns (GameStatus _status) { return gameStatus; } function gameType() external pure returns (GameType _gameType) { return GameType.ATTESTATION; } function rootClaim() external view override returns (Claim _rootClaim) { return rc; } function extraData() external view returns (bytes memory _extraData) { return ed; } function bondManager() external view override returns (IBondManager _bondManager) { return IBondManager(address(bm)); } function resolve() external returns (GameStatus _status) { bm.seize(bondId); return gameStatus; } }