diff --git a/.circleci/config.yml b/.circleci/config.yml index 6ae6249ad25..ded47d01b96 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1041,9 +1041,10 @@ jobs: name: Run heavy fuzz tests command: | mkdir -p results - just test --junit > results/results.xml + just test environment: FOUNDRY_PROFILE: ciheavy + JUNIT_TEST_PATH: results/results.xml working_directory: packages/contracts-bedrock no_output_timeout: 90m - run: @@ -1248,8 +1249,9 @@ jobs: name: Run tests command: | mkdir -p results - just test-upgrade --junit > results/results.xml + just test-upgrade environment: + JUNIT_TEST_PATH: results/results.xml FOUNDRY_FUZZ_SEED: 42424242 FOUNDRY_FUZZ_RUNS: 1 FOUNDRY_PROFILE: ci @@ -1371,7 +1373,7 @@ jobs: command: forge --version - run: name: Pull cached artifacts - command: bash scripts/ops/pull-artifacts.sh --fallback-to-latest + command: bash scripts/ops/pull-artifacts.sh working_directory: packages/contracts-bedrock - run: name: Run checks @@ -2778,10 +2780,9 @@ workflows: parameters: features: &features_matrix - main + - CUSTOM_GAS_TOKEN - OPTIMISM_PORTAL_INTEROP - - CANNON_KONA,DEPLOY_V2_DISPUTE_GAMES - OPCM_V2 - - CUSTOM_GAS_TOKEN - OPCM_V2,CUSTOM_GAS_TOKEN - OPCM_V2,OPTIMISM_PORTAL_INTEROP context: diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 15c6b3be40d..6c5d44c4384 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -13,6 +13,9 @@ /op-devstack @ethereum-optimism/op-stack @ethereum-optimism/go-reviewers # Expert areas +/op-deployer @ethereum-optimism/platforms-team +/op-validator @ethereum-optimism/platforms-team + /op-node/rollup @ethereum-optimism/consensus @ethereum-optimism/go-reviewers /op-supervisor @ethereum-optimism/interop @ethereum-optimism/go-reviewers diff --git a/.semgrep/rules/sol-rules.yaml b/.semgrep/rules/sol-rules.yaml index ed1bb708a3d..f260b778a02 100644 --- a/.semgrep/rules/sol-rules.yaml +++ b/.semgrep/rules/sol-rules.yaml @@ -398,3 +398,14 @@ rules: paths: include: - packages/contracts-bedrock/src/L1/opcm/OPContractsManagerV2.sol + + - id: sol-style-ban-forge-std-test-import + languages: [solidity] + severity: ERROR + message: Import Test from test/setup/Test.sol, not forge-std/Test.sol. Import other forge-std components (stdStorage, StdStorage, stdError, StdUtils, Vm, console2, etc.) from their specific files (forge-std/StdStorage.sol, forge-std/StdError.sol, forge-std/StdUtils.sol, forge-std/Vm.sol, forge-std/console2.sol, etc.) + pattern-regex: import\s+(\{[^}]*\}\s+from\s+)?"forge-std/Test\.sol"\s*; + paths: + include: + - packages/contracts-bedrock/test + exclude: + - packages/contracts-bedrock/test/setup/Test.sol diff --git a/.semgrep/tests/sol-rules.sol-style-ban-forge-std-test-import.t.sol b/.semgrep/tests/sol-rules.sol-style-ban-forge-std-test-import.t.sol new file mode 100644 index 00000000000..58f6f19357e --- /dev/null +++ b/.semgrep/tests/sol-rules.sol-style-ban-forge-std-test-import.t.sol @@ -0,0 +1,20 @@ +// ruleid: sol-style-ban-forge-std-test-import +import { Test } from "forge-std/Test.sol"; + +// ruleid: sol-style-ban-forge-std-test-import +import { Test as ForgeTest } from "forge-std/Test.sol"; + +// ruleid: sol-style-ban-forge-std-test-import +import { Test, Vm } from "forge-std/Test.sol"; + +// ok: sol-style-ban-forge-std-test-import +import { Test } from "test/setup/Test.sol"; + +// ok: sol-style-ban-forge-std-test-import +import { Test as BaseTest } from "test/setup/Test.sol"; + +// ok: sol-style-ban-forge-std-test-import +import { Vm } from "forge-std/Vm.sol"; + +// ok: sol-style-ban-forge-std-test-import +import { StdUtils } from "forge-std/StdUtils.sol"; diff --git a/go.mod b/go.mod index 791784f4b85..79a8ad9fcc5 100644 --- a/go.mod +++ b/go.mod @@ -310,7 +310,7 @@ require ( lukechampine.com/blake3 v1.3.0 // indirect ) -replace github.com/ethereum/go-ethereum => github.com/ethereum-optimism/op-geth v1.101604.0-synctest.0.0.20251208094937-ba6bdcfef423 +replace github.com/ethereum/go-ethereum => github.com/ethereum-optimism/op-geth v1.101603.6-rc.1 // replace github.com/ethereum/go-ethereum => ../op-geth diff --git a/go.sum b/go.sum index d4297507e1c..746881d6621 100644 --- a/go.sum +++ b/go.sum @@ -240,8 +240,8 @@ github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= github.com/ethereum-optimism/go-ethereum-hdwallet v0.1.4-0.20251001155152-4eb15ccedf7e h1:iy1vBIzACYUyOVyoADUwvAiq2eOPC0yVsDUdolPwQjk= github.com/ethereum-optimism/go-ethereum-hdwallet v0.1.4-0.20251001155152-4eb15ccedf7e/go.mod h1:DYj7+vYJ4cIB7zera9mv4LcAynCL5u4YVfoeUu6Wa+w= -github.com/ethereum-optimism/op-geth v1.101604.0-synctest.0.0.20251208094937-ba6bdcfef423 h1:5xVkCCBRWkOt+bzVWL1p3mOwrpZLjxi/+yWUsja0E48= -github.com/ethereum-optimism/op-geth v1.101604.0-synctest.0.0.20251208094937-ba6bdcfef423/go.mod h1:fCNAwDynfAP6EKsmLqwSDUDgi+GtJIir74Ui3fXXMps= +github.com/ethereum-optimism/op-geth v1.101603.6-rc.1 h1:C4MAM29WbeXeNELMLpX1xU6c6OIwBRNposAcl1NHvVk= +github.com/ethereum-optimism/op-geth v1.101603.6-rc.1/go.mod h1:fCNAwDynfAP6EKsmLqwSDUDgi+GtJIir74Ui3fXXMps= github.com/ethereum-optimism/superchain-registry/validation v0.0.0-20251121143344-5ac16e0fbb00 h1:TR5Y7B+5m63V0Dno7MHcFqv/XZByQzx/4THV1T1A7+U= github.com/ethereum-optimism/superchain-registry/validation v0.0.0-20251121143344-5ac16e0fbb00/go.mod h1:NZ816PzLU1TLv1RdAvYAb6KWOj4Zm5aInT0YpDVml2Y= github.com/ethereum/c-kzg-4844/v2 v2.1.5 h1:aVtoLK5xwJ6c5RiqO8g8ptJ5KU+2Hdquf6G3aXiHh5s= diff --git a/justfile b/justfile index ccc0c0e8d0a..aac07f68f8b 100644 --- a/justfile +++ b/justfile @@ -45,4 +45,3 @@ update-op-geth ref: go mod edit -replace=github.com/ethereum/go-ethereum=github.com/ethereum-optimism/op-geth@"$ver"; \ go mod tidy; \ echo "Updated op-geth to $ver" - diff --git a/op-acceptance-tests/tests/interop/proofs/challenger_test.go b/op-acceptance-tests/tests/interop/proofs/challenger_test.go index 4d3bf8b21e8..83ee70bd888 100644 --- a/op-acceptance-tests/tests/interop/proofs/challenger_test.go +++ b/op-acceptance-tests/tests/interop/proofs/challenger_test.go @@ -25,8 +25,7 @@ func TestChallengerPlaysGame(gt *testing.T) { badClaim := common.HexToHash("0xdeadbeef00000000000000000000000000000000000000000000000000000000") attacker := sys.FunderL1.NewFundedEOA(eth.Ether(15)) dgf := sys.DisputeGameFactory() - - game := dgf.StartSuperCannonGame(attacker, proofs.WithRootClaim(badClaim)) + game := dgf.StartSuperCannonGame(attacker, proofs.WithSuperRootFrom(eth.Bytes32(badClaim), eth.Bytes32(badClaim))) claim := game.RootClaim() // This is the bad claim from attacker counterClaim := claim.WaitForCounterClaim() // This is the counter-claim from the challenger diff --git a/op-acceptance-tests/tests/sequencer/init_test.go b/op-acceptance-tests/tests/sequencer/init_test.go new file mode 100644 index 00000000000..ed9fb2d9555 --- /dev/null +++ b/op-acceptance-tests/tests/sequencer/init_test.go @@ -0,0 +1,17 @@ +package sequencer + +import ( + "log/slog" + "testing" + + "github.com/ethereum-optimism/optimism/op-devstack/compat" + "github.com/ethereum-optimism/optimism/op-devstack/presets" +) + +// TestMain creates the test-setups against the shared backend +func TestMain(m *testing.M) { + presets.DoMain(m, presets.WithMinimal(), + presets.WithCompatibleTypes(compat.SysGo), + presets.WithLogLevel(slog.LevelDebug), + ) +} diff --git a/op-acceptance-tests/tests/sequencer/recover_mode_test.go b/op-acceptance-tests/tests/sequencer/recover_mode_test.go new file mode 100644 index 00000000000..a0f3382e181 --- /dev/null +++ b/op-acceptance-tests/tests/sequencer/recover_mode_test.go @@ -0,0 +1,45 @@ +package sequencer + +import ( + "testing" + "time" + + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/presets" + "github.com/stretchr/testify/require" +) + +// TestRecoverModeWhenChainHealthy checks that the chain +// can progress as normal when recover mode is activated. +// Recover mode is designed to recover from a sequencing +// window expiry when there are ample L1 blocks to eagerly +// progress the l1 origin to. But when the l1 origin is +// close to the tip of the l1 chain, the eagerness would cause +// a delay in unsafe block production while the sequencer waits +// for the next l1 origin to become available. Recover mode +// has since been patched, and the sequencer will not demand the +// next l1 origin until it is actually available. This tests +// protects against a regeression in that behavior. +func TestRecoverModeWhenChainHealthy(gt *testing.T) { + t := devtest.ParallelT(gt) + sys := presets.NewMinimal(t) + tracer := t.Tracer() + ctx := t.Ctx() + + err := sys.L2CL.SetSequencerRecoverMode(true) + require.NoError(t, err) + blockTime := sys.L2Chain.Escape().RollupConfig().BlockTime + numL2Blocks := uint64(20) + waitTime := time.Duration(blockTime*numL2Blocks+5) * time.Second + + num := sys.L2CL.SyncStatus().UnsafeL2.Number + new_num := num + require.Eventually(t, func() bool { + ctx, span := tracer.Start(ctx, "check head") + defer span.End() + + new_num, num = sys.L2CL.SyncStatus().UnsafeL2.Number, new_num + t.Logger().InfoContext(ctx, "unsafe head", "number", new_num, "safe head", sys.L2CL.SyncStatus().SafeL2.Number) + return new_num >= numL2Blocks + }, waitTime, time.Duration(blockTime)*time.Second) +} diff --git a/op-chain-ops/cmd/check-output-root/README.md b/op-chain-ops/cmd/check-output-root/README.md new file mode 100644 index 00000000000..2e5dc983a5c --- /dev/null +++ b/op-chain-ops/cmd/check-output-root/README.md @@ -0,0 +1,30 @@ +# Overview + +Generates an output root for a given block using only the execution client RPC endpoint. + +## Prerequisites: + +1. git clone or pull the latest develop branch of the optimism repo +2. Go Installed + - You can follow the instructions in the [CONTRIBUTING.md](http://CONTRIBUTING.md) to install all software dependencies of the repo +3. RPC URL for the **L2** chain execution client you want to generate a output root for. + - **Important**: The RPC endpoint must be trusted as it provide the chain state used to compute the output root. + +## Usage: + +```bash +go run op-chain-ops/cmd/check-output-root/ --l2-eth-rpc $RPC_URL --block-num $BLOCK_NUM +``` + +Output: + +```text +0xfefc68b1c0aa7f6e744a8c74084142cf3daa8692179fd5b9ff46c6eacdffe9aa +``` + +## Environment Variables + +Alternatively, you can use environment variables to configure the script: + +- `CHECK_OUTPUT_ROOT_L2_ETH_RPC`: L2 execution client RPC endpoint. +- `CHECK_OUTPUT_ROOT_BLOCK_NUM`: Block number to calculate the output root for. diff --git a/op-chain-ops/cmd/check-output-root/main.go b/op-chain-ops/cmd/check-output-root/main.go new file mode 100644 index 00000000000..6b34bcaaa02 --- /dev/null +++ b/op-chain-ops/cmd/check-output-root/main.go @@ -0,0 +1,91 @@ +package main + +import ( + "context" + "fmt" + "math/big" + "os" + + "github.com/ethereum-optimism/optimism/op-service/ctxinterrupt" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/log" + "github.com/urfave/cli/v2" + + oplog "github.com/ethereum-optimism/optimism/op-service/log" +) + +const ( + // L2EthRpcFlagname defines the flag name for the l2 eth RPC endpoint. + L2EthRpcFlagName = "l2-eth-rpc" + // BlockNumberFlagName defines the flag name for the L2 block number. + BlockNumberFlagName = "block-num" +) + +// Flags contains the list of configuration options available to the binary. +var Flags = []cli.Flag{ + &cli.StringFlag{ + Name: L2EthRpcFlagName, + Usage: "Required: L2 execution client RPC endpoint (e.g., http://host:port).", + Required: true, + EnvVars: []string{"CHECK_OUTPUT_ROOT_L2_ETH_RPC"}, + }, + &cli.Uint64Flag{ + Name: BlockNumberFlagName, + Usage: "Required: L2 block number to calculate the output root for.", + EnvVars: []string{"CHECK_OUTPUT_ROOT_BLOCK_NUM"}, + }, +} + +func main() { + oplog.SetupDefaults() + + app := cli.NewApp() + app.Name = "check-output-root" + app.Usage = "Calculates a output root from an L2 EL endpoint." + // Combine specific flags with log flags + app.Flags = append(Flags, oplog.CLIFlags("CHECK_OUTPUT_ROOT")...) + + app.Action = func(c *cli.Context) error { + ctx := ctxinterrupt.WithCancelOnInterrupt(c.Context) + rpcUrl := c.String(L2EthRpcFlagName) + blockNum := c.Uint64(BlockNumberFlagName) + root, err := CalculateOutputRoot(ctx, rpcUrl, blockNum) + if err != nil { + return err + } + fmt.Println(root.Hex()) + return nil + } + + if err := app.Run(os.Args); err != nil { + log.Crit("Application failed", "err", err) + } +} + +func CalculateOutputRoot(ctx context.Context, rpcUrl string, blockNum uint64) (common.Hash, error) { + client, err := ethclient.DialContext(ctx, rpcUrl) + if err != nil { + return common.Hash{}, fmt.Errorf("failed to connect to L2 RPC endpoint: %w", err) + } + header, err := client.HeaderByNumber(ctx, new(big.Int).SetUint64(blockNum)) + if err != nil { + return common.Hash{}, fmt.Errorf("failed to get L2 block header: %w", err) + } + // Isthmus assumes WithdrawalsHash is present in the header. + if header.WithdrawalsHash == nil { + return common.Hash{}, fmt.Errorf("target block %d (%s) is missing withdrawals hash, required for Isthmus output root calculation", + header.Number.Uint64(), header.Hash()) + } + + // Construct OutputV0 using StateRoot, WithdrawalsHash (as MessagePasserStorageRoot), and BlockHash + output := ð.OutputV0{ + StateRoot: eth.Bytes32(header.Root), + MessagePasserStorageRoot: eth.Bytes32(*header.WithdrawalsHash), + BlockHash: header.Hash(), + } + + // Calculate the output root hash + return common.Hash(eth.OutputRoot(output)), nil +} diff --git a/op-chain-ops/interopgen/deploy.go b/op-chain-ops/interopgen/deploy.go index aaaeb32456f..9937c5dbf4f 100644 --- a/op-chain-ops/interopgen/deploy.go +++ b/op-chain-ops/interopgen/deploy.go @@ -254,6 +254,7 @@ func DeployL2ToL1(l1Host *script.Host, superCfg *SuperchainConfig, superDeployme AllowCustomDisputeParameters: true, OperatorFeeScalar: cfg.GasPriceOracleOperatorFeeScalar, OperatorFeeConstant: cfg.GasPriceOracleOperatorFeeConstant, + SuperchainConfig: superDeployment.SuperchainConfigProxy, UseCustomGasToken: cfg.UseCustomGasToken, }) if err != nil { diff --git a/op-deployer/pkg/deployer/devfeatures.go b/op-deployer/pkg/deployer/devfeatures.go index 85fb8e7ddd2..88993d916ef 100644 --- a/op-deployer/pkg/deployer/devfeatures.go +++ b/op-deployer/pkg/deployer/devfeatures.go @@ -18,8 +18,8 @@ var ( // DeployV2DisputeGamesDevFlag enables deployment of V2 dispute game contracts. DeployV2DisputeGamesDevFlag = common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000100") - // OpcmV2DevFlag enables deployment of OPCM V2. - OpcmV2DevFlag = common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000010000") + // OPCMV2DevFlag enables the OPContractsManagerV2 contract. + OPCMV2DevFlag = common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000010000") ) // IsDevFeatureEnabled checks if a specific development feature is enabled in a feature bitmap. diff --git a/op-deployer/pkg/deployer/integration_test/apply_test.go b/op-deployer/pkg/deployer/integration_test/apply_test.go index 63da290d7ab..70685fe6051 100644 --- a/op-deployer/pkg/deployer/integration_test/apply_test.go +++ b/op-deployer/pkg/deployer/integration_test/apply_test.go @@ -362,6 +362,62 @@ func TestEndToEndApply(t *testing.T) { require.True(t, exists, "Native asset liquidity predeploy should exist in L2 genesis") require.Equal(t, amount, account.Balance, "Native asset liquidity predeploy should have the configured balance") }) + + t.Run("OPCMV2 deployment", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + lgr := testlog.Logger(t, slog.LevelDebug) + l1RPC, l1Client := devnet.DefaultAnvilRPC(t, lgr) + _, pk, dk := shared.DefaultPrivkey(t) + l1ChainID := new(big.Int).SetUint64(devnet.DefaultChainID) + l2ChainID := uint256.NewInt(1) + loc, _ := testutil.LocalArtifacts(t) + testCacheDir := testutils.IsolatedTestDirWithAutoCleanup(t) + + intent, st := shared.NewIntent(t, l1ChainID, dk, l2ChainID, loc, loc, testCustomGasLimit) + + // Enable OPCMV2 dev flag + intent.GlobalDeployOverrides = map[string]any{ + "devFeatureBitmap": deployer.OPCMV2DevFlag, + } + + require.NoError(t, deployer.ApplyPipeline( + ctx, + deployer.ApplyPipelineOpts{ + DeploymentTarget: deployer.DeploymentTargetLive, + L1RPCUrl: l1RPC, + DeployerPrivateKey: pk, + Intent: intent, + State: st, + Logger: lgr, + StateWriter: pipeline.NoopStateWriter(), + CacheDir: testCacheDir, + }, + )) + + // Verify that OPCMV2 was deployed in implementations + require.NotEmpty(t, st.ImplementationsDeployment.OpcmV2Impl, "OPCMV2 implementation should be deployed") + require.NotEmpty(t, st.ImplementationsDeployment.OpcmContainerImpl, "OPCM container implementation should be deployed") + require.NotEmpty(t, st.ImplementationsDeployment.OpcmStandardValidatorImpl, "OPCM standard validator implementation should be deployed") + + // Verify that implementations are deployed on L1 + cg := ethClientCodeGetter(ctx, l1Client) + + opcmV2Code := cg(t, st.ImplementationsDeployment.OpcmV2Impl) + require.NotEmpty(t, opcmV2Code, "OPCMV2 should have code deployed") + + // Verify that the dev feature bitmap is set to OPCMV2 + require.Equal(t, deployer.OPCMV2DevFlag, intent.GlobalDeployOverrides["devFeatureBitmap"]) + + // Assert that the OPCM V1 addresses are zero + require.Equal(t, common.Address{}, st.ImplementationsDeployment.OpcmImpl, "OPCM V1 implementation should be zero") + require.Equal(t, common.Address{}, st.ImplementationsDeployment.OpcmContractsContainerImpl, "OPCM container implementation should be zero") + require.Equal(t, common.Address{}, st.ImplementationsDeployment.OpcmGameTypeAdderImpl, "OPCM game type adder implementation should be zero") + require.Equal(t, common.Address{}, st.ImplementationsDeployment.OpcmDeployerImpl, "OPCM deployer implementation should be zero") + require.Equal(t, common.Address{}, st.ImplementationsDeployment.OpcmUpgraderImpl, "OPCM upgrader implementation should be zero") + require.Equal(t, common.Address{}, st.ImplementationsDeployment.OpcmInteropMigratorImpl, "OPCM interop migrator implementation should be zero") + }) } func TestGlobalOverrides(t *testing.T) { diff --git a/op-deployer/pkg/deployer/opcm/opchain.go b/op-deployer/pkg/deployer/opcm/opchain.go index 115bb14f9af..a0298c71b66 100644 --- a/op-deployer/pkg/deployer/opcm/opchain.go +++ b/op-deployer/pkg/deployer/opcm/opchain.go @@ -43,6 +43,7 @@ type DeployOPChainInput struct { OperatorFeeScalar uint32 OperatorFeeConstant uint64 + SuperchainConfig common.Address UseCustomGasToken bool } diff --git a/op-deployer/pkg/deployer/opcm/read_superchain_deployment.go b/op-deployer/pkg/deployer/opcm/read_superchain_deployment.go index add97bfe4ad..e5f60ab0eb3 100644 --- a/op-deployer/pkg/deployer/opcm/read_superchain_deployment.go +++ b/op-deployer/pkg/deployer/opcm/read_superchain_deployment.go @@ -6,21 +6,23 @@ import ( ) type ReadSuperchainDeploymentInput struct { - OPCMAddress common.Address `abi:"opcmAddress"` + OPCMAddress common.Address `abi:"opcmAddress"` // TODO(#18612): Remove OPCMAddress field when OPCMv1 gets deprecated + SuperchainConfigProxy common.Address `abi:"superchainConfigProxy"` } type ReadSuperchainDeploymentOutput struct { - ProtocolVersionsImpl common.Address - ProtocolVersionsProxy common.Address - SuperchainConfigImpl common.Address - SuperchainConfigProxy common.Address - SuperchainProxyAdmin common.Address - - Guardian common.Address + // TODO(#18612): Remove ProtocolVersions fields when OPCMv1 gets deprecated + ProtocolVersionsImpl common.Address + ProtocolVersionsProxy common.Address ProtocolVersionsOwner common.Address - SuperchainProxyAdminOwner common.Address RecommendedProtocolVersion [32]byte RequiredProtocolVersion [32]byte + + SuperchainConfigImpl common.Address + SuperchainConfigProxy common.Address + SuperchainProxyAdmin common.Address + Guardian common.Address + SuperchainProxyAdminOwner common.Address } type ReadSuperchainDeploymentScript script.DeployScriptWithOutput[ReadSuperchainDeploymentInput, ReadSuperchainDeploymentOutput] diff --git a/op-deployer/pkg/deployer/pipeline/implementations.go b/op-deployer/pkg/deployer/pipeline/implementations.go index 138186f905a..e3a01d14dfb 100644 --- a/op-deployer/pkg/deployer/pipeline/implementations.go +++ b/op-deployer/pkg/deployer/pipeline/implementations.go @@ -74,6 +74,8 @@ func DeployImplementations(env *Env, intent *state.Intent, st *state.State) erro OpcmUpgraderImpl: dio.OpcmUpgrader, OpcmInteropMigratorImpl: dio.OpcmInteropMigrator, OpcmStandardValidatorImpl: dio.OpcmStandardValidator, + OpcmV2Impl: dio.OpcmV2, + OpcmContainerImpl: dio.OpcmContainer, DelayedWethImpl: dio.DelayedWETHImpl, OptimismPortalImpl: dio.OptimismPortalImpl, OptimismPortalInteropImpl: dio.OptimismPortalInteropImpl, diff --git a/op-deployer/pkg/deployer/pipeline/init.go b/op-deployer/pkg/deployer/pipeline/init.go index f17fe61c00c..00e26709df3 100644 --- a/op-deployer/pkg/deployer/pipeline/init.go +++ b/op-deployer/pkg/deployer/pipeline/init.go @@ -27,25 +27,37 @@ func InitLiveStrategy(ctx context.Context, env *Env, intent *state.Intent, st *s } hasPredeployedOPCM := intent.OPCMAddress != nil + hasSuperchainConfigProxy := intent.SuperchainConfigProxy != nil - if hasPredeployedOPCM { - if intent.SuperchainConfigProxy != nil { - return fmt.Errorf("cannot set superchain config proxy for predeployed OPCM") + if hasPredeployedOPCM || hasSuperchainConfigProxy { + if intent.SuperchainRoles != nil { + return fmt.Errorf("cannot set superchain roles when using predeployed OPCM or SuperchainConfig") } - if intent.SuperchainRoles != nil { - return fmt.Errorf("cannot set superchain roles for predeployed OPCM") + opcmAddr := common.Address{} + if hasPredeployedOPCM { + opcmAddr = *intent.OPCMAddress } - superDeployment, superRoles, err := PopulateSuperchainState(env.L1ScriptHost, *intent.OPCMAddress) + superchainConfigAddr := common.Address{} + if hasSuperchainConfigProxy { + superchainConfigAddr = *intent.SuperchainConfigProxy + } + + // The ReadSuperchainDeployment script (packages/contracts-bedrock/scripts/deploy/ReadSuperchainDeployment.s.sol) + // uses the OPCM's semver version (>= 7.0.0 indicates v2) to determine how to populate the superchain state: + // - OPCMv1 (< 7.0.0): Queries the OPCM contract to get SuperchainConfig and ProtocolVersions + // - OPCMv2 (>= 7.0.0): Uses the provided SuperchainConfigProxy address; ProtocolVersions is deprecated + superDeployment, superRoles, err := PopulateSuperchainState(env.L1ScriptHost, opcmAddr, superchainConfigAddr) if err != nil { return fmt.Errorf("error populating superchain state: %w", err) } st.SuperchainDeployment = superDeployment st.SuperchainRoles = superRoles - if st.ImplementationsDeployment == nil { + + if hasPredeployedOPCM && st.ImplementationsDeployment == nil { st.ImplementationsDeployment = &addresses.ImplementationsContracts{ - OpcmImpl: *intent.OPCMAddress, + OpcmImpl: opcmAddr, } } } @@ -125,14 +137,17 @@ func immutableErr(field string, was, is any) error { return fmt.Errorf("%s is immutable: was %v, is %v", field, was, is) } -func PopulateSuperchainState(host *script.Host, opcmAddr common.Address) (*addresses.SuperchainContracts, *addresses.SuperchainRoles, error) { +// TODO(#18612): Remove OPCMAddress field when OPCMv1 gets deprecated +// TODO(#18612): Remove ProtocolVersions fields when OPCMv1 gets deprecated +func PopulateSuperchainState(host *script.Host, opcmAddr common.Address, superchainConfigProxy common.Address) (*addresses.SuperchainContracts, *addresses.SuperchainRoles, error) { readScript, err := opcm.NewReadSuperchainDeploymentScript(host) if err != nil { return nil, nil, fmt.Errorf("error generating read superchain deployment script: %w", err) } out, err := readScript.Run(opcm.ReadSuperchainDeploymentInput{ - OPCMAddress: opcmAddr, + OPCMAddress: opcmAddr, + SuperchainConfigProxy: superchainConfigProxy, }) if err != nil { return nil, nil, fmt.Errorf("error reading superchain deployment: %w", err) diff --git a/op-deployer/pkg/deployer/pipeline/init_test.go b/op-deployer/pkg/deployer/pipeline/init_test.go index 0a3df1b37b9..c08b4c57a7e 100644 --- a/op-deployer/pkg/deployer/pipeline/init_test.go +++ b/op-deployer/pkg/deployer/pipeline/init_test.go @@ -236,18 +236,600 @@ func TestPopulateSuperchainState(t *testing.T) { superchain, err := standard.SuperchainFor(11155111) require.NoError(t, err) opcmAddr := l1Versions["op-contracts/v2.0.0-rc.1"].OPContractsManager.Address - dep, roles, err := PopulateSuperchainState(host, common.Address(*opcmAddr)) - require.NoError(t, err) - require.Equal(t, addresses.SuperchainContracts{ - SuperchainProxyAdminImpl: common.HexToAddress("0x189aBAAaa82DfC015A588A7dbaD6F13b1D3485Bc"), - SuperchainConfigProxy: superchain.SuperchainConfigAddr, - SuperchainConfigImpl: common.HexToAddress("0x4da82a327773965b8d4D85Fa3dB8249b387458E7"), - ProtocolVersionsProxy: superchain.ProtocolVersionsAddr, - ProtocolVersionsImpl: common.HexToAddress("0x37E15e4d6DFFa9e5E320Ee1eC036922E563CB76C"), - }, *dep) - require.Equal(t, addresses.SuperchainRoles{ - SuperchainProxyAdminOwner: common.HexToAddress("0x1Eb2fFc903729a0F03966B917003800b145F56E2"), - ProtocolVersionsOwner: common.HexToAddress("0xfd1D2e729aE8eEe2E146c033bf4400fE75284301"), - SuperchainGuardian: common.HexToAddress("0x7a50f00e8D05b95F98fE38d8BeE366a7324dCf7E"), - }, *roles) + + t.Run("valid OPCM address only", func(t *testing.T) { + dep, roles, err := PopulateSuperchainState(host, common.Address(*opcmAddr), common.Address{}) + require.NoError(t, err) + require.Equal(t, addresses.SuperchainContracts{ + SuperchainProxyAdminImpl: common.HexToAddress("0x189aBAAaa82DfC015A588A7dbaD6F13b1D3485Bc"), + SuperchainConfigProxy: superchain.SuperchainConfigAddr, + SuperchainConfigImpl: common.HexToAddress("0x4da82a327773965b8d4D85Fa3dB8249b387458E7"), + ProtocolVersionsProxy: superchain.ProtocolVersionsAddr, + ProtocolVersionsImpl: common.HexToAddress("0x37E15e4d6DFFa9e5E320Ee1eC036922E563CB76C"), + }, *dep) + require.Equal(t, addresses.SuperchainRoles{ + SuperchainProxyAdminOwner: common.HexToAddress("0x1Eb2fFc903729a0F03966B917003800b145F56E2"), + ProtocolVersionsOwner: common.HexToAddress("0xfd1D2e729aE8eEe2E146c033bf4400fE75284301"), + SuperchainGuardian: common.HexToAddress("0x7a50f00e8D05b95F98fE38d8BeE366a7324dCf7E"), + }, *roles) + }) + + t.Run("OPCM address with SuperchainConfigProxy", func(t *testing.T) { + // When both are provided and OPCM version < 7.0.0, the script uses v1 flow + // The SuperchainConfigProxy parameter is ignored in v1 flow + dep, roles, err := PopulateSuperchainState(host, common.Address(*opcmAddr), superchain.SuperchainConfigAddr) + require.NoError(t, err) + require.NotNil(t, dep) + require.NotNil(t, roles) + + // For OPCMv1, ProtocolVersions should be populated (read from OPCM) + require.NotEqual(t, common.Address{}, dep.ProtocolVersionsProxy, "ProtocolVersionsProxy should be populated for v1") + require.NotEqual(t, common.Address{}, dep.ProtocolVersionsImpl, "ProtocolVersionsImpl should be populated for v1") + require.NotEqual(t, common.Address{}, roles.ProtocolVersionsOwner, "ProtocolVersionsOwner should be populated for v1") + + // Verify that values match what OPCM returns (not the SuperchainConfigProxy parameter) + require.Equal(t, superchain.SuperchainConfigAddr, dep.SuperchainConfigProxy) + require.Equal(t, superchain.ProtocolVersionsAddr, dep.ProtocolVersionsProxy) + }) + + t.Run("invalid OPCM address", func(t *testing.T) { + // Use an invalid address (non-existent contract) + invalidOpcmAddr := common.HexToAddress("0x1234567890123456789012345678901234567890") + dep, roles, err := PopulateSuperchainState(host, invalidOpcmAddr, common.Address{}) + require.Error(t, err) + require.Nil(t, dep) + require.Nil(t, roles) + require.Contains(t, err.Error(), "error reading superchain deployment") + }) + + t.Run("output mapping validation", func(t *testing.T) { + dep, roles, err := PopulateSuperchainState(host, common.Address(*opcmAddr), common.Address{}) + require.NoError(t, err) + require.NotNil(t, dep) + require.NotNil(t, roles) + + // Verify all SuperchainContracts fields are populated correctly + require.NotEqual(t, common.Address{}, dep.SuperchainProxyAdminImpl, "SuperchainProxyAdminImpl should be populated") + require.NotEqual(t, common.Address{}, dep.SuperchainConfigProxy, "SuperchainConfigProxy should be populated") + require.NotEqual(t, common.Address{}, dep.SuperchainConfigImpl, "SuperchainConfigImpl should be populated") + require.NotEqual(t, common.Address{}, dep.ProtocolVersionsProxy, "ProtocolVersionsProxy should be populated for v1") + require.NotEqual(t, common.Address{}, dep.ProtocolVersionsImpl, "ProtocolVersionsImpl should be populated for v1") + + // Verify implementations are different from proxies + require.NotEqual(t, dep.SuperchainConfigImpl, dep.SuperchainConfigProxy, "SuperchainConfigImpl should differ from proxy") + require.NotEqual(t, dep.ProtocolVersionsImpl, dep.ProtocolVersionsProxy, "ProtocolVersionsImpl should differ from proxy") + + // Verify all SuperchainRoles fields are populated correctly + require.NotEqual(t, common.Address{}, roles.SuperchainProxyAdminOwner, "SuperchainProxyAdminOwner should be populated") + require.NotEqual(t, common.Address{}, roles.ProtocolVersionsOwner, "ProtocolVersionsOwner should be populated for v1") + require.NotEqual(t, common.Address{}, roles.SuperchainGuardian, "SuperchainGuardian should be populated") + + // Verify expected values match + require.Equal(t, superchain.SuperchainConfigAddr, dep.SuperchainConfigProxy) + require.Equal(t, superchain.ProtocolVersionsAddr, dep.ProtocolVersionsProxy) + }) +} + +// TestPopulateSuperchainState_OPCMV2 validates that PopulateSuperchainState handles the OPCM v2 flow, where only a SuperchainConfigProxy +// is provided. This test uses a forked script host configured to a pinned Sepolia block to guarantee deterministic results. +// It asserts that returned roles and addresses are correct for the superchain config under OPCM v2, and that ProtocolVersions +// contract fields—which are not present in OPCM v2—are zeroed out as expected. +func TestPopulateSuperchainState_OPCMV2(t *testing.T) { + t.Parallel() + + rpcURL := os.Getenv("SEPOLIA_RPC_URL") + require.NotEmpty(t, rpcURL, "SEPOLIA_RPC_URL must be set") + + lgr := testlog.Logger(t, slog.LevelInfo) + retryProxy := devnet.NewRetryProxy(lgr, rpcURL) + require.NoError(t, retryProxy.Start()) + t.Cleanup(func() { + require.NoError(t, retryProxy.Stop()) + }) + + rpcClient, err := rpc.Dial(retryProxy.Endpoint()) + require.NoError(t, err) + + _, afacts := testutil.LocalArtifacts(t) + host, err := env.ForkedScriptHost( + broadcaster.NoopBroadcaster(), + testlog.Logger(t, log.LevelInfo), + common.Address{'D'}, + afacts, + rpcClient, + // corresponds to the latest block on sepolia as of 04/30/2025. used to prevent config drift on sepolia + // from failing this test + big.NewInt(8227159), + ) + require.NoError(t, err) + + superchain, err := standard.SuperchainFor(11155111) + require.NoError(t, err) + + t.Run("SuperchainConfigProxy only", func(t *testing.T) { + // opcmAddr is set to 0, all config is provided in the superchainConfigProxy + dep, roles, err := PopulateSuperchainState(host, common.Address{}, superchain.SuperchainConfigAddr) + require.NoError(t, err) + + require.Equal(t, addresses.SuperchainContracts{ + SuperchainProxyAdminImpl: common.HexToAddress("0x189aBAAaa82DfC015A588A7dbaD6F13b1D3485Bc"), + SuperchainConfigProxy: superchain.SuperchainConfigAddr, + SuperchainConfigImpl: common.HexToAddress("0x4da82a327773965b8d4D85Fa3dB8249b387458E7"), + // TODO(#18612): Remove ProtocolVersions fields when OPCMv1 gets deprecated + ProtocolVersionsProxy: common.Address{}, + ProtocolVersionsImpl: common.Address{}, + }, *dep) + require.Equal(t, addresses.SuperchainRoles{ + SuperchainProxyAdminOwner: common.HexToAddress("0x1Eb2fFc903729a0F03966B917003800b145F56E2"), + // TODO(#18612): Remove ProtocolVersions fields when OPCMv1 gets deprecated + ProtocolVersionsOwner: common.Address{}, + SuperchainGuardian: common.HexToAddress("0x7a50f00e8D05b95F98fE38d8BeE366a7324dCf7E"), + }, *roles) + }) + + t.Run("both addresses zero", func(t *testing.T) { + // When both are zero, the script detects OPCMv2 flow (because opcmAddr == 0) + // but then requires SuperchainConfigProxy to be set, so it should error + dep, roles, err := PopulateSuperchainState(host, common.Address{}, common.Address{}) + require.Error(t, err) + require.Nil(t, dep) + require.Nil(t, roles) + require.Contains(t, err.Error(), "superchainConfigProxy required for OPCM v2") + }) + + t.Run("invalid SuperchainConfigProxy", func(t *testing.T) { + // Use an invalid address (non-existent contract) + invalidSuperchainConfigProxy := common.HexToAddress("0x1234567890123456789012345678901234567890") + dep, roles, err := PopulateSuperchainState(host, common.Address{}, invalidSuperchainConfigProxy) + require.Error(t, err) + require.Nil(t, dep) + require.Nil(t, roles) + require.Contains(t, err.Error(), "error reading superchain deployment") + }) + + t.Run("output mapping validation", func(t *testing.T) { + dep, roles, err := PopulateSuperchainState(host, common.Address{}, superchain.SuperchainConfigAddr) + require.NoError(t, err) + require.NotNil(t, dep) + require.NotNil(t, roles) + + // Verify SuperchainConfig fields are populated + require.NotEqual(t, common.Address{}, dep.SuperchainProxyAdminImpl, "SuperchainProxyAdminImpl should be populated") + require.NotEqual(t, common.Address{}, dep.SuperchainConfigProxy, "SuperchainConfigProxy should be populated") + require.NotEqual(t, common.Address{}, dep.SuperchainConfigImpl, "SuperchainConfigImpl should be populated") + require.NotEqual(t, dep.SuperchainConfigImpl, dep.SuperchainConfigProxy, "SuperchainConfigImpl should differ from proxy") + + // Verify ProtocolVersions fields are zeroed for v2 + require.Equal(t, common.Address{}, dep.ProtocolVersionsProxy, "ProtocolVersionsProxy should be zero for v2") + require.Equal(t, common.Address{}, dep.ProtocolVersionsImpl, "ProtocolVersionsImpl should be zero for v2") + + // Verify SuperchainRoles fields are populated correctly + require.NotEqual(t, common.Address{}, roles.SuperchainProxyAdminOwner, "SuperchainProxyAdminOwner should be populated") + require.NotEqual(t, common.Address{}, roles.SuperchainGuardian, "SuperchainGuardian should be populated") + + // Verify ProtocolVersionsOwner is zeroed for v2 + require.Equal(t, common.Address{}, roles.ProtocolVersionsOwner, "ProtocolVersionsOwner should be zero for v2") + + // Verify expected values match + require.Equal(t, superchain.SuperchainConfigAddr, dep.SuperchainConfigProxy) + }) +} + +// Validates the OPCM v2 flow in InitLiveStrategy +// when SuperchainConfigProxy is provided and opcmV2Enabled is true. +func TestInitLiveStrategy_OPCMV2WithSuperchainConfigProxy(t *testing.T) { + t.Parallel() + + rpcURL := os.Getenv("SEPOLIA_RPC_URL") + require.NotEmpty(t, rpcURL, "SEPOLIA_RPC_URL must be set") + + lgr := testlog.Logger(t, slog.LevelInfo) + retryProxy := devnet.NewRetryProxy(lgr, rpcURL) + require.NoError(t, retryProxy.Start()) + t.Cleanup(func() { + require.NoError(t, retryProxy.Stop()) + }) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + rpcClient, err := rpc.Dial(retryProxy.Endpoint()) + require.NoError(t, err) + client := ethclient.NewClient(rpcClient) + + l1ChainID := uint64(11155111) + superchain, err := standard.SuperchainFor(l1ChainID) + require.NoError(t, err) + + _, afacts := testutil.LocalArtifacts(t) + host, err := env.DefaultForkedScriptHost( + ctx, + broadcaster.NoopBroadcaster(), + testlog.Logger(t, log.LevelInfo), + common.Address{'D'}, + afacts, + rpcClient, + ) + require.NoError(t, err) + + // Set opcmV2Enabled flag via devFeatureBitmap + opcmV2Flag := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000010000") + intent := &state.Intent{ + ConfigType: state.IntentTypeStandard, + L1ChainID: l1ChainID, + L1ContractsLocator: artifacts.EmbeddedLocator, + L2ContractsLocator: artifacts.EmbeddedLocator, + SuperchainConfigProxy: &superchain.SuperchainConfigAddr, + GlobalDeployOverrides: map[string]any{ + "devFeatureBitmap": opcmV2Flag, + }, + } + + st := &state.State{ + Version: 1, + } + + err = InitLiveStrategy( + ctx, + &Env{ + L1Client: client, + Logger: lgr, + L1ScriptHost: host, + }, + intent, + st, + ) + require.NoError(t, err) + + // Verify state was populated + require.NotNil(t, st.SuperchainDeployment, "SuperchainDeployment should be populated") + require.NotNil(t, st.SuperchainRoles, "SuperchainRoles should be populated") + + // Verify ProtocolVersions fields are zeroed for v2 + require.Equal(t, common.Address{}, st.SuperchainDeployment.ProtocolVersionsProxy, "ProtocolVersionsProxy should be zero for v2") + require.Equal(t, common.Address{}, st.SuperchainDeployment.ProtocolVersionsImpl, "ProtocolVersionsImpl should be zero for v2") + require.Equal(t, common.Address{}, st.SuperchainRoles.ProtocolVersionsOwner, "ProtocolVersionsOwner should be zero for v2") + + // Verify SuperchainConfig fields are populated + require.Equal(t, superchain.SuperchainConfigAddr, st.SuperchainDeployment.SuperchainConfigProxy) + require.NotEqual(t, common.Address{}, st.SuperchainDeployment.SuperchainConfigImpl) + require.NotEqual(t, common.Address{}, st.SuperchainDeployment.SuperchainProxyAdminImpl) +} + +// Validates that providing both +// SuperchainConfigProxy and SuperchainRoles with opcmV2Enabled returns an error. +func TestInitLiveStrategy_OPCMV2WithSuperchainConfigProxyAndRoles_reverts(t *testing.T) { + t.Parallel() + + rpcURL := os.Getenv("SEPOLIA_RPC_URL") + require.NotEmpty(t, rpcURL, "SEPOLIA_RPC_URL must be set") + + lgr := testlog.Logger(t, slog.LevelInfo) + retryProxy := devnet.NewRetryProxy(lgr, rpcURL) + require.NoError(t, retryProxy.Start()) + t.Cleanup(func() { + require.NoError(t, retryProxy.Stop()) + }) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + rpcClient, err := rpc.Dial(retryProxy.Endpoint()) + require.NoError(t, err) + client := ethclient.NewClient(rpcClient) + + l1ChainID := uint64(11155111) + superchain, err := standard.SuperchainFor(l1ChainID) + require.NoError(t, err) + + // Set opcmV2Enabled flag via devFeatureBitmap + opcmV2Flag := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000010000") + intent := &state.Intent{ + ConfigType: state.IntentTypeStandard, + L1ChainID: l1ChainID, + L1ContractsLocator: artifacts.EmbeddedLocator, + L2ContractsLocator: artifacts.EmbeddedLocator, + SuperchainConfigProxy: &superchain.SuperchainConfigAddr, + SuperchainRoles: &addresses.SuperchainRoles{ + SuperchainGuardian: common.Address{0: 99}, + }, + GlobalDeployOverrides: map[string]any{ + "devFeatureBitmap": opcmV2Flag, + }, + } + + st := &state.State{ + Version: 1, + } + + err = InitLiveStrategy( + ctx, + &Env{ + L1Client: client, + Logger: lgr, + }, + intent, + st, + ) + require.Error(t, err) + require.Contains(t, err.Error(), "cannot set superchain roles when using predeployed OPCM or SuperchainConfig") +} + +// Validates that providing both OPCMAddress and SuperchainConfigProxy works correctly +// The script will use the OPCM's semver to determine the version +func TestInitLiveStrategy_OPCMV1WithSuperchainConfigProxy(t *testing.T) { + t.Parallel() + + rpcURL := os.Getenv("SEPOLIA_RPC_URL") + require.NotEmpty(t, rpcURL, "SEPOLIA_RPC_URL must be set") + + lgr := testlog.Logger(t, slog.LevelInfo) + retryProxy := devnet.NewRetryProxy(lgr, rpcURL) + require.NoError(t, retryProxy.Start()) + t.Cleanup(func() { + require.NoError(t, retryProxy.Stop()) + }) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + rpcClient, err := rpc.Dial(retryProxy.Endpoint()) + require.NoError(t, err) + client := ethclient.NewClient(rpcClient) + + l1ChainID := uint64(11155111) + superchain, err := standard.SuperchainFor(l1ChainID) + require.NoError(t, err) + + opcmAddr, err := standard.OPCMImplAddressFor(l1ChainID, standard.CurrentTag) + require.NoError(t, err) + + _, afacts := testutil.LocalArtifacts(t) + host, err := env.DefaultForkedScriptHost( + ctx, + broadcaster.NoopBroadcaster(), + testlog.Logger(t, log.LevelInfo), + common.Address{'D'}, + afacts, + rpcClient, + ) + require.NoError(t, err) + + // Provide both OPCM address and SuperchainConfigProxy + // The script will check the OPCM version and handle accordingly + intent := &state.Intent{ + ConfigType: state.IntentTypeStandard, + L1ChainID: l1ChainID, + L1ContractsLocator: artifacts.EmbeddedLocator, + L2ContractsLocator: artifacts.EmbeddedLocator, + OPCMAddress: &opcmAddr, + SuperchainConfigProxy: &superchain.SuperchainConfigAddr, + } + + st := &state.State{ + Version: 1, + } + + err = InitLiveStrategy( + ctx, + &Env{ + L1Client: client, + Logger: lgr, + L1ScriptHost: host, + }, + intent, + st, + ) + // Should succeed - the script handles version detection + require.NoError(t, err) + + // For OPCMv1, ProtocolVersions should be populated + require.NotNil(t, st.SuperchainDeployment) + require.NotEqual(t, common.Address{}, st.SuperchainDeployment.ProtocolVersionsProxy) + require.NotEqual(t, common.Address{}, st.SuperchainDeployment.ProtocolVersionsImpl) +} + +// Validates that providing both +// OPCMAddress and SuperchainRoles with opcmV2Enabled=false returns an error. +func TestInitLiveStrategy_OPCMV1WithSuperchainRoles_reverts(t *testing.T) { + t.Parallel() + + rpcURL := os.Getenv("SEPOLIA_RPC_URL") + require.NotEmpty(t, rpcURL, "SEPOLIA_RPC_URL must be set") + + lgr := testlog.Logger(t, slog.LevelInfo) + retryProxy := devnet.NewRetryProxy(lgr, rpcURL) + require.NoError(t, retryProxy.Start()) + t.Cleanup(func() { + require.NoError(t, retryProxy.Stop()) + }) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + rpcClient, err := rpc.Dial(retryProxy.Endpoint()) + require.NoError(t, err) + client := ethclient.NewClient(rpcClient) + + l1ChainID := uint64(11155111) + opcmAddr, err := standard.OPCMImplAddressFor(l1ChainID, standard.CurrentTag) + require.NoError(t, err) + + // Don't set opcmV2Enabled flag (defaults to false) + intent := &state.Intent{ + ConfigType: state.IntentTypeStandard, + L1ChainID: l1ChainID, + L1ContractsLocator: artifacts.EmbeddedLocator, + L2ContractsLocator: artifacts.EmbeddedLocator, + OPCMAddress: &opcmAddr, + SuperchainRoles: &addresses.SuperchainRoles{ + SuperchainGuardian: common.Address{0: 99}, + }, + } + + st := &state.State{ + Version: 1, + } + + err = InitLiveStrategy( + ctx, + &Env{ + L1Client: client, + Logger: lgr, + }, + intent, + st, + ) + require.Error(t, err) + require.Contains(t, err.Error(), "cannot set superchain roles when using predeployed OPCM or SuperchainConfig") +} + +// Validates that the correct flow is chosen when +// hasPredeployedOPCM && !opcmV2Enabled, and that PopulateSuperchainState is called with correct parameters. +func TestInitLiveStrategy_FlowSelection_OPCMV1(t *testing.T) { + t.Parallel() + + rpcURL := os.Getenv("SEPOLIA_RPC_URL") + require.NotEmpty(t, rpcURL, "SEPOLIA_RPC_URL must be set") + + lgr := testlog.Logger(t, slog.LevelInfo) + retryProxy := devnet.NewRetryProxy(lgr, rpcURL) + require.NoError(t, retryProxy.Start()) + t.Cleanup(func() { + require.NoError(t, retryProxy.Stop()) + }) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + rpcClient, err := rpc.Dial(retryProxy.Endpoint()) + require.NoError(t, err) + client := ethclient.NewClient(rpcClient) + + l1ChainID := uint64(11155111) + opcmAddr, err := standard.OPCMImplAddressFor(l1ChainID, standard.CurrentTag) + require.NoError(t, err) + + _, afacts := testutil.LocalArtifacts(t) + host, err := env.DefaultForkedScriptHost( + ctx, + broadcaster.NoopBroadcaster(), + testlog.Logger(t, log.LevelInfo), + common.Address{'D'}, + afacts, + rpcClient, + ) + require.NoError(t, err) + + // Don't set opcmV2Enabled flag (defaults to false) + intent := &state.Intent{ + ConfigType: state.IntentTypeStandard, + L1ChainID: l1ChainID, + L1ContractsLocator: artifacts.EmbeddedLocator, + L2ContractsLocator: artifacts.EmbeddedLocator, + OPCMAddress: &opcmAddr, + } + + st := &state.State{ + Version: 1, + } + + err = InitLiveStrategy( + ctx, + &Env{ + L1Client: client, + Logger: lgr, + L1ScriptHost: host, + }, + intent, + st, + ) + require.NoError(t, err) + + // Verify OPCM v1 flow was used - ProtocolVersions should be populated + require.NotNil(t, st.SuperchainDeployment) + require.NotEqual(t, common.Address{}, st.SuperchainDeployment.ProtocolVersionsProxy, "ProtocolVersionsProxy should be populated for v1") + require.NotEqual(t, common.Address{}, st.SuperchainDeployment.ProtocolVersionsImpl, "ProtocolVersionsImpl should be populated for v1") + require.NotEqual(t, common.Address{}, st.SuperchainRoles.ProtocolVersionsOwner, "ProtocolVersionsOwner should be populated for v1") + + // Verify ImplementationsDeployment was set + require.NotNil(t, st.ImplementationsDeployment) + require.Equal(t, opcmAddr, st.ImplementationsDeployment.OpcmImpl) +} + +// Validates that the correct flow is chosen when +// hasSuperchainConfigProxy && opcmV2Enabled, and that PopulateSuperchainState is called with correct parameters. +func TestInitLiveStrategy_FlowSelection_OPCMV2(t *testing.T) { + t.Parallel() + + rpcURL := os.Getenv("SEPOLIA_RPC_URL") + require.NotEmpty(t, rpcURL, "SEPOLIA_RPC_URL must be set") + + lgr := testlog.Logger(t, slog.LevelInfo) + retryProxy := devnet.NewRetryProxy(lgr, rpcURL) + require.NoError(t, retryProxy.Start()) + t.Cleanup(func() { + require.NoError(t, retryProxy.Stop()) + }) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + rpcClient, err := rpc.Dial(retryProxy.Endpoint()) + require.NoError(t, err) + client := ethclient.NewClient(rpcClient) + + l1ChainID := uint64(11155111) + superchain, err := standard.SuperchainFor(l1ChainID) + require.NoError(t, err) + + _, afacts := testutil.LocalArtifacts(t) + host, err := env.DefaultForkedScriptHost( + ctx, + broadcaster.NoopBroadcaster(), + testlog.Logger(t, log.LevelInfo), + common.Address{'D'}, + afacts, + rpcClient, + ) + require.NoError(t, err) + + // Set opcmV2Enabled flag via devFeatureBitmap + opcmV2Flag := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000010000") + intent := &state.Intent{ + ConfigType: state.IntentTypeStandard, + L1ChainID: l1ChainID, + L1ContractsLocator: artifacts.EmbeddedLocator, + L2ContractsLocator: artifacts.EmbeddedLocator, + SuperchainConfigProxy: &superchain.SuperchainConfigAddr, + GlobalDeployOverrides: map[string]any{ + "devFeatureBitmap": opcmV2Flag, + }, + } + + st := &state.State{ + Version: 1, + } + + err = InitLiveStrategy( + ctx, + &Env{ + L1Client: client, + Logger: lgr, + L1ScriptHost: host, + }, + intent, + st, + ) + require.NoError(t, err) + + // Verify OPCM v2 flow was used - ProtocolVersions should be zeroed + require.NotNil(t, st.SuperchainDeployment) + require.Equal(t, common.Address{}, st.SuperchainDeployment.ProtocolVersionsProxy, "ProtocolVersionsProxy should be zero for v2") + require.Equal(t, common.Address{}, st.SuperchainDeployment.ProtocolVersionsImpl, "ProtocolVersionsImpl should be zero for v2") + require.Equal(t, common.Address{}, st.SuperchainRoles.ProtocolVersionsOwner, "ProtocolVersionsOwner should be zero for v2") + + // Verify SuperchainConfig is populated + require.Equal(t, superchain.SuperchainConfigAddr, st.SuperchainDeployment.SuperchainConfigProxy) + require.NotEqual(t, common.Address{}, st.SuperchainDeployment.SuperchainConfigImpl) } diff --git a/op-deployer/pkg/deployer/pipeline/opchain.go b/op-deployer/pkg/deployer/pipeline/opchain.go index 2ef41d73d60..5fad788d25e 100644 --- a/op-deployer/pkg/deployer/pipeline/opchain.go +++ b/op-deployer/pkg/deployer/pipeline/opchain.go @@ -104,6 +104,15 @@ func makeDCI(intent *state.Intent, thisIntent *state.ChainIntent, chainID common return opcm.DeployOPChainInput{}, fmt.Errorf("error merging proof params from overrides: %w", err) } + // Select which OPCM to use based on dev feature flag + opcmAddr := st.ImplementationsDeployment.OpcmImpl + if devFeatureBitmap, ok := intent.GlobalDeployOverrides["devFeatureBitmap"].(common.Hash); ok { + opcmV2Flag := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000010000") + if isDevFeatureEnabled(devFeatureBitmap, opcmV2Flag) && st.ImplementationsDeployment.OpcmV2Impl != (common.Address{}) { + opcmAddr = st.ImplementationsDeployment.OpcmV2Impl + } + } + return opcm.DeployOPChainInput{ OpChainProxyAdminOwner: thisIntent.Roles.L1ProxyAdminOwner, SystemConfigOwner: thisIntent.Roles.SystemConfigOwner, @@ -114,7 +123,7 @@ func makeDCI(intent *state.Intent, thisIntent *state.ChainIntent, chainID common BasefeeScalar: standard.BasefeeScalar, BlobBaseFeeScalar: standard.BlobBaseFeeScalar, L2ChainId: chainID.Big(), - Opcm: st.ImplementationsDeployment.OpcmImpl, + Opcm: opcmAddr, SaltMixer: st.Create2Salt.String(), // passing through salt generated at state initialization GasLimit: thisIntent.GasLimit, DisputeGameType: proofParams.DisputeGameType, @@ -126,6 +135,7 @@ func makeDCI(intent *state.Intent, thisIntent *state.ChainIntent, chainID common AllowCustomDisputeParameters: proofParams.DangerouslyAllowCustomDisputeParameters, OperatorFeeScalar: thisIntent.OperatorFeeScalar, OperatorFeeConstant: thisIntent.OperatorFeeConstant, + SuperchainConfig: st.SuperchainDeployment.SuperchainConfigProxy, UseCustomGasToken: thisIntent.IsCustomGasTokenEnabled(), }, nil } @@ -170,3 +180,14 @@ func shouldDeployOPChain(st *state.State, chainID common.Hash) bool { return true } + +// isDevFeatureEnabled checks if a specific development feature is enabled in a feature bitmap. +// This mirrors the function in devfeatures.go to avoid import cycles. +func isDevFeatureEnabled(bitmap, flag common.Hash) bool { + b := new(big.Int).SetBytes(bitmap[:]) + f := new(big.Int).SetBytes(flag[:]) + + featuresIsNonZero := f.Cmp(big.NewInt(0)) != 0 + bitmapContainsFeatures := new(big.Int).And(b, f).Cmp(f) == 0 + return featuresIsNonZero && bitmapContainsFeatures +} diff --git a/op-devstack/dsl/l2_cl.go b/op-devstack/dsl/l2_cl.go index f478410f17f..80ee03638a6 100644 --- a/op-devstack/dsl/l2_cl.go +++ b/op-devstack/dsl/l2_cl.go @@ -91,6 +91,10 @@ func (cl *L2CLNode) StopSequencer() common.Hash { return unsafeHead } +func (cl *L2CLNode) SetSequencerRecoverMode(b bool) error { + return cl.inner.RollupAPI().SetRecoverMode(cl.ctx, b) +} + func (cl *L2CLNode) SyncStatus() *eth.SyncStatus { ctx, cancel := context.WithTimeout(cl.ctx, DefaultTimeout) defer cancel() diff --git a/op-devstack/dsl/proofs/dispute_game_factory.go b/op-devstack/dsl/proofs/dispute_game_factory.go index d8247b4c1d5..a26052d6ffc 100644 --- a/op-devstack/dsl/proofs/dispute_game_factory.go +++ b/op-devstack/dsl/proofs/dispute_game_factory.go @@ -19,6 +19,7 @@ import ( safetyTypes "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" "github.com/stretchr/testify/require" @@ -84,6 +85,7 @@ type GameCfg struct { l2SequenceNumberSet bool rootClaimSet bool rootClaim common.Hash + superOutputRoots []eth.Bytes32 } type GameOpt interface { Apply(cfg *GameCfg) @@ -120,6 +122,14 @@ func WithL2SequenceNumber(seqNum uint64) GameOpt { }) } +// WithSuperRootFrom sets the output roots to use in a super root game. +// The length of outputRoots must match the number of chains in the super root. +func WithSuperRootFrom(outputRoots ...eth.Bytes32) GameOpt { + return gameOptFn(func(c *GameCfg) { + c.superOutputRoots = outputRoots + }) +} + func NewGameCfg(opts ...GameOpt) *GameCfg { cfg := &GameCfg{} for _, opt := range opts { @@ -181,6 +191,10 @@ func (f *DisputeGameFactory) StartSuperCannonGame(eoa *dsl.EOA, opts ...GameOpt) func (f *DisputeGameFactory) startSuperCannonGameOfType(eoa *dsl.EOA, gameType gameTypes.GameType, opts ...GameOpt) *SuperFaultDisputeGame { cfg := NewGameCfg(opts...) + if len(cfg.superOutputRoots) != 0 && cfg.rootClaimSet { + f.t.Error("cannot set both super output roots and root claim in super game") + f.t.FailNow() + } timestamp := cfg.l2SequenceNumber if !cfg.l2SequenceNumberSet { timestamp = f.supervisor.FetchSyncStatus().SafeTimestamp @@ -188,9 +202,7 @@ func (f *DisputeGameFactory) startSuperCannonGameOfType(eoa *dsl.EOA, gameType g extraData := f.createSuperGameExtraData(timestamp, cfg) rootClaim := cfg.rootClaim if !cfg.rootClaimSet { - // Default to the correct root claim - response := f.supervisor.FetchSuperRootAtTimestamp(timestamp) - rootClaim = common.Hash(response.SuperRoot) + rootClaim = crypto.Keccak256Hash(extraData) } game, addr := f.createNewGame(eoa, gameType, rootClaim, extraData) @@ -202,8 +214,18 @@ func (f *DisputeGameFactory) createSuperGameExtraData(timestamp uint64, cfg *Gam if !cfg.allowFuture { f.supervisor.AwaitMinCrossSafeTimestamp(timestamp) } - extraData := make([]byte, 32) - binary.BigEndian.PutUint64(extraData[24:], timestamp) + super, err := f.supervisor.FetchSuperRootAtTimestamp(timestamp).ToSuper() + f.require.NoError(err, "Failed to fetch super root for timestamp %v", timestamp) + + superV1, ok := super.(*eth.SuperV1) + f.require.Truef(ok, "Unsupported super type %T", super) + if len(cfg.superOutputRoots) != 0 { + f.require.Len(cfg.superOutputRoots, len(superV1.Chains), "Super output roots length mismatch") + for i := range superV1.Chains { + superV1.Chains[i].Output = cfg.superOutputRoots[i] + } + } + extraData := superV1.Marshal() return extraData } diff --git a/op-e2e/actions/helpers/l2_batcher.go b/op-e2e/actions/helpers/l2_batcher.go index e0d8eac5a28..2e1a941721d 100644 --- a/op-e2e/actions/helpers/l2_batcher.go +++ b/op-e2e/actions/helpers/l2_batcher.go @@ -270,7 +270,7 @@ func (s *L2Batcher) Buffer(t Testing, bufferOpts ...BufferOption) error { } } - s.ActCreateChannel(t, s.rollupCfg.IsDelta(block.Time()), options.channelModifiers...) + s.ActCreateChannel(t, s.rollupCfg.IsDelta(block.Time()) && !s.l2BatcherCfg.ForceSubmitSingularBatch, options.channelModifiers...) if _, err := s.L2ChannelOut.AddBlock(s.rollupCfg, block); err != nil { return err diff --git a/op-e2e/actions/helpers/l2_proposer.go b/op-e2e/actions/helpers/l2_proposer.go index 2ba93d7e7c3..82404117b6b 100644 --- a/op-e2e/actions/helpers/l2_proposer.go +++ b/op-e2e/actions/helpers/l2_proposer.go @@ -4,6 +4,7 @@ import ( "context" "crypto/ecdsa" "encoding/binary" + "errors" "math/big" "time" @@ -195,6 +196,9 @@ func (p *L2Proposer) fetchNextOutput(t Testing) (source.Proposal, bool, error) { if err != nil || !shouldPropose { return source.Proposal{}, false, err } + if output.IsSuperRootProposal() { + return source.Proposal{}, false, errors.New("unexpected super root proposal") + } encodedBlockNumber := make([]byte, 32) binary.BigEndian.PutUint64(encodedBlockNumber[24:], output.SequenceNum) game, err := p.disputeGameFactory.Games(&bind.CallOpts{}, p.driver.Cfg.DisputeGameType, output.Root, encodedBlockNumber) diff --git a/op-e2e/actions/helpers/l2_sequencer.go b/op-e2e/actions/helpers/l2_sequencer.go index 97d8100b1a5..ef3d4e29dfb 100644 --- a/op-e2e/actions/helpers/l2_sequencer.go +++ b/op-e2e/actions/helpers/l2_sequencer.go @@ -40,8 +40,8 @@ func (m *MockL1OriginSelector) FindL1Origin(ctx context.Context, l2Head eth.L2Bl return m.actual.FindL1Origin(ctx, l2Head) } -func (m *MockL1OriginSelector) SetRecoverMode(bool) { - // noop +func (m *MockL1OriginSelector) SetRecoverMode(b bool) { + m.actual.SetRecoverMode(b) } // L2Sequencer is an actor that functions like a rollup node, @@ -98,20 +98,28 @@ func NewL2Sequencer(t Testing, log log.Logger, l1 derive.L1Fetcher, blobSrc deri // ActL2StartBlock starts building of a new L2 block on top of the head func (s *L2Sequencer) ActL2StartBlock(t Testing) { + err := s.ActMaybeL2StartBlock(t) + require.NoError(t, err, "failed to start block building") +} + +// ActMaybeL2StartBlock tries to start building a new L2 block on top of the head +func (s *L2Sequencer) ActMaybeL2StartBlock(t Testing) error { require.NoError(t, s.drainer.Drain()) // can't build when other work is still blocking if !s.L2PipelineIdle { t.InvalidAction("cannot start L2 build when derivation is not idle") - return + return nil } if s.l2Building { t.InvalidAction("already started building L2 block") - return + return nil } s.synchronousEvents.Emit(t.Ctx(), sequencing.SequencerActionEvent{}) - require.NoError(t, s.drainer.DrainUntil(event.Is[engine.BuildStartedEvent], false), - "failed to start block building") - + err := s.drainer.DrainUntil(event.Is[engine.BuildStartedEvent], false) + if err != nil { + return err + } s.l2Building = true + return nil } // ActL2EndBlock completes a new L2 block and applies it to the L2 chain as new canonical unsafe head @@ -272,3 +280,7 @@ func (s *L2Sequencer) ActBuildL2ToInterop(t Testing) { s.ActL2EmptyBlock(t) } } + +func (s *L2Sequencer) ActSetRecoverMode(t Testing, b bool) { + s.sequencer.SetRecoverMode(b) +} diff --git a/op-e2e/actions/helpers/utils.go b/op-e2e/actions/helpers/utils.go index 9fa5d043874..e8dc404ea16 100644 --- a/op-e2e/actions/helpers/utils.go +++ b/op-e2e/actions/helpers/utils.go @@ -15,7 +15,7 @@ func DefaultRollupTestParams() *e2eutils.TestParams { MaxSequencerDrift: 40, SequencerWindowSize: 120, ChannelTimeout: 120, - L1BlockTime: 15, + L1BlockTime: 12, // Many of the action helpers assume a 12s L1 block time AllocType: config.DefaultAllocType, } } diff --git a/op-e2e/actions/proofs/helpers/env.go b/op-e2e/actions/proofs/helpers/env.go index 6872c1d0e1e..d700763d7f7 100644 --- a/op-e2e/actions/proofs/helpers/env.go +++ b/op-e2e/actions/proofs/helpers/env.go @@ -243,15 +243,12 @@ func (env *L2FaultProofEnv) BatchAndMine(t helpers.Testing) { // Returns the L2 Safe Block Reference func (env *L2FaultProofEnv) BatchMineAndSync(t helpers.Testing) eth.L2BlockRef { t.Helper() - id := env.Miner.UnsafeID() env.BatchAndMine(t) env.Sequencer.ActL1HeadSignal(t) env.Sequencer.ActL2PipelineFull(t) // Assertions - syncStatus := env.Sequencer.SyncStatus() - require.Equal(t, syncStatus.UnsafeL2.L1Origin, id, "UnsafeL2.L1Origin should equal L1 Unsafe ID before batch submitted") require.Equal(t, syncStatus.UnsafeL2, syncStatus.SafeL2, "UnsafeL2 should equal SafeL2") return syncStatus.SafeL2 diff --git a/op-e2e/actions/proofs/sequence_window_expiry_test.go b/op-e2e/actions/proofs/sequence_window_expiry_test.go index cb702fe8eb6..cd614afb97a 100644 --- a/op-e2e/actions/proofs/sequence_window_expiry_test.go +++ b/op-e2e/actions/proofs/sequence_window_expiry_test.go @@ -5,18 +5,39 @@ import ( actionsHelpers "github.com/ethereum-optimism/optimism/op-e2e/actions/helpers" "github.com/ethereum-optimism/optimism/op-e2e/actions/proofs/helpers" + "github.com/ethereum-optimism/optimism/op-e2e/e2eutils" "github.com/ethereum-optimism/optimism/op-program/client/claim" "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" ) -// Run a test that proves a deposit-only block generated due to sequence window expiry. +// Run a test that proves a deposit-only block generated due to sequence window expiry, +// and then recovers the chain using sequencer recover mode. func runSequenceWindowExpireTest(gt *testing.T, testCfg *helpers.TestCfg[any]) { t := actionsHelpers.NewDefaultTesting(gt) - tp := helpers.NewTestParams() - env := helpers.NewL2FaultProofEnv(t, testCfg, tp, helpers.NewBatcherCfg()) + const SEQUENCER_WINDOW_SIZE = 50 // (short, to keep test fast) + tp := helpers.NewTestParams(func(p *e2eutils.TestParams) { + p.SequencerWindowSize = SEQUENCER_WINDOW_SIZE + p.MaxSequencerDrift = 1800 // use 1800 seconds (30 minutes), which is the protocol constant since Fjord + }) + + // It seems more difficult (almost impossible) to recover from sequencing window expiry with span batches, + // since the singular batches within are invalidated _atomically_. + // That is to say, if the oldest batch in the span batch fails the sequencing window check + // (l1 origin + seq window < l1 inclusion) + // All following batches are invalidated / dropped as well. + // https://github.com/ethereum-optimism/optimism/blob/73339162d78a1ebf2daadab01736382eed6f4527/op-node/rollup/derive/batches.go#L96-L100 + // + // If the same blocks were batched with singular batches, the validation rules are different + // https://github.com/ethereum-optimism/optimism/blob/73339162d78a1ebf2daadab01736382eed6f4527/op-node/rollup/derive/batches.go#L83-L86 + // In the case of recover mode, the noTxPool=true condition means autoderviation actually fills + // the gap with identical blocks anyway, meaning the following batches are actually still valid. + bc := helpers.NewBatcherCfg() + bc.ForceSubmitSingularBatch = true - // Mine an empty block for gas estimation purposes. + env := helpers.NewL2FaultProofEnv(t, testCfg, tp, bc) + + // Mine an empty L1 block for gas estimation purposes. env.Miner.ActEmptyBlock(t) // Expire the sequence window by building `SequenceWindow + 1` empty blocks on L1. @@ -24,7 +45,7 @@ func runSequenceWindowExpireTest(gt *testing.T, testCfg *helpers.TestCfg[any]) { env.Alice.L1.ActResetTxOpts(t) env.Alice.ActDeposit(t) - env.Miner.ActL1StartBlock(12)(t) + env.Miner.ActL1StartBlock(tp.L1BlockTime)(t) env.Miner.ActL1IncludeTx(env.Alice.Address())(t) env.Miner.ActL1EndBlock(t) @@ -42,10 +63,107 @@ func runSequenceWindowExpireTest(gt *testing.T, testCfg *helpers.TestCfg[any]) { // Ensure the safe head advanced forcefully. l2SafeHead = env.Engine.L2Chain().CurrentSafeBlock() - require.Greater(t, l2SafeHead.Number.Uint64(), uint64(0)) + require.Greater(t, l2SafeHead.Number.Uint64(), uint64(0), + "The safe head failed to progress after the sequencing window expired (expected deposit-only blocks to be derived).") - // Run the FPP on one of the auto-derived blocks. env.RunFaultProofProgram(t, l2SafeHead.Number.Uint64()/2, testCfg.CheckResult, testCfg.InputParams...) + + // Set recover mode on the sequencer: + env.Sequencer.ActSetRecoverMode(t, true) + // Since recover mode only affects the L2 CL (op-node), + // it won't stop the test environment injecting transactions + // directly into the engine. So we will force the engine + // to ignore such injections if recover mode is enabled. + env.Engine.EngineApi.SetForceEmpty(true) + + // Define "lag" as the difference between the current L1 block number and the safe L2 block's L1 origin number. + computeLag := func() int { + ss := env.Sequencer.SyncStatus() + return int(ss.CurrentL1.Number - ss.SafeL2.L1Origin.Number) + } + + // Define "drift" as the difference between the current L2 block's timestamp and the unsafe L2 block's L1 origin's timestamp. + computeDrift := func() int { + ss := env.Sequencer.SyncStatus() + l2header, err := env.Engine.EthClient().HeaderByHash(t.Ctx(), ss.UnsafeL2.Hash) + require.NoError(t, err) + l1header, err := env.Miner.EthClient().HeaderByHash(t.Ctx(), ss.UnsafeL2.L1Origin.Hash) + require.NoError(t, err) + t.Log("l2header.Time", l2header.Time) + t.Log("l1header.Time", l1header.Time) + return int(l2header.Time) - int(l1header.Time) + } + + // Build both chains and assert the L1 origin catches back up with the tip of the L1 chain. + lag := computeLag() + t.Log("lag", lag) + drift := computeDrift() + t.Log("drift", drift) + require.GreaterOrEqual(t, uint64(lag), tp.SequencerWindowSize, "Lag is less than sequencing window size") + numL1Blocks := 0 + timeout := tp.SequencerWindowSize * 50 + + for numL1Blocks < int(timeout) { + for range 100 * tp.L1BlockTime / env.Sd.RollupCfg.BlockTime { // go at 100x real time + err := env.Sequencer.ActMaybeL2StartBlock(t) + if err != nil { + break + } + env.Bob.L2.ActResetTxOpts(t) + env.Bob.L2.ActMakeTx(t) + env.Engine.ActL2IncludeTx(env.Bob.Address())(t) + // RecoverMode (enabled above) should prevent this + // transaction from being included in the block, which + // is critical for recover mode to work. + env.Sequencer.ActL2EndBlock(t) + drift = computeDrift() + t.Log("drift", drift) + } + env.BatchMineAndSync(t) // Mines 1 block on L1 + numL1Blocks++ + lag = computeLag() + t.Log("lag", lag) + drift = computeDrift() + t.Log("drift", drift) + if lag == 1 { // A lag of 1 is the minimum possible. + break + } + } + + if uint64(numL1Blocks) >= timeout { + t.Fatal("L1 Origin did not catch up to tip within %d L1 blocks (lag is %d)", numL1Blocks, lag) + } else { + t.Logf("L1 Origin caught up to within %d blocks of the tip within %d L1 blocks (sequencing window size %d)", + lag, numL1Blocks, tp.SequencerWindowSize) + } + + switch { + case drift == 0: + t.Fatal("drift is zero, this implies the unsafe l2 head is pinned to the l1 head") + case drift > int(tp.MaxSequencerDrift): + t.Fatal("drift is too high") + default: + t.Log("drift", drift) + } + + // Disable recover mode so we can get some user transactions in again. + env.Sequencer.ActSetRecoverMode(t, false) + env.Engine.EngineApi.SetForceEmpty(false) + l2SafeBefore := env.Sequencer.L2Safe() + env.Sequencer.ActL2StartBlock(t) + env.Bob.L2.ActResetTxOpts(t) + env.Bob.L2.ActMakeTx(t) + env.Engine.ActL2IncludeTx(env.Bob.Address())(t) + env.Sequencer.ActL2EndBlock(t) + env.BatchMineAndSync(t) + l2Safe := env.Sequencer.L2Safe() + require.Equal(t, l2Safe.Number, l2SafeBefore.Number+1, "safe chain did not progress with user transactions") + l2SafeBlock, err := env.Engine.EthClient().BlockByHash(t.Ctx(), l2Safe.Hash) + require.NoError(t, err) + // Assert safe block has at least two transactions + require.GreaterOrEqual(t, len(l2SafeBlock.Transactions()), 2, "safe block did not have at least two transactions") + + env.RunFaultProofProgram(t, l2Safe.Number, testCfg.CheckResult, testCfg.InputParams...) } // Runs a that proves a block in a chain where the batcher opens a channel, the sequence window expires, and then the diff --git a/op-node/rollup/sequencing/origin_selector.go b/op-node/rollup/sequencing/origin_selector.go index c5f23407d76..368226db34e 100644 --- a/op-node/rollup/sequencing/origin_selector.go +++ b/op-node/rollup/sequencing/origin_selector.go @@ -70,80 +70,51 @@ func (los *L1OriginSelector) OnEvent(ctx context.Context, ev event.Event) bool { return true } -// FindL1Origin determines what the next L1 Origin should be. -// The L1 Origin is either the L2 Head's Origin, or the following L1 block -// if the next L2 block's time is greater than or equal to the L2 Head's Origin. -// The origin selection relies purely on block numbers and it is the caller's -// responsibility to detect and handle L1 reorgs. +// FindL1Origin determines what the L1 Origin for the next L2 Block should be. +// It wraps the FindL1OriginOfNextL2Block function and handles caching and network requests. func (los *L1OriginSelector) FindL1Origin(ctx context.Context, l2Head eth.L2BlockRef) (eth.L1BlockRef, error) { + recoverMode := los.recoverMode.Load() + // Get cached values for currentOrigin and nextOrigin currentOrigin, nextOrigin, err := los.CurrentAndNextOrigin(ctx, l2Head) if err != nil { return eth.L1BlockRef{}, err } - - // If the next L2 block time is greater than the next origin block's time, we can choose to - // start building on top of the next origin. Sequencer implementation has some leeway here and - // could decide to continue to build on top of the previous origin until the Sequencer runs out - // of slack. For simplicity, we implement our Sequencer to always start building on the latest - // L1 block when we can. - if nextOrigin != (eth.L1BlockRef{}) && l2Head.Time+los.cfg.BlockTime >= nextOrigin.Time { - return nextOrigin, nil - } - - msd := los.spec.MaxSequencerDrift(currentOrigin.Time) - log := los.log.New("current", currentOrigin, "current_time", currentOrigin.Time, - "l2_head", l2Head, "l2_head_time", l2Head.Time, "max_seq_drift", msd) - - pastSeqDrift := l2Head.Time+los.cfg.BlockTime-currentOrigin.Time > msd - - // If we are not past the max sequencer drift, we can just return the current origin. - if !pastSeqDrift { - return currentOrigin, nil - } - - // Otherwise, we need to find the next L1 origin block in order to continue producing blocks. - log.Warn("Next L2 block time is past the sequencer drift + current origin time") - - if nextOrigin == (eth.L1BlockRef{}) { - fetchCtx, cancel := context.WithTimeout(ctx, 10*time.Second) - defer cancel() - - // If the next origin is not set, we need to fetch it now. - nextOrigin, err = los.fetch(fetchCtx, currentOrigin.Number+1) - if err != nil { - return eth.L1BlockRef{}, fmt.Errorf("cannot build next L2 block past current L1 origin %s by more than sequencer time drift, and failed to find next L1 origin: %w", currentOrigin, err) + // Try to find the L1 origin given the current data in cache + o, err := los.findL1OriginOfNextL2Block( + l2Head, + currentOrigin, + nextOrigin, + recoverMode) + + // If the cache doesn't have the next origin, but we now + // know we definitely need it, fetch it and try again. + if errors.Is(err, ErrNextL1OriginRequired) { + nextOrigin, err = los.fetch(ctx, currentOrigin.Number+1) + if err == nil || (recoverMode && errors.Is(err, ethereum.NotFound)) { + // If we got the origin, or we are in recover mode and the origin is not found + // (because we recovered the l1 origin up to the l1 tip) + // try again with matchAutoDerivation = false. + return los.findL1OriginOfNextL2Block( + l2Head, + currentOrigin, + nextOrigin, + false) + } else { + return eth.L1BlockRef{}, ErrNextL1OriginRequired } } - - // If the next origin is ahead of the L2 head, we must return the current origin. - if l2Head.Time+los.cfg.BlockTime < nextOrigin.Time { - return currentOrigin, nil - } - - return nextOrigin, nil + return o, err } +// CurrentAndNextOrigin returns the current cached values for the current L1 origin for the supplied l2Head, and its successor. +// It only performs a fetch to L1 if the cache is invalid. +// The cache can be updated asynchronously by other methods on L1OriginSelector. +// The returned currentOrigin should _always_ be non-empty, because it is populated from l2Head whose +// l1Origin is first specified in the rollup.Config.Genesis.L1 and progressed to non-empty values thereafter. func (los *L1OriginSelector) CurrentAndNextOrigin(ctx context.Context, l2Head eth.L2BlockRef) (eth.L1BlockRef, eth.L1BlockRef, error) { los.mu.Lock() defer los.mu.Unlock() - if los.recoverMode.Load() { - currentOrigin, err := los.l1.L1BlockRefByHash(ctx, l2Head.L1Origin.Hash) - if err != nil { - return eth.L1BlockRef{}, eth.L1BlockRef{}, - derive.NewTemporaryError(fmt.Errorf("failed to fetch current L1 origin: %w", err)) - } - los.currentOrigin = currentOrigin - nextOrigin, err := los.l1.L1BlockRefByNumber(ctx, currentOrigin.Number+1) - if err != nil { - return eth.L1BlockRef{}, eth.L1BlockRef{}, - derive.NewTemporaryError(fmt.Errorf("failed to fetch next L1 origin: %w", err)) - } - los.nextOrigin = nextOrigin - los.log.Info("origin selector in recover mode", "current_origin", los.currentOrigin, "next_origin", los.nextOrigin, "l2_head", l2Head) - return los.currentOrigin, los.nextOrigin, nil - } - if l2Head.L1Origin == los.currentOrigin.ID() { // Most likely outcome: the L2 head is still on the current origin. } else if l2Head.L1Origin == los.nextOrigin.ID() { @@ -238,3 +209,69 @@ func (los *L1OriginSelector) reset() { los.currentOrigin = eth.L1BlockRef{} los.nextOrigin = eth.L1BlockRef{} } + +var ( + ErrInvalidL1Origin = fmt.Errorf("origin-selector: currentL1Origin.Hash != l2Head.L1Origin.Hash") + ErrNextL1OriginOrphaned = fmt.Errorf("origin-selector: nextL1Origin.ParentHash != currentL1Origin.Hash") + ErrNextL1OriginRequired = fmt.Errorf("origin-selector: nextL1Origin not supplied but required to satisfy constraints") +) + +// FindL1OriginOfNextL2Block finds the L1 origin of the next L2 block. +// It returns an error if there is no way to build a block satisfying +// derivation constraints with the supplied data. +// You can pass an empty nextL1Origin if it is not yet available +// removing the need for block building to wait on the result of network calls. +// This method is designed to be pure (it only reads the cfg property of the receiver) +// and should not have any side effects. +func (los *L1OriginSelector) findL1OriginOfNextL2Block( + l2Head eth.L2BlockRef, + currentL1Origin eth.L1BlockRef, nextL1Origin eth.L1BlockRef, + matchAutoDerivation bool) (eth.L1BlockRef, error) { + + if (currentL1Origin == eth.L1BlockRef{}) { + // This would indicate a programming error, since the currentL1Origin + // should _always_ be available. + // The first value (for block 1) is specified in rollup.Config.Genesis.L1 + // and it is then only updated to non-empty values. + panic("origin-selector: currentL1Origin is empty") + } + if l2Head.L1Origin.Hash != currentL1Origin.Hash { + return currentL1Origin, ErrInvalidL1Origin + } + if (nextL1Origin != eth.L1BlockRef{} && nextL1Origin.ParentHash != currentL1Origin.Hash) { + return nextL1Origin, ErrNextL1OriginOrphaned + } + + l2BlockTime := los.cfg.BlockTime + maxDrift := rollup.NewChainSpec(los.cfg).MaxSequencerDrift(currentL1Origin.Time) + nextL2BlockTime := l2Head.Time + l2BlockTime + driftCurrent := int64(nextL2BlockTime) - int64(currentL1Origin.Time) + + if (nextL1Origin == eth.L1BlockRef{}) { + if matchAutoDerivation { + // See https://github.com/ethereum-optimism/optimism/blob/ce9fa62d0c0325304fc37d91d87aa2e16a7f8356/op-node/rollup/derive/base_batch_stage.go#L186-L205 + // We need the next L1 origin to decide whether we can eagerly adopt it. + // NOTE: This can cause unsafe block production to slow to the rate of L1 block production, if the L1 origin is caught up to the L1 Head. + // Code higher up the call stack should ensure that matchAutoDerivation is false under such conditions. + return eth.L1BlockRef{}, ErrNextL1OriginRequired + } else { + // If we don't yet have the nextL1Origin, stick with the current L1 origin unless doing so would exceed the maximum drift. + if driftCurrent > int64(maxDrift) { + // Return an error so the caller knows it needs to fetch the next l1 origin now. + return eth.L1BlockRef{}, fmt.Errorf("%w: drift of next L2 block would exceed maximum %d unless nextl1Origin is adopted", ErrNextL1OriginRequired, maxDrift) + } + return currentL1Origin, nil + } + } + + driftNext := int64(nextL2BlockTime) - int64(nextL1Origin.Time) + + // Progress to l1OriginChild if doing so would respect the requirement + // that L2 blocks cannot point to a future L1 block (negative drift). + if driftNext >= 0 { + return nextL1Origin, nil + } else { + // If we cannot adopt the l1OriginChild, use the current l1 origin. + return currentL1Origin, nil + } +} diff --git a/op-node/rollup/sequencing/origin_selector_test.go b/op-node/rollup/sequencing/origin_selector_test.go index be8a0dc7625..5ab947df69f 100644 --- a/op-node/rollup/sequencing/origin_selector_test.go +++ b/op-node/rollup/sequencing/origin_selector_test.go @@ -7,7 +7,6 @@ import ( "github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-node/rollup/confdepth" - "github.com/ethereum-optimism/optimism/op-node/rollup/derive" "github.com/ethereum-optimism/optimism/op-node/rollup/engine" "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-service/testlog" @@ -208,18 +207,14 @@ func TestOriginSelectorAdvances(t *testing.T) { } if recoverMode { - // In recovery mode (only) we make two RPC calls. + // In recovery mode (only) we make an RPC call to find the next origin. // First, cover the case where the nextOrigin // is not ready yet by simulating a NotFound error. - l1.ExpectL1BlockRefByHash(c.Hash, c, nil) l1.ExpectL1BlockRefByNumber(d.Number, eth.BlockRef{}, ethereum.NotFound) - _, err := s.FindL1Origin(ctx, l2Head) - require.ErrorIs(t, err, derive.ErrTemporary) - require.ErrorIs(t, err, ethereum.NotFound) + requireL1OriginAt(l2Head, c) // Now, simulate the block being ready, and ensure // that the origin advances to the next block. - l1.ExpectL1BlockRefByHash(c.Hash, c, nil) l1.ExpectL1BlockRefByNumber(d.Number, d, nil) requireL1OriginAt(l2Head, d) } else { @@ -344,7 +339,7 @@ func TestOriginSelectorFetchesNextOrigin(t *testing.T) { // // There are 3 blocks [a, b, c]. After advancing to b, a reorg is simulated // where b is reorged and replaced by providing a `c` next that has a different parent hash. -// The origin should still provide c as the next origin so upstream services can detect the reorg. +// A sentinel error should be returned. func TestOriginSelectorHandlesReorg(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -384,6 +379,11 @@ func TestOriginSelectorHandlesReorg(t *testing.T) { require.Equal(t, l1ref, next) } + requireFindL1OriginError := func(e error) { + _, err := s.FindL1Origin(ctx, l2Head) + require.ErrorIs(t, err, e) + } + requireFindl1OriginEqual(a) // Selection is stable until the next origin is fetched @@ -413,9 +413,8 @@ func TestOriginSelectorHandlesReorg(t *testing.T) { handled = s.OnEvent(context.Background(), engine.ForkchoiceUpdateEvent{UnsafeL2Head: l2Head}) require.True(t, handled) - // The next origin should be `c` now, otherwise an upstream service cannot detect the reorg - // and the origin will be stuck at `b` - requireFindl1OriginEqual(c) + // We shuold get a sentinel error + requireFindL1OriginError(ErrNextL1OriginOrphaned) } // TestOriginSelectorRespectsOriginTiming ensures that the origin selector @@ -593,7 +592,7 @@ func TestOriginSelectorStrictConfDepth(t *testing.T) { s := NewL1OriginSelector(ctx, log, cfg, confDepthL1) _, err := s.FindL1Origin(ctx, l2Head) - require.ErrorContains(t, err, "sequencer time drift") + require.ErrorIs(t, err, ErrNextL1OriginRequired) } func u64ptr(n uint64) *uint64 { @@ -779,11 +778,11 @@ func TestOriginSelectorHandlesLateL1Blocks(t *testing.T) { s := NewL1OriginSelector(ctx, log, cfg, confDepthL1) _, err := s.FindL1Origin(ctx, l2Head) - require.ErrorContains(t, err, "sequencer time drift") + require.ErrorIs(t, err, ErrNextL1OriginRequired) l1Head = c _, err = s.FindL1Origin(ctx, l2Head) - require.ErrorContains(t, err, "sequencer time drift") + require.ErrorIs(t, err, ErrNextL1OriginRequired) l1Head = d next, err := s.FindL1Origin(ctx, l2Head) @@ -811,3 +810,196 @@ func TestOriginSelectorMiscEvent(t *testing.T) { handled := s.OnEvent(context.Background(), rollup.L1TemporaryErrorEvent{}) require.False(t, handled) } + +func TestFindL1OriginOfNextL2Block(t *testing.T) { + cfg := &rollup.Config{ + MaxSequencerDrift: 1800, // Use Fjord constant value + BlockTime: 2, + } + + los := NewL1OriginSelector(context.Background(), testlog.Logger(t, log.LevelDebug), cfg, &testutils.MockL1Source{}) + + require.Panics(t, func() { + _, _ = los.findL1OriginOfNextL2Block( + eth.L2BlockRef{}, + eth.L1BlockRef{}, + eth.L1BlockRef{}, + false) + }) + + type testCase struct { + name string + l2Head eth.L2BlockRef + currentL1Origin eth.L1BlockRef + nextL1Origin eth.L1BlockRef + matchAutoderivation bool + expectedResult eth.L1BlockRef + expectedError error + } + + tcs := []testCase{} + + // Scenarios with valid data, no drift concerns + // but availability of next l1 origin is modulated. + // + // L1 chain: a100(1200) <- [ a101(1212) ] + // /\ + // L2 chain \_ b1000(1220) + a100 := eth.L1BlockRef{ + Number: 100, + Hash: common.Hash{'a', '1', '0', '0'}, + Time: 1200, + } + a101 := eth.L1BlockRef{ + Number: 101, + ParentHash: a100.Hash, + Hash: common.Hash{'a', '0', '0'}, + Time: 1212, + } + b1000 := eth.L2BlockRef{ + Number: 1000, + Hash: common.Hash{'b', '1', '0', '0', '0'}, + L1Origin: a100.ID(), + Time: 1220, + } + + tcs = append(tcs, + testCase{ + name: "normal operation, progress because we can", + l2Head: b1000, + currentL1Origin: a100, + nextL1Origin: a101, + expectedResult: a101, + }, + testCase{ + name: "recover mode, progress because we can", + l2Head: b1000, + currentL1Origin: a100, + nextL1Origin: a101, + expectedResult: a101, + matchAutoderivation: true, + }, + testCase{ + name: "normal operation, don't need to progress", + l2Head: b1000, + currentL1Origin: a100, + expectedResult: a100, + }, + testCase{ + name: "recover mode, need to progress but can't", + l2Head: b1000, + currentL1Origin: a100, + matchAutoderivation: true, + expectedError: ErrNextL1OriginRequired, + }, + ) + + // Bad input data / reorg scenarios + // L1 chain: c100(1200) <-[x]- c101(1212) + // /\ + // L2 chain \_[x]- d/e1000(1220) + c100 := eth.L1BlockRef{ + Number: 100, + Hash: common.Hash{'a', '1', '0', '0'}, + Time: 1200, + } + c101 := eth.L1BlockRef{ + Number: 101, + ParentHash: common.Hash{}, // does not point to c100 + Hash: common.Hash{'a', '0', '0'}, + Time: 1212, + } + d1000 := eth.L2BlockRef{ + Number: 1000, + L1Origin: c100.ID(), + Hash: common.Hash{'d', '1', '0', '0', '0'}, + Time: 1220, + } + e1000 := eth.L2BlockRef{ + Number: 1000, + L1Origin: eth.BlockID{}, // does not point to c100 + Hash: common.Hash{'d', '1', '0', '0', '0'}, + Time: 1220, + } + tcs = append(tcs, + testCase{ + name: "L1 reorg", + currentL1Origin: c100, + nextL1Origin: c101, + l2Head: d1000, + expectedResult: c101, + expectedError: ErrNextL1OriginOrphaned, + }, + testCase{ + name: "Invalid l1 origin", + currentL1Origin: c100, + nextL1Origin: c101, + l2Head: e1000, + expectedResult: c100, + expectedError: ErrInvalidL1Origin, + }) + + // Drift at maximum, + // L1 chain: a100(1200) <- [ a101(1212) ] + // /\ + // L2 chain \_ f1000(3000) + f1000 := eth.L2BlockRef{ + Number: 1000, + L1Origin: a100.ID(), + Hash: common.Hash{'f', '1', '0', '0', '0'}, + Time: 3000, + } + tcs = append(tcs, + testCase{ + name: "Drift at maximum, nextL1Origin available", + currentL1Origin: a100, + nextL1Origin: a101, + l2Head: f1000, + expectedResult: a101, + }, + testCase{ + name: "Drift at maximum, nextL1Origin unavailable", + currentL1Origin: a100, + l2Head: f1000, + expectedError: ErrNextL1OriginRequired, + }) + + // Negative drift, + // L1 chain: a100(1200) <- a101(1212) + // /\ + // L2 chain \_ g1000(1200) + // Current drift is 0 + // adopting the nextLOrigin would make it negative (add 2 subtract 12) + g1000 := eth.L2BlockRef{ + Number: 1000, + L1Origin: a100.ID(), + Hash: common.Hash{'g', '1', '0', '0', '0'}, + Time: 1200, + } + tcs = append(tcs, + testCase{ + name: "Negative drift", + currentL1Origin: a100, + nextL1Origin: a101, + l2Head: g1000, + expectedResult: a100, + }) + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + result, err := los.findL1OriginOfNextL2Block( + tc.l2Head, + tc.currentL1Origin, + tc.nextL1Origin, + tc.matchAutoderivation) + if tc.expectedError != nil { + require.ErrorIs(t, err, tc.expectedError) + } else { + require.NoError(t, err) + } + if result != tc.expectedResult { + t.Errorf("expected result %v, got %v", tc.expectedResult, result) + } + }) + } +} diff --git a/op-node/rollup/sequencing/sequencer.go b/op-node/rollup/sequencing/sequencer.go index 9e2b348c93b..76e743af1dd 100644 --- a/op-node/rollup/sequencing/sequencer.go +++ b/op-node/rollup/sequencing/sequencer.go @@ -503,21 +503,23 @@ func (d *Sequencer) startBuildingBlock() { // Figure out which L1 origin block we're going to be building on top of. l1Origin, err := d.l1OriginSelector.FindL1Origin(ctx, l2Head) - if err != nil { - d.nextAction = d.timeNow().Add(time.Second) - d.nextActionOK = d.active.Load() - d.log.Error("Error finding next L1 Origin", "err", err) - d.emitter.Emit(d.ctx, rollup.L1TemporaryErrorEvent{Err: err}) - return - } - - if !(l2Head.L1Origin.Hash == l1Origin.ParentHash || l2Head.L1Origin.Hash == l1Origin.Hash) { + switch { + case err == nil: + case errors.Is(err, ErrInvalidL1Origin), errors.Is(err, ErrNextL1OriginOrphaned): d.metrics.RecordSequencerInconsistentL1Origin(l2Head.L1Origin, l1Origin.ID()) d.emitter.Emit(d.ctx, rollup.ResetEvent{ Err: fmt.Errorf("cannot build new L2 block with L1 origin %s (parent L1 %s) on current L2 head %s with L1 origin %s", l1Origin, l1Origin.ParentHash, l2Head, l2Head.L1Origin), }) return + case errors.Is(err, ErrNextL1OriginRequired): + fallthrough + default: + d.nextAction = d.timeNow().Add(time.Second) + d.nextActionOK = d.active.Load() + d.log.Error("Error finding next L1 Origin", "err", err) + d.emitter.Emit(d.ctx, rollup.L1TemporaryErrorEvent{Err: err}) + return } d.log.Info("Started sequencing new block", "parent", l2Head, "l1Origin", l1Origin) diff --git a/op-program/scripts/build-prestates.sh b/op-program/scripts/build-prestates.sh index 5d765796ced..882f551d5a8 100755 --- a/op-program/scripts/build-prestates.sh +++ b/op-program/scripts/build-prestates.sh @@ -129,10 +129,17 @@ EOF VERSIONS_JSON="[]" readarray -t VERSIONS < <(git tag --list 'op-program/v*' --sort taggerdate) -for VERSION in "${VERSIONS[@]}"; do +for i in "${!VERSIONS[@]}"; do pushd . - build_op_program_prestate "${VERSION}" + build_op_program_prestate "${VERSIONS[i]}" popd + # after every 10 builds, cleanup docker artifacts to reclaim disk space + if [ "$CIRCLECI" = "true" ]; then + if (( (i + 1) % 10 == 0 )); then + echo "Pruning docker build artifacts after ${i} builds" + docker system prune -f + fi + fi done echo "${VERSIONS_JSON}" > "${VERSIONS_FILE}" @@ -151,10 +158,17 @@ readarray -t KONA_VERSIONS < <(git ls-remote --tags "$KONA_REPO_URL" | grep kona | sed 's|.*refs/tags/||' | sed 's/\^{}//' | sort -u \ | grep -v beta | grep -v alpha | grep -v -F -f excluded.txt) -for VERSION in "${KONA_VERSIONS[@]}"; do +for i in "${!KONA_VERSIONS[@]}"; do pushd . - build_kona_prestate "${VERSION}" + build_kona_prestate "${KONA_VERSIONS[i]}" popd + # after every 10 builds, cleanup docker artifacts to reclaim disk space + if [ "$CIRCLECI" = "true" ]; then + if (( (i + 1) % 10 == 0 )); then + echo "Pruning docker build artifacts after ${i} builds" + docker system prune -f + fi + fi done echo "${VERSIONS_JSON}" > "${VERSIONS_FILE}" diff --git a/op-proposer/contracts/disputegamefactory.go b/op-proposer/contracts/disputegamefactory.go index 838887209e1..527d0e50160 100644 --- a/op-proposer/contracts/disputegamefactory.go +++ b/op-proposer/contracts/disputegamefactory.go @@ -94,7 +94,7 @@ func (f *DisputeGameFactory) HasProposedSince(ctx context.Context, proposer comm } } -func (f *DisputeGameFactory) ProposalTx(ctx context.Context, gameType uint32, outputRoot common.Hash, l2BlockNum uint64) (txmgr.TxCandidate, error) { +func (f *DisputeGameFactory) ProposalTx(ctx context.Context, gameType uint32, outputRoot common.Hash, extraData []byte) (txmgr.TxCandidate, error) { cCtx, cancel := context.WithTimeout(ctx, f.networkTimeout) defer cancel() result, err := f.caller.SingleCall(cCtx, rpcblock.Latest, f.contract.Call(methodInitBonds, gameType)) @@ -102,7 +102,7 @@ func (f *DisputeGameFactory) ProposalTx(ctx context.Context, gameType uint32, ou return txmgr.TxCandidate{}, fmt.Errorf("failed to fetch init bond: %w", err) } initBond := result.GetBigInt(0) - call := f.contract.Call(methodCreateGame, gameType, outputRoot, common.BigToHash(big.NewInt(int64(l2BlockNum))).Bytes()) + call := f.contract.Call(methodCreateGame, gameType, outputRoot, extraData) candidate, err := call.ToTxCandidate() if err != nil { return txmgr.TxCandidate{}, err diff --git a/op-proposer/contracts/disputegamefactory_test.go b/op-proposer/contracts/disputegamefactory_test.go index 84c7dae1a98..dfd89a10900 100644 --- a/op-proposer/contracts/disputegamefactory_test.go +++ b/op-proposer/contracts/disputegamefactory_test.go @@ -210,7 +210,7 @@ func TestProposalTx(t *testing.T) { bond := big.NewInt(49284294829) stubRpc.SetResponse(factoryAddr, methodInitBonds, rpcblock.Latest, []interface{}{gameType}, []interface{}{bond}) stubRpc.SetResponse(factoryAddr, methodCreateGame, rpcblock.Latest, []interface{}{gameType, outputRoot, l2BlockNum}, nil) - tx, err := factory.ProposalTx(context.Background(), gameType, outputRoot, uint64(456)) + tx, err := factory.ProposalTx(context.Background(), gameType, outputRoot, l2BlockNum) require.NoError(t, err) stubRpc.VerifyTxCandidate(tx) require.NotNil(t, tx.Value) diff --git a/op-proposer/proposer/driver.go b/op-proposer/proposer/driver.go index 1e5e96bf39c..cb5a5757e88 100644 --- a/op-proposer/proposer/driver.go +++ b/op-proposer/proposer/driver.go @@ -48,7 +48,7 @@ type L2OOContract interface { type DGFContract interface { Version(ctx context.Context) (string, error) HasProposedSince(ctx context.Context, proposer common.Address, cutoff time.Time, gameType uint32) (bool, time.Time, common.Hash, error) - ProposalTx(ctx context.Context, gameType uint32, outputRoot common.Hash, l2BlockNum uint64) (txmgr.TxCandidate, error) + ProposalTx(ctx context.Context, gameType uint32, outputRoot common.Hash, extraData []byte) (txmgr.TxCandidate, error) } type RollupClient interface { @@ -237,21 +237,24 @@ func (l *L2OutputSubmitter) FetchL2OOOutput(ctx context.Context) (source.Proposa return source.Proposal{}, false, nil } - output, err := l.FetchOutput(ctx, nextCheckpointBlock) + proposal, err := l.FetchOutput(ctx, nextCheckpointBlock) if err != nil { return source.Proposal{}, false, fmt.Errorf("fetching output: %w", err) } + if proposal.IsSuperRootProposal() { + panic("L2 Output Submitter should not be configured for Super Root proposals") + } // Always propose if it's part of the Finalized L2 chain. Or if allowed, if it's part of the safe L2 chain. - if output.SequenceNum > output.Legacy.FinalizedL2.Number && (!l.Cfg.AllowNonFinalized || output.SequenceNum > output.Legacy.SafeL2.Number) { + if proposal.SequenceNum > proposal.Legacy.FinalizedL2.Number && (!l.Cfg.AllowNonFinalized || proposal.SequenceNum > proposal.Legacy.SafeL2.Number) { l.Log.Debug("Not proposing yet, L2 block is not ready for proposal", - "l2_proposal", output.SequenceNum, - "l2_safe", output.Legacy.SafeL2, - "l2_finalized", output.Legacy.FinalizedL2, + "l2_proposal", proposal.SequenceNum, + "l2_safe", proposal.Legacy.SafeL2, + "l2_finalized", proposal.Legacy.FinalizedL2, "allow_non_finalized", l.Cfg.AllowNonFinalized) - return output, false, nil + return proposal, false, nil } - return output, true, nil + return proposal, true, nil } // FetchDGFOutput queries the DGF for the latest game and infers whether it is time to make another proposal @@ -313,14 +316,14 @@ func (l *L2OutputSubmitter) FetchCurrentBlockNumber(ctx context.Context) (uint64 } func (l *L2OutputSubmitter) FetchOutput(ctx context.Context, block uint64) (source.Proposal, error) { - output, err := l.ProposalSource.ProposalAtSequenceNum(ctx, block) + proposal, err := l.ProposalSource.ProposalAtSequenceNum(ctx, block) if err != nil { - return source.Proposal{}, fmt.Errorf("fetching output at block %d: %w", block, err) + return source.Proposal{}, fmt.Errorf("fetching proposal at block %d: %w", block, err) } - if onum := output.SequenceNum; onum != block { // sanity check, e.g. in case of bad RPC caching - return source.Proposal{}, fmt.Errorf("output block number %d mismatches requested %d", output.SequenceNum, block) + if onum := proposal.SequenceNum; onum != block && !proposal.IsSuperRootProposal() { // sanity check, e.g. in case of bad RPC caching + return source.Proposal{}, fmt.Errorf("proposal block number %d mismatches requested %d", proposal.SequenceNum, block) } - return output, nil + return proposal, nil } // ProposeL2OutputTxData creates the transaction data for the ProposeL2Output function @@ -341,7 +344,7 @@ func proposeL2OutputTxData(abi *abi.ABI, output source.Proposal) ([]byte, error) func (l *L2OutputSubmitter) ProposeL2OutputDGFTxCandidate(ctx context.Context, output source.Proposal) (txmgr.TxCandidate, error) { cCtx, cancel := context.WithTimeout(ctx, l.Cfg.NetworkTimeout) defer cancel() - return l.dgfContract.ProposalTx(cCtx, l.Cfg.DisputeGameType, output.Root, output.SequenceNum) + return l.dgfContract.ProposalTx(cCtx, l.Cfg.DisputeGameType, output.Root, output.ExtraData()) } // We wait until l1head advances beyond blocknum. This is used to make sure proposal tx won't @@ -374,7 +377,7 @@ func (l *L2OutputSubmitter) waitForL1Head(ctx context.Context, blockNum uint64) // sendTransaction creates & sends transactions through the underlying transaction manager. func (l *L2OutputSubmitter) sendTransaction(ctx context.Context, output source.Proposal) error { - l.Log.Info("Proposing output root", "output", output.Root, "block", output.SequenceNum) + l.Log.Info("Proposing output root", "output", output.Root, "sequenceNum", output.SequenceNum, "extraData", output.ExtraData()) var receipt *types.Receipt if l.Cfg.DisputeGameFactoryAddr != nil { candidate, err := l.ProposeL2OutputDGFTxCandidate(ctx, output) diff --git a/op-proposer/proposer/driver_test.go b/op-proposer/proposer/driver_test.go index 1e34910d0af..58e49e002a0 100644 --- a/op-proposer/proposer/driver_test.go +++ b/op-proposer/proposer/driver_test.go @@ -48,7 +48,7 @@ func (m *StubDGFContract) HasProposedSince(_ context.Context, _ common.Address, return false, time.Unix(1000, 0), common.Hash{0xdd}, nil } -func (m *StubDGFContract) ProposalTx(_ context.Context, _ uint32, _ common.Hash, _ uint64) (txmgr.TxCandidate, error) { +func (m *StubDGFContract) ProposalTx(_ context.Context, _ uint32, _ common.Hash, _ []byte) (txmgr.TxCandidate, error) { panic("not implemented") } diff --git a/op-proposer/proposer/source/source.go b/op-proposer/proposer/source/source.go index 2c4e5e4d810..0aae82ce47e 100644 --- a/op-proposer/proposer/source/source.go +++ b/op-proposer/proposer/source/source.go @@ -2,6 +2,7 @@ package source import ( "context" + "encoding/binary" "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum/go-ethereum/common" @@ -10,17 +11,36 @@ import ( type Proposal struct { // Root is the proposal hash Root common.Hash - // SequenceNum identifies the position in the overall state transition. - // For output roots this is the L2 block number. - // For super roots this is the timestamp. + + // SequenceNum represents the L2 Block number or Super Root L2 timestamp SequenceNum uint64 - CurrentL1 eth.BlockID + + // Super is present if, and only if, this Proposal is a Super Root proposal + Super eth.Super + + CurrentL1 eth.BlockID // Legacy provides data that is only available when retrieving data from a single rollup node. // It should only be used for L2OO proposals. Legacy LegacyProposalData } +// IsSuperRootProposal returns true if the proposal is a Super Root proposal. +func (p *Proposal) IsSuperRootProposal() bool { + return p.Super != nil +} + +// ExtraData returns the Dispute Game extra data as appropriate for the proposal type. +func (p *Proposal) ExtraData() []byte { + if p.Super != nil { + return p.Super.Marshal() + } else { + var extraData [32]byte + binary.BigEndian.PutUint64(extraData[24:], p.SequenceNum) + return extraData[:] + } +} + type LegacyProposalData struct { HeadL1 eth.L1BlockRef SafeL2 eth.L2BlockRef diff --git a/op-proposer/proposer/source/source_supervisor.go b/op-proposer/proposer/source/source_supervisor.go index 7253c03b37a..508dc19617b 100644 --- a/op-proposer/proposer/source/source_supervisor.go +++ b/op-proposer/proposer/source/source_supervisor.go @@ -95,9 +95,23 @@ func (s *SupervisorProposalSource) ProposalAtSequenceNum(ctx context.Context, ti s.log.Warn("Failed to retrieve proposal from supervisor", "idx", i, "err", err) continue } + + super, err := output.ToSuper() + if err != nil { + errs = append(errs, err) + s.log.Warn("Failed to construct super from supervisor", "idx", i, "err", err) + continue + } + if ver := super.Version(); ver != eth.SuperRootVersionV1 { + errs = append(errs, fmt.Errorf("unsupported super type %d from supervisor idx %d", ver, i)) + s.log.Warn("Unsupported super type from supervisor", "idx", i) + continue + } + return Proposal{ Root: common.Hash(output.SuperRoot), SequenceNum: output.Timestamp, + Super: super, CurrentL1: output.CrossSafeDerivedFrom, // Unsupported by super root proposals diff --git a/op-proposer/proposer/source/source_supervisor_test.go b/op-proposer/proposer/source/source_supervisor_test.go index 75c7bbf817b..6c77a513d05 100644 --- a/op-proposer/proposer/source/source_supervisor_test.go +++ b/op-proposer/proposer/source/source_supervisor_test.go @@ -232,14 +232,17 @@ func TestSupervisorSource_ProposalAtSequenceNum(t *testing.T) { }, Timestamp: 59298244, SuperRoot: eth.Bytes32{0xaa, 0xbb}, - Version: 3, + Version: 1, Chains: nil, } + responseSuper, err := response.ToSuper() + require.NoError(t, err) expected := Proposal{ Root: common.Hash(response.SuperRoot), - SequenceNum: response.Timestamp, + SequenceNum: 59298244, CurrentL1: response.CrossSafeDerivedFrom, Legacy: LegacyProposalData{}, + Super: responseSuper, } sequenceNum := uint64(599) t.Run("Single-Success", func(t *testing.T) { diff --git a/packages/contracts-bedrock/foundry.toml b/packages/contracts-bedrock/foundry.toml index b27dbe8207b..ad3b0e163ea 100644 --- a/packages/contracts-bedrock/foundry.toml +++ b/packages/contracts-bedrock/foundry.toml @@ -25,7 +25,8 @@ compilation_restrictions = [ { paths = "src/dispute/v2/FaultDisputeGameV2.sol", optimizer_runs = 5000 }, { paths = "src/dispute/PermissionedDisputeGame.sol", optimizer_runs = 5000 }, { paths = "src/dispute/v2/PermissionedDisputeGameV2.sol", optimizer_runs = 5000 }, - { paths = "src/dispute/zk/OPSuccinctFaultDisputeGame.sol", optimizer_runs = 5000 }, + { paths = "src/dispute/SuperFaultDisputeGame.sol", optimizer_runs = 5000 }, + { paths = "src/dispute/SuperPermissionedDisputeGame.sol", optimizer_runs = 5000 }, { paths = "src/L1/OPContractsManager.sol", optimizer_runs = 5000 }, { paths = "src/L1/OPContractsManagerStandardValidator.sol", optimizer_runs = 5000 }, { paths = "src/L1/opcm/OPContractsManagerV2.sol", optimizer_runs = 5000 }, @@ -155,7 +156,8 @@ compilation_restrictions = [ { paths = "src/dispute/v2/FaultDisputeGameV2.sol", optimizer_runs = 0 }, { paths = "src/dispute/PermissionedDisputeGame.sol", optimizer_runs = 0 }, { paths = "src/dispute/v2/PermissionedDisputeGameV2.sol", optimizer_runs = 0 }, - { paths = "src/dispute/zk/OPSuccinctFaultDisputeGame.sol", optimizer_runs = 0 }, + { paths = "src/dispute/SuperFaultDisputeGame.sol", optimizer_runs = 0 }, + { paths = "src/dispute/SuperPermissionedDisputeGame.sol", optimizer_runs = 0 }, { paths = "src/L1/OPContractsManager.sol", optimizer_runs = 0 }, { paths = "src/L1/OPContractsManagerStandardValidator.sol", optimizer_runs = 0 }, { paths = "src/L1/opcm/OPContractsManagerV2.sol", optimizer_runs = 0 }, diff --git a/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol b/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol index 48c7ab9c6b5..c3a799ecb0e 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol @@ -300,6 +300,8 @@ interface IOPContractsManager { error InvalidDevFeatureAccess(bytes32 devFeature); + error OPContractsManager_V2Enabled(); + // -------- Methods -------- function __constructor__( diff --git a/packages/contracts-bedrock/interfaces/L1/opcm/IOPContractsManagerUtilsCaller.sol b/packages/contracts-bedrock/interfaces/L1/opcm/IOPContractsManagerUtilsCaller.sol index f061d52afdc..05bb1010c97 100644 --- a/packages/contracts-bedrock/interfaces/L1/opcm/IOPContractsManagerUtilsCaller.sol +++ b/packages/contracts-bedrock/interfaces/L1/opcm/IOPContractsManagerUtilsCaller.sol @@ -5,5 +5,5 @@ import { IOPContractsManagerUtils } from "interfaces/L1/opcm/IOPContractsManager interface IOPContractsManagerUtilsCaller { function __constructor__(IOPContractsManagerUtils _utils) external; - function utils() external view returns (IOPContractsManagerUtils); + function opcmUtils() external view returns (IOPContractsManagerUtils); } diff --git a/packages/contracts-bedrock/interfaces/L1/opcm/IOPContractsManagerV2.sol b/packages/contracts-bedrock/interfaces/L1/opcm/IOPContractsManagerV2.sol index 9dcc891cc99..94e5ad3ff75 100644 --- a/packages/contracts-bedrock/interfaces/L1/opcm/IOPContractsManagerV2.sol +++ b/packages/contracts-bedrock/interfaces/L1/opcm/IOPContractsManagerV2.sol @@ -130,11 +130,11 @@ interface IOPContractsManagerV2 { function contractsContainer() external view returns (IOPContractsManagerContainer); - function standardValidator() external view returns (IOPContractsManagerStandardValidator); + function opcmStandardValidator() external view returns (IOPContractsManagerStandardValidator); - function thisOPCM() external view returns (IOPContractsManagerV2); + function opcmV2() external view returns (IOPContractsManagerV2); - function utils() external view returns (IOPContractsManagerUtils); + function opcmUtils() external view returns (IOPContractsManagerUtils); function version() external view returns (string memory); @@ -152,4 +152,7 @@ interface IOPContractsManagerV2 { /// @notice Checks if the upgrade sequence from the last used OPCM to this OPCM is permitted. function isPermittedUpgradeSequence(ISystemConfig _systemConfig) external view returns (bool); + + /// @notice Returns the development feature bitmap. + function devFeatureBitmap() external view returns (bytes32); } diff --git a/packages/contracts-bedrock/interfaces/dispute/ISuperFaultDisputeGame.sol b/packages/contracts-bedrock/interfaces/dispute/ISuperFaultDisputeGame.sol index b9633919fce..40f7b0e87ca 100644 --- a/packages/contracts-bedrock/interfaces/dispute/ISuperFaultDisputeGame.sol +++ b/packages/contracts-bedrock/interfaces/dispute/ISuperFaultDisputeGame.sol @@ -64,6 +64,10 @@ interface ISuperFaultDisputeGame is IDisputeGame { error GameNotFinalized(); error GameNotResolved(); error GamePaused(); + error UnknownChainId(); + error Encoding_EmptySuperRoot(); + error Encoding_InvalidSuperRootVersion(); + error Encoding_InvalidSuperRootEncoding(); event Move(uint256 indexed parentIndex, Claim indexed claim, address indexed claimant); event GameClosed(BondDistributionMode bondDistributionMode); @@ -117,6 +121,8 @@ interface ISuperFaultDisputeGame is IDisputeGame { function vm() external view returns (IBigStepper vm_); function wasRespectedGameTypeWhenCreated() external view returns (bool); function weth() external view returns (IDelayedWETH weth_); + // TODO(#18516): Remove once IDisputeGame includes this interface + function rootClaimByChainId(uint256 _chainId) external view returns (Claim outputRootClaim_); function __constructor__(GameConstructorParams memory _params) external; } diff --git a/packages/contracts-bedrock/interfaces/dispute/ISuperPermissionedDisputeGame.sol b/packages/contracts-bedrock/interfaces/dispute/ISuperPermissionedDisputeGame.sol index edaf24d5244..06e9d2cbd25 100644 --- a/packages/contracts-bedrock/interfaces/dispute/ISuperPermissionedDisputeGame.sol +++ b/packages/contracts-bedrock/interfaces/dispute/ISuperPermissionedDisputeGame.sol @@ -59,6 +59,10 @@ interface ISuperPermissionedDisputeGame is IDisputeGame { error GameNotResolved(); error BadAuth(); error GamePaused(); + error UnknownChainId(); + error Encoding_EmptySuperRoot(); + error Encoding_InvalidSuperRootVersion(); + error Encoding_InvalidSuperRootEncoding(); event Move(uint256 indexed parentIndex, Claim indexed claim, address indexed claimant); event GameClosed(BondDistributionMode bondDistributionMode); @@ -115,6 +119,8 @@ interface ISuperPermissionedDisputeGame is IDisputeGame { function vm() external view returns (IBigStepper vm_); function wasRespectedGameTypeWhenCreated() external view returns (bool); function weth() external view returns (IDelayedWETH weth_); + // TODO(#18516): Remove once IDisputeGame includes this interface + function rootClaimByChainId(uint256 _chainId) external view returns (Claim outputRootClaim_); function __constructor__(ISuperFaultDisputeGame.GameConstructorParams memory _params) external; } diff --git a/packages/contracts-bedrock/interfaces/dispute/zk/IOPSuccinctFaultDisputeGame.sol b/packages/contracts-bedrock/interfaces/dispute/zk/IOptimisticZkGame.sol similarity index 96% rename from packages/contracts-bedrock/interfaces/dispute/zk/IOPSuccinctFaultDisputeGame.sol rename to packages/contracts-bedrock/interfaces/dispute/zk/IOptimisticZkGame.sol index 9d852b5f76f..748ccb06642 100644 --- a/packages/contracts-bedrock/interfaces/dispute/zk/IOPSuccinctFaultDisputeGame.sol +++ b/packages/contracts-bedrock/interfaces/dispute/zk/IOptimisticZkGame.sol @@ -18,9 +18,9 @@ import { ISP1Verifier } from "src/dispute/zk/ISP1Verifier.sol"; import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; import { AccessManager } from "src/dispute/zk/AccessManager.sol"; -/// @title IOPSuccinctFaultDisputeGame -/// @notice Interface for the OPSuccinctFaultDisputeGame contract. -interface IOPSuccinctFaultDisputeGame is IDisputeGame, ISemver { +/// @title IOptimisticZkGame +/// @notice Interface for the OptimisticZkGame contract. +interface IOptimisticZkGame is IDisputeGame, ISemver { enum ProposalStatus { Unchallenged, Challenged, diff --git a/packages/contracts-bedrock/justfile b/packages/contracts-bedrock/justfile index cff45827452..efeed6c7c0b 100644 --- a/packages/contracts-bedrock/justfile +++ b/packages/contracts-bedrock/justfile @@ -73,7 +73,12 @@ clean: # Runs standard contract tests. test *ARGS: build-go-ffi - forge test {{ARGS}} + #!/bin/bash + if [ -n "$JUNIT_TEST_PATH" ]; then + forge test {{ARGS}} --junit > "$JUNIT_TEST_PATH" + else + forge test {{ARGS}} + fi # Runs standard contract tests (developer mode). test-dev *ARGS: build-go-ffi @@ -81,8 +86,8 @@ test-dev *ARGS: build-go-ffi # Default block number for the forked upgrade path. -export sepoliaBlockNumber := "9366100" -export mainnetBlockNumber := "23530400" +export sepoliaBlockNumber := "9770063" +export mainnetBlockNumber := "23942395" export pinnedBlockNumber := if env_var_or_default("FORK_BASE_CHAIN", "") == "mainnet" { mainnetBlockNumber @@ -115,7 +120,12 @@ prepare-upgrade-env *ARGS : build-go-ffi # Runs upgrade path variant of contract tests. test-upgrade *ARGS: - just prepare-upgrade-env "forge test {{ARGS}}" + #!/bin/bash + if [ -n "$JUNIT_TEST_PATH" ]; then + just prepare-upgrade-env "forge test {{ARGS}} --junit > \"$JUNIT_TEST_PATH\"" + else + just prepare-upgrade-env "forge test {{ARGS}}" + fi test-upgrade-rerun *ARGS: build-go-ffi just test-upgrade {{ARGS}} --rerun -vvvv diff --git a/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol b/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol index 0ac35465ff8..ec618a04bd1 100644 --- a/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol +++ b/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol @@ -16,6 +16,7 @@ import { Types } from "scripts/libraries/Types.sol"; import { Blueprint } from "src/libraries/Blueprint.sol"; import { GameTypes } from "src/dispute/lib/Types.sol"; import { Hash } from "src/dispute/lib/Types.sol"; +import { DevFeatures } from "src/libraries/DevFeatures.sol"; // Interfaces import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; @@ -35,6 +36,8 @@ import { IMIPS64 } from "interfaces/cannon/IMIPS64.sol"; import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; import { IProxyAdminOwnedBase } from "interfaces/L1/IProxyAdminOwnedBase.sol"; import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; +import { IOPContractsManagerV2 } from "interfaces/L1/opcm/IOPContractsManagerV2.sol"; +import { IOPContractsManagerUtils } from "interfaces/L1/opcm/IOPContractsManagerUtils.sol"; library ChainAssertions { Vm internal constant vm = Vm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); @@ -109,10 +112,19 @@ library ChainAssertions { require(config.scalar() >> 248 == 1, "CHECK-SCFG-70"); // Depends on start block being set to 0 in `initialize` require(config.startBlock() == block.number, "CHECK-SCFG-140"); - require( - config.batchInbox() == IOPContractsManager(_doi.opcm).chainIdToBatchInboxAddress(_doi.l2ChainId), - "CHECK-SCFG-150" - ); + if (IOPContractsManager(_doi.opcm).isDevFeatureEnabled(DevFeatures.OPCM_V2)) { + require( + config.batchInbox() + == IOPContractsManagerUtils(IOPContractsManagerV2(address(_doi.opcm)).opcmUtils()) + .chainIdToBatchInboxAddress(_doi.l2ChainId), + "CHECK-SCFG-150" + ); + } else { + require( + config.batchInbox() == IOPContractsManager(_doi.opcm).chainIdToBatchInboxAddress(_doi.l2ChainId), + "CHECK-SCFG-150" + ); + } // Check _addresses require(config.l1CrossDomainMessenger() == _contracts.L1CrossDomainMessenger, "CHECK-SCFG-160"); require(config.l1ERC721Bridge() == _contracts.L1ERC721Bridge, "CHECK-SCFG-170"); @@ -385,9 +397,10 @@ library ChainAssertions { require(address(_opcm) != address(0), "CHECK-OPCM-10"); require(bytes(_opcm.version()).length > 0, "CHECK-OPCM-15"); - require(address(_opcm.protocolVersions()) == _proxies.ProtocolVersions, "CHECK-OPCM-17"); - require(address(_opcm.superchainConfig()) == _proxies.SuperchainConfig, "CHECK-OPCM-19"); - + if (!_opcm.isDevFeatureEnabled(DevFeatures.OPCM_V2)) { + require(address(_opcm.protocolVersions()) == _proxies.ProtocolVersions, "CHECK-OPCM-17"); + require(address(_opcm.superchainConfig()) == _proxies.SuperchainConfig, "CHECK-OPCM-19"); + } // Ensure that the OPCM impls are correctly saved IOPContractsManager.Implementations memory impls = _opcm.implementations(); require(impls.l1ERC721BridgeImpl == _impls.L1ERC721Bridge, "CHECK-OPCM-50"); diff --git a/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol b/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol index 3467c24dfff..99cf62c9d09 100644 --- a/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol @@ -301,7 +301,7 @@ contract Deploy is Deployer { // Save the implementation addresses which are needed outside of this function or script. // When called in a fork test, this will overwrite the existing implementations. artifacts.save("MipsSingleton", address(dio.mipsSingleton)); - if (DevFeatures.isDevFeatureEnabled(dio.opcm.devFeatureBitmap(), DevFeatures.OPCM_V2)) { + if (DevFeatures.isDevFeatureEnabled(cfg.devFeatureBitmap(), DevFeatures.OPCM_V2)) { artifacts.save("OPContractsManagerV2", address(dio.opcmV2)); } else { artifacts.save("OPContractsManager", address(dio.opcm)); @@ -336,10 +336,16 @@ contract Deploy is Deployer { _mips: IMIPS64(address(dio.mipsSingleton)), _oracle: IPreimageOracle(address(dio.preimageOracleSingleton)) }); + IOPContractsManager _opcm; + if (DevFeatures.isDevFeatureEnabled(cfg.devFeatureBitmap(), DevFeatures.OPCM_V2)) { + _opcm = IOPContractsManager(address(dio.opcmV2)); + } else { + _opcm = IOPContractsManager(address(dio.opcm)); + } ChainAssertions.checkOPContractsManager({ _impls: impls, _proxies: _proxies(), - _opcm: IOPContractsManager(address(dio.opcm)), + _opcm: _opcm, _mips: IMIPS64(address(dio.mipsSingleton)) }); ChainAssertions.checkSystemConfigImpls(impls); diff --git a/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol index d6d660ff64b..5ccbf1e174c 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol @@ -179,7 +179,41 @@ contract DeployImplementations is Script { superPermissionedDisputeGameImpl: address(_output.superPermissionedDisputeGameImpl) }); - IOPContractsManagerContainer.Implementations memory implementationsV2 = IOPContractsManagerContainer + // Deploy OPCM V1 components + deployOPCMBPImplsContainer(_input, _output, _blueprints, implementations); + deployOPCMGameTypeAdder(_output); + deployOPCMDeployer(_input, _output); + deployOPCMUpgrader(_output); + deployOPCMInteropMigrator(_output); + deployOPCMStandardValidator(_input, _output, implementations); + + // Semgrep rule will fail because the arguments are encoded inside of a separate function. + opcm_ = IOPContractsManager( + // nosemgrep: sol-safety-deployutils-args + DeployUtils.createDeterministic({ + _name: "OPContractsManager", + _args: encodeOPCMConstructor(_input, _output), + _salt: _salt + }) + ); + + vm.label(address(opcm_), "OPContractsManager"); + _output.opcm = opcm_; + + // Set OPCM V2 addresses to zero (not deployed) + _output.opcmV2 = IOPContractsManagerV2(address(0)); + _output.opcmContainer = IOPContractsManagerContainer(address(0)); + } + + function createOPCMContractV2( + Input memory _input, + Output memory _output, + IOPContractsManager.Blueprints memory _blueprints + ) + private + returns (IOPContractsManagerV2 opcmV2_) + { + IOPContractsManagerContainer.Implementations memory implementations = IOPContractsManagerContainer .Implementations({ superchainConfigImpl: address(_output.superchainConfigImpl), protocolVersionsImpl: address(_output.protocolVersionsImpl), @@ -203,7 +237,7 @@ contract DeployImplementations is Script { }); // Convert blueprints to V2 blueprints - IOPContractsManagerContainer.Blueprints memory blueprintsV2 = IOPContractsManagerContainer.Blueprints({ + IOPContractsManagerContainer.Blueprints memory blueprints = IOPContractsManagerContainer.Blueprints({ addressManager: _blueprints.addressManager, proxy: _blueprints.proxy, proxyAdmin: _blueprints.proxyAdmin, @@ -211,28 +245,21 @@ contract DeployImplementations is Script { resolvedDelegateProxy: _blueprints.resolvedDelegateProxy }); - deployOPCMBPImplsContainer(_input, _output, _blueprints, implementations); - deployOPCMContainer(_input, _output, blueprintsV2, implementationsV2); - deployOPCMGameTypeAdder(_output); - deployOPCMDeployer(_input, _output); - deployOPCMUpgrader(_output); - deployOPCMInteropMigrator(_output); - deployOPCMStandardValidator(_input, _output, implementations); + // Deploy OPCM V2 components + deployOPCMContainer(_input, _output, blueprints, implementations); + deployOPCMStandardValidatorV2(_input, _output, implementations); deployOPCMUtils(_output); - deployOPCMV2(_output); + opcmV2_ = deployOPCMV2(_output); - // Semgrep rule will fail because the arguments are encoded inside of a separate function. - opcm_ = IOPContractsManager( - // nosemgrep: sol-safety-deployutils-args - DeployUtils.createDeterministic({ - _name: "OPContractsManager", - _args: encodeOPCMConstructor(_input, _output), - _salt: _salt - }) - ); + // Set OPCM V1 addresses to zero (not deployed) + _output.opcm = IOPContractsManager(address(0)); + _output.opcmContractsContainer = IOPContractsManagerContractsContainer(address(0)); + _output.opcmGameTypeAdder = IOPContractsManagerGameTypeAdder(address(0)); + _output.opcmDeployer = IOPContractsManagerDeployer(address(0)); + _output.opcmUpgrader = IOPContractsManagerUpgrader(address(0)); + _output.opcmInteropMigrator = IOPContractsManagerInteropMigrator(address(0)); - vm.label(address(opcm_), "OPContractsManager"); - _output.opcm = opcm_; + return opcmV2_; } /// @notice Encodes the constructor of the OPContractsManager contract. Used to avoid stack too @@ -283,10 +310,18 @@ contract DeployImplementations is Script { // forgefmt: disable-end vm.stopBroadcast(); - IOPContractsManager opcm = createOPCMContract(_input, _output, blueprints); - - vm.label(address(opcm), "OPContractsManager"); - _output.opcm = opcm; + // Check if OPCM V2 should be deployed + bool deployV2 = DevFeatures.isDevFeatureEnabled(_input.devFeatureBitmap, DevFeatures.OPCM_V2); + + if (deployV2) { + IOPContractsManagerV2 opcmV2 = createOPCMContractV2(_input, _output, blueprints); + vm.label(address(opcmV2), "OPContractsManagerV2"); + _output.opcmV2 = opcmV2; + } else { + IOPContractsManager opcm = createOPCMContract(_input, _output, blueprints); + vm.label(address(opcm), "OPContractsManager"); + _output.opcm = opcm; + } } // --- Core Contracts --- @@ -769,10 +804,56 @@ contract DeployImplementations is Script { _output.opcmUtils = impl; } - function deployOPCMV2(Output memory _output) private { - IOPContractsManagerV2 impl = IOPContractsManagerV2( + function deployOPCMStandardValidatorV2( + Input memory _input, + Output memory _output, + IOPContractsManagerContainer.Implementations memory _implementations + ) + private + { + IOPContractsManagerStandardValidator.Implementations memory opcmImplementations; + opcmImplementations.l1ERC721BridgeImpl = _implementations.l1ERC721BridgeImpl; + opcmImplementations.optimismPortalImpl = _implementations.optimismPortalImpl; + opcmImplementations.optimismPortalInteropImpl = _implementations.optimismPortalInteropImpl; + opcmImplementations.ethLockboxImpl = _implementations.ethLockboxImpl; + opcmImplementations.systemConfigImpl = _implementations.systemConfigImpl; + opcmImplementations.optimismMintableERC20FactoryImpl = _implementations.optimismMintableERC20FactoryImpl; + opcmImplementations.l1CrossDomainMessengerImpl = _implementations.l1CrossDomainMessengerImpl; + opcmImplementations.l1StandardBridgeImpl = _implementations.l1StandardBridgeImpl; + opcmImplementations.disputeGameFactoryImpl = _implementations.disputeGameFactoryImpl; + opcmImplementations.anchorStateRegistryImpl = _implementations.anchorStateRegistryImpl; + opcmImplementations.delayedWETHImpl = _implementations.delayedWETHImpl; + opcmImplementations.mipsImpl = _implementations.mipsImpl; + opcmImplementations.faultDisputeGameImpl = _implementations.faultDisputeGameV2Impl; + opcmImplementations.permissionedDisputeGameImpl = _implementations.permissionedDisputeGameV2Impl; + + IOPContractsManagerStandardValidator impl = IOPContractsManagerStandardValidator( + DeployUtils.createDeterministic({ + _name: "OPContractsManagerStandardValidator.sol:OPContractsManagerStandardValidator", + _args: DeployUtils.encodeConstructor( + abi.encodeCall( + IOPContractsManagerStandardValidator.__constructor__, + ( + opcmImplementations, + _input.superchainConfigProxy, + _input.l1ProxyAdminOwner, + _input.challenger, + _input.withdrawalDelaySeconds, + _input.devFeatureBitmap + ) + ) + ), + _salt: _salt + }) + ); + vm.label(address(impl), "OPContractsManagerStandardValidatorImpl"); + _output.opcmStandardValidator = impl; + } + + function deployOPCMV2(Output memory _output) private returns (IOPContractsManagerV2 opcmV2_) { + opcmV2_ = IOPContractsManagerV2( DeployUtils.createDeterministic({ - _name: "OPContractsManagerV2.sol:OPContractsManagerV2", + _name: "OPContractsManagerV2", _args: DeployUtils.encodeConstructor( abi.encodeCall( IOPContractsManagerV2.__constructor__, @@ -782,8 +863,7 @@ contract DeployImplementations is Script { _salt: _salt }) ); - vm.label(address(impl), "OPContractsManagerV2Impl"); - _output.opcmV2 = impl; + vm.label(address(opcmV2_), "OPContractsManagerV2"); } function deployStorageSetterImpl(Output memory _output) private { @@ -851,8 +931,12 @@ contract DeployImplementations is Script { function assertValidOutput(Input memory _input, Output memory _output) private { // With 12 addresses, we'd get a stack too deep error if we tried to do this inline as a // single call to `Solarray.addresses`. So we split it into two calls. + + // Check which OPCM version was deployed + bool deployedV2 = DevFeatures.isDevFeatureEnabled(_input.devFeatureBitmap, DevFeatures.OPCM_V2); + address[] memory addrs1 = Solarray.addresses( - address(_output.opcm), + deployedV2 ? address(_output.opcmV2) : address(_output.opcm), address(_output.optimismPortalImpl), address(_output.delayedWETHImpl), address(_output.preimageOracleSingleton), @@ -883,6 +967,27 @@ contract DeployImplementations is Script { DeployUtils.assertValidContractAddresses(Solarray.extend(addrs1, addrs2)); + // Validate OPCM V2 flag + if (DevFeatures.isDevFeatureEnabled(_input.devFeatureBitmap, DevFeatures.OPCM_V2)) { + require( + address(_output.opcmV2) != address(0), + "DeployImplementations: OPCM V2 flag enabled but OPCM V2 not deployed" + ); + require( + address(_output.opcm) == address(0), + "DeployImplementations: OPCM V2 flag enabled but OPCM V1 was deployed" + ); + } else { + require( + address(_output.opcm) != address(0), + "DeployImplementations: OPCM V2 flag disabled but OPCM V1 not deployed" + ); + require( + address(_output.opcmV2) == address(0), + "DeployImplementations: OPCM V2 flag disabled but OPCM V2 was deployed" + ); + } + if (!DevFeatures.isDevFeatureEnabled(_input.devFeatureBitmap, DevFeatures.OPTIMISM_PORTAL_INTEROP)) { require( address(_output.superFaultDisputeGameImpl) == address(0), @@ -909,15 +1014,18 @@ contract DeployImplementations is Script { ChainAssertions.checkL1StandardBridgeImpl(_output.l1StandardBridgeImpl); ChainAssertions.checkMIPS(_output.mipsSingleton, _output.preimageOracleSingleton); - Types.ContractSet memory proxies; - proxies.SuperchainConfig = address(_input.superchainConfigProxy); - proxies.ProtocolVersions = address(_input.protocolVersionsProxy); - ChainAssertions.checkOPContractsManager({ - _impls: impls, - _proxies: proxies, - _opcm: IOPContractsManager(address(_output.opcm)), - _mips: IMIPS64(address(_output.mipsSingleton)) - }); + // Only check OPCM V1 if it was deployed + if (!DevFeatures.isDevFeatureEnabled(_input.devFeatureBitmap, DevFeatures.OPCM_V2)) { + Types.ContractSet memory proxies; + proxies.SuperchainConfig = address(_input.superchainConfigProxy); + proxies.ProtocolVersions = address(_input.protocolVersionsProxy); + ChainAssertions.checkOPContractsManager({ + _impls: impls, + _proxies: proxies, + _opcm: IOPContractsManager(address(_output.opcm)), + _mips: IMIPS64(address(_output.mipsSingleton)) + }); + } ChainAssertions.checkOptimismMintableERC20FactoryImpl(_output.optimismMintableERC20FactoryImpl); ChainAssertions.checkOptimismPortal2({ diff --git a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol index 2f5ff24e752..ddecc78a9df 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol @@ -3,6 +3,8 @@ pragma solidity 0.8.15; import { Script } from "forge-std/Script.sol"; +import { DevFeatures } from "src/libraries/DevFeatures.sol"; +import { Constants } from "src/libraries/Constants.sol"; import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; import { Solarray } from "scripts/libraries/Solarray.sol"; import { ChainAssertions } from "scripts/deploy/ChainAssertions.sol"; @@ -11,6 +13,7 @@ import { Types } from "scripts/libraries/Types.sol"; import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; +import { IOPContractsManagerV2 } from "interfaces/L1/opcm/IOPContractsManagerV2.sol"; import { IAddressManager } from "interfaces/legacy/IAddressManager.sol"; import { IDelayedWETH } from "interfaces/dispute/IDelayedWETH.sol"; import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; @@ -24,7 +27,7 @@ import { IL1ERC721Bridge } from "interfaces/L1/IL1ERC721Bridge.sol"; import { IL1StandardBridge } from "interfaces/L1/IL1StandardBridge.sol"; import { IOptimismMintableERC20Factory } from "interfaces/universal/IOptimismMintableERC20Factory.sol"; import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; -import { IOPContractsManager } from "../../interfaces/L1/IOPContractsManager.sol"; +import { GameTypes } from "src/dispute/lib/Types.sol"; contract DeployOPChain is Script { struct Output { @@ -54,8 +57,66 @@ contract DeployOPChain is Script { function run(Types.DeployOPChainInput memory _input) public returns (Output memory output_) { checkInput(_input); - IOPContractsManager opcm = IOPContractsManager(_input.opcm); + // Check if OPCM v2 should be used. + bool useV2 = isDevFeatureOpcmV2Enabled(_input.opcm); + if (useV2) { + IOPContractsManagerV2 opcmV2 = IOPContractsManagerV2(_input.opcm); + IOPContractsManagerV2.FullConfig memory config = toOPCMV2DeployInput(_input); + + vm.broadcast(msg.sender); + IOPContractsManagerV2.ChainContracts memory chainContracts = opcmV2.deploy(config); + output_ = fromOPCMV2OutputToOutput(chainContracts); + } else { + IOPContractsManager opcm = IOPContractsManager(_input.opcm); + IOPContractsManager.DeployInput memory deployInput = toOPCMV1DeployInput(_input); + + vm.broadcast(msg.sender); + IOPContractsManager.DeployOutput memory deployOutput = opcm.deploy(deployInput); + + output_ = fromOPCMV1OutputToOutput(deployOutput); + } + + checkOutput(_input, output_); + + vm.label(address(output_.opChainProxyAdmin), "opChainProxyAdmin"); + vm.label(address(output_.addressManager), "addressManager"); + vm.label(address(output_.l1ERC721BridgeProxy), "l1ERC721BridgeProxy"); + vm.label(address(output_.systemConfigProxy), "systemConfigProxy"); + vm.label(address(output_.optimismMintableERC20FactoryProxy), "optimismMintableERC20FactoryProxy"); + vm.label(address(output_.l1StandardBridgeProxy), "l1StandardBridgeProxy"); + vm.label(address(output_.l1CrossDomainMessengerProxy), "l1CrossDomainMessengerProxy"); + vm.label(address(output_.optimismPortalProxy), "optimismPortalProxy"); + vm.label(address(output_.ethLockboxProxy), "ethLockboxProxy"); + vm.label(address(output_.disputeGameFactoryProxy), "disputeGameFactoryProxy"); + vm.label(address(output_.anchorStateRegistryProxy), "anchorStateRegistryProxy"); + vm.label(address(output_.delayedWETHPermissionedGameProxy), "delayedWETHPermissionedGameProxy"); + // TODO: Eventually switch from Permissioned to Permissionless. + // vm.label(address(output_.faultDisputeGame), "faultDisputeGame"); + // vm.label(address(output_.delayedWETHPermissionlessGameProxy), "delayedWETHPermissionlessGameProxy"); + } + + // -------- Features -------- + + /// @notice Checks if OPCM v2 dev feature flag is enabled from the contract's dev feature bitmap. + function isDevFeatureOpcmV2Enabled(address _opcmAddr) internal view returns (bool) { + // Both v1 and v2 share the same interface for this function. + return IOPContractsManager(_opcmAddr).isDevFeatureEnabled(DevFeatures.OPCM_V2); + } + + function isDevFeatureV2DisputeGamesEnabled(address _opcmAddr) internal view returns (bool) { + IOPContractsManager opcm = IOPContractsManager(_opcmAddr); + return DevFeatures.isDevFeatureEnabled(opcm.devFeatureBitmap(), DevFeatures.DEPLOY_V2_DISPUTE_GAMES); + } + + /// @notice Converts Types.DeployOPChainInput to IOPContractsManager.DeployInput. + /// @param _input The input parameters. + /// @return deployInput_ The deployed input parameters. + function toOPCMV1DeployInput(Types.DeployOPChainInput memory _input) + internal + pure + returns (IOPContractsManager.DeployInput memory deployInput_) + { IOPContractsManager.Roles memory roles = IOPContractsManager.Roles({ opChainProxyAdminOwner: _input.opChainProxyAdminOwner, systemConfigOwner: _input.systemConfigOwner, @@ -64,7 +125,7 @@ contract DeployOPChain is Script { proposer: _input.proposer, challenger: _input.challenger }); - IOPContractsManager.DeployInput memory deployInput = IOPContractsManager.DeployInput({ + deployInput_ = IOPContractsManager.DeployInput({ roles: roles, basefeeScalar: _input.basefeeScalar, blobBasefeeScalar: _input.blobBaseFeeScalar, @@ -80,46 +141,133 @@ contract DeployOPChain is Script { disputeMaxClockDuration: _input.disputeMaxClockDuration, useCustomGasToken: _input.useCustomGasToken }); + } - vm.broadcast(msg.sender); - IOPContractsManager.DeployOutput memory deployOutput = opcm.deploy(deployInput); - - vm.label(address(deployOutput.opChainProxyAdmin), "opChainProxyAdmin"); - vm.label(address(deployOutput.addressManager), "addressManager"); - vm.label(address(deployOutput.l1ERC721BridgeProxy), "l1ERC721BridgeProxy"); - vm.label(address(deployOutput.systemConfigProxy), "systemConfigProxy"); - vm.label(address(deployOutput.optimismMintableERC20FactoryProxy), "optimismMintableERC20FactoryProxy"); - vm.label(address(deployOutput.l1StandardBridgeProxy), "l1StandardBridgeProxy"); - vm.label(address(deployOutput.l1CrossDomainMessengerProxy), "l1CrossDomainMessengerProxy"); - vm.label(address(deployOutput.optimismPortalProxy), "optimismPortalProxy"); - vm.label(address(deployOutput.ethLockboxProxy), "ethLockboxProxy"); - vm.label(address(deployOutput.disputeGameFactoryProxy), "disputeGameFactoryProxy"); - vm.label(address(deployOutput.anchorStateRegistryProxy), "anchorStateRegistryProxy"); - vm.label(address(deployOutput.permissionedDisputeGame), "permissionedDisputeGame"); - vm.label(address(deployOutput.delayedWETHPermissionedGameProxy), "delayedWETHPermissionedGameProxy"); - // TODO: Eventually switch from Permissioned to Permissionless. - // vm.label(address(deployOutput.faultDisputeGame), "faultDisputeGame"); - // vm.label(address(deployOutput.delayedWETHPermissionlessGameProxy), "delayedWETHPermissionlessGameProxy"); + /// @notice Converts Types.DeployOPChainInput to IOPContractsManagerV2.FullConfig. + /// @param _input The input parameters. + /// @return config_ The deployed input parameters. + function toOPCMV2DeployInput(Types.DeployOPChainInput memory _input) + internal + pure + returns (IOPContractsManagerV2.FullConfig memory config_) + { + // Build dispute game configs - OPCMV2 requires exactly 3 configs: CANNON, PERMISSIONED_CANNON, CANNON_KONA + IOPContractsManagerV2.DisputeGameConfig[] memory disputeGameConfigs = + new IOPContractsManagerV2.DisputeGameConfig[](3); + + // Determine which games should be enabled based on the starting respected game type + bool cannonEnabled = _input.disputeGameType.raw() == GameTypes.CANNON.raw(); + bool permissionedCannonEnabled = true; // PERMISSIONED_CANNON must always be enabled + bool cannonKonaEnabled = _input.disputeGameType.raw() == GameTypes.CANNON_KONA.raw(); + + // Config 0: CANNON + IOPContractsManagerV2.FaultDisputeGameConfig memory cannonConfig = + IOPContractsManagerV2.FaultDisputeGameConfig({ absolutePrestate: _input.disputeAbsolutePrestate }); + + disputeGameConfigs[0] = IOPContractsManagerV2.DisputeGameConfig({ + enabled: cannonEnabled, + initBond: cannonEnabled ? 0.08 ether : 0, // Standard init bond if enabled + gameType: GameTypes.CANNON, + gameArgs: abi.encode(cannonConfig) + }); + + // Config 1: PERMISSIONED_CANNON (must be enabled) + IOPContractsManagerV2.PermissionedDisputeGameConfig memory pdgConfig = IOPContractsManagerV2 + .PermissionedDisputeGameConfig({ + absolutePrestate: _input.disputeAbsolutePrestate, + proposer: _input.proposer, + challenger: _input.challenger + }); + disputeGameConfigs[1] = IOPContractsManagerV2.DisputeGameConfig({ + enabled: permissionedCannonEnabled, + initBond: 0.08 ether, // Standard init bond + gameType: GameTypes.PERMISSIONED_CANNON, + gameArgs: abi.encode(pdgConfig) + }); + + // Config 2: CANNON_KONA + IOPContractsManagerV2.FaultDisputeGameConfig memory cannonKonaConfig = + IOPContractsManagerV2.FaultDisputeGameConfig({ absolutePrestate: _input.disputeAbsolutePrestate }); + + disputeGameConfigs[2] = IOPContractsManagerV2.DisputeGameConfig({ + enabled: cannonKonaEnabled, + initBond: cannonKonaEnabled ? 0.08 ether : 0, // Standard init bond if enabled + gameType: GameTypes.CANNON_KONA, + gameArgs: abi.encode(cannonKonaConfig) + }); + + config_ = IOPContractsManagerV2.FullConfig({ + saltMixer: _input.saltMixer, + superchainConfig: _input.superchainConfig, + proxyAdminOwner: _input.opChainProxyAdminOwner, + systemConfigOwner: _input.systemConfigOwner, + unsafeBlockSigner: _input.unsafeBlockSigner, + batcher: _input.batcher, + startingAnchorRoot: ScriptConstants.DEFAULT_OUTPUT_ROOT(), + startingRespectedGameType: _input.disputeGameType, + basefeeScalar: _input.basefeeScalar, + blobBasefeeScalar: _input.blobBaseFeeScalar, + gasLimit: _input.gasLimit, + l2ChainId: _input.l2ChainId, + resourceConfig: Constants.DEFAULT_RESOURCE_CONFIG(), + disputeGameConfigs: disputeGameConfigs, + useCustomGasToken: _input.useCustomGasToken + }); + } + + /// @notice Converts IOPContractsManagerV2.ChainContracts to Output. + /// @param _chainContracts The chain contracts. + /// @return output_ The output parameters. + function fromOPCMV2OutputToOutput(IOPContractsManagerV2.ChainContracts memory _chainContracts) + internal + pure + returns (Output memory output_) + { output_ = Output({ - opChainProxyAdmin: deployOutput.opChainProxyAdmin, - addressManager: deployOutput.addressManager, - l1ERC721BridgeProxy: deployOutput.l1ERC721BridgeProxy, - systemConfigProxy: deployOutput.systemConfigProxy, - optimismMintableERC20FactoryProxy: deployOutput.optimismMintableERC20FactoryProxy, - l1StandardBridgeProxy: deployOutput.l1StandardBridgeProxy, - l1CrossDomainMessengerProxy: deployOutput.l1CrossDomainMessengerProxy, - optimismPortalProxy: deployOutput.optimismPortalProxy, - ethLockboxProxy: deployOutput.ethLockboxProxy, - disputeGameFactoryProxy: deployOutput.disputeGameFactoryProxy, - anchorStateRegistryProxy: deployOutput.anchorStateRegistryProxy, - faultDisputeGame: deployOutput.faultDisputeGame, - permissionedDisputeGame: deployOutput.permissionedDisputeGame, - delayedWETHPermissionedGameProxy: deployOutput.delayedWETHPermissionedGameProxy, - delayedWETHPermissionlessGameProxy: deployOutput.delayedWETHPermissionlessGameProxy + opChainProxyAdmin: _chainContracts.proxyAdmin, + addressManager: _chainContracts.addressManager, + l1ERC721BridgeProxy: _chainContracts.l1ERC721Bridge, + systemConfigProxy: _chainContracts.systemConfig, + optimismMintableERC20FactoryProxy: _chainContracts.optimismMintableERC20Factory, + l1StandardBridgeProxy: _chainContracts.l1StandardBridge, + l1CrossDomainMessengerProxy: _chainContracts.l1CrossDomainMessenger, + optimismPortalProxy: _chainContracts.optimismPortal, + ethLockboxProxy: _chainContracts.ethLockbox, + disputeGameFactoryProxy: _chainContracts.disputeGameFactory, + anchorStateRegistryProxy: _chainContracts.anchorStateRegistry, + faultDisputeGame: IFaultDisputeGame(address(0)), + permissionedDisputeGame: IPermissionedDisputeGame(address(0)), + delayedWETHPermissionedGameProxy: _chainContracts.delayedWETH, + delayedWETHPermissionlessGameProxy: IDelayedWETH(payable(address(0))) }); + } - checkOutput(_input, output_); + /// @notice Converts IOPContractsManager.DeployOutput to Output. + /// @param _deployOutput The deploy output. + /// @return output_ The output parameters. + function fromOPCMV1OutputToOutput(IOPContractsManager.DeployOutput memory _deployOutput) + internal + pure + returns (Output memory output_) + { + output_ = Output({ + opChainProxyAdmin: _deployOutput.opChainProxyAdmin, + addressManager: _deployOutput.addressManager, + l1ERC721BridgeProxy: _deployOutput.l1ERC721BridgeProxy, + systemConfigProxy: _deployOutput.systemConfigProxy, + optimismMintableERC20FactoryProxy: _deployOutput.optimismMintableERC20FactoryProxy, + l1StandardBridgeProxy: _deployOutput.l1StandardBridgeProxy, + l1CrossDomainMessengerProxy: _deployOutput.l1CrossDomainMessengerProxy, + optimismPortalProxy: _deployOutput.optimismPortalProxy, + ethLockboxProxy: _deployOutput.ethLockboxProxy, + disputeGameFactoryProxy: _deployOutput.disputeGameFactoryProxy, + anchorStateRegistryProxy: _deployOutput.anchorStateRegistryProxy, + faultDisputeGame: _deployOutput.faultDisputeGame, + permissionedDisputeGame: _deployOutput.permissionedDisputeGame, + delayedWETHPermissionedGameProxy: _deployOutput.delayedWETHPermissionedGameProxy, + delayedWETHPermissionlessGameProxy: _deployOutput.delayedWETHPermissionlessGameProxy + }); } // -------- Validations -------- @@ -187,12 +335,23 @@ contract DeployOPChain is Script { SystemConfig: address(_o.systemConfigProxy), L1ERC721Bridge: address(_o.l1ERC721BridgeProxy), ProtocolVersions: address(0), - SuperchainConfig: address(0) + SuperchainConfig: address(_i.superchainConfig) }); - // Check dispute games - // With v2 game contracts enabled, we use the predeployed pdg implementation - address expectedPDGImpl = IOPContractsManager(_i.opcm).implementations().permissionedDisputeGameV2Impl; + // Check dispute games and get superchain config + address expectedPDGImpl = address(_o.permissionedDisputeGame); + + if (isDevFeatureOpcmV2Enabled(_i.opcm)) { + // OPCM v2: use implementations from v2 contract + IOPContractsManagerV2 opcmV2 = IOPContractsManagerV2(_i.opcm); + expectedPDGImpl = opcmV2.implementations().permissionedDisputeGameV2Impl; + } else { + // OPCM v1: use implementations from v1 contract + IOPContractsManager opcm = IOPContractsManager(_i.opcm); + // With v2 game contracts enabled, we use the predeployed pdg implementation + expectedPDGImpl = opcm.implementations().permissionedDisputeGameV2Impl; + } + ChainAssertions.checkDisputeGameFactory( _o.disputeGameFactoryProxy, _i.opChainProxyAdminOwner, expectedPDGImpl, true ); @@ -201,7 +360,7 @@ contract DeployOPChain is Script { ChainAssertions.checkL1CrossDomainMessenger(_o.l1CrossDomainMessengerProxy, vm, true); ChainAssertions.checkOptimismPortal2({ _contracts: proxies, - _superchainConfig: IOPContractsManager(_i.opcm).superchainConfig(), + _superchainConfig: _i.superchainConfig, _opChainProxyAdminOwner: _i.opChainProxyAdminOwner, _isProxy: true }); diff --git a/packages/contracts-bedrock/scripts/deploy/ReadImplementationAddresses.s.sol b/packages/contracts-bedrock/scripts/deploy/ReadImplementationAddresses.s.sol index 23deddc11e7..3fb08277fca 100644 --- a/packages/contracts-bedrock/scripts/deploy/ReadImplementationAddresses.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/ReadImplementationAddresses.s.sol @@ -5,8 +5,11 @@ import { IProxy } from "interfaces/universal/IProxy.sol"; import { Script } from "forge-std/Script.sol"; import { IMIPS64 } from "interfaces/cannon/IMIPS64.sol"; import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; +import { IOPContractsManagerV2 } from "interfaces/L1/opcm/IOPContractsManagerV2.sol"; +import { IOPContractsManagerContainer } from "interfaces/L1/opcm/IOPContractsManagerContainer.sol"; import { IAddressManager } from "interfaces/legacy/IAddressManager.sol"; import { IStaticL1ChugSplashProxy } from "interfaces/legacy/IL1ChugSplashProxy.sol"; +import { DevFeatures } from "src/libraries/DevFeatures.sol"; contract ReadImplementationAddresses is Script { struct Input { @@ -59,24 +62,53 @@ contract ReadImplementationAddresses is Script { vm.prank(address(0)); output_.l1StandardBridge = IStaticL1ChugSplashProxy(_input.l1StandardBridgeProxy).getImplementation(); - // Get implementations from OPCM - IOPContractsManager opcm = IOPContractsManager(_input.opcm); - output_.opcmGameTypeAdder = address(opcm.opcmGameTypeAdder()); - output_.opcmDeployer = address(opcm.opcmDeployer()); - output_.opcmUpgrader = address(opcm.opcmUpgrader()); - output_.opcmInteropMigrator = address(opcm.opcmInteropMigrator()); - output_.opcmStandardValidator = address(opcm.opcmStandardValidator()); + // Check if OPCM v2 is being used + bool useV2 = IOPContractsManager(_input.opcm).isDevFeatureEnabled(DevFeatures.OPCM_V2); - IOPContractsManager.Implementations memory impls = opcm.implementations(); - output_.mipsSingleton = impls.mipsImpl; - output_.delayedWETH = impls.delayedWETHImpl; - output_.ethLockbox = impls.ethLockboxImpl; - output_.anchorStateRegistry = impls.anchorStateRegistryImpl; - output_.optimismPortalInterop = impls.optimismPortalInteropImpl; - output_.faultDisputeGameV2 = impls.faultDisputeGameV2Impl; - output_.permissionedDisputeGameV2 = impls.permissionedDisputeGameV2Impl; - output_.superFaultDisputeGame = impls.superFaultDisputeGameImpl; - output_.superPermissionedDisputeGame = impls.superPermissionedDisputeGameImpl; + if (useV2) { + // Get implementations from OPCM V2 + IOPContractsManagerV2 opcmV2 = IOPContractsManagerV2(_input.opcm); + + // OPCMV2 doesn't expose these addresses directly, so we set them to zero + // These are internal to the OPCM container and not meant to be accessed externally + output_.opcmGameTypeAdder = address(0); + output_.opcmDeployer = address(0); + output_.opcmUpgrader = address(0); + output_.opcmInteropMigrator = address(0); + + // StandardValidator is accessible via the standardValidator() method + output_.opcmStandardValidator = address(opcmV2.opcmStandardValidator()); + + IOPContractsManagerContainer.Implementations memory impls = opcmV2.implementations(); + output_.mipsSingleton = impls.mipsImpl; + output_.delayedWETH = impls.delayedWETHImpl; + output_.ethLockbox = impls.ethLockboxImpl; + output_.anchorStateRegistry = impls.anchorStateRegistryImpl; + output_.optimismPortalInterop = impls.optimismPortalInteropImpl; + output_.faultDisputeGameV2 = impls.faultDisputeGameV2Impl; + output_.permissionedDisputeGameV2 = impls.permissionedDisputeGameV2Impl; + output_.superFaultDisputeGame = impls.superFaultDisputeGameImpl; + output_.superPermissionedDisputeGame = impls.superPermissionedDisputeGameImpl; + } else { + // Get implementations from OPCM V1 + IOPContractsManager opcm = IOPContractsManager(_input.opcm); + output_.opcmGameTypeAdder = address(opcm.opcmGameTypeAdder()); + output_.opcmDeployer = address(opcm.opcmDeployer()); + output_.opcmUpgrader = address(opcm.opcmUpgrader()); + output_.opcmInteropMigrator = address(opcm.opcmInteropMigrator()); + output_.opcmStandardValidator = address(opcm.opcmStandardValidator()); + + IOPContractsManager.Implementations memory impls = opcm.implementations(); + output_.mipsSingleton = impls.mipsImpl; + output_.delayedWETH = impls.delayedWETHImpl; + output_.ethLockbox = impls.ethLockboxImpl; + output_.anchorStateRegistry = impls.anchorStateRegistryImpl; + output_.optimismPortalInterop = impls.optimismPortalInteropImpl; + output_.faultDisputeGameV2 = impls.faultDisputeGameV2Impl; + output_.permissionedDisputeGameV2 = impls.permissionedDisputeGameV2Impl; + output_.superFaultDisputeGame = impls.superFaultDisputeGameImpl; + output_.superPermissionedDisputeGame = impls.superPermissionedDisputeGameImpl; + } // Get L1CrossDomainMessenger from AddressManager IAddressManager am = IAddressManager(_input.addressManager); diff --git a/packages/contracts-bedrock/scripts/deploy/ReadSuperchainDeployment.s.sol b/packages/contracts-bedrock/scripts/deploy/ReadSuperchainDeployment.s.sol index 6c7b60a714f..dc2422d819c 100644 --- a/packages/contracts-bedrock/scripts/deploy/ReadSuperchainDeployment.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/ReadSuperchainDeployment.s.sol @@ -9,49 +9,81 @@ import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; import { IProxy } from "interfaces/universal/IProxy.sol"; import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; +import { SemverComp } from "src/libraries/SemverComp.sol"; contract ReadSuperchainDeployment is Script { struct Input { - IOPContractsManager opcmAddress; + IOPContractsManager opcmAddress; // TODO(#18612): Remove OPCMAddress field when OPCMv1 gets deprecated + ISuperchainConfig superchainConfigProxy; } struct Output { + // TODO(#18612): Remove ProtocolVersions fields when OPCMv1 gets deprecated IProtocolVersions protocolVersionsImpl; IProtocolVersions protocolVersionsProxy; + address protocolVersionsOwner; + bytes32 recommendedProtocolVersion; + bytes32 requiredProtocolVersion; + // Superchain config ISuperchainConfig superchainConfigImpl; ISuperchainConfig superchainConfigProxy; IProxyAdmin superchainProxyAdmin; address guardian; - address protocolVersionsOwner; address superchainProxyAdminOwner; - bytes32 recommendedProtocolVersion; - bytes32 requiredProtocolVersion; } function run(Input memory _input) public returns (Output memory output_) { - require(address(_input.opcmAddress) != address(0), "ReadSuperchainDeployment: opcmAddress not set"); - + // Determine OPCM version by checking the semver or if the OPCM address is set. OPCM v2 starts at version 7.0.0. IOPContractsManager opcm = IOPContractsManager(_input.opcmAddress); + bool isOPCMV2; + if (address(opcm) == address(0)) { + isOPCMV2 = true; + } else { + require(address(opcm).code.length > 0, "ReadSuperchainDeployment: OPCM address has no code"); + isOPCMV2 = SemverComp.gte(opcm.version(), "7.0.0"); + } + + if (isOPCMV2) { + require( + address(_input.superchainConfigProxy) != address(0), + "ReadSuperchainDeployment: superchainConfigProxy required for OPCM v2" + ); + + // For OPCM v2, ProtocolVersions is being removed. Therefore, the ProtocolVersions-related fields + // (protocolVersionsImpl, protocolVersionsProxy, protocolVersionsOwner, recommendedProtocolVersion, + // requiredProtocolVersion) are intentionally left uninitialized. + output_.superchainConfigProxy = _input.superchainConfigProxy; + output_.superchainProxyAdmin = IProxyAdmin(EIP1967Helper.getAdmin(address(output_.superchainConfigProxy))); + + IProxy superchainConfigProxy = IProxy(payable(address(output_.superchainConfigProxy))); + + vm.startPrank(address(0)); + output_.superchainConfigImpl = ISuperchainConfig(superchainConfigProxy.implementation()); + vm.stopPrank(); + + output_.guardian = output_.superchainConfigProxy.guardian(); + output_.superchainProxyAdminOwner = output_.superchainProxyAdmin.owner(); + } else { + // When running on OPCM v1, the OPCM address is used to read the ProtocolVersions contract and + // SuperchainConfig. + output_.protocolVersionsProxy = IProtocolVersions(opcm.protocolVersions()); + output_.superchainConfigProxy = ISuperchainConfig(opcm.superchainConfig()); + output_.superchainProxyAdmin = IProxyAdmin(EIP1967Helper.getAdmin(address(output_.superchainConfigProxy))); + + IProxy protocolVersionsProxy = IProxy(payable(address(output_.protocolVersionsProxy))); + IProxy superchainConfigProxy = IProxy(payable(address(output_.superchainConfigProxy))); + + vm.startPrank(address(0)); + output_.protocolVersionsImpl = IProtocolVersions(protocolVersionsProxy.implementation()); + output_.superchainConfigImpl = ISuperchainConfig(superchainConfigProxy.implementation()); + vm.stopPrank(); - output_.protocolVersionsProxy = IProtocolVersions(opcm.protocolVersions()); - output_.superchainConfigProxy = ISuperchainConfig(opcm.superchainConfig()); - output_.superchainProxyAdmin = IProxyAdmin(EIP1967Helper.getAdmin(address(output_.superchainConfigProxy))); - - IProxy protocolVersionsProxy = IProxy(payable(address(output_.protocolVersionsProxy))); - IProxy superchainConfigProxy = IProxy(payable(address(output_.superchainConfigProxy))); - - vm.startPrank(address(0)); - output_.protocolVersionsImpl = IProtocolVersions(address(protocolVersionsProxy.implementation())); - output_.superchainConfigImpl = ISuperchainConfig(address(superchainConfigProxy.implementation())); - output_.protocolVersionsImpl = IProtocolVersions(protocolVersionsProxy.implementation()); - output_.superchainConfigImpl = ISuperchainConfig(superchainConfigProxy.implementation()); - vm.stopPrank(); - - output_.guardian = output_.superchainConfigProxy.guardian(); - output_.protocolVersionsOwner = output_.protocolVersionsProxy.owner(); - output_.superchainProxyAdminOwner = output_.superchainProxyAdmin.owner(); - output_.recommendedProtocolVersion = - bytes32(ProtocolVersion.unwrap(output_.protocolVersionsProxy.recommended())); - output_.requiredProtocolVersion = bytes32(ProtocolVersion.unwrap(output_.protocolVersionsProxy.required())); + output_.guardian = output_.superchainConfigProxy.guardian(); + output_.protocolVersionsOwner = output_.protocolVersionsProxy.owner(); + output_.superchainProxyAdminOwner = output_.superchainProxyAdmin.owner(); + output_.recommendedProtocolVersion = + bytes32(ProtocolVersion.unwrap(output_.protocolVersionsProxy.recommended())); + output_.requiredProtocolVersion = bytes32(ProtocolVersion.unwrap(output_.protocolVersionsProxy.required())); + } } } diff --git a/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol b/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol index ea8374a9519..11c209ee151 100644 --- a/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol @@ -13,6 +13,7 @@ import { Process } from "scripts/libraries/Process.sol"; import { Config } from "scripts/libraries/Config.sol"; import { Bytes } from "src/libraries/Bytes.sol"; import { DevFeatures } from "src/libraries/DevFeatures.sol"; +import { SemverComp } from "src/libraries/SemverComp.sol"; // Interfaces import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; @@ -129,8 +130,18 @@ contract VerifyOPCM is Script { fieldNameOverrides["opcmUpgrader"] = "OPContractsManagerUpgrader"; fieldNameOverrides["opcmInteropMigrator"] = "OPContractsManagerInteropMigrator"; fieldNameOverrides["opcmStandardValidator"] = "OPContractsManagerStandardValidator"; + + // Since both OPCM V1 and V2 have contractsContainer var and they point to different contract file names, + // in the code logic, we rename any occurrences of it to "contractsContainerV1" or "contractsContainerV2" before + // using it to read the mapping. + fieldNameOverrides["contractsContainerV1"] = "OPContractsManagerContractsContainer"; + fieldNameOverrides["contractsContainerV2"] = "OPContractsManagerContainer"; + + // OPCM V2 Specific field name overrides. + fieldNameOverrides["standardValidator"] = "OPContractsManagerStandardValidator"; + fieldNameOverrides["storageSetterImpl"] = "StorageSetter"; fieldNameOverrides["opcmV2"] = "OPContractsManagerV2"; - fieldNameOverrides["contractsContainer"] = "OPContractsManagerContractsContainer"; + fieldNameOverrides["opcmUtils"] = "OPContractsManagerUtils"; // Overrides for situations where contracts have differently named source files. sourceNameOverrides["OPContractsManagerGameTypeAdder"] = "OPContractsManager"; @@ -157,11 +168,16 @@ contract VerifyOPCM is Script { expectedGetters["opcmInteropMigrator"] = "SKIP"; // Address verified via bytecode comparison expectedGetters["opcmStandardValidator"] = "SKIP"; // Address verified via bytecode comparison expectedGetters["opcmUpgrader"] = "SKIP"; // Address verified via bytecode comparison + + // OPCM V2 Specific expected getters overrides expectedGetters["opcmV2"] = "SKIP"; // Address verified via bytecode comparison + expectedGetters["opcmUtils"] = "SKIP"; // Address verified via bytecode comparison + expectedGetters["contractsContainer"] = "SKIP"; // Address verified via bytecode comparison // Getters that don't need any sort of verification expectedGetters["devFeatureBitmap"] = "SKIP"; expectedGetters["isDevFeatureEnabled"] = "SKIP"; + expectedGetters["version"] = "SKIP"; // Mark as ready. ready = true; @@ -277,10 +293,11 @@ contract VerifyOPCM is Script { new OpcmContractRef[](propRefs.length + implRefs.length + bpRefs.length + extraRefs); // References for OPCM and linked contracts. - refs[0] = OpcmContractRef({ field: "opcm", name: "OPContractsManager", addr: address(_opcm), blueprint: false }); + refs[0] = OpcmContractRef({ field: "opcm", name: _opcmContractName(), addr: address(_opcm), blueprint: false }); refs[1] = OpcmContractRef({ field: "contractsContainer", - name: "OPContractsManagerContractsContainer", + // nosemgrep: sol-style-vm-env-only-in-config-sol + name: _isOPCMV2() ? "OPContractsManagerContainer" : "OPContractsManagerContractsContainer", addr: contractsContainerAddr, blueprint: false }); @@ -756,7 +773,7 @@ contract VerifyOPCM is Script { } /// @notice Uses the OPContractsManager ABI JSON and the live OPCM contract to extract a list - /// of contract names and their corresonding addresses for the various immutable + /// of contract names and their corresponding addresses for the various immutable /// references to other OPCM contracts. /// @param _opcm The live OPCM contract. /// @return Array of OpcmContractRef structs containing contract names/addresses. @@ -767,7 +784,7 @@ contract VerifyOPCM is Script { Process.bash( string.concat( "jq -r '[.abi[] | select(.name? and (.name | type == \"string\") and (.name | startswith(\"opcm\"))) | .name]' ", - _buildArtifactPath("OPContractsManager") + _buildArtifactPath(_opcmContractName()) ) ) ), @@ -824,7 +841,7 @@ contract VerifyOPCM is Script { "jq -r '[.abi[] | select(.name == \"", _property, "\") | .outputs[0].components[].name]' ", - _buildArtifactPath("OPContractsManager") + _buildArtifactPath(_opcmContractName()) ) ) ), @@ -869,6 +886,10 @@ contract VerifyOPCM is Script { /// @param _fieldName The field name to convert. /// @return The contract name. function _getContractNameFromFieldName(string memory _fieldName) internal view returns (string memory) { + if (LibString.eq(_fieldName, "contractsContainer")) { + _fieldName = _isOPCMV2() ? "contractsContainerV2" : "contractsContainerV1"; + } + // Check for an explicit override string memory overrideName = fieldNameOverrides[_fieldName]; if (bytes(overrideName).length > 0) { @@ -993,7 +1014,7 @@ contract VerifyOPCM is Script { Process.bash( string.concat( "jq -r '[.abi[] | select(.type == \"function\" and .stateMutability == \"view\" and (.inputs | length) == 0) | .name]' ", - _buildArtifactPath("OPContractsManager") + _buildArtifactPath(_opcmContractName()) ) ) ), @@ -1046,4 +1067,34 @@ contract VerifyOPCM is Script { revert VerifyOPCM_UnaccountedGetters(trimmedUnaccounted); } } + + /// @notice Returns the name of the OPCM contract depending on whether the OPCM is V2. + /// @return The name of the OPCM contract. + function _opcmContractName() internal view returns (string memory) { + return _isOPCMV2() ? "OPContractsManagerV2" : "OPContractsManager"; + } + + /// @notice Checks if the OPCM is V2. + /// @dev If the OPCM address is not set, default to false. + /// @return True if the OPCM is V2, false otherwise. + function _isOPCMV2() internal view returns (bool) { + // Get the OPCM contract address from the environment variables. + address opcmAddress = _getOPCMAddress(); + + // If the OPCM contract address is not set, default to V1. + if (opcmAddress == address(0)) { + return false; + } + + // If the OPCM contract version is greater than or equal to 7.0.0, then it is OPCM V2. + return SemverComp.gte(IOPContractsManager(opcmAddress).version(), "7.0.0"); + } + + /// @notice Gets the address of the OPCM contract from the environment variables. + /// @dev If not set, default to address(0). + /// @return The address of the OPCM contract. + function _getOPCMAddress() internal view returns (address) { + // nosemgrep: sol-style-vm-env-only-in-config-sol + return vm.envOr("OPCM_ADDRESS", address(0)); + } } diff --git a/packages/contracts-bedrock/scripts/libraries/Types.sol b/packages/contracts-bedrock/scripts/libraries/Types.sol index 7b90b7204e9..bec2f022e91 100644 --- a/packages/contracts-bedrock/scripts/libraries/Types.sol +++ b/packages/contracts-bedrock/scripts/libraries/Types.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.0; import { Claim, Duration, GameType } from "src/dispute/lib/Types.sol"; +import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; library Types { /// @notice Represents a set of L1 contracts. Used to represent a set of proxies. @@ -49,6 +50,8 @@ library Types { // Fee params uint32 operatorFeeScalar; uint64 operatorFeeConstant; + // Superchain contracts + ISuperchainConfig superchainConfig; // Whether to use the custom gas token. bool useCustomGasToken; } diff --git a/packages/contracts-bedrock/snapshots/abi/AccessManager.json b/packages/contracts-bedrock/snapshots/abi/AccessManager.json new file mode 100644 index 00000000000..adcdb13fd52 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/abi/AccessManager.json @@ -0,0 +1,285 @@ +[ + { + "inputs": [ + { + "internalType": "uint256", + "name": "_fallbackTimeout", + "type": "uint256" + }, + { + "internalType": "contract IDisputeGameFactory", + "name": "_disputeGameFactory", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "DEPLOYMENT_TIMESTAMP", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "DISPUTE_GAME_FACTORY", + "outputs": [ + { + "internalType": "contract IDisputeGameFactory", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "FALLBACK_TIMEOUT", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "challengers", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getLastProposalTimestamp", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_challenger", + "type": "address" + } + ], + "name": "isAllowedChallenger", + "outputs": [ + { + "internalType": "bool", + "name": "allowed_", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_proposer", + "type": "address" + } + ], + "name": "isAllowedProposer", + "outputs": [ + { + "internalType": "bool", + "name": "allowed_", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isProposalPermissionlessMode", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "proposers", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_challenger", + "type": "address" + }, + { + "internalType": "bool", + "name": "_allowed", + "type": "bool" + } + ], + "name": "setChallenger", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_proposer", + "type": "address" + }, + { + "internalType": "bool", + "name": "_allowed", + "type": "bool" + } + ], + "name": "setProposer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "challenger", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "allowed", + "type": "bool" + } + ], + "name": "ChallengerPermissionUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "proposer", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "allowed", + "type": "bool" + } + ], + "name": "ProposerPermissionUpdated", + "type": "event" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json index b5871765022..07ce75d6ff8 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json @@ -1104,6 +1104,11 @@ "name": "LatestReleaseNotSet", "type": "error" }, + { + "inputs": [], + "name": "OPContractsManager_V2Enabled", + "type": "error" + }, { "inputs": [], "name": "OnlyDelegatecall", diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerV2.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerV2.json index 59886e85dc8..17555aea061 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerV2.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerV2.json @@ -297,6 +297,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [], + "name": "devFeatureBitmap", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "implementations", @@ -447,7 +460,7 @@ }, { "inputs": [], - "name": "standardValidator", + "name": "opcmStandardValidator", "outputs": [ { "internalType": "contract IOPContractsManagerStandardValidator", @@ -460,7 +473,20 @@ }, { "inputs": [], - "name": "thisOPCM", + "name": "opcmUtils", + "outputs": [ + { + "internalType": "contract IOPContractsManagerUtils", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "opcmV2", "outputs": [ { "internalType": "contract OPContractsManagerV2", @@ -653,19 +679,6 @@ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [], - "name": "utils", - "outputs": [ - { - "internalType": "contract IOPContractsManagerUtils", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [], "name": "version", diff --git a/packages/contracts-bedrock/snapshots/abi/OPSuccinctFaultDisputeGame.json b/packages/contracts-bedrock/snapshots/abi/OptimisticZkGame.json similarity index 98% rename from packages/contracts-bedrock/snapshots/abi/OPSuccinctFaultDisputeGame.json rename to packages/contracts-bedrock/snapshots/abi/OptimisticZkGame.json index 9e60e00347a..0109d574c03 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPSuccinctFaultDisputeGame.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimisticZkGame.json @@ -112,7 +112,7 @@ "name": "challenge", "outputs": [ { - "internalType": "enum OPSuccinctFaultDisputeGame.ProposalStatus", + "internalType": "enum OptimisticZkGame.ProposalStatus", "name": "", "type": "uint8" } @@ -171,7 +171,7 @@ "type": "bytes32" }, { - "internalType": "enum OPSuccinctFaultDisputeGame.ProposalStatus", + "internalType": "enum OptimisticZkGame.ProposalStatus", "name": "status", "type": "uint8" }, @@ -413,7 +413,7 @@ "name": "prove", "outputs": [ { - "internalType": "enum OPSuccinctFaultDisputeGame.ProposalStatus", + "internalType": "enum OptimisticZkGame.ProposalStatus", "name": "", "type": "uint8" } diff --git a/packages/contracts-bedrock/snapshots/abi/SuperFaultDisputeGame.json b/packages/contracts-bedrock/snapshots/abi/SuperFaultDisputeGame.json index 598292e2f78..20554462ddb 100644 --- a/packages/contracts-bedrock/snapshots/abi/SuperFaultDisputeGame.json +++ b/packages/contracts-bedrock/snapshots/abi/SuperFaultDisputeGame.json @@ -659,6 +659,25 @@ "stateMutability": "pure", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_chainId", + "type": "uint256" + } + ], + "name": "rootClaimByChainId", + "outputs": [ + { + "internalType": "Claim", + "name": "outputRootClaim_", + "type": "bytes32" + } + ], + "stateMutability": "pure", + "type": "function" + }, { "inputs": [], "name": "splitDepth", @@ -939,6 +958,21 @@ "name": "DuplicateStep", "type": "error" }, + { + "inputs": [], + "name": "Encoding_EmptySuperRoot", + "type": "error" + }, + { + "inputs": [], + "name": "Encoding_InvalidSuperRootEncoding", + "type": "error" + }, + { + "inputs": [], + "name": "Encoding_InvalidSuperRootVersion", + "type": "error" + }, { "inputs": [], "name": "GameDepthExceeded", @@ -1045,6 +1079,11 @@ "name": "UnexpectedRootClaim", "type": "error" }, + { + "inputs": [], + "name": "UnknownChainId", + "type": "error" + }, { "inputs": [], "name": "ValidStep", diff --git a/packages/contracts-bedrock/snapshots/abi/SuperPermissionedDisputeGame.json b/packages/contracts-bedrock/snapshots/abi/SuperPermissionedDisputeGame.json index 829a145f051..26743d7e383 100644 --- a/packages/contracts-bedrock/snapshots/abi/SuperPermissionedDisputeGame.json +++ b/packages/contracts-bedrock/snapshots/abi/SuperPermissionedDisputeGame.json @@ -685,6 +685,25 @@ "stateMutability": "pure", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_chainId", + "type": "uint256" + } + ], + "name": "rootClaimByChainId", + "outputs": [ + { + "internalType": "Claim", + "name": "outputRootClaim_", + "type": "bytes32" + } + ], + "stateMutability": "pure", + "type": "function" + }, { "inputs": [], "name": "splitDepth", @@ -970,6 +989,21 @@ "name": "DuplicateStep", "type": "error" }, + { + "inputs": [], + "name": "Encoding_EmptySuperRoot", + "type": "error" + }, + { + "inputs": [], + "name": "Encoding_InvalidSuperRootEncoding", + "type": "error" + }, + { + "inputs": [], + "name": "Encoding_InvalidSuperRootVersion", + "type": "error" + }, { "inputs": [], "name": "GameDepthExceeded", @@ -1076,6 +1110,11 @@ "name": "UnexpectedRootClaim", "type": "error" }, + { + "inputs": [], + "name": "UnknownChainId", + "type": "error" + }, { "inputs": [], "name": "ValidStep", diff --git a/packages/contracts-bedrock/snapshots/abi_loader.go b/packages/contracts-bedrock/snapshots/abi_loader.go index 8a69f9321a9..83119016ae9 100644 --- a/packages/contracts-bedrock/snapshots/abi_loader.go +++ b/packages/contracts-bedrock/snapshots/abi_loader.go @@ -16,7 +16,7 @@ var superFaultDisputeGame []byte //go:embed abi/FaultDisputeGame.json var faultDisputeGame []byte -//go:embed abi/OPSuccinctFaultDisputeGame.json +//go:embed abi/OptimisticZkGame.json var zkDisputeGame []byte //go:embed abi/PreimageOracle.json diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 68fd89aff3d..dc38b2a0205 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -24,8 +24,8 @@ "sourceCodeHash": "0xfca613b5d055ffc4c3cbccb0773ddb9030abedc1aa6508c9e2e7727cc0cd617b" }, "src/L1/OPContractsManager.sol:OPContractsManager": { - "initCodeHash": "0x51bb4fa1d01503ec16e8611ac1e2f042ea51280310f1cca2a15a4826acfc2db5", - "sourceCodeHash": "0xacc5a0e75797686ad9545dcae82c89b2ca847ba42988eb63466ef03f4e1c739e" + "initCodeHash": "0xdbf23ba71f865d1c3086a10b48f8faaa21ed0d689fdd14ec9ad988f8f013b5c3", + "sourceCodeHash": "0x048f592543a93c05085919f6a1670600ead00991e8370ae83fea1665ca09a5b4" }, "src/L1/OPContractsManagerStandardValidator.sol:OPContractsManagerStandardValidator": { "initCodeHash": "0xdec828fdb9f9bb7a35ca03d851b041fcd088681957642e949b5d320358d9b9a1", @@ -36,8 +36,8 @@ "sourceCodeHash": "0x16fb96f4d29a10d03b3b9c70edf56df51e97c2a1a3f0ba36aae79469b446ad5c" }, "src/L1/OptimismPortalInterop.sol:OptimismPortalInterop": { - "initCodeHash": "0x087281cd2a48e882648c09fa90bfcca7487d222e16300f9372deba6b2b8ccfad", - "sourceCodeHash": "0x1cc641a4272aea85e13cbf42d9032d1b91ef858eafe3be6b5649cc8504c9cf69" + "initCodeHash": "0xd361ed3b8d56dcc1f3c068ef3af9c83f3da1165bcdab097250ad4772f350c52e", + "sourceCodeHash": "0xd7d2166d29a22f3a051bc832cbce05f9ca06f1ac1bfb0790f29579f12bb95b8f" }, "src/L1/ProtocolVersions.sol:ProtocolVersions": { "initCodeHash": "0xcb59ad9a5ec2a0831b7f4daa74bdacba82ffa03035dafb499a732c641e017f4e", @@ -52,8 +52,8 @@ "sourceCodeHash": "0xb3184aa5d95a82109e7134d1f61941b30e25f655b9849a0e303d04bbce0cde0b" }, "src/L1/opcm/OPContractsManagerV2.sol:OPContractsManagerV2": { - "initCodeHash": "0x7648333338c6ca7e7e39421259b7c78174771263d84d03f9e363a8e0f9ba74f9", - "sourceCodeHash": "0x4c4f6079034ed5dfd0bf242a57a353583ed9ff948b6f9018d91b091a555574db" + "initCodeHash": "0x4d2822fdc1f81c51f843d027ccece5c4e847f7c5870bb068a05bd568f7354c22", + "sourceCodeHash": "0xdb47dbef330b9a8c6e644d5791ed3414796381d90b22078db682b14de30ed16d" }, "src/L2/BaseFeeVault.sol:BaseFeeVault": { "initCodeHash": "0x838bbd7f381e84e21887f72bd1da605bfc4588b3c39aed96cbce67c09335b3ee", @@ -88,8 +88,8 @@ "sourceCodeHash": "0x34186bcab29963237b4e0d7575b0a1cff7caf42ccdb55d4b2b2c767db3279189" }, "src/L2/L1Withdrawer.sol:L1Withdrawer": { - "initCodeHash": "0x91e0be0d49636212678191c06b9b6840c399f08ad946bc7b52f24231691be28b", - "sourceCodeHash": "0x25422bdaf51d611c1688a835737368c0ff2ab639dac852af8a20ebb4e16fc103" + "initCodeHash": "0x6efb9055142e90b408c6312074243769df0d365f6f984e226e0320bec55a45b8", + "sourceCodeHash": "0x6a12e541b47b79f19d1061ff7b64ffdcffa1e8d06225cca6798daca53fd96890" }, "src/L2/L2CrossDomainMessenger.sol:L2CrossDomainMessenger": { "initCodeHash": "0xe160be403df12709c371c33195d1b9c3b5e9499e902e86bdabc8eed749c3fd61", @@ -204,12 +204,12 @@ "sourceCodeHash": "0x335a503a4cc02dd30d88d163393680f3fd89168e0faa4fa4b0ae5da399656f91" }, "src/dispute/SuperFaultDisputeGame.sol:SuperFaultDisputeGame": { - "initCodeHash": "0x388bba903940171322e271b37a9359d4da100afb809a6d6b6b9e798532fca743", - "sourceCodeHash": "0xc29624464bce5382394b281b1e444a248f463f3879381bc7cf4f80c08c5c98ea" + "initCodeHash": "0xb5ce71bc56109055cd0dc71fc63015443bbdb29c5975e049802cd1b5188f06ca", + "sourceCodeHash": "0x3096a447574168528555f091a357a120b1dee6f35b50634b7d705ace1ef9c0ad" }, "src/dispute/SuperPermissionedDisputeGame.sol:SuperPermissionedDisputeGame": { - "initCodeHash": "0x4686e904225a7bce9b7d6b7e9827f557881cf4fab375df3a565b5667062db923", - "sourceCodeHash": "0x00e899175371a254ff49f3b485f60a4e8db9ade04d1807df5934c8b3fe9b2816" + "initCodeHash": "0xa080730728e812e8b02d03b5857b23d16ade46f1656f26f22274835a3100edd7", + "sourceCodeHash": "0x314b6e0412f698ce3531e8176ce8e5b8a3976cc3fa9d7ecb1f3278612f90ed4e" }, "src/dispute/v2/FaultDisputeGameV2.sol:FaultDisputeGameV2": { "initCodeHash": "0x6fc59e2da083c9e2093e42b0fda705e8215cc216e4dcedbf728c08f69ec2d3bd", @@ -219,9 +219,9 @@ "initCodeHash": "0x9896fd04e9a3f9fe4f1d6e93eb298b37a6bfa33424aa705e68cc58d0ba7f3f90", "sourceCodeHash": "0xc0ff6e93b6e2b9111c11e81b5df8948ab71d02b9d2c4dfda982fcb615519f1f7" }, - "src/dispute/zk/OPSuccinctFaultDisputeGame.sol:OPSuccinctFaultDisputeGame": { - "initCodeHash": "0xb9d0d9ca4df242f188b2d5be7d692459a12409a67a6504ef44ef589c6ca2c1a9", - "sourceCodeHash": "0x85f80adb845f59e9137d462e219c0cdba27058be77d855075e286aa316735aa0" + "src/dispute/zk/OptimisticZkGame.sol:OptimisticZkGame": { + "initCodeHash": "0x0e905fc81f45b1e9aa786e66bfe70b3ec4abd8d550be5d4c8f43cdad6f2618b2", + "sourceCodeHash": "0x162408c90e8d9d8afd982e9825db093eba5c387cbe1145c1a9b86e2361547138" }, "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x2e0ef4c341367eb59cc6c25190c64eff441d3fe130189da91d4d126f6bdbc9b5", diff --git a/packages/contracts-bedrock/snapshots/storageLayout/AccessManager.json b/packages/contracts-bedrock/snapshots/storageLayout/AccessManager.json new file mode 100644 index 00000000000..dbeafc3e604 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/storageLayout/AccessManager.json @@ -0,0 +1,23 @@ +[ + { + "bytes": "20", + "label": "_owner", + "offset": 0, + "slot": "0", + "type": "address" + }, + { + "bytes": "32", + "label": "proposers", + "offset": 0, + "slot": "1", + "type": "mapping(address => bool)" + }, + { + "bytes": "32", + "label": "challengers", + "offset": 0, + "slot": "2", + "type": "mapping(address => bool)" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OPSuccinctFaultDisputeGame.json b/packages/contracts-bedrock/snapshots/storageLayout/OptimisticZkGame.json similarity index 95% rename from packages/contracts-bedrock/snapshots/storageLayout/OPSuccinctFaultDisputeGame.json rename to packages/contracts-bedrock/snapshots/storageLayout/OptimisticZkGame.json index d50456001fd..34ce5c1cfc3 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/OPSuccinctFaultDisputeGame.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/OptimisticZkGame.json @@ -32,7 +32,7 @@ "label": "claimData", "offset": 0, "slot": "1", - "type": "struct OPSuccinctFaultDisputeGame.ClaimData" + "type": "struct OptimisticZkGame.ClaimData" }, { "bytes": "32", diff --git a/packages/contracts-bedrock/src/L1/OPContractsManager.sol b/packages/contracts-bedrock/src/L1/OPContractsManager.sol index b35ee3d98a4..85332af9532 100644 --- a/packages/contracts-bedrock/src/L1/OPContractsManager.sol +++ b/packages/contracts-bedrock/src/L1/OPContractsManager.sol @@ -1977,9 +1977,9 @@ contract OPContractsManager is ISemver { // -------- Constants and Variables -------- - /// @custom:semver 6.0.0 + /// @custom:semver 6.0.1 function version() public pure virtual returns (string memory) { - return "6.0.0"; + return "6.0.1"; } OPContractsManagerGameTypeAdder public immutable opcmGameTypeAdder; @@ -2046,6 +2046,9 @@ contract OPContractsManager is ISemver { /// @notice Thrown if logic gated by a dev feature flag is incorrectly accessed. error InvalidDevFeatureAccess(bytes32 devFeature); + /// @notice Thrown when OPCM v2 is enabled via dev feature flag. + error OPContractsManager_V2Enabled(); + // -------- Methods -------- constructor( @@ -2130,6 +2133,8 @@ contract OPContractsManager is ISemver { /// @param _input The deploy input parameters for the deployment. /// @return The deploy output values of the deployment. function deploy(DeployInput calldata _input) external virtual returns (DeployOutput memory) { + _assertV2NotEnabled(); + return opcmDeployer.deploy(_input, superchainConfig, msg.sender); } @@ -2139,6 +2144,8 @@ contract OPContractsManager is ISemver { /// `_opChainConfigs`'s ProxyAdmin. /// @dev This function requires that each chain's superchainConfig is already upgraded. function upgrade(OpChainConfig[] memory _opChainConfigs) external virtual { + _assertV2NotEnabled(); + if (address(this) == address(thisOPCM)) revert OnlyDelegatecall(); bytes memory data = abi.encodeCall(OPContractsManagerUpgrader.upgrade, (_opChainConfigs)); @@ -2150,6 +2157,8 @@ contract OPContractsManager is ISemver { /// @dev This function is intended to be DELEGATECALLed by the superchainConfig's ProxyAdminOwner. /// @dev This function will revert if the SuperchainConfig is already at or above the target version. function upgradeSuperchainConfig(ISuperchainConfig _superchainConfig) external { + _assertV2NotEnabled(); + if (address(this) == address(thisOPCM)) revert OnlyDelegatecall(); bytes memory data = abi.encodeCall(OPContractsManagerUpgrader.upgradeSuperchainConfig, (_superchainConfig)); @@ -2159,6 +2168,8 @@ contract OPContractsManager is ISemver { /// @notice addGameType deploys a new dispute game and links it to the DisputeGameFactory. The inputted _gameConfigs /// must be added in ascending GameType order. function addGameType(AddGameInput[] memory _gameConfigs) public virtual returns (AddGameOutput[] memory) { + _assertV2NotEnabled(); + if (address(this) == address(thisOPCM)) revert OnlyDelegatecall(); bytes memory data = abi.encodeCall(OPContractsManagerGameTypeAdder.addGameType, (_gameConfigs)); @@ -2170,6 +2181,8 @@ contract OPContractsManager is ISemver { /// @notice Updates the prestate hash for dispute games while keeping all other parameters the same /// @param _prestateUpdateInputs The new prestate hashes to use function updatePrestate(UpdatePrestateInput[] memory _prestateUpdateInputs) public { + _assertV2NotEnabled(); + if (address(this) == address(thisOPCM)) revert OnlyDelegatecall(); bytes memory data = abi.encodeCall(OPContractsManagerGameTypeAdder.updatePrestate, (_prestateUpdateInputs)); @@ -2180,6 +2193,8 @@ contract OPContractsManager is ISemver { /// @notice Migrates the Optimism contracts to the latest version. /// @param _input Input parameters for the migration. function migrate(OPContractsManagerInteropMigrator.MigrateInput calldata _input) external virtual { + _assertV2NotEnabled(); + if (address(this) == address(thisOPCM)) revert OnlyDelegatecall(); bytes memory data = abi.encodeCall(OPContractsManagerInteropMigrator.migrate, (_input)); @@ -2220,6 +2235,13 @@ contract OPContractsManager is ISemver { return opcmDeployer.isDevFeatureEnabled(_feature); } + /// @notice Reverts if the dev feature flag for OPCM v2 is enabled. + function _assertV2NotEnabled() internal view { + if (isDevFeatureEnabled(DevFeatures.OPCM_V2)) { + revert OPContractsManager_V2Enabled(); + } + } + /// @notice Helper function to perform a delegatecall to a target contract /// @param _target The target contract address /// @param _data The calldata to send to the target diff --git a/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol b/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol index 54db98d889f..dec588d5cc1 100644 --- a/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol +++ b/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol @@ -229,9 +229,9 @@ contract OptimismPortalInterop is Initializable, ResourceMetering, Reinitializab error OptimismPortal_MigratingToSameRegistry(); /// @notice Semantic version. - /// @custom:semver 5.1.0+interop + /// @custom:semver 5.2.0+interop function version() public pure virtual returns (string memory) { - return "5.1.0+interop"; + return "5.2.0+interop"; } /// @param _proofMaturityDelaySeconds The proof maturity delay in seconds. diff --git a/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerUtilsCaller.sol b/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerUtilsCaller.sol index 232deac1dac..22e4ce10ce1 100644 --- a/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerUtilsCaller.sol +++ b/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerUtilsCaller.sol @@ -15,11 +15,11 @@ import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; /// this is much easier for humans to read and for us to validate offchain. abstract contract OPContractsManagerUtilsCaller { /// @notice Address of the OPContractsManagerUtils contract. - IOPContractsManagerUtils public immutable utils; + IOPContractsManagerUtils public immutable opcmUtils; /// @param _utils Address of the OPContractsManagerUtils contract. constructor(IOPContractsManagerUtils _utils) { - utils = _utils; + opcmUtils = _utils; } /// @notice Maps an L2 chain ID to an L1 batch inbox address as defined by the standard @@ -185,7 +185,7 @@ abstract contract OPContractsManagerUtilsCaller { /// @param _data Calldata to send to the utils contract. /// @return Result of the call. function _delegatecall(bytes memory _data) internal returns (bytes memory) { - (bool success, bytes memory result) = address(utils).delegatecall(_data); + (bool success, bytes memory result) = address(opcmUtils).delegatecall(_data); if (!success) { assembly { revert(add(result, 0x20), mload(result)) @@ -198,7 +198,7 @@ abstract contract OPContractsManagerUtilsCaller { /// @param _data Calldata to send to the utils contract. /// @return Result of the call. function _staticcall(bytes memory _data) internal view returns (bytes memory) { - (bool success, bytes memory result) = address(utils).staticcall(_data); + (bool success, bytes memory result) = address(opcmUtils).staticcall(_data); if (!success) { assembly { revert(add(result, 0x20), mload(result)) diff --git a/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerV2.sol b/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerV2.sol index d59a081cf25..3b937106b27 100644 --- a/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerV2.sol +++ b/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerV2.sol @@ -158,20 +158,20 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller { IOPContractsManagerContainer public immutable contractsContainer; /// @notice Address of the Standard Validator for this OPCM release. - IOPContractsManagerStandardValidator public immutable standardValidator; + IOPContractsManagerStandardValidator public immutable opcmStandardValidator; /// @notice Immutable reference to this OPCM contract so that the address of this contract can /// be used when this contract is DELEGATECALLed. - OPContractsManagerV2 public immutable thisOPCM; + OPContractsManagerV2 public immutable opcmV2; /// @notice The version of the OPCM contract. /// WARNING: OPCM versioning rules differ from other contracts: /// - Major bump: New required sequential upgrade /// - Minor bump: Replacement OPCM for same upgrade /// - Patch bump: Development changes (expected for normal dev work) - /// @custom:semver 7.0.0 + /// @custom:semver 7.0.1 function version() public pure returns (string memory) { - return "7.0.0"; + return "7.0.1"; } /// @param _contractsContainer The container of blueprint and implementation contract addresses. @@ -185,8 +185,8 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller { OPContractsManagerUtilsCaller(_utils) { contractsContainer = _contractsContainer; - standardValidator = _standardValidator; - thisOPCM = this; + opcmStandardValidator = _standardValidator; + opcmV2 = this; } /////////////////////////////////////////////////////////////////////////// @@ -905,7 +905,7 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller { optimismPortal: address(_cts.optimismPortal), optimismMintableERC20Factory: address(_cts.optimismMintableERC20Factory), delayedWETH: address(_cts.delayedWETH), - opcm: address(thisOPCM) + opcm: address(opcmV2) }); // Generate the initializer arguments. @@ -1022,7 +1022,7 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller { // 1. Address of the last used OPCM is identical to the address of this OPCM (re-running). // 2. This OPCM version is the same major version but a greater minor version (patch). // 3. This OPCM version is the next major version (sequential upgrade). - bool isSameOPCM = address(lastUsedOPCM) == address(thisOPCM); + bool isSameOPCM = address(lastUsedOPCM) == address(opcmV2); bool isNextMajor = thisSemver.major == lastUsedSemver.major + 1; bool isSameMajorHigherMinor = thisSemver.major == lastUsedSemver.major && thisSemver.minor > lastUsedSemver.minor; @@ -1052,10 +1052,16 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller { /////////////////////////////////////////////////////////////////////////// /// @notice Helper for retrieving the version of the OPCM contract. - /// @dev We use thisOPCM.version() because it allows us to properly mock the version function + /// @dev We use opcmV2.version() because it allows us to properly mock the version function /// in tests without running into issues because this contract is being DELEGATECALLed. /// @return The version of the OPCM contract. function _version() internal view returns (string memory) { - return thisOPCM.version(); + return opcmV2.version(); + } + + /// @notice Returns the development feature bitmap. + /// @return The development feature bitmap. + function devFeatureBitmap() public view returns (bytes32) { + return contractsContainer.devFeatureBitmap(); } } diff --git a/packages/contracts-bedrock/src/L2/L1Withdrawer.sol b/packages/contracts-bedrock/src/L2/L1Withdrawer.sol index bbaf18e4638..8b55fdea67e 100644 --- a/packages/contracts-bedrock/src/L2/L1Withdrawer.sol +++ b/packages/contracts-bedrock/src/L2/L1Withdrawer.sol @@ -51,14 +51,14 @@ contract L1Withdrawer is ISemver { event WithdrawalGasLimitUpdated(uint32 oldWithdrawalGasLimit, uint32 newWithdrawalGasLimit); /// @notice Semantic version. - /// @custom:semver 1.0.0 - string public constant version = "1.0.0"; + /// @custom:semver 1.0.1 + string public constant version = "1.0.1"; /// @notice Constructs the L1Withdrawer contract. /// @param _minWithdrawalAmount The minimum amount of ETH required to trigger a withdrawal. /// @param _recipient The L1 address that will receive withdrawals. /// @param _withdrawalGasLimit The gas limit for the L1 withdrawal transaction. - /// @dev If target on L1 is `FeesDepositor`, the gas limit should be above 800k gas. + /// @dev If target on L1 is `FeesDepositor`, the gas limit should be at or above 800k gas. constructor(uint256 _minWithdrawalAmount, address _recipient, uint32 _withdrawalGasLimit) { minWithdrawalAmount = _minWithdrawalAmount; recipient = _recipient; @@ -105,7 +105,7 @@ contract L1Withdrawer is ISemver { /// @notice Updates the withdrawal gas limit. Only callable by the ProxyAdmin owner. /// @param _newWithdrawalGasLimit The new withdrawal gas limit. - /// @dev If target on L1 is `FeesDepositor`, the gas limit should be above 800k gas. + /// @dev If target on L1 is `FeesDepositor`, the gas limit should be at or above 800k gas. function setWithdrawalGasLimit(uint32 _newWithdrawalGasLimit) external { if (msg.sender != IProxyAdmin(Predeploys.PROXY_ADMIN).owner()) { revert L1Withdrawer_OnlyProxyAdminOwner(); diff --git a/packages/contracts-bedrock/src/dispute/SuperFaultDisputeGame.sol b/packages/contracts-bedrock/src/dispute/SuperFaultDisputeGame.sol index 05578687eb2..d34297c51db 100644 --- a/packages/contracts-bedrock/src/dispute/SuperFaultDisputeGame.sol +++ b/packages/contracts-bedrock/src/dispute/SuperFaultDisputeGame.sol @@ -19,6 +19,7 @@ import { LocalPreimageKey, VMStatuses } from "src/dispute/lib/Types.sol"; +import { Types } from "src/libraries/Types.sol"; import { Position, LibPosition } from "src/dispute/lib/LibPosition.sol"; import { InvalidParent, @@ -50,8 +51,11 @@ import { InvalidBondDistributionMode, GameNotResolved, GamePaused, - BadExtraData + BadExtraData, + UnknownChainId } from "src/dispute/lib/Errors.sol"; +import { Hashing } from "src/libraries/Hashing.sol"; +import { Encoding } from "src/libraries/Encoding.sol"; // Interfaces import { ISemver } from "interfaces/universal/ISemver.sol"; @@ -142,9 +146,9 @@ contract SuperFaultDisputeGame is Clone, ISemver { Position internal constant ROOT_POSITION = Position.wrap(1); /// @notice Semantic version. - /// @custom:semver 0.6.0 + /// @custom:semver 0.7.0 function version() public pure virtual returns (string memory) { - return "0.6.0"; + return "0.7.0"; } /// @notice The starting timestamp of the game @@ -247,7 +251,15 @@ contract SuperFaultDisputeGame is Clone, ISemver { // This is to prevent adding extra or omitting bytes from to `extraData` that result in a different game UUID // in the factory, but are not used by the game, which would allow for multiple dispute games for the same // super proposal to be created. - if (msg.data.length != expectedInitCallDataLength()) revert BadExtraData(); + if (!_verifyInitCallDataLength()) revert BadExtraData(); + + // Sanity check to prevent initializing right away with an invalid claim state that is used as convention + // Should be impossible to find a valid Super preimage of INVALID_ROOT_CLAIM + if (rootClaim().raw() == INVALID_ROOT_CLAIM) revert SuperFaultDisputeGameInvalidRootClaim(); + + // Revert if the super root proof in extraData does not match the root claim. + Types.SuperRootProof memory superRootProof = Encoding.decodeSuperRootProof(extraData()); + if (Hashing.hashSuperRootProof(superRootProof) != rootClaim().raw()) revert BadExtraData(); // Grab the latest anchor root. (Hash root, uint256 rootL2SequenceNumber) = anchorStateRegistry().getAnchorRoot(); @@ -255,9 +267,6 @@ contract SuperFaultDisputeGame is Clone, ISemver { // Should only happen if this is a new game type that hasn't been set up yet. if (root.raw() == bytes32(0)) revert AnchorRootNotFound(); - // Prevent initializing right away with an invalid claim state that is used as convention - if (rootClaim().raw() == INVALID_ROOT_CLAIM) revert SuperFaultDisputeGameInvalidRootClaim(); - // Set the starting Proposal. startingProposal = Proposal({ l2SequenceNumber: rootL2SequenceNumber, root: root }); @@ -314,29 +323,79 @@ contract SuperFaultDisputeGame is Clone, ISemver { GameType.unwrap(anchorStateRegistry().respectedGameType()) == GameType.unwrap(gameType()); } - /// @notice Returns the expected calldata length for the initialize method - function expectedInitCallDataLength() internal pure returns (uint256) { - // Expected length: 6 bytes + immutable args byte count - // - 4 bytes: selector - // - 2 bytes: CWIA length prefix - // - n bytes: Immutable args data - return 6 + immutableArgsByteCount(); + /// @notice Validates the expected length of msg.data for the initialize() call. + /// @dev This function must only be called by initialize(). + /// Note that implementations may override the expected game impl args (see SuperPermissionedDisputeGame). + /// + /// Expected msg.data structure: + /// ┌────────────────────────────────────────────────────────────────────┐ + /// │ 4 bytes │ Function selector (initialize()) │ + /// │ 2 bytes │ CWIA length prefix │ + /// │===================│ ============ pre extra data ================== │ + /// │ 20 bytes │ creator address │ + /// │ 32 bytes │ root claim │ + /// │ 32 bytes │ l1 head │ + /// │ 4 bytes │ game type │ + /// │===================│ ============ extra data ====================== │ + /// │ 1 byte │ super version │ + /// │ 8 bytes │ super timestamp (seqnr) │ + /// │ n * (32+32) bytes │ (chainId, outputRoot) tuples │ + /// │===================│ ============ end extra data ================== │ + /// │ 124 bytes │ game impl args │ + /// └────────────────────────────────────────────────────────────────────┘ + function _verifyInitCallDataLength() internal pure returns (bool) { + uint256 preExtraDataLen = 4 + 2 + _preExtraDataByteCount(); + if (msg.data.length < preExtraDataLen) { + return false; + } + + uint256 postExtraDataLen = gameImplArgsByteCount(); + uint256 extraDataAndGameArgsLength = msg.data.length - preExtraDataLen; + // ensure we have enough data for the game impl args + if (extraDataAndGameArgsLength < postExtraDataLen) { + return false; + } + + uint256 superLen = extraDataAndGameArgsLength - postExtraDataLen; + if (superLen < 9) { + return false; + } + uint256 rem = superLen - 9; + // there must be at least one (chainId, outputRoot) tuple + if (rem == 0) { + return false; + } + return (rem % 64) == 0; + } + + /// @notice Returns the length of the super extra data in the initialize() call. + /// @dev Precondition: msg.data has a valid length. + function _extraDataByteCount() internal pure returns (uint256) { + // The CWIA runtime appends the immutable args and a 2-byte length suffix to every call; + // strip the original calldata and suffix so offsets stay correct for functions with params. + uint256 immutableArgsLength = msg.data.length - _getImmutableArgsOffset() - 2; + return immutableArgsLength - _preExtraDataByteCount() - gameImplArgsByteCount(); } - /// @notice Returns the byte count of the immutable args for this contract. - function immutableArgsByteCount() internal pure virtual returns (uint256) { - // Expected length: 244 bytes + /// @notice Returns the byte count of the data before the extra data (super) in the initialize() call. + function _preExtraDataByteCount() internal pure returns (uint256) { + // Expected length: 88 bytes // - 20 bytes: creator address // - 32 bytes: root claim // - 32 bytes: l1 head - // - 4 bytes: game type - // - 32 bytes: extraData + // - 4 bytes: game type + return 88; + } + + /// @notice Returns the byte count of the game implementation args for this contract. + function gameImplArgsByteCount() internal pure virtual returns (uint256) { + // Expected length: 124 bytes // - 32 bytes: absolutePrestate // - 20 bytes: vm address // - 20 bytes: anchorStateRegistry address // - 20 bytes: weth address // - 32 bytes: l2ChainId (unused) - return 244; + return 124; } //////////////////////////////////////////////////////////////// @@ -608,7 +667,7 @@ contract SuperFaultDisputeGame is Clone, ISemver { /// @notice The l2SequenceNumber (timestamp) of the disputed super root in game root claim. function l2SequenceNumber() public pure returns (uint256 l2SequenceNumber_) { - l2SequenceNumber_ = _getArgUint256(88); + l2SequenceNumber_ = _getArgUint64(89); } /// @notice Only the starting sequence number (timestamp) of the game. @@ -764,6 +823,21 @@ contract SuperFaultDisputeGame is Clone, ISemver { rootClaim_ = Claim.wrap(_getArgBytes32(20)); } + /// @notice Returns the output root in the root claim for the specified L2 chain ID. + /// @param _chainId The L2 chain ID to get the output root claim for. + /// @return outputRootClaim_ The output root claim for the specified L2 chain ID. + function rootClaimByChainId(uint256 _chainId) public pure returns (Claim outputRootClaim_) { + Types.SuperRootProof memory superRootProof = Encoding.decodeSuperRootProof(extraData()); + Types.OutputRootWithChainId[] memory outputRoots = superRootProof.outputRoots; + + for (uint256 i = 0; i < outputRoots.length; i++) { + if (outputRoots[i].chainId == _chainId) { + return Claim.wrap(outputRoots[i].root); + } + } + revert UnknownChainId(); + } + /// @notice Getter for the parent hash of the L1 block when the dispute game was created. /// @dev `clones-with-immutable-args` argument #3 /// @return l1Head_ The parent hash of the L1 block when the dispute game was created. @@ -782,44 +856,42 @@ contract SuperFaultDisputeGame is Clone, ISemver { /// @dev `clones-with-immutable-args` argument #4 /// @return extraData_ Any extra data supplied to the dispute game contract by the creator. function extraData() public pure returns (bytes memory extraData_) { - // The extra data starts at the second word within the cwia calldata and - // is 32 bytes long. - extraData_ = _getArgBytes(88, 32); + extraData_ = _getArgBytes(88, _extraDataByteCount()); } /// @notice Getter for the absolute prestate of the instruction trace. /// @dev `clones-with-immutable-args` argument #6 /// @return absolutePrestate_ The absolute prestate of the instruction trace. function absolutePrestate() public pure returns (Claim absolutePrestate_) { - absolutePrestate_ = Claim.wrap(_getArgBytes32(120)); + absolutePrestate_ = Claim.wrap(_getArgBytes32(_preExtraDataByteCount() + _extraDataByteCount())); } /// @notice Getter for the VM implementation. /// @dev `clones-with-immutable-args` argument #7 /// @return vm_ The onchain VM implementation address. function vm() public pure returns (IBigStepper vm_) { - vm_ = IBigStepper(_getArgAddress(152)); + vm_ = IBigStepper(_getArgAddress(_preExtraDataByteCount() + _extraDataByteCount() + 32)); } /// @notice Getter for the anchor state registry. /// @dev `clones-with-immutable-args` argument #8 /// @return registry_ The anchor state registry contract address. function anchorStateRegistry() public pure returns (IAnchorStateRegistry registry_) { - registry_ = IAnchorStateRegistry(_getArgAddress(172)); + registry_ = IAnchorStateRegistry(_getArgAddress(_preExtraDataByteCount() + _extraDataByteCount() + 52)); } /// @notice Getter for the WETH contract. /// @dev `clones-with-immutable-args` argument #9 /// @return weth_ The WETH contract for holding ETH. function weth() public pure returns (IDelayedWETH weth_) { - weth_ = IDelayedWETH(payable(_getArgAddress(192))); + weth_ = IDelayedWETH(payable(_getArgAddress(_preExtraDataByteCount() + _extraDataByteCount() + 72))); } /// @notice Getter for the L2 chain ID. /// @dev `clones-with-immutable-args` argument #10 /// @return l2ChainId_ The L2 chain ID. function _l2ChainId() internal pure returns (uint256 l2ChainId_) { - l2ChainId_ = _getArgUint256(212); + l2ChainId_ = _getArgUint256(_preExtraDataByteCount() + _extraDataByteCount() + 92); } /// @notice A compliant implementation of this interface should return the components of the diff --git a/packages/contracts-bedrock/src/dispute/SuperPermissionedDisputeGame.sol b/packages/contracts-bedrock/src/dispute/SuperPermissionedDisputeGame.sol index ae64b35dd3b..a8c754f8a2f 100644 --- a/packages/contracts-bedrock/src/dispute/SuperPermissionedDisputeGame.sol +++ b/packages/contracts-bedrock/src/dispute/SuperPermissionedDisputeGame.sol @@ -27,9 +27,9 @@ contract SuperPermissionedDisputeGame is SuperFaultDisputeGame { } /// @notice Semantic version. - /// @custom:semver 0.6.0 + /// @custom:semver 0.7.0 function version() public pure override returns (string memory) { - return "0.6.0"; + return "0.7.0"; } /// @param _params Parameters for creating a new FaultDisputeGame. @@ -76,11 +76,12 @@ contract SuperPermissionedDisputeGame is SuperFaultDisputeGame { if (tx.origin != proposer()) revert BadAuth(); } - function immutableArgsByteCount() internal pure override returns (uint256) { + /// @notice Returns the byte count of the game implementation args for this contract. + function gameImplArgsByteCount() internal pure override returns (uint256) { // Extend expected data length to account for proposer and challenger addresses // - 20 bytes: proposer address // - 20 bytes: challenger address - return super.immutableArgsByteCount() + 40; + return super.gameImplArgsByteCount() + 40; } //////////////////////////////////////////////////////////////// @@ -90,11 +91,14 @@ contract SuperPermissionedDisputeGame is SuperFaultDisputeGame { /// @notice Returns the proposer address. The proposer role is allowed to create proposals and participate in the /// dispute game. function proposer() public pure returns (address proposer_) { - proposer_ = _getArgAddress(super.immutableArgsByteCount()); + proposer_ = + _getArgAddress(super._preExtraDataByteCount() + super._extraDataByteCount() + super.gameImplArgsByteCount()); } /// @notice Returns the challenger address. The challenger role is allowed to participate in the dispute game. function challenger() public pure returns (address challenger_) { - challenger_ = _getArgAddress(super.immutableArgsByteCount() + 20); + challenger_ = _getArgAddress( + super._preExtraDataByteCount() + super._extraDataByteCount() + super.gameImplArgsByteCount() + 20 + ); } } diff --git a/packages/contracts-bedrock/src/dispute/lib/Errors.sol b/packages/contracts-bedrock/src/dispute/lib/Errors.sol index 223bf6281f8..5779fc3f92e 100644 --- a/packages/contracts-bedrock/src/dispute/lib/Errors.sol +++ b/packages/contracts-bedrock/src/dispute/lib/Errors.sol @@ -151,7 +151,7 @@ error GamePaused(); error InvalidGameArgsLength(); //////////////////////////////////////////////////////////////// -// `OPSuccinctFaultDisputeGame` Errors // +// `OptimisticZkGame` Errors // //////////////////////////////////////////////////////////////// /// @notice Thrown when the claim has already been challenged. @@ -177,3 +177,10 @@ error InvalidProposalStatus(); /// @notice Thrown when the game is initialized by an incorrect factory. error IncorrectDisputeGameFactory(); + +//////////////////////////////////////////////////////////////// +// `SuperFaultDisputeGame` Errors // +//////////////////////////////////////////////////////////////// + +/// @notice Thrown when an unknown chain ID is encountered. +error UnknownChainId(); diff --git a/packages/contracts-bedrock/src/dispute/lib/Types.sol b/packages/contracts-bedrock/src/dispute/lib/Types.sol index 54f795db549..eb9438e2b5b 100644 --- a/packages/contracts-bedrock/src/dispute/lib/Types.sol +++ b/packages/contracts-bedrock/src/dispute/lib/Types.sol @@ -89,6 +89,8 @@ library GameTypes { /// @notice A dispute game type that uses RISC Zero's Kailua GameType internal constant KAILUA = GameType.wrap(1337); + + GameType internal constant OPTIMISTIC_ZK_GAME_TYPE = GameType.wrap(10); } /// @title VMStatuses @@ -127,11 +129,9 @@ library LocalPreimageKey { } //////////////////////////////////////////////////////////////// -// `OPSuccinctFaultDisputeGame` Types // +// `OptimisticZkGame` Types // //////////////////////////////////////////////////////////////// -uint32 constant OP_SUCCINCT_FAULT_DISPUTE_GAME_TYPE = 42; - /// @notice The public values committed to for an OP Succinct aggregation program. struct AggregationOutputs { bytes32 l1Head; diff --git a/packages/contracts-bedrock/src/dispute/zk/AccessManager.sol b/packages/contracts-bedrock/src/dispute/zk/AccessManager.sol index fe2d5fdb0ad..b03b034a1a3 100644 --- a/packages/contracts-bedrock/src/dispute/zk/AccessManager.sol +++ b/packages/contracts-bedrock/src/dispute/zk/AccessManager.sol @@ -1,15 +1,14 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.15; +pragma solidity 0.8.15; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; -import { GameType } from "src/dispute/lib/Types.sol"; +import { GameType, GameTypes } from "src/dispute/lib/Types.sol"; import { Timestamp } from "src/dispute/lib/LibUDT.sol"; -import { OP_SUCCINCT_FAULT_DISPUTE_GAME_TYPE } from "src/dispute/lib/Types.sol"; /// @title AccessManager /// @notice Manages permissions for dispute game proposers and challengers. -abstract contract AccessManager is Ownable { +contract AccessManager is Ownable { //////////////////////////////////////////////////////////////// // Events // //////////////////////////////////////////////////////////////// @@ -79,7 +78,7 @@ abstract contract AccessManager is Ownable { /// @return The last proposal timestamp. function getLastProposalTimestamp() public view returns (uint256) { // Get the latest game to check its timestamp. - GameType gameType = GameType.wrap(OP_SUCCINCT_FAULT_DISPUTE_GAME_TYPE); + GameType gameType = GameTypes.OPTIMISTIC_ZK_GAME_TYPE; uint256 numGames = DISPUTE_GAME_FACTORY.gameCount(); // Early return if no games exist. diff --git a/packages/contracts-bedrock/src/dispute/zk/OPSuccinctFaultDisputeGame.sol b/packages/contracts-bedrock/src/dispute/zk/OptimisticZkGame.sol similarity index 98% rename from packages/contracts-bedrock/src/dispute/zk/OPSuccinctFaultDisputeGame.sol rename to packages/contracts-bedrock/src/dispute/zk/OptimisticZkGame.sol index f7c1e6e7639..ee9ab2589b1 100644 --- a/packages/contracts-bedrock/src/dispute/zk/OPSuccinctFaultDisputeGame.sol +++ b/packages/contracts-bedrock/src/dispute/zk/OptimisticZkGame.sol @@ -13,7 +13,7 @@ import { Timestamp, Proposal } from "src/dispute/lib/Types.sol"; -import { AggregationOutputs, OP_SUCCINCT_FAULT_DISPUTE_GAME_TYPE } from "src/dispute/lib/Types.sol"; +import { AggregationOutputs, GameTypes } from "src/dispute/lib/Types.sol"; import { AlreadyInitialized, BadAuth, @@ -43,10 +43,10 @@ import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.so // Contracts import { AccessManager } from "src/dispute/zk/AccessManager.sol"; -/// @title OPSuccinctFaultDisputeGame +/// @title OptimisticZkGame /// @notice An implementation of the `IFaultDisputeGame` interface. /// @dev Derived from https://github.com/succinctlabs/op-succinct (at commit c13844a9bbc330cca69eef2538d8f8ec123e1653) -contract OPSuccinctFaultDisputeGame is Clone, ISemver, IDisputeGame { +contract OptimisticZkGame is Clone, ISemver, IDisputeGame { //////////////////////////////////////////////////////////////// // Enums // //////////////////////////////////////////////////////////////// @@ -146,8 +146,8 @@ contract OPSuccinctFaultDisputeGame is Clone, ISemver, IDisputeGame { AccessManager internal immutable ACCESS_MANAGER; /// @notice Semantic version. - /// @custom:semver 0.0.0 - string public constant version = "0.0.0"; + /// @custom:semver 0.0.1 + string public constant version = "0.0.1"; /// @notice The starting timestamp of the game. Timestamp public createdAt; @@ -202,7 +202,7 @@ contract OPSuccinctFaultDisputeGame is Clone, ISemver, IDisputeGame { AccessManager _accessManager ) { // Set up initial game state. - GAME_TYPE = GameType.wrap(OP_SUCCINCT_FAULT_DISPUTE_GAME_TYPE); + GAME_TYPE = GameTypes.OPTIMISTIC_ZK_GAME_TYPE; MAX_CHALLENGE_DURATION = _maxChallengeDuration; MAX_PROVE_DURATION = _maxProveDuration; DISPUTE_GAME_FACTORY = _disputeGameFactory; @@ -279,8 +279,8 @@ contract OPSuccinctFaultDisputeGame is Clone, ISemver, IDisputeGame { } startingProposal = Proposal({ - l2SequenceNumber: OPSuccinctFaultDisputeGame(address(proxy)).l2SequenceNumber(), - root: Hash.wrap(OPSuccinctFaultDisputeGame(address(proxy)).rootClaim().raw()) + l2SequenceNumber: OptimisticZkGame(address(proxy)).l2SequenceNumber(), + root: Hash.wrap(OptimisticZkGame(address(proxy)).rootClaim().raw()) }); // INVARIANT: The parent game must be a valid game. diff --git a/packages/contracts-bedrock/src/libraries/Encoding.sol b/packages/contracts-bedrock/src/libraries/Encoding.sol index ce31d43c0b0..483efc5bdcf 100644 --- a/packages/contracts-bedrock/src/libraries/Encoding.sol +++ b/packages/contracts-bedrock/src/libraries/Encoding.sol @@ -15,6 +15,9 @@ library Encoding { /// @notice Thrown when a provided Super Root proof has no Output Roots. error Encoding_EmptySuperRoot(); + /// @notice Thrown when attempting to decode an invalid Super Root Proof encoding. + error Encoding_InvalidSuperRootEncoding(); + /// @notice RLP encodes the L2 transaction that would be generated when a given deposit is sent /// to the L2 system. Useful for searching for a deposit in the L2 system. The /// transaction is prefixed with 0x7e to identify its EIP-2718 type. @@ -298,4 +301,46 @@ library Encoding { return encoded; } + + /// @notice Decodes a super root proof from the preimage of a Super Root. + /// @param _super Encoded super root proof. + /// @return Decoded super root proof. + function decodeSuperRootProof(bytes memory _super) internal pure returns (Types.SuperRootProof memory) { + if (_super.length < 9) { + revert Encoding_InvalidSuperRootEncoding(); + } + uint8 version = uint8(_super[0]); + if (version != 0x01) { + revert Encoding_InvalidSuperRootVersion(); + } + + uint256 offset = 1; + uint64 superTimestamp; + assembly { + superTimestamp := shr(192, mload(add(_super, add(32, offset)))) + } + offset += 8; + + if (_super.length <= offset) { + revert Encoding_EmptySuperRoot(); + } + if ((_super.length - offset) % 64 != 0) { + revert Encoding_InvalidSuperRootEncoding(); + } + + Types.OutputRootWithChainId[] memory outputRoots = + new Types.OutputRootWithChainId[]((_super.length - offset) / 64); + for (uint256 i = 0; i < outputRoots.length; i++) { + uint256 chainId; + bytes32 root; + assembly { + chainId := mload(add(_super, add(32, offset))) + root := mload(add(_super, add(32, add(offset, 0x20)))) + } + offset += 64; + outputRoots[i] = Types.OutputRootWithChainId({ chainId: chainId, root: root }); + } + + return Types.SuperRootProof({ version: bytes1(version), timestamp: superTimestamp, outputRoots: outputRoots }); + } } diff --git a/packages/contracts-bedrock/test/L1/L1StandardBridge.t.sol b/packages/contracts-bedrock/test/L1/L1StandardBridge.t.sol index abec06bf78b..294545b1561 100644 --- a/packages/contracts-bedrock/test/L1/L1StandardBridge.t.sol +++ b/packages/contracts-bedrock/test/L1/L1StandardBridge.t.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.15; // Testing -import { stdStorage, StdStorage } from "forge-std/Test.sol"; +import { stdStorage, StdStorage } from "forge-std/StdStorage.sol"; import { CommonTest } from "test/setup/CommonTest.sol"; import { ForgeArtifacts, StorageSlot } from "scripts/libraries/ForgeArtifacts.sol"; diff --git a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol index ce5457ead99..7feffe72daa 100644 --- a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol +++ b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol @@ -2,7 +2,8 @@ pragma solidity 0.8.15; // Testing -import { Test, stdStorage, StdStorage } from "forge-std/Test.sol"; +import { Test } from "test/setup/Test.sol"; +import { stdStorage, StdStorage } from "forge-std/StdStorage.sol"; import { VmSafe } from "forge-std/Vm.sol"; import { CommonTest } from "test/setup/CommonTest.sol"; import { FeatureFlags } from "test/setup/FeatureFlags.sol"; @@ -22,6 +23,9 @@ import { GameType, Duration, Hash, Claim } from "src/dispute/lib/LibUDT.sol"; import { Proposal, GameTypes } from "src/dispute/lib/Types.sol"; import { LibGameArgs } from "src/dispute/lib/LibGameArgs.sol"; import { DevFeatures } from "src/libraries/DevFeatures.sol"; +import { Types as LibTypes } from "src/libraries/Types.sol"; +import { Encoding } from "src/libraries/Encoding.sol"; +import { Hashing } from "src/libraries/Hashing.sol"; // Interfaces import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; @@ -831,13 +835,24 @@ contract OPContractsManager_AddGameType_Test is OPContractsManager_TestInit { returns (IFaultDisputeGame) { // Create a game so we can assert on game args which aren't baked into the implementation contract - Claim claim = Claim.wrap(bytes32(uint256(9876))); - uint256 l2SequenceNumber = uint256(123); + Claim claim; + bytes memory extraData; + if (isSuperGame(agi.disputeGameType)) { + LibTypes.OutputRootWithChainId[] memory outputRoots = new LibTypes.OutputRootWithChainId[](1); + outputRoots[0] = LibTypes.OutputRootWithChainId({ chainId: 100, root: keccak256(abi.encode(gasleft())) }); + LibTypes.SuperRootProof memory superRootProof; + superRootProof.version = bytes1(uint8(1)); + superRootProof.timestamp = uint64(123); + superRootProof.outputRoots = outputRoots; + extraData = Encoding.encodeSuperRootProof(superRootProof); + claim = Claim.wrap(Hashing.hashSuperRootProof(superRootProof)); + } else { + claim = Claim.wrap(bytes32(uint256(9876))); + extraData = abi.encode(uint256(123)); // l2BlockNumber + } IFaultDisputeGame game = IFaultDisputeGame( payable( - createGame( - chainDeployOutput1.disputeGameFactoryProxy, agi.disputeGameType, proposer, claim, l2SequenceNumber - ) + createGame(chainDeployOutput1.disputeGameFactoryProxy, agi.disputeGameType, proposer, claim, extraData) ) ); @@ -850,7 +865,7 @@ contract OPContractsManager_AddGameType_Test is OPContractsManager_TestInit { assertEq(game.gameCreator(), proposer, "Game creator should match"); assertEq(game.rootClaim().raw(), claim.raw(), "Claim should match"); assertEq(game.l1Head().raw(), blockhash(block.number - 1), "L1 head should match"); - assertEq(game.l2SequenceNumber(), l2SequenceNumber, "L2 sequence number should match"); + assertEq(game.l2SequenceNumber(), 123, "L2 sequence number should match"); assertEq( game.absolutePrestate().raw(), agi.disputeAbsolutePrestate.raw(), "Absolute prestate should match input" ); @@ -1785,6 +1800,28 @@ contract OPContractsManager_Migrate_Test is OPContractsManager_TestInit { assertEq(_dgf.gameArgs(_gameType), hex"", string.concat("Game args should be empty: ", _label)); } + /// @notice Creates a dummy super root proof consisting of all chains being migrated + function _createSuperRootProof( + IOPContractsManagerInteropMigrator.MigrateInput memory _input, + uint64 _l2SequenceNumber + ) + internal + view + returns (LibTypes.SuperRootProof memory super_) + { + LibTypes.OutputRootWithChainId[] memory outputRoots = + new LibTypes.OutputRootWithChainId[](_input.opChainConfigs.length); + for (uint256 j; j < _input.opChainConfigs.length; j++) { + outputRoots[j] = LibTypes.OutputRootWithChainId({ + chainId: uint32(_input.opChainConfigs[j].systemConfigProxy.l2ChainId()), + root: keccak256(abi.encode(gasleft())) + }); + } + super_.version = bytes1(uint8(1)); + super_.timestamp = uint64(_l2SequenceNumber); + super_.outputRoots = outputRoots; + } + /// @notice Runs some tests after opcm.migrate function _runPostMigrateSmokeTests(IOPContractsManagerInteropMigrator.MigrateInput memory _input) internal { IDisputeGameFactory dgf = IDisputeGameFactory(chainDeployOutput1.systemConfigProxy.disputeGameFactory()); @@ -1817,12 +1854,19 @@ contract OPContractsManager_Migrate_Test is OPContractsManager_TestInit { assertEq(gameArgs.weth, permissionlessWeth, "gameArgs weth mismatch"); } - Claim rootClaim = Claim.wrap(bytes32(uint256(1))); + LibTypes.SuperRootProof memory superRootProof = _createSuperRootProof(_input, uint64(l2SequenceNumber)); uint256 bondAmount = dgf.initBonds(gameTypes[i]); vm.deal(address(proposer), bondAmount); vm.prank(proposer, proposer); + ISuperPermissionedDisputeGame game = ISuperPermissionedDisputeGame( - address(dgf.create{ value: bondAmount }(gameTypes[i], rootClaim, abi.encode(l2SequenceNumber))) + address( + dgf.create{ value: bondAmount }( + gameTypes[i], + Claim.wrap(Hashing.hashSuperRootProof(superRootProof)), + Encoding.encodeSuperRootProof(superRootProof) + ) + ) ); assertEq(game.gameType().raw(), gameTypes[i].raw(), "Super Cannon game type not set properly"); @@ -2198,6 +2242,11 @@ contract OPContractsManager_Migrate_Test is OPContractsManager_TestInit { contract OPContractsManager_Deploy_Test is DeployOPChain_TestBase, DisputeGames { using stdStorage for StdStorage; + function setUp() public override { + super.setUp(); + skipIfDevFeatureEnabled(DevFeatures.OPCM_V2); + } + // This helper function is used to convert the input struct type defined in DeployOPChain.s.sol // to the input struct type defined in OPContractsManager.sol. function toOPCMDeployInput(Types.DeployOPChainInput memory _doi) @@ -2235,7 +2284,7 @@ contract OPContractsManager_Deploy_Test is DeployOPChain_TestBase, DisputeGames input.l2ChainId = 0; vm.expectRevert(IOPContractsManager.InvalidChainId.selector); - opcm.deploy(input); + IOPContractsManager(opcmAddr).deploy(input); } function test_deploy_l2ChainIdEqualsCurrentChainId_reverts() public { @@ -2243,19 +2292,19 @@ contract OPContractsManager_Deploy_Test is DeployOPChain_TestBase, DisputeGames input.l2ChainId = block.chainid; vm.expectRevert(IOPContractsManager.InvalidChainId.selector); - opcm.deploy(input); + IOPContractsManager(opcmAddr).deploy(input); } function test_deploy_succeeds() public { vm.expectEmit(true, true, true, false); // TODO precompute the expected `deployOutput`. emit Deployed(deployOPChainInput.l2ChainId, address(this), bytes("")); - opcm.deploy(toOPCMDeployInput(deployOPChainInput)); + IOPContractsManager(opcmAddr).deploy(toOPCMDeployInput(deployOPChainInput)); } /// @notice Test that deploy sets the permissioned dispute game implementation function test_deployPermissioned_succeeds() public { // Sanity-check setup is consistent with devFeatures flag - IOPContractsManager.Implementations memory impls = opcm.implementations(); + IOPContractsManager.Implementations memory impls = IOPContractsManager(opcmAddr).implementations(); address pdgImpl = address(impls.permissionedDisputeGameV2Impl); address fdgImpl = address(impls.faultDisputeGameV2Impl); assertFalse(pdgImpl == address(0), "PDG implementation address should be non-zero"); @@ -2263,7 +2312,7 @@ contract OPContractsManager_Deploy_Test is DeployOPChain_TestBase, DisputeGames // Run OPCM.deploy IOPContractsManager.DeployInput memory opcmInput = toOPCMDeployInput(deployOPChainInput); - IOPContractsManager.DeployOutput memory opcmOutput = opcm.deploy(opcmInput); + IOPContractsManager.DeployOutput memory opcmOutput = IOPContractsManager(opcmAddr).deploy(opcmInput); // Verify that the DisputeGameFactory has registered an implementation for the PERMISSIONED_CANNON game type address actualPDGAddress = address(opcmOutput.disputeGameFactoryProxy.gameImpls(GameTypes.PERMISSIONED_CANNON)); diff --git a/packages/contracts-bedrock/test/L1/OPContractsManagerStandardValidator.t.sol b/packages/contracts-bedrock/test/L1/OPContractsManagerStandardValidator.t.sol index 9ca73cd450f..eceb10760a5 100644 --- a/packages/contracts-bedrock/test/L1/OPContractsManagerStandardValidator.t.sol +++ b/packages/contracts-bedrock/test/L1/OPContractsManagerStandardValidator.t.sol @@ -167,7 +167,7 @@ abstract contract OPContractsManagerStandardValidator_TestInit is CommonTest, Di preimageOracle = IPreimageOracle(artifacts.mustGetAddress("PreimageOracle")); if (isDevFeatureEnabled(DevFeatures.OPCM_V2)) { - standardValidator = opcmV2.standardValidator(); + standardValidator = opcmV2.opcmStandardValidator(); } else { standardValidator = opcm.opcmStandardValidator(); } diff --git a/packages/contracts-bedrock/test/L1/ResourceMetering.t.sol b/packages/contracts-bedrock/test/L1/ResourceMetering.t.sol index 8e64d609e9e..eca3d477053 100644 --- a/packages/contracts-bedrock/test/L1/ResourceMetering.t.sol +++ b/packages/contracts-bedrock/test/L1/ResourceMetering.t.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.15; // Testing -import { Test } from "forge-std/Test.sol"; +import { Test } from "test/setup/Test.sol"; // Contracts import { ResourceMetering } from "src/L1/ResourceMetering.sol"; diff --git a/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerContainer.t.sol b/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerContainer.t.sol index 78eafbdf763..fbb579449e9 100644 --- a/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerContainer.t.sol +++ b/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerContainer.t.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.15; // Testing -import { Test } from "forge-std/Test.sol"; +import { Test } from "test/setup/Test.sol"; // Contracts import { OPContractsManagerContainer } from "src/L1/opcm/OPContractsManagerContainer.sol"; diff --git a/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerUtils.t.sol b/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerUtils.t.sol index 5ca424905f4..ce79986542f 100644 --- a/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerUtils.t.sol +++ b/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerUtils.t.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.15; // Testing -import { Test } from "forge-std/Test.sol"; +import { Test } from "test/setup/Test.sol"; // Contracts import { OPContractsManagerUtils } from "src/L1/opcm/OPContractsManagerUtils.sol"; diff --git a/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol b/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol index bf994e4ed25..917e82b3921 100644 --- a/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol +++ b/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol @@ -118,7 +118,7 @@ contract OPContractsManagerV2_TestInit is CommonTest, DisputeGames { }); // Grab the validator before we do the error assertion. - IOPContractsManagerStandardValidator validator = _opcm.standardValidator(); + IOPContractsManagerStandardValidator validator = _opcm.opcmStandardValidator(); // Expect validator errors if the user provides them. if (bytes(_expectedValidatorErrors).length > 0) { @@ -373,7 +373,7 @@ contract OPContractsManagerV2_Upgrade_TestInit is OPContractsManagerV2_TestInit // Grab the validator before we do the error assertion because otherwise the assertion will // try to apply to this function call instead. - IOPContractsManagerStandardValidator validator = _opcm.standardValidator(); + IOPContractsManagerStandardValidator validator = _opcm.opcmStandardValidator(); // Expect validator errors if the user provides them. We always expect the L1PAOMultisig // and Challenger overrides so we don't need to repeat them here. @@ -1128,3 +1128,16 @@ contract OPContractsManagerV2_Deploy_Test is OPContractsManagerV2_TestInit { ); } } + +/// @title OPContractsManagerV2_DevFeatureBitmap_Test +/// @notice Tests OPContractsManagerV2.devFeatureBitmap +contract OPContractsManagerV2_DevFeatureBitmap_Test is OPContractsManagerV2_TestInit { + /// @notice Tests that the devFeatureBitmap returned by opcmV2 matches the contractsContainer address's own. + function test_devFeatureBitmap_succeeds() public view { + assertEq( + opcmV2.devFeatureBitmap(), + opcmV2.contractsContainer().devFeatureBitmap(), + "devFeatureBitmap on opcmV2 does not match contractsContainer bitmap" + ); + } +} diff --git a/packages/contracts-bedrock/test/L2/CrossDomainOwnable.t.sol b/packages/contracts-bedrock/test/L2/CrossDomainOwnable.t.sol index 56c3f74dda1..99898df6f4d 100644 --- a/packages/contracts-bedrock/test/L2/CrossDomainOwnable.t.sol +++ b/packages/contracts-bedrock/test/L2/CrossDomainOwnable.t.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Testing utilities +// Testing +import { Test } from "test/setup/Test.sol"; import { VmSafe } from "forge-std/Vm.sol"; -import { Test } from "forge-std/Test.sol"; import { CommonTest } from "test/setup/CommonTest.sol"; // Libraries diff --git a/packages/contracts-bedrock/test/L2/CrossL2Inbox.t.sol b/packages/contracts-bedrock/test/L2/CrossL2Inbox.t.sol index 323ac2c5b06..7d1b2460099 100644 --- a/packages/contracts-bedrock/test/L2/CrossL2Inbox.t.sol +++ b/packages/contracts-bedrock/test/L2/CrossL2Inbox.t.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Testing utilities -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; import { CommonTest } from "test/setup/CommonTest.sol"; import { VmSafe } from "forge-std/Vm.sol"; diff --git a/packages/contracts-bedrock/test/L2/FeeSplitterVaults.t.sol b/packages/contracts-bedrock/test/L2/FeeSplitterVaults.t.sol index fbb475373cd..c316e23af8a 100644 --- a/packages/contracts-bedrock/test/L2/FeeSplitterVaults.t.sol +++ b/packages/contracts-bedrock/test/L2/FeeSplitterVaults.t.sol @@ -1,13 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -// Libraries -import { Predeploys } from "src/libraries/Predeploys.sol"; - // Testing -import { Test } from "forge-std/Test.sol"; +import { Test } from "test/setup/Test.sol"; import { FeeSplitterForTest } from "test/mocks/FeeSplitterForTest.sol"; +// Libraries +import { Predeploys } from "src/libraries/Predeploys.sol"; + // Interfaces import { IFeeSplitter } from "interfaces/L2/IFeeSplitter.sol"; diff --git a/packages/contracts-bedrock/test/L2/GasPriceOracle.t.sol b/packages/contracts-bedrock/test/L2/GasPriceOracle.t.sol index e0f959333c4..72b74f0c36b 100644 --- a/packages/contracts-bedrock/test/L2/GasPriceOracle.t.sol +++ b/packages/contracts-bedrock/test/L2/GasPriceOracle.t.sol @@ -1,13 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Testing utilities +// Testing import { CommonTest } from "test/setup/CommonTest.sol"; import { Fork } from "scripts/libraries/Config.sol"; +import { stdError } from "forge-std/StdError.sol"; // Libraries import { Encoding } from "src/libraries/Encoding.sol"; -import { stdError } from "forge-std/Test.sol"; contract GasPriceOracle_Test is CommonTest { address depositor; diff --git a/packages/contracts-bedrock/test/L2/L1Block.t.sol b/packages/contracts-bedrock/test/L2/L1Block.t.sol index 5d36bb88631..8b31d1e2ac3 100644 --- a/packages/contracts-bedrock/test/L2/L1Block.t.sol +++ b/packages/contracts-bedrock/test/L2/L1Block.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.15; // Testing import { CommonTest } from "test/setup/CommonTest.sol"; -import { stdStorage, StdStorage } from "forge-std/Test.sol"; +import { stdStorage, StdStorage } from "forge-std/StdStorage.sol"; // Libraries import { Encoding } from "src/libraries/Encoding.sol"; diff --git a/packages/contracts-bedrock/test/L2/L2StandardBridge.t.sol b/packages/contracts-bedrock/test/L2/L2StandardBridge.t.sol index a0f1c0aec8c..8ea1e57ad20 100644 --- a/packages/contracts-bedrock/test/L2/L2StandardBridge.t.sol +++ b/packages/contracts-bedrock/test/L2/L2StandardBridge.t.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.15; // Testing -import { stdStorage, StdStorage } from "forge-std/Test.sol"; +import { stdStorage, StdStorage } from "forge-std/StdStorage.sol"; import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; import { CommonTest } from "test/setup/CommonTest.sol"; diff --git a/packages/contracts-bedrock/test/L2/L2ToL2CrossDomainMessenger.t.sol b/packages/contracts-bedrock/test/L2/L2ToL2CrossDomainMessenger.t.sol index 9b2d8642360..508aac46590 100644 --- a/packages/contracts-bedrock/test/L2/L2ToL2CrossDomainMessenger.t.sol +++ b/packages/contracts-bedrock/test/L2/L2ToL2CrossDomainMessenger.t.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -// Testing utilities -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; import { Vm } from "forge-std/Vm.sol"; // Libraries diff --git a/packages/contracts-bedrock/test/L2/LiquidityController.t.sol b/packages/contracts-bedrock/test/L2/LiquidityController.t.sol index 82208200fcc..9b77c45ea75 100644 --- a/packages/contracts-bedrock/test/L2/LiquidityController.t.sol +++ b/packages/contracts-bedrock/test/L2/LiquidityController.t.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Testing utilities +// Testing import { CommonTest } from "test/setup/CommonTest.sol"; -import { stdStorage, StdStorage } from "forge-std/Test.sol"; +import { stdStorage, StdStorage } from "forge-std/StdStorage.sol"; // Libraries import { Features } from "src/libraries/Features.sol"; diff --git a/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol index 3479ec0330d..3950de5901a 100644 --- a/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol +++ b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -// Testing utilities -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; // Libraries diff --git a/packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol b/packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol index b409c2c42bd..7835c11542b 100644 --- a/packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol +++ b/packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -// Testing utilities -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; // Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; diff --git a/packages/contracts-bedrock/test/L2/SuperchainTokenBridge.t.sol b/packages/contracts-bedrock/test/L2/SuperchainTokenBridge.t.sol index 059d66cc9c1..daf0f1546ee 100644 --- a/packages/contracts-bedrock/test/L2/SuperchainTokenBridge.t.sol +++ b/packages/contracts-bedrock/test/L2/SuperchainTokenBridge.t.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -// Testing utilities -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; // Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; diff --git a/packages/contracts-bedrock/test/cannon/MIPS64.t.sol b/packages/contracts-bedrock/test/cannon/MIPS64.t.sol index 6e3caaa1695..774324a22d4 100644 --- a/packages/contracts-bedrock/test/cannon/MIPS64.t.sol +++ b/packages/contracts-bedrock/test/cannon/MIPS64.t.sol @@ -1,10 +1,16 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; +// Scripts import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; + +// Libraries import { UnsupportedStateVersion } from "src/cannon/libraries/CannonErrors.sol"; + +// Interfaces import { IPreimageOracle } from "interfaces/cannon/IPreimageOracle.sol"; import { IMIPS64 } from "interfaces/cannon/IMIPS64.sol"; diff --git a/packages/contracts-bedrock/test/cannon/PreimageOracle.t.sol b/packages/contracts-bedrock/test/cannon/PreimageOracle.t.sol index 59cef2f3653..ce423dae10d 100644 --- a/packages/contracts-bedrock/test/cannon/PreimageOracle.t.sol +++ b/packages/contracts-bedrock/test/cannon/PreimageOracle.t.sol @@ -2,7 +2,9 @@ pragma solidity 0.8.15; // Testing -import { Test, Vm, console2 as console } from "forge-std/Test.sol"; +import { Test } from "test/setup/Test.sol"; +import { Vm } from "forge-std/Vm.sol"; +import { console2 as console } from "forge-std/console2.sol"; // Scripts import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; diff --git a/packages/contracts-bedrock/test/dispute/DisputeGameFactory.t.sol b/packages/contracts-bedrock/test/dispute/DisputeGameFactory.t.sol index e17fde3c0a6..f628b17cf7b 100644 --- a/packages/contracts-bedrock/test/dispute/DisputeGameFactory.t.sol +++ b/packages/contracts-bedrock/test/dispute/DisputeGameFactory.t.sol @@ -25,6 +25,12 @@ import { IPermissionedDisputeGameV2 } from "interfaces/dispute/v2/IPermissionedD import { ISuperPermissionedDisputeGame } from "interfaces/dispute/ISuperPermissionedDisputeGame.sol"; // Mocks import { AlphabetVM } from "test/mocks/AlphabetVM.sol"; +import { SP1MockVerifier } from "test/dispute/zk/mocks/SP1MockVerifier.sol"; + +// OptimisticZk +import { OptimisticZkGame } from "src/dispute/zk/OptimisticZkGame.sol"; +import { AccessManager } from "src/dispute/zk/AccessManager.sol"; +import { ISP1Verifier } from "src/dispute/zk/ISP1Verifier.sol"; /// @notice A fake clone used for testing the `DisputeGameFactory` contract's `create` function. contract DisputeGameFactory_FakeClone_Harness { @@ -388,6 +394,59 @@ abstract contract DisputeGameFactory_TestInit is CommonTest { }); _setGame(gameImpl_, GameTypes.SUPER_PERMISSIONED_CANNON, _implArgs); } + + /// @notice Parameters for OptimisticZk game setup + struct OptimisticZkGameParams { + Duration maxChallengeDuration; + Duration maxProveDuration; + address proposer; + address challenger; + bytes32 rollupConfigHash; + bytes32 aggregationVkey; + bytes32 rangeVkeyCommitment; + uint256 challengerBond; + } + + /// @notice Sets up an OptimisticZk game implementation + function setupOptimisticZkGame(OptimisticZkGameParams memory _params) + internal + returns (address gameImpl_, AccessManager accessManager_, ISP1Verifier sp1Verifier_) + { + // Deploy mock verifier + sp1Verifier_ = ISP1Verifier(address(new SP1MockVerifier())); + + // Deploy access manager + accessManager_ = new AccessManager(2 weeks, disputeGameFactory); + accessManager_.setProposer(_params.proposer, true); + accessManager_.setChallenger(_params.challenger, true); + + // Deploy game implementation + gameImpl_ = address( + new OptimisticZkGame( + _params.maxChallengeDuration, + _params.maxProveDuration, + disputeGameFactory, + sp1Verifier_, + _params.rollupConfigHash, + _params.aggregationVkey, + _params.rangeVkeyCommitment, + _params.challengerBond, + anchorStateRegistry, + accessManager_ + ) + ); + + // Set respected game type for OptimisticZk + GameType gameType = GameTypes.OPTIMISTIC_ZK_GAME_TYPE; + vm.prank(superchainConfig.guardian()); + anchorStateRegistry.setRespectedGameType(gameType); + + // Register with factory + vm.startPrank(disputeGameFactory.owner()); + disputeGameFactory.setImplementation(gameType, IDisputeGame(gameImpl_)); + disputeGameFactory.setInitBond(gameType, _params.challengerBond); + vm.stopPrank(); + } } /// @title DisputeGameFactory_Initialize_Test diff --git a/packages/contracts-bedrock/test/dispute/SuperFaultDisputeGame.t.sol b/packages/contracts-bedrock/test/dispute/SuperFaultDisputeGame.t.sol index 9138d9d47bc..c3a78ce42f8 100644 --- a/packages/contracts-bedrock/test/dispute/SuperFaultDisputeGame.t.sol +++ b/packages/contracts-bedrock/test/dispute/SuperFaultDisputeGame.t.sol @@ -17,6 +17,7 @@ import { DisputeActor, HonestDisputeActor } from "test/actors/FaultDisputeActors // Libraries import { Types } from "src/libraries/Types.sol"; import { Hashing } from "src/libraries/Hashing.sol"; +import { Encoding } from "src/libraries/Encoding.sol"; import { RLPWriter } from "src/libraries/rlp/RLPWriter.sol"; import { LibClock } from "src/dispute/lib/LibUDT.sol"; import { LibPosition } from "src/dispute/lib/LibPosition.sol"; @@ -78,14 +79,14 @@ abstract contract BaseSuperFaultDisputeGame_TestInit is DisputeGameFactory_TestI event GameClosed(BondDistributionMode bondDistributionMode); event ReceiveETH(uint256 amount); - function init(Claim _rootClaim, Claim _absolutePrestate, uint256 _l2SequenceNumber) public { + function init(Claim _rootClaim, Claim _absolutePrestate, Types.SuperRootProof memory _super) public { // Set the time to a realistic date. if (!isForkTest()) { vm.warp(1690906994); } // Set the extra data for the game creation - extraData = abi.encode(_l2SequenceNumber); + extraData = Encoding.encodeSuperRootProof(_super); (address _impl, AlphabetVM _vm,) = setupSuperFaultDisputeGame(_absolutePrestate); gameImpl = ISuperFaultDisputeGame(_impl); @@ -115,6 +116,11 @@ abstract contract BaseSuperFaultDisputeGame_TestInit is DisputeGameFactory_TestI assertEq(address(gameProxy.weth()), address(delayedWeth)); assertEq(address(gameProxy.anchorStateRegistry()), address(anchorStateRegistry)); assertEq(address(gameProxy.vm()), address(_vm)); + // Check extra data + assertEq(gameProxy.l2SequenceNumber(), _super.timestamp); + for (uint256 i; i < _super.outputRoots.length; i++) { + assertEq(gameProxy.rootClaimByChainId(_super.outputRoots[i].chainId).raw(), _super.outputRoots[i].root); + } // Label the proxy vm.label(address(gameProxy), "SuperFaultDisputeGame_Clone"); @@ -131,8 +137,8 @@ abstract contract SuperFaultDisputeGame_TestInit is BaseSuperFaultDisputeGame_Te /// @dev The root claim of the game. Claim internal ROOT_CLAIM; - /// @dev An arbitrary root claim for testing. - Claim internal arbitaryRootClaim = Claim.wrap(bytes32(uint256(123))); + /// @dev The super root preimage of the game + Types.SuperRootProof SUPER_ROOT_PROOF; /// @dev The preimage of the absolute prestate claim bytes internal absolutePrestateData; @@ -150,8 +156,8 @@ abstract contract SuperFaultDisputeGame_TestInit is BaseSuperFaultDisputeGame_Te super.setUp(); // Get the actual anchor roots - (Hash root, uint256 l2Bn) = anchorStateRegistry.getAnchorRoot(); - validl2SequenceNumber = l2Bn + 1; + (, uint256 l2Seqno) = anchorStateRegistry.getAnchorRoot(); + validl2SequenceNumber = l2Seqno + 1; if (isForkTest()) { // Set the init bond of anchor game type 4 to be 0. @@ -160,8 +166,13 @@ abstract contract SuperFaultDisputeGame_TestInit is BaseSuperFaultDisputeGame_Te ); } - ROOT_CLAIM = Claim.wrap(Hash.unwrap(root)); - init({ _rootClaim: ROOT_CLAIM, _absolutePrestate: absolutePrestate, _l2SequenceNumber: validl2SequenceNumber }); + SUPER_ROOT_PROOF.version = bytes1(uint8(1)); + SUPER_ROOT_PROOF.timestamp = uint64(validl2SequenceNumber); + SUPER_ROOT_PROOF.outputRoots.push(Types.OutputRootWithChainId({ chainId: 5, root: keccak256(abi.encode(5)) })); + SUPER_ROOT_PROOF.outputRoots.push(Types.OutputRootWithChainId({ chainId: 6, root: keccak256(abi.encode(6)) })); + ROOT_CLAIM = Claim.wrap(Hashing.hashSuperRootProof(SUPER_ROOT_PROOF)); + + init({ _rootClaim: ROOT_CLAIM, _absolutePrestate: absolutePrestate, _super: SUPER_ROOT_PROOF }); } /// @notice Helper to generate a mock RLP encoded header (with only a real block number) & an @@ -205,6 +216,45 @@ abstract contract SuperFaultDisputeGame_TestInit is BaseSuperFaultDisputeGame_Te bond_ = gameProxy.getRequiredBond(pos); } + /// @notice Helper to return a pseudo-random super root proof + function _dummySuper() internal view returns (Types.SuperRootProof memory superRootProof_) { + return _dummySuper(uint64(validl2SequenceNumber)); + } + + /// @notice Helper to return a pseudo-random super root proof with the specified l2SequenceNumber + function _dummySuper(uint64 _l2SequenceNumber) + internal + view + returns (Types.SuperRootProof memory superRootProof_) + { + Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](1); + outputRoots[0] = Types.OutputRootWithChainId({ chainId: 5, root: keccak256(abi.encode(gasleft())) }); + superRootProof_.version = bytes1(uint8(1)); + superRootProof_.timestamp = uint64(_l2SequenceNumber); + superRootProof_.outputRoots = outputRoots; + } + + /// @notice Helper to return a pseudo-random super root with the specified l2 sequence number + function _dummySuperRoot(uint64 _l2SequenceNumber) internal view returns (bytes32 superRoot_________) { + return Hashing.hashSuperRootProof(_dummySuper(_l2SequenceNumber)); + } + + /// @notice Helper to return a pseudo-random root claim with the specified l2 sequence number + function _dummyRootClaim(uint64 _l2SequenceNumber) + internal + view + returns (Claim rootClaim_, bytes memory extraData_) + { + Types.SuperRootProof memory superRootProof = _dummySuper(_l2SequenceNumber); + rootClaim_ = Claim.wrap(Hashing.hashSuperRootProof(superRootProof)); + extraData_ = Encoding.encodeSuperRootProof(superRootProof); + } + + /// @notice Helper to return a pseudo-random root claim + function _dummyRootClaim() internal view returns (Claim rootClaim_, bytes memory extraData_) { + return _dummyRootClaim(uint64(validl2SequenceNumber)); + } + /// @notice Helper to return a pseudo-random claim function _dummyClaim() internal view returns (Claim) { return Claim.wrap(keccak256(abi.encode(gasleft()))); @@ -345,14 +395,17 @@ contract SuperFaultDisputeGame_Constructor_Test is SuperFaultDisputeGame_TestIni contract SuperFaultDisputeGame_Initialize_Test is SuperFaultDisputeGame_TestInit { /// @notice Tests that the game cannot be initialized with an output root that commits to /// <= the configured starting block number - function testFuzz_initialize_cannotProposeGenesis_reverts(uint256 _blockNumber) public { + function testFuzz_initialize_cannotProposeGenesis_reverts(uint64 _blockNumber) public { (, uint256 startingL2Block) = gameProxy.startingProposal(); - _blockNumber = bound(_blockNumber, 0, startingL2Block); + _blockNumber = uint64(bound(_blockNumber, 0, uint64(startingL2Block))); + + Types.SuperRootProof memory superRootProof = _dummySuper(_blockNumber); + Claim claim = Claim.wrap(Hashing.hashSuperRootProof(superRootProof)); + bytes memory extraData = Encoding.encodeSuperRootProof(superRootProof); - Claim claim = _dummyClaim(); vm.expectRevert(abi.encodeWithSelector(UnexpectedRootClaim.selector, claim)); gameProxy = ISuperFaultDisputeGame( - payable(address(disputeGameFactory.create{ value: initBond }(GAME_TYPE, claim, abi.encode(_blockNumber)))) + payable(address(disputeGameFactory.create{ value: initBond }(GAME_TYPE, claim, extraData))) ); } @@ -361,15 +414,10 @@ contract SuperFaultDisputeGame_Initialize_Test is SuperFaultDisputeGame_TestInit uint256 _value = disputeGameFactory.initBonds(GAME_TYPE); vm.deal(address(this), _value); + (Claim rootClaim, bytes memory extraData) = _dummyRootClaim(); assertEq(address(gameProxy).balance, 0); gameProxy = ISuperFaultDisputeGame( - payable( - address( - disputeGameFactory.create{ value: _value }( - GAME_TYPE, arbitaryRootClaim, abi.encode(validl2SequenceNumber) - ) - ) - ) + payable(address(disputeGameFactory.create{ value: _value }(GAME_TYPE, rootClaim, extraData))) ); assertEq(address(gameProxy).balance, 0); assertEq(delayedWeth.balanceOf(address(gameProxy)), _value); @@ -395,7 +443,8 @@ contract SuperFaultDisputeGame_Initialize_Test is SuperFaultDisputeGame_TestInit // bytecode. // [0 bytes, 31 bytes] u [33 bytes, 23.5 KB] _extraDataLen = bound(_extraDataLen, 0, 23_500); - if (_extraDataLen == 32) { + + if (_extraDataLen > 9 && (_extraDataLen - 9) % 64 == 0) { _extraDataLen++; } bytes memory _extraData = new bytes(_extraDataLen); @@ -451,11 +500,10 @@ contract SuperFaultDisputeGame_Initialize_Test is SuperFaultDisputeGame_TestInit ); // Creation should fail. + (Claim rootClaim, bytes memory extraData) = _dummyRootClaim(); vm.expectRevert(AnchorRootNotFound.selector); gameProxy = ISuperFaultDisputeGame( - payable( - address(disputeGameFactory.create{ value: initBond }(GAME_TYPE, _dummyClaim(), new bytes(uint256(32)))) - ) + payable(address(disputeGameFactory.create{ value: initBond }(GAME_TYPE, rootClaim, extraData))) ); } @@ -478,17 +526,11 @@ contract SuperFaultDisputeGame_Initialize_Test is SuperFaultDisputeGame_TestInit address(vm_.oracle()), abi.encodeCall(IPreimageOracle.challengePeriod, ()), abi.encode(_challengePeriod) ); - // Create game via factory - initialize() is called automatically and should revert - (, uint256 anchorSeqNo) = anchorStateRegistry.getAnchorRoot(); - // Expect the initialize call to revert with InvalidChallengePeriod + (Claim rootClaim, bytes memory extraData) = _dummyRootClaim(); vm.expectRevert(InvalidChallengePeriod.selector); gameProxy = ISuperFaultDisputeGame( - payable( - address( - disputeGameFactory.create{ value: initBond }(GAME_TYPE, _dummyClaim(), abi.encode(anchorSeqNo + 1)) - ) - ) + payable(address(disputeGameFactory.create{ value: initBond }(GAME_TYPE, rootClaim, extraData))) ); } @@ -500,11 +542,10 @@ contract SuperFaultDisputeGame_Initialize_Test is SuperFaultDisputeGame_TestInit ByteUtils.overwriteAtOffset(gameArgs, l2ChainIdOffset, abi.encodePacked(uint256(1))); disputeGameFactory.setImplementation(GAME_TYPE, impl, gameArgs); + (Claim claim, bytes memory extraData) = _dummyRootClaim(); vm.expectRevert(ISuperFaultDisputeGame.NoChainIdNeeded.selector); gameProxy = ISuperFaultDisputeGame( - payable( - address(disputeGameFactory.create{ value: initBond }(GAME_TYPE, _dummyClaim(), new bytes(uint256(32)))) - ) + payable(address(disputeGameFactory.create{ value: initBond }(GAME_TYPE, claim, extraData))) ); } } @@ -1200,14 +1241,12 @@ contract SuperFaultDisputeGame_AddLocalData_Test is SuperFaultDisputeGame_TestIn /// @notice Tests that the L2 block number claim is favored over the bisected-to block when /// adding data. function test_addLocalData_l2SequenceNumberExtension_succeeds() public { + (Claim rootClaim, bytes memory extraData) = _dummyRootClaim(uint64(validl2SequenceNumber)); + // Deploy a new dispute game with a L2 block number claim of 8. This is directly in the // middle of the leaves in our output bisection test tree, at SPLIT_DEPTH = 2 ** 2 ISuperFaultDisputeGame game = ISuperFaultDisputeGame( - address( - disputeGameFactory.create{ value: initBond }( - GAME_TYPE, Claim.wrap(bytes32(uint256(0xFF))), abi.encode(uint256(validl2SequenceNumber)) - ) - ) + address(disputeGameFactory.create{ value: initBond }(GAME_TYPE, rootClaim, extraData)) ); // Get a claim below the split depth so that we can add local data for an execution trace @@ -1239,7 +1278,7 @@ contract SuperFaultDisputeGame_AddLocalData_Test is SuperFaultDisputeGame_TestIn // Expected start/disputed claims bytes32 startingClaim = bytes32(uint256(3)); Position startingPos = LibPosition.wrap(4, 14); - bytes32 disputedClaim = bytes32(uint256(0xFF)); + bytes32 disputedClaim = Claim.unwrap(rootClaim); Position disputedPos = LibPosition.wrap(0, 0); // Expected local data. This should be `l2SequenceNumber`, and not the actual bisected-to @@ -1862,6 +1901,21 @@ contract SuperFaultDisputeGame_RootClaim_Test is SuperFaultDisputeGame_TestInit function test_rootClaim_succeeds() public view { assertEq(gameProxy.rootClaim().raw(), ROOT_CLAIM.raw()); } + + /// @notice Tests that the game's root claim for each output root is set correctly. + function test_rootClaimForOutputRoot_succeeds() public view { + for (uint256 i = 0; i < SUPER_ROOT_PROOF.outputRoots.length; i++) { + uint256 chainId = SUPER_ROOT_PROOF.outputRoots[i].chainId; + assertEq(gameProxy.rootClaimByChainId(chainId).raw(), SUPER_ROOT_PROOF.outputRoots[i].root); + } + } + + /// @notice Tests that requesting the root claim for an unknown chain ID reverts. + function test_rootClaimForOutputRoot_unknownChainId_reverts() public { + uint256 invalidChainId = 9999; + vm.expectRevert(UnknownChainId.selector); + gameProxy.rootClaimByChainId(invalidChainId); + } } /// @title SuperFaultDisputeGame_ExtraData_Test @@ -2379,6 +2433,35 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_TestInit { super.setUp(); } + /// @notice Helper to create a dummy super root proof with two output roots. + function createSuper(uint256 _timestamp) internal pure returns (Types.SuperRootProof memory) { + Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](2); + outputRoots[0] = Types.OutputRootWithChainId({ chainId: 5, root: keccak256(abi.encode(5)) }); + outputRoots[1] = Types.OutputRootWithChainId({ chainId: 6, root: keccak256(abi.encode(6)) }); + Types.SuperRootProof memory superRootProof; + superRootProof.version = bytes1(uint8(1)); + superRootProof.timestamp = uint64(_timestamp); + superRootProof.outputRoots = outputRoots; + return superRootProof; + } + + /// @notice Helper to create a dummy super root proof with arbitrary number of output roots. + function createSuper( + uint256 _timestamp, + uint256[] memory _l2Outputs + ) + internal + pure + returns (Types.SuperRootProof memory) + { + Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](_l2Outputs.length); + for (uint256 i = 0; i < _l2Outputs.length; i++) { + outputRoots[i] = Types.OutputRootWithChainId({ chainId: i + 110101, root: bytes32(_l2Outputs[i]) }); + } + return + Types.SuperRootProof({ version: bytes1(uint8(1)), timestamp: uint64(_timestamp), outputRoots: outputRoots }); + } + /// @notice Static unit test for a 1v1 output bisection dispute. function test_static_1v1dishonestRootGenesisAbsolutePrestate_succeeds() public { // The honest l2 outputs are from [1, 16] in this game. @@ -2407,7 +2490,7 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_TestInit { // Run the actor test _actorTest({ - _rootClaim: 17, + _super: createSuper(17), _absolutePrestateData: 0, _honestTrace: honestTrace, _honestL2Outputs: honestL2Outputs, @@ -2419,11 +2502,15 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_TestInit { /// @notice Static unit test for a 1v1 output bisection dispute. function test_static_1v1honestRoot_succeeds() public { + Types.SuperRootProof memory honestSuper = _dummySuper(uint64(validl2SequenceNumber)); // The honest l2 outputs are from [1, 16] in this game. uint256[] memory honestL2Outputs = new uint256[](16); - for (uint256 i; i < honestL2Outputs.length; i++) { + for (uint256 i; i < honestL2Outputs.length - 1; i++) { honestL2Outputs[i] = i + 1; } + // to ensure that the trace ancestor at split depth is the root claim + honestL2Outputs[15] = uint256(Hashing.hashSuperRootProof(honestSuper)); + // The honest trace covers all block -> block + 1 transitions, and is 256 bytes long, // consisting of bytes [0, 255]. bytes memory honestTrace = new bytes(256); @@ -2442,7 +2529,7 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_TestInit { // Run the actor test _actorTest({ - _rootClaim: 16, + _super: honestSuper, _absolutePrestateData: 0, _honestTrace: honestTrace, _honestL2Outputs: honestL2Outputs, @@ -2477,7 +2564,7 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_TestInit { // Run the actor test _actorTest({ - _rootClaim: 17, + _super: createSuper(17), _absolutePrestateData: 0, _honestTrace: honestTrace, _honestL2Outputs: honestL2Outputs, @@ -2489,11 +2576,15 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_TestInit { /// @notice Static unit test for a 1v1 output bisection dispute. function test_static_1v1correctRootHalfWay_succeeds() public { + Types.SuperRootProof memory honestSuper = _dummySuper(uint64(validl2SequenceNumber)); // The honest l2 outputs are from [1, 16] in this game. uint256[] memory honestL2Outputs = new uint256[](16); - for (uint256 i; i < honestL2Outputs.length; i++) { + for (uint256 i; i < honestL2Outputs.length - 1; i++) { honestL2Outputs[i] = i + 1; } + // to ensure that the trace ancestor at split depth is the root claim + honestL2Outputs[15] = uint256(Hashing.hashSuperRootProof(honestSuper)); + // The honest trace covers all block -> block + 1 transitions, and is 256 bytes long, // consisting of bytes [0, 255]. bytes memory honestTrace = new bytes(256); @@ -2504,7 +2595,7 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_TestInit { // The dishonest l2 outputs are half correct, half incorrect. uint256[] memory dishonestL2Outputs = new uint256[](16); for (uint256 i; i < dishonestL2Outputs.length; i++) { - dishonestL2Outputs[i] = i > 7 ? 0xFF : i + 1; + dishonestL2Outputs[i] = i > 7 ? 0xFF : honestL2Outputs[i]; } // The dishonest trace is half correct, half incorrect. bytes memory dishonestTrace = new bytes(256); @@ -2514,7 +2605,7 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_TestInit { // Run the actor test _actorTest({ - _rootClaim: 16, + _super: honestSuper, _absolutePrestateData: 0, _honestTrace: honestTrace, _honestL2Outputs: honestL2Outputs, @@ -2538,11 +2629,16 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_TestInit { honestTrace[i] = bytes1(uint8(i)); } + Types.SuperRootProof memory dishonestSuper = createSuper(0xFF); + // The dishonest l2 outputs are half correct, half incorrect. uint256[] memory dishonestL2Outputs = new uint256[](16); - for (uint256 i; i < dishonestL2Outputs.length; i++) { - dishonestL2Outputs[i] = i > 7 ? 0xFF : i + 1; + for (uint256 i; i < dishonestL2Outputs.length - 1; i++) { + dishonestL2Outputs[i] = i > 7 ? 0xFF : honestL2Outputs[i]; } + // to ensure that the trace ancestor at split depth is the root claim + dishonestL2Outputs[15] = uint256(Hashing.hashSuperRootProof(dishonestSuper)); + // The dishonest trace is half correct, half incorrect. bytes memory dishonestTrace = new bytes(256); for (uint256 i; i < dishonestTrace.length; i++) { @@ -2551,7 +2647,7 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_TestInit { // Run the actor test _actorTest({ - _rootClaim: 0xFF, + _super: dishonestSuper, _absolutePrestateData: 0, _honestTrace: honestTrace, _honestL2Outputs: honestL2Outputs, @@ -2563,11 +2659,17 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_TestInit { /// @notice Static unit test for a 1v1 output bisection dispute. function test_static_1v1dishonestAbsolutePrestate_succeeds() public { + Types.SuperRootProof memory honestSuper = createSuper(validl2SequenceNumber, new uint256[](16)); + bytes32 honestRootClaimHash = Hashing.hashSuperRootProof(honestSuper); + // The honest l2 outputs are from [1, 16] in this game. uint256[] memory honestL2Outputs = new uint256[](16); - for (uint256 i; i < honestL2Outputs.length; i++) { + for (uint256 i; i < honestL2Outputs.length - 1; i++) { honestL2Outputs[i] = i + 1; } + // to ensure that the trace ancestor at split depth is the root claim + honestL2Outputs[15] = uint256(honestRootClaimHash); + // The honest trace covers all block -> block + 1 transitions, and is 256 bytes long, // consisting of bytes [0, 255]. bytes memory honestTrace = new bytes(256); @@ -2578,7 +2680,7 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_TestInit { // The dishonest l2 outputs are half correct, half incorrect. uint256[] memory dishonestL2Outputs = new uint256[](16); for (uint256 i; i < dishonestL2Outputs.length; i++) { - dishonestL2Outputs[i] = i > 7 ? 0xFF : i + 1; + dishonestL2Outputs[i] = i > 7 ? 0xFF : honestL2Outputs[i]; } // The dishonest trace correct is half correct, half incorrect. bytes memory dishonestTrace = new bytes(256); @@ -2588,7 +2690,7 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_TestInit { // Run the actor test _actorTest({ - _rootClaim: 0xFF, + _super: honestSuper, _absolutePrestateData: 0, _honestTrace: honestTrace, _honestL2Outputs: honestL2Outputs, @@ -2600,11 +2702,16 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_TestInit { /// @notice Static unit test for a 1v1 output bisection dispute. function test_static_1v1honestRootFinalInstruction_succeeds() public { + Types.SuperRootProof memory honestSuper = _dummySuper(uint64(validl2SequenceNumber)); + // The honest l2 outputs are from [1, 16] in this game. uint256[] memory honestL2Outputs = new uint256[](16); - for (uint256 i; i < honestL2Outputs.length; i++) { + for (uint256 i; i < honestL2Outputs.length - 1; i++) { honestL2Outputs[i] = i + 1; } + // to ensure that the trace ancestor at split depth is the root claim + honestL2Outputs[15] = uint256(Hashing.hashSuperRootProof(honestSuper)); + // The honest trace covers all block -> block + 1 transitions, and is 256 bytes long, // consisting of bytes [0, 255]. bytes memory honestTrace = new bytes(256); @@ -2615,7 +2722,7 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_TestInit { // The dishonest l2 outputs are half correct, half incorrect. uint256[] memory dishonestL2Outputs = new uint256[](16); for (uint256 i; i < dishonestL2Outputs.length; i++) { - dishonestL2Outputs[i] = i > 7 ? 0xFF : i + 1; + dishonestL2Outputs[i] = i > 7 ? 0xFF : honestL2Outputs[i]; } // The dishonest trace is half correct, and correct all the way up to the final instruction // of the exec subgame. @@ -2626,7 +2733,7 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_TestInit { // Run the actor test _actorTest({ - _rootClaim: 16, + _super: honestSuper, _absolutePrestateData: 0, _honestTrace: honestTrace, _honestL2Outputs: honestL2Outputs, @@ -2650,11 +2757,16 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_TestInit { honestTrace[i] = bytes1(uint8(i)); } + Types.SuperRootProof memory dishonestSuper = createSuper(0xFF); + // The dishonest l2 outputs are half correct, half incorrect. uint256[] memory dishonestL2Outputs = new uint256[](16); - for (uint256 i; i < dishonestL2Outputs.length; i++) { - dishonestL2Outputs[i] = i > 7 ? 0xFF : i + 1; + for (uint256 i; i < dishonestL2Outputs.length - 1; i++) { + dishonestL2Outputs[i] = i > 7 ? 0xFF : honestL2Outputs[i]; } + // to ensure that the trace ancestor at split depth is the root claim + dishonestL2Outputs[15] = uint256(Hashing.hashSuperRootProof(dishonestSuper)); + // The dishonest trace is half correct, and correct all the way up to the final instruction // of the exec subgame. bytes memory dishonestTrace = new bytes(256); @@ -2664,7 +2776,7 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_TestInit { // Run the actor test _actorTest({ - _rootClaim: 0xFF, + _super: dishonestSuper, _absolutePrestateData: 0, _honestTrace: honestTrace, _honestL2Outputs: honestL2Outputs, @@ -2680,7 +2792,7 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_TestInit { /// @notice Helper to run a 1v1 actor test function _actorTest( - uint256 _rootClaim, + Types.SuperRootProof memory _super, uint256 _absolutePrestateData, bytes memory _honestTrace, uint256[] memory _honestL2Outputs, @@ -2701,8 +2813,7 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_TestInit { } // Setup the environment - bytes memory absolutePrestateData = - _setup({ _absolutePrestateData: _absolutePrestateData, _rootClaim: _rootClaim }); + bytes memory absolutePrestateData = _setup({ _absolutePrestateData: _absolutePrestateData, _super: _super }); // Create actors _createActors({ @@ -2725,7 +2836,7 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_TestInit { /// @notice Helper to setup the 1v1 test function _setup( uint256 _absolutePrestateData, - uint256 _rootClaim + Types.SuperRootProof memory _super ) internal returns (bytes memory absolutePrestateData_) @@ -2733,8 +2844,8 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_TestInit { absolutePrestateData_ = abi.encode(_absolutePrestateData); Claim absolutePrestateExec = _changeClaimStatus(Claim.wrap(keccak256(absolutePrestateData_)), VMStatuses.UNFINISHED); - Claim rootClaim = Claim.wrap(bytes32(uint256(_rootClaim))); - init({ _rootClaim: rootClaim, _absolutePrestate: absolutePrestateExec, _l2SequenceNumber: _rootClaim }); + Claim rootClaim = Claim.wrap(Hashing.hashSuperRootProof(_super)); + init({ _rootClaim: rootClaim, _absolutePrestate: absolutePrestateExec, _super: _super }); } /// @notice Helper to create actors for the 1v1 dispute. diff --git a/packages/contracts-bedrock/test/dispute/SuperPermissionedDisputeGame.t.sol b/packages/contracts-bedrock/test/dispute/SuperPermissionedDisputeGame.t.sol index 5a4361dec01..c3d0dd5412b 100644 --- a/packages/contracts-bedrock/test/dispute/SuperPermissionedDisputeGame.t.sol +++ b/packages/contracts-bedrock/test/dispute/SuperPermissionedDisputeGame.t.sol @@ -8,6 +8,9 @@ import { AlphabetVM } from "test/mocks/AlphabetVM.sol"; // Libraries import "src/dispute/lib/Types.sol"; import "src/dispute/lib/Errors.sol"; +import { Types } from "src/libraries/Types.sol"; +import { Hashing } from "src/libraries/Hashing.sol"; +import { Encoding } from "src/libraries/Encoding.sol"; // Interfaces import { ISuperPermissionedDisputeGame } from "interfaces/dispute/ISuperPermissionedDisputeGame.sol"; @@ -36,8 +39,9 @@ abstract contract SuperPermissionedDisputeGame_TestInit is DisputeGameFactory_Te /// @notice The root claim of the game. Claim internal rootClaim; - /// @notice An arbitrary root claim for testing. - Claim internal arbitaryRootClaim = Claim.wrap(bytes32(uint256(123))); + /// @notice The Super Root Proof of the rootClaim + Types.SuperRootProof internal superRootProof; + /// @notice Minimum bond value that covers all possible moves. uint256 internal constant MIN_BOND = 50 ether; @@ -50,7 +54,7 @@ abstract contract SuperPermissionedDisputeGame_TestInit is DisputeGameFactory_Te event Move(uint256 indexed parentIndex, Claim indexed pivot, address indexed claimant); - function init(Claim _rootClaim, Claim _absolutePrestate, uint256 _l2BlockNumber) public { + function init(Claim _rootClaim, Claim _absolutePrestate, Types.SuperRootProof memory _super) public { // Set the time to a realistic date. if (!isForkTest()) { vm.warp(1690906994); @@ -60,7 +64,7 @@ abstract contract SuperPermissionedDisputeGame_TestInit is DisputeGameFactory_Te vm.deal(PROPOSER, 100 ether); // Set the extra data for the game creation - extraData = abi.encode(_l2BlockNumber); + extraData = Encoding.encodeSuperRootProof(_super); (address _impl, AlphabetVM _vm,) = setupSuperPermissionedDisputeGame(_absolutePrestate, PROPOSER, CHALLENGER); gameImpl = ISuperPermissionedDisputeGame(_impl); @@ -89,6 +93,11 @@ abstract contract SuperPermissionedDisputeGame_TestInit is DisputeGameFactory_Te assertEq(address(gameProxy.anchorStateRegistry()), address(anchorStateRegistry)); assertEq(address(gameProxy.vm()), address(_vm)); assertEq(address(gameProxy.gameCreator()), PROPOSER); + // Check extra data + assertEq(gameProxy.l2SequenceNumber(), _super.timestamp); + for (uint256 i; i < _super.outputRoots.length; i++) { + assertEq(gameProxy.rootClaimByChainId(_super.outputRoots[i].chainId).raw(), _super.outputRoots[i].root); + } // Label the proxy vm.label(address(gameProxy), "FaultDisputeGame_Clone"); @@ -101,10 +110,16 @@ abstract contract SuperPermissionedDisputeGame_TestInit is DisputeGameFactory_Te super.setUp(); // Get the actual anchor roots - (Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot(); - validL2BlockNumber = l2BlockNumber + 1; - rootClaim = Claim.wrap(Hash.unwrap(root)); - init({ _rootClaim: rootClaim, _absolutePrestate: absolutePrestate, _l2BlockNumber: validL2BlockNumber }); + (, uint256 l2Seqno) = anchorStateRegistry.getAnchorRoot(); + validL2BlockNumber = l2Seqno + 1; + + superRootProof.version = bytes1(uint8(1)); + superRootProof.timestamp = uint64(validL2BlockNumber); + superRootProof.outputRoots.push(Types.OutputRootWithChainId({ chainId: 5, root: keccak256(abi.encode(5)) })); + superRootProof.outputRoots.push(Types.OutputRootWithChainId({ chainId: 6, root: keccak256(abi.encode(6)) })); + rootClaim = Claim.wrap(Hashing.hashSuperRootProof(superRootProof)); + + init({ _rootClaim: rootClaim, _absolutePrestate: absolutePrestate, _super: superRootProof }); } /// @notice Helper to return a pseudo-random claim @@ -126,6 +141,16 @@ abstract contract SuperPermissionedDisputeGame_TestInit is DisputeGameFactory_Te } } + /// @notice Helper to create an arbitrary SuperRootProof + function _arbitraryRootClaim() internal view returns (Types.SuperRootProof memory super_) { + Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](1); + outputRoots[0] = Types.OutputRootWithChainId({ chainId: 99, root: keccak256(abi.encode(5)) }); + super_.version = bytes1(uint8(1)); + super_.timestamp = uint64(validL2BlockNumber + 100); + super_.outputRoots = outputRoots; + return super_; + } + fallback() external payable { } receive() external payable { } @@ -209,8 +234,13 @@ contract SuperPermissionedDisputeGame_Uncategorized_Test is SuperPermissionedDis /// @notice Tests that the proposer can create a permissioned dispute game. function test_createGame_proposer_succeeds() public { uint256 bondAmount = disputeGameFactory.initBonds(GAME_TYPE); + Types.SuperRootProof memory arbitrarySuperRootProof = _arbitraryRootClaim(); vm.prank(PROPOSER, PROPOSER); - disputeGameFactory.create{ value: bondAmount }(GAME_TYPE, arbitaryRootClaim, abi.encode(validL2BlockNumber)); + disputeGameFactory.create{ value: bondAmount }( + GAME_TYPE, + Claim.wrap(Hashing.hashSuperRootProof(arbitrarySuperRootProof)), + Encoding.encodeSuperRootProof(arbitrarySuperRootProof) + ); } /// @notice Tests that the permissioned game cannot be created by any address other than the @@ -218,11 +248,16 @@ contract SuperPermissionedDisputeGame_Uncategorized_Test is SuperPermissionedDis function testFuzz_createGame_notProposer_reverts(address _p) public { vm.assume(_p != PROPOSER); + Types.SuperRootProof memory arbitrarySuperRootProof = _arbitraryRootClaim(); uint256 bondAmount = disputeGameFactory.initBonds(GAME_TYPE); vm.deal(_p, bondAmount); vm.prank(_p, _p); vm.expectRevert(BadAuth.selector); - disputeGameFactory.create{ value: bondAmount }(GAME_TYPE, arbitaryRootClaim, abi.encode(validL2BlockNumber)); + disputeGameFactory.create{ value: bondAmount }( + GAME_TYPE, + Claim.wrap(Hashing.hashSuperRootProof(arbitrarySuperRootProof)), + Encoding.encodeSuperRootProof(arbitrarySuperRootProof) + ); } /// @notice Tests that the challenger can participate in a permissioned dispute game. @@ -293,7 +328,7 @@ contract SuperPermissionedDisputeGame_Initialize_Test is SuperPermissionedDisput // bytecode. // [0 bytes, 31 bytes] u [33 bytes, 23.5 KB] _extraDataLen = bound(_extraDataLen, 0, 23_500); - if (_extraDataLen == 32) { + if (_extraDataLen > 9 && (_extraDataLen - 9) % 64 == 0) { _extraDataLen++; } bytes memory _extraData = new bytes(_extraDataLen); diff --git a/packages/contracts-bedrock/test/dispute/lib/LibClock.t.sol b/packages/contracts-bedrock/test/dispute/lib/LibClock.t.sol index 4bf90c1dfc1..8ec76658b9b 100644 --- a/packages/contracts-bedrock/test/dispute/lib/LibClock.t.sol +++ b/packages/contracts-bedrock/test/dispute/lib/LibClock.t.sol @@ -1,7 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; + +// Libraries import { LibClock } from "src/dispute/lib/LibUDT.sol"; import "src/dispute/lib/Types.sol"; diff --git a/packages/contracts-bedrock/test/dispute/lib/LibGameArgs.t.sol b/packages/contracts-bedrock/test/dispute/lib/LibGameArgs.t.sol index 6fd4c21e22a..1eee52105da 100644 --- a/packages/contracts-bedrock/test/dispute/lib/LibGameArgs.t.sol +++ b/packages/contracts-bedrock/test/dispute/lib/LibGameArgs.t.sol @@ -1,7 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; + +// Libraries import { LibGameArgs } from "src/dispute/lib/LibGameArgs.sol"; import { InvalidGameArgsLength } from "src/dispute/lib/Errors.sol"; diff --git a/packages/contracts-bedrock/test/dispute/lib/LibGameId.t.sol b/packages/contracts-bedrock/test/dispute/lib/LibGameId.t.sol index b1878178c1a..d8ea4729d6c 100644 --- a/packages/contracts-bedrock/test/dispute/lib/LibGameId.t.sol +++ b/packages/contracts-bedrock/test/dispute/lib/LibGameId.t.sol @@ -1,8 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; +// Libraries import "src/dispute/lib/Types.sol"; /// @title LibGameId_Pack_Test diff --git a/packages/contracts-bedrock/test/dispute/lib/LibPosition.t.sol b/packages/contracts-bedrock/test/dispute/lib/LibPosition.t.sol index 8d392fa49c5..d38223ff0d0 100644 --- a/packages/contracts-bedrock/test/dispute/lib/LibPosition.t.sol +++ b/packages/contracts-bedrock/test/dispute/lib/LibPosition.t.sol @@ -1,7 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; + +// Libraries import { LibPosition } from "src/dispute/lib/LibPosition.sol"; import "src/dispute/lib/Types.sol"; diff --git a/packages/contracts-bedrock/test/dispute/zk/OptimisticZkGame.t.sol b/packages/contracts-bedrock/test/dispute/zk/OptimisticZkGame.t.sol new file mode 100644 index 00000000000..fff896b9872 --- /dev/null +++ b/packages/contracts-bedrock/test/dispute/zk/OptimisticZkGame.t.sol @@ -0,0 +1,732 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Testing +import { DisputeGameFactory_TestInit } from "test/dispute/DisputeGameFactory.t.sol"; + +// Libraries +import { Claim, Duration, GameStatus, GameType, Timestamp } from "src/dispute/lib/Types.sol"; +import { + BadAuth, + IncorrectBondAmount, + UnexpectedRootClaim, + NoCreditToClaim, + GameNotFinalized, + ParentGameNotResolved, + InvalidParentGame, + ClaimAlreadyChallenged, + GameOver, + GameNotOver, + IncorrectDisputeGameFactory +} from "src/dispute/lib/Errors.sol"; +import { GameTypes } from "src/dispute/lib/Types.sol"; + +// Contracts +import { DisputeGameFactory } from "src/dispute/DisputeGameFactory.sol"; +import { OptimisticZkGame } from "src/dispute/zk/OptimisticZkGame.sol"; +import { AccessManager } from "src/dispute/zk/AccessManager.sol"; + +// Interfaces +import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; +import { Proxy } from "src/universal/Proxy.sol"; + +/// @title OptimisticZkGame_TestInit +/// @notice Base test contract with shared setup for OptimisticZkGame tests. +abstract contract OptimisticZkGame_TestInit is DisputeGameFactory_TestInit { + // Events + event Challenged(address indexed challenger); + event Proved(address indexed prover); + event Resolved(GameStatus indexed status); + + OptimisticZkGame gameImpl; + OptimisticZkGame parentGame; + OptimisticZkGame game; + AccessManager accessManager; + + address proposer = address(0x123); + address challenger = address(0x456); + address prover = address(0x789); + + // Fixed parameters. + GameType gameType = GameTypes.OPTIMISTIC_ZK_GAME_TYPE; + Duration maxChallengeDuration = Duration.wrap(12 hours); + Duration maxProveDuration = Duration.wrap(3 days); + Claim rootClaim = Claim.wrap(keccak256("rootClaim")); + + // Sequence number offsets from anchor state (for parent and child games). + uint256 parentSequenceOffset = 1000; + uint256 childSequenceOffset = 2000; + + // Game indices are set dynamically in setUp (on fork, existing games already exist) + uint32 parentGameIndex; + uint32 childGameIndex; + + // Offsets from child sequence number for grandchild games. + uint256 grandchildOffset1 = 1000; + uint256 grandchildOffset2 = 2000; + uint256 grandchildOffset3 = 3000; + uint256 grandchildOffset4 = 8000; + + // Actual sequence numbers (set in setUp based on anchor state) + uint256 anchorL2SequenceNumber; + uint256 parentL2SequenceNumber; + uint256 childL2SequenceNumber; + + // For a new parent game that we manipulate separately in some tests. + OptimisticZkGame separateParentGame; + + function setUp() public virtual override { + super.setUp(); + skipIfForkTest("Skip not supported yet"); + + // Get anchor state to calculate valid sequence numbers + (, anchorL2SequenceNumber) = anchorStateRegistry.getAnchorRoot(); + parentL2SequenceNumber = anchorL2SequenceNumber + parentSequenceOffset; + childL2SequenceNumber = anchorL2SequenceNumber + childSequenceOffset; + + // Setup game implementation using shared helper + address impl; + (impl, accessManager,) = setupOptimisticZkGame( + OptimisticZkGameParams({ + maxChallengeDuration: maxChallengeDuration, + maxProveDuration: maxProveDuration, + proposer: proposer, + challenger: challenger, + rollupConfigHash: bytes32(0), + aggregationVkey: bytes32(0), + rangeVkeyCommitment: bytes32(0), + challengerBond: 1 ether + }) + ); + gameImpl = OptimisticZkGame(impl); + + // Create the first (parent) game – it uses uint32.max as parent index. + vm.startPrank(proposer); + vm.deal(proposer, 2 ether); + + // Warp time forward to ensure the parent game is created after the respectedGameTypeUpdatedAt timestamp. + vm.warp(block.timestamp + 1000); + + // Create parent game (uses uint32.max to indicate first game in chain). + parentGame = OptimisticZkGame( + address( + disputeGameFactory.create{ value: 1 ether }( + gameType, + Claim.wrap(keccak256("genesis")), + abi.encodePacked(parentL2SequenceNumber, type(uint32).max) + ) + ) + ); + + // Record actual index of parent game (on fork, existing games already occupy indices 0, 1, ...) + parentGameIndex = uint32(disputeGameFactory.gameCount() - 1); + + // We want the parent game to finalize. We'll skip its challenge period. + (,,,,, Timestamp parentGameDeadline) = parentGame.claimData(); + vm.warp(parentGameDeadline.raw() + 1 seconds); + parentGame.resolve(); + + uint256 finalityDelay = anchorStateRegistry.disputeGameFinalityDelaySeconds(); + vm.warp(parentGame.resolvedAt().raw() + finalityDelay + 1 seconds); + parentGame.claimCredit(proposer); + + // Create the child game referencing actual parent game index. + game = OptimisticZkGame( + address( + disputeGameFactory.create{ value: 1 ether }( + gameType, rootClaim, abi.encodePacked(childL2SequenceNumber, parentGameIndex) + ) + ) + ); + + // Record actual index of child game. + childGameIndex = uint32(disputeGameFactory.gameCount() - 1); + + vm.stopPrank(); + } +} + +/// @title OptimisticZkGame_Initialize_Test +/// @notice Tests for initialization of OptimisticZkGame. +contract OptimisticZkGame_Initialize_Test is OptimisticZkGame_TestInit { + function test_initialize_succeeds() public view { + // Test that the factory is correctly initialized. + assertEq(address(disputeGameFactory.owner()), address(this)); + assertEq(address(disputeGameFactory.gameImpls(gameType)), address(gameImpl)); + // We expect games including parent and child (indices may vary on fork). + assertEq(disputeGameFactory.gameCount(), childGameIndex + 1); + + // Check that our child game matches the game at childGameIndex. + (,, IDisputeGame proxy_) = disputeGameFactory.gameAtIndex(childGameIndex); + assertEq(address(game), address(proxy_)); + + // Check the child game fields. + assertEq(game.gameType().raw(), gameType.raw()); + assertEq(game.rootClaim().raw(), rootClaim.raw()); + assertEq(game.maxChallengeDuration().raw(), maxChallengeDuration.raw()); + assertEq(game.maxProveDuration().raw(), maxProveDuration.raw()); + assertEq(address(game.disputeGameFactory()), address(disputeGameFactory)); + assertEq(game.l2SequenceNumber(), childL2SequenceNumber); + + // The parent's sequence number (startingBlockNumber() returns l2SequenceNumber). + assertEq(game.startingBlockNumber(), parentL2SequenceNumber); + + // The parent's root was keccak256("genesis"). + assertEq(game.startingRootHash().raw(), keccak256("genesis")); + + assertEq(address(game).balance, 1 ether); + + // Check the claimData. + ( + uint32 parentIndex_, + address counteredBy_, + address prover_, + Claim claim_, + OptimisticZkGame.ProposalStatus status_, + Timestamp deadline_ + ) = game.claimData(); + + assertEq(parentIndex_, parentGameIndex); + assertEq(counteredBy_, address(0)); + assertEq(game.gameCreator(), proposer); + assertEq(prover_, address(0)); + assertEq(claim_.raw(), rootClaim.raw()); + + // Initially, the status is Unchallenged. + assertEq(uint8(status_), uint8(OptimisticZkGame.ProposalStatus.Unchallenged)); + + // The child's initial deadline is block.timestamp + maxChallengeDuration. + uint256 currentTime = block.timestamp; + uint256 expectedDeadline = currentTime + maxChallengeDuration.raw(); + assertEq(deadline_.raw(), expectedDeadline); + } + + function test_initialize_blockNumberTooSmall_reverts() public { + // Try to create a child game that references a block number smaller than parent's. + vm.startPrank(proposer); + vm.deal(proposer, 1 ether); + + // We expect revert because l2BlockNumber (1) < parent's block number + vm.expectRevert( + abi.encodeWithSelector( + UnexpectedRootClaim.selector, + Claim.wrap(keccak256("rootClaim")) // The rootClaim we pass. + ) + ); + + disputeGameFactory.create{ value: 1 ether }( + gameType, + rootClaim, + abi.encodePacked(uint256(1), parentGameIndex) // L2 block is smaller than parent's block. + ); + vm.stopPrank(); + } + + function test_initialize_parentBlacklisted_reverts() public { + // Blacklist the game on the anchor state registry (which is what's actually used for validation). + vm.prank(superchainConfig.guardian()); + anchorStateRegistry.blacklistDisputeGame(IDisputeGame(address(game))); + + vm.startPrank(proposer); + vm.deal(proposer, 1 ether); + vm.expectRevert(InvalidParentGame.selector); + disputeGameFactory.create{ value: 1 ether }( + gameType, + Claim.wrap(keccak256("blacklisted-parent-game")), + abi.encodePacked(childL2SequenceNumber + grandchildOffset1, childGameIndex) + ); + vm.stopPrank(); + } + + function test_initialize_parentNotRespected_reverts() public { + // Create a new game which will be the parent. + vm.startPrank(proposer); + vm.deal(proposer, 1 ether); + OptimisticZkGame parentNotRespected = OptimisticZkGame( + address( + disputeGameFactory.create{ value: 1 ether }( + gameType, + Claim.wrap(keccak256("not-respected-parent-game")), + abi.encodePacked(childL2SequenceNumber + grandchildOffset1, childGameIndex) + ) + ) + ); + uint32 parentNotRespectedIndex = uint32(disputeGameFactory.gameCount() - 1); + vm.stopPrank(); + + // Blacklist the parent game to make it invalid. + vm.prank(superchainConfig.guardian()); + anchorStateRegistry.blacklistDisputeGame(IDisputeGame(address(parentNotRespected))); + + // Try to create a game with a parent game that is not valid. + vm.startPrank(proposer); + vm.deal(proposer, 1 ether); + vm.expectRevert(InvalidParentGame.selector); + disputeGameFactory.create{ value: 1 ether }( + gameType, + Claim.wrap(keccak256("child-with-not-respected-parent")), + abi.encodePacked(childL2SequenceNumber + grandchildOffset2, parentNotRespectedIndex) + ); + vm.stopPrank(); + } + + function test_initialize_noPermission_reverts() public { + address maliciousProposer = address(0x1234); + + vm.startPrank(maliciousProposer); + vm.deal(maliciousProposer, 1 ether); + + vm.expectRevert(BadAuth.selector); + disputeGameFactory.create{ value: 1 ether }( + gameType, + Claim.wrap(keccak256("new-claim")), + abi.encodePacked(childL2SequenceNumber + grandchildOffset1, childGameIndex) + ); + + vm.stopPrank(); + } + + function test_initialize_wrongFactory_reverts() public { + // Deploy the implementation contract for new DisputeGameFactory. + DisputeGameFactory newFactoryImpl = new DisputeGameFactory(); + + // Deploy a proxy pointing to the new factory implementation. + Proxy newFactoryProxyContract = new Proxy(address(this)); + newFactoryProxyContract.upgradeTo(address(newFactoryImpl)); + + // Cast the proxy to the DisputeGameFactory interface and initialize it. + DisputeGameFactory newFactory = DisputeGameFactory(address(newFactoryProxyContract)); + newFactory.initialize(address(this)); + + // Set the implementation with the same implementation as the old disputeGameFactory. + newFactory.setImplementation(gameType, IDisputeGame(address(gameImpl))); + newFactory.setInitBond(gameType, 1 ether); + + vm.startPrank(proposer); + vm.deal(proposer, 1 ether); + + vm.expectRevert(IncorrectDisputeGameFactory.selector); + newFactory.create{ value: 1 ether }( + gameType, + Claim.wrap(keccak256("new-claim")), + abi.encodePacked(childL2SequenceNumber + grandchildOffset1, childGameIndex) + ); + + vm.stopPrank(); + } +} + +/// @title OptimisticZkGame_Resolve_Test +/// @notice Tests for resolve functionality of OptimisticZkGame. +contract OptimisticZkGame_Resolve_Test is OptimisticZkGame_TestInit { + function test_resolve_unchallenged_succeeds() public { + assertEq(uint8(game.status()), uint8(GameStatus.IN_PROGRESS)); + + // Should revert if we try to resolve before deadline. + vm.expectRevert(GameNotOver.selector); + game.resolve(); + + // Warp forward past the challenge deadline. + (,,,,, Timestamp deadline) = game.claimData(); + vm.warp(deadline.raw() + 1); + + // Expect the Resolved event. + vm.expectEmit(true, false, false, false, address(game)); + emit Resolved(GameStatus.DEFENDER_WINS); + + // Now we can resolve successfully. + game.resolve(); + + // Proposer gets the bond back. + vm.warp(game.resolvedAt().raw() + anchorStateRegistry.disputeGameFinalityDelaySeconds() + 1 seconds); + game.claimCredit(proposer); + + // Check final state + assertEq(uint8(game.status()), uint8(GameStatus.DEFENDER_WINS)); + // The contract should have paid back the proposer. + assertEq(address(game).balance, 0); + // Proposer posted 1 ether, so they get it back. + assertEq(proposer.balance, 2 ether); + assertEq(challenger.balance, 0); + } + + function test_resolve_unchallengedWithProof_succeeds() public { + assertEq(uint8(game.status()), uint8(GameStatus.IN_PROGRESS)); + + // Should revert if we try to resolve before the first challenge deadline. + vm.expectRevert(GameNotOver.selector); + game.resolve(); + + // Prover proves the claim while unchallenged. + vm.startPrank(prover); + game.prove(bytes("")); + vm.stopPrank(); + + // Now the proposal is UnchallengedAndValidProofProvided; we can resolve immediately. + game.resolve(); + + // Prover does not get any credit. + vm.warp(game.resolvedAt().raw() + anchorStateRegistry.disputeGameFinalityDelaySeconds() + 1 seconds); + vm.expectRevert(NoCreditToClaim.selector); + game.claimCredit(prover); + + // Proposer gets the bond back. + game.claimCredit(proposer); + + // Final status: DEFENDER_WINS. + assertEq(uint8(game.status()), uint8(GameStatus.DEFENDER_WINS)); + assertEq(address(game).balance, 0); + + // Proposer gets their 1 ether back. + assertEq(proposer.balance, 2 ether); + // Prover does NOT get the reward because no challenger posted a bond. + assertEq(prover.balance, 0 ether); + assertEq(challenger.balance, 0); + } + + function test_resolve_challengedWithProof_succeeds() public { + assertEq(uint8(game.status()), uint8(GameStatus.IN_PROGRESS)); + assertEq(address(game).balance, 1 ether); + + // Try to resolve too early. + vm.expectRevert(GameNotOver.selector); + game.resolve(); + + // Challenger posts the bond incorrectly. + vm.startPrank(challenger); + vm.deal(challenger, 1 ether); + + // Must pay exactly the required bond. + vm.expectRevert(IncorrectBondAmount.selector); + game.challenge{ value: 0.5 ether }(); + + // Correctly challenge the game. + game.challenge{ value: 1 ether }(); + vm.stopPrank(); + + // Now the contract holds 2 ether total. + assertEq(address(game).balance, 2 ether); + + // Confirm the proposal is in Challenged state. + (, address counteredBy_,,, OptimisticZkGame.ProposalStatus challStatus,) = game.claimData(); + assertEq(counteredBy_, challenger); + assertEq(uint8(challStatus), uint8(OptimisticZkGame.ProposalStatus.Challenged)); + + // Prover proves the claim in time. + vm.startPrank(prover); + game.prove(bytes("")); + vm.stopPrank(); + + // Confirm the proposal is now ChallengedAndValidProofProvided. + (,,,, challStatus,) = game.claimData(); + assertEq(uint8(challStatus), uint8(OptimisticZkGame.ProposalStatus.ChallengedAndValidProofProvided)); + assertEq(uint8(game.status()), uint8(GameStatus.IN_PROGRESS)); + + // Resolve the game. + game.resolve(); + + // Prover gets the proof reward. + vm.warp(game.resolvedAt().raw() + anchorStateRegistry.disputeGameFinalityDelaySeconds() + 1 seconds); + game.claimCredit(prover); + + // Proposer gets the bond back. + game.claimCredit(proposer); + + assertEq(uint8(game.status()), uint8(GameStatus.DEFENDER_WINS)); + assertEq(address(game).balance, 0); + + // Final balances: + // - The proposer recovers their 1 ether stake. + // - The prover gets 1 ether reward. + // - The challenger gets nothing. + assertEq(proposer.balance, 2 ether); + assertEq(prover.balance, 1 ether); + assertEq(challenger.balance, 0); + } + + function test_resolve_challengedNoProof_succeeds() public { + // Challenge the game. + vm.startPrank(challenger); + vm.deal(challenger, 2 ether); + game.challenge{ value: 1 ether }(); + vm.stopPrank(); + + // The contract now has 2 ether total. + assertEq(address(game).balance, 2 ether); + + // We must wait for the prove deadline to pass. + (,,,,, Timestamp deadline) = game.claimData(); + vm.warp(deadline.raw() + 1); + + // Now we can resolve, resulting in CHALLENGER_WINS. + game.resolve(); + + // Challenger gets the bond back and wins proposer's bond. + vm.warp(game.resolvedAt().raw() + anchorStateRegistry.disputeGameFinalityDelaySeconds() + 1 seconds); + game.claimCredit(challenger); + + assertEq(uint8(game.status()), uint8(GameStatus.CHALLENGER_WINS)); + + // The challenger receives the entire 3 ether. + assertEq(challenger.balance, 3 ether); // started with 2, spent 1, got 2 from the game. + + // The proposer loses their 1 ether stake. + assertEq(proposer.balance, 1 ether); // started with 2, lost 1. + // The contract balance is zero. + assertEq(address(game).balance, 0); + } + + function test_resolve_parentGameInProgress_reverts() public { + vm.startPrank(proposer); + + // Create a new game referencing the child game as parent. + OptimisticZkGame childGame = OptimisticZkGame( + address( + disputeGameFactory.create{ value: 1 ether }( + gameType, + Claim.wrap(keccak256("new-claim")), + abi.encodePacked(childL2SequenceNumber + grandchildOffset1, childGameIndex) + ) + ) + ); + + vm.stopPrank(); + + // The parent game is still in progress, not resolved. + // So, if we try to resolve the childGame, it should revert with ParentGameNotResolved. + vm.expectRevert(ParentGameNotResolved.selector); + childGame.resolve(); + } + + function test_resolve_parentGameInvalid_succeeds() public { + // 1) Now create a child game referencing that losing parent at index 1. + vm.startPrank(proposer); + OptimisticZkGame childGame = OptimisticZkGame( + address( + disputeGameFactory.create{ value: 1 ether }( + gameType, + Claim.wrap(keccak256("child-of-loser")), + abi.encodePacked(childL2SequenceNumber + grandchildOffset4, childGameIndex) + ) + ) + ); + vm.stopPrank(); + + // 2) Challenge the parent game so that it ends up CHALLENGER_WINS when proof is not provided within the prove + // deadline. + vm.startPrank(challenger); + vm.deal(challenger, 2 ether); + game.challenge{ value: 1 ether }(); + vm.stopPrank(); + + // 3) Warp past the prove deadline. + (,,,,, Timestamp gameDeadline) = game.claimData(); + vm.warp(gameDeadline.raw() + 1); + + // 4) The game resolves as CHALLENGER_WINS. + game.resolve(); + + // Challenger gets the bond back and wins proposer's bond. + vm.warp(game.resolvedAt().raw() + anchorStateRegistry.disputeGameFinalityDelaySeconds() + 1 seconds); + game.claimCredit(challenger); + + assertEq(uint8(game.status()), uint8(GameStatus.CHALLENGER_WINS)); + + // 5) If we try to resolve the child game, it should be resolved as CHALLENGER_WINS + // because parent's claim is invalid. + // The child's bond is lost since there is no challenger for the child game. + childGame.resolve(); + + // Challenger hasn't challenged the child game, so it gets nothing. + vm.warp(childGame.resolvedAt().raw() + anchorStateRegistry.disputeGameFinalityDelaySeconds() + 1 seconds); + + vm.expectRevert(NoCreditToClaim.selector); + childGame.claimCredit(challenger); + + assertEq(uint8(childGame.status()), uint8(GameStatus.CHALLENGER_WINS)); + + assertEq(address(childGame).balance, 1 ether); + assertEq(address(challenger).balance, 3 ether); + assertEq(address(proposer).balance, 0 ether); + } +} + +/// @title OptimisticZkGame_Challenge_Test +/// @notice Tests for challenge functionality of OptimisticZkGame. +contract OptimisticZkGame_Challenge_Test is OptimisticZkGame_TestInit { + function test_challenge_alreadyChallenged_reverts() public { + // Initially unchallenged. + (, address counteredBy_,,, OptimisticZkGame.ProposalStatus status_,) = game.claimData(); + assertEq(counteredBy_, address(0)); + assertEq(uint8(status_), uint8(OptimisticZkGame.ProposalStatus.Unchallenged)); + + // The first challenge is valid. + vm.startPrank(challenger); + vm.deal(challenger, 2 ether); + game.challenge{ value: 1 ether }(); + + // A second challenge from any party should revert because the proposal is no longer "Unchallenged". + vm.expectRevert(ClaimAlreadyChallenged.selector); + game.challenge{ value: 1 ether }(); + vm.stopPrank(); + } + + function test_challenge_noPermission_reverts() public { + address maliciousChallenger = address(0x1234); + + vm.startPrank(maliciousChallenger); + vm.deal(maliciousChallenger, 1 ether); + + vm.expectRevert(BadAuth.selector); + game.challenge{ value: 1 ether }(); + + vm.stopPrank(); + } +} + +/// @title OptimisticZkGame_Prove_Test +/// @notice Tests for prove functionality of OptimisticZkGame. +contract OptimisticZkGame_Prove_Test is OptimisticZkGame_TestInit { + function test_prove_afterDeadline_reverts() public { + // Challenge first. + vm.startPrank(challenger); + vm.deal(challenger, 1 ether); + game.challenge{ value: 1 ether }(); + vm.stopPrank(); + + // Move time forward beyond the prove period. + (,,,,, Timestamp deadline) = game.claimData(); + vm.warp(deadline.raw() + 1); + + vm.startPrank(prover); + // Attempting to prove after the deadline is exceeded. + vm.expectRevert(GameOver.selector); + game.prove(bytes("")); + vm.stopPrank(); + } + + function test_prove_alreadyProved_reverts() public { + vm.startPrank(prover); + game.prove(bytes("")); + vm.expectRevert(GameOver.selector); + game.prove(bytes("")); + vm.stopPrank(); + } +} + +/// @title OptimisticZkGame_ClaimCredit_Test +/// @notice Tests for claimCredit functionality of OptimisticZkGame. +contract OptimisticZkGame_ClaimCredit_Test is OptimisticZkGame_TestInit { + function test_claimCredit_notFinalized_reverts() public { + (,,,,, Timestamp deadline) = game.claimData(); + vm.warp(deadline.raw() + 1); + game.resolve(); + + vm.expectRevert(GameNotFinalized.selector); + game.claimCredit(proposer); + } +} + +/// @title OptimisticZkGame_CloseGame_Test +/// @notice Tests for closeGame functionality of OptimisticZkGame. +contract OptimisticZkGame_CloseGame_Test is OptimisticZkGame_TestInit { + function test_closeGame_notResolved_reverts() public { + vm.expectRevert(GameNotFinalized.selector); + game.closeGame(); + } + + function test_closeGame_updatesAnchorGame_succeeds() public { + (,,,,, Timestamp deadline) = game.claimData(); + vm.warp(deadline.raw() + 1); + game.resolve(); + + vm.warp(game.resolvedAt().raw() + anchorStateRegistry.disputeGameFinalityDelaySeconds() + 1 seconds); + game.closeGame(); + + assertEq(address(anchorStateRegistry.anchorGame()), address(game)); + } +} + +/// @title OptimisticZkGame_AccessManager_Test +/// @notice Tests for AccessManager permissionless fallback functionality. +contract OptimisticZkGame_AccessManager_Test is OptimisticZkGame_TestInit { + function test_accessManager_permissionlessAfterTimeout_succeeds() public { + // Initially, unauthorized user should not be allowed + address unauthorizedUser = address(0x9999); + + // Try to create a game as unauthorized user - should fail + vm.prank(unauthorizedUser); + vm.deal(unauthorizedUser, 1 ether); + vm.expectRevert(BadAuth.selector); + disputeGameFactory.create{ value: 1 ether }( + gameType, + Claim.wrap(keccak256("new-claim-1")), + abi.encodePacked(childL2SequenceNumber + grandchildOffset1, childGameIndex) + ); + + vm.prank(proposer); + vm.deal(proposer, 1 ether); + disputeGameFactory.create{ value: 1 ether }( + gameType, Claim.wrap(keccak256("new-claim-2")), abi.encodePacked(childL2SequenceNumber, parentGameIndex) + ); + + // Warp time forward past the timeout + vm.warp(block.timestamp + 2 weeks + 1); + + // Now unauthorized user should be allowed due to timeout + vm.prank(unauthorizedUser); + vm.deal(unauthorizedUser, 1 ether); + disputeGameFactory.create{ value: 1 ether }( + gameType, + Claim.wrap(keccak256("new-claim-3")), + abi.encodePacked(childL2SequenceNumber + grandchildOffset2, childGameIndex) + ); + + // After the new game, timeout resets - unauthorized user should not be allowed immediately + vm.prank(unauthorizedUser); + vm.deal(unauthorizedUser, 1 ether); + vm.expectRevert(BadAuth.selector); + disputeGameFactory.create{ value: 1 ether }( + gameType, + Claim.wrap(keccak256("new-claim-4")), + abi.encodePacked(childL2SequenceNumber + grandchildOffset3, childGameIndex) + ); + } + + function test_accessManager_permissionlessNoGamesAfterTimeout_succeeds() public { + // Initially, unauthorized user should not be allowed + address unauthorizedUser = address(0x9999); + + // Try to create a game as unauthorized user - should fail + vm.prank(unauthorizedUser); + vm.deal(unauthorizedUser, 1 ether); + vm.expectRevert(BadAuth.selector); + disputeGameFactory.create{ value: 1 ether }( + gameType, + Claim.wrap(keccak256("new-claim-1")), + abi.encodePacked(childL2SequenceNumber + grandchildOffset1, childGameIndex) + ); + + // Warp time forward past the timeout + vm.warp(block.timestamp + 2 weeks + 1 hours); + + // Now unauthorized user should be allowed due to timeout + vm.prank(unauthorizedUser); + vm.deal(unauthorizedUser, 1 ether); + disputeGameFactory.create{ value: 1 ether }( + gameType, + Claim.wrap(keccak256("new-claim-3")), + abi.encodePacked(childL2SequenceNumber + grandchildOffset2, childGameIndex) + ); + + // After the new game, timeout resets - unauthorized user should not be allowed immediately + vm.prank(unauthorizedUser); + vm.deal(unauthorizedUser, 1 ether); + vm.expectRevert(BadAuth.selector); + disputeGameFactory.create{ value: 1 ether }( + gameType, + Claim.wrap(keccak256("new-claim-4")), + abi.encodePacked(childL2SequenceNumber + grandchildOffset3, childGameIndex) + ); + } +} diff --git a/packages/contracts-bedrock/test/dispute/zk/mocks/SP1MockVerifier.sol b/packages/contracts-bedrock/test/dispute/zk/mocks/SP1MockVerifier.sol new file mode 100644 index 00000000000..974aa1c8b31 --- /dev/null +++ b/packages/contracts-bedrock/test/dispute/zk/mocks/SP1MockVerifier.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import { ISP1Verifier } from "src/dispute/zk/ISP1Verifier.sol"; + +contract SP1MockVerifier is ISP1Verifier { + /// @notice Verifies a mock proof with given public values and vkey. + /// @param proofBytes The proof of the program execution the SP1 zkVM encoded as bytes. + function verifyProof(bytes32, bytes calldata, bytes calldata proofBytes) external pure { + assert(proofBytes.length == 0); + } +} diff --git a/packages/contracts-bedrock/test/integration/EventLogger.t.sol b/packages/contracts-bedrock/test/integration/EventLogger.t.sol index 51ecccc1a4e..46265da67b6 100644 --- a/packages/contracts-bedrock/test/integration/EventLogger.t.sol +++ b/packages/contracts-bedrock/test/integration/EventLogger.t.sol @@ -1,17 +1,20 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import { Test } from "forge-std/Test.sol"; - -import { Identifier as IfaceIdentifier } from "interfaces/L2/ICrossL2Inbox.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; +import { VmSafe } from "forge-std/Vm.sol"; +// Contracts import { EventLogger } from "../../src/integration/EventLogger.sol"; +import { CrossL2Inbox } from "src/L2/CrossL2Inbox.sol"; +// Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; +// Interfaces +import { Identifier as IfaceIdentifier } from "interfaces/L2/ICrossL2Inbox.sol"; import { ICrossL2Inbox, Identifier as ImplIdentifier } from "interfaces/L2/ICrossL2Inbox.sol"; -import { VmSafe } from "forge-std/Vm.sol"; -import { CrossL2Inbox } from "src/L2/CrossL2Inbox.sol"; /// @title EventLogger_TestInit /// @notice Reusable test initialization for `EventLogger` tests. diff --git a/packages/contracts-bedrock/test/invariants/CustomGasToken.t.sol b/packages/contracts-bedrock/test/invariants/CustomGasToken.t.sol index 764a3a0236c..334d8d44a56 100644 --- a/packages/contracts-bedrock/test/invariants/CustomGasToken.t.sol +++ b/packages/contracts-bedrock/test/invariants/CustomGasToken.t.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.15; // Testing -import { StdUtils } from "forge-std/Test.sol"; +import { StdUtils } from "forge-std/StdUtils.sol"; import { Vm } from "forge-std/Vm.sol"; import { CommonTest } from "test/setup/CommonTest.sol"; diff --git a/packages/contracts-bedrock/test/invariants/ETHLiquidity.t.sol b/packages/contracts-bedrock/test/invariants/ETHLiquidity.t.sol index ec046d1ebb5..02a4e63e316 100644 --- a/packages/contracts-bedrock/test/invariants/ETHLiquidity.t.sol +++ b/packages/contracts-bedrock/test/invariants/ETHLiquidity.t.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.15; // Testing -import { StdUtils } from "forge-std/Test.sol"; +import { StdUtils } from "forge-std/StdUtils.sol"; import { Vm } from "forge-std/Vm.sol"; import { CommonTest } from "test/setup/CommonTest.sol"; diff --git a/packages/contracts-bedrock/test/invariants/InvariantTest.sol b/packages/contracts-bedrock/test/invariants/InvariantTest.sol index eea6c158b35..c4720ec15d1 100644 --- a/packages/contracts-bedrock/test/invariants/InvariantTest.sol +++ b/packages/contracts-bedrock/test/invariants/InvariantTest.sol @@ -1,9 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; +// Testing +import { Test } from "test/setup/Test.sol"; import { FFIInterface } from "test/setup/FFIInterface.sol"; + +// Scripts import { Deploy } from "scripts/deploy/Deploy.s.sol"; -import { Test } from "forge-std/Test.sol"; /// @title InvariantTest /// @dev An extension to `Test` that sets up excluded contracts for invariant testing. diff --git a/packages/contracts-bedrock/test/invariants/OptimismPortal2.t.sol b/packages/contracts-bedrock/test/invariants/OptimismPortal2.t.sol index f826e026291..297f9a0e47a 100644 --- a/packages/contracts-bedrock/test/invariants/OptimismPortal2.t.sol +++ b/packages/contracts-bedrock/test/invariants/OptimismPortal2.t.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.15; // Testing -import { StdUtils } from "forge-std/Test.sol"; +import { StdUtils } from "forge-std/StdUtils.sol"; import { Vm } from "forge-std/Vm.sol"; import { CommonTest } from "test/setup/CommonTest.sol"; import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; diff --git a/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20/OptimismSuperchainERC20.t.sol b/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20/OptimismSuperchainERC20.t.sol index d53d2fd29f9..0d01a97a483 100644 --- a/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20/OptimismSuperchainERC20.t.sol +++ b/packages/contracts-bedrock/test/invariants/OptimismSuperchainERC20/OptimismSuperchainERC20.t.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -// Testing utilities -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; // Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; diff --git a/packages/contracts-bedrock/test/invariants/SuperFaultDisputeGame.t.sol b/packages/contracts-bedrock/test/invariants/SuperFaultDisputeGame.t.sol index 4e33e83b78f..cc0816d67e7 100644 --- a/packages/contracts-bedrock/test/invariants/SuperFaultDisputeGame.t.sol +++ b/packages/contracts-bedrock/test/invariants/SuperFaultDisputeGame.t.sol @@ -9,9 +9,11 @@ import { RandomClaimActor } from "test/invariants/FaultDisputeGame.t.sol"; // Libraries import "src/dispute/lib/Types.sol"; import "src/dispute/lib/Errors.sol"; +import { Types } from "src/libraries/Types.sol"; +import { Hashing } from "src/libraries/Hashing.sol"; contract SuperFaultDisputeGame_Solvency_Invariant is BaseSuperFaultDisputeGame_TestInit { - Claim internal constant ROOT_CLAIM = Claim.wrap(bytes32(uint256(10))); + Claim internal ROOT_CLAIM; Claim internal constant ABSOLUTE_PRESTATE = Claim.wrap(bytes32((uint256(3) << 248) | uint256(0))); RandomClaimActor internal actor; @@ -19,7 +21,17 @@ contract SuperFaultDisputeGame_Solvency_Invariant is BaseSuperFaultDisputeGame_T function setUp() public override { super.setUp(); - super.init({ _rootClaim: ROOT_CLAIM, _absolutePrestate: ABSOLUTE_PRESTATE, _l2SequenceNumber: 0x10 }); + + Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](2); + outputRoots[0] = Types.OutputRootWithChainId({ chainId: 5, root: keccak256(abi.encode(5)) }); + outputRoots[1] = Types.OutputRootWithChainId({ chainId: 6, root: keccak256(abi.encode(6)) }); + Types.SuperRootProof memory superRootProof; + superRootProof.version = bytes1(uint8(1)); + superRootProof.timestamp = uint64(0x10); + superRootProof.outputRoots = outputRoots; + ROOT_CLAIM = Claim.wrap(Hashing.hashSuperRootProof(superRootProof)); + + super.init({ _rootClaim: ROOT_CLAIM, _absolutePrestate: ABSOLUTE_PRESTATE, _super: superRootProof }); actor = new RandomClaimActor(IFaultDisputeGame(address(gameProxy)), vm); diff --git a/packages/contracts-bedrock/test/invariants/SystemConfig.t.sol b/packages/contracts-bedrock/test/invariants/SystemConfig.t.sol index 8dcd0da95a5..b10a10471ba 100644 --- a/packages/contracts-bedrock/test/invariants/SystemConfig.t.sol +++ b/packages/contracts-bedrock/test/invariants/SystemConfig.t.sol @@ -1,11 +1,18 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; + +// Scripts +import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; + +// Libraries +import { Constants } from "src/libraries/Constants.sol"; + +// Interfaces import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { IProxy } from "interfaces/universal/IProxy.sol"; -import { Constants } from "src/libraries/Constants.sol"; -import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; contract SystemConfig_GasLimitBoundaries_Invariant is Test { diff --git a/packages/contracts-bedrock/test/legacy/DeployerWhitelist.t.sol b/packages/contracts-bedrock/test/legacy/DeployerWhitelist.t.sol index 764e9ef8897..b148bcf3e8c 100644 --- a/packages/contracts-bedrock/test/legacy/DeployerWhitelist.t.sol +++ b/packages/contracts-bedrock/test/legacy/DeployerWhitelist.t.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Testing utilities -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; // Target contract import { IDeployerWhitelist } from "interfaces/legacy/IDeployerWhitelist.sol"; diff --git a/packages/contracts-bedrock/test/legacy/L1BlockNumber.t.sol b/packages/contracts-bedrock/test/legacy/L1BlockNumber.t.sol index d536ad006e1..2d215dd284c 100644 --- a/packages/contracts-bedrock/test/legacy/L1BlockNumber.t.sol +++ b/packages/contracts-bedrock/test/legacy/L1BlockNumber.t.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.15; // Testing -import { Test } from "forge-std/Test.sol"; +import { Test } from "test/setup/Test.sol"; // Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; diff --git a/packages/contracts-bedrock/test/legacy/L1ChugSplashProxy.t.sol b/packages/contracts-bedrock/test/legacy/L1ChugSplashProxy.t.sol index aebc7e30725..c81a0c888b3 100644 --- a/packages/contracts-bedrock/test/legacy/L1ChugSplashProxy.t.sol +++ b/packages/contracts-bedrock/test/legacy/L1ChugSplashProxy.t.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.15; -// Forge -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; import { VmSafe } from "forge-std/Vm.sol"; // Scripts diff --git a/packages/contracts-bedrock/test/legacy/ResolvedDelegateProxy.t.sol b/packages/contracts-bedrock/test/legacy/ResolvedDelegateProxy.t.sol index 9df666980ae..9c4a7bd6a67 100644 --- a/packages/contracts-bedrock/test/legacy/ResolvedDelegateProxy.t.sol +++ b/packages/contracts-bedrock/test/legacy/ResolvedDelegateProxy.t.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Testing utilities -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; // Target contract dependencies import { IAddressManager } from "interfaces/legacy/IAddressManager.sol"; diff --git a/packages/contracts-bedrock/test/libraries/Blueprint.t.sol b/packages/contracts-bedrock/test/libraries/Blueprint.t.sol index 433271f4cf3..ae03959dcb3 100644 --- a/packages/contracts-bedrock/test/libraries/Blueprint.t.sol +++ b/packages/contracts-bedrock/test/libraries/Blueprint.t.sol @@ -1,7 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; + +// Libraries import { Blueprint } from "src/libraries/Blueprint.sol"; /// @dev Used to test that constructor args are appended properly when deploying from a blueprint. diff --git a/packages/contracts-bedrock/test/libraries/Bytes.t.sol b/packages/contracts-bedrock/test/libraries/Bytes.t.sol index aa919f19a0b..3cd5b0e3551 100644 --- a/packages/contracts-bedrock/test/libraries/Bytes.t.sol +++ b/packages/contracts-bedrock/test/libraries/Bytes.t.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Testing utilities -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; -// Target contract +// Libraries import { Bytes } from "src/libraries/Bytes.sol"; contract Bytes_Harness { diff --git a/packages/contracts-bedrock/test/libraries/Constants.t.sol b/packages/contracts-bedrock/test/libraries/Constants.t.sol index b83029e8226..60bb52bd878 100644 --- a/packages/contracts-bedrock/test/libraries/Constants.t.sol +++ b/packages/contracts-bedrock/test/libraries/Constants.t.sol @@ -1,8 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; + +// Libraries import { Constants } from "src/libraries/Constants.sol"; + +// Interfaces import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; /// @title Constants_Test diff --git a/packages/contracts-bedrock/test/libraries/DeployUtils.t.sol b/packages/contracts-bedrock/test/libraries/DeployUtils.t.sol index 1b1af20cf53..4bee5fe1908 100644 --- a/packages/contracts-bedrock/test/libraries/DeployUtils.t.sol +++ b/packages/contracts-bedrock/test/libraries/DeployUtils.t.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Forge -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; // Libraries import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; diff --git a/packages/contracts-bedrock/test/libraries/DevFeatures.t.sol b/packages/contracts-bedrock/test/libraries/DevFeatures.t.sol index f4851b4afeb..31cf7d6a62a 100644 --- a/packages/contracts-bedrock/test/libraries/DevFeatures.t.sol +++ b/packages/contracts-bedrock/test/libraries/DevFeatures.t.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Testing utilities -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; -// Target contract +// Libraries import { DevFeatures } from "src/libraries/DevFeatures.sol"; contract DevFeatures_isDevFeatureEnabled_Test is Test { diff --git a/packages/contracts-bedrock/test/libraries/EOA.t.sol b/packages/contracts-bedrock/test/libraries/EOA.t.sol index 69a894c90a6..cc6712eb28e 100644 --- a/packages/contracts-bedrock/test/libraries/EOA.t.sol +++ b/packages/contracts-bedrock/test/libraries/EOA.t.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Forge -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; // Libraries import { EOA } from "src/libraries/EOA.sol"; diff --git a/packages/contracts-bedrock/test/libraries/Encoding.t.sol b/packages/contracts-bedrock/test/libraries/Encoding.t.sol index 0402e54ebbd..260844cefba 100644 --- a/packages/contracts-bedrock/test/libraries/Encoding.t.sol +++ b/packages/contracts-bedrock/test/libraries/Encoding.t.sol @@ -28,6 +28,10 @@ contract Encoding_Harness { function encodeSuperRootProof(Types.SuperRootProof memory proof) external pure returns (bytes memory) { return Encoding.encodeSuperRootProof(proof); } + + function decodeSuperRootProof(bytes memory _super) external pure returns (Types.SuperRootProof memory) { + return Encoding.decodeSuperRootProof(_super); + } } /// @title Encoding_TestInit @@ -303,6 +307,255 @@ contract Encoding_EncodeSuperRootProof_Test is Encoding_TestInit { } } +/// @title Encoding_DecodeSuperRootProof_Test +/// @notice Tests the `decodeSuperRootProof` function of the `Encoding` contract. +contract Encoding_DecodeSuperRootProof_Test is Encoding_TestInit { + /// @notice Tests successful decoding of a valid super root proof + /// @param _timestamp The timestamp of the super root proof + /// @param _length The number of output roots in the super root proof + /// @param _seed The seed used to generate the output roots + function testFuzz_decodeSuperRootProof_succeeds(uint64 _timestamp, uint256 _length, uint256 _seed) external pure { + // Ensure at least 1 element and cap at a reasonable maximum to avoid gas issues + _length = uint256(bound(_length, 1, 50)); + + // Create output roots array + Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](_length); + + // Generate deterministic chain IDs and roots based on the seed + for (uint256 i = 0; i < _length; i++) { + // Use different derivations of the seed for each value + uint256 chainId = uint256(keccak256(abi.encode(_seed, "chainId", i))); + bytes32 root = keccak256(abi.encode(_seed, "root", i)); + + outputRoots[i] = Types.OutputRootWithChainId({ chainId: chainId, root: root }); + } + + // Create the super root proof + Types.SuperRootProof memory proof = + Types.SuperRootProof({ version: 0x01, timestamp: _timestamp, outputRoots: outputRoots }); + + // Encode the proof + bytes memory encoded = Encoding.encodeSuperRootProof(proof); + + // Decode the proof + Types.SuperRootProof memory decoded = Encoding.decodeSuperRootProof(encoded); + + // Verify the decoded values match the original + assertEq(uint8(decoded.version), uint8(proof.version), "Version should match"); + assertEq(decoded.timestamp, proof.timestamp, "Timestamp should match"); + assertEq(decoded.outputRoots.length, proof.outputRoots.length, "Output roots length should match"); + + // Verify each output root + for (uint256 i = 0; i < _length; i++) { + assertEq(decoded.outputRoots[i].chainId, proof.outputRoots[i].chainId, "Chain ID should match"); + assertEq(decoded.outputRoots[i].root, proof.outputRoots[i].root, "Root should match"); + } + } + + /// @notice Tests decoding with a single output root + function test_decodeSuperRootProof_singleOutputRoot_succeeds() external pure { + // Create a single output root + Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](1); + outputRoots[0] = Types.OutputRootWithChainId({ chainId: 10, root: bytes32(uint256(0xdeadbeef)) }); + + // Create the super root proof + Types.SuperRootProof memory proof = + Types.SuperRootProof({ version: 0x01, timestamp: 1234567890, outputRoots: outputRoots }); + + // Encode the proof + bytes memory encoded = Encoding.encodeSuperRootProof(proof); + + // Decode the proof + Types.SuperRootProof memory decoded = Encoding.decodeSuperRootProof(encoded); + + // Verify the decoded values match the original + assertEq(uint8(decoded.version), 0x01, "Version should be 0x01"); + assertEq(decoded.timestamp, 1234567890, "Timestamp should match"); + assertEq(decoded.outputRoots.length, 1, "Should have one output root"); + assertEq(decoded.outputRoots[0].chainId, 10, "Chain ID should match"); + assertEq(decoded.outputRoots[0].root, bytes32(uint256(0xdeadbeef)), "Root should match"); + } + + /// @notice Tests decoding with multiple output roots + function test_decodeSuperRootProof_multipleOutputRoots_succeeds() external pure { + // Create multiple output roots + Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](3); + outputRoots[0] = Types.OutputRootWithChainId({ chainId: 10, root: bytes32(uint256(0xdeadbeef)) }); + outputRoots[1] = Types.OutputRootWithChainId({ chainId: 20, root: bytes32(uint256(0xbeefcafe)) }); + outputRoots[2] = Types.OutputRootWithChainId({ chainId: 30, root: bytes32(uint256(0xcafebabe)) }); + + // Create the super root proof + Types.SuperRootProof memory proof = + Types.SuperRootProof({ version: 0x01, timestamp: 1234567890, outputRoots: outputRoots }); + + // Encode the proof + bytes memory encoded = Encoding.encodeSuperRootProof(proof); + + // Decode the proof + Types.SuperRootProof memory decoded = Encoding.decodeSuperRootProof(encoded); + + // Verify the decoded values match the original + assertEq(uint8(decoded.version), 0x01, "Version should be 0x01"); + assertEq(decoded.timestamp, 1234567890, "Timestamp should match"); + assertEq(decoded.outputRoots.length, 3, "Should have three output roots"); + + // Verify each output root + assertEq(decoded.outputRoots[0].chainId, 10, "Chain ID 0 should match"); + assertEq(decoded.outputRoots[0].root, bytes32(uint256(0xdeadbeef)), "Root 0 should match"); + assertEq(decoded.outputRoots[1].chainId, 20, "Chain ID 1 should match"); + assertEq(decoded.outputRoots[1].root, bytes32(uint256(0xbeefcafe)), "Root 1 should match"); + assertEq(decoded.outputRoots[2].chainId, 30, "Chain ID 2 should match"); + assertEq(decoded.outputRoots[2].root, bytes32(uint256(0xcafebabe)), "Root 2 should match"); + } + + /// @notice Tests decoding with maximum timestamp value + function test_decodeSuperRootProof_maxTimestamp_succeeds() external pure { + // Create a single output root + Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](1); + outputRoots[0] = Types.OutputRootWithChainId({ chainId: 1, root: bytes32(uint256(1)) }); + + // Create the super root proof with max timestamp + Types.SuperRootProof memory proof = + Types.SuperRootProof({ version: 0x01, timestamp: type(uint64).max, outputRoots: outputRoots }); + + // Encode the proof + bytes memory encoded = Encoding.encodeSuperRootProof(proof); + + // Decode the proof + Types.SuperRootProof memory decoded = Encoding.decodeSuperRootProof(encoded); + + // Verify timestamp is preserved correctly + assertEq(decoded.timestamp, type(uint64).max, "Max timestamp should be preserved"); + } + + /// @notice Tests decoding with maximum chain ID values + function test_decodeSuperRootProof_maxChainId_succeeds() external pure { + // Create output roots with max chain ID + Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](2); + outputRoots[0] = Types.OutputRootWithChainId({ chainId: type(uint256).max, root: bytes32(uint256(1)) }); + outputRoots[1] = Types.OutputRootWithChainId({ chainId: type(uint256).max - 1, root: bytes32(uint256(2)) }); + + // Create the super root proof + Types.SuperRootProof memory proof = + Types.SuperRootProof({ version: 0x01, timestamp: 1234567890, outputRoots: outputRoots }); + + // Encode the proof + bytes memory encoded = Encoding.encodeSuperRootProof(proof); + + // Decode the proof + Types.SuperRootProof memory decoded = Encoding.decodeSuperRootProof(encoded); + + // Verify chain IDs are preserved correctly + assertEq(decoded.outputRoots[0].chainId, type(uint256).max, "Max chain ID should be preserved"); + assertEq(decoded.outputRoots[1].chainId, type(uint256).max - 1, "Max-1 chain ID should be preserved"); + } + + /// @notice Tests that decoding fails when version is not 0x01 + /// @param _version The version to use in the encoded bytes + /// @param _timestamp The timestamp to use in the encoded bytes + function testFuzz_decodeSuperRootProof_invalidVersion_reverts(bytes1 _version, uint64 _timestamp) external { + // Ensure version is not 0x01 + if (_version == 0x01) { + _version = 0x02; + } + + // Manually construct encoded bytes with invalid version + // Structure: 1 byte version + 8 bytes timestamp + (32 bytes chainId + 32 bytes root) * n + // Minimum valid structure needs at least one output root + bytes memory encoded = new bytes(73); // 1 + 8 + 64 + encoded[0] = _version; + + // Encode timestamp (8 bytes) + for (uint256 i = 0; i < 8; i++) { + encoded[1 + i] = bytes1(uint8(_timestamp >> (56 - i * 8))); + } + + // Add a dummy output root (chainId = 1, root = bytes32(uint256(1))) + // ChainId (32 bytes starting at byte 9) + encoded[40] = bytes1(uint8(1)); // Last byte of chainId at offset 9+31=40 + // Root (32 bytes starting at byte 41) + encoded[72] = bytes1(uint8(1)); // Last byte of root at offset 41+31=72 + + // Expect revert when decoding + vm.expectRevert(Encoding.Encoding_InvalidSuperRootVersion.selector); + encoding.decodeSuperRootProof(encoded); + } + + /// @notice Tests that decoding fails when version byte is 0x00 + function test_decodeSuperRootProof_versionZero_reverts() external { + // Create encoded bytes with version 0x00 + bytes memory encoded = new bytes(73); // 1 + 8 + 64 + encoded[0] = bytes1(0x00); + + // Add timestamp and dummy output root + encoded[40] = bytes1(uint8(1)); // ChainId last byte + encoded[72] = bytes1(uint8(1)); // Root last byte + + // Expect revert when decoding + vm.expectRevert(Encoding.Encoding_InvalidSuperRootVersion.selector); + encoding.decodeSuperRootProof(encoded); + } + + /// @notice Tests that decoding fails when version byte is 0xFF + function test_decodeSuperRootProof_versionFF_reverts() external { + // Create encoded bytes with version 0xFF + bytes memory encoded = new bytes(73); // 1 + 8 + 64 + encoded[0] = bytes1(0xFF); + + // Add timestamp and dummy output root + encoded[40] = bytes1(uint8(1)); // ChainId last byte + encoded[72] = bytes1(uint8(1)); // Root last byte + + // Expect revert when decoding + vm.expectRevert(Encoding.Encoding_InvalidSuperRootVersion.selector); + encoding.decodeSuperRootProof(encoded); + } + + /// @notice Tests that decoding fails when output roots array is empty + function testFuzz_decodeSuperRootProof_emptyOutputRoots_reverts(uint64 _timestamp) external { + // Manually construct encoded bytes with no output roots + // Structure: 1 byte version + 8 bytes timestamp + bytes memory encoded = new bytes(9); // 1 + 8 + encoded[0] = bytes1(0x01); + + // Encode timestamp (8 bytes) + for (uint256 i = 0; i < 8; i++) { + encoded[1 + i] = bytes1(uint8(_timestamp >> (56 - i * 8))); + } + + // Expect revert when decoding + vm.expectRevert(Encoding.Encoding_EmptySuperRoot.selector); + encoding.decodeSuperRootProof(encoded); + } + + /// @notice Tests that decoding fails when encoded bytes are incomplete + function testFuzz_decodeSuperRootProof_partial_reverts(uint256 _length) external { + _length = uint256(bound(_length, 0, 8)); + bytes memory encoded = new bytes(_length); + vm.expectRevert(Encoding.Encoding_InvalidSuperRootEncoding.selector); + encoding.decodeSuperRootProof(encoded); + } + + /// @notice Tests that decoding fails when output roots array is incomplete + function testFuzz_decodeSuperRootProof_partialOutputRoots_reverts(uint64 _timestamp, uint256 _length) external { + _length = uint256(bound(_length, 10, 72)); + + // Manually construct encoded bytes with no output roots + // Structure: 1 byte version + 8 bytes timestamp + bytes memory encoded = new bytes(_length); + encoded[0] = bytes1(0x01); + + // Encode timestamp (8 bytes) + for (uint256 i = 0; i < 8; i++) { + encoded[1 + i] = bytes1(uint8(_timestamp >> (56 - i * 8))); + } + + // Expect revert when decoding + vm.expectRevert(Encoding.Encoding_InvalidSuperRootEncoding.selector); + encoding.decodeSuperRootProof(encoded); + } +} + /// @title Encoding_Uncategorized_Test /// @notice General tests that are not testing any function directly of the `Encoding` contract or /// are testing multiple functions at once. diff --git a/packages/contracts-bedrock/test/libraries/GasPayingToken.t.sol b/packages/contracts-bedrock/test/libraries/GasPayingToken.t.sol index c20ade9a9d6..66f4d8adea0 100644 --- a/packages/contracts-bedrock/test/libraries/GasPayingToken.t.sol +++ b/packages/contracts-bedrock/test/libraries/GasPayingToken.t.sol @@ -1,10 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Target contract +// Testing +import { Test } from "test/setup/Test.sol"; + +// Libraries import { GasPayingToken } from "src/libraries/GasPayingToken.sol"; import { Constants } from "src/libraries/Constants.sol"; -import { Test } from "forge-std/Test.sol"; import { LibString } from "@solady/utils/LibString.sol"; contract GasPayingToken_Harness { diff --git a/packages/contracts-bedrock/test/libraries/SafeCall.t.sol b/packages/contracts-bedrock/test/libraries/SafeCall.t.sol index 084c601cbcb..3f6a3bdf255 100644 --- a/packages/contracts-bedrock/test/libraries/SafeCall.t.sol +++ b/packages/contracts-bedrock/test/libraries/SafeCall.t.sol @@ -1,14 +1,14 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Scripts -import { Config } from "scripts/libraries/Config.sol"; - -// Forge -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; import { VmSafe } from "forge-std/Vm.sol"; import { StdCheatsSafe } from "forge-std/StdCheats.sol"; +// Scripts +import { Config } from "scripts/libraries/Config.sol"; + // Libraries import { LibString } from "@solady/utils/LibString.sol"; import { SafeCall } from "src/libraries/SafeCall.sol"; diff --git a/packages/contracts-bedrock/test/libraries/SemverComp.t.sol b/packages/contracts-bedrock/test/libraries/SemverComp.t.sol index b182af378ff..45f2ace041c 100644 --- a/packages/contracts-bedrock/test/libraries/SemverComp.t.sol +++ b/packages/contracts-bedrock/test/libraries/SemverComp.t.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Forge -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; // Libraries import { JSONParserLib } from "solady/src/utils/JSONParserLib.sol"; diff --git a/packages/contracts-bedrock/test/libraries/StaticConfig.t.sol b/packages/contracts-bedrock/test/libraries/StaticConfig.t.sol index 00c8fe20957..47f51380b47 100644 --- a/packages/contracts-bedrock/test/libraries/StaticConfig.t.sol +++ b/packages/contracts-bedrock/test/libraries/StaticConfig.t.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Testing utilities -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; import { FFIInterface } from "test/setup/FFIInterface.sol"; -// Target contract +// Libraries import { StaticConfig } from "src/libraries/StaticConfig.sol"; /// @title StaticConfig_TestInit diff --git a/packages/contracts-bedrock/test/libraries/Storage.t.sol b/packages/contracts-bedrock/test/libraries/Storage.t.sol index 5a801c24afc..7ead6532477 100644 --- a/packages/contracts-bedrock/test/libraries/Storage.t.sol +++ b/packages/contracts-bedrock/test/libraries/Storage.t.sol @@ -1,9 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Target contract +// Testing +import { Test } from "test/setup/Test.sol"; + +// Contracts import { StorageSetter } from "src/universal/StorageSetter.sol"; -import { Test } from "forge-std/Test.sol"; /// @title Storage_TestInit /// @notice Reusable test initialization for `Storage` tests. diff --git a/packages/contracts-bedrock/test/libraries/TransientContext.t.sol b/packages/contracts-bedrock/test/libraries/TransientContext.t.sol index 99d6264e2ba..9d3511cf023 100644 --- a/packages/contracts-bedrock/test/libraries/TransientContext.t.sol +++ b/packages/contracts-bedrock/test/libraries/TransientContext.t.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -// Testing utilities -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; -// Target contractS +// Target contracts import { TransientContext } from "src/libraries/TransientContext.sol"; import { TransientReentrancyAware } from "src/libraries/TransientContext.sol"; diff --git a/packages/contracts-bedrock/test/libraries/rlp/RLPReader.t.sol b/packages/contracts-bedrock/test/libraries/rlp/RLPReader.t.sol index e4ba42414a9..7e6f752950c 100644 --- a/packages/contracts-bedrock/test/libraries/rlp/RLPReader.t.sol +++ b/packages/contracts-bedrock/test/libraries/rlp/RLPReader.t.sol @@ -1,8 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { stdError } from "forge-std/Test.sol"; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; +import { stdError } from "forge-std/StdError.sol"; + +// Libraries import { RLPReader } from "src/libraries/rlp/RLPReader.sol"; import "src/libraries/rlp/RLPErrors.sol"; diff --git a/packages/contracts-bedrock/test/libraries/rlp/RLPWriter.t.sol b/packages/contracts-bedrock/test/libraries/rlp/RLPWriter.t.sol index 7ff838845be..a143d4e68d4 100644 --- a/packages/contracts-bedrock/test/libraries/rlp/RLPWriter.t.sol +++ b/packages/contracts-bedrock/test/libraries/rlp/RLPWriter.t.sol @@ -1,8 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; +// Testing +import { Test } from "test/setup/Test.sol"; + +// Libraries import { RLPWriter } from "src/libraries/rlp/RLPWriter.sol"; -import { Test } from "forge-std/Test.sol"; /// @title RLPWriter_writeString_Test /// @notice Tests the `writeString` function of the `RLPWriter` library. diff --git a/packages/contracts-bedrock/test/libraries/trie/MerkleTrie.t.sol b/packages/contracts-bedrock/test/libraries/trie/MerkleTrie.t.sol index d746933990d..51e0cb1ae9a 100644 --- a/packages/contracts-bedrock/test/libraries/trie/MerkleTrie.t.sol +++ b/packages/contracts-bedrock/test/libraries/trie/MerkleTrie.t.sol @@ -1,10 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; +import { FFIInterface } from "test/setup/FFIInterface.sol"; + +// Libraries import { MerkleTrie } from "src/libraries/trie/MerkleTrie.sol"; import { RLPReader } from "src/libraries/rlp/RLPReader.sol"; -import { FFIInterface } from "test/setup/FFIInterface.sol"; import "src/libraries/rlp/RLPErrors.sol"; contract MerkleTrie_Harness { diff --git a/packages/contracts-bedrock/test/opcm/DeployAlphabetVM.t.sol b/packages/contracts-bedrock/test/opcm/DeployAlphabetVM.t.sol index 64f9f380dc4..bc87618e0ee 100644 --- a/packages/contracts-bedrock/test/opcm/DeployAlphabetVM.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployAlphabetVM.t.sol @@ -1,13 +1,15 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; + +// Scripts +import { DeployAlphabetVM } from "scripts/deploy/DeployAlphabetVM.s.sol"; // Interfaces import { IPreimageOracle } from "interfaces/cannon/IPreimageOracle.sol"; -import { DeployAlphabetVM } from "scripts/deploy/DeployAlphabetVM.s.sol"; - contract DeployAlphabetVM2_Test is Test { DeployAlphabetVM deployAlphanetVM; diff --git a/packages/contracts-bedrock/test/opcm/DeployAltDA.t.sol b/packages/contracts-bedrock/test/opcm/DeployAltDA.t.sol index 312e82d57e1..39828f6691d 100644 --- a/packages/contracts-bedrock/test/opcm/DeployAltDA.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployAltDA.t.sol @@ -1,13 +1,17 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; +// Scripts import { DeployAltDA } from "scripts/deploy/DeployAltDA.s.sol"; +import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; + +// Interfaces import { IDataAvailabilityChallenge } from "interfaces/L1/IDataAvailabilityChallenge.sol"; import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; import { IProxy } from "interfaces/universal/IProxy.sol"; -import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; contract DeployAltDA_Test is Test { DeployAltDA deployAltDA; diff --git a/packages/contracts-bedrock/test/opcm/DeployAsterisc.t.sol b/packages/contracts-bedrock/test/opcm/DeployAsterisc.t.sol index f2bd971e646..4abe6967de7 100644 --- a/packages/contracts-bedrock/test/opcm/DeployAsterisc.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployAsterisc.t.sol @@ -1,15 +1,16 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; +// Scripts import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; +import { DeployAsterisc } from "scripts/deploy/DeployAsterisc.s.sol"; // Interfaces import { IPreimageOracle } from "interfaces/cannon/IPreimageOracle.sol"; -import { DeployAsterisc } from "scripts/deploy/DeployAsterisc.s.sol"; - contract DeployAsterisc_Test is Test { DeployAsterisc deployAsterisc; diff --git a/packages/contracts-bedrock/test/opcm/DeployDisputeGame.t.sol b/packages/contracts-bedrock/test/opcm/DeployDisputeGame.t.sol index 6931a6e13c2..f71c30d88dd 100644 --- a/packages/contracts-bedrock/test/opcm/DeployDisputeGame.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployDisputeGame.t.sol @@ -1,7 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; + +// Libraries +import { LibPosition } from "src/dispute/lib/LibPosition.sol"; +import { GameType } from "src/dispute/lib/Types.sol"; +import { LibString } from "@solady/utils/LibString.sol"; // Interfaces import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; @@ -14,6 +20,7 @@ import { LibPosition } from "src/dispute/lib/LibPosition.sol"; import { GameType } from "src/dispute/lib/Types.sol"; import { LibString } from "@solady/utils/LibString.sol"; +// Contracts import { PreimageOracle } from "src/cannon/PreimageOracle.sol"; import { DeployDisputeGame } from "scripts/deploy/DeployDisputeGame.s.sol"; diff --git a/packages/contracts-bedrock/test/opcm/DeployFeesDepositor.t.sol b/packages/contracts-bedrock/test/opcm/DeployFeesDepositor.t.sol index e8d934427be..d0a277673fa 100644 --- a/packages/contracts-bedrock/test/opcm/DeployFeesDepositor.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployFeesDepositor.t.sol @@ -1,18 +1,22 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; +import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; + +// Scripts +import { DeployFeesDepositor } from "scripts/deploy/DeployFeesDepositor.s.sol"; + +// Contracts +import { FeesDepositor } from "src/L1/FeesDepositor.sol"; +import { Proxy } from "src/universal/Proxy.sol"; // Interfaces import { IFeesDepositor } from "interfaces/L1/IFeesDepositor.sol"; import { IL1CrossDomainMessenger } from "interfaces/L1/IL1CrossDomainMessenger.sol"; import { IProxy } from "interfaces/universal/IProxy.sol"; -import { DeployFeesDepositor } from "scripts/deploy/DeployFeesDepositor.s.sol"; -import { FeesDepositor } from "src/L1/FeesDepositor.sol"; -import { Proxy } from "src/universal/Proxy.sol"; -import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; - /// @title DeployFeesDepositor_Test /// @notice This test is used to test the DeployFeesDepositor script. contract DeployFeesDepositor_Test is Test { diff --git a/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol b/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol index 3136aaeb0c2..4b58a66ede8 100644 --- a/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol @@ -2,7 +2,8 @@ pragma solidity 0.8.15; // Testing -import { Test, stdStorage, StdStorage } from "forge-std/Test.sol"; +import { Test } from "test/setup/Test.sol"; +import { stdStorage, StdStorage } from "forge-std/StdStorage.sol"; import "../setup/FeatureFlags.sol"; // Libraries @@ -245,6 +246,9 @@ contract DeployImplementations_Test is Test, FeatureFlags { DeployImplementations.Output memory output = deployImplementations.run(input); + // Check which OPCM version is deployed + bool opcmV2Enabled = DevFeatures.isDevFeatureEnabled(_devFeatureBitmap, DevFeatures.OPCM_V2); + // Basic assertions assertNotEq(address(output.anchorStateRegistryImpl), address(0), "100"); assertNotEq(address(output.delayedWETHImpl), address(0), "200"); @@ -254,10 +258,26 @@ contract DeployImplementations_Test is Test, FeatureFlags { assertNotEq(address(output.l1ERC721BridgeImpl), address(0), "500"); assertNotEq(address(output.l1StandardBridgeImpl), address(0), "600"); assertNotEq(address(output.mipsSingleton), address(0), "700"); - assertNotEq(address(output.opcm), address(0), "800"); - assertNotEq(address(output.opcmContractsContainer), address(0), "900"); - assertNotEq(address(output.opcmDeployer), address(0), "1000"); - assertNotEq(address(output.opcmGameTypeAdder), address(0), "1100"); + + // OPCM version-specific assertions + if (opcmV2Enabled) { + assertNotEq(address(output.opcmV2), address(0), "800"); + assertNotEq(address(output.opcmContainer), address(0), "900"); + assertNotEq(address(output.opcmStandardValidator), address(0), "1000"); + // V1 contracts should be null when V2 is enabled + assertEq(address(output.opcm), address(0), "800-v1"); + assertEq(address(output.opcmContractsContainer), address(0), "900-v1"); + assertEq(address(output.opcmDeployer), address(0), "1000-v1"); + assertEq(address(output.opcmGameTypeAdder), address(0), "1100-v1"); + } else { + assertNotEq(address(output.opcm), address(0), "800"); + assertNotEq(address(output.opcmContractsContainer), address(0), "900"); + assertNotEq(address(output.opcmDeployer), address(0), "1000"); + assertNotEq(address(output.opcmGameTypeAdder), address(0), "1100"); + // V2 contracts should be null when V1 is enabled + assertEq(address(output.opcmV2), address(0), "800-v2"); + assertEq(address(output.opcmContainer), address(0), "900-v2"); + } assertNotEq(address(output.faultDisputeGameV2Impl), address(0), "V2 should be deployed when enabled"); assertNotEq(address(output.permissionedDisputeGameV2Impl), address(0), "V2 should be deployed when enabled"); @@ -351,10 +371,26 @@ contract DeployImplementations_Test is Test, FeatureFlags { assertNotEq(address(output.l1ERC721BridgeImpl).code, empty, "1700"); assertNotEq(address(output.l1StandardBridgeImpl).code, empty, "1800"); assertNotEq(address(output.mipsSingleton).code, empty, "1900"); - assertNotEq(address(output.opcm).code, empty, "2000"); - assertNotEq(address(output.opcmContractsContainer).code, empty, "2100"); - assertNotEq(address(output.opcmDeployer).code, empty, "2200"); - assertNotEq(address(output.opcmGameTypeAdder).code, empty, "2300"); + + // OPCM version-specific code assertions + if (opcmV2Enabled) { + assertNotEq(address(output.opcmV2).code, empty, "2000"); + assertNotEq(address(output.opcmContainer).code, empty, "2100"); + assertNotEq(address(output.opcmStandardValidator).code, empty, "2200"); + // V1 contracts should be empty when V2 is enabled + assertEq(address(output.opcm).code, empty, "2000-v1"); + assertEq(address(output.opcmContractsContainer).code, empty, "2100-v1"); + assertEq(address(output.opcmDeployer).code, empty, "2200-v1"); + assertEq(address(output.opcmGameTypeAdder).code, empty, "2300-v1"); + } else { + assertNotEq(address(output.opcm).code, empty, "2000"); + assertNotEq(address(output.opcmContractsContainer).code, empty, "2100"); + assertNotEq(address(output.opcmDeployer).code, empty, "2200"); + assertNotEq(address(output.opcmGameTypeAdder).code, empty, "2300"); + // V2 contracts should be empty when V1 is enabled + assertEq(address(output.opcmV2).code, empty, "2000-v2"); + assertEq(address(output.opcmContainer).code, empty, "2100-v2"); + } assertNotEq(address(output.faultDisputeGameV2Impl).code, empty, "V2 FDG should have code when enabled"); assertNotEq(address(output.permissionedDisputeGameV2Impl).code, empty, "V2 PDG should have code when enabled"); diff --git a/packages/contracts-bedrock/test/opcm/DeployMIPS2.t.sol b/packages/contracts-bedrock/test/opcm/DeployMIPS2.t.sol index 71f6de0f2dd..e1a61fd2b2a 100644 --- a/packages/contracts-bedrock/test/opcm/DeployMIPS2.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployMIPS2.t.sol @@ -1,15 +1,19 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; - -// Interfaces -import { IPreimageOracle } from "interfaces/cannon/IPreimageOracle.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; +// Scripts import { DeployMIPS2 } from "scripts/deploy/DeployMIPS2.s.sol"; -import { MIPS64 } from "src/cannon/MIPS64.sol"; import { StandardConstants } from "scripts/deploy/StandardConstants.sol"; +// Contracts +import { MIPS64 } from "src/cannon/MIPS64.sol"; + +// Interfaces +import { IPreimageOracle } from "interfaces/cannon/IPreimageOracle.sol"; + contract DeployMIPS2_Test is Test { DeployMIPS2 deployMIPS; diff --git a/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol index 53defc933f1..d5a3b3d7507 100644 --- a/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol @@ -1,19 +1,26 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; import { FeatureFlags } from "test/setup/FeatureFlags.sol"; -import { Features } from "src/libraries/Features.sol"; +import { DevFeatures } from "src/libraries/DevFeatures.sol"; +// Scripts import { DeploySuperchain } from "scripts/deploy/DeploySuperchain.s.sol"; import { DeployImplementations } from "scripts/deploy/DeployImplementations.s.sol"; import { DeployOPChain } from "scripts/deploy/DeployOPChain.s.sol"; import { StandardConstants } from "scripts/deploy/StandardConstants.sol"; import { Types } from "scripts/libraries/Types.sol"; +// Libraries +import { Features } from "src/libraries/Features.sol"; + +// Interfaces import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; import { Claim, Duration, GameType, GameTypes } from "src/dispute/lib/Types.sol"; import { IPermissionedDisputeGame } from "interfaces/dispute/IPermissionedDisputeGame.sol"; +import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; contract DeployOPChain_TestBase is Test, FeatureFlags { DeploySuperchain deploySuperchain; @@ -57,7 +64,8 @@ contract DeployOPChain_TestBase is Test, FeatureFlags { uint256 disputeSplitDepth = 30; Duration disputeClockExtension = Duration.wrap(3 hours); Duration disputeMaxClockDuration = Duration.wrap(3.5 days); - IOPContractsManager opcm; + address opcmAddr; + ISuperchainConfig superchainConfig; bool useCustomGasToken = false; event Deployed(uint256 indexed l2ChainId, address indexed deployer, bytes deployOutput); @@ -101,8 +109,13 @@ contract DeployOPChain_TestBase is Test, FeatureFlags { devFeatureBitmap: devFeatureBitmap }) ); - opcm = dio.opcm; - vm.label(address(opcm), "opcm"); + // Select OPCM v1 or v2 based on feature flag + opcmAddr = isDevFeatureEnabled(DevFeatures.OPCM_V2) ? address(dio.opcmV2) : address(dio.opcm); + vm.label(address(dio.opcm), "opcm"); + vm.label(address(dio.opcmV2), "opcmV2"); + + // Set superchainConfig from deployment + superchainConfig = dso.superchainConfigProxy; // 3) Build DeployOPChainInput struct deployOPChainInput = Types.DeployOPChainInput({ @@ -115,7 +128,7 @@ contract DeployOPChain_TestBase is Test, FeatureFlags { basefeeScalar: basefeeScalar, blobBaseFeeScalar: blobBaseFeeScalar, l2ChainId: l2ChainId, - opcm: address(opcm), + opcm: opcmAddr, saltMixer: saltMixer, gasLimit: gasLimit, disputeGameType: disputeGameType, @@ -127,6 +140,7 @@ contract DeployOPChain_TestBase is Test, FeatureFlags { allowCustomDisputeParameters: false, operatorFeeScalar: 0, operatorFeeConstant: 0, + superchainConfig: superchainConfig, useCustomGasToken: useCustomGasToken }); } @@ -162,6 +176,34 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { useCustomGasToken, "SystemConfig CUSTOM_GAS_TOKEN feature" ); + + // Verify superchainConfig is set correctly + assertEq( + address(doo.systemConfigProxy.superchainConfig()), + address(deployOPChainInput.superchainConfig), + "superchainConfig mismatch" + ); + + // OPCM v2 specific assertions + if (isDevFeatureEnabled(DevFeatures.OPCM_V2)) { + // PERMISSIONED_CANNON must always be enabled with 0.08 ether init bond + assertEq(doo.disputeGameFactoryProxy.initBonds(GameTypes.PERMISSIONED_CANNON), 0.08 ether); + assertNotEq(address(doo.disputeGameFactoryProxy.gameImpls(GameTypes.PERMISSIONED_CANNON)), address(0)); + + // CANNON is only enabled if it's the starting game type + bool cannonEnabled = deployOPChainInput.disputeGameType.raw() == GameTypes.CANNON.raw(); + assertEq(doo.disputeGameFactoryProxy.initBonds(GameTypes.CANNON), cannonEnabled ? 0.08 ether : 0); + if (cannonEnabled) { + assertNotEq(address(doo.disputeGameFactoryProxy.gameImpls(GameTypes.CANNON)), address(0)); + } + + // CANNON_KONA is only enabled if it's the starting game type + bool cannonKonaEnabled = deployOPChainInput.disputeGameType.raw() == GameTypes.CANNON_KONA.raw(); + assertEq(doo.disputeGameFactoryProxy.initBonds(GameTypes.CANNON_KONA), cannonKonaEnabled ? 0.08 ether : 0); + if (cannonKonaEnabled) { + assertNotEq(address(doo.disputeGameFactoryProxy.gameImpls(GameTypes.CANNON_KONA)), address(0)); + } + } } function testFuzz_run_memory_succeeds(bytes32 _seed) public { @@ -178,27 +220,33 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { DeployOPChain.Output memory doo = deployOPChain.run(deployOPChainInput); - // Verify that the initial bonds are zero. - assertEq(doo.disputeGameFactoryProxy.initBonds(GameTypes.CANNON), 0, "2700"); - assertEq(doo.disputeGameFactoryProxy.initBonds(GameTypes.PERMISSIONED_CANNON), 0, "2800"); + // Skip init bond checks for OPCM v2 (bonds are set during deployment, not zero) + if (!isDevFeatureEnabled(DevFeatures.OPCM_V2)) { + // Verify that the initial bonds are zero for OPCM v1. + assertEq(doo.disputeGameFactoryProxy.initBonds(GameTypes.CANNON), 0, "2700"); + assertEq(doo.disputeGameFactoryProxy.initBonds(GameTypes.PERMISSIONED_CANNON), 0, "2800"); + } // Check dispute game deployments // Validate permissionedDisputeGame (PDG) address - IOPContractsManager.Implementations memory impls = opcm.implementations(); + IOPContractsManager.Implementations memory impls = IOPContractsManager(opcmAddr).implementations(); address expectedPDGAddress = impls.permissionedDisputeGameV2Impl; address actualPDGAddress = address(doo.disputeGameFactoryProxy.gameImpls(GameTypes.PERMISSIONED_CANNON)); assertNotEq(actualPDGAddress, address(0), "PDG address should be non-zero"); assertEq(actualPDGAddress, expectedPDGAddress, "PDG address should match expected address"); - // Check PDG getters - IPermissionedDisputeGame pdg = IPermissionedDisputeGame(actualPDGAddress); - bytes32 expectedPrestate = bytes32(0); - assertEq(pdg.l2BlockNumber(), 0, "3000"); - assertEq(Claim.unwrap(pdg.absolutePrestate()), expectedPrestate, "3100"); - assertEq(Duration.unwrap(pdg.clockExtension()), 10800, "3200"); - assertEq(Duration.unwrap(pdg.maxClockDuration()), 302400, "3300"); - assertEq(pdg.splitDepth(), 30, "3400"); - assertEq(pdg.maxGameDepth(), 73, "3500"); + // Skip PDG getter checks for OPCM v2 (game args are passed at creation time) + if (!isDevFeatureEnabled(DevFeatures.OPCM_V2)) { + // Check PDG getters + IPermissionedDisputeGame pdg = IPermissionedDisputeGame(actualPDGAddress); + bytes32 expectedPrestate = bytes32(0); + assertEq(pdg.l2BlockNumber(), 0, "3000"); + assertEq(Claim.unwrap(pdg.absolutePrestate()), expectedPrestate, "3100"); + assertEq(Duration.unwrap(pdg.clockExtension()), 10800, "3200"); + assertEq(Duration.unwrap(pdg.maxClockDuration()), 302400, "3300"); + assertEq(pdg.splitDepth(), 30, "3400"); + assertEq(pdg.maxGameDepth(), 73, "3500"); + } // Verify custom gas token feature is set as seeded assertEq( diff --git a/packages/contracts-bedrock/test/opcm/DeploySuperchain.t.sol b/packages/contracts-bedrock/test/opcm/DeploySuperchain.t.sol index ae52362446b..3c8983c7b63 100644 --- a/packages/contracts-bedrock/test/opcm/DeploySuperchain.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeploySuperchain.t.sol @@ -1,11 +1,17 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; +// Scripts +import { DeploySuperchain } from "scripts/deploy/DeploySuperchain.s.sol"; + +// Contracts import { Proxy } from "src/universal/Proxy.sol"; + +// Interfaces import { ProtocolVersion } from "interfaces/L1/IProtocolVersions.sol"; -import { DeploySuperchain } from "scripts/deploy/DeploySuperchain.s.sol"; contract DeploySuperchain_Test is Test { DeploySuperchain deploySuperchain; diff --git a/packages/contracts-bedrock/test/opcm/InteropMigration.t.sol b/packages/contracts-bedrock/test/opcm/InteropMigration.t.sol index 27c71d4329c..a98f8731ed6 100644 --- a/packages/contracts-bedrock/test/opcm/InteropMigration.t.sol +++ b/packages/contracts-bedrock/test/opcm/InteropMigration.t.sol @@ -1,13 +1,19 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; +// Scripts import { InteropMigrationInput, InteropMigration, InteropMigrationOutput } from "scripts/deploy/InteropMigration.s.sol"; + +// Libraries +import { Claim } from "src/dispute/lib/Types.sol"; + +// Interfaces import { IOPContractsManagerInteropMigrator, IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; import { IOptimismPortal2 as IOptimismPortal } from "interfaces/L1/IOptimismPortal2.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; -import { Claim } from "src/dispute/lib/Types.sol"; contract InteropMigrationInput_Test is Test { InteropMigrationInput input; diff --git a/packages/contracts-bedrock/test/opcm/SetDisputeGameImpl.t.sol b/packages/contracts-bedrock/test/opcm/SetDisputeGameImpl.t.sol index ad266dccd79..d0a86a4c6a1 100644 --- a/packages/contracts-bedrock/test/opcm/SetDisputeGameImpl.t.sol +++ b/packages/contracts-bedrock/test/opcm/SetDisputeGameImpl.t.sol @@ -1,16 +1,25 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import { Test } from "forge-std/Test.sol"; -import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; -import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; -import { GameType, Proposal, Hash } from "src/dispute/lib/Types.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; + +// Scripts import { SetDisputeGameImpl, SetDisputeGameImplInput } from "scripts/deploy/SetDisputeGameImpl.s.sol"; + +// Contracts import { DisputeGameFactory } from "src/dispute/DisputeGameFactory.sol"; import { Proxy } from "src/universal/Proxy.sol"; import { SuperchainConfig } from "src/L1/SuperchainConfig.sol"; import { AnchorStateRegistry } from "src/dispute/AnchorStateRegistry.sol"; import { SystemConfig } from "src/L1/SystemConfig.sol"; + +// Libraries +import { GameType, Proposal, Hash } from "src/dispute/lib/Types.sol"; + +// Interfaces +import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; +import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; diff --git a/packages/contracts-bedrock/test/opcm/UpgradeOPChain.t.sol b/packages/contracts-bedrock/test/opcm/UpgradeOPChain.t.sol index 60f2226372b..35accd9af1c 100644 --- a/packages/contracts-bedrock/test/opcm/UpgradeOPChain.t.sol +++ b/packages/contracts-bedrock/test/opcm/UpgradeOPChain.t.sol @@ -1,7 +1,16 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; + +// Scripts +import { UpgradeOPChain, UpgradeOPChainInput } from "scripts/deploy/UpgradeOPChain.s.sol"; + +// Contracts +import { OPContractsManager } from "src/L1/OPContractsManager.sol"; +import { OPContractsManagerV2 } from "src/L1/opcm/OPContractsManagerV2.sol"; +import { UpgradeOPChain, UpgradeOPChainInput } from "scripts/deploy/UpgradeOPChain.s.sol"; // Libraries import { Claim } from "src/dispute/lib/Types.sol"; @@ -12,11 +21,6 @@ import { DevFeatures } from "src/libraries/DevFeatures.sol"; import { IOPContractsManagerUtils } from "interfaces/L1/opcm/IOPContractsManagerUtils.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; -// Contracts -import { OPContractsManager } from "src/L1/OPContractsManager.sol"; -import { OPContractsManagerV2 } from "src/L1/opcm/OPContractsManagerV2.sol"; -import { UpgradeOPChain, UpgradeOPChainInput } from "scripts/deploy/UpgradeOPChain.s.sol"; - contract UpgradeOPChainInput_Test is Test { UpgradeOPChainInput input; diff --git a/packages/contracts-bedrock/test/opcm/UpgradeSuperchainConfig.t.sol b/packages/contracts-bedrock/test/opcm/UpgradeSuperchainConfig.t.sol index e3c36525af5..a8795476a11 100644 --- a/packages/contracts-bedrock/test/opcm/UpgradeSuperchainConfig.t.sol +++ b/packages/contracts-bedrock/test/opcm/UpgradeSuperchainConfig.t.sol @@ -1,11 +1,15 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; - -import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; +// Scripts import { UpgradeSuperchainConfig } from "scripts/deploy/UpgradeSuperchainConfig.s.sol"; + +// Interfaces +import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; +import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; import { IOPContractsManagerV2 } from "interfaces/L1/opcm/IOPContractsManagerV2.sol"; import { IOPContractsManagerUtils } from "interfaces/L1/opcm/IOPContractsManagerUtils.sol"; diff --git a/packages/contracts-bedrock/test/periphery/AssetReceiver.t.sol b/packages/contracts-bedrock/test/periphery/AssetReceiver.t.sol index 3e493984df1..45e7aa87dc5 100644 --- a/packages/contracts-bedrock/test/periphery/AssetReceiver.t.sol +++ b/packages/contracts-bedrock/test/periphery/AssetReceiver.t.sol @@ -1,10 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Testing utilities -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; import { TestERC20 } from "test/mocks/TestERC20.sol"; import { TestERC721 } from "test/mocks/TestERC721.sol"; + +// Contracts import { AssetReceiver } from "src/periphery/AssetReceiver.sol"; /// @title AssetReceiver_TestInit diff --git a/packages/contracts-bedrock/test/periphery/Transactor.t.sol b/packages/contracts-bedrock/test/periphery/Transactor.t.sol index e62a484e648..a05bc2f6a53 100644 --- a/packages/contracts-bedrock/test/periphery/Transactor.t.sol +++ b/packages/contracts-bedrock/test/periphery/Transactor.t.sol @@ -1,8 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Testing utilities -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; + +// Contracts import { Transactor } from "src/periphery/Transactor.sol"; /// @title Transactor_TestInit diff --git a/packages/contracts-bedrock/test/periphery/TransferOnion.t.sol b/packages/contracts-bedrock/test/periphery/TransferOnion.t.sol index 2d4261cc93d..fcc1b9406b3 100644 --- a/packages/contracts-bedrock/test/periphery/TransferOnion.t.sol +++ b/packages/contracts-bedrock/test/periphery/TransferOnion.t.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Testing utilities -import { Test } from "forge-std/Test.sol"; -import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; -// Target contract +// Contracts +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { TransferOnion } from "src/periphery/TransferOnion.sol"; /// @title TransferOnion_TestInit diff --git a/packages/contracts-bedrock/test/periphery/drippie/Drippie.t.sol b/packages/contracts-bedrock/test/periphery/drippie/Drippie.t.sol index e230f705902..e1869ef176d 100644 --- a/packages/contracts-bedrock/test/periphery/drippie/Drippie.t.sol +++ b/packages/contracts-bedrock/test/periphery/drippie/Drippie.t.sol @@ -1,11 +1,16 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; +import { SimpleStorage } from "test/mocks/SimpleStorage.sol"; + +// Contracts import { Drippie } from "src/periphery/drippie/Drippie.sol"; -import { IDripCheck } from "src/periphery/drippie/IDripCheck.sol"; import { CheckTrue } from "src/periphery/drippie/dripchecks/CheckTrue.sol"; -import { SimpleStorage } from "test/mocks/SimpleStorage.sol"; + +// Interfaces +import { IDripCheck } from "src/periphery/drippie/IDripCheck.sol"; /// @title TestDrippie /// @notice This is a wrapper contract around Drippie used for testing. Returning an entire diff --git a/packages/contracts-bedrock/test/periphery/drippie/dripchecks/CheckBalanceLow.t.sol b/packages/contracts-bedrock/test/periphery/drippie/dripchecks/CheckBalanceLow.t.sol index bfc051c9eac..980c46dc044 100644 --- a/packages/contracts-bedrock/test/periphery/drippie/dripchecks/CheckBalanceLow.t.sol +++ b/packages/contracts-bedrock/test/periphery/drippie/dripchecks/CheckBalanceLow.t.sol @@ -1,7 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; + +// Contracts import { CheckBalanceLow } from "src/periphery/drippie/dripchecks/CheckBalanceLow.sol"; /// @title CheckBalanceLow_TestInit diff --git a/packages/contracts-bedrock/test/periphery/drippie/dripchecks/CheckSecrets.t.sol b/packages/contracts-bedrock/test/periphery/drippie/dripchecks/CheckSecrets.t.sol index 9da99bfcd97..891c6d874d5 100644 --- a/packages/contracts-bedrock/test/periphery/drippie/dripchecks/CheckSecrets.t.sol +++ b/packages/contracts-bedrock/test/periphery/drippie/dripchecks/CheckSecrets.t.sol @@ -1,7 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; + +// Contracts import { CheckSecrets } from "src/periphery/drippie/dripchecks/CheckSecrets.sol"; /// @title CheckSecrets_TestInit diff --git a/packages/contracts-bedrock/test/periphery/drippie/dripchecks/CheckTrue.t.sol b/packages/contracts-bedrock/test/periphery/drippie/dripchecks/CheckTrue.t.sol index 4fc04862b87..ef6dccdfb5a 100644 --- a/packages/contracts-bedrock/test/periphery/drippie/dripchecks/CheckTrue.t.sol +++ b/packages/contracts-bedrock/test/periphery/drippie/dripchecks/CheckTrue.t.sol @@ -1,7 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; + +// Contracts import { CheckTrue } from "src/periphery/drippie/dripchecks/CheckTrue.sol"; /// @title CheckTrue_TestInit diff --git a/packages/contracts-bedrock/test/periphery/faucet/Faucet.t.sol b/packages/contracts-bedrock/test/periphery/faucet/Faucet.t.sol index 98c97df6556..7d663af9c1c 100644 --- a/packages/contracts-bedrock/test/periphery/faucet/Faucet.t.sol +++ b/packages/contracts-bedrock/test/periphery/faucet/Faucet.t.sol @@ -1,10 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; +import { FaucetHelper } from "test/mocks/FaucetHelper.sol"; + +// Contracts import { Faucet } from "src/periphery/faucet/Faucet.sol"; import { AdminFaucetAuthModule } from "src/periphery/faucet/authmodules/AdminFaucetAuthModule.sol"; -import { FaucetHelper } from "test/mocks/FaucetHelper.sol"; /// @title Faucet_TestInit /// @notice Reusable test initialization for `Faucet` tests. diff --git a/packages/contracts-bedrock/test/periphery/faucet/authmodules/AdminFaucetAuthModule.t.sol b/packages/contracts-bedrock/test/periphery/faucet/authmodules/AdminFaucetAuthModule.t.sol index 2bc5e7b4d56..57ca1937a72 100644 --- a/packages/contracts-bedrock/test/periphery/faucet/authmodules/AdminFaucetAuthModule.t.sol +++ b/packages/contracts-bedrock/test/periphery/faucet/authmodules/AdminFaucetAuthModule.t.sol @@ -1,10 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; +import { FaucetHelper } from "test/mocks/FaucetHelper.sol"; + +// Contracts import { AdminFaucetAuthModule } from "src/periphery/faucet/authmodules/AdminFaucetAuthModule.sol"; import { Faucet } from "src/periphery/faucet/Faucet.sol"; -import { FaucetHelper } from "test/mocks/FaucetHelper.sol"; /// @title AdminFaucetAuthModule_TestInit /// @notice Reusable test initialization for `AdminFaucetAuthModule` tests. diff --git a/packages/contracts-bedrock/test/safe-tools/SafeTestTools.sol b/packages/contracts-bedrock/test/safe-tools/SafeTestTools.sol index 15457e8aefc..35b0ef9ee8e 100644 --- a/packages/contracts-bedrock/test/safe-tools/SafeTestTools.sol +++ b/packages/contracts-bedrock/test/safe-tools/SafeTestTools.sol @@ -1,16 +1,24 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.7.0 <0.9.0; -import "forge-std/Test.sol"; -import { LibSort } from "@solady/utils/LibSort.sol"; +// Forge +import { console2 as console } from "forge-std/console2.sol"; +import { Vm } from "forge-std/Vm.sol"; + +// Testing +import "./CompatibilityFallbackHandler_1_3_0.sol"; + +// Contracts import { Safe as GnosisSafe } from "safe-contracts/Safe.sol"; +import { SafeProxyFactory as GnosisSafeProxyFactory } from "safe-contracts/proxies/SafeProxyFactory.sol"; +import { SignMessageLib } from "safe-contracts/libraries/SignMessageLib.sol"; + +// Libraries +import { LibSort } from "@solady/utils/LibSort.sol"; import { OwnerManager } from "safe-contracts/base/OwnerManager.sol"; import { ModuleManager } from "safe-contracts/base/ModuleManager.sol"; import { GuardManager } from "safe-contracts/base/GuardManager.sol"; -import { SafeProxyFactory as GnosisSafeProxyFactory } from "safe-contracts/proxies/SafeProxyFactory.sol"; import { Enum } from "safe-contracts/common/Enum.sol"; -import { SignMessageLib } from "safe-contracts/libraries/SignMessageLib.sol"; -import "./CompatibilityFallbackHandler_1_3_0.sol"; // Tools to simplify testing Safe contracts // Author: Colin Nielsen (https://github.com/colinnielsen/safe-tools) diff --git a/packages/contracts-bedrock/test/safe/LivenessGuard.t.sol b/packages/contracts-bedrock/test/safe/LivenessGuard.t.sol index 11891092201..17733a9bda3 100644 --- a/packages/contracts-bedrock/test/safe/LivenessGuard.t.sol +++ b/packages/contracts-bedrock/test/safe/LivenessGuard.t.sol @@ -1,17 +1,19 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; -import { StdUtils } from "forge-std/StdUtils.sol"; -import { StdCheats } from "forge-std/StdCheats.sol"; -import { Safe } from "safe-contracts/Safe.sol"; -import { OwnerManager } from "safe-contracts/base/OwnerManager.sol"; -import { Enum } from "safe-contracts/common/Enum.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; import "test/safe-tools/SafeTestTools.sol"; -import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +// Contracts +import { Safe } from "safe-contracts/Safe.sol"; +import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import { LivenessGuard } from "src/safe/LivenessGuard.sol"; +// Libraries +import { OwnerManager } from "safe-contracts/base/OwnerManager.sol"; +import { Enum } from "safe-contracts/common/Enum.sol"; + /// @notice A wrapper contract exposing the length of the ownersBefore set in the LivenessGuard. contract LivenessGuard_WrappedGuard_Harness is LivenessGuard { using EnumerableSet for EnumerableSet.AddressSet; @@ -165,7 +167,7 @@ contract LivenessGuard_ShowLiveness_Test is LivenessGuard_TestInit { /// @title LivenessGuard_Uncategorized_Test /// @notice General tests that are not testing any function directly of the `LivenessGuard` /// contract or are testing multiple functions at once. -contract LivenessGuard_Uncategorized_Test is StdCheats, StdUtils, LivenessGuard_TestInit { +contract LivenessGuard_Uncategorized_Test is LivenessGuard_TestInit { using SafeTestLib for SafeInstance; /// @notice Enumerates the possible owner management operations diff --git a/packages/contracts-bedrock/test/safe/LivenessModule.t.sol b/packages/contracts-bedrock/test/safe/LivenessModule.t.sol index ee69895946b..12b15d5ebfa 100644 --- a/packages/contracts-bedrock/test/safe/LivenessModule.t.sol +++ b/packages/contracts-bedrock/test/safe/LivenessModule.t.sol @@ -1,14 +1,18 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; -import { Safe } from "safe-contracts/Safe.sol"; -import { OwnerManager } from "safe-contracts/base/OwnerManager.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; import "test/safe-tools/SafeTestTools.sol"; +// Contracts +import { Safe } from "safe-contracts/Safe.sol"; import { LivenessModule } from "src/safe/LivenessModule.sol"; import { LivenessGuard } from "src/safe/LivenessGuard.sol"; +// Libraries +import { OwnerManager } from "safe-contracts/base/OwnerManager.sol"; + /// @title LivenessModule_TestInit /// @notice Reusable test initialization for `LivenessModule` tests. abstract contract LivenessModule_TestInit is Test, SafeTestTools { diff --git a/packages/contracts-bedrock/test/safe/LivenessModule2.t.sol b/packages/contracts-bedrock/test/safe/LivenessModule2.t.sol index e64b0b27e2d..1059c0703ec 100644 --- a/packages/contracts-bedrock/test/safe/LivenessModule2.t.sol +++ b/packages/contracts-bedrock/test/safe/LivenessModule2.t.sol @@ -1,17 +1,21 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; -import { Enum } from "safe-contracts/common/Enum.sol"; -import { Safe } from "safe-contracts/Safe.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; import "test/safe-tools/SafeTestTools.sol"; -import { Constants } from "src/libraries/Constants.sol"; +import { DummyGuard } from "test/mocks/DummyGuard.sol"; +// Contracts +import { Safe } from "safe-contracts/Safe.sol"; import { LivenessModule2 } from "src/safe/LivenessModule2.sol"; import { SaferSafes } from "src/safe/SaferSafes.sol"; + +// Libraries +import { Enum } from "safe-contracts/common/Enum.sol"; +import { Constants } from "src/libraries/Constants.sol"; import { ModuleManager } from "safe-contracts/base/ModuleManager.sol"; import { GuardManager } from "safe-contracts/base/GuardManager.sol"; -import { DummyGuard } from "test/mocks/DummyGuard.sol"; /// @title LivenessModule2_TestUtils /// @notice Reusable helper methods for LivenessModule2 tests. diff --git a/packages/contracts-bedrock/test/safe/SafeSigners.t.sol b/packages/contracts-bedrock/test/safe/SafeSigners.t.sol index f59ad036064..b2aec90c5a1 100644 --- a/packages/contracts-bedrock/test/safe/SafeSigners.t.sol +++ b/packages/contracts-bedrock/test/safe/SafeSigners.t.sol @@ -1,10 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; -import { SafeSigners } from "src/safe/SafeSigners.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; import "test/safe-tools/SafeTestTools.sol"; +// Contracts +import { SafeSigners } from "src/safe/SafeSigners.sol"; + /// @title SafeSigners_TestInit /// @notice Reusable test initialization for `SafeSigners` tests. abstract contract SafeSigners_TestInit is Test { diff --git a/packages/contracts-bedrock/test/safe/TimelockGuard.t.sol b/packages/contracts-bedrock/test/safe/TimelockGuard.t.sol index 2333c23208d..0bb2e4b84c0 100644 --- a/packages/contracts-bedrock/test/safe/TimelockGuard.t.sol +++ b/packages/contracts-bedrock/test/safe/TimelockGuard.t.sol @@ -1,15 +1,22 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; -import { Safe } from "safe-contracts/Safe.sol"; -import { GuardManager } from "safe-contracts/base/GuardManager.sol"; -import { ITransactionGuard } from "interfaces/safe/ITransactionGuard.sol"; +// Testing import "test/safe-tools/SafeTestTools.sol"; +import { Test } from "test/setup/Test.sol"; +import { stdStorage, StdStorage } from "forge-std/StdStorage.sol"; +// Contracts +import { Safe } from "safe-contracts/Safe.sol"; import { TimelockGuard } from "src/safe/TimelockGuard.sol"; import { SaferSafes } from "src/safe/SaferSafes.sol"; +// Libraries +import { GuardManager } from "safe-contracts/base/GuardManager.sol"; + +// Interfaces +import { ITransactionGuard } from "interfaces/safe/ITransactionGuard.sol"; + using TransactionBuilder for TransactionBuilder.Transaction; /// @title TransactionBuilder diff --git a/packages/contracts-bedrock/test/scripts/DeployOwnership.t.sol b/packages/contracts-bedrock/test/scripts/DeployOwnership.t.sol index 7f9df4cd260..eec100fa85d 100644 --- a/packages/contracts-bedrock/test/scripts/DeployOwnership.t.sol +++ b/packages/contracts-bedrock/test/scripts/DeployOwnership.t.sol @@ -1,19 +1,27 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; +// Forge +import { StdCheatsSafe } from "forge-std/StdCheats.sol"; + +// Testing +import { Test } from "test/setup/Test.sol"; + +// Scripts import { DeployOwnership, SafeConfig, SecurityCouncilConfig, LivenessModuleConfig } from "scripts/deploy/DeployOwnership.s.sol"; -import { Test } from "forge-std/Test.sol"; +// Contracts import { Safe } from "safe-contracts/Safe.sol"; -import { ModuleManager } from "safe-contracts/base/ModuleManager.sol"; - import { LivenessModule2 } from "src/safe/LivenessModule2.sol"; +// Libraries +import { ModuleManager } from "safe-contracts/base/ModuleManager.sol"; + contract DeployOwnershipTest is Test, DeployOwnership { address internal constant SENTINEL_MODULES = address(0x1); @@ -22,6 +30,10 @@ contract DeployOwnershipTest is Test, DeployOwnership { run(); } + function makeAddr(string memory _name) internal override(Test, StdCheatsSafe) returns (address) { + return Test.makeAddr(_name); + } + /// @dev Helper function to make assertions on basic Safe config properties. function _checkSafeConfig(SafeConfig memory _safeConfig, Safe _safe) internal view { assertEq(_safe.getThreshold(), _safeConfig.threshold); diff --git a/packages/contracts-bedrock/test/scripts/FetchChainInfo.t.sol b/packages/contracts-bedrock/test/scripts/FetchChainInfo.t.sol index 95a93038751..906c47d37ff 100644 --- a/packages/contracts-bedrock/test/scripts/FetchChainInfo.t.sol +++ b/packages/contracts-bedrock/test/scripts/FetchChainInfo.t.sol @@ -1,8 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; + +// Scripts import { FetchChainInfo, FetchChainInfoInput, FetchChainInfoOutput } from "scripts/FetchChainInfo.s.sol"; + +// Libraries import { GameTypes, GameType } from "src/dispute/lib/Types.sol"; import { LibGameType } from "src/dispute/lib/LibUDT.sol"; diff --git a/packages/contracts-bedrock/test/scripts/L2Genesis.t.sol b/packages/contracts-bedrock/test/scripts/L2Genesis.t.sol index f4943f82ea8..e157c1cff99 100644 --- a/packages/contracts-bedrock/test/scripts/L2Genesis.t.sol +++ b/packages/contracts-bedrock/test/scripts/L2Genesis.t.sol @@ -1,11 +1,18 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; + +// Scripts import { L2Genesis } from "scripts/L2Genesis.s.sol"; -import { Predeploys } from "src/libraries/Predeploys.sol"; import { LATEST_FORK } from "scripts/libraries/Config.sol"; + +// Libraries +import { Predeploys } from "src/libraries/Predeploys.sol"; + +// Interfaces import { ISuperchainRevSharesCalculator } from "interfaces/L2/ISuperchainRevSharesCalculator.sol"; import { ISequencerFeeVault } from "interfaces/L2/ISequencerFeeVault.sol"; import { IBaseFeeVault } from "interfaces/L2/IBaseFeeVault.sol"; diff --git a/packages/contracts-bedrock/test/scripts/VerifyOPCM.t.sol b/packages/contracts-bedrock/test/scripts/VerifyOPCM.t.sol index 8455c09462f..48407923434 100644 --- a/packages/contracts-bedrock/test/scripts/VerifyOPCM.t.sol +++ b/packages/contracts-bedrock/test/scripts/VerifyOPCM.t.sol @@ -6,7 +6,7 @@ import { LibString } from "@solady/utils/LibString.sol"; import { DevFeatures } from "src/libraries/DevFeatures.sol"; // Tests -import { OPContractsManager_TestInit } from "test/L1/OPContractsManager.t.sol"; +import { CommonTest } from "test/setup/CommonTest.sol"; // Scripts import { VerifyOPCM } from "scripts/deploy/VerifyOPCM.s.sol"; @@ -61,7 +61,7 @@ contract VerifyOPCM_Harness is VerifyOPCM { /// @title VerifyOPCM_TestInit /// @notice Reusable test initialization for `VerifyOPCM` tests. -abstract contract VerifyOPCM_TestInit is OPContractsManager_TestInit { +abstract contract VerifyOPCM_TestInit is CommonTest { VerifyOPCM_Harness internal harness; function setUp() public virtual override { @@ -69,6 +69,12 @@ abstract contract VerifyOPCM_TestInit is OPContractsManager_TestInit { harness = new VerifyOPCM_Harness(); harness.setUp(); } + + /// @notice Sets up the environment variables for the VerifyOPCM test. + function setupEnvVars() public { + vm.setEnv("EXPECTED_SUPERCHAIN_CONFIG", vm.toString(address(opcm.superchainConfig()))); + vm.setEnv("EXPECTED_PROTOCOL_VERSIONS", vm.toString(address(opcm.protocolVersions()))); + } } /// @title VerifyOPCM_Run_Test @@ -76,8 +82,17 @@ abstract contract VerifyOPCM_TestInit is OPContractsManager_TestInit { contract VerifyOPCM_Run_Test is VerifyOPCM_TestInit { function setUp() public override { super.setUp(); - skipIfDevFeatureEnabled(DevFeatures.OPCM_V2); - setupEnvVars(); + + // If OPCM V2 is enabled, set up the test environment for OPCM V2. + // nosemgrep: sol-style-vm-env-only-in-config-sol + if (vm.envOr("DEV_FEATURE__OPCM_V2", false)) { + opcm = IOPContractsManager(address(opcmV2)); + } else { + setupEnvVars(); + } + + // Set the OPCM address so that runSingle also runs for V2 OPCM if the dev feature is enabled. + vm.setEnv("OPCM_ADDRESS", vm.toString(address(opcm))); } /// @notice Tests that the script succeeds when no changes are introduced. @@ -468,6 +483,9 @@ contract VerifyOPCM_Run_Test is VerifyOPCM_TestInit { // Coverage changes bytecode and causes failures, skip. skipIfCoverage(); + // If OPCM V2 is enabled because we do not use environment variables for OPCM V2. + skipIfDevFeatureEnabled(DevFeatures.OPCM_V2); + // Set expected addresses via environment variables address expectedSuperchainConfig = address(0x1111); address expectedProtocolVersions = address(0x2222); diff --git a/packages/contracts-bedrock/test/setup/CommonTest.sol b/packages/contracts-bedrock/test/setup/CommonTest.sol index 529534b7ceb..5185f52d8ca 100644 --- a/packages/contracts-bedrock/test/setup/CommonTest.sol +++ b/packages/contracts-bedrock/test/setup/CommonTest.sol @@ -1,10 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -// Forge -import { Test } from "forge-std/Test.sol"; - // Testing +import { Test } from "test/setup/Test.sol"; import { Setup } from "test/setup/Setup.sol"; import { Events } from "test/setup/Events.sol"; import { FFIInterface } from "test/setup/FFIInterface.sol"; diff --git a/packages/contracts-bedrock/test/setup/DisputeGames.sol b/packages/contracts-bedrock/test/setup/DisputeGames.sol index cd389c9042c..abda463cd15 100644 --- a/packages/contracts-bedrock/test/setup/DisputeGames.sol +++ b/packages/contracts-bedrock/test/setup/DisputeGames.sol @@ -33,6 +33,20 @@ contract DisputeGames is FeatureFlags { ) internal returns (address) + { + return createGame(_factory, _gameType, _proposer, _claim, abi.encode(bytes32(_l2BlockNumber))); + } + + /// @notice Helper function to create a permissioned game through the factory + function createGame( + IDisputeGameFactory _factory, + GameType _gameType, + address _proposer, + Claim _claim, + bytes memory _extraData + ) + internal + returns (address) { // Check if there's an init bond required for the game type uint256 initBond = _factory.initBonds(_gameType); @@ -46,8 +60,7 @@ contract DisputeGames is FeatureFlags { // We use vm.startPrank to set both msg.sender and tx.origin to the proposer vm.startPrank(_proposer, _proposer); - IDisputeGame gameProxy = - _factory.create{ value: initBond }(_gameType, _claim, abi.encode(bytes32(_l2BlockNumber))); + IDisputeGame gameProxy = _factory.create{ value: initBond }(_gameType, _claim, _extraData); vm.stopPrank(); @@ -59,6 +72,12 @@ contract DisputeGames is FeatureFlags { || _gameType.raw() == GameTypes.SUPER_PERMISSIONED_CANNON.raw(); } + /// @notice Checks if the game type is a super game type + function isSuperGame(GameType _gameType) internal pure returns (bool) { + return _gameType.raw() == GameTypes.SUPER_PERMISSIONED_CANNON.raw() + || _gameType.raw() == GameTypes.SUPER_CANNON.raw() || _gameType.raw() == GameTypes.SUPER_CANNON_KONA.raw(); + } + enum GameArg { PRESTATE, VM, diff --git a/packages/contracts-bedrock/test/setup/Test.sol b/packages/contracts-bedrock/test/setup/Test.sol new file mode 100644 index 00000000000..5ce331b9109 --- /dev/null +++ b/packages/contracts-bedrock/test/setup/Test.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// Forge +import { Test as ForgeTest } from "forge-std/Test.sol"; + +/// @title Test +/// @notice Test is a minimal extension of the Test contract with op-specific tweaks. +abstract contract Test is ForgeTest { + /// @notice Makes an address without a private key, labels it, and cleans it. + /// @param _name The name of the address. + /// @return The address. + function makeAddr(string memory _name) internal virtual override returns (address) { + address addr = address(uint160(uint256(keccak256(abi.encode(_name))))); + destroyAccount(addr, address(0)); + vm.label(addr, _name); + return addr; + } +} diff --git a/packages/contracts-bedrock/test/universal/CrossDomainMessenger.t.sol b/packages/contracts-bedrock/test/universal/CrossDomainMessenger.t.sol index dd9cf4eb1e3..3d11320d33a 100644 --- a/packages/contracts-bedrock/test/universal/CrossDomainMessenger.t.sol +++ b/packages/contracts-bedrock/test/universal/CrossDomainMessenger.t.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Testing utilities -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; import { CommonTest } from "test/setup/CommonTest.sol"; // Libraries diff --git a/packages/contracts-bedrock/test/universal/Proxy.t.sol b/packages/contracts-bedrock/test/universal/Proxy.t.sol index a19cb26d8af..b3b7b52342e 100644 --- a/packages/contracts-bedrock/test/universal/Proxy.t.sol +++ b/packages/contracts-bedrock/test/universal/Proxy.t.sol @@ -1,10 +1,17 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; + +// Scripts +import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; + +// Libraries import { Bytes32AddressLib } from "@rari-capital/solmate/src/utils/Bytes32AddressLib.sol"; + +// Interfaces import { IProxy } from "interfaces/universal/IProxy.sol"; -import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; contract Proxy_SimpleStorage_Harness { mapping(uint256 => uint256) internal store; diff --git a/packages/contracts-bedrock/test/universal/ProxyAdmin.t.sol b/packages/contracts-bedrock/test/universal/ProxyAdmin.t.sol index b86196e60bb..f81ec40007e 100644 --- a/packages/contracts-bedrock/test/universal/ProxyAdmin.t.sol +++ b/packages/contracts-bedrock/test/universal/ProxyAdmin.t.sol @@ -2,9 +2,12 @@ pragma solidity 0.8.15; // Testing -import { Test } from "forge-std/Test.sol"; +import { Test } from "test/setup/Test.sol"; import { Proxy_SimpleStorage_Harness } from "test/universal/Proxy.t.sol"; +// Scripts +import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; + // Interfaces import { IAddressManager } from "interfaces/legacy/IAddressManager.sol"; import { IL1ChugSplashProxy } from "interfaces/legacy/IL1ChugSplashProxy.sol"; @@ -12,8 +15,6 @@ import { IResolvedDelegateProxy } from "interfaces/legacy/IResolvedDelegateProxy import { IProxy } from "interfaces/universal/IProxy.sol"; import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; -import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; - /// @title ProxyAdmin_TestInit /// @notice Reusable test initialization for `ProxyAdmin` tests. abstract contract ProxyAdmin_TestInit is Test { diff --git a/packages/contracts-bedrock/test/universal/ReinitializableBase.t.sol b/packages/contracts-bedrock/test/universal/ReinitializableBase.t.sol index 805da5b5fab..a6d61ec6a32 100644 --- a/packages/contracts-bedrock/test/universal/ReinitializableBase.t.sol +++ b/packages/contracts-bedrock/test/universal/ReinitializableBase.t.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.15; // Testing -import { Test } from "forge-std/Test.sol"; +import { Test } from "test/setup/Test.sol"; // Contracts import { ReinitializableBase } from "src/universal/ReinitializableBase.sol"; diff --git a/packages/contracts-bedrock/test/universal/WETH98.t.sol b/packages/contracts-bedrock/test/universal/WETH98.t.sol index 96065c8c890..8173dec67d3 100644 --- a/packages/contracts-bedrock/test/universal/WETH98.t.sol +++ b/packages/contracts-bedrock/test/universal/WETH98.t.sol @@ -2,12 +2,14 @@ pragma solidity 0.8.15; // Testing -import { Test } from "forge-std/Test.sol"; +import { Test } from "test/setup/Test.sol"; -// Contracts -import { IWETH98 } from "interfaces/universal/IWETH98.sol"; +// Scripts import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; +// Interfaces +import { IWETH98 } from "interfaces/universal/IWETH98.sol"; + /// @title WETH98_TestInit /// @notice Reusable test initialization for `WETH98` tests. abstract contract WETH98_TestInit is Test { diff --git a/packages/contracts-bedrock/test/vendor/AddressAliasHelper.t.sol b/packages/contracts-bedrock/test/vendor/AddressAliasHelper.t.sol index b803abba9fa..68d838f6921 100644 --- a/packages/contracts-bedrock/test/vendor/AddressAliasHelper.t.sol +++ b/packages/contracts-bedrock/test/vendor/AddressAliasHelper.t.sol @@ -1,7 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test } from "forge-std/Test.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; + +// Libraries import { AddressAliasHelper } from "src/vendor/AddressAliasHelper.sol"; /// @title AddressAliasHelper_ApplyL1ToL2Alias_Test diff --git a/packages/contracts-bedrock/test/vendor/Initializable.t.sol b/packages/contracts-bedrock/test/vendor/Initializable.t.sol index 6bde0b7ccbb..e74a08deec2 100644 --- a/packages/contracts-bedrock/test/vendor/Initializable.t.sol +++ b/packages/contracts-bedrock/test/vendor/Initializable.t.sol @@ -398,7 +398,7 @@ contract Initializer_Test is CommonTest { excludes[j++] = "src/dispute/SuperFaultDisputeGame.sol"; excludes[j++] = "src/dispute/PermissionedDisputeGame.sol"; excludes[j++] = "src/dispute/SuperPermissionedDisputeGame.sol"; - excludes[j++] = "src/dispute/zk/OPSuccinctFaultDisputeGame.sol"; + excludes[j++] = "src/dispute/zk/OptimisticZkGame.sol"; // TODO: Eventually remove this exclusion. Same reason as above dispute contracts. excludes[j++] = "src/L1/OPContractsManager.sol"; // TODO: Eventually remove this exclusion. Same reason as above dispute contracts. diff --git a/packages/contracts-bedrock/test/vendor/InitializableOZv5.t.sol b/packages/contracts-bedrock/test/vendor/InitializableOZv5.t.sol index b12bade2a9f..c8077d79b04 100644 --- a/packages/contracts-bedrock/test/vendor/InitializableOZv5.t.sol +++ b/packages/contracts-bedrock/test/vendor/InitializableOZv5.t.sol @@ -1,13 +1,22 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -import { Test } from "forge-std/Test.sol"; -import { IOptimismSuperchainERC20 } from "interfaces/L2/IOptimismSuperchainERC20.sol"; -import { Initializable } from "@openzeppelin/contracts-v5/proxy/utils/Initializable.sol"; +// Testing +import { Test } from "test/setup/Test.sol"; + +// Scripts import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; -import { IFeeVault } from "interfaces/L2/IFeeVault.sol"; + +// Contracts +import { Initializable } from "@openzeppelin/contracts-v5/proxy/utils/Initializable.sol"; + +// Libraries import { Types } from "src/libraries/Types.sol"; +// Interfaces +import { IOptimismSuperchainERC20 } from "interfaces/L2/IOptimismSuperchainERC20.sol"; +import { IFeeVault } from "interfaces/L2/IFeeVault.sol"; + /// @title InitializerOZv5_Test /// @dev Ensures that the `initialize()` function on contracts cannot be called more than /// once. Tests the contracts inheriting from `Initializable` from OpenZeppelin Contracts v5.