Commit 984bae91 authored by smartcontracts's avatar smartcontracts Committed by GitHub

feat: incident response improvements (#13711)

* feat: incident response improvements

First half of the original incident response improvements PR.
Co-authored-by: default avatarwildmolasses <changes@gmail.com>

* fix tests and add specs

* misc fixes

* more fixes

* emit event on setRespectedGameTypeUpdatedAt, and test wasRespectedGameType as withdrawal finality condition

* withdrawal when gameWasNotRespectedGameType reverts

* anchor game blacklisted and getAnchorGame tests

* isGameAirgapped

* tiny specs change

* add snapshots

* fix specs test and ASR snapshot

* update semver

* no compilation rrestrictions when optimizer is off

* interop portal semver

* justfile ignore, semver

* minor tweaks

* expanded test coverage

* various logical tweaks

* test fix

* clearer error

* fix test flake in go tests

* add portal tests

* portal2 tests: encodeCall

* FDG test: recipient can't receive value reverts

* various final tweaks

* regenerate snapshots

* fix specs tests

* final test fixes

---------
Co-authored-by: default avatarwildmolasses <changes@gmail.com>
parent 10defcd1
...@@ -31,9 +31,9 @@ just = "1.37.0" ...@@ -31,9 +31,9 @@ just = "1.37.0"
# Foundry dependencies # Foundry dependencies
# Foundry is a special case because it supplies multiple binaries at the same # Foundry is a special case because it supplies multiple binaries at the same
# GitHub release, so we need to use the aliasing trick to get mise to not error # GitHub release, so we need to use the aliasing trick to get mise to not error
forge = "nightly-59f354c179f4e7f6d7292acb3d068815c79286d1" forge = "nightly-017c59d6806ce11f1dc131f8607178efad79d84a"
cast = "nightly-59f354c179f4e7f6d7292acb3d068815c79286d1" cast = "nightly-017c59d6806ce11f1dc131f8607178efad79d84a"
anvil = "nightly-59f354c179f4e7f6d7292acb3d068815c79286d1" anvil = "nightly-017c59d6806ce11f1dc131f8607178efad79d84a"
# Fake dependencies # Fake dependencies
# Put things here if you need to track versions of tools or projects that can't # Put things here if you need to track versions of tools or projects that can't
......
...@@ -71,7 +71,7 @@ func (r *InteropDevRecipe) Build(addrs devkeys.Addresses) (*WorldConfig, error) ...@@ -71,7 +71,7 @@ func (r *InteropDevRecipe) Build(addrs devkeys.Addresses) (*WorldConfig, error)
Implementations: OPCMImplementationsConfig{ Implementations: OPCMImplementationsConfig{
L1ContractsRelease: "dev", L1ContractsRelease: "dev",
FaultProof: SuperFaultProofConfig{ FaultProof: SuperFaultProofConfig{
WithdrawalDelaySeconds: big.NewInt(604800), WithdrawalDelaySeconds: big.NewInt(302400),
MinProposalSizeBytes: big.NewInt(10000), MinProposalSizeBytes: big.NewInt(10000),
ChallengePeriodSeconds: big.NewInt(120), ChallengePeriodSeconds: big.NewInt(120),
ProofMaturityDelaySeconds: big.NewInt(12), ProofMaturityDelaySeconds: big.NewInt(12),
......
...@@ -53,6 +53,7 @@ var ( ...@@ -53,6 +53,7 @@ var (
methodL2BlockNumberChallenged = "l2BlockNumberChallenged" methodL2BlockNumberChallenged = "l2BlockNumberChallenged"
methodL2BlockNumberChallenger = "l2BlockNumberChallenger" methodL2BlockNumberChallenger = "l2BlockNumberChallenger"
methodChallengeRootL2Block = "challengeRootL2Block" methodChallengeRootL2Block = "challengeRootL2Block"
methodBondDistributionMode = "bondDistributionMode"
) )
var ( var (
...@@ -455,6 +456,14 @@ func (f *FaultDisputeGameContractLatest) GetAllClaims(ctx context.Context, block ...@@ -455,6 +456,14 @@ func (f *FaultDisputeGameContractLatest) GetAllClaims(ctx context.Context, block
return claims, nil return claims, nil
} }
func (f *FaultDisputeGameContractLatest) BondDistributionMode(ctx context.Context) (uint8, error) {
result, err := f.multiCaller.SingleCall(ctx, rpcblock.Latest, f.contract.Call(methodBondDistributionMode))
if err != nil {
return 0, fmt.Errorf("failed to fetch bond mode: %w", err)
}
return result.GetUint8(0), nil
}
func (f *FaultDisputeGameContractLatest) IsResolved(ctx context.Context, block rpcblock.Block, claims ...types.Claim) ([]bool, error) { func (f *FaultDisputeGameContractLatest) IsResolved(ctx context.Context, block rpcblock.Block, claims ...types.Claim) ([]bool, error) {
defer f.metrics.StartContractRequest("IsResolved")() defer f.metrics.StartContractRequest("IsResolved")()
calls := make([]batching.Call, 0, len(claims)) calls := make([]batching.Call, 0, len(claims))
...@@ -639,4 +648,5 @@ type FaultDisputeGameContract interface { ...@@ -639,4 +648,5 @@ type FaultDisputeGameContract interface {
CallResolve(ctx context.Context) (gameTypes.GameStatus, error) CallResolve(ctx context.Context) (gameTypes.GameStatus, error)
ResolveTx() (txmgr.TxCandidate, error) ResolveTx() (txmgr.TxCandidate, error)
Vm(ctx context.Context) (*VMContract, error) Vm(ctx context.Context) (*VMContract, error)
BondDistributionMode(ctx context.Context) (uint8, error)
} }
...@@ -15,7 +15,7 @@ const ( ...@@ -15,7 +15,7 @@ const (
GasLimit uint64 = 60_000_000 GasLimit uint64 = 60_000_000
BasefeeScalar uint32 = 1368 BasefeeScalar uint32 = 1368
BlobBaseFeeScalar uint32 = 801949 BlobBaseFeeScalar uint32 = 801949
WithdrawalDelaySeconds uint64 = 604800 WithdrawalDelaySeconds uint64 = 302400
MinProposalSizeBytes uint64 = 126000 MinProposalSizeBytes uint64 = 126000
ChallengePeriodSeconds uint64 = 86400 ChallengePeriodSeconds uint64 = 86400
ProofMaturityDelaySeconds uint64 = 604800 ProofMaturityDelaySeconds uint64 = 604800
......
...@@ -368,6 +368,18 @@ func (g *OutputGameHelper) Status(ctx context.Context) gameTypes.GameStatus { ...@@ -368,6 +368,18 @@ func (g *OutputGameHelper) Status(ctx context.Context) gameTypes.GameStatus {
return status return status
} }
func (g *OutputGameHelper) WaitForBondModeSet(ctx context.Context) {
g.T.Logf("Waiting for game %v to have bond mode set", g.Addr)
timedCtx, cancel := context.WithTimeout(ctx, defaultTimeout)
defer cancel()
err := wait.For(timedCtx, time.Second, func() (bool, error) {
bondMode, err := g.Game.BondDistributionMode(ctx)
g.Require.NoError(err)
return bondMode != 0, nil
})
g.Require.NoError(err, "Failed to wait for bond mode to be set")
}
func (g *OutputGameHelper) WaitForGameStatus(ctx context.Context, expected gameTypes.GameStatus) { func (g *OutputGameHelper) WaitForGameStatus(ctx context.Context, expected gameTypes.GameStatus) {
g.T.Logf("Waiting for game %v to have status %v", g.Addr, expected) g.T.Logf("Waiting for game %v to have status %v", g.Addr, expected)
timedCtx, cancel := context.WithTimeout(ctx, defaultTimeout) timedCtx, cancel := context.WithTimeout(ctx, defaultTimeout)
......
...@@ -115,13 +115,22 @@ func TestOutputAlphabetGame_ReclaimBond(t *testing.T) { ...@@ -115,13 +115,22 @@ func TestOutputAlphabetGame_ReclaimBond(t *testing.T) {
game.WaitForGameStatus(ctx, types.GameStatusChallengerWon) game.WaitForGameStatus(ctx, types.GameStatusChallengerWon)
game.LogGameData(ctx) game.LogGameData(ctx)
// Advance the time past the finalization delay
// Finalization delay is the same as the credit unlock delay
// But just warp way into the future to be safe
sys.TimeTravelClock.AdvanceTime(game.CreditUnlockDuration(ctx) * 2)
require.NoError(t, wait.ForNextBlock(ctx, l1Client))
// Wait for the game to have bond mode set
game.WaitForBondModeSet(ctx)
// Expect Alice's credit to be non-zero // Expect Alice's credit to be non-zero
// But it can't be claimed right now since there is a delay on the weth unlock // But it can't be claimed right now since there is a delay on the weth unlock
require.Truef(t, game.AvailableCredit(ctx, alice).Cmp(big.NewInt(0)) > 0, "Expected alice credit to be above zero") require.Truef(t, game.AvailableCredit(ctx, alice).Cmp(big.NewInt(0)) > 0, "Expected alice credit to be above zero")
// The actor should have no credit available because all its bonds were paid to Alice. // The actor should have no credit available because all its bonds were paid to Alice.
actorCredit := game.AvailableCredit(ctx, disputegame.TestAddress) actorCredit := game.AvailableCredit(ctx, disputegame.TestAddress)
require.True(t, actorCredit.Cmp(big.NewInt(0)) == 0, "Expected alice available credit to be zero") require.True(t, actorCredit.Cmp(big.NewInt(0)) == 0, "Expected actor available credit to be zero")
// Advance the time past the weth unlock delay // Advance the time past the weth unlock delay
sys.TimeTravelClock.AdvanceTime(game.CreditUnlockDuration(ctx)) sys.TimeTravelClock.AdvanceTime(game.CreditUnlockDuration(ctx))
......
...@@ -13,6 +13,14 @@ snapshots = 'notarealpath' # workaround for foundry#9477 ...@@ -13,6 +13,14 @@ snapshots = 'notarealpath' # workaround for foundry#9477
optimizer = true optimizer = true
optimizer_runs = 999999 optimizer_runs = 999999
additional_compiler_profiles = [
{ name = "dispute", optimizer_runs = 5000 },
]
compilation_restrictions = [
{ paths = "src/dispute/FaultDisputeGame.sol", optimizer_runs = 5000 },
{ paths = "src/dispute/PermissionedDisputeGame.sol", optimizer_runs = 5000 },
]
extra_output = ['devdoc', 'userdoc', 'metadata', 'storageLayout'] extra_output = ['devdoc', 'userdoc', 'metadata', 'storageLayout']
bytecode_hash = 'none' bytecode_hash = 'none'
ast = true ast = true
...@@ -85,6 +93,7 @@ depth = 32 ...@@ -85,6 +93,7 @@ depth = 32
[profile.cicoverage] [profile.cicoverage]
optimizer = false optimizer = false
compilation_restrictions = []
[profile.cicoverage.fuzz] [profile.cicoverage.fuzz]
runs = 1 runs = 1
...@@ -112,6 +121,8 @@ timeout = 300 ...@@ -112,6 +121,8 @@ timeout = 300
[profile.lite] [profile.lite]
optimizer = false optimizer = false
compilation_restrictions = []
################################################################ ################################################################
# PROFILE: KONTROL # # PROFILE: KONTROL #
......
...@@ -31,6 +31,7 @@ interface IOptimismPortal2 { ...@@ -31,6 +31,7 @@ interface IOptimismPortal2 {
error UnexpectedList(); error UnexpectedList();
error UnexpectedString(); error UnexpectedString();
error Unproven(); error Unproven();
error LegacyGame();
event DisputeGameBlacklisted(IDisputeGame indexed disputeGame); event DisputeGameBlacklisted(IDisputeGame indexed disputeGame);
event Initialized(uint8 version); event Initialized(uint8 version);
......
...@@ -33,6 +33,7 @@ interface IOptimismPortalInterop { ...@@ -33,6 +33,7 @@ interface IOptimismPortalInterop {
error UnexpectedList(); error UnexpectedList();
error UnexpectedString(); error UnexpectedString();
error Unproven(); error Unproven();
error LegacyGame();
event DisputeGameBlacklisted(IDisputeGame indexed disputeGame); event DisputeGameBlacklisted(IDisputeGame indexed disputeGame);
event Initialized(uint8 version); event Initialized(uint8 version);
......
...@@ -10,8 +10,8 @@ import { GameType, Hash, OutputRoot } from "src/dispute/lib/Types.sol"; ...@@ -10,8 +10,8 @@ import { GameType, Hash, OutputRoot } from "src/dispute/lib/Types.sol";
interface IAnchorStateRegistry { interface IAnchorStateRegistry {
error AnchorStateRegistry_Unauthorized(); error AnchorStateRegistry_Unauthorized();
error AnchorStateRegistry_ImproperAnchorGame();
error AnchorStateRegistry_InvalidAnchorGame(); error AnchorStateRegistry_InvalidAnchorGame();
error AnchorStateRegistry_AnchorGameBlacklisted();
event AnchorNotUpdated(IFaultDisputeGame indexed game); event AnchorNotUpdated(IFaultDisputeGame indexed game);
event AnchorUpdated(IFaultDisputeGame indexed game); event AnchorUpdated(IFaultDisputeGame indexed game);
...@@ -21,16 +21,27 @@ interface IAnchorStateRegistry { ...@@ -21,16 +21,27 @@ interface IAnchorStateRegistry {
function anchors(GameType) external view returns (Hash, uint256); function anchors(GameType) external view returns (Hash, uint256);
function getAnchorRoot() external view returns (Hash, uint256); function getAnchorRoot() external view returns (Hash, uint256);
function disputeGameFactory() external view returns (IDisputeGameFactory); function disputeGameFactory() external view returns (IDisputeGameFactory);
function initialize(ISuperchainConfig _superchainConfig, IDisputeGameFactory _disputeGameFactory, IOptimismPortal2 _portal, OutputRoot memory _startingAnchorRoot) external; function initialize(
function isGameRegistered(IDisputeGame _game) external view returns (bool); ISuperchainConfig _superchainConfig,
IDisputeGameFactory _disputeGameFactory,
IOptimismPortal2 _portal,
OutputRoot memory _startingAnchorRoot
)
external;
function isGameAirgapped(IDisputeGame _game) external view returns (bool);
function isGameBlacklisted(IDisputeGame _game) external view returns (bool); function isGameBlacklisted(IDisputeGame _game) external view returns (bool);
function isGameProper(IDisputeGame _game) external view returns (bool);
function isGameRegistered(IDisputeGame _game) external view returns (bool);
function isGameResolved(IDisputeGame _game) external view returns (bool);
function isGameRespected(IDisputeGame _game) external view returns (bool); function isGameRespected(IDisputeGame _game) external view returns (bool);
function isGameRetired(IDisputeGame _game) external view returns (bool); function isGameRetired(IDisputeGame _game) external view returns (bool);
function isGameProper(IDisputeGame _game) external view returns (bool); function isGameFinalized(IDisputeGame _game) external view returns (bool);
function isGameClaimValid(IDisputeGame _game) external view returns (bool);
function portal() external view returns (IOptimismPortal2); function portal() external view returns (IOptimismPortal2);
function setAnchorState(IFaultDisputeGame _game) external; function respectedGameType() external view returns (GameType);
function setAnchorState(IDisputeGame _game) external;
function superchainConfig() external view returns (ISuperchainConfig); function superchainConfig() external view returns (ISuperchainConfig);
function tryUpdateAnchorState() external;
function version() external view returns (string memory); function version() external view returns (string memory);
function __constructor__() external; function __constructor__() external;
......
...@@ -11,13 +11,13 @@ interface IDelayedWETH { ...@@ -11,13 +11,13 @@ interface IDelayedWETH {
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
event Initialized(uint8 version); event Initialized(uint8 version);
event Unwrap(address indexed src, uint256 wad);
fallback() external payable; fallback() external payable;
receive() external payable; receive() external payable;
function config() external view returns (ISuperchainConfig); function config() external view returns (ISuperchainConfig);
function delay() external view returns (uint256); function delay() external view returns (uint256);
function hold(address _guy) external;
function hold(address _guy, uint256 _wad) external; function hold(address _guy, uint256 _wad) external;
function initialize(address _owner, ISuperchainConfig _config) external; function initialize(address _owner, ISuperchainConfig _config) external;
function owner() external view returns (address); function owner() external view returns (address);
......
...@@ -14,7 +14,9 @@ interface IDisputeGame is IInitializable { ...@@ -14,7 +14,9 @@ interface IDisputeGame is IInitializable {
function gameCreator() external pure returns (address creator_); function gameCreator() external pure returns (address creator_);
function rootClaim() external pure returns (Claim rootClaim_); function rootClaim() external pure returns (Claim rootClaim_);
function l1Head() external pure returns (Hash l1Head_); function l1Head() external pure returns (Hash l1Head_);
function l2BlockNumber() external pure returns (uint256 l2BlockNumber_);
function extraData() external pure returns (bytes memory extraData_); function extraData() external pure returns (bytes memory extraData_);
function resolve() external returns (GameStatus status_); function resolve() external returns (GameStatus status_);
function gameData() external view returns (GameType gameType_, Claim rootClaim_, bytes memory extraData_); function gameData() external view returns (GameType gameType_, Claim rootClaim_, bytes memory extraData_);
function wasRespectedGameTypeWhenCreated() external view returns (bool);
} }
...@@ -6,7 +6,7 @@ import { IDelayedWETH } from "interfaces/dispute/IDelayedWETH.sol"; ...@@ -6,7 +6,7 @@ import { IDelayedWETH } from "interfaces/dispute/IDelayedWETH.sol";
import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol";
import { IBigStepper } from "interfaces/dispute/IBigStepper.sol"; import { IBigStepper } from "interfaces/dispute/IBigStepper.sol";
import { Types } from "src/libraries/Types.sol"; import { Types } from "src/libraries/Types.sol";
import { GameType, Claim, Position, Clock, Hash, Duration } from "src/dispute/lib/Types.sol"; import { GameType, Claim, Position, Clock, Hash, Duration, BondDistributionMode } from "src/dispute/lib/Types.sol";
interface IFaultDisputeGame is IDisputeGame { interface IFaultDisputeGame is IDisputeGame {
struct ClaimData { struct ClaimData {
...@@ -74,13 +74,19 @@ interface IFaultDisputeGame is IDisputeGame { ...@@ -74,13 +74,19 @@ interface IFaultDisputeGame is IDisputeGame {
error UnexpectedRootClaim(Claim rootClaim); error UnexpectedRootClaim(Claim rootClaim);
error UnexpectedString(); error UnexpectedString();
error ValidStep(); error ValidStep();
error InvalidBondDistributionMode();
error GameNotFinalized();
error GameNotResolved();
error ReservedGameType();
event Move(uint256 indexed parentIndex, Claim indexed claim, address indexed claimant); event Move(uint256 indexed parentIndex, Claim indexed claim, address indexed claimant);
event GameClosed(BondDistributionMode bondDistributionMode);
function absolutePrestate() external view returns (Claim absolutePrestate_); function absolutePrestate() external view returns (Claim absolutePrestate_);
function addLocalData(uint256 _ident, uint256 _execLeafIdx, uint256 _partOffset) external; function addLocalData(uint256 _ident, uint256 _execLeafIdx, uint256 _partOffset) external;
function anchorStateRegistry() external view returns (IAnchorStateRegistry registry_); function anchorStateRegistry() external view returns (IAnchorStateRegistry registry_);
function attack(Claim _disputed, uint256 _parentIndex, Claim _claim) external payable; function attack(Claim _disputed, uint256 _parentIndex, Claim _claim) external payable;
function bondDistributionMode() external view returns (BondDistributionMode);
function challengeRootL2Block(Types.OutputRootProof memory _outputRootProof, bytes memory _headerRLP) external; function challengeRootL2Block(Types.OutputRootProof memory _outputRootProof, bytes memory _headerRLP) external;
function claimCredit(address _recipient) external; function claimCredit(address _recipient) external;
function claimData(uint256) function claimData(uint256)
...@@ -98,11 +104,13 @@ interface IFaultDisputeGame is IDisputeGame { ...@@ -98,11 +104,13 @@ interface IFaultDisputeGame is IDisputeGame {
function claimDataLen() external view returns (uint256 len_); function claimDataLen() external view returns (uint256 len_);
function claims(Hash) external view returns (bool); function claims(Hash) external view returns (bool);
function clockExtension() external view returns (Duration clockExtension_); function clockExtension() external view returns (Duration clockExtension_);
function credit(address) external view returns (uint256); function closeGame() external;
function credit(address _recipient) external view returns (uint256 credit_);
function defend(Claim _disputed, uint256 _parentIndex, Claim _claim) external payable; function defend(Claim _disputed, uint256 _parentIndex, Claim _claim) external payable;
function getChallengerDuration(uint256 _claimIndex) external view returns (Duration duration_); function getChallengerDuration(uint256 _claimIndex) external view returns (Duration duration_);
function getNumToResolve(uint256 _claimIndex) external view returns (uint256 numRemainingChildren_); function getNumToResolve(uint256 _claimIndex) external view returns (uint256 numRemainingChildren_);
function getRequiredBond(Position _position) external view returns (uint256 requiredBond_); function getRequiredBond(Position _position) external view returns (uint256 requiredBond_);
function hasUnlockedCredit(address) external view returns (bool);
function l2BlockNumber() external pure returns (uint256 l2BlockNumber_); function l2BlockNumber() external pure returns (uint256 l2BlockNumber_);
function l2BlockNumberChallenged() external view returns (bool); function l2BlockNumberChallenged() external view returns (bool);
function l2BlockNumberChallenger() external view returns (address); function l2BlockNumberChallenger() external view returns (address);
...@@ -110,6 +118,8 @@ interface IFaultDisputeGame is IDisputeGame { ...@@ -110,6 +118,8 @@ interface IFaultDisputeGame is IDisputeGame {
function maxClockDuration() external view returns (Duration maxClockDuration_); function maxClockDuration() external view returns (Duration maxClockDuration_);
function maxGameDepth() external view returns (uint256 maxGameDepth_); function maxGameDepth() external view returns (uint256 maxGameDepth_);
function move(Claim _disputed, uint256 _challengeIndex, Claim _claim, bool _isAttack) external payable; function move(Claim _disputed, uint256 _challengeIndex, Claim _claim, bool _isAttack) external payable;
function normalModeCredit(address) external view returns (uint256);
function refundModeCredit(address) external view returns (uint256);
function resolutionCheckpoints(uint256) function resolutionCheckpoints(uint256)
external external
view view
...@@ -124,6 +134,7 @@ interface IFaultDisputeGame is IDisputeGame { ...@@ -124,6 +134,7 @@ interface IFaultDisputeGame is IDisputeGame {
function subgames(uint256, uint256) external view returns (uint256); function subgames(uint256, uint256) external view returns (uint256);
function version() external view returns (string memory); function version() external view returns (string memory);
function vm() external view returns (IBigStepper vm_); function vm() external view returns (IBigStepper vm_);
function wasRespectedGameTypeWhenCreated() external view returns (bool);
function weth() external view returns (IDelayedWETH weth_); function weth() external view returns (IDelayedWETH weth_);
function __constructor__(GameConstructorParams memory _params) external; function __constructor__(GameConstructorParams memory _params) external;
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
pragma solidity ^0.8.0; pragma solidity ^0.8.0;
import { Types } from "src/libraries/Types.sol"; import { Types } from "src/libraries/Types.sol";
import { Claim, Position, Clock, Hash, Duration } from "src/dispute/lib/Types.sol"; import { Claim, Position, Clock, Hash, Duration, BondDistributionMode } from "src/dispute/lib/Types.sol";
import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol";
import { IDelayedWETH } from "interfaces/dispute/IDelayedWETH.sol"; import { IDelayedWETH } from "interfaces/dispute/IDelayedWETH.sol";
...@@ -63,13 +63,19 @@ interface IPermissionedDisputeGame is IDisputeGame { ...@@ -63,13 +63,19 @@ interface IPermissionedDisputeGame is IDisputeGame {
error UnexpectedRootClaim(Claim rootClaim); error UnexpectedRootClaim(Claim rootClaim);
error UnexpectedString(); error UnexpectedString();
error ValidStep(); error ValidStep();
error InvalidBondDistributionMode();
error GameNotFinalized();
error GameNotResolved();
error ReservedGameType();
event Move(uint256 indexed parentIndex, Claim indexed claim, address indexed claimant); event Move(uint256 indexed parentIndex, Claim indexed claim, address indexed claimant);
event GameClosed(BondDistributionMode bondDistributionMode);
function absolutePrestate() external view returns (Claim absolutePrestate_); function absolutePrestate() external view returns (Claim absolutePrestate_);
function addLocalData(uint256 _ident, uint256 _execLeafIdx, uint256 _partOffset) external; function addLocalData(uint256 _ident, uint256 _execLeafIdx, uint256 _partOffset) external;
function anchorStateRegistry() external view returns (IAnchorStateRegistry registry_); function anchorStateRegistry() external view returns (IAnchorStateRegistry registry_);
function attack(Claim _disputed, uint256 _parentIndex, Claim _claim) external payable; function attack(Claim _disputed, uint256 _parentIndex, Claim _claim) external payable;
function bondDistributionMode() external view returns (BondDistributionMode);
function challengeRootL2Block(Types.OutputRootProof memory _outputRootProof, bytes memory _headerRLP) external; function challengeRootL2Block(Types.OutputRootProof memory _outputRootProof, bytes memory _headerRLP) external;
function claimCredit(address _recipient) external; function claimCredit(address _recipient) external;
function claimData(uint256) function claimData(uint256)
...@@ -87,11 +93,14 @@ interface IPermissionedDisputeGame is IDisputeGame { ...@@ -87,11 +93,14 @@ interface IPermissionedDisputeGame is IDisputeGame {
function claimDataLen() external view returns (uint256 len_); function claimDataLen() external view returns (uint256 len_);
function claims(Hash) external view returns (bool); function claims(Hash) external view returns (bool);
function clockExtension() external view returns (Duration clockExtension_); function clockExtension() external view returns (Duration clockExtension_);
function credit(address) external view returns (uint256); function closeGame() external;
function credit(address _recipient) external view returns (uint256 credit_);
function defend(Claim _disputed, uint256 _parentIndex, Claim _claim) external payable; function defend(Claim _disputed, uint256 _parentIndex, Claim _claim) external payable;
function getChallengerDuration(uint256 _claimIndex) external view returns (Duration duration_); function getChallengerDuration(uint256 _claimIndex) external view returns (Duration duration_);
function getNumToResolve(uint256 _claimIndex) external view returns (uint256 numRemainingChildren_); function getNumToResolve(uint256 _claimIndex) external view returns (uint256 numRemainingChildren_);
function getRequiredBond(Position _position) external view returns (uint256 requiredBond_); function getRequiredBond(Position _position) external view returns (uint256 requiredBond_);
function hasUnlockedCredit(address) external view returns (bool);
function initialize() external payable;
function l2BlockNumber() external pure returns (uint256 l2BlockNumber_); function l2BlockNumber() external pure returns (uint256 l2BlockNumber_);
function l2BlockNumberChallenged() external view returns (bool); function l2BlockNumberChallenged() external view returns (bool);
function l2BlockNumberChallenger() external view returns (address); function l2BlockNumberChallenger() external view returns (address);
...@@ -99,6 +108,8 @@ interface IPermissionedDisputeGame is IDisputeGame { ...@@ -99,6 +108,8 @@ interface IPermissionedDisputeGame is IDisputeGame {
function maxClockDuration() external view returns (Duration maxClockDuration_); function maxClockDuration() external view returns (Duration maxClockDuration_);
function maxGameDepth() external view returns (uint256 maxGameDepth_); function maxGameDepth() external view returns (uint256 maxGameDepth_);
function move(Claim _disputed, uint256 _challengeIndex, Claim _claim, bool _isAttack) external payable; function move(Claim _disputed, uint256 _challengeIndex, Claim _claim, bool _isAttack) external payable;
function normalModeCredit(address) external view returns (uint256);
function refundModeCredit(address) external view returns (uint256);
function resolutionCheckpoints(uint256) function resolutionCheckpoints(uint256)
external external
view view
...@@ -113,6 +124,7 @@ interface IPermissionedDisputeGame is IDisputeGame { ...@@ -113,6 +124,7 @@ interface IPermissionedDisputeGame is IDisputeGame {
function subgames(uint256, uint256) external view returns (uint256); function subgames(uint256, uint256) external view returns (uint256);
function version() external view returns (string memory); function version() external view returns (string memory);
function vm() external view returns (IBigStepper vm_); function vm() external view returns (IBigStepper vm_);
function wasRespectedGameTypeWhenCreated() external view returns (bool);
function weth() external view returns (IDelayedWETH weth_); function weth() external view returns (IDelayedWETH weth_);
error BadAuth(); error BadAuth();
......
...@@ -73,7 +73,7 @@ test-upgrade *ARGS: build-go-ffi ...@@ -73,7 +73,7 @@ test-upgrade *ARGS: build-go-ffi
#!/bin/bash #!/bin/bash
echo "Running upgrade tests at block $pinnedBlockNumber" echo "Running upgrade tests at block $pinnedBlockNumber"
export FORK_BLOCK_NUMBER=$pinnedBlockNumber export FORK_BLOCK_NUMBER=$pinnedBlockNumber
export NO_MATCH_CONTRACTS="OptimismPortal2WithMockERC20_Test|OptimismPortal2_FinalizeWithdrawal_Test|'AnchorStateRegistry_*'|FaultDisputeGame_Test|FaultDispute_1v1_Actors_Test" export NO_MATCH_CONTRACTS="OptimismPortal2WithMockERC20_Test|OptimismPortal2_FinalizeWithdrawal_Test|'AnchorStateRegistry_*'|FaultDisputeGame_Test|PermissionedDisputeGame_Test|FaultDispute_1v1_Actors_Test|DelayedWETH_Hold_Test"
export NO_MATCH_PATHS="test/dispute/AnchorStateRegistry.t.sol" export NO_MATCH_PATHS="test/dispute/AnchorStateRegistry.t.sol"
FORK_RPC_URL=$ETH_RPC_URL \ FORK_RPC_URL=$ETH_RPC_URL \
FORK_TEST=true \ FORK_TEST=true \
......
...@@ -112,6 +112,25 @@ ...@@ -112,6 +112,25 @@
"stateMutability": "nonpayable", "stateMutability": "nonpayable",
"type": "function" "type": "function"
}, },
{
"inputs": [
{
"internalType": "contract IDisputeGame",
"name": "_game",
"type": "address"
}
],
"name": "isGameAirgapped",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{ {
"inputs": [ "inputs": [
{ {
...@@ -131,6 +150,44 @@ ...@@ -131,6 +150,44 @@
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
}, },
{
"inputs": [
{
"internalType": "contract IDisputeGame",
"name": "_game",
"type": "address"
}
],
"name": "isGameClaimValid",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "contract IDisputeGame",
"name": "_game",
"type": "address"
}
],
"name": "isGameFinalized",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{ {
"inputs": [ "inputs": [
{ {
...@@ -169,6 +226,25 @@ ...@@ -169,6 +226,25 @@
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
}, },
{
"inputs": [
{
"internalType": "contract IDisputeGame",
"name": "_game",
"type": "address"
}
],
"name": "isGameResolved",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{ {
"inputs": [ "inputs": [
{ {
...@@ -220,10 +296,23 @@ ...@@ -220,10 +296,23 @@
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
}, },
{
"inputs": [],
"name": "respectedGameType",
"outputs": [
{
"internalType": "GameType",
"name": "",
"type": "uint32"
}
],
"stateMutability": "view",
"type": "function"
},
{ {
"inputs": [ "inputs": [
{ {
"internalType": "contract IFaultDisputeGame", "internalType": "contract IDisputeGame",
"name": "_game", "name": "_game",
"type": "address" "type": "address"
} }
...@@ -246,13 +335,6 @@ ...@@ -246,13 +335,6 @@
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
}, },
{
"inputs": [],
"name": "tryUpdateAnchorState",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{ {
"inputs": [], "inputs": [],
"name": "version", "name": "version",
...@@ -307,7 +389,7 @@ ...@@ -307,7 +389,7 @@
}, },
{ {
"inputs": [], "inputs": [],
"name": "AnchorStateRegistry_ImproperAnchorGame", "name": "AnchorStateRegistry_AnchorGameBlacklisted",
"type": "error" "type": "error"
}, },
{ {
......
...@@ -149,6 +149,19 @@ ...@@ -149,6 +149,19 @@
"stateMutability": "nonpayable", "stateMutability": "nonpayable",
"type": "function" "type": "function"
}, },
{
"inputs": [
{
"internalType": "address",
"name": "_guy",
"type": "address"
}
],
"name": "hold",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{ {
"inputs": [ "inputs": [
{ {
...@@ -497,25 +510,6 @@ ...@@ -497,25 +510,6 @@
"name": "Transfer", "name": "Transfer",
"type": "event" "type": "event"
}, },
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "src",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "wad",
"type": "uint256"
}
],
"name": "Unwrap",
"type": "event"
},
{ {
"anonymous": false, "anonymous": false,
"inputs": [ "inputs": [
......
...@@ -134,6 +134,19 @@ ...@@ -134,6 +134,19 @@
"stateMutability": "payable", "stateMutability": "payable",
"type": "function" "type": "function"
}, },
{
"inputs": [],
"name": "bondDistributionMode",
"outputs": [
{
"internalType": "enum BondDistributionMode",
"name": "",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
},
{ {
"inputs": [ "inputs": [
{ {
...@@ -281,6 +294,13 @@ ...@@ -281,6 +294,13 @@
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
}, },
{
"inputs": [],
"name": "closeGame",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{ {
"inputs": [], "inputs": [],
"name": "createdAt", "name": "createdAt",
...@@ -298,7 +318,7 @@ ...@@ -298,7 +318,7 @@
"inputs": [ "inputs": [
{ {
"internalType": "address", "internalType": "address",
"name": "", "name": "_recipient",
"type": "address" "type": "address"
} }
], ],
...@@ -306,7 +326,7 @@ ...@@ -306,7 +326,7 @@
"outputs": [ "outputs": [
{ {
"internalType": "uint256", "internalType": "uint256",
"name": "", "name": "credit_",
"type": "uint256" "type": "uint256"
} }
], ],
...@@ -455,6 +475,25 @@ ...@@ -455,6 +475,25 @@
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
}, },
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "hasUnlockedCredit",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{ {
"inputs": [], "inputs": [],
"name": "initialize", "name": "initialize",
...@@ -581,6 +620,44 @@ ...@@ -581,6 +620,44 @@
"stateMutability": "payable", "stateMutability": "payable",
"type": "function" "type": "function"
}, },
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "normalModeCredit",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "refundModeCredit",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{ {
"inputs": [ "inputs": [
{ {
...@@ -839,6 +916,19 @@ ...@@ -839,6 +916,19 @@
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
}, },
{
"inputs": [],
"name": "wasRespectedGameTypeWhenCreated",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{ {
"inputs": [], "inputs": [],
"name": "weth", "name": "weth",
...@@ -852,6 +942,19 @@ ...@@ -852,6 +942,19 @@
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
}, },
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "enum BondDistributionMode",
"name": "bondDistributionMode",
"type": "uint8"
}
],
"name": "GameClosed",
"type": "event"
},
{ {
"anonymous": false, "anonymous": false,
"inputs": [ "inputs": [
...@@ -960,16 +1063,31 @@ ...@@ -960,16 +1063,31 @@
"name": "GameDepthExceeded", "name": "GameDepthExceeded",
"type": "error" "type": "error"
}, },
{
"inputs": [],
"name": "GameNotFinalized",
"type": "error"
},
{ {
"inputs": [], "inputs": [],
"name": "GameNotInProgress", "name": "GameNotInProgress",
"type": "error" "type": "error"
}, },
{
"inputs": [],
"name": "GameNotResolved",
"type": "error"
},
{ {
"inputs": [], "inputs": [],
"name": "IncorrectBondAmount", "name": "IncorrectBondAmount",
"type": "error" "type": "error"
}, },
{
"inputs": [],
"name": "InvalidBondDistributionMode",
"type": "error"
},
{ {
"inputs": [], "inputs": [],
"name": "InvalidChallengePeriod", "name": "InvalidChallengePeriod",
...@@ -1045,6 +1163,11 @@ ...@@ -1045,6 +1163,11 @@
"name": "OutOfOrderResolution", "name": "OutOfOrderResolution",
"type": "error" "type": "error"
}, },
{
"inputs": [],
"name": "ReservedGameType",
"type": "error"
},
{ {
"inputs": [], "inputs": [],
"name": "UnexpectedList", "name": "UnexpectedList",
......
...@@ -812,6 +812,11 @@ ...@@ -812,6 +812,11 @@
"name": "LargeCalldata", "name": "LargeCalldata",
"type": "error" "type": "error"
}, },
{
"inputs": [],
"name": "LegacyGame",
"type": "error"
},
{ {
"inputs": [], "inputs": [],
"name": "NonReentrant", "name": "NonReentrant",
......
...@@ -835,6 +835,11 @@ ...@@ -835,6 +835,11 @@
"name": "LargeCalldata", "name": "LargeCalldata",
"type": "error" "type": "error"
}, },
{
"inputs": [],
"name": "LegacyGame",
"type": "error"
},
{ {
"inputs": [], "inputs": [],
"name": "NonReentrant", "name": "NonReentrant",
......
...@@ -144,6 +144,19 @@ ...@@ -144,6 +144,19 @@
"stateMutability": "payable", "stateMutability": "payable",
"type": "function" "type": "function"
}, },
{
"inputs": [],
"name": "bondDistributionMode",
"outputs": [
{
"internalType": "enum BondDistributionMode",
"name": "",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
},
{ {
"inputs": [ "inputs": [
{ {
...@@ -304,6 +317,13 @@ ...@@ -304,6 +317,13 @@
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
}, },
{
"inputs": [],
"name": "closeGame",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{ {
"inputs": [], "inputs": [],
"name": "createdAt", "name": "createdAt",
...@@ -321,7 +341,7 @@ ...@@ -321,7 +341,7 @@
"inputs": [ "inputs": [
{ {
"internalType": "address", "internalType": "address",
"name": "", "name": "_recipient",
"type": "address" "type": "address"
} }
], ],
...@@ -329,7 +349,7 @@ ...@@ -329,7 +349,7 @@
"outputs": [ "outputs": [
{ {
"internalType": "uint256", "internalType": "uint256",
"name": "", "name": "credit_",
"type": "uint256" "type": "uint256"
} }
], ],
...@@ -478,6 +498,25 @@ ...@@ -478,6 +498,25 @@
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
}, },
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "hasUnlockedCredit",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{ {
"inputs": [], "inputs": [],
"name": "initialize", "name": "initialize",
...@@ -604,6 +643,25 @@ ...@@ -604,6 +643,25 @@
"stateMutability": "payable", "stateMutability": "payable",
"type": "function" "type": "function"
}, },
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "normalModeCredit",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{ {
"inputs": [], "inputs": [],
"name": "proposer", "name": "proposer",
...@@ -617,6 +675,25 @@ ...@@ -617,6 +675,25 @@
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
}, },
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "refundModeCredit",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{ {
"inputs": [ "inputs": [
{ {
...@@ -875,6 +952,19 @@ ...@@ -875,6 +952,19 @@
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
}, },
{
"inputs": [],
"name": "wasRespectedGameTypeWhenCreated",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{ {
"inputs": [], "inputs": [],
"name": "weth", "name": "weth",
...@@ -888,6 +978,19 @@ ...@@ -888,6 +978,19 @@
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
}, },
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "enum BondDistributionMode",
"name": "bondDistributionMode",
"type": "uint8"
}
],
"name": "GameClosed",
"type": "event"
},
{ {
"anonymous": false, "anonymous": false,
"inputs": [ "inputs": [
...@@ -1001,16 +1104,31 @@ ...@@ -1001,16 +1104,31 @@
"name": "GameDepthExceeded", "name": "GameDepthExceeded",
"type": "error" "type": "error"
}, },
{
"inputs": [],
"name": "GameNotFinalized",
"type": "error"
},
{ {
"inputs": [], "inputs": [],
"name": "GameNotInProgress", "name": "GameNotInProgress",
"type": "error" "type": "error"
}, },
{
"inputs": [],
"name": "GameNotResolved",
"type": "error"
},
{ {
"inputs": [], "inputs": [],
"name": "IncorrectBondAmount", "name": "IncorrectBondAmount",
"type": "error" "type": "error"
}, },
{
"inputs": [],
"name": "InvalidBondDistributionMode",
"type": "error"
},
{ {
"inputs": [], "inputs": [],
"name": "InvalidChallengePeriod", "name": "InvalidChallengePeriod",
...@@ -1086,6 +1204,11 @@ ...@@ -1086,6 +1204,11 @@
"name": "OutOfOrderResolution", "name": "OutOfOrderResolution",
"type": "error" "type": "error"
}, },
{
"inputs": [],
"name": "ReservedGameType",
"type": "error"
},
{ {
"inputs": [], "inputs": [],
"name": "UnexpectedList", "name": "UnexpectedList",
......
...@@ -20,12 +20,12 @@ ...@@ -20,12 +20,12 @@
"sourceCodeHash": "0xdfd5c91e5ddbbf2ad82b867cbf7403437decd6ca70b87891eec935665f17ffd5" "sourceCodeHash": "0xdfd5c91e5ddbbf2ad82b867cbf7403437decd6ca70b87891eec935665f17ffd5"
}, },
"src/L1/OptimismPortal2.sol": { "src/L1/OptimismPortal2.sol": {
"initCodeHash": "0x2121a97875875150106a54a71c6c4c03afe90b3364e416be047f55fdeab57204", "initCodeHash": "0x969e3687d4497cc168af61e610ba0ae187e80f86aaa7b5d5bb598de19f279f08",
"sourceCodeHash": "0x96e3de3ef0025a6def702eeb481acd2d2d88971fd418be657472f51a98029773" "sourceCodeHash": "0xf215a31954f2ef166cfb26d20e466c62fafa235a08fc42c55131dcb81998ff01"
}, },
"src/L1/OptimismPortalInterop.sol": { "src/L1/OptimismPortalInterop.sol": {
"initCodeHash": "0x09ffe45f91bf59315b9fd4a2941b819ed8b1bb0d8643a630c6193bd67acea0ed", "initCodeHash": "0x057c56174304f3773654fed39abf5fab70d9446f531d07fdb225b738a680ad46",
"sourceCodeHash": "0xbb6acc3e88af9594ffcb8a2f30860511b76e09024330e70052316668fe55fd1f" "sourceCodeHash": "0xc04a7f9c14a13ec3587f5cc351c8e9f27fbbe9f1291a1aba07de29edbeef418a"
}, },
"src/L1/ProtocolVersions.sol": { "src/L1/ProtocolVersions.sol": {
"initCodeHash": "0x0000ec89712d8b4609873f1ba76afffd4205bf9110818995c90134dbec12e91e", "initCodeHash": "0x0000ec89712d8b4609873f1ba76afffd4205bf9110818995c90134dbec12e91e",
...@@ -152,20 +152,20 @@ ...@@ -152,20 +152,20 @@
"sourceCodeHash": "0xb7b0a06cd971c4647247dc19ce997d0c64a73e87c81d30731da9cf9efa1b952a" "sourceCodeHash": "0xb7b0a06cd971c4647247dc19ce997d0c64a73e87c81d30731da9cf9efa1b952a"
}, },
"src/dispute/AnchorStateRegistry.sol": { "src/dispute/AnchorStateRegistry.sol": {
"initCodeHash": "0xfbeeac40d86d13e71c7add66eef6357576a93b6a175c9cff6ec6ef587fe3acc4", "initCodeHash": "0xb2618d650808a7a335db7cc56d15ccaf432f50aa551c01be8bde8356893c0e0d",
"sourceCodeHash": "0xbb2e08da74d470fc30dd35dc39834e19f676a45974aa2403eb97e84bc5bed0a8" "sourceCodeHash": "0x745f0e2b07b8f6492e11ca2f69b53d129177fbfd346d5ca4729d72792aff1f83"
}, },
"src/dispute/DelayedWETH.sol": { "src/dispute/DelayedWETH.sol": {
"initCodeHash": "0x759d7f9c52b7c13ce4502f39dae3a75d130c6278240cde0b60ae84616aa2bd48", "initCodeHash": "0xb1f04c9ee86984a157b92a18754c84104e9d4df7a3838633301ca7f557d0220a",
"sourceCodeHash": "0x4406c78e0557bedb88b4ee5977acb1ef13e7bd92b7dbf79f56f8bad95c53e229" "sourceCodeHash": "0x0162302b9c71f184d45bee34ecfb1dfbf427f38fc5652709ab7ffef1ac816d82"
}, },
"src/dispute/DisputeGameFactory.sol": { "src/dispute/DisputeGameFactory.sol": {
"initCodeHash": "0xa728192115c5fdb08c633a0899043318289b1d413d7afeed06356008b2a5a7fa", "initCodeHash": "0xa728192115c5fdb08c633a0899043318289b1d413d7afeed06356008b2a5a7fa",
"sourceCodeHash": "0x155c0334f63616ed245aadf9a94f419ef7d5e2237b3b32172484fd19890a61dc" "sourceCodeHash": "0x155c0334f63616ed245aadf9a94f419ef7d5e2237b3b32172484fd19890a61dc"
}, },
"src/dispute/FaultDisputeGame.sol": { "src/dispute/FaultDisputeGame.sol": {
"initCodeHash": "0x423e8488731c0b0f87b435174f412c09fbf0b17eb0b8c9a03efa37d779ec0cae", "initCodeHash": "0x152fbb1f82488d815f56087fc464b9478f1390e3ecd67ae595344115fdd9ba91",
"sourceCodeHash": "0xe53b970922b309ada1c59f94d5935ffca669e909c797f17ba8a3d309c487e7e8" "sourceCodeHash": "0x9bfea41bd993bc1ef2ede9a5846a432ed5ea183868634fd77c4068b0a4a779b2"
}, },
"src/legacy/DeployerWhitelist.sol": { "src/legacy/DeployerWhitelist.sol": {
"initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29",
......
...@@ -50,7 +50,7 @@ ...@@ -50,7 +50,7 @@
}, },
{ {
"bytes": "32", "bytes": "32",
"label": "credit", "label": "normalModeCredit",
"offset": 0, "offset": 0,
"slot": "3", "slot": "3",
"type": "mapping(address => uint256)" "type": "mapping(address => uint256)"
...@@ -89,5 +89,33 @@ ...@@ -89,5 +89,33 @@
"offset": 0, "offset": 0,
"slot": "8", "slot": "8",
"type": "struct OutputRoot" "type": "struct OutputRoot"
},
{
"bytes": "1",
"label": "wasRespectedGameTypeWhenCreated",
"offset": 0,
"slot": "10",
"type": "bool"
},
{
"bytes": "32",
"label": "refundModeCredit",
"offset": 0,
"slot": "11",
"type": "mapping(address => uint256)"
},
{
"bytes": "32",
"label": "hasUnlockedCredit",
"offset": 0,
"slot": "12",
"type": "mapping(address => bool)"
},
{
"bytes": "1",
"label": "bondDistributionMode",
"offset": 0,
"slot": "13",
"type": "enum BondDistributionMode"
} }
] ]
\ No newline at end of file
...@@ -50,7 +50,7 @@ ...@@ -50,7 +50,7 @@
}, },
{ {
"bytes": "32", "bytes": "32",
"label": "credit", "label": "normalModeCredit",
"offset": 0, "offset": 0,
"slot": "3", "slot": "3",
"type": "mapping(address => uint256)" "type": "mapping(address => uint256)"
...@@ -89,5 +89,33 @@ ...@@ -89,5 +89,33 @@
"offset": 0, "offset": 0,
"slot": "8", "slot": "8",
"type": "struct OutputRoot" "type": "struct OutputRoot"
},
{
"bytes": "1",
"label": "wasRespectedGameTypeWhenCreated",
"offset": 0,
"slot": "10",
"type": "bool"
},
{
"bytes": "32",
"label": "refundModeCredit",
"offset": 0,
"slot": "11",
"type": "mapping(address => uint256)"
},
{
"bytes": "32",
"label": "hasUnlockedCredit",
"offset": 0,
"slot": "12",
"type": "mapping(address => bool)"
},
{
"bytes": "1",
"label": "bondDistributionMode",
"offset": 0,
"slot": "13",
"type": "enum BondDistributionMode"
} }
] ]
\ No newline at end of file
...@@ -28,7 +28,8 @@ import { ...@@ -28,7 +28,8 @@ import {
Blacklisted, Blacklisted,
Unproven, Unproven,
ProposalNotValidated, ProposalNotValidated,
AlreadyFinalized AlreadyFinalized,
LegacyGame
} from "src/libraries/PortalErrors.sol"; } from "src/libraries/PortalErrors.sol";
import { GameStatus, GameType, Claim, Timestamp } from "src/dispute/lib/Types.sol"; import { GameStatus, GameType, Claim, Timestamp } from "src/dispute/lib/Types.sol";
...@@ -176,9 +177,9 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { ...@@ -176,9 +177,9 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver {
} }
/// @notice Semantic version. /// @notice Semantic version.
/// @custom:semver 3.11.0-beta.11 /// @custom:semver 3.12.0-beta.1
function version() public pure virtual returns (string memory) { function version() public pure virtual returns (string memory) {
return "3.11.0-beta.11"; return "3.12.0-beta.1";
} }
/// @notice Constructs the OptimismPortal contract. /// @notice Constructs the OptimismPortal contract.
...@@ -308,6 +309,24 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { ...@@ -308,6 +309,24 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver {
// The game type of the dispute game must be the respected game type. // The game type of the dispute game must be the respected game type.
if (gameType.raw() != respectedGameType.raw()) revert InvalidGameType(); if (gameType.raw() != respectedGameType.raw()) revert InvalidGameType();
// The game type of the DisputeGame must have been the respected game type at creation.
try gameProxy.wasRespectedGameTypeWhenCreated() returns (bool wasRespected_) {
if (!wasRespected_) revert InvalidGameType();
} catch {
revert LegacyGame();
}
// Game must have been created after the respected game type was updated. This check is a
// strict inequality because we want to prevent users from being able to prove or finalize
// withdrawals against games that were created in the same block that the retirement
// timestamp was set. If the retirement timestamp and game type are changed in the same
// block, such games could still be considered valid even if they used the old game type
// that we intended to invalidate.
require(
gameProxy.createdAt().raw() > respectedGameTypeUpdatedAt,
"OptimismPortal: dispute game created before respected game type was updated"
);
// Verify that the output root can be generated with the elements in the proof. // Verify that the output root can be generated with the elements in the proof.
if (outputRoot.raw() != Hashing.hashOutputRootProof(_outputRootProof)) revert InvalidProof(); if (outputRoot.raw() != Hashing.hashOutputRootProof(_outputRootProof)) revert InvalidProof();
...@@ -476,9 +495,16 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { ...@@ -476,9 +495,16 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver {
/// @param _gameType The game type to consult for output proposals. /// @param _gameType The game type to consult for output proposals.
function setRespectedGameType(GameType _gameType) external { function setRespectedGameType(GameType _gameType) external {
if (msg.sender != guardian()) revert Unauthorized(); if (msg.sender != guardian()) revert Unauthorized();
respectedGameType = _gameType; // respectedGameTypeUpdatedAt is now no longer set by default. We want to avoid modifying
// this function's signature as that would result in changes to the DeputyGuardianModule.
// We use type(uint32).max as a temporary solution to allow us to update the
// respectedGameTypeUpdatedAt timestamp without modifying this function's signature.
if (_gameType.raw() == type(uint32).max) {
respectedGameTypeUpdatedAt = uint64(block.timestamp); respectedGameTypeUpdatedAt = uint64(block.timestamp);
emit RespectedGameTypeSet(_gameType, Timestamp.wrap(respectedGameTypeUpdatedAt)); } else {
respectedGameType = _gameType;
}
emit RespectedGameTypeSet(respectedGameType, Timestamp.wrap(respectedGameTypeUpdatedAt));
} }
/// @notice Checks if a withdrawal can be finalized. This function will revert if the withdrawal cannot be /// @notice Checks if a withdrawal can be finalized. This function will revert if the withdrawal cannot be
...@@ -497,6 +523,7 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { ...@@ -497,6 +523,7 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver {
// a timestamp of zero. // a timestamp of zero.
if (provenWithdrawal.timestamp == 0) revert Unproven(); if (provenWithdrawal.timestamp == 0) revert Unproven();
// Grab the createdAt timestamp once.
uint64 createdAt = disputeGameProxy.createdAt().raw(); uint64 createdAt = disputeGameProxy.createdAt().raw();
// As a sanity check, we make sure that the proven withdrawal's timestamp is greater than // As a sanity check, we make sure that the proven withdrawal's timestamp is greater than
...@@ -518,15 +545,25 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { ...@@ -518,15 +545,25 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver {
// from finalizing withdrawals proven against non-finalized output roots. // from finalizing withdrawals proven against non-finalized output roots.
if (disputeGameProxy.status() != GameStatus.DEFENDER_WINS) revert ProposalNotValidated(); if (disputeGameProxy.status() != GameStatus.DEFENDER_WINS) revert ProposalNotValidated();
// The game type of the dispute game must be the respected game type. This was also checked in // The game type of the dispute game must have been the respected game type at creation
// `proveWithdrawalTransaction`, but we check it again in case the respected game type has changed since // time. We check that the game type is the respected game type at proving time, but it's
// the withdrawal was proven. // possible that the respected game type has since changed. Users can still use this game
if (disputeGameProxy.gameType().raw() != respectedGameType.raw()) revert InvalidGameType(); // to finalize a withdrawal as long as it has not been otherwise invalidated.
// The game type of the DisputeGame must have been the respected game type at creation.
try disputeGameProxy.wasRespectedGameTypeWhenCreated() returns (bool wasRespected_) {
if (!wasRespected_) revert InvalidGameType();
} catch {
revert LegacyGame();
}
// The game must have been created after `respectedGameTypeUpdatedAt`. This is to prevent users from creating // Game must have been created after the respected game type was updated. This check is a
// invalid disputes against a deployed game type while the off-chain challenge agents are not watching. // strict inequality because we want to prevent users from being able to prove or finalize
// withdrawals against games that were created in the same block that the retirement
// timestamp was set. If the retirement timestamp and game type are changed in the same
// block, such games could still be considered valid even if they used the old game type
// that we intended to invalidate.
require( require(
createdAt >= respectedGameTypeUpdatedAt, createdAt > respectedGameTypeUpdatedAt,
"OptimismPortal: dispute game created before respected game type was updated" "OptimismPortal: dispute game created before respected game type was updated"
); );
......
...@@ -28,9 +28,9 @@ contract OptimismPortalInterop is OptimismPortal2 { ...@@ -28,9 +28,9 @@ contract OptimismPortalInterop is OptimismPortal2 {
OptimismPortal2(_proofMaturityDelaySeconds, _disputeGameFinalityDelaySeconds) OptimismPortal2(_proofMaturityDelaySeconds, _disputeGameFinalityDelaySeconds)
{ } { }
/// @custom:semver +interop-beta.8 /// @custom:semver +interop-beta.9
function version() public pure override returns (string memory) { function version() public pure override returns (string memory) {
return string.concat(super.version(), "+interop-beta.8"); return string.concat(super.version(), "+interop-beta.9");
} }
/// @notice Sets static configuration options for the L2 system. /// @notice Sets static configuration options for the L2 system.
......
...@@ -23,8 +23,8 @@ import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; ...@@ -23,8 +23,8 @@ import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol";
/// be initialized with a more recent starting state which reduces the amount of required offchain computation. /// be initialized with a more recent starting state which reduces the amount of required offchain computation.
contract AnchorStateRegistry is Initializable, ISemver { contract AnchorStateRegistry is Initializable, ISemver {
/// @notice Semantic version. /// @notice Semantic version.
/// @custom:semver 2.1.0-beta.1 /// @custom:semver 2.2.0-beta.1
string public constant version = "2.1.0-beta.1"; string public constant version = "2.2.0-beta.1";
/// @notice Address of the SuperchainConfig contract. /// @notice Address of the SuperchainConfig contract.
ISuperchainConfig public superchainConfig; ISuperchainConfig public superchainConfig;
...@@ -52,12 +52,12 @@ contract AnchorStateRegistry is Initializable, ISemver { ...@@ -52,12 +52,12 @@ contract AnchorStateRegistry is Initializable, ISemver {
/// @notice Thrown when an unauthorized caller attempts to set the anchor state. /// @notice Thrown when an unauthorized caller attempts to set the anchor state.
error AnchorStateRegistry_Unauthorized(); error AnchorStateRegistry_Unauthorized();
/// @notice Thrown when an improper anchor game is provided.
error AnchorStateRegistry_ImproperAnchorGame();
/// @notice Thrown when an invalid anchor game is provided. /// @notice Thrown when an invalid anchor game is provided.
error AnchorStateRegistry_InvalidAnchorGame(); error AnchorStateRegistry_InvalidAnchorGame();
/// @notice Thrown when the anchor root is requested, but the anchor game is blacklisted.
error AnchorStateRegistry_AnchorGameBlacklisted();
/// @notice Constructor to disable initializers. /// @notice Constructor to disable initializers.
constructor() { constructor() {
_disableInitializers(); _disableInitializers();
...@@ -83,6 +83,12 @@ contract AnchorStateRegistry is Initializable, ISemver { ...@@ -83,6 +83,12 @@ contract AnchorStateRegistry is Initializable, ISemver {
startingAnchorRoot = _startingAnchorRoot; startingAnchorRoot = _startingAnchorRoot;
} }
/// @notice Returns the respected game type.
/// @return The respected game type.
function respectedGameType() public view returns (GameType) {
return portal.respectedGameType();
}
/// @custom:legacy /// @custom:legacy
/// @notice Returns the anchor root. Note that this is a legacy deprecated function and will /// @notice Returns the anchor root. Note that this is a legacy deprecated function and will
/// be removed in a future release. Use getAnchorRoot() instead. Anchor roots are no /// be removed in a future release. Use getAnchorRoot() instead. Anchor roots are no
...@@ -100,6 +106,10 @@ contract AnchorStateRegistry is Initializable, ISemver { ...@@ -100,6 +106,10 @@ contract AnchorStateRegistry is Initializable, ISemver {
return (startingAnchorRoot.root, startingAnchorRoot.l2BlockNumber); return (startingAnchorRoot.root, startingAnchorRoot.l2BlockNumber);
} }
if (isGameBlacklisted(anchorGame)) {
revert AnchorStateRegistry_AnchorGameBlacklisted();
}
// Otherwise, return the anchor root. // Otherwise, return the anchor root.
return (Hash.wrap(anchorGame.rootClaim().raw()), anchorGame.l2BlockNumber()); return (Hash.wrap(anchorGame.rootClaim().raw()), anchorGame.l2BlockNumber());
} }
...@@ -123,7 +133,7 @@ contract AnchorStateRegistry is Initializable, ISemver { ...@@ -123,7 +133,7 @@ contract AnchorStateRegistry is Initializable, ISemver {
/// @param _game The game to check. /// @param _game The game to check.
/// @return Whether the game is of a respected game type. /// @return Whether the game is of a respected game type.
function isGameRespected(IDisputeGame _game) public view returns (bool) { function isGameRespected(IDisputeGame _game) public view returns (bool) {
return _game.gameType().raw() == portal.respectedGameType().raw(); return _game.wasRespectedGameTypeWhenCreated();
} }
/// @notice Determines whether a game is blacklisted. /// @notice Determines whether a game is blacklisted.
...@@ -137,9 +147,25 @@ contract AnchorStateRegistry is Initializable, ISemver { ...@@ -137,9 +147,25 @@ contract AnchorStateRegistry is Initializable, ISemver {
/// @param _game The game to check. /// @param _game The game to check.
/// @return Whether the game is retired. /// @return Whether the game is retired.
function isGameRetired(IDisputeGame _game) public view returns (bool) { function isGameRetired(IDisputeGame _game) public view returns (bool) {
// Must be created at or after the respectedGameTypeUpdatedAt timestamp. Note that the // Must be created after the respectedGameTypeUpdatedAt timestamp. Note that this means all
// strict inequality exactly mirrors the logic in the OptimismPortal contract. // games created in the same block as the respectedGameTypeUpdatedAt timestamp are
return _game.createdAt().raw() < portal.respectedGameTypeUpdatedAt(); // considered retired.
return _game.createdAt().raw() <= portal.respectedGameTypeUpdatedAt();
}
/// @notice Returns whether a game is resolved.
/// @param _game The game to check.
/// @return Whether the game is resolved.
function isGameResolved(IDisputeGame _game) public view returns (bool) {
return _game.resolvedAt().raw() != 0
&& (_game.status() == GameStatus.DEFENDER_WINS || _game.status() == GameStatus.CHALLENGER_WINS);
}
/// @notice Returns whether a game is beyond the airgap period.
/// @param _game The game to check.
/// @return Whether the game is beyond the airgap period.
function isGameAirgapped(IDisputeGame _game) public view returns (bool) {
return block.timestamp - _game.resolvedAt().raw() > portal.disputeGameFinalityDelaySeconds();
} }
/// @notice **READ THIS FUNCTION DOCUMENTATION CAREFULLY.** /// @notice **READ THIS FUNCTION DOCUMENTATION CAREFULLY.**
...@@ -147,13 +173,12 @@ contract AnchorStateRegistry is Initializable, ISemver { ...@@ -147,13 +173,12 @@ contract AnchorStateRegistry is Initializable, ISemver {
/// invalidation conditions. The root claim of a proper game IS NOT guaranteed to be /// invalidation conditions. The root claim of a proper game IS NOT guaranteed to be
/// valid. The root claim of a proper game CAN BE incorrect and still be a proper game. /// valid. The root claim of a proper game CAN BE incorrect and still be a proper game.
/// DO NOT USE THIS FUNCTION ALONE TO DETERMINE IF A ROOT CLAIM IS VALID. /// DO NOT USE THIS FUNCTION ALONE TO DETERMINE IF A ROOT CLAIM IS VALID.
/// @dev Note that it is possible for games to be created when their game type is not the /// @dev Note that isGameProper previously checked that the game type was equal to the
/// respected game type. We do not consider these games to be Proper Games. isGameProper() /// respected game type. However, it should be noted that it is possible for a game other
/// can currently guarantee this because the OptimismPortal contract will always set the /// than the respected game type to resolve without being invalidated. Since isGameProper
/// retirement timestamp whenever the respected game type is updated such that any games /// exists to determine if a game has (or has not) been invalidated, we now allow any game
/// created before any update of the respected game type are automatically retired. If /// type to be considered a proper game. We enforce checks on the game type in
/// this coupling is broken, then we must instead check that the game type *was* the /// isGameClaimValid().
/// respected game type at the time of the game's creation.
/// @param _game The game to check. /// @param _game The game to check.
/// @return Whether the game is a proper game. /// @return Whether the game is a proper game.
function isGameProper(IDisputeGame _game) public view returns (bool) { function isGameProper(IDisputeGame _game) public view returns (bool) {
...@@ -162,11 +187,6 @@ contract AnchorStateRegistry is Initializable, ISemver { ...@@ -162,11 +187,6 @@ contract AnchorStateRegistry is Initializable, ISemver {
return false; return false;
} }
// Must be respected game type.
if (!isGameRespected(_game)) {
return false;
}
// Must not be blacklisted. // Must not be blacklisted.
if (isGameBlacklisted(_game)) { if (isGameBlacklisted(_game)) {
return false; return false;
...@@ -180,60 +200,80 @@ contract AnchorStateRegistry is Initializable, ISemver { ...@@ -180,60 +200,80 @@ contract AnchorStateRegistry is Initializable, ISemver {
return true; return true;
} }
/// @notice Allows FaultDisputeGame contracts to attempt to become the new anchor game. A game /// @notice Returns whether a game is finalized.
/// can only become the new anchor game if it is not invalid (it is a Proper Game), it /// @param _game The game to check.
/// resolved in favor of the root claim, and it is newer than the current anchor game. /// @return Whether the game is finalized.
function tryUpdateAnchorState() external { function isGameFinalized(IDisputeGame _game) public view returns (bool) {
// Grab the game. // Game must be resolved.
IFaultDisputeGame game = IFaultDisputeGame(msg.sender); if (!isGameResolved(_game)) {
return false;
// Check if the game is a proper game.
if (!isGameProper(game)) {
emit AnchorNotUpdated(game);
return;
} }
// Must be a game that resolved in favor of the state. // Game must be beyond the airgap period.
if (game.status() != GameStatus.DEFENDER_WINS) { if (!isGameAirgapped(_game)) {
emit AnchorNotUpdated(game); return false;
return;
} }
// Must be newer than the current anchor game. return true;
(, uint256 anchorL2BlockNumber) = getAnchorRoot();
if (game.l2BlockNumber() <= anchorL2BlockNumber) {
emit AnchorNotUpdated(game);
return;
} }
// Update the anchor game. /// @notice Returns whether a game's root claim is valid.
anchorGame = game; /// @param _game The game to check.
emit AnchorUpdated(game); /// @return Whether the game's root claim is valid.
function isGameClaimValid(IDisputeGame _game) public view returns (bool) {
// Game must be a proper game.
bool properGame = isGameProper(_game);
if (!properGame) {
return false;
} }
/// @notice Sets the anchor state given the game. Can only be triggered by the Guardian // Must be respected.
/// address. Unlike tryUpdateAnchorState(), this function does not check if the bool respected = isGameRespected(_game);
/// provided is newer than the existing anchor game. This allows the Guardian to if (!respected) {
/// recover from situations in which the current anchor game is invalid. return false;
/// @param _game The game to set the anchor state for.
function setAnchorState(IFaultDisputeGame _game) external {
// Function can only be triggered by the guardian.
if (msg.sender != superchainConfig.guardian()) {
revert AnchorStateRegistry_Unauthorized();
} }
// Check if the game is a proper game. // Game must be finalized.
if (!isGameProper(_game)) { bool finalized = isGameFinalized(_game);
revert AnchorStateRegistry_ImproperAnchorGame(); if (!finalized) {
return false;
} }
// The game must have resolved in favor of the root claim. // Game must be resolved in favor of the defender.
if (_game.status() != GameStatus.DEFENDER_WINS) { if (_game.status() != GameStatus.DEFENDER_WINS) {
return false;
}
return true;
}
/// @notice Updates the anchor game.
/// @param _game New candidate anchor game.
function setAnchorState(IDisputeGame _game) public {
// Convert game to FaultDisputeGame.
// We can't use FaultDisputeGame in the interface because this function is called from the
// FaultDisputeGame contract which can't import IFaultDisputeGame by convention. We should
// likely introduce a new interface (e.g., StateDisputeGame) that can act as a more useful
// version of IDisputeGame in the future.
IFaultDisputeGame game = IFaultDisputeGame(address(_game));
// Check if the candidate game is valid.
bool valid = isGameClaimValid(game);
if (!valid) {
revert AnchorStateRegistry_InvalidAnchorGame();
}
// Must be newer than the current anchor game.
// Note that this WILL block/brick if getAnchorRoot() ever reverts because the current
// anchor game is blacklisted. A blacklisted anchor game is *very* bad and we deliberately
// want to force the situation to be handled manually.
(, uint256 anchorL2BlockNumber) = getAnchorRoot();
if (game.l2BlockNumber() <= anchorL2BlockNumber) {
revert AnchorStateRegistry_InvalidAnchorGame(); revert AnchorStateRegistry_InvalidAnchorGame();
} }
// Update the anchor game. // Update the anchor game.
anchorGame = _game; anchorGame = game;
emit AnchorUpdated(_game); emit AnchorUpdated(game);
} }
} }
...@@ -26,14 +26,9 @@ contract DelayedWETH is OwnableUpgradeable, WETH98, ISemver { ...@@ -26,14 +26,9 @@ contract DelayedWETH is OwnableUpgradeable, WETH98, ISemver {
uint256 timestamp; uint256 timestamp;
} }
/// @notice Emitted when an unwrap is started.
/// @param src The address that started the unwrap.
/// @param wad The amount of WETH that was unwrapped.
event Unwrap(address indexed src, uint256 wad);
/// @notice Semantic version. /// @notice Semantic version.
/// @custom:semver 1.2.0-beta.5 /// @custom:semver 1.3.0-beta.1
string public constant version = "1.2.0-beta.5"; string public constant version = "1.3.0-beta.1";
/// @notice Returns a withdrawal request for the given address. /// @notice Returns a withdrawal request for the given address.
mapping(address => mapping(address => WithdrawalRequest)) public withdrawals; mapping(address => mapping(address => WithdrawalRequest)) public withdrawals;
...@@ -107,12 +102,19 @@ contract DelayedWETH is OwnableUpgradeable, WETH98, ISemver { ...@@ -107,12 +102,19 @@ contract DelayedWETH is OwnableUpgradeable, WETH98, ISemver {
require(success, "DelayedWETH: recover failed"); require(success, "DelayedWETH: recover failed");
} }
/// @notice Allows the owner to recover from error cases by pulling ETH from a specific owner. /// @notice Allows the owner to recover from error cases by pulling all WETH from a specific owner.
/// @param _guy The address to recover the WETH from.
function hold(address _guy) external {
hold(_guy, balanceOf(_guy));
}
/// @notice Allows the owner to recover from error cases by pulling a specific amount of WETH from a specific owner.
/// @param _guy The address to recover the WETH from. /// @param _guy The address to recover the WETH from.
/// @param _wad The amount of WETH to recover. /// @param _wad The amount of WETH to recover.
function hold(address _guy, uint256 _wad) external { function hold(address _guy, uint256 _wad) public {
require(msg.sender == owner(), "DelayedWETH: not owner"); require(msg.sender == owner(), "DelayedWETH: not owner");
_allowance[_guy][msg.sender] = _wad; _allowance[_guy][msg.sender] = _wad;
emit Approval(_guy, msg.sender, _wad); emit Approval(_guy, msg.sender, _wad);
transferFrom(_guy, msg.sender, _wad);
} }
} }
...@@ -11,6 +11,7 @@ import { RLPReader } from "src/libraries/rlp/RLPReader.sol"; ...@@ -11,6 +11,7 @@ import { RLPReader } from "src/libraries/rlp/RLPReader.sol";
import { import {
GameStatus, GameStatus,
GameType, GameType,
BondDistributionMode,
Claim, Claim,
Clock, Clock,
Duration, Duration,
...@@ -51,7 +52,11 @@ import { ...@@ -51,7 +52,11 @@ import {
BondTransferFailed, BondTransferFailed,
NoCreditToClaim, NoCreditToClaim,
InvalidOutputRootProof, InvalidOutputRootProof,
ClaimAboveSplit ClaimAboveSplit,
GameNotFinalized,
InvalidBondDistributionMode,
GameNotResolved,
ReservedGameType
} from "src/dispute/lib/Errors.sol"; } from "src/dispute/lib/Errors.sol";
// Interfaces // Interfaces
...@@ -59,6 +64,7 @@ import { ISemver } from "interfaces/universal/ISemver.sol"; ...@@ -59,6 +64,7 @@ import { ISemver } from "interfaces/universal/ISemver.sol";
import { IDelayedWETH } from "interfaces/dispute/IDelayedWETH.sol"; import { IDelayedWETH } from "interfaces/dispute/IDelayedWETH.sol";
import { IBigStepper, IPreimageOracle } from "interfaces/dispute/IBigStepper.sol"; import { IBigStepper, IPreimageOracle } from "interfaces/dispute/IBigStepper.sol";
import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol";
import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol";
/// @title FaultDisputeGame /// @title FaultDisputeGame
/// @notice An implementation of the `IFaultDisputeGame` interface. /// @notice An implementation of the `IFaultDisputeGame` interface.
...@@ -115,6 +121,9 @@ contract FaultDisputeGame is Clone, ISemver { ...@@ -115,6 +121,9 @@ contract FaultDisputeGame is Clone, ISemver {
/// @param claimant The address of the claimant /// @param claimant The address of the claimant
event Move(uint256 indexed parentIndex, Claim indexed claim, address indexed claimant); event Move(uint256 indexed parentIndex, Claim indexed claim, address indexed claimant);
/// @notice Emitted when the game is closed.
event GameClosed(BondDistributionMode bondDistributionMode);
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
// State Vars // // State Vars //
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
...@@ -161,8 +170,8 @@ contract FaultDisputeGame is Clone, ISemver { ...@@ -161,8 +170,8 @@ contract FaultDisputeGame is Clone, ISemver {
uint256 internal constant HEADER_BLOCK_NUMBER_INDEX = 8; uint256 internal constant HEADER_BLOCK_NUMBER_INDEX = 8;
/// @notice Semantic version. /// @notice Semantic version.
/// @custom:semver 1.3.1-beta.9 /// @custom:semver 1.4.0-beta.1
string public constant version = "1.3.1-beta.9"; string public constant version = "1.4.0-beta.1";
/// @notice The starting timestamp of the game /// @notice The starting timestamp of the game
Timestamp public createdAt; Timestamp public createdAt;
...@@ -187,7 +196,7 @@ contract FaultDisputeGame is Clone, ISemver { ...@@ -187,7 +196,7 @@ contract FaultDisputeGame is Clone, ISemver {
ClaimData[] public claimData; ClaimData[] public claimData;
/// @notice Credited balances for winning participants. /// @notice Credited balances for winning participants.
mapping(address => uint256) public credit; mapping(address => uint256) public normalModeCredit;
/// @notice A mapping to allow for constant-time lookups of existing claims. /// @notice A mapping to allow for constant-time lookups of existing claims.
mapping(Hash => bool) public claims; mapping(Hash => bool) public claims;
...@@ -204,6 +213,18 @@ contract FaultDisputeGame is Clone, ISemver { ...@@ -204,6 +213,18 @@ contract FaultDisputeGame is Clone, ISemver {
/// @notice The latest finalized output root, serving as the anchor for output bisection. /// @notice The latest finalized output root, serving as the anchor for output bisection.
OutputRoot public startingOutputRoot; OutputRoot public startingOutputRoot;
/// @notice A boolean for whether or not the game type was respected when the game was created.
bool public wasRespectedGameTypeWhenCreated;
/// @notice A mapping of each claimant's refund mode credit.
mapping(address => uint256) public refundModeCredit;
/// @notice A mapping of whether a claimant has unlocked their credit.
mapping(address => bool) public hasUnlockedCredit;
/// @notice The bond distribution mode of the game.
BondDistributionMode public bondDistributionMode;
/// @param _params Parameters for creating a new FaultDisputeGame. /// @param _params Parameters for creating a new FaultDisputeGame.
constructor(GameConstructorParams memory _params) { constructor(GameConstructorParams memory _params) {
// The max game depth may not be greater than `LibPosition.MAX_POSITION_BITLEN - 1`. // The max game depth may not be greater than `LibPosition.MAX_POSITION_BITLEN - 1`.
...@@ -239,6 +260,10 @@ contract FaultDisputeGame is Clone, ISemver { ...@@ -239,6 +260,10 @@ contract FaultDisputeGame is Clone, ISemver {
// The maximum clock extension may not be greater than the maximum clock duration. // The maximum clock extension may not be greater than the maximum clock duration.
if (uint64(maxClockExtension) > _params.maxClockDuration.raw()) revert InvalidClockExtension(); if (uint64(maxClockExtension) > _params.maxClockDuration.raw()) revert InvalidClockExtension();
// Block type(uint32).max from being used as a game type so that it can be used in the
// OptimismPortal respected game type trick.
if (_params.gameType.raw() == type(uint32).max) revert ReservedGameType();
// Set up initial game state. // Set up initial game state.
GAME_TYPE = _params.gameType; GAME_TYPE = _params.gameType;
ABSOLUTE_PRESTATE = _params.absolutePrestate; ABSOLUTE_PRESTATE = _params.absolutePrestate;
...@@ -270,7 +295,7 @@ contract FaultDisputeGame is Clone, ISemver { ...@@ -270,7 +295,7 @@ contract FaultDisputeGame is Clone, ISemver {
if (initialized) revert AlreadyInitialized(); if (initialized) revert AlreadyInitialized();
// Grab the latest anchor root. // Grab the latest anchor root.
(Hash root, uint256 rootBlockNumber) = ANCHOR_STATE_REGISTRY.anchors(GAME_TYPE); (Hash root, uint256 rootBlockNumber) = ANCHOR_STATE_REGISTRY.getAnchorRoot();
// Should only happen if this is a new game type that hasn't been set up yet. // Should only happen if this is a new game type that hasn't been set up yet.
if (root.raw() == bytes32(0)) revert AnchorRootNotFound(); if (root.raw() == bytes32(0)) revert AnchorRootNotFound();
...@@ -320,10 +345,15 @@ contract FaultDisputeGame is Clone, ISemver { ...@@ -320,10 +345,15 @@ contract FaultDisputeGame is Clone, ISemver {
initialized = true; initialized = true;
// Deposit the bond. // Deposit the bond.
refundModeCredit[gameCreator()] += msg.value;
WETH.deposit{ value: msg.value }(); WETH.deposit{ value: msg.value }();
// Set the game's starting timestamp // Set the game's starting timestamp
createdAt = Timestamp.wrap(uint64(block.timestamp)); createdAt = Timestamp.wrap(uint64(block.timestamp));
// Set whether the game type was respected when the game was created.
wasRespectedGameTypeWhenCreated =
GameType.unwrap(ANCHOR_STATE_REGISTRY.respectedGameType()) == GameType.unwrap(GAME_TYPE);
} }
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
...@@ -531,6 +561,7 @@ contract FaultDisputeGame is Clone, ISemver { ...@@ -531,6 +561,7 @@ contract FaultDisputeGame is Clone, ISemver {
subgames[_challengeIndex].push(claimData.length - 1); subgames[_challengeIndex].push(claimData.length - 1);
// Deposit the bond. // Deposit the bond.
refundModeCredit[msg.sender] += msg.value;
WETH.deposit{ value: msg.value }(); WETH.deposit{ value: msg.value }();
// Emit the appropriate event for the attack or defense. // Emit the appropriate event for the attack or defense.
...@@ -695,9 +726,6 @@ contract FaultDisputeGame is Clone, ISemver { ...@@ -695,9 +726,6 @@ contract FaultDisputeGame is Clone, ISemver {
// Update the status and emit the resolved event, note that we're performing an assignment here. // Update the status and emit the resolved event, note that we're performing an assignment here.
emit Resolved(status = status_); emit Resolved(status = status_);
// Try to update the anchor state, this should not revert.
ANCHOR_STATE_REGISTRY.tryUpdateAnchorState();
} }
/// @notice Resolves the subgame rooted at the given claim index. `_numToResolve` specifies how many children of /// @notice Resolves the subgame rooted at the given claim index. `_numToResolve` specifies how many children of
...@@ -913,16 +941,43 @@ contract FaultDisputeGame is Clone, ISemver { ...@@ -913,16 +941,43 @@ contract FaultDisputeGame is Clone, ISemver {
requiredBond_ = assumedBaseFee * requiredGas; requiredBond_ = assumedBaseFee * requiredGas;
} }
/// @notice Claim the credit belonging to the recipient address. /// @notice Claim the credit belonging to the recipient address. Reverts if the game isn't
/// finalized, if the recipient has no credit to claim, or if the bond transfer
/// fails. If the game is finalized but no bond has been paid out yet, this method
/// will determine the bond distribution mode and also try to update anchor game.
/// @param _recipient The owner and recipient of the credit. /// @param _recipient The owner and recipient of the credit.
function claimCredit(address _recipient) external { function claimCredit(address _recipient) external {
// Remove the credit from the recipient prior to performing the external call. // Close out the game and determine the bond distribution mode if not already set.
uint256 recipientCredit = credit[_recipient]; // We call this as part of claim credit to reduce the number of additional calls that a
credit[_recipient] = 0; // Challenger needs to make to this contract.
closeGame();
// Fetch the recipient's credit balance based on the bond distribution mode.
uint256 recipientCredit;
if (bondDistributionMode == BondDistributionMode.REFUND) {
recipientCredit = refundModeCredit[_recipient];
} else if (bondDistributionMode == BondDistributionMode.NORMAL) {
recipientCredit = normalModeCredit[_recipient];
} else {
// We shouldn't get here, but sanity check just in case.
revert InvalidBondDistributionMode();
}
// If the game is in refund mode, and the recipient has not unlocked their refund mode
// credit, we unlock it and return early.
if (!hasUnlockedCredit[_recipient]) {
hasUnlockedCredit[_recipient] = true;
WETH.unlock(_recipient, recipientCredit);
return;
}
// Revert if the recipient has no credit to claim. // Revert if the recipient has no credit to claim.
if (recipientCredit == 0) revert NoCreditToClaim(); if (recipientCredit == 0) revert NoCreditToClaim();
// Set the recipient's credit balances to 0.
refundModeCredit[_recipient] = 0;
normalModeCredit[_recipient] = 0;
// Try to withdraw the WETH amount so it can be used here. // Try to withdraw the WETH amount so it can be used here.
WETH.withdraw(_recipient, recipientCredit); WETH.withdraw(_recipient, recipientCredit);
...@@ -931,6 +986,50 @@ contract FaultDisputeGame is Clone, ISemver { ...@@ -931,6 +986,50 @@ contract FaultDisputeGame is Clone, ISemver {
if (!success) revert BondTransferFailed(); if (!success) revert BondTransferFailed();
} }
/// @notice Closes out the game, determines the bond distribution mode, attempts to register
/// the game as the anchor game, and emits an event.
function closeGame() public {
// If the bond distribution mode has already been determined, we can return early.
if (bondDistributionMode == BondDistributionMode.REFUND || bondDistributionMode == BondDistributionMode.NORMAL)
{
// We can't revert or we'd break claimCredit().
return;
} else if (bondDistributionMode != BondDistributionMode.UNDECIDED) {
// We shouldn't get here, but sanity check just in case.
revert InvalidBondDistributionMode();
}
// Make sure that the game is resolved.
// AnchorStateRegistry should be checking this but we're being defensive here.
if (resolvedAt.raw() == 0) {
revert GameNotResolved();
}
// Game must be finalized according to the AnchorStateRegistry.
bool finalized = ANCHOR_STATE_REGISTRY.isGameFinalized(IDisputeGame(address(this)));
if (!finalized) {
revert GameNotFinalized();
}
// Try to update the anchor game first. Won't always succeed because delays can lead
// to situations in which this game might not be eligible to be a new anchor game.
try ANCHOR_STATE_REGISTRY.setAnchorState(IDisputeGame(address(this))) { } catch { }
// Check if the game is a proper game, which will determine the bond distribution mode.
bool properGame = ANCHOR_STATE_REGISTRY.isGameProper(IDisputeGame(address(this)));
// If the game is a proper game, the bonds should be distributed normally. Otherwise, go
// into refund mode and distribute bonds back to their original depositors.
if (properGame) {
bondDistributionMode = BondDistributionMode.NORMAL;
} else {
bondDistributionMode = BondDistributionMode.REFUND;
}
// Emit an event to signal that the game has been closed.
emit GameClosed(bondDistributionMode);
}
/// @notice Returns the amount of time elapsed on the potential challenger to `_claimIndex`'s chess clock. Maxes /// @notice Returns the amount of time elapsed on the potential challenger to `_claimIndex`'s chess clock. Maxes
/// out at `MAX_CLOCK_DURATION`. /// out at `MAX_CLOCK_DURATION`.
/// @param _claimIndex The index of the subgame root claim. /// @param _claimIndex The index of the subgame root claim.
...@@ -961,6 +1060,18 @@ contract FaultDisputeGame is Clone, ISemver { ...@@ -961,6 +1060,18 @@ contract FaultDisputeGame is Clone, ISemver {
len_ = claimData.length; len_ = claimData.length;
} }
/// @notice Returns the credit balance of a given recipient.
/// @param _recipient The recipient of the credit.
/// @return credit_ The credit balance of the recipient.
function credit(address _recipient) external view returns (uint256 credit_) {
if (bondDistributionMode == BondDistributionMode.REFUND) {
credit_ = refundModeCredit[_recipient];
} else {
// Always return normal credit balance by default unless we're in refund mode.
credit_ = normalModeCredit[_recipient];
}
}
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
// IMMUTABLE GETTERS // // IMMUTABLE GETTERS //
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
...@@ -1018,14 +1129,7 @@ contract FaultDisputeGame is Clone, ISemver { ...@@ -1018,14 +1129,7 @@ contract FaultDisputeGame is Clone, ISemver {
/// @param _recipient The recipient of the bond. /// @param _recipient The recipient of the bond.
/// @param _bonded The claim to pay out the bond of. /// @param _bonded The claim to pay out the bond of.
function _distributeBond(address _recipient, ClaimData storage _bonded) internal { function _distributeBond(address _recipient, ClaimData storage _bonded) internal {
// Set all bits in the bond value to indicate that the bond has been paid out. normalModeCredit[_recipient] += _bonded.bond;
uint256 bond = _bonded.bond;
// Increase the recipient's credit.
credit[_recipient] += bond;
// Unlock the bond.
WETH.unlock(_recipient, bond);
} }
/// @notice Verifies the integrity of an execution bisection subgame's root claim. Reverts if the claim /// @notice Verifies the integrity of an execution bisection subgame's root claim. Reverts if the claim
......
...@@ -121,6 +121,18 @@ error BlockNumberMatches(); ...@@ -121,6 +121,18 @@ error BlockNumberMatches();
/// @notice Thrown when the L2 block number claim has already been challenged. /// @notice Thrown when the L2 block number claim has already been challenged.
error L2BlockNumberChallenged(); error L2BlockNumberChallenged();
/// @notice Thrown when the game is not yet finalized.
error GameNotFinalized();
/// @notice Thrown when an invalid bond distribution mode is supplied.
error InvalidBondDistributionMode();
/// @notice Thrown when the game is not yet resolved.
error GameNotResolved();
/// @notice Thrown when a reserved game type is used.
error ReservedGameType();
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
// `PermissionedDisputeGame` Errors // // `PermissionedDisputeGame` Errors //
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
......
...@@ -26,6 +26,17 @@ enum GameStatus { ...@@ -26,6 +26,17 @@ enum GameStatus {
DEFENDER_WINS DEFENDER_WINS
} }
/// @notice The game's bond distribution type. Games are expected to start in the `UNDECIDED`
/// state, and then choose either `NORMAL` or `REFUND`.
enum BondDistributionMode {
// Bond distribution strategy has not been chosen.
UNDECIDED,
// Bonds should be distributed as normal.
NORMAL,
// Bonds should be refunded to claimants.
REFUND
}
/// @notice Represents an L2 output root and the L2 block number at which it was generated. /// @notice Represents an L2 output root and the L2 block number at which it was generated.
/// @custom:field root The output root. /// @custom:field root The output root.
/// @custom:field l2BlockNumber The L2 block number at which the output root was generated. /// @custom:field l2BlockNumber The L2 block number at which the output root was generated.
......
...@@ -38,3 +38,5 @@ error Unproven(); ...@@ -38,3 +38,5 @@ error Unproven();
error ProposalNotValidated(); error ProposalNotValidated();
/// @notice Error for when a withdrawal has already been finalized. /// @notice Error for when a withdrawal has already been finalized.
error AlreadyFinalized(); error AlreadyFinalized();
/// @notice Error for when a game is a legacy game.
error LegacyGame();
...@@ -395,18 +395,23 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { ...@@ -395,18 +395,23 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest {
/// @dev Setup the system for a ready-to-use state. /// @dev Setup the system for a ready-to-use state.
function setUp() public virtual override { function setUp() public virtual override {
// Warp forward in time to ensure that the game is created after the retirement timestamp.
vm.warp(optimismPortal2.respectedGameTypeUpdatedAt() + 1 seconds);
// Set up the dummy game.
_proposedBlockNumber = 0xFF; _proposedBlockNumber = 0xFF;
GameType respectedGameType = optimismPortal2.respectedGameType(); GameType respectedGameType = optimismPortal2.respectedGameType();
uint256 bondAmount = disputeGameFactory.initBonds(respectedGameType);
game = IFaultDisputeGame( game = IFaultDisputeGame(
payable( payable(
address( address(
disputeGameFactory.create{ value: bondAmount }( disputeGameFactory.create{ value: disputeGameFactory.initBonds(respectedGameType) }(
optimismPortal2.respectedGameType(), Claim.wrap(_outputRoot), abi.encode(_proposedBlockNumber) respectedGameType, Claim.wrap(_outputRoot), abi.encode(_proposedBlockNumber)
) )
) )
) )
); );
// Grab the index of the game we just created.
_proposedGameIndex = disputeGameFactory.gameCount() - 1; _proposedGameIndex = disputeGameFactory.gameCount() - 1;
// Warp beyond the chess clocks and finalize the game. // Warp beyond the chess clocks and finalize the game.
...@@ -455,13 +460,33 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { ...@@ -455,13 +460,33 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest {
} }
/// @dev Tests that the guardian role can set the respected game type to anything they want. /// @dev Tests that the guardian role can set the respected game type to anything they want.
function testFuzz_setRespectedGameType_guardian_succeeds(GameType _ty) external { function testFuzz_setRespectedGameType_guardianCanSetRespectedGameType_succeeds(GameType _ty) external {
vm.assume(_ty.raw() != type(uint32).max);
uint64 respectedGameTypeUpdatedAt = optimismPortal2.respectedGameTypeUpdatedAt();
vm.expectEmit(address(optimismPortal2)); vm.expectEmit(address(optimismPortal2));
emit RespectedGameTypeSet(_ty, Timestamp.wrap(uint64(block.timestamp))); emit RespectedGameTypeSet(_ty, Timestamp.wrap(respectedGameTypeUpdatedAt));
vm.prank(optimismPortal2.guardian()); vm.prank(optimismPortal2.guardian());
optimismPortal2.setRespectedGameType(_ty); optimismPortal2.setRespectedGameType(_ty);
// GameType changes, but the timestamp doesn't.
assertEq(optimismPortal2.respectedGameType().raw(), _ty.raw()); assertEq(optimismPortal2.respectedGameType().raw(), _ty.raw());
assertEq(optimismPortal2.respectedGameTypeUpdatedAt(), respectedGameTypeUpdatedAt);
}
/// @dev Tests that the guardian can set the `respectedGameTypeUpdatedAt` timestamp to current timestamp.
function testFuzz_setRespectedGameType_guardianCanSetRespectedGameTypeUpdatedAt_succeeds(uint64 _elapsed)
external
{
_elapsed = uint64(bound(_elapsed, 0, type(uint64).max - uint64(block.timestamp)));
GameType _ty = GameType.wrap(type(uint32).max);
uint64 _newRespectedGameTypeUpdatedAt = uint64(block.timestamp) + _elapsed;
GameType _existingGameType = optimismPortal2.respectedGameType();
vm.warp(_newRespectedGameTypeUpdatedAt);
emit RespectedGameTypeSet(_existingGameType, Timestamp.wrap(_newRespectedGameTypeUpdatedAt));
vm.prank(optimismPortal2.guardian());
optimismPortal2.setRespectedGameType(_ty);
// GameType doesn't change, but the timestamp does.
assertEq(optimismPortal2.respectedGameType().raw(), _existingGameType.raw());
assertEq(optimismPortal2.respectedGameTypeUpdatedAt(), _newRespectedGameTypeUpdatedAt);
} }
/// @dev Tests that `proveWithdrawalTransaction` reverts when paused. /// @dev Tests that `proveWithdrawalTransaction` reverts when paused.
...@@ -565,6 +590,60 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { ...@@ -565,6 +590,60 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest {
}); });
} }
/// @dev Tests that `proveWithdrawalTransaction` reverts if the game was not the respected game type when created.
function test_proveWithdrawalTransaction_wasNotRespectedGameTypeWhenCreated_reverts() external {
vm.mockCall(address(game), abi.encodeCall(game.wasRespectedGameTypeWhenCreated, ()), abi.encode(false));
vm.expectRevert(InvalidGameType.selector);
optimismPortal2.proveWithdrawalTransaction({
_tx: _defaultTx,
_disputeGameIndex: _proposedGameIndex,
_outputRootProof: _outputRootProof,
_withdrawalProof: _withdrawalProof
});
}
/// @dev Tests that `proveWithdrawalTransaction` reverts if the game is a legacy game that does not implement
/// `wasRespectedGameTypeWhenCreated`.
function test_proveWithdrawalTransaction_legacyGame_reverts() external {
vm.mockCallRevert(address(game), abi.encodeCall(game.wasRespectedGameTypeWhenCreated, ()), "");
vm.expectRevert(LegacyGame.selector);
optimismPortal2.proveWithdrawalTransaction({
_tx: _defaultTx,
_disputeGameIndex: _proposedGameIndex,
_outputRootProof: _outputRootProof,
_withdrawalProof: _withdrawalProof
});
}
/// @dev Tests that `proveWithdrawalTransaction` succeeds if the game was created after the
/// game retirement timestamp.
function testFuzz_proveWithdrawalTransaction_createdAfterRetirementTimestamp_succeeds(uint64 _createdAt) external {
_createdAt = uint64(bound(_createdAt, optimismPortal2.respectedGameTypeUpdatedAt() + 1, type(uint64).max));
vm.mockCall(address(game), abi.encodeCall(game.createdAt, ()), abi.encode(uint64(_createdAt)));
optimismPortal2.proveWithdrawalTransaction({
_tx: _defaultTx,
_disputeGameIndex: _proposedGameIndex,
_outputRootProof: _outputRootProof,
_withdrawalProof: _withdrawalProof
});
}
/// @dev Tests that `proveWithdrawalTransaction` reverts if the game was created before or at
/// the game retirement timestamp.
function testFuzz_proveWithdrawalTransaction_createdBeforeOrAtRetirementTimestamp_reverts(uint64 _createdAt)
external
{
_createdAt = uint64(bound(_createdAt, 0, optimismPortal2.respectedGameTypeUpdatedAt()));
vm.mockCall(address(game), abi.encodeCall(game.createdAt, ()), abi.encode(uint64(_createdAt)));
vm.expectRevert("OptimismPortal: dispute game created before respected game type was updated");
optimismPortal2.proveWithdrawalTransaction({
_tx: _defaultTx,
_disputeGameIndex: _proposedGameIndex,
_outputRootProof: _outputRootProof,
_withdrawalProof: _withdrawalProof
});
}
/// @dev Tests that `proveWithdrawalTransaction` can be re-executed if the dispute game proven against has been /// @dev Tests that `proveWithdrawalTransaction` can be re-executed if the dispute game proven against has been
/// blacklisted. /// blacklisted.
function test_proveWithdrawalTransaction_replayProveBlacklisted_succeeds() external { function test_proveWithdrawalTransaction_replayProveBlacklisted_succeeds() external {
...@@ -652,13 +731,15 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { ...@@ -652,13 +731,15 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest {
_withdrawalProof: _withdrawalProof _withdrawalProof: _withdrawalProof
}); });
// Create a new game.
IDisputeGame newGame =
disputeGameFactory.create(GameType.wrap(0), Claim.wrap(_outputRoot), abi.encode(_proposedBlockNumber + 1));
// Update the respected game type to 0xbeef. // Update the respected game type to 0xbeef.
vm.prank(optimismPortal2.guardian()); vm.prank(optimismPortal2.guardian());
optimismPortal2.setRespectedGameType(GameType.wrap(0xbeef)); optimismPortal2.setRespectedGameType(GameType.wrap(0xbeef));
// Create a new game and mock the game type as 0xbeef in the factory. // Create a new game and mock the game type as 0xbeef in the factory.
IDisputeGame newGame =
disputeGameFactory.create(GameType.wrap(0), Claim.wrap(_outputRoot), abi.encode(_proposedBlockNumber + 1));
vm.mockCall( vm.mockCall(
address(disputeGameFactory), address(disputeGameFactory),
abi.encodeCall(disputeGameFactory.gameAtIndex, (_proposedGameIndex + 1)), abi.encodeCall(disputeGameFactory.gameAtIndex, (_proposedGameIndex + 1)),
...@@ -1236,6 +1317,88 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { ...@@ -1236,6 +1317,88 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest {
assertTrue(optimismPortal2.finalizedWithdrawals(withdrawalHash)); assertTrue(optimismPortal2.finalizedWithdrawals(withdrawalHash));
} }
/// @dev Tests that `finalizeWithdrawalTransaction` succeeds even if the respected game type is changed.
function test_finalizeWithdrawalTransaction_wasRespectedGameType_succeeds(
address _sender,
address _target,
uint256 _value,
uint256 _gasLimit,
bytes memory _data,
GameType _newGameType
)
external
{
vm.assume(
_target != address(optimismPortal2) // Cannot call the optimism portal or a contract
&& _target.code.length == 0 // No accounts with code
&& _target != CONSOLE // The console has no code but behaves like a contract
&& uint160(_target) > 9 // No precompiles (or zero address)
);
// Bound to prevent changes in respectedGameTypeUpdatedAt
_newGameType = GameType.wrap(uint32(bound(_newGameType.raw(), 0, type(uint32).max - 1)));
// Total ETH supply is currently about 120M ETH.
uint256 value = bound(_value, 0, 200_000_000 ether);
vm.deal(address(optimismPortal2), value);
uint256 gasLimit = bound(_gasLimit, 0, 50_000_000);
uint256 nonce = l2ToL1MessagePasser.messageNonce();
// Get a withdrawal transaction and mock proof from the differential testing script.
Types.WithdrawalTransaction memory _tx = Types.WithdrawalTransaction({
nonce: nonce,
sender: _sender,
target: _target,
value: value,
gasLimit: gasLimit,
data: _data
});
(
bytes32 stateRoot,
bytes32 storageRoot,
bytes32 outputRoot,
bytes32 withdrawalHash,
bytes[] memory withdrawalProof
) = ffi.getProveWithdrawalTransactionInputs(_tx);
// Create the output root proof
Types.OutputRootProof memory proof = Types.OutputRootProof({
version: bytes32(uint256(0)),
stateRoot: stateRoot,
messagePasserStorageRoot: storageRoot,
latestBlockhash: bytes32(uint256(0))
});
// Ensure the values returned from ffi are correct
assertEq(outputRoot, Hashing.hashOutputRootProof(proof));
assertEq(withdrawalHash, Hashing.hashWithdrawal(_tx));
// Setup the dispute game to return the output root
vm.mockCall(address(game), abi.encodeCall(game.rootClaim, ()), abi.encode(outputRoot));
// Prove the withdrawal transaction
optimismPortal2.proveWithdrawalTransaction(_tx, _proposedGameIndex, proof, withdrawalProof);
(IDisputeGame _game,) = optimismPortal2.provenWithdrawals(withdrawalHash, address(this));
assertTrue(_game.rootClaim().raw() != bytes32(0));
// Resolve the dispute game
game.resolveClaim(0, 0);
game.resolve();
// Warp past the finalization period
vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1);
// Change the respectedGameType
vm.prank(optimismPortal2.guardian());
optimismPortal2.setRespectedGameType(_newGameType);
// Withdrawal transaction still finalizable
vm.expectCallMinGas(_tx.target, _tx.value, uint64(_tx.gasLimit), _tx.data);
optimismPortal2.finalizeWithdrawalTransaction(_tx);
assertTrue(optimismPortal2.finalizedWithdrawals(withdrawalHash));
}
/// @dev Tests that `finalizeWithdrawalTransaction` reverts if the withdrawal's dispute game has been blacklisted. /// @dev Tests that `finalizeWithdrawalTransaction` reverts if the withdrawal's dispute game has been blacklisted.
function test_finalizeWithdrawalTransaction_blacklisted_reverts() external { function test_finalizeWithdrawalTransaction_blacklisted_reverts() external {
vm.expectEmit(true, true, true, true); vm.expectEmit(true, true, true, true);
...@@ -1293,12 +1456,12 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { ...@@ -1293,12 +1456,12 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest {
assertTrue(optimismPortal2.finalizedWithdrawals(_withdrawalHash)); assertTrue(optimismPortal2.finalizedWithdrawals(_withdrawalHash));
} }
/// @dev Tests that `finalizeWithdrawalTransaction` reverts if the respected game type has changed since the /// @dev Tests that `finalizeWithdrawalTransaction` reverts if the respected game type was updated after the
/// withdrawal was proven. /// dispute game was created.
function test_finalizeWithdrawalTransaction_respectedTypeChangedSinceProving_reverts() external { function test_finalizeWithdrawalTransaction_gameOlderThanRespectedGameTypeUpdate_reverts() external {
vm.expectEmit(true, true, true, true); vm.expectEmit(address(optimismPortal2));
emit WithdrawalProven(_withdrawalHash, alice, bob); emit WithdrawalProven(_withdrawalHash, alice, bob);
vm.expectEmit(true, true, true, true); vm.expectEmit(address(optimismPortal2));
emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); emit WithdrawalProvenExtension1(_withdrawalHash, address(this));
optimismPortal2.proveWithdrawalTransaction({ optimismPortal2.proveWithdrawalTransaction({
_tx: _defaultTx, _tx: _defaultTx,
...@@ -1314,17 +1477,51 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { ...@@ -1314,17 +1477,51 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest {
game.resolveClaim(0, 0); game.resolveClaim(0, 0);
game.resolve(); game.resolve();
// Change the respected game type in the portal. // Warp past the dispute game finality delay.
vm.warp(block.timestamp + optimismPortal2.disputeGameFinalityDelaySeconds() + 1);
// Set respectedGameTypeUpdatedAt.
vm.prank(optimismPortal2.guardian()); vm.prank(optimismPortal2.guardian());
optimismPortal2.setRespectedGameType(GameType.wrap(0xFF)); optimismPortal2.setRespectedGameType(GameType.wrap(type(uint32).max));
vm.expectRevert("OptimismPortal: dispute game created before respected game type was updated");
optimismPortal2.finalizeWithdrawalTransaction(_defaultTx);
}
/// @dev Tests that `finalizeWithdrawalTransaction` reverts if the game was not the respected game type when it was
/// created. `proveWithdrawalTransaction` should already prevent this, but we remove that assumption here.
function test_finalizeWithdrawalTransaction_gameWasNotRespectedGameType_reverts() external {
vm.expectEmit(address(optimismPortal2));
emit WithdrawalProven(_withdrawalHash, alice, bob);
vm.expectEmit(address(optimismPortal2));
emit WithdrawalProvenExtension1(_withdrawalHash, address(this));
optimismPortal2.proveWithdrawalTransaction({
_tx: _defaultTx,
_disputeGameIndex: _proposedGameIndex,
_outputRootProof: _outputRootProof,
_withdrawalProof: _withdrawalProof
});
// Warp past the finalization period.
vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1);
// Resolve the dispute game.
game.resolveClaim(0, 0);
game.resolve();
// Warp past the dispute game finality delay.
vm.warp(block.timestamp + optimismPortal2.disputeGameFinalityDelaySeconds() + 1);
vm.mockCall(address(game), abi.encodeCall(game.wasRespectedGameTypeWhenCreated, ()), abi.encode(false));
vm.expectRevert(InvalidGameType.selector); vm.expectRevert(InvalidGameType.selector);
optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx);
} }
/// @dev Tests that `finalizeWithdrawalTransaction` reverts if the respected game type was updated after the /// @dev Tests that `finalizeWithdrawalTransaction` reverts if the game is a legacy game that does not implement
/// dispute game was created. /// `wasRespectedGameTypeWhenCreated`. `proveWithdrawalTransaction` should already prevent this, but we remove
function test_finalizeWithdrawalTransaction_gameOlderThanRespectedGameTypeUpdate_reverts() external { /// that assumption here.
function test_finalizeWithdrawalTransaction_legacyGame_reverts() external {
vm.expectEmit(address(optimismPortal2)); vm.expectEmit(address(optimismPortal2));
emit WithdrawalProven(_withdrawalHash, alice, bob); emit WithdrawalProven(_withdrawalHash, alice, bob);
vm.expectEmit(address(optimismPortal2)); vm.expectEmit(address(optimismPortal2));
...@@ -1343,14 +1540,12 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { ...@@ -1343,14 +1540,12 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest {
game.resolveClaim(0, 0); game.resolveClaim(0, 0);
game.resolve(); game.resolve();
// Change the respected game type in the portal. // Warp past the dispute game finality delay.
vm.prank(optimismPortal2.guardian()); vm.warp(block.timestamp + optimismPortal2.disputeGameFinalityDelaySeconds() + 1);
optimismPortal2.setRespectedGameType(GameType.wrap(0xFF));
// Mock the game's type so that we pass the correct game type check. vm.mockCallRevert(address(game), abi.encodeCall(game.wasRespectedGameTypeWhenCreated, ()), "");
vm.mockCall(address(game), abi.encodeCall(game.gameType, ()), abi.encode(GameType.wrap(0xFF)));
vm.expectRevert("OptimismPortal: dispute game created before respected game type was updated"); vm.expectRevert(LegacyGame.selector);
optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx);
} }
......
...@@ -77,6 +77,48 @@ contract AnchorStateRegistry_GetAnchorRoot_Test is AnchorStateRegistry_Init { ...@@ -77,6 +77,48 @@ contract AnchorStateRegistry_GetAnchorRoot_Test is AnchorStateRegistry_Init {
assertEq(root.raw(), 0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF); assertEq(root.raw(), 0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF);
assertEq(l2BlockNumber, 0); assertEq(l2BlockNumber, 0);
} }
/// @notice Tests that getAnchorRoot will return the correct anchor root if an anchor game exists.
function test_getAnchorRoot_anchorGameExists_succeeds() public {
// Mock the game to be resolved.
vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.resolvedAt, ()), abi.encode(block.timestamp));
vm.warp(block.timestamp + optimismPortal2.disputeGameFinalityDelaySeconds() + 1);
// Mock the game to be the defender wins.
vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.DEFENDER_WINS));
// Set the anchor game to the game proxy.
anchorStateRegistry.setAnchorState(gameProxy);
// We should get the anchor root back.
(Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot();
assertEq(root.raw(), gameProxy.rootClaim().raw());
assertEq(l2BlockNumber, gameProxy.l2BlockNumber());
}
}
contract AnchorStateRegistry_GetAnchorRoot_TestFail is AnchorStateRegistry_Init {
/// @notice Tests that getAnchorRoot will revert if the anchor game is blacklisted.
function test_getAnchorRoot_blacklistedGame_fails() public {
// Mock the game to be resolved.
vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.resolvedAt, ()), abi.encode(block.timestamp));
vm.warp(block.timestamp + optimismPortal2.disputeGameFinalityDelaySeconds() + 1);
// Mock the game to be the defender wins.
vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.DEFENDER_WINS));
// Set the anchor game to the game proxy.
anchorStateRegistry.setAnchorState(gameProxy);
// Mock the disputeGameBlacklist call to return true.
vm.mockCall(
address(optimismPortal2),
abi.encodeCall(optimismPortal2.disputeGameBlacklist, (gameProxy)),
abi.encode(true)
);
vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_AnchorGameBlacklisted.selector);
anchorStateRegistry.getAnchorRoot();
}
} }
contract AnchorStateRegistry_Anchors_Test is AnchorStateRegistry_Init { contract AnchorStateRegistry_Anchors_Test is AnchorStateRegistry_Init {
...@@ -143,26 +185,17 @@ contract AnchorStateRegistry_IsGameBlacklisted_Test is AnchorStateRegistry_Init ...@@ -143,26 +185,17 @@ contract AnchorStateRegistry_IsGameBlacklisted_Test is AnchorStateRegistry_Init
contract AnchorStateRegistry_IsGameRespected_Test is AnchorStateRegistry_Init { contract AnchorStateRegistry_IsGameRespected_Test is AnchorStateRegistry_Init {
/// @notice Tests that isGameRespected will return true if the game is of the respected game type. /// @notice Tests that isGameRespected will return true if the game is of the respected game type.
function test_isGameRespected_isRespected_succeeds() public { function test_isGameRespected_isRespected_succeeds() public {
// Make our game type the respected game type. // Mock that the game was respected.
vm.mockCall( vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.wasRespectedGameTypeWhenCreated, ()), abi.encode(true));
address(optimismPortal2),
abi.encodeCall(optimismPortal2.respectedGameType, ()),
abi.encode(gameProxy.gameType())
);
assertTrue(anchorStateRegistry.isGameRespected(gameProxy)); assertTrue(anchorStateRegistry.isGameRespected(gameProxy));
} }
/// @notice Tests that isGameRespected will return false if the game is not of the respected game /// @notice Tests that isGameRespected will return false if the game is not of the respected game
/// type. /// type.
/// @param _gameType The game type to use for the test. function test_isGameRespected_isNotRespected_succeeds() public {
function testFuzz_isGameRespected_isNotRespected_succeeds(GameType _gameType) public { // Mock that the game was not respected.
if (_gameType.raw() == gameProxy.gameType().raw()) {
_gameType = GameType.wrap(_gameType.raw() + 1);
}
// Make our game type NOT the respected game type.
vm.mockCall( vm.mockCall(
address(optimismPortal2), abi.encodeCall(optimismPortal2.respectedGameType, ()), abi.encode(_gameType) address(gameProxy), abi.encodeCall(gameProxy.wasRespectedGameTypeWhenCreated, ()), abi.encode(false)
); );
assertFalse(anchorStateRegistry.isGameRespected(gameProxy)); assertFalse(anchorStateRegistry.isGameRespected(gameProxy));
} }
...@@ -172,15 +205,17 @@ contract AnchorStateRegistry_IsGameRetired_Test is AnchorStateRegistry_Init { ...@@ -172,15 +205,17 @@ contract AnchorStateRegistry_IsGameRetired_Test is AnchorStateRegistry_Init {
/// @notice Tests that isGameRetired will return true if the game is retired. /// @notice Tests that isGameRetired will return true if the game is retired.
/// @param _retirementTimestamp The retirement timestamp to use for the test. /// @param _retirementTimestamp The retirement timestamp to use for the test.
function testFuzz_isGameRetired_isRetired_succeeds(uint64 _retirementTimestamp) public { function testFuzz_isGameRetired_isRetired_succeeds(uint64 _retirementTimestamp) public {
// Make sure retirement timestamp is later than the game's creation time. // Make sure retirement timestamp is greater than or equal to the game's creation time.
_retirementTimestamp = uint64(bound(_retirementTimestamp, gameProxy.createdAt().raw() + 1, type(uint64).max)); _retirementTimestamp = uint64(bound(_retirementTimestamp, gameProxy.createdAt().raw(), type(uint64).max));
// Mock the respectedGameTypeUpdatedAt call to be later than the game's creation time. // Mock the respectedGameTypeUpdatedAt call.
vm.mockCall( vm.mockCall(
address(optimismPortal2), address(optimismPortal2),
abi.encodeCall(optimismPortal2.respectedGameTypeUpdatedAt, ()), abi.encodeCall(optimismPortal2.respectedGameTypeUpdatedAt, ()),
abi.encode(_retirementTimestamp) abi.encode(_retirementTimestamp)
); );
// Game should be retired.
assertTrue(anchorStateRegistry.isGameRetired(gameProxy)); assertTrue(anchorStateRegistry.isGameRetired(gameProxy));
} }
...@@ -188,7 +223,7 @@ contract AnchorStateRegistry_IsGameRetired_Test is AnchorStateRegistry_Init { ...@@ -188,7 +223,7 @@ contract AnchorStateRegistry_IsGameRetired_Test is AnchorStateRegistry_Init {
/// @param _retirementTimestamp The retirement timestamp to use for the test. /// @param _retirementTimestamp The retirement timestamp to use for the test.
function testFuzz_isGameRetired_isNotRetired_succeeds(uint64 _retirementTimestamp) public { function testFuzz_isGameRetired_isNotRetired_succeeds(uint64 _retirementTimestamp) public {
// Make sure retirement timestamp is earlier than the game's creation time. // Make sure retirement timestamp is earlier than the game's creation time.
_retirementTimestamp = uint64(bound(_retirementTimestamp, 0, gameProxy.createdAt().raw())); _retirementTimestamp = uint64(bound(_retirementTimestamp, 0, gameProxy.createdAt().raw() - 1));
// Mock the respectedGameTypeUpdatedAt call to be earlier than the game's creation time. // Mock the respectedGameTypeUpdatedAt call to be earlier than the game's creation time.
vm.mockCall( vm.mockCall(
...@@ -196,20 +231,16 @@ contract AnchorStateRegistry_IsGameRetired_Test is AnchorStateRegistry_Init { ...@@ -196,20 +231,16 @@ contract AnchorStateRegistry_IsGameRetired_Test is AnchorStateRegistry_Init {
abi.encodeCall(optimismPortal2.respectedGameTypeUpdatedAt, ()), abi.encodeCall(optimismPortal2.respectedGameTypeUpdatedAt, ()),
abi.encode(_retirementTimestamp) abi.encode(_retirementTimestamp)
); );
// Game should not be retired.
assertFalse(anchorStateRegistry.isGameRetired(gameProxy)); assertFalse(anchorStateRegistry.isGameRetired(gameProxy));
} }
} }
contract AnchorStateRegistry_IsGameProper_Test is AnchorStateRegistry_Init { contract AnchorStateRegistry_IsGameProper_Test is AnchorStateRegistry_Init {
/// @notice Tests that isGameProper will return true if the game meets all conditions. /// @notice Tests that isGameProper will return true if the game meets all conditions.
function test_isGameProper_meetsAllConditions_succeeds() public { function test_isGameProper_meetsAllConditions_succeeds() public view {
// Make our game type the respected game type. // Game will meet all conditions by default.
vm.mockCall(
address(optimismPortal2),
abi.encodeCall(optimismPortal2.respectedGameType, ()),
abi.encode(gameProxy.gameType())
);
assertTrue(anchorStateRegistry.isGameProper(gameProxy)); assertTrue(anchorStateRegistry.isGameProper(gameProxy));
} }
...@@ -228,17 +259,19 @@ contract AnchorStateRegistry_IsGameProper_Test is AnchorStateRegistry_Init { ...@@ -228,17 +259,19 @@ contract AnchorStateRegistry_IsGameProper_Test is AnchorStateRegistry_Init {
} }
/// @notice Tests that isGameProper will return false if the game is not the respected game type. /// @notice Tests that isGameProper will return false if the game is not the respected game type.
function testFuzz_isGameProper_isNotRespected_succeeds(GameType _gameType) public { /// @param _gameType The game type to use for the test.
function testFuzz_isGameProper_anyGameType_succeeds(GameType _gameType) public {
if (_gameType.raw() == gameProxy.gameType().raw()) { if (_gameType.raw() == gameProxy.gameType().raw()) {
_gameType = GameType.wrap(_gameType.raw() + 1); _gameType = GameType.wrap(_gameType.raw() + 1);
} }
// Make our game type NOT the respected game type. // Mock that the game was not respected.
vm.mockCall( vm.mockCall(
address(optimismPortal2), abi.encodeCall(optimismPortal2.respectedGameType, ()), abi.encode(_gameType) address(gameProxy), abi.encodeCall(gameProxy.wasRespectedGameTypeWhenCreated, ()), abi.encode(false)
); );
assertFalse(anchorStateRegistry.isGameProper(gameProxy)); // Still a proper game.
assertTrue(anchorStateRegistry.isGameProper(gameProxy));
} }
/// @notice Tests that isGameProper will return false if the game is blacklisted. /// @notice Tests that isGameProper will return false if the game is blacklisted.
...@@ -254,6 +287,7 @@ contract AnchorStateRegistry_IsGameProper_Test is AnchorStateRegistry_Init { ...@@ -254,6 +287,7 @@ contract AnchorStateRegistry_IsGameProper_Test is AnchorStateRegistry_Init {
} }
/// @notice Tests that isGameProper will return false if the game is retired. /// @notice Tests that isGameProper will return false if the game is retired.
/// @param _retirementTimestamp The retirement timestamp to use for the test.
function testFuzz_isGameProper_isRetired_succeeds(uint64 _retirementTimestamp) public { function testFuzz_isGameProper_isRetired_succeeds(uint64 _retirementTimestamp) public {
// Make sure retirement timestamp is later than the game's creation time. // Make sure retirement timestamp is later than the game's creation time.
_retirementTimestamp = uint64(bound(_retirementTimestamp, gameProxy.createdAt().raw() + 1, type(uint64).max)); _retirementTimestamp = uint64(bound(_retirementTimestamp, gameProxy.createdAt().raw() + 1, type(uint64).max));
...@@ -269,105 +303,119 @@ contract AnchorStateRegistry_IsGameProper_Test is AnchorStateRegistry_Init { ...@@ -269,105 +303,119 @@ contract AnchorStateRegistry_IsGameProper_Test is AnchorStateRegistry_Init {
} }
} }
contract AnchorStateRegistry_TryUpdateAnchorState_Test is AnchorStateRegistry_Init { contract AnchorStateRegistry_IsGameResolved_Test is AnchorStateRegistry_Init {
/// @notice Tests that tryUpdateAnchorState will succeed if the game is valid, the game block /// @notice Tests that isGameResolved will return true if the game is resolved.
/// number is greater than the current anchor root block number, and the game is the /// @param _resolvedAtTimestamp The resolvedAt timestamp to use for the test.
/// currently respected game type. function testFuzz_isGameResolved_challengerWins_succeeds(uint256 _resolvedAtTimestamp) public {
/// @param _l2BlockNumber The L2 block number to use for the game. // Bound resolvedAt to be less than or equal to current timestamp.
function testFuzz_tryUpdateAnchorState_validNewerState_succeeds(uint256 _l2BlockNumber) public { _resolvedAtTimestamp = bound(_resolvedAtTimestamp, 1, block.timestamp);
// Grab block number of the existing anchor root.
(Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot();
// Bound the new block number. // Mock the resolvedAt timestamp.
_l2BlockNumber = bound(_l2BlockNumber, l2BlockNumber + 1, type(uint256).max); vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.resolvedAt, ()), abi.encode(_resolvedAtTimestamp));
// Mock the l2BlockNumber call. // Mock the status to be CHALLENGER_WINS.
vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2BlockNumber, ()), abi.encode(_l2BlockNumber)); vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.CHALLENGER_WINS));
// Mock the DEFENDER_WINS state. // Game should be resolved.
assertTrue(anchorStateRegistry.isGameResolved(gameProxy));
}
/// @notice Tests that isGameResolved will return true if the game is resolved.
/// @param _resolvedAtTimestamp The resolvedAt timestamp to use for the test.
function testFuzz_isGameResolved_defenderWins_succeeds(uint256 _resolvedAtTimestamp) public {
// Bound resolvedAt to be less than or equal to current timestamp.
_resolvedAtTimestamp = bound(_resolvedAtTimestamp, 1, block.timestamp);
// Mock the resolvedAt timestamp.
vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.resolvedAt, ()), abi.encode(_resolvedAtTimestamp));
// Mock the status to be DEFENDER_WINS.
vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.DEFENDER_WINS)); vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.DEFENDER_WINS));
// Make our game type the respected game type. // Game should be resolved.
vm.mockCall( assertTrue(anchorStateRegistry.isGameResolved(gameProxy));
address(optimismPortal2), }
abi.encodeCall(optimismPortal2.respectedGameType, ()),
abi.encode(gameProxy.gameType())
);
// Update the anchor state. /// @notice Tests that isGameResolved will return false if the game is in progress and not resolved.
vm.prank(address(gameProxy)); /// @param _resolvedAtTimestamp The resolvedAt timestamp to use for the test.
vm.expectEmit(address(anchorStateRegistry)); function testFuzz_isGameResolved_inProgressNotResolved_succeeds(uint256 _resolvedAtTimestamp) public {
emit AnchorUpdated(gameProxy); // Bound resolvedAt to be less than or equal to current timestamp.
anchorStateRegistry.tryUpdateAnchorState(); _resolvedAtTimestamp = bound(_resolvedAtTimestamp, 1, block.timestamp);
// Confirm that the anchor state is now the same as the game state. // Mock the resolvedAt timestamp.
(root, l2BlockNumber) = anchorStateRegistry.getAnchorRoot(); vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.resolvedAt, ()), abi.encode(_resolvedAtTimestamp));
assertEq(l2BlockNumber, gameProxy.l2BlockNumber());
assertEq(root.raw(), gameProxy.rootClaim().raw());
// Confirm that the anchor game is now set. // Mock the status to be IN_PROGRESS.
IFaultDisputeGame anchorGame = anchorStateRegistry.anchorGame(); vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.IN_PROGRESS));
assertEq(address(anchorGame), address(gameProxy));
// Game should not be resolved.
assertFalse(anchorStateRegistry.isGameResolved(gameProxy));
} }
}
/// @notice Tests that tryUpdateAnchorState will not update the anchor state if the game block contract AnchorStateRegistry_IsGameAirgapped_TestFail is AnchorStateRegistry_Init {
/// number is less than or equal to the current anchor root block number. /// @notice Tests that isGameAirgapped will return true if the game is airgapped.
/// @param _l2BlockNumber The L2 block number to use for the game. /// @param _resolvedAtTimestamp The resolvedAt timestamp to use for the test.
function testFuzz_tryUpdateAnchorState_validOlderStateNoUpdate_succeeds(uint256 _l2BlockNumber) public { function testFuzz_isGameAirgapped_isAirgapped_succeeds(uint256 _resolvedAtTimestamp) public {
// Grab block number of the existing anchor root. // Warp forward by disputeGameFinalityDelaySeconds.
(Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot(); vm.warp(block.timestamp + optimismPortal2.disputeGameFinalityDelaySeconds());
// Bound the new block number. // Bound resolvedAt to be at least disputeGameFinalityDelaySeconds in the past.
_l2BlockNumber = bound(_l2BlockNumber, 0, l2BlockNumber); _resolvedAtTimestamp =
bound(_resolvedAtTimestamp, 0, block.timestamp - optimismPortal2.disputeGameFinalityDelaySeconds() - 1);
// Mock the l2BlockNumber call. // Mock the resolvedAt timestamp.
vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2BlockNumber, ()), abi.encode(_l2BlockNumber)); vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.resolvedAt, ()), abi.encode(_resolvedAtTimestamp));
// Mock the DEFENDER_WINS state. // Game should be airgapped.
vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.DEFENDER_WINS)); assertTrue(anchorStateRegistry.isGameAirgapped(gameProxy));
}
// Make our game type the respected game type. /// @notice Tests that isGameAirgapped will return false if the game is not airgapped.
vm.mockCall( /// @param _resolvedAtTimestamp The resolvedAt timestamp to use for the test.
address(optimismPortal2), function testFuzz_isGameAirgapped_isNotAirgapped_succeeds(uint256 _resolvedAtTimestamp) public {
abi.encodeCall(optimismPortal2.respectedGameType, ()), // Warp forward by disputeGameFinalityDelaySeconds.
abi.encode(gameProxy.gameType()) vm.warp(block.timestamp + optimismPortal2.disputeGameFinalityDelaySeconds());
// Bound resolvedAt to be less than disputeGameFinalityDelaySeconds in the past.
_resolvedAtTimestamp = bound(
_resolvedAtTimestamp, block.timestamp - optimismPortal2.disputeGameFinalityDelaySeconds(), block.timestamp
); );
// Try to update the anchor state. // Mock the resolvedAt timestamp.
vm.prank(address(gameProxy)); vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.resolvedAt, ()), abi.encode(_resolvedAtTimestamp));
vm.expectEmit(address(anchorStateRegistry));
emit AnchorNotUpdated(gameProxy);
anchorStateRegistry.tryUpdateAnchorState();
// Confirm that the anchor state has not updated. // Game should not be airgapped.
(Hash updatedRoot, uint256 updatedL2BlockNumber) = anchorStateRegistry.getAnchorRoot(); assertFalse(anchorStateRegistry.isGameAirgapped(gameProxy));
assertEq(updatedL2BlockNumber, l2BlockNumber);
assertEq(updatedRoot.raw(), root.raw());
} }
}
/// @notice Tests that tryUpdateAnchorState will not update the anchor state if the game is not contract AnchorStateRegistry_IsGameClaimValid_Test is AnchorStateRegistry_Init {
/// registered. /// @notice Tests that isGameClaimValid will return true if the game claim is valid.
/// @param _l2BlockNumber The L2 block number to use for the game. /// @param _resolvedAtTimestamp The resolvedAt timestamp to use for the test.
function testFuzz_tryUpdateAnchorState_notFactoryRegisteredGameNoUpdate_succeeds(uint256 _l2BlockNumber) public { function testFuzz_isGameClaimValid_claimIsValid_succeeds(uint256 _resolvedAtTimestamp) public {
// Grab block number of the existing anchor root. // Warp forward by disputeGameFinalityDelaySeconds.
(Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot(); vm.warp(block.timestamp + optimismPortal2.disputeGameFinalityDelaySeconds());
// Bound the new block number. // Bound resolvedAt to be at least disputeGameFinalityDelaySeconds in the past.
_l2BlockNumber = bound(_l2BlockNumber, l2BlockNumber, type(uint256).max); _resolvedAtTimestamp =
bound(_resolvedAtTimestamp, 1, block.timestamp - optimismPortal2.disputeGameFinalityDelaySeconds() - 1);
// Mock the l2BlockNumber call. // Mock that the game was respected.
vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2BlockNumber, ()), abi.encode(_l2BlockNumber)); vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.wasRespectedGameTypeWhenCreated, ()), abi.encode(true));
// Mock the DEFENDER_WINS state. // Mock the resolvedAt timestamp.
vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.resolvedAt, ()), abi.encode(_resolvedAtTimestamp));
// Mock the status to be DEFENDER_WINS.
vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.DEFENDER_WINS)); vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.DEFENDER_WINS));
// Make our game type the respected game type. // Claim should be valid.
vm.mockCall( assertTrue(anchorStateRegistry.isGameClaimValid(gameProxy));
address(optimismPortal2), }
abi.encodeCall(optimismPortal2.respectedGameType, ()),
abi.encode(gameProxy.gameType())
);
/// @notice Tests that isGameClaimValid will return false if the game is not registered.
function testFuzz_isGameClaimValid_notRegistered_succeeds() public {
// Mock the DisputeGameFactory to make it seem that the game was not registered. // Mock the DisputeGameFactory to make it seem that the game was not registered.
vm.mockCall( vm.mockCall(
address(disputeGameFactory), address(disputeGameFactory),
...@@ -377,165 +425,90 @@ contract AnchorStateRegistry_TryUpdateAnchorState_Test is AnchorStateRegistry_In ...@@ -377,165 +425,90 @@ contract AnchorStateRegistry_TryUpdateAnchorState_Test is AnchorStateRegistry_In
abi.encode(address(0), 0) abi.encode(address(0), 0)
); );
// Try to update the anchor state. // Claim should not be valid.
vm.prank(address(gameProxy)); assertFalse(anchorStateRegistry.isGameClaimValid(gameProxy));
vm.expectEmit(address(anchorStateRegistry));
emit AnchorNotUpdated(gameProxy);
anchorStateRegistry.tryUpdateAnchorState();
// Confirm that the anchor state has not updated.
(Hash updatedRoot, uint256 updatedL2BlockNumber) = anchorStateRegistry.getAnchorRoot();
assertEq(updatedL2BlockNumber, l2BlockNumber);
assertEq(updatedRoot.raw(), root.raw());
} }
/// @notice Tests that tryUpdateAnchorState will not update the anchor state if the game status /// @notice Tests that isGameClaimValid will return false if the game is not respected.
/// is CHALLENGER_WINS. /// @param _gameType The game type to use for the test.
/// @param _l2BlockNumber The L2 block number to use for the game. function testFuzz_isGameClaimValid_isNotRespected_succeeds(GameType _gameType) public {
function testFuzz_tryUpdateAnchorState_challengerWinsNoUpdate_succeeds(uint256 _l2BlockNumber) public { if (_gameType.raw() == gameProxy.gameType().raw()) {
// Grab block number of the existing anchor root. _gameType = GameType.wrap(_gameType.raw() + 1);
(Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot(); }
// Bound the new block number.
_l2BlockNumber = bound(_l2BlockNumber, l2BlockNumber, type(uint256).max);
// Mock the l2BlockNumber call. // Mock that the game was not respected.
vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2BlockNumber, ()), abi.encode(_l2BlockNumber)); vm.mockCall(
address(gameProxy), abi.encodeCall(gameProxy.wasRespectedGameTypeWhenCreated, ()), abi.encode(false)
);
// Mock the CHALLENGER_WINS state. // Claim should not be valid.
vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.CHALLENGER_WINS)); assertFalse(anchorStateRegistry.isGameClaimValid(gameProxy));
}
// Make our game type the respected game type. /// @notice Tests that isGameClaimValid will return false if the game is blacklisted.
function testFuzz_isGameClaimValid_isBlacklisted_succeeds() public {
// Mock the disputeGameBlacklist call to return true.
vm.mockCall( vm.mockCall(
address(optimismPortal2), address(optimismPortal2),
abi.encodeCall(optimismPortal2.respectedGameType, ()), abi.encodeCall(optimismPortal2.disputeGameBlacklist, (gameProxy)),
abi.encode(gameProxy.gameType()) abi.encode(true)
); );
// Try to update the anchor state. // Claim should not be valid.
vm.prank(address(gameProxy)); assertFalse(anchorStateRegistry.isGameClaimValid(gameProxy));
vm.expectEmit(address(anchorStateRegistry));
emit AnchorNotUpdated(gameProxy);
anchorStateRegistry.tryUpdateAnchorState();
// Confirm that the anchor state has not updated.
(Hash updatedRoot, uint256 updatedL2BlockNumber) = anchorStateRegistry.getAnchorRoot();
assertEq(updatedL2BlockNumber, l2BlockNumber);
assertEq(updatedRoot.raw(), root.raw());
} }
/// @notice Tests that tryUpdateAnchorState will not update the anchor state if the game status /// @notice Tests that isGameClaimValid will return false if the game is retired.
/// is IN_PROGRESS. /// @param _resolvedAtTimestamp The resolvedAt timestamp to use for the test.
/// @param _l2BlockNumber The L2 block number to use for the game. function testFuzz_isGameClaimValid_isRetired_succeeds(uint256 _resolvedAtTimestamp) public {
function testFuzz_tryUpdateAnchorState_inProgressNoUpdate_succeeds(uint256 _l2BlockNumber) public { // Make sure retirement timestamp is later than the game's creation time.
// Grab block number of the existing anchor root. _resolvedAtTimestamp = uint64(bound(_resolvedAtTimestamp, gameProxy.createdAt().raw() + 1, type(uint64).max));
(Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot();
// Bound the new block number.
_l2BlockNumber = bound(_l2BlockNumber, l2BlockNumber, type(uint256).max);
// Mock the l2BlockNumber call.
vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2BlockNumber, ()), abi.encode(_l2BlockNumber));
// Mock the CHALLENGER_WINS state.
vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.IN_PROGRESS));
// Make our game type the respected game type. // Mock the respectedGameTypeUpdatedAt call to be later than the game's creation time.
vm.mockCall( vm.mockCall(
address(optimismPortal2), address(optimismPortal2),
abi.encodeCall(optimismPortal2.respectedGameType, ()), abi.encodeCall(optimismPortal2.respectedGameTypeUpdatedAt, ()),
abi.encode(gameProxy.gameType()) abi.encode(_resolvedAtTimestamp)
); );
// Try to update the anchor state. // Claim should not be valid.
vm.prank(address(gameProxy)); assertFalse(anchorStateRegistry.isGameClaimValid(gameProxy));
vm.expectEmit(address(anchorStateRegistry));
emit AnchorNotUpdated(gameProxy);
anchorStateRegistry.tryUpdateAnchorState();
// Confirm that the anchor state has not updated.
(Hash updatedRoot, uint256 updatedL2BlockNumber) = anchorStateRegistry.getAnchorRoot();
assertEq(updatedL2BlockNumber, l2BlockNumber);
assertEq(updatedRoot.raw(), root.raw());
} }
/// @notice Tests that tryUpdateAnchorState will not update the anchor state if the game type /// @notice Tests that isGameClaimValid will return false if the game is not resolved.
/// is not the respected game type. function testFuzz_isGameClaimValid_notResolved_succeeds() public {
/// @param _l2BlockNumber The L2 block number to use for the game. // Mock the status to be IN_PROGRESS.
function testFuzz_tryUpdateAnchorState_notRespectedGameTypeNoUpdate_succeeds(uint256 _l2BlockNumber) public { vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.IN_PROGRESS));
// Grab block number of the existing anchor root.
(Hash root, uint256 l2BlockNumber) = anchorStateRegistry.anchors(gameProxy.gameType());
// Bound the new block number.
_l2BlockNumber = bound(_l2BlockNumber, l2BlockNumber, type(uint256).max);
// Mock the l2BlockNumber call.
vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2BlockNumber, ()), abi.encode(_l2BlockNumber));
// Mock the DEFENDER_WINS state.
vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.DEFENDER_WINS));
// Mock the respectedGameType call so that it does NOT match our game type.
vm.mockCall(address(optimismPortal2), abi.encodeCall(optimismPortal2.respectedGameType, ()), abi.encode(999));
// Try to update the anchor state.
vm.prank(address(gameProxy));
vm.expectEmit(address(anchorStateRegistry));
emit AnchorNotUpdated(gameProxy);
anchorStateRegistry.tryUpdateAnchorState();
// Confirm that the anchor state has not updated. // Claim should not be valid.
(Hash updatedRoot, uint256 updatedL2BlockNumber) = anchorStateRegistry.anchors(gameProxy.gameType()); assertFalse(anchorStateRegistry.isGameClaimValid(gameProxy));
assertEq(updatedL2BlockNumber, l2BlockNumber);
assertEq(updatedRoot.raw(), root.raw());
} }
/// @notice Tests that tryUpdateAnchorState will not update the anchor state if the game is /// @notice Tests that isGameClaimValid will return false if the game is not airgapped.
/// blacklisted. /// @param _resolvedAtTimestamp The resolvedAt timestamp to use for the test.
/// @param _l2BlockNumber The L2 block number to use for the game. function testFuzz_isGameClaimValid_notAirgapped_succeeds(uint256 _resolvedAtTimestamp) public {
function testFuzz_tryUpdateAnchorState_blacklistedGameNoUpdate_succeeds(uint256 _l2BlockNumber) public { // Warp forward by disputeGameFinalityDelaySeconds.
// Grab block number of the existing anchor root. vm.warp(block.timestamp + optimismPortal2.disputeGameFinalityDelaySeconds());
(Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot();
// Bound the new block number.
_l2BlockNumber = bound(_l2BlockNumber, l2BlockNumber + 1, type(uint256).max);
// Mock the l2BlockNumber call.
vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2BlockNumber, ()), abi.encode(_l2BlockNumber));
// Mock the DEFENDER_WINS state.
vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.DEFENDER_WINS));
// Make our game type the respected game type.
vm.mockCall(
address(optimismPortal2),
abi.encodeCall(optimismPortal2.respectedGameType, ()),
abi.encode(gameProxy.gameType())
);
// Mock the disputeGameBlacklist call to return true. // Bound resolvedAt to be less than disputeGameFinalityDelaySeconds in the past.
vm.mockCall( _resolvedAtTimestamp = bound(
address(optimismPortal2), _resolvedAtTimestamp, block.timestamp - optimismPortal2.disputeGameFinalityDelaySeconds(), block.timestamp
abi.encodeCall(optimismPortal2.disputeGameBlacklist, (gameProxy)),
abi.encode(true)
); );
// Update the anchor state. // Mock the resolvedAt timestamp.
vm.prank(address(gameProxy)); vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.resolvedAt, ()), abi.encode(_resolvedAtTimestamp));
vm.expectEmit(address(anchorStateRegistry));
emit AnchorNotUpdated(gameProxy);
anchorStateRegistry.tryUpdateAnchorState();
// Confirm that the anchor state has not updated. // Claim should not be valid.
(Hash updatedRoot, uint256 updatedL2BlockNumber) = anchorStateRegistry.anchors(gameProxy.gameType()); assertFalse(anchorStateRegistry.isGameClaimValid(gameProxy));
assertEq(updatedL2BlockNumber, l2BlockNumber);
assertEq(updatedRoot.raw(), root.raw());
} }
}
/// @notice Tests that tryUpdateAnchorState will not update the anchor state if the game is contract AnchorStateRegistry_SetAnchorState_Test is AnchorStateRegistry_Init {
/// retired. /// @notice Tests that setAnchorState will succeed if the game is valid, the game block
/// number is greater than the current anchor root block number, and the game is the
/// currently respected game type.
/// @param _l2BlockNumber The L2 block number to use for the game. /// @param _l2BlockNumber The L2 block number to use for the game.
function testFuzz_tryUpdateAnchorState_retiredGameNoUpdate_succeeds(uint256 _l2BlockNumber) public { function testFuzz_setAnchorState_validNewerState_succeeds(uint256 _l2BlockNumber) public {
// Grab block number of the existing anchor root. // Grab block number of the existing anchor root.
(Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot(); (Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot();
...@@ -548,61 +521,23 @@ contract AnchorStateRegistry_TryUpdateAnchorState_Test is AnchorStateRegistry_In ...@@ -548,61 +521,23 @@ contract AnchorStateRegistry_TryUpdateAnchorState_Test is AnchorStateRegistry_In
// Mock the DEFENDER_WINS state. // Mock the DEFENDER_WINS state.
vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.DEFENDER_WINS)); vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.DEFENDER_WINS));
// Make our game type the respected game type. // Mock that the game was respected.
vm.mockCall( vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.wasRespectedGameTypeWhenCreated, ()), abi.encode(true));
address(optimismPortal2),
abi.encodeCall(optimismPortal2.respectedGameType, ()),
abi.encode(gameProxy.gameType())
);
// Mock the respectedGameTypeUpdatedAt call to be later than the game's creation time. // Mock the resolvedAt timestamp and fast forward to beyond the delay.
vm.mockCall( vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.resolvedAt, ()), abi.encode(block.timestamp));
address(optimismPortal2), vm.warp(block.timestamp + optimismPortal2.disputeGameFinalityDelaySeconds() + 1);
abi.encodeCall(optimismPortal2.respectedGameTypeUpdatedAt, ()),
abi.encode(gameProxy.createdAt().raw() + 1)
);
// Update the anchor state. // Update the anchor state.
vm.prank(address(gameProxy)); vm.prank(address(gameProxy));
vm.expectEmit(address(anchorStateRegistry)); vm.expectEmit(address(anchorStateRegistry));
emit AnchorNotUpdated(gameProxy);
anchorStateRegistry.tryUpdateAnchorState();
// Confirm that the anchor state has not updated.
(Hash updatedRoot, uint256 updatedL2BlockNumber) = anchorStateRegistry.anchors(gameProxy.gameType());
assertEq(updatedL2BlockNumber, l2BlockNumber);
assertEq(updatedRoot.raw(), root.raw());
}
}
contract AnchorStateRegistry_SetAnchorState_Test is AnchorStateRegistry_Init {
/// @notice Tests that setAnchorState will succeed with a game with any L2 block number as long
/// as the game is valid and is the currently respected game type.
/// @param _l2BlockNumber The L2 block number to use for the game.
function testFuzz_setAnchorState_anyL2BlockNumber_succeeds(uint256 _l2BlockNumber) public {
// Mock the l2BlockNumber call.
vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2BlockNumber, ()), abi.encode(_l2BlockNumber));
// Mock the DEFENDER_WINS state.
vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.DEFENDER_WINS));
// Make our game type the respected game type.
vm.mockCall(
address(optimismPortal2),
abi.encodeCall(optimismPortal2.respectedGameType, ()),
abi.encode(gameProxy.gameType())
);
// Set the anchor state.
vm.prank(superchainConfig.guardian());
vm.expectEmit(address(anchorStateRegistry));
emit AnchorUpdated(gameProxy); emit AnchorUpdated(gameProxy);
anchorStateRegistry.setAnchorState(gameProxy); anchorStateRegistry.setAnchorState(gameProxy);
// Confirm that the anchor state has updated. // Confirm that the anchor state is now the same as the game state.
(Hash updatedRoot, uint256 updatedL2BlockNumber) = anchorStateRegistry.anchors(gameProxy.gameType()); (root, l2BlockNumber) = anchorStateRegistry.getAnchorRoot();
assertEq(updatedL2BlockNumber, gameProxy.l2BlockNumber()); assertEq(l2BlockNumber, gameProxy.l2BlockNumber());
assertEq(updatedRoot.raw(), gameProxy.rootClaim().raw()); assertEq(root.raw(), gameProxy.rootClaim().raw());
// Confirm that the anchor game is now set. // Confirm that the anchor game is now set.
IFaultDisputeGame anchorGame = anchorStateRegistry.anchorGame(); IFaultDisputeGame anchorGame = anchorStateRegistry.anchorGame();
...@@ -611,42 +546,36 @@ contract AnchorStateRegistry_SetAnchorState_Test is AnchorStateRegistry_Init { ...@@ -611,42 +546,36 @@ contract AnchorStateRegistry_SetAnchorState_Test is AnchorStateRegistry_Init {
} }
contract AnchorStateRegistry_SetAnchorState_TestFail is AnchorStateRegistry_Init { contract AnchorStateRegistry_SetAnchorState_TestFail is AnchorStateRegistry_Init {
/// @notice Tests that setAnchorState will revert if the sender is not the guardian. /// @notice Tests that setAnchorState will revert if the game is valid and the game block
/// @param _sender The address of the sender. /// number is less than or equal to the current anchor root block number.
/// @param _l2BlockNumber The L2 block number to use for the game. /// @param _l2BlockNumber The L2 block number to use for the game.
function testFuzz_setAnchorState_notGuardian_fails(address _sender, uint256 _l2BlockNumber) public { function testFuzz_setAnchorState_olderValidGameClaim_fails(uint256 _l2BlockNumber) public {
// Grab block number of the existing anchor root. // Grab block number of the existing anchor root.
(Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot(); (Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot();
// Bound the new block number.
_l2BlockNumber = bound(_l2BlockNumber, 0, l2BlockNumber);
// Mock the l2BlockNumber call. // Mock the l2BlockNumber call.
vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2BlockNumber, ()), abi.encode(_l2BlockNumber)); vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2BlockNumber, ()), abi.encode(_l2BlockNumber));
// Mock the DEFENDER_WINS state. // Mock the DEFENDER_WINS state.
vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.DEFENDER_WINS)); vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.DEFENDER_WINS));
// Make our game type the respected game type. // Mock that the game was respected.
vm.mockCall( vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.wasRespectedGameTypeWhenCreated, ()), abi.encode(true));
address(optimismPortal2),
abi.encodeCall(optimismPortal2.respectedGameType, ()),
abi.encode(gameProxy.gameType())
);
// Mock the DisputeGameFactory to make it seem that the game was not registered. // Mock the resolvedAt timestamp and fast forward to beyond the delay.
vm.mockCall( vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.resolvedAt, ()), abi.encode(block.timestamp));
address(disputeGameFactory), vm.warp(block.timestamp + optimismPortal2.disputeGameFinalityDelaySeconds() + 1);
abi.encodeCall(
disputeGameFactory.games, (gameProxy.gameType(), gameProxy.rootClaim(), gameProxy.extraData())
),
abi.encode(address(0), 0)
);
// Try to update the anchor state. // Try to update the anchor state.
vm.prank(_sender); vm.prank(address(gameProxy));
vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_Unauthorized.selector); vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_InvalidAnchorGame.selector);
anchorStateRegistry.setAnchorState(gameProxy); anchorStateRegistry.setAnchorState(gameProxy);
// Confirm that the anchor state has not updated. // Confirm that the anchor state has not updated.
(Hash updatedRoot, uint256 updatedL2BlockNumber) = anchorStateRegistry.anchors(gameProxy.gameType()); (Hash updatedRoot, uint256 updatedL2BlockNumber) = anchorStateRegistry.getAnchorRoot();
assertEq(updatedL2BlockNumber, l2BlockNumber); assertEq(updatedL2BlockNumber, l2BlockNumber);
assertEq(updatedRoot.raw(), root.raw()); assertEq(updatedRoot.raw(), root.raw());
} }
...@@ -657,18 +586,17 @@ contract AnchorStateRegistry_SetAnchorState_TestFail is AnchorStateRegistry_Init ...@@ -657,18 +586,17 @@ contract AnchorStateRegistry_SetAnchorState_TestFail is AnchorStateRegistry_Init
// Grab block number of the existing anchor root. // Grab block number of the existing anchor root.
(Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot(); (Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot();
// Bound the new block number.
_l2BlockNumber = bound(_l2BlockNumber, l2BlockNumber, type(uint256).max);
// Mock the l2BlockNumber call. // Mock the l2BlockNumber call.
vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2BlockNumber, ()), abi.encode(_l2BlockNumber)); vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2BlockNumber, ()), abi.encode(_l2BlockNumber));
// Mock the DEFENDER_WINS state. // Mock the DEFENDER_WINS state.
vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.DEFENDER_WINS)); vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.DEFENDER_WINS));
// Make our game type the respected game type. // Mock that the game was respected.
vm.mockCall( vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.wasRespectedGameTypeWhenCreated, ()), abi.encode(true));
address(optimismPortal2),
abi.encodeCall(optimismPortal2.respectedGameType, ()),
abi.encode(gameProxy.gameType())
);
// Mock the DisputeGameFactory to make it seem that the game was not registered. // Mock the DisputeGameFactory to make it seem that the game was not registered.
vm.mockCall( vm.mockCall(
...@@ -681,37 +609,40 @@ contract AnchorStateRegistry_SetAnchorState_TestFail is AnchorStateRegistry_Init ...@@ -681,37 +609,40 @@ contract AnchorStateRegistry_SetAnchorState_TestFail is AnchorStateRegistry_Init
// Try to update the anchor state. // Try to update the anchor state.
vm.prank(superchainConfig.guardian()); vm.prank(superchainConfig.guardian());
vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_ImproperAnchorGame.selector); vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_InvalidAnchorGame.selector);
anchorStateRegistry.setAnchorState(gameProxy); anchorStateRegistry.setAnchorState(gameProxy);
// Confirm that the anchor state has not updated. // Confirm that the anchor state has not updated.
(Hash updatedRoot, uint256 updatedL2BlockNumber) = anchorStateRegistry.anchors(gameProxy.gameType()); (Hash updatedRoot, uint256 updatedL2BlockNumber) = anchorStateRegistry.getAnchorRoot();
assertEq(updatedL2BlockNumber, l2BlockNumber); assertEq(updatedL2BlockNumber, l2BlockNumber);
assertEq(updatedRoot.raw(), root.raw()); assertEq(updatedRoot.raw(), root.raw());
} }
/// @notice Tests that setAnchorState will revert if the game is valid and the game status is /// @notice Tests that setAnchorState will revert if the game is valid and the game status
/// CHALLENGER_WINS. /// is CHALLENGER_WINS.
/// @param _l2BlockNumber The L2 block number to use for the game. /// @param _l2BlockNumber The L2 block number to use for the game.
function testFuzz_setAnchorState_challengerWins_fails(uint256 _l2BlockNumber) public { function testFuzz_setAnchorState_challengerWins_fails(uint256 _l2BlockNumber) public {
// Grab block number of the existing anchor root. // Grab block number of the existing anchor root.
(Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot(); (Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot();
// Bound the new block number.
_l2BlockNumber = bound(_l2BlockNumber, l2BlockNumber, type(uint256).max);
// Mock the l2BlockNumber call. // Mock the l2BlockNumber call.
vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2BlockNumber, ()), abi.encode(_l2BlockNumber)); vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2BlockNumber, ()), abi.encode(_l2BlockNumber));
// Mock the CHALLENGER_WINS state. // Mock the CHALLENGER_WINS state.
vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.CHALLENGER_WINS)); vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.CHALLENGER_WINS));
// Make our game type the respected game type. // Mock that the game was respected.
vm.mockCall( vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.wasRespectedGameTypeWhenCreated, ()), abi.encode(true));
address(optimismPortal2),
abi.encodeCall(optimismPortal2.respectedGameType, ()), // Mock the resolvedAt timestamp and fast forward to beyond the delay.
abi.encode(gameProxy.gameType()) vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.resolvedAt, ()), abi.encode(block.timestamp));
); vm.warp(block.timestamp + optimismPortal2.disputeGameFinalityDelaySeconds() + 1);
// Try to update the anchor state. // Try to update the anchor state.
vm.prank(superchainConfig.guardian()); vm.prank(address(gameProxy));
vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_InvalidAnchorGame.selector); vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_InvalidAnchorGame.selector);
anchorStateRegistry.setAnchorState(gameProxy); anchorStateRegistry.setAnchorState(gameProxy);
...@@ -721,8 +652,8 @@ contract AnchorStateRegistry_SetAnchorState_TestFail is AnchorStateRegistry_Init ...@@ -721,8 +652,8 @@ contract AnchorStateRegistry_SetAnchorState_TestFail is AnchorStateRegistry_Init
assertEq(updatedRoot.raw(), root.raw()); assertEq(updatedRoot.raw(), root.raw());
} }
/// @notice Tests that setAnchorState will revert if the game is valid and the game status is /// @notice Tests that setAnchorState will revert if the game is valid and the game status
/// IN_PROGRESS. /// is IN_PROGRESS.
/// @param _l2BlockNumber The L2 block number to use for the game. /// @param _l2BlockNumber The L2 block number to use for the game.
function testFuzz_setAnchorState_inProgress_fails(uint256 _l2BlockNumber) public { function testFuzz_setAnchorState_inProgress_fails(uint256 _l2BlockNumber) public {
// Grab block number of the existing anchor root. // Grab block number of the existing anchor root.
...@@ -734,18 +665,18 @@ contract AnchorStateRegistry_SetAnchorState_TestFail is AnchorStateRegistry_Init ...@@ -734,18 +665,18 @@ contract AnchorStateRegistry_SetAnchorState_TestFail is AnchorStateRegistry_Init
// Mock the l2BlockNumber call. // Mock the l2BlockNumber call.
vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2BlockNumber, ()), abi.encode(_l2BlockNumber)); vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2BlockNumber, ()), abi.encode(_l2BlockNumber));
// Mock the IN_PROGRESS state. // Mock the CHALLENGER_WINS state.
vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.IN_PROGRESS)); vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.IN_PROGRESS));
// Make our game type the respected game type. // Mock that the game was respected.
vm.mockCall( vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.wasRespectedGameTypeWhenCreated, ()), abi.encode(true));
address(optimismPortal2),
abi.encodeCall(optimismPortal2.respectedGameType, ()), // Mock the resolvedAt timestamp and fast forward to beyond the delay.
abi.encode(gameProxy.gameType()) vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.resolvedAt, ()), abi.encode(block.timestamp));
); vm.warp(block.timestamp + optimismPortal2.disputeGameFinalityDelaySeconds() + 1);
// Try to update the anchor state. // Try to update the anchor state.
vm.prank(superchainConfig.guardian()); vm.prank(address(gameProxy));
vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_InvalidAnchorGame.selector); vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_InvalidAnchorGame.selector);
anchorStateRegistry.setAnchorState(gameProxy); anchorStateRegistry.setAnchorState(gameProxy);
...@@ -755,25 +686,33 @@ contract AnchorStateRegistry_SetAnchorState_TestFail is AnchorStateRegistry_Init ...@@ -755,25 +686,33 @@ contract AnchorStateRegistry_SetAnchorState_TestFail is AnchorStateRegistry_Init
assertEq(updatedRoot.raw(), root.raw()); assertEq(updatedRoot.raw(), root.raw());
} }
/// @notice Tests that setAnchorState will revert if the game is valid and the game type is not /// @notice Tests that setAnchorState will revert if the game is not respected.
/// the respected game type.
/// @param _l2BlockNumber The L2 block number to use for the game. /// @param _l2BlockNumber The L2 block number to use for the game.
function testFuzz_setAnchorState_notRespectedGameType_fails(uint256 _l2BlockNumber) public { function testFuzz_setAnchorState_isNotRespectedGame_fails(uint256 _l2BlockNumber) public {
// Grab block number of the existing anchor root. // Grab block number of the existing anchor root.
(Hash root, uint256 l2BlockNumber) = anchorStateRegistry.anchors(gameProxy.gameType()); (Hash root, uint256 l2BlockNumber) = anchorStateRegistry.anchors(gameProxy.gameType());
// Bound the new block number.
_l2BlockNumber = bound(_l2BlockNumber, l2BlockNumber, type(uint256).max);
// Mock the l2BlockNumber call. // Mock the l2BlockNumber call.
vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2BlockNumber, ()), abi.encode(_l2BlockNumber)); vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2BlockNumber, ()), abi.encode(_l2BlockNumber));
// Mock the DEFENDER_WINS state. // Mock the DEFENDER_WINS state.
vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.DEFENDER_WINS)); vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.DEFENDER_WINS));
// Mock the respectedGameType call so that it does NOT match our game type. // Mock the resolvedAt timestamp and fast forward to beyond the delay.
vm.mockCall(address(optimismPortal2), abi.encodeCall(optimismPortal2.respectedGameType, ()), abi.encode(999)); vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.resolvedAt, ()), abi.encode(block.timestamp));
vm.warp(block.timestamp + optimismPortal2.disputeGameFinalityDelaySeconds() + 1);
// Mock that the game was not respected when created.
vm.mockCall(
address(gameProxy), abi.encodeCall(gameProxy.wasRespectedGameTypeWhenCreated, ()), abi.encode(false)
);
// Try to update the anchor state. // Try to update the anchor state.
vm.prank(superchainConfig.guardian()); vm.prank(address(gameProxy));
vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_ImproperAnchorGame.selector); vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_InvalidAnchorGame.selector);
anchorStateRegistry.setAnchorState(gameProxy); anchorStateRegistry.setAnchorState(gameProxy);
// Confirm that the anchor state has not updated. // Confirm that the anchor state has not updated.
...@@ -782,24 +721,25 @@ contract AnchorStateRegistry_SetAnchorState_TestFail is AnchorStateRegistry_Init ...@@ -782,24 +721,25 @@ contract AnchorStateRegistry_SetAnchorState_TestFail is AnchorStateRegistry_Init
assertEq(updatedRoot.raw(), root.raw()); assertEq(updatedRoot.raw(), root.raw());
} }
/// @notice Tests that setAnchorState will revert if the game is valid and the game is blacklisted. /// @notice Tests that setAnchorState will revert if the game is valid and the game is
/// blacklisted.
/// @param _l2BlockNumber The L2 block number to use for the game. /// @param _l2BlockNumber The L2 block number to use for the game.
function test_setAnchorState_blacklistedGame_fails(uint256 _l2BlockNumber) public { function testFuzz_setAnchorState_blacklistedGame_fails(uint256 _l2BlockNumber) public {
// Grab block number of the existing anchor root. // Grab block number of the existing anchor root.
(Hash root, uint256 l2BlockNumber) = anchorStateRegistry.anchors(gameProxy.gameType()); (Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot();
// Mock the l2BlockNumber call. // Bound the new block number.
vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2BlockNumber, ()), abi.encode(_l2BlockNumber)); _l2BlockNumber = bound(_l2BlockNumber, l2BlockNumber + 1, type(uint256).max);
// Mock the DEFENDER_WINS state. // Mock the DEFENDER_WINS state.
vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.DEFENDER_WINS)); vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.DEFENDER_WINS));
// Make our game type the respected game type. // Mock that the game was respected.
vm.mockCall( vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.wasRespectedGameTypeWhenCreated, ()), abi.encode(true));
address(optimismPortal2),
abi.encodeCall(optimismPortal2.respectedGameType, ()), // Mock the resolvedAt timestamp and fast forward to beyond the delay.
abi.encode(gameProxy.gameType()) vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.resolvedAt, ()), abi.encode(block.timestamp));
); vm.warp(block.timestamp + optimismPortal2.disputeGameFinalityDelaySeconds() + 1);
// Mock the disputeGameBlacklist call to return true. // Mock the disputeGameBlacklist call to return true.
vm.mockCall( vm.mockCall(
...@@ -808,9 +748,9 @@ contract AnchorStateRegistry_SetAnchorState_TestFail is AnchorStateRegistry_Init ...@@ -808,9 +748,9 @@ contract AnchorStateRegistry_SetAnchorState_TestFail is AnchorStateRegistry_Init
abi.encode(true) abi.encode(true)
); );
// Set the anchor state. // Update the anchor state.
vm.prank(superchainConfig.guardian()); vm.prank(address(gameProxy));
vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_ImproperAnchorGame.selector); vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_InvalidAnchorGame.selector);
anchorStateRegistry.setAnchorState(gameProxy); anchorStateRegistry.setAnchorState(gameProxy);
// Confirm that the anchor state has not updated. // Confirm that the anchor state has not updated.
...@@ -819,24 +759,21 @@ contract AnchorStateRegistry_SetAnchorState_TestFail is AnchorStateRegistry_Init ...@@ -819,24 +759,21 @@ contract AnchorStateRegistry_SetAnchorState_TestFail is AnchorStateRegistry_Init
assertEq(updatedRoot.raw(), root.raw()); assertEq(updatedRoot.raw(), root.raw());
} }
/// @notice Tests that setAnchorState will revert if the game is valid and the game is retired. /// @notice Tests that setAnchorState will revert if the game is valid and the game is
/// retired.
/// @param _l2BlockNumber The L2 block number to use for the game. /// @param _l2BlockNumber The L2 block number to use for the game.
function test_setAnchorState_retiredGame_fails(uint256 _l2BlockNumber) public { function testFuzz_setAnchorState_retiredGame_fails(uint256 _l2BlockNumber) public {
// Grab block number of the existing anchor root. // Grab block number of the existing anchor root.
(Hash root, uint256 l2BlockNumber) = anchorStateRegistry.anchors(gameProxy.gameType()); (Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot();
// Mock the l2BlockNumber call. // Bound the new block number.
vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2BlockNumber, ()), abi.encode(_l2BlockNumber)); _l2BlockNumber = bound(_l2BlockNumber, l2BlockNumber + 1, type(uint256).max);
// Mock the DEFENDER_WINS state. // Mock the DEFENDER_WINS state.
vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.DEFENDER_WINS)); vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.DEFENDER_WINS));
// Make our game type the respected game type. // Mock that the game was respected.
vm.mockCall( vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.wasRespectedGameTypeWhenCreated, ()), abi.encode(true));
address(optimismPortal2),
abi.encodeCall(optimismPortal2.respectedGameType, ()),
abi.encode(gameProxy.gameType())
);
// Mock the respectedGameTypeUpdatedAt call to be later than the game's creation time. // Mock the respectedGameTypeUpdatedAt call to be later than the game's creation time.
vm.mockCall( vm.mockCall(
...@@ -845,9 +782,9 @@ contract AnchorStateRegistry_SetAnchorState_TestFail is AnchorStateRegistry_Init ...@@ -845,9 +782,9 @@ contract AnchorStateRegistry_SetAnchorState_TestFail is AnchorStateRegistry_Init
abi.encode(gameProxy.createdAt().raw() + 1) abi.encode(gameProxy.createdAt().raw() + 1)
); );
// Set the anchor state. // Update the anchor state.
vm.prank(superchainConfig.guardian()); vm.prank(address(gameProxy));
vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_ImproperAnchorGame.selector); vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_InvalidAnchorGame.selector);
anchorStateRegistry.setAnchorState(gameProxy); anchorStateRegistry.setAnchorState(gameProxy);
// Confirm that the anchor state has not updated. // Confirm that the anchor state has not updated.
......
...@@ -352,26 +352,48 @@ contract DelayedWETH_Recover_Test is DelayedWETH_Init { ...@@ -352,26 +352,48 @@ contract DelayedWETH_Recover_Test is DelayedWETH_Init {
contract DelayedWETH_Hold_Test is DelayedWETH_Init { contract DelayedWETH_Hold_Test is DelayedWETH_Init {
/// @dev Tests that holding WETH succeeds. /// @dev Tests that holding WETH succeeds.
function test_hold_succeeds() public { function test_hold_byOwner_succeeds() public {
uint256 amount = 1 ether; uint256 amount = 1 ether;
// Pretend to be alice and deposit some WETH. // Pretend to be alice and deposit some WETH.
vm.prank(alice); vm.prank(alice);
delayedWeth.deposit{ value: amount }(); delayedWeth.deposit{ value: amount }();
// Get our balance before.
uint256 initialBalance = delayedWeth.balanceOf(address(this));
// Hold some WETH. // Hold some WETH.
vm.expectEmit(true, true, true, false); vm.expectEmit(true, true, true, false);
emit Approval(alice, address(this), amount); emit Approval(alice, address(this), amount);
delayedWeth.hold(alice, amount); delayedWeth.hold(alice, amount);
// Verify the allowance. // Get our balance after.
assertEq(delayedWeth.allowance(alice, address(this)), amount); uint256 finalBalance = delayedWeth.balanceOf(address(this));
// Verify the transfer.
assertEq(finalBalance, initialBalance + amount);
}
function test_hold_withoutAmount_succeeds() public {
uint256 amount = 1 ether;
// Pretend to be alice and deposit some WETH.
vm.prank(alice);
delayedWeth.deposit{ value: amount }();
// Get our balance before.
uint256 initialBalance = delayedWeth.balanceOf(address(this));
// Hold some WETH.
vm.expectEmit(true, true, true, false);
emit Approval(alice, address(this), amount);
delayedWeth.hold(alice); // without amount parameter
// We can transfer. // Get our balance after.
delayedWeth.transferFrom(alice, address(this), amount); uint256 finalBalance = delayedWeth.balanceOf(address(this));
// Verify the transfer. // Verify the transfer.
assertEq(delayedWeth.balanceOf(address(this)), amount); assertEq(finalBalance, initialBalance + amount);
} }
/// @dev Tests that holding WETH by non-owner fails. /// @dev Tests that holding WETH by non-owner fails.
......
...@@ -42,6 +42,7 @@ contract FaultDisputeGame_Init is DisputeGameFactory_Init { ...@@ -42,6 +42,7 @@ contract FaultDisputeGame_Init is DisputeGameFactory_Init {
bytes internal extraData; bytes internal extraData;
event Move(uint256 indexed parentIndex, Claim indexed pivot, address indexed claimant); event Move(uint256 indexed parentIndex, Claim indexed pivot, address indexed claimant);
event GameClosed(BondDistributionMode bondDistributionMode);
event ReceiveETH(uint256 amount); event ReceiveETH(uint256 amount);
...@@ -350,6 +351,44 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init { ...@@ -350,6 +351,44 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init {
}); });
} }
/// @dev Tests that the constructor of the `FaultDisputeGame` reverts when the `_gameType`
/// parameter is set to the reserved `type(uint32).max` game type.
function test_constructor_reservedGameType_reverts() public {
AlphabetVM alphabetVM = new AlphabetVM(
absolutePrestate,
IPreimageOracle(
DeployUtils.create1({
_name: "PreimageOracle",
_args: DeployUtils.encodeConstructor(abi.encodeCall(IPreimageOracle.__constructor__, (0, 0)))
})
)
);
vm.expectRevert(ReservedGameType.selector);
DeployUtils.create1({
_name: "FaultDisputeGame",
_args: DeployUtils.encodeConstructor(
abi.encodeCall(
IFaultDisputeGame.__constructor__,
(
IFaultDisputeGame.GameConstructorParams({
gameType: GameType.wrap(type(uint32).max),
absolutePrestate: absolutePrestate,
maxGameDepth: 16,
splitDepth: 8,
clockExtension: Duration.wrap(3 hours),
maxClockDuration: Duration.wrap(3.5 days),
vm: alphabetVM,
weth: IDelayedWETH(payable(address(0))),
anchorStateRegistry: IAnchorStateRegistry(address(0)),
l2ChainId: 10
})
)
)
)
});
}
/// @dev Tests that the game's root claim is set correctly. /// @dev Tests that the game's root claim is set correctly.
function test_rootClaim_succeeds() public view { function test_rootClaim_succeeds() public view {
assertEq(gameProxy.rootClaim().raw(), ROOT_CLAIM.raw()); assertEq(gameProxy.rootClaim().raw(), ROOT_CLAIM.raw());
...@@ -459,6 +498,20 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init { ...@@ -459,6 +498,20 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init {
assertEq(gameProxy.l1Head().raw(), blockhash(block.number - 1)); assertEq(gameProxy.l1Head().raw(), blockhash(block.number - 1));
} }
/// @dev Tests that the game cannot be initialized when the anchor root is not found.
function test_initialize_anchorRootNotFound_reverts() public {
// Mock the AnchorStateRegistry to return a zero root.
vm.mockCall(
address(anchorStateRegistry),
abi.encodeCall(IAnchorStateRegistry.getAnchorRoot, ()),
abi.encode(Hash.wrap(bytes32(0)), 0)
);
// Creation should fail.
vm.expectRevert(AnchorRootNotFound.selector);
gameProxy = IFaultDisputeGame(payable(address(disputeGameFactory.create(GAME_TYPE, _dummyClaim(), hex""))));
}
/// @dev Tests that the game cannot be initialized twice. /// @dev Tests that the game cannot be initialized twice.
function test_initialize_onlyOnce_succeeds() public { function test_initialize_onlyOnce_succeeds() public {
vm.expectRevert(AlreadyInitialized.selector); vm.expectRevert(AlreadyInitialized.selector);
...@@ -967,6 +1020,17 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init { ...@@ -967,6 +1020,17 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init {
// Ensure the challenge was successful. // Ensure the challenge was successful.
assertEq(uint8(fdg.status()), uint8(GameStatus.CHALLENGER_WINS)); assertEq(uint8(fdg.status()), uint8(GameStatus.CHALLENGER_WINS));
// Wait for finalization delay.
vm.warp(block.timestamp + 3.5 days + 1 seconds);
// Close the game.
fdg.closeGame();
// Claim credit once to trigger unlock period.
fdg.claimCredit(address(this));
fdg.claimCredit(address(0xb0b));
fdg.claimCredit(address(0xace));
// Wait for the withdrawal delay. // Wait for the withdrawal delay.
vm.warp(block.timestamp + delayedWeth.delay() + 1 seconds); vm.warp(block.timestamp + delayedWeth.delay() + 1 seconds);
...@@ -1415,19 +1479,11 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init { ...@@ -1415,19 +1479,11 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init {
vm.warp(block.timestamp + 3 days + 12 hours); vm.warp(block.timestamp + 3 days + 12 hours);
assertEq(address(this).balance, 0);
gameProxy.resolveClaim(2, 0); gameProxy.resolveClaim(2, 0);
gameProxy.resolveClaim(1, 0); gameProxy.resolveClaim(1, 0);
// Wait for the withdrawal delay.
vm.warp(block.timestamp + delayedWeth.delay() + 1 seconds);
gameProxy.claimCredit(address(this));
assertEq(address(this).balance, firstBond + secondBond);
vm.expectRevert(ClaimAlreadyResolved.selector); vm.expectRevert(ClaimAlreadyResolved.selector);
gameProxy.resolveClaim(1, 0); gameProxy.resolveClaim(1, 0);
assertEq(address(this).balance, firstBond + secondBond);
} }
/// @dev Static unit test asserting that resolve reverts when attempting to resolve a subgame at max depth /// @dev Static unit test asserting that resolve reverts when attempting to resolve a subgame at max depth
...@@ -1447,16 +1503,8 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init { ...@@ -1447,16 +1503,8 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init {
vm.warp(block.timestamp + 3 days + 12 hours); vm.warp(block.timestamp + 3 days + 12 hours);
// Resolve to claim bond
uint256 balanceBefore = address(this).balance;
gameProxy.resolveClaim(8, 0); gameProxy.resolveClaim(8, 0);
// Wait for the withdrawal delay.
vm.warp(block.timestamp + delayedWeth.delay() + 1 seconds);
gameProxy.claimCredit(address(this));
assertEq(address(this).balance, balanceBefore + _getRequiredBond(7));
vm.expectRevert(ClaimAlreadyResolved.selector); vm.expectRevert(ClaimAlreadyResolved.selector);
gameProxy.resolveClaim(8, 0); gameProxy.resolveClaim(8, 0);
} }
...@@ -1534,9 +1582,19 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init { ...@@ -1534,9 +1582,19 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init {
} }
gameProxy.resolve(); gameProxy.resolve();
// Wait for finalization delay
vm.warp(block.timestamp + 3.5 days + 1 seconds);
// Close the game.
gameProxy.closeGame();
// Claim credit once to trigger unlock period.
gameProxy.claimCredit(address(this));
// Wait for the withdrawal delay. // Wait for the withdrawal delay.
vm.warp(block.timestamp + delayedWeth.delay() + 1 seconds); vm.warp(block.timestamp + delayedWeth.delay() + 1 seconds);
// Claim credit again to get the bond back.
gameProxy.claimCredit(address(this)); gameProxy.claimCredit(address(this));
// Ensure that bonds were paid out correctly. // Ensure that bonds were paid out correctly.
...@@ -1622,11 +1680,24 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init { ...@@ -1622,11 +1680,24 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init {
(bool success,) = address(gameProxy).call(abi.encodeCall(gameProxy.resolveClaim, (i - 1, 0))); (bool success,) = address(gameProxy).call(abi.encodeCall(gameProxy.resolveClaim, (i - 1, 0)));
assertTrue(success); assertTrue(success);
} }
// Resolve the game.
gameProxy.resolve(); gameProxy.resolve();
// Wait for finalization delay
vm.warp(block.timestamp + 3.5 days + 1 seconds);
// Close the game.
gameProxy.closeGame();
// Claim credit once to trigger unlock period.
gameProxy.claimCredit(address(this));
gameProxy.claimCredit(bob);
// Wait for the withdrawal delay. // Wait for the withdrawal delay.
vm.warp(block.timestamp + delayedWeth.delay() + 1 seconds); vm.warp(block.timestamp + delayedWeth.delay() + 1 seconds);
// Claim credit again to get the bond back.
gameProxy.claimCredit(address(this)); gameProxy.claimCredit(address(this));
// Bob's claim should revert since it's value is 0 // Bob's claim should revert since it's value is 0
...@@ -1684,9 +1755,22 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init { ...@@ -1684,9 +1755,22 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init {
} }
gameProxy.resolve(); gameProxy.resolve();
// Wait for finalization delay
vm.warp(block.timestamp + 3.5 days + 1 seconds);
// Close the game.
gameProxy.closeGame();
// Claim credit once to trigger unlock period.
gameProxy.claimCredit(address(this));
gameProxy.claimCredit(alice);
gameProxy.claimCredit(bob);
gameProxy.claimCredit(charlie);
// Wait for the withdrawal delay. // Wait for the withdrawal delay.
vm.warp(block.timestamp + delayedWeth.delay() + 1 seconds); vm.warp(block.timestamp + delayedWeth.delay() + 1 seconds);
// All of these claims should work.
gameProxy.claimCredit(address(this)); gameProxy.claimCredit(address(this));
gameProxy.claimCredit(alice); gameProxy.claimCredit(alice);
gameProxy.claimCredit(bob); gameProxy.claimCredit(bob);
...@@ -1720,6 +1804,12 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init { ...@@ -1720,6 +1804,12 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init {
gameProxy.resolveClaim(0, 0); gameProxy.resolveClaim(0, 0);
assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.DEFENDER_WINS)); assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.DEFENDER_WINS));
// Wait for finalization delay.
vm.warp(block.timestamp + 3.5 days + 1 seconds);
// Close the game.
gameProxy.closeGame();
// Confirm that the anchor state is now the same as the game state. // Confirm that the anchor state is now the same as the game state.
(root, l2BlockNumber) = anchorStateRegistry.anchors(gameProxy.gameType()); (root, l2BlockNumber) = anchorStateRegistry.anchors(gameProxy.gameType());
assertEq(l2BlockNumber, gameProxy.l2BlockNumber()); assertEq(l2BlockNumber, gameProxy.l2BlockNumber());
...@@ -1742,6 +1832,12 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init { ...@@ -1742,6 +1832,12 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init {
gameProxy.resolveClaim(0, 0); gameProxy.resolveClaim(0, 0);
assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.DEFENDER_WINS)); assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.DEFENDER_WINS));
// Wait for finalization delay.
vm.warp(block.timestamp + 3.5 days + 1 seconds);
// Close the game.
gameProxy.closeGame();
// Confirm that the anchor state is the same as the initial anchor state. // Confirm that the anchor state is the same as the initial anchor state.
(Hash updatedRoot, uint256 updatedL2BlockNumber) = anchorStateRegistry.anchors(gameProxy.gameType()); (Hash updatedRoot, uint256 updatedL2BlockNumber) = anchorStateRegistry.anchors(gameProxy.gameType());
assertEq(updatedL2BlockNumber, l2BlockNumber); assertEq(updatedL2BlockNumber, l2BlockNumber);
...@@ -1763,12 +1859,89 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init { ...@@ -1763,12 +1859,89 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init {
gameProxy.resolveClaim(0, 0); gameProxy.resolveClaim(0, 0);
assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.CHALLENGER_WINS)); assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.CHALLENGER_WINS));
// Wait for finalization delay.
vm.warp(block.timestamp + 3.5 days + 1 seconds);
// Close the game.
gameProxy.closeGame();
// Confirm that the anchor state is the same as the initial anchor state. // Confirm that the anchor state is the same as the initial anchor state.
(Hash updatedRoot, uint256 updatedL2BlockNumber) = anchorStateRegistry.anchors(gameProxy.gameType()); (Hash updatedRoot, uint256 updatedL2BlockNumber) = anchorStateRegistry.anchors(gameProxy.gameType());
assertEq(updatedL2BlockNumber, l2BlockNumber); assertEq(updatedL2BlockNumber, l2BlockNumber);
assertEq(updatedRoot.raw(), root.raw()); assertEq(updatedRoot.raw(), root.raw());
} }
function test_claimCredit_refundMode_succeeds() public {
// Set up actors.
address alice = address(0xa11ce);
address bob = address(0xb0b);
// Give the game proxy 1 extra ether, unregistered.
vm.deal(address(gameProxy), 1 ether);
// Perform a bonded move.
Claim claim = _dummyClaim();
// Bond the first claim.
uint256 firstBond = _getRequiredBond(0);
vm.deal(alice, firstBond);
(,,,, Claim disputed,,) = gameProxy.claimData(0);
vm.prank(alice);
gameProxy.attack{ value: firstBond }(disputed, 0, claim);
// Bond the second claim.
uint256 secondBond = _getRequiredBond(1);
vm.deal(bob, secondBond);
(,,,, disputed,,) = gameProxy.claimData(1);
vm.prank(bob);
gameProxy.attack{ value: secondBond }(disputed, 1, claim);
// Warp past the finalization period
vm.warp(block.timestamp + 3 days + 12 hours);
// Resolve the game.
// Second claim wins, so bob should get alice's credit.
gameProxy.resolveClaim(2, 0);
gameProxy.resolveClaim(1, 0);
gameProxy.resolveClaim(0, 0);
gameProxy.resolve();
// Wait for finalization delay.
vm.warp(block.timestamp + 3.5 days + 1 seconds);
// Mock that the game proxy is not proper, trigger refund mode.
vm.mockCall(
address(anchorStateRegistry),
abi.encodeCall(anchorStateRegistry.isGameProper, (gameProxy)),
abi.encode(false)
);
// Close the game.
gameProxy.closeGame();
// Assert bond distribution mode is refund mode.
assertTrue(gameProxy.bondDistributionMode() == BondDistributionMode.REFUND);
// Claim credit once to trigger unlock period.
gameProxy.claimCredit(alice);
gameProxy.claimCredit(bob);
// Wait for the withdrawal delay.
vm.warp(block.timestamp + delayedWeth.delay() + 1 seconds);
// Grab balances before claim.
uint256 aliceBalanceBefore = alice.balance;
uint256 bobBalanceBefore = bob.balance;
// Claim credit again to get the bond back.
gameProxy.claimCredit(alice);
gameProxy.claimCredit(bob);
// Should have original balance again.
assertEq(alice.balance, aliceBalanceBefore + firstBond);
assertEq(bob.balance, bobBalanceBefore + secondBond);
}
/// @dev Static unit test asserting that credit may not be drained past allowance through reentrancy. /// @dev Static unit test asserting that credit may not be drained past allowance through reentrancy.
function test_claimCredit_claimAlreadyResolved_reverts() public { function test_claimCredit_claimAlreadyResolved_reverts() public {
ClaimCreditReenter reenter = new ClaimCreditReenter(gameProxy, vm); ClaimCreditReenter reenter = new ClaimCreditReenter(gameProxy, vm);
...@@ -1808,6 +1981,21 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init { ...@@ -1808,6 +1981,21 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init {
// Ensure that the game registered the `reenter` contract's credit. // Ensure that the game registered the `reenter` contract's credit.
assertEq(gameProxy.credit(address(reenter)), reenterBond); assertEq(gameProxy.credit(address(reenter)), reenterBond);
// Resolve the root claim.
gameProxy.resolveClaim(0, 0);
// Resolve the game.
gameProxy.resolve();
// Wait for finalization delay.
vm.warp(block.timestamp + 3.5 days + 1 seconds);
// Close the game.
gameProxy.closeGame();
// Claim credit once to trigger unlock period.
gameProxy.claimCredit(address(reenter));
// Wait for the withdrawal delay. // Wait for the withdrawal delay.
vm.warp(block.timestamp + delayedWeth.delay() + 1 seconds); vm.warp(block.timestamp + delayedWeth.delay() + 1 seconds);
...@@ -1826,6 +2014,62 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init { ...@@ -1826,6 +2014,62 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init {
vm.stopPrank(); vm.stopPrank();
} }
/// @dev Tests that claimCredit reverts when recipient can't receive value.
function test_claimCredit_recipientCantReceiveValue_reverts() public {
// Set up actors.
address alice = address(0xa11ce);
address bob = address(0xb0b);
// Give the game proxy 1 extra ether, unregistered.
vm.deal(address(gameProxy), 1 ether);
// Perform a bonded move.
Claim claim = _dummyClaim();
// Bond the first claim.
uint256 firstBond = _getRequiredBond(0);
vm.deal(alice, firstBond);
(,,,, Claim disputed,,) = gameProxy.claimData(0);
vm.prank(alice);
gameProxy.attack{ value: firstBond }(disputed, 0, claim);
// Bond the second claim.
uint256 secondBond = _getRequiredBond(1);
vm.deal(bob, secondBond);
(,,,, disputed,,) = gameProxy.claimData(1);
vm.prank(bob);
gameProxy.attack{ value: secondBond }(disputed, 1, claim);
// Warp past the finalization period
vm.warp(block.timestamp + 3 days + 12 hours);
// Resolve the game.
// Second claim wins, so bob should get alice's credit.
gameProxy.resolveClaim(2, 0);
gameProxy.resolveClaim(1, 0);
gameProxy.resolveClaim(0, 0);
gameProxy.resolve();
// Wait for finalization delay.
vm.warp(block.timestamp + 3.5 days + 1 seconds);
// Close the game.
gameProxy.closeGame();
// Claim credit once to trigger unlock period.
gameProxy.claimCredit(alice);
gameProxy.claimCredit(bob);
// Wait for the withdrawal delay.
vm.warp(block.timestamp + delayedWeth.delay() + 1 seconds);
// make bob not be able to receive value by setting his contract code to something without `receive`
vm.etch(address(bob), address(L1Token).code);
vm.expectRevert(BondTransferFailed.selector);
gameProxy.claimCredit(address(bob));
}
/// @dev Tests that adding local data with an out of bounds identifier reverts. /// @dev Tests that adding local data with an out of bounds identifier reverts.
function testFuzz_addLocalData_oob_reverts(uint256 _ident) public { function testFuzz_addLocalData_oob_reverts(uint256 _ident) public {
Claim disputed; Claim disputed;
...@@ -2115,6 +2359,90 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init { ...@@ -2115,6 +2359,90 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init {
assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.DEFENDER_WINS)); assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.DEFENDER_WINS));
} }
/// @dev Tests that closeGame reverts if the game is not resolved
function test_closeGame_gameNotResolved_reverts() public {
vm.expectRevert(GameNotResolved.selector);
gameProxy.closeGame();
}
/// @dev Tests that closeGame reverts if the game is not finalized
function test_closeGame_gameNotFinalized_reverts() public {
// Resolve the game
vm.warp(block.timestamp + 3 days + 12 hours);
gameProxy.resolveClaim(0, 0);
gameProxy.resolve();
// Don't wait the finalization delay
vm.expectRevert(GameNotFinalized.selector);
gameProxy.closeGame();
}
/// @dev Tests that closeGame succeeds for a proper game (normal distribution)
function test_closeGame_properGame_succeeds() public {
// Resolve the game
vm.warp(block.timestamp + 3 days + 12 hours);
gameProxy.resolveClaim(0, 0);
gameProxy.resolve();
// Wait for finalization delay
vm.warp(block.timestamp + 3.5 days + 1 seconds);
// Close the game and verify normal distribution mode
vm.expectEmit(true, true, true, true);
emit GameClosed(BondDistributionMode.NORMAL);
gameProxy.closeGame();
assertEq(uint8(gameProxy.bondDistributionMode()), uint8(BondDistributionMode.NORMAL));
// Check that the anchor state was set correctly.
assertEq(address(gameProxy.anchorStateRegistry().anchorGame()), address(gameProxy));
}
/// @dev Tests that closeGame succeeds for an improper game (refund mode)
function test_closeGame_improperGame_succeeds() public {
// Resolve the game
vm.warp(block.timestamp + 3 days + 12 hours);
gameProxy.resolveClaim(0, 0);
gameProxy.resolve();
// Wait for finalization delay
vm.warp(block.timestamp + 3.5 days + 1 seconds);
// Mock the anchor registry to return improper game
vm.mockCall(
address(anchorStateRegistry),
abi.encodeCall(anchorStateRegistry.isGameProper, (IDisputeGame(address(gameProxy)))),
abi.encode(false, "")
);
// Close the game and verify refund mode
vm.expectEmit(true, true, true, true);
emit GameClosed(BondDistributionMode.REFUND);
gameProxy.closeGame();
assertEq(uint8(gameProxy.bondDistributionMode()), uint8(BondDistributionMode.REFUND));
}
/// @dev Tests that multiple calls to closeGame succeed after initial distribution mode is set
function test_closeGame_multipleCallsAfterSet_succeeds() public {
// Resolve and close the game first
vm.warp(block.timestamp + 3 days + 12 hours);
gameProxy.resolveClaim(0, 0);
gameProxy.resolve();
// Wait for finalization delay
vm.warp(block.timestamp + 3.5 days + 1 seconds);
// First close sets the mode
gameProxy.closeGame();
assertEq(uint8(gameProxy.bondDistributionMode()), uint8(BondDistributionMode.NORMAL));
// Subsequent closes should succeed without changing the mode
gameProxy.closeGame();
assertEq(uint8(gameProxy.bondDistributionMode()), uint8(BondDistributionMode.NORMAL));
gameProxy.closeGame();
assertEq(uint8(gameProxy.bondDistributionMode()), uint8(BondDistributionMode.NORMAL));
}
/// @dev Helper to generate a mock RLP encoded header (with only a real block number) & an output root proof. /// @dev Helper to generate a mock RLP encoded header (with only a real block number) & an output root proof.
function _generateOutputRootProof( function _generateOutputRootProof(
bytes32 _storageRoot, bytes32 _storageRoot,
......
...@@ -45,6 +45,16 @@ contract FaultDisputeGame_Solvency_Invariant is FaultDisputeGame_Init { ...@@ -45,6 +45,16 @@ contract FaultDisputeGame_Solvency_Invariant is FaultDisputeGame_Init {
} }
gameProxy.resolve(); gameProxy.resolve();
// Wait for finalization delay
vm.warp(block.timestamp + 3.5 days + 1 seconds);
// Close the game.
gameProxy.closeGame();
// Claim credit once to trigger unlock period.
gameProxy.claimCredit(address(this));
gameProxy.claimCredit(address(actor));
// Wait for the withdrawal delay. // Wait for the withdrawal delay.
vm.warp(block.timestamp + 7 days + 1 seconds); vm.warp(block.timestamp + 7 days + 1 seconds);
......
...@@ -118,6 +118,9 @@ contract OptimismPortal2_Invariant_Harness is CommonTest { ...@@ -118,6 +118,9 @@ contract OptimismPortal2_Invariant_Harness is CommonTest {
latestBlockhash: bytes32(uint256(0)) latestBlockhash: bytes32(uint256(0))
}); });
// Warp forward in time to ensure that the game is created after the retirement timestamp.
vm.warp(optimismPortal2.respectedGameTypeUpdatedAt() + 1 seconds);
// Create a dispute game with the output root we've proposed. // Create a dispute game with the output root we've proposed.
_proposedBlockNumber = 0xFF; _proposedBlockNumber = 0xFF;
IFaultDisputeGame game = IFaultDisputeGame( IFaultDisputeGame game = IFaultDisputeGame(
......
...@@ -240,6 +240,11 @@ contract DeputyGuardianModule_setRespectedGameType_Test is DeputyGuardianModule_ ...@@ -240,6 +240,11 @@ contract DeputyGuardianModule_setRespectedGameType_Test is DeputyGuardianModule_
/// @dev Tests that `setRespectedGameType` successfully updates the respected game type when called by the deputy /// @dev Tests that `setRespectedGameType` successfully updates the respected game type when called by the deputy
/// guardian. /// guardian.
function testFuzz_setRespectedGameType_succeeds(GameType _gameType) external { function testFuzz_setRespectedGameType_succeeds(GameType _gameType) external {
// Game type(uint32).max is reserved for setting the respectedGameTypeUpdatedAt timestamp.
// TODO(kelvin): Remove this once we've removed the hack.
uint32 boundedGameType = uint32(bound(_gameType.raw(), 0, type(uint32).max - 1));
_gameType = GameType.wrap(boundedGameType);
vm.expectEmit(address(safeInstance.safe)); vm.expectEmit(address(safeInstance.safe));
emit ExecutionFromModuleSuccess(address(deputyGuardianModule)); emit ExecutionFromModuleSuccess(address(deputyGuardianModule));
......
...@@ -23,7 +23,7 @@ import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; ...@@ -23,7 +23,7 @@ import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol";
/// @dev Specifies common security properties of entrypoints to L1 contracts, including authorization and /// @dev Specifies common security properties of entrypoints to L1 contracts, including authorization and
/// pausability. /// pausability.
/// When adding new functions to the L1 system, the `setUp` function must be updated to document the security /// When adding new functions to the L1 system, the `setUp` function must be updated to document the security
/// properties of the new function. The `Spec` struct reppresents this documentation. However, this contract does /// properties of the new function. The `Spec` struct represents this documentation. However, this contract does
/// not actually test to verify these properties, only that a spec is defined. /// not actually test to verify these properties, only that a spec is defined.
contract Specification_Test is CommonTest { contract Specification_Test is CommonTest {
enum Role { enum Role {
...@@ -551,21 +551,25 @@ contract Specification_Test is CommonTest { ...@@ -551,21 +551,25 @@ contract Specification_Test is CommonTest {
_addSpec({ _name: "MintManager", _sel: _getSel("upgrade(address)"), _auth: Role.MINTMANAGEROWNER }); _addSpec({ _name: "MintManager", _sel: _getSel("upgrade(address)"), _auth: Role.MINTMANAGEROWNER });
// AnchorStateRegistry // AnchorStateRegistry
_addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("anchorGame()") });
_addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("anchors(uint32)") }); _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("anchors(uint32)") });
_addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("getAnchorRoot()") }); _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("getAnchorRoot()") });
_addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("disputeGameFactory()") }); _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("disputeGameFactory()") });
_addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("portal()") });
_addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("anchorGame()") });
_addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("initialize(address,address,address,(bytes32,uint256))") }); _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("initialize(address,address,address,(bytes32,uint256))") });
_addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("tryUpdateAnchorState()") }); _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("isGameAirgapped(address)") });
_addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("setAnchorState(address)"), _auth: Role.GUARDIAN }); _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("isGameBlacklisted(address)") });
_addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("version()") }); _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("isGameClaimValid(address)") });
_addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("isGameFinalized(address)") });
_addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("isGameProper(address)") });
_addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("isGameRegistered(address)") }); _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("isGameRegistered(address)") });
_addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("isGameResolved(address)") });
_addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("isGameRespected(address)") }); _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("isGameRespected(address)") });
_addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("isGameBlacklisted(address)") });
_addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("isGameRetired(address)") }); _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("isGameRetired(address)") });
_addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("isGameProper(address)") }); _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("portal()") });
_addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("respectedGameType()") });
_addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("setAnchorState(address)") });
_addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("superchainConfig()") }); _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("superchainConfig()") });
_addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("version()") });
// PermissionedDisputeGame // PermissionedDisputeGame
_addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("absolutePrestate()") }); _addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("absolutePrestate()") });
...@@ -576,6 +580,7 @@ contract Specification_Test is CommonTest { ...@@ -576,6 +580,7 @@ contract Specification_Test is CommonTest {
_sel: _getSel("attack(bytes32,uint256,bytes32)"), _sel: _getSel("attack(bytes32,uint256,bytes32)"),
_auth: Role.CHALLENGER _auth: Role.CHALLENGER
}); });
_addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("bondDistributionMode()") });
_addSpec({ _addSpec({
_name: "PermissionedDisputeGame", _name: "PermissionedDisputeGame",
_sel: _getSel("challengeRootL2Block((bytes32,bytes32,bytes32,bytes32),bytes)"), _sel: _getSel("challengeRootL2Block((bytes32,bytes32,bytes32,bytes32),bytes)"),
...@@ -587,6 +592,7 @@ contract Specification_Test is CommonTest { ...@@ -587,6 +592,7 @@ contract Specification_Test is CommonTest {
_addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("claimDataLen()") }); _addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("claimDataLen()") });
_addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("claims(bytes32)") }); _addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("claims(bytes32)") });
_addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("clockExtension()") }); _addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("clockExtension()") });
_addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("closeGame()") });
_addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("createdAt()") }); _addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("createdAt()") });
_addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("credit(address)") }); _addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("credit(address)") });
_addSpec({ _addSpec({
...@@ -601,6 +607,7 @@ contract Specification_Test is CommonTest { ...@@ -601,6 +607,7 @@ contract Specification_Test is CommonTest {
_addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("getChallengerDuration(uint256)") }); _addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("getChallengerDuration(uint256)") });
_addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("getNumToResolve(uint256)") }); _addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("getNumToResolve(uint256)") });
_addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("getRequiredBond(uint128)") }); _addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("getRequiredBond(uint128)") });
_addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("hasUnlockedCredit(address)") });
_addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("initialize()") }); _addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("initialize()") });
_addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("l1Head()") }); _addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("l1Head()") });
_addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("l2BlockNumber()") }); _addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("l2BlockNumber()") });
...@@ -615,6 +622,8 @@ contract Specification_Test is CommonTest { ...@@ -615,6 +622,8 @@ contract Specification_Test is CommonTest {
_auth: Role.CHALLENGER _auth: Role.CHALLENGER
}); });
_addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("proposer()") }); _addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("proposer()") });
_addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("normalModeCredit(address)") });
_addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("refundModeCredit(address)") });
_addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("resolutionCheckpoints(uint256)") }); _addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("resolutionCheckpoints(uint256)") });
_addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("resolve()") }); _addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("resolve()") });
_addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("resolveClaim(uint256,uint256)") }); _addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("resolveClaim(uint256,uint256)") });
...@@ -634,6 +643,7 @@ contract Specification_Test is CommonTest { ...@@ -634,6 +643,7 @@ contract Specification_Test is CommonTest {
_addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("subgames(uint256,uint256)") }); _addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("subgames(uint256,uint256)") });
_addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("version()") }); _addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("version()") });
_addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("vm()") }); _addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("vm()") });
_addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("wasRespectedGameTypeWhenCreated()") });
_addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("weth()") }); _addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("weth()") });
// FaultDisputeGame // FaultDisputeGame
...@@ -641,6 +651,7 @@ contract Specification_Test is CommonTest { ...@@ -641,6 +651,7 @@ contract Specification_Test is CommonTest {
_addSpec({ _name: "FaultDisputeGame", _sel: _getSel("addLocalData(uint256,uint256,uint256)") }); _addSpec({ _name: "FaultDisputeGame", _sel: _getSel("addLocalData(uint256,uint256,uint256)") });
_addSpec({ _name: "FaultDisputeGame", _sel: _getSel("anchorStateRegistry()") }); _addSpec({ _name: "FaultDisputeGame", _sel: _getSel("anchorStateRegistry()") });
_addSpec({ _name: "FaultDisputeGame", _sel: _getSel("attack(bytes32,uint256,bytes32)") }); _addSpec({ _name: "FaultDisputeGame", _sel: _getSel("attack(bytes32,uint256,bytes32)") });
_addSpec({ _name: "FaultDisputeGame", _sel: _getSel("bondDistributionMode()") });
_addSpec({ _addSpec({
_name: "FaultDisputeGame", _name: "FaultDisputeGame",
_sel: _getSel("challengeRootL2Block((bytes32,bytes32,bytes32,bytes32),bytes)") _sel: _getSel("challengeRootL2Block((bytes32,bytes32,bytes32,bytes32),bytes)")
...@@ -650,6 +661,7 @@ contract Specification_Test is CommonTest { ...@@ -650,6 +661,7 @@ contract Specification_Test is CommonTest {
_addSpec({ _name: "FaultDisputeGame", _sel: _getSel("claimDataLen()") }); _addSpec({ _name: "FaultDisputeGame", _sel: _getSel("claimDataLen()") });
_addSpec({ _name: "FaultDisputeGame", _sel: _getSel("claims(bytes32)") }); _addSpec({ _name: "FaultDisputeGame", _sel: _getSel("claims(bytes32)") });
_addSpec({ _name: "FaultDisputeGame", _sel: _getSel("clockExtension()") }); _addSpec({ _name: "FaultDisputeGame", _sel: _getSel("clockExtension()") });
_addSpec({ _name: "FaultDisputeGame", _sel: _getSel("closeGame()") });
_addSpec({ _name: "FaultDisputeGame", _sel: _getSel("createdAt()") }); _addSpec({ _name: "FaultDisputeGame", _sel: _getSel("createdAt()") });
_addSpec({ _name: "FaultDisputeGame", _sel: _getSel("credit(address)") }); _addSpec({ _name: "FaultDisputeGame", _sel: _getSel("credit(address)") });
_addSpec({ _name: "FaultDisputeGame", _sel: _getSel("defend(bytes32,uint256,bytes32)") }); _addSpec({ _name: "FaultDisputeGame", _sel: _getSel("defend(bytes32,uint256,bytes32)") });
...@@ -659,6 +671,7 @@ contract Specification_Test is CommonTest { ...@@ -659,6 +671,7 @@ contract Specification_Test is CommonTest {
_addSpec({ _name: "FaultDisputeGame", _sel: _getSel("gameType()") }); _addSpec({ _name: "FaultDisputeGame", _sel: _getSel("gameType()") });
_addSpec({ _name: "FaultDisputeGame", _sel: _getSel("getChallengerDuration(uint256)") }); _addSpec({ _name: "FaultDisputeGame", _sel: _getSel("getChallengerDuration(uint256)") });
_addSpec({ _name: "FaultDisputeGame", _sel: _getSel("getRequiredBond(uint128)") }); _addSpec({ _name: "FaultDisputeGame", _sel: _getSel("getRequiredBond(uint128)") });
_addSpec({ _name: "FaultDisputeGame", _sel: _getSel("hasUnlockedCredit(address)") });
_addSpec({ _name: "FaultDisputeGame", _sel: _getSel("initialize()") }); _addSpec({ _name: "FaultDisputeGame", _sel: _getSel("initialize()") });
_addSpec({ _name: "FaultDisputeGame", _sel: _getSel("l1Head()") }); _addSpec({ _name: "FaultDisputeGame", _sel: _getSel("l1Head()") });
_addSpec({ _name: "FaultDisputeGame", _sel: _getSel("l2BlockNumber()") }); _addSpec({ _name: "FaultDisputeGame", _sel: _getSel("l2BlockNumber()") });
...@@ -671,6 +684,8 @@ contract Specification_Test is CommonTest { ...@@ -671,6 +684,8 @@ contract Specification_Test is CommonTest {
_addSpec({ _name: "FaultDisputeGame", _sel: _getSel("resolutionCheckpoints(uint256)") }); _addSpec({ _name: "FaultDisputeGame", _sel: _getSel("resolutionCheckpoints(uint256)") });
_addSpec({ _name: "FaultDisputeGame", _sel: _getSel("resolve()") }); _addSpec({ _name: "FaultDisputeGame", _sel: _getSel("resolve()") });
_addSpec({ _name: "FaultDisputeGame", _sel: _getSel("getNumToResolve(uint256)") }); _addSpec({ _name: "FaultDisputeGame", _sel: _getSel("getNumToResolve(uint256)") });
_addSpec({ _name: "FaultDisputeGame", _sel: _getSel("normalModeCredit(address)") });
_addSpec({ _name: "FaultDisputeGame", _sel: _getSel("refundModeCredit(address)") });
_addSpec({ _name: "FaultDisputeGame", _sel: _getSel("resolveClaim(uint256,uint256)") }); _addSpec({ _name: "FaultDisputeGame", _sel: _getSel("resolveClaim(uint256,uint256)") });
_addSpec({ _name: "FaultDisputeGame", _sel: _getSel("resolvedAt()") }); _addSpec({ _name: "FaultDisputeGame", _sel: _getSel("resolvedAt()") });
_addSpec({ _name: "FaultDisputeGame", _sel: _getSel("resolvedSubgames(uint256)") }); _addSpec({ _name: "FaultDisputeGame", _sel: _getSel("resolvedSubgames(uint256)") });
...@@ -684,6 +699,7 @@ contract Specification_Test is CommonTest { ...@@ -684,6 +699,7 @@ contract Specification_Test is CommonTest {
_addSpec({ _name: "FaultDisputeGame", _sel: _getSel("subgames(uint256,uint256)") }); _addSpec({ _name: "FaultDisputeGame", _sel: _getSel("subgames(uint256,uint256)") });
_addSpec({ _name: "FaultDisputeGame", _sel: _getSel("version()") }); _addSpec({ _name: "FaultDisputeGame", _sel: _getSel("version()") });
_addSpec({ _name: "FaultDisputeGame", _sel: _getSel("vm()") }); _addSpec({ _name: "FaultDisputeGame", _sel: _getSel("vm()") });
_addSpec({ _name: "FaultDisputeGame", _sel: _getSel("wasRespectedGameTypeWhenCreated()") });
_addSpec({ _name: "FaultDisputeGame", _sel: _getSel("weth()") }); _addSpec({ _name: "FaultDisputeGame", _sel: _getSel("weth()") });
// DisputeGameFactory // DisputeGameFactory
...@@ -728,6 +744,7 @@ contract Specification_Test is CommonTest { ...@@ -728,6 +744,7 @@ contract Specification_Test is CommonTest {
_addSpec({ _name: "DelayedWETH", _sel: _getSel("delay()") }); _addSpec({ _name: "DelayedWETH", _sel: _getSel("delay()") });
_addSpec({ _name: "DelayedWETH", _sel: _getSel("deposit()") }); _addSpec({ _name: "DelayedWETH", _sel: _getSel("deposit()") });
_addSpec({ _name: "DelayedWETH", _sel: _getSel("hold(address,uint256)"), _auth: Role.DELAYEDWETHOWNER }); _addSpec({ _name: "DelayedWETH", _sel: _getSel("hold(address,uint256)"), _auth: Role.DELAYEDWETHOWNER });
_addSpec({ _name: "DelayedWETH", _sel: _getSel("hold(address)"), _auth: Role.DELAYEDWETHOWNER });
_addSpec({ _name: "DelayedWETH", _sel: _getSel("initialize(address,address)") }); _addSpec({ _name: "DelayedWETH", _sel: _getSel("initialize(address,address)") });
_addSpec({ _name: "DelayedWETH", _sel: _getSel("name()") }); _addSpec({ _name: "DelayedWETH", _sel: _getSel("name()") });
_addSpec({ _name: "DelayedWETH", _sel: _getSel("owner()") }); _addSpec({ _name: "DelayedWETH", _sel: _getSel("owner()") });
...@@ -931,14 +948,14 @@ contract Specification_Test is CommonTest { ...@@ -931,14 +948,14 @@ contract Specification_Test is CommonTest {
/// @notice Ensures that the DeputyGuardian is authorized to take all Guardian actions. /// @notice Ensures that the DeputyGuardian is authorized to take all Guardian actions.
function test_deputyGuardianAuth_works() public view { function test_deputyGuardianAuth_works() public view {
// Additional 2 roles for the DeputyPauseModule. // Additional 2 roles for the DeputyPauseModule
assertEq(specsByRole[Role.GUARDIAN].length, 5); // Additional role for `setAnchorState` which is in DGM but no longer role-restricted.
assertEq(specsByRole[Role.DEPUTYGUARDIAN].length, specsByRole[Role.GUARDIAN].length + 2); assertEq(specsByRole[Role.GUARDIAN].length, 4);
assertEq(specsByRole[Role.DEPUTYGUARDIAN].length, specsByRole[Role.GUARDIAN].length + 3);
mapping(bytes4 => Spec) storage dgmFuncSpecs = specs["DeputyGuardianModule"]; mapping(bytes4 => Spec) storage dgmFuncSpecs = specs["DeputyGuardianModule"];
mapping(bytes4 => Spec) storage superchainConfigFuncSpecs = specs["SuperchainConfig"]; mapping(bytes4 => Spec) storage superchainConfigFuncSpecs = specs["SuperchainConfig"];
mapping(bytes4 => Spec) storage portal2FuncSpecs = specs["OptimismPortal2"]; mapping(bytes4 => Spec) storage portal2FuncSpecs = specs["OptimismPortal2"];
mapping(bytes4 => Spec) storage anchorRegFuncSpecs = specs["AnchorStateRegistry"];
// Ensure that for each of the DeputyGuardianModule's methods there is a corresponding method on another // Ensure that for each of the DeputyGuardianModule's methods there is a corresponding method on another
// system contract authed to the Guardian role. // system contract authed to the Guardian role.
...@@ -955,6 +972,5 @@ contract Specification_Test is CommonTest { ...@@ -955,6 +972,5 @@ contract Specification_Test is CommonTest {
_assertRolesEq(portal2FuncSpecs[_getSel("setRespectedGameType(uint32)")].auth, Role.GUARDIAN); _assertRolesEq(portal2FuncSpecs[_getSel("setRespectedGameType(uint32)")].auth, Role.GUARDIAN);
_assertRolesEq(dgmFuncSpecs[_getSel("setAnchorState(address,address)")].auth, Role.DEPUTYGUARDIAN); _assertRolesEq(dgmFuncSpecs[_getSel("setAnchorState(address,address)")].auth, Role.DEPUTYGUARDIAN);
_assertRolesEq(anchorRegFuncSpecs[_getSel("setAnchorState(address)")].auth, Role.GUARDIAN);
} }
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment