github.com/ethereum-optimism/optimism@v1.7.2/packages/contracts-bedrock/test/invariants/FaultDisputeGame.t.sol (about) 1 // SPDX-License-Identifier: MIT 2 pragma solidity 0.8.15; 3 4 import { Vm } from "forge-std/Vm.sol"; 5 import { StdUtils } from "forge-std/StdUtils.sol"; 6 import { FaultDisputeGame } from "src/dispute/FaultDisputeGame.sol"; 7 import { FaultDisputeGame_Init } from "test/dispute/FaultDisputeGame.t.sol"; 8 9 import "src/libraries/DisputeTypes.sol"; 10 import "src/libraries/DisputeErrors.sol"; 11 12 contract FaultDisputeGame_Solvency_Invariant is FaultDisputeGame_Init { 13 Claim internal constant ROOT_CLAIM = Claim.wrap(bytes32(uint256(10))); 14 Claim internal constant ABSOLUTE_PRESTATE = Claim.wrap(bytes32((uint256(3) << 248) | uint256(0))); 15 16 RandomClaimActor internal actor; 17 uint256 internal defaultSenderBalance; 18 19 function setUp() public override { 20 super.setUp(); 21 super.init({ rootClaim: ROOT_CLAIM, absolutePrestate: ABSOLUTE_PRESTATE, l2BlockNumber: 0x10 }); 22 23 actor = new RandomClaimActor(gameProxy, vm); 24 25 targetContract(address(actor)); 26 vm.startPrank(address(actor)); 27 } 28 29 /// @custom:invariant FaultDisputeGame always returns all ETH on total resolution 30 /// 31 /// The FaultDisputeGame contract should always return all ETH in the contract to the correct recipients upon 32 /// resolution of all outstanding claims. There may never be any ETH left in the contract after a full resolution. 33 function invariant_faultDisputeGame_solvency() public { 34 vm.warp(block.timestamp + 7 days + 1 seconds); 35 36 (,,, uint256 rootBond,,,) = gameProxy.claimData(0); 37 38 for (uint256 i = gameProxy.claimDataLen(); i > 0; i--) { 39 (bool success,) = address(gameProxy).call(abi.encodeCall(gameProxy.resolveClaim, (i - 1))); 40 assertTrue(success); 41 } 42 gameProxy.resolve(); 43 44 // Wait for the withdrawal delay. 45 vm.warp(block.timestamp + 7 days + 1 seconds); 46 47 if (gameProxy.credit(address(this)) == 0) { 48 vm.expectRevert(NoCreditToClaim.selector); 49 gameProxy.claimCredit(address(this)); 50 } else { 51 gameProxy.claimCredit(address(this)); 52 } 53 54 if (gameProxy.credit(address(actor)) == 0) { 55 vm.expectRevert(NoCreditToClaim.selector); 56 gameProxy.claimCredit(address(actor)); 57 } else { 58 gameProxy.claimCredit(address(actor)); 59 } 60 61 if (gameProxy.status() == GameStatus.DEFENDER_WINS) { 62 assertEq(address(this).balance, type(uint96).max); 63 assertEq(address(actor).balance, actor.totalBonded() - rootBond); 64 } else if (gameProxy.status() == GameStatus.CHALLENGER_WINS) { 65 assertEq(DEFAULT_SENDER.balance, type(uint96).max - rootBond); 66 assertEq(address(actor).balance, actor.totalBonded() + rootBond); 67 } else { 68 revert("unreachable"); 69 } 70 71 assertEq(address(gameProxy).balance, 0); 72 } 73 } 74 75 contract RandomClaimActor is StdUtils { 76 FaultDisputeGame internal immutable GAME; 77 Vm internal immutable VM; 78 79 uint256 public totalBonded; 80 81 constructor(FaultDisputeGame _gameProxy, Vm _vm) { 82 GAME = _gameProxy; 83 VM = _vm; 84 } 85 86 function move(bool _isAttack, uint256 _parentIndex, Claim _claim, uint64 _bondAmount) public { 87 _parentIndex = bound(_parentIndex, 0, GAME.claimDataLen()); 88 VM.deal(address(this), _bondAmount); 89 90 totalBonded += _bondAmount; 91 92 GAME.move{ value: _bondAmount }(_parentIndex, _claim, _isAttack); 93 } 94 95 fallback() external payable { } 96 97 receive() external payable { } 98 }