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"
# Foundry dependencies
# 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
forge = "nightly-59f354c179f4e7f6d7292acb3d068815c79286d1"
cast = "nightly-59f354c179f4e7f6d7292acb3d068815c79286d1"
anvil = "nightly-59f354c179f4e7f6d7292acb3d068815c79286d1"
forge = "nightly-017c59d6806ce11f1dc131f8607178efad79d84a"
cast = "nightly-017c59d6806ce11f1dc131f8607178efad79d84a"
anvil = "nightly-017c59d6806ce11f1dc131f8607178efad79d84a"
# Fake dependencies
# 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)
Implementations: OPCMImplementationsConfig{
L1ContractsRelease: "dev",
FaultProof: SuperFaultProofConfig{
WithdrawalDelaySeconds: big.NewInt(604800),
WithdrawalDelaySeconds: big.NewInt(302400),
MinProposalSizeBytes: big.NewInt(10000),
ChallengePeriodSeconds: big.NewInt(120),
ProofMaturityDelaySeconds: big.NewInt(12),
......
......@@ -53,6 +53,7 @@ var (
methodL2BlockNumberChallenged = "l2BlockNumberChallenged"
methodL2BlockNumberChallenger = "l2BlockNumberChallenger"
methodChallengeRootL2Block = "challengeRootL2Block"
methodBondDistributionMode = "bondDistributionMode"
)
var (
......@@ -455,6 +456,14 @@ func (f *FaultDisputeGameContractLatest) GetAllClaims(ctx context.Context, block
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) {
defer f.metrics.StartContractRequest("IsResolved")()
calls := make([]batching.Call, 0, len(claims))
......@@ -639,4 +648,5 @@ type FaultDisputeGameContract interface {
CallResolve(ctx context.Context) (gameTypes.GameStatus, error)
ResolveTx() (txmgr.TxCandidate, error)
Vm(ctx context.Context) (*VMContract, error)
BondDistributionMode(ctx context.Context) (uint8, error)
}
......@@ -15,7 +15,7 @@ const (
GasLimit uint64 = 60_000_000
BasefeeScalar uint32 = 1368
BlobBaseFeeScalar uint32 = 801949
WithdrawalDelaySeconds uint64 = 604800
WithdrawalDelaySeconds uint64 = 302400
MinProposalSizeBytes uint64 = 126000
ChallengePeriodSeconds uint64 = 86400
ProofMaturityDelaySeconds uint64 = 604800
......
......@@ -368,6 +368,18 @@ func (g *OutputGameHelper) Status(ctx context.Context) gameTypes.GameStatus {
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) {
g.T.Logf("Waiting for game %v to have status %v", g.Addr, expected)
timedCtx, cancel := context.WithTimeout(ctx, defaultTimeout)
......
......@@ -115,13 +115,22 @@ func TestOutputAlphabetGame_ReclaimBond(t *testing.T) {
game.WaitForGameStatus(ctx, types.GameStatusChallengerWon)
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
// 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")
// The actor should have no credit available because all its bonds were paid to Alice.
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
sys.TimeTravelClock.AdvanceTime(game.CreditUnlockDuration(ctx))
......
......@@ -13,6 +13,14 @@ snapshots = 'notarealpath' # workaround for foundry#9477
optimizer = true
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']
bytecode_hash = 'none'
ast = true
......@@ -85,6 +93,7 @@ depth = 32
[profile.cicoverage]
optimizer = false
compilation_restrictions = []
[profile.cicoverage.fuzz]
runs = 1
......@@ -112,6 +121,8 @@ timeout = 300
[profile.lite]
optimizer = false
compilation_restrictions = []
################################################################
# PROFILE: KONTROL #
......
......@@ -31,6 +31,7 @@ interface IOptimismPortal2 {
error UnexpectedList();
error UnexpectedString();
error Unproven();
error LegacyGame();
event DisputeGameBlacklisted(IDisputeGame indexed disputeGame);
event Initialized(uint8 version);
......
......@@ -33,6 +33,7 @@ interface IOptimismPortalInterop {
error UnexpectedList();
error UnexpectedString();
error Unproven();
error LegacyGame();
event DisputeGameBlacklisted(IDisputeGame indexed disputeGame);
event Initialized(uint8 version);
......
......@@ -10,8 +10,8 @@ import { GameType, Hash, OutputRoot } from "src/dispute/lib/Types.sol";
interface IAnchorStateRegistry {
error AnchorStateRegistry_Unauthorized();
error AnchorStateRegistry_ImproperAnchorGame();
error AnchorStateRegistry_InvalidAnchorGame();
error AnchorStateRegistry_AnchorGameBlacklisted();
event AnchorNotUpdated(IFaultDisputeGame indexed game);
event AnchorUpdated(IFaultDisputeGame indexed game);
......@@ -21,16 +21,27 @@ interface IAnchorStateRegistry {
function anchors(GameType) external view returns (Hash, uint256);
function getAnchorRoot() external view returns (Hash, uint256);
function disputeGameFactory() external view returns (IDisputeGameFactory);
function initialize(ISuperchainConfig _superchainConfig, IDisputeGameFactory _disputeGameFactory, IOptimismPortal2 _portal, OutputRoot memory _startingAnchorRoot) external;
function isGameRegistered(IDisputeGame _game) external view returns (bool);
function initialize(
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 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 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 setAnchorState(IFaultDisputeGame _game) external;
function respectedGameType() external view returns (GameType);
function setAnchorState(IDisputeGame _game) external;
function superchainConfig() external view returns (ISuperchainConfig);
function tryUpdateAnchorState() external;
function version() external view returns (string memory);
function __constructor__() external;
......
......@@ -11,13 +11,13 @@ interface IDelayedWETH {
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
event Initialized(uint8 version);
event Unwrap(address indexed src, uint256 wad);
fallback() external payable;
receive() external payable;
function config() external view returns (ISuperchainConfig);
function delay() external view returns (uint256);
function hold(address _guy) external;
function hold(address _guy, uint256 _wad) external;
function initialize(address _owner, ISuperchainConfig _config) external;
function owner() external view returns (address);
......
......@@ -14,7 +14,9 @@ interface IDisputeGame is IInitializable {
function gameCreator() external pure returns (address creator_);
function rootClaim() external pure returns (Claim rootClaim_);
function l1Head() external pure returns (Hash l1Head_);
function l2BlockNumber() external pure returns (uint256 l2BlockNumber_);
function extraData() external pure returns (bytes memory extraData_);
function resolve() external returns (GameStatus status_);
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";
import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol";
import { IBigStepper } from "interfaces/dispute/IBigStepper.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 {
struct ClaimData {
......@@ -74,13 +74,19 @@ interface IFaultDisputeGame is IDisputeGame {
error UnexpectedRootClaim(Claim rootClaim);
error UnexpectedString();
error ValidStep();
error InvalidBondDistributionMode();
error GameNotFinalized();
error GameNotResolved();
error ReservedGameType();
event Move(uint256 indexed parentIndex, Claim indexed claim, address indexed claimant);
event GameClosed(BondDistributionMode bondDistributionMode);
function absolutePrestate() external view returns (Claim absolutePrestate_);
function addLocalData(uint256 _ident, uint256 _execLeafIdx, uint256 _partOffset) external;
function anchorStateRegistry() external view returns (IAnchorStateRegistry registry_);
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 claimCredit(address _recipient) external;
function claimData(uint256)
......@@ -98,11 +104,13 @@ interface IFaultDisputeGame is IDisputeGame {
function claimDataLen() external view returns (uint256 len_);
function claims(Hash) external view returns (bool);
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 getChallengerDuration(uint256 _claimIndex) external view returns (Duration duration_);
function getNumToResolve(uint256 _claimIndex) external view returns (uint256 numRemainingChildren_);
function getRequiredBond(Position _position) external view returns (uint256 requiredBond_);
function hasUnlockedCredit(address) external view returns (bool);
function l2BlockNumber() external pure returns (uint256 l2BlockNumber_);
function l2BlockNumberChallenged() external view returns (bool);
function l2BlockNumberChallenger() external view returns (address);
......@@ -110,6 +118,8 @@ interface IFaultDisputeGame is IDisputeGame {
function maxClockDuration() external view returns (Duration maxClockDuration_);
function maxGameDepth() external view returns (uint256 maxGameDepth_);
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)
external
view
......@@ -124,6 +134,7 @@ interface IFaultDisputeGame is IDisputeGame {
function subgames(uint256, uint256) external view returns (uint256);
function version() external view returns (string memory);
function vm() external view returns (IBigStepper vm_);
function wasRespectedGameTypeWhenCreated() external view returns (bool);
function weth() external view returns (IDelayedWETH weth_);
function __constructor__(GameConstructorParams memory _params) external;
......
......@@ -2,7 +2,7 @@
pragma solidity ^0.8.0;
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 { IDelayedWETH } from "interfaces/dispute/IDelayedWETH.sol";
......@@ -63,13 +63,19 @@ interface IPermissionedDisputeGame is IDisputeGame {
error UnexpectedRootClaim(Claim rootClaim);
error UnexpectedString();
error ValidStep();
error InvalidBondDistributionMode();
error GameNotFinalized();
error GameNotResolved();
error ReservedGameType();
event Move(uint256 indexed parentIndex, Claim indexed claim, address indexed claimant);
event GameClosed(BondDistributionMode bondDistributionMode);
function absolutePrestate() external view returns (Claim absolutePrestate_);
function addLocalData(uint256 _ident, uint256 _execLeafIdx, uint256 _partOffset) external;
function anchorStateRegistry() external view returns (IAnchorStateRegistry registry_);
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 claimCredit(address _recipient) external;
function claimData(uint256)
......@@ -87,11 +93,14 @@ interface IPermissionedDisputeGame is IDisputeGame {
function claimDataLen() external view returns (uint256 len_);
function claims(Hash) external view returns (bool);
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 getChallengerDuration(uint256 _claimIndex) external view returns (Duration duration_);
function getNumToResolve(uint256 _claimIndex) external view returns (uint256 numRemainingChildren_);
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 l2BlockNumberChallenged() external view returns (bool);
function l2BlockNumberChallenger() external view returns (address);
......@@ -99,6 +108,8 @@ interface IPermissionedDisputeGame is IDisputeGame {
function maxClockDuration() external view returns (Duration maxClockDuration_);
function maxGameDepth() external view returns (uint256 maxGameDepth_);
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)
external
view
......@@ -113,6 +124,7 @@ interface IPermissionedDisputeGame is IDisputeGame {
function subgames(uint256, uint256) external view returns (uint256);
function version() external view returns (string memory);
function vm() external view returns (IBigStepper vm_);
function wasRespectedGameTypeWhenCreated() external view returns (bool);
function weth() external view returns (IDelayedWETH weth_);
error BadAuth();
......
......@@ -73,7 +73,7 @@ test-upgrade *ARGS: build-go-ffi
#!/bin/bash
echo "Running upgrade tests at block $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"
FORK_RPC_URL=$ETH_RPC_URL \
FORK_TEST=true \
......
......@@ -112,6 +112,25 @@
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "contract IDisputeGame",
"name": "_game",
"type": "address"
}
],
"name": "isGameAirgapped",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
......@@ -131,6 +150,44 @@
"stateMutability": "view",
"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": [
{
......@@ -169,6 +226,25 @@
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "contract IDisputeGame",
"name": "_game",
"type": "address"
}
],
"name": "isGameResolved",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
......@@ -220,10 +296,23 @@
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "respectedGameType",
"outputs": [
{
"internalType": "GameType",
"name": "",
"type": "uint32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "contract IFaultDisputeGame",
"internalType": "contract IDisputeGame",
"name": "_game",
"type": "address"
}
......@@ -246,13 +335,6 @@
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "tryUpdateAnchorState",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "version",
......@@ -307,7 +389,7 @@
},
{
"inputs": [],
"name": "AnchorStateRegistry_ImproperAnchorGame",
"name": "AnchorStateRegistry_AnchorGameBlacklisted",
"type": "error"
},
{
......
......@@ -149,6 +149,19 @@
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_guy",
"type": "address"
}
],
"name": "hold",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
......@@ -497,25 +510,6 @@
"name": "Transfer",
"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,
"inputs": [
......
......@@ -134,6 +134,19 @@
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "bondDistributionMode",
"outputs": [
{
"internalType": "enum BondDistributionMode",
"name": "",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
......@@ -281,6 +294,13 @@
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "closeGame",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "createdAt",
......@@ -298,7 +318,7 @@
"inputs": [
{
"internalType": "address",
"name": "",
"name": "_recipient",
"type": "address"
}
],
......@@ -306,7 +326,7 @@
"outputs": [
{
"internalType": "uint256",
"name": "",
"name": "credit_",
"type": "uint256"
}
],
......@@ -455,6 +475,25 @@
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "hasUnlockedCredit",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "initialize",
......@@ -581,6 +620,44 @@
"stateMutability": "payable",
"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": [
{
......@@ -839,6 +916,19 @@
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "wasRespectedGameTypeWhenCreated",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "weth",
......@@ -852,6 +942,19 @@
"stateMutability": "view",
"type": "function"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "enum BondDistributionMode",
"name": "bondDistributionMode",
"type": "uint8"
}
],
"name": "GameClosed",
"type": "event"
},
{
"anonymous": false,
"inputs": [
......@@ -960,16 +1063,31 @@
"name": "GameDepthExceeded",
"type": "error"
},
{
"inputs": [],
"name": "GameNotFinalized",
"type": "error"
},
{
"inputs": [],
"name": "GameNotInProgress",
"type": "error"
},
{
"inputs": [],
"name": "GameNotResolved",
"type": "error"
},
{
"inputs": [],
"name": "IncorrectBondAmount",
"type": "error"
},
{
"inputs": [],
"name": "InvalidBondDistributionMode",
"type": "error"
},
{
"inputs": [],
"name": "InvalidChallengePeriod",
......@@ -1045,6 +1163,11 @@
"name": "OutOfOrderResolution",
"type": "error"
},
{
"inputs": [],
"name": "ReservedGameType",
"type": "error"
},
{
"inputs": [],
"name": "UnexpectedList",
......
......@@ -812,6 +812,11 @@
"name": "LargeCalldata",
"type": "error"
},
{
"inputs": [],
"name": "LegacyGame",
"type": "error"
},
{
"inputs": [],
"name": "NonReentrant",
......
......@@ -835,6 +835,11 @@
"name": "LargeCalldata",
"type": "error"
},
{
"inputs": [],
"name": "LegacyGame",
"type": "error"
},
{
"inputs": [],
"name": "NonReentrant",
......
......@@ -144,6 +144,19 @@
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "bondDistributionMode",
"outputs": [
{
"internalType": "enum BondDistributionMode",
"name": "",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
......@@ -304,6 +317,13 @@
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "closeGame",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "createdAt",
......@@ -321,7 +341,7 @@
"inputs": [
{
"internalType": "address",
"name": "",
"name": "_recipient",
"type": "address"
}
],
......@@ -329,7 +349,7 @@
"outputs": [
{
"internalType": "uint256",
"name": "",
"name": "credit_",
"type": "uint256"
}
],
......@@ -478,6 +498,25 @@
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "hasUnlockedCredit",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "initialize",
......@@ -604,6 +643,25 @@
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "normalModeCredit",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "proposer",
......@@ -617,6 +675,25 @@
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "refundModeCredit",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
......@@ -875,6 +952,19 @@
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "wasRespectedGameTypeWhenCreated",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "weth",
......@@ -888,6 +978,19 @@
"stateMutability": "view",
"type": "function"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "enum BondDistributionMode",
"name": "bondDistributionMode",
"type": "uint8"
}
],
"name": "GameClosed",
"type": "event"
},
{
"anonymous": false,
"inputs": [
......@@ -1001,16 +1104,31 @@
"name": "GameDepthExceeded",
"type": "error"
},
{
"inputs": [],
"name": "GameNotFinalized",
"type": "error"
},
{
"inputs": [],
"name": "GameNotInProgress",
"type": "error"
},
{
"inputs": [],
"name": "GameNotResolved",
"type": "error"
},
{
"inputs": [],
"name": "IncorrectBondAmount",
"type": "error"
},
{
"inputs": [],
"name": "InvalidBondDistributionMode",
"type": "error"
},
{
"inputs": [],
"name": "InvalidChallengePeriod",
......@@ -1086,6 +1204,11 @@
"name": "OutOfOrderResolution",
"type": "error"
},
{
"inputs": [],
"name": "ReservedGameType",
"type": "error"
},
{
"inputs": [],
"name": "UnexpectedList",
......
......@@ -20,12 +20,12 @@
"sourceCodeHash": "0xdfd5c91e5ddbbf2ad82b867cbf7403437decd6ca70b87891eec935665f17ffd5"
},
"src/L1/OptimismPortal2.sol": {
"initCodeHash": "0x2121a97875875150106a54a71c6c4c03afe90b3364e416be047f55fdeab57204",
"sourceCodeHash": "0x96e3de3ef0025a6def702eeb481acd2d2d88971fd418be657472f51a98029773"
"initCodeHash": "0x969e3687d4497cc168af61e610ba0ae187e80f86aaa7b5d5bb598de19f279f08",
"sourceCodeHash": "0xf215a31954f2ef166cfb26d20e466c62fafa235a08fc42c55131dcb81998ff01"
},
"src/L1/OptimismPortalInterop.sol": {
"initCodeHash": "0x09ffe45f91bf59315b9fd4a2941b819ed8b1bb0d8643a630c6193bd67acea0ed",
"sourceCodeHash": "0xbb6acc3e88af9594ffcb8a2f30860511b76e09024330e70052316668fe55fd1f"
"initCodeHash": "0x057c56174304f3773654fed39abf5fab70d9446f531d07fdb225b738a680ad46",
"sourceCodeHash": "0xc04a7f9c14a13ec3587f5cc351c8e9f27fbbe9f1291a1aba07de29edbeef418a"
},
"src/L1/ProtocolVersions.sol": {
"initCodeHash": "0x0000ec89712d8b4609873f1ba76afffd4205bf9110818995c90134dbec12e91e",
......@@ -152,20 +152,20 @@
"sourceCodeHash": "0xb7b0a06cd971c4647247dc19ce997d0c64a73e87c81d30731da9cf9efa1b952a"
},
"src/dispute/AnchorStateRegistry.sol": {
"initCodeHash": "0xfbeeac40d86d13e71c7add66eef6357576a93b6a175c9cff6ec6ef587fe3acc4",
"sourceCodeHash": "0xbb2e08da74d470fc30dd35dc39834e19f676a45974aa2403eb97e84bc5bed0a8"
"initCodeHash": "0xb2618d650808a7a335db7cc56d15ccaf432f50aa551c01be8bde8356893c0e0d",
"sourceCodeHash": "0x745f0e2b07b8f6492e11ca2f69b53d129177fbfd346d5ca4729d72792aff1f83"
},
"src/dispute/DelayedWETH.sol": {
"initCodeHash": "0x759d7f9c52b7c13ce4502f39dae3a75d130c6278240cde0b60ae84616aa2bd48",
"sourceCodeHash": "0x4406c78e0557bedb88b4ee5977acb1ef13e7bd92b7dbf79f56f8bad95c53e229"
"initCodeHash": "0xb1f04c9ee86984a157b92a18754c84104e9d4df7a3838633301ca7f557d0220a",
"sourceCodeHash": "0x0162302b9c71f184d45bee34ecfb1dfbf427f38fc5652709ab7ffef1ac816d82"
},
"src/dispute/DisputeGameFactory.sol": {
"initCodeHash": "0xa728192115c5fdb08c633a0899043318289b1d413d7afeed06356008b2a5a7fa",
"sourceCodeHash": "0x155c0334f63616ed245aadf9a94f419ef7d5e2237b3b32172484fd19890a61dc"
},
"src/dispute/FaultDisputeGame.sol": {
"initCodeHash": "0x423e8488731c0b0f87b435174f412c09fbf0b17eb0b8c9a03efa37d779ec0cae",
"sourceCodeHash": "0xe53b970922b309ada1c59f94d5935ffca669e909c797f17ba8a3d309c487e7e8"
"initCodeHash": "0x152fbb1f82488d815f56087fc464b9478f1390e3ecd67ae595344115fdd9ba91",
"sourceCodeHash": "0x9bfea41bd993bc1ef2ede9a5846a432ed5ea183868634fd77c4068b0a4a779b2"
},
"src/legacy/DeployerWhitelist.sol": {
"initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29",
......
......@@ -50,7 +50,7 @@
},
{
"bytes": "32",
"label": "credit",
"label": "normalModeCredit",
"offset": 0,
"slot": "3",
"type": "mapping(address => uint256)"
......@@ -89,5 +89,33 @@
"offset": 0,
"slot": "8",
"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 @@
},
{
"bytes": "32",
"label": "credit",
"label": "normalModeCredit",
"offset": 0,
"slot": "3",
"type": "mapping(address => uint256)"
......@@ -89,5 +89,33 @@
"offset": 0,
"slot": "8",
"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 {
Blacklisted,
Unproven,
ProposalNotValidated,
AlreadyFinalized
AlreadyFinalized,
LegacyGame
} from "src/libraries/PortalErrors.sol";
import { GameStatus, GameType, Claim, Timestamp } from "src/dispute/lib/Types.sol";
......@@ -176,9 +177,9 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver {
}
/// @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) {
return "3.11.0-beta.11";
return "3.12.0-beta.1";
}
/// @notice Constructs the OptimismPortal contract.
......@@ -308,6 +309,24 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver {
// The game type of the dispute game must be the respected game type.
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.
if (outputRoot.raw() != Hashing.hashOutputRootProof(_outputRootProof)) revert InvalidProof();
......@@ -476,9 +495,16 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver {
/// @param _gameType The game type to consult for output proposals.
function setRespectedGameType(GameType _gameType) external {
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);
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
......@@ -497,6 +523,7 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver {
// a timestamp of zero.
if (provenWithdrawal.timestamp == 0) revert Unproven();
// Grab the createdAt timestamp once.
uint64 createdAt = disputeGameProxy.createdAt().raw();
// 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 {
// from finalizing withdrawals proven against non-finalized output roots.
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
// `proveWithdrawalTransaction`, but we check it again in case the respected game type has changed since
// the withdrawal was proven.
if (disputeGameProxy.gameType().raw() != respectedGameType.raw()) revert InvalidGameType();
// The game type of the dispute game must have been the respected game type at creation
// time. We check that the game type is the respected game type at proving time, but it's
// possible that the respected game type has since changed. Users can still use this game
// 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
// invalid disputes against a deployed game type while the off-chain challenge agents are not watching.
// 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(
createdAt >= respectedGameTypeUpdatedAt,
createdAt > respectedGameTypeUpdatedAt,
"OptimismPortal: dispute game created before respected game type was updated"
);
......
......@@ -28,9 +28,9 @@ contract OptimismPortalInterop is OptimismPortal2 {
OptimismPortal2(_proofMaturityDelaySeconds, _disputeGameFinalityDelaySeconds)
{ }
/// @custom:semver +interop-beta.8
/// @custom:semver +interop-beta.9
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.
......
......@@ -26,14 +26,9 @@ contract DelayedWETH is OwnableUpgradeable, WETH98, ISemver {
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.
/// @custom:semver 1.2.0-beta.5
string public constant version = "1.2.0-beta.5";
/// @custom:semver 1.3.0-beta.1
string public constant version = "1.3.0-beta.1";
/// @notice Returns a withdrawal request for the given address.
mapping(address => mapping(address => WithdrawalRequest)) public withdrawals;
......@@ -107,12 +102,19 @@ contract DelayedWETH is OwnableUpgradeable, WETH98, ISemver {
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 _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");
_allowance[_guy][msg.sender] = _wad;
emit Approval(_guy, msg.sender, _wad);
transferFrom(_guy, msg.sender, _wad);
}
}
......@@ -121,6 +121,18 @@ error BlockNumberMatches();
/// @notice Thrown when the L2 block number claim has already been challenged.
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 //
////////////////////////////////////////////////////////////////
......
......@@ -26,6 +26,17 @@ enum GameStatus {
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.
/// @custom:field root The output root.
/// @custom:field l2BlockNumber The L2 block number at which the output root was generated.
......
......@@ -38,3 +38,5 @@ error Unproven();
error ProposalNotValidated();
/// @notice Error for when a withdrawal has already been finalized.
error AlreadyFinalized();
/// @notice Error for when a game is a legacy game.
error LegacyGame();
......@@ -352,26 +352,48 @@ contract DelayedWETH_Recover_Test is DelayedWETH_Init {
contract DelayedWETH_Hold_Test is DelayedWETH_Init {
/// @dev Tests that holding WETH succeeds.
function test_hold_succeeds() public {
function test_hold_byOwner_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, amount);
// Verify the allowance.
assertEq(delayedWeth.allowance(alice, address(this)), amount);
// Get our balance after.
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.
delayedWeth.transferFrom(alice, address(this), amount);
// Get our balance after.
uint256 finalBalance = delayedWeth.balanceOf(address(this));
// Verify the transfer.
assertEq(delayedWeth.balanceOf(address(this)), amount);
assertEq(finalBalance, initialBalance + amount);
}
/// @dev Tests that holding WETH by non-owner fails.
......
......@@ -45,6 +45,16 @@ contract FaultDisputeGame_Solvency_Invariant is FaultDisputeGame_Init {
}
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.
vm.warp(block.timestamp + 7 days + 1 seconds);
......
......@@ -118,6 +118,9 @@ contract OptimismPortal2_Invariant_Harness is CommonTest {
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.
_proposedBlockNumber = 0xFF;
IFaultDisputeGame game = IFaultDisputeGame(
......
......@@ -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
/// guardian.
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));
emit ExecutionFromModuleSuccess(address(deputyGuardianModule));
......
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