diff --git a/.circleci/config.yml b/.circleci/config.yml index 1f31c32031a55..4deee36609ce3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -420,7 +420,7 @@ jobs: contracts-bedrock-build: docker: - image: <> - resource_class: xlarge + resource_class: 2xlarge parameters: build_args: description: Forge build arguments @@ -720,7 +720,7 @@ jobs: circleci_ip_ranges: true docker: - image: <> - resource_class: xlarge + resource_class: 2xlarge parameters: test_list: description: List of test files to run @@ -1929,7 +1929,8 @@ workflows: dev_features: <> matrix: parameters: - dev_features: ["main", "OPTIMISM_PORTAL_INTEROP"] + dev_features: + ["main", "OPTIMISM_PORTAL_INTEROP", "CUSTOM_GAS_TOKEN"] # need this requires to ensure that all FFI JSONs exist requires: - contracts-bedrock-build diff --git a/op-acceptance-tests/acceptance-tests.yaml b/op-acceptance-tests/acceptance-tests.yaml index 275d4f7c789e5..25bcac26c63c2 100644 --- a/op-acceptance-tests/acceptance-tests.yaml +++ b/op-acceptance-tests/acceptance-tests.yaml @@ -100,4 +100,9 @@ gates: description: "Jovian network tests." tests: - package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/jovian + + - id: cgt + description: "Custom Gas Token (CGT) network tests." + tests: + - package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/custom_gas_token timeout: 10m \ No newline at end of file diff --git a/op-acceptance-tests/justfile b/op-acceptance-tests/justfile index 807fa41390f11..00080189fcbb6 100644 --- a/op-acceptance-tests/justfile +++ b/op-acceptance-tests/justfile @@ -14,6 +14,9 @@ jovian: interop: @just acceptance-test "" interop +cgt: + @just acceptance-test "" cgt + # Run acceptance tests with mise-managed binary # Usage: just acceptance-test [devnet] [gate] diff --git a/op-acceptance-tests/tests/custom_gas_token/cgt_introspection_test.go b/op-acceptance-tests/tests/custom_gas_token/cgt_introspection_test.go new file mode 100644 index 0000000000000..06a77a3878d8b --- /dev/null +++ b/op-acceptance-tests/tests/custom_gas_token/cgt_introspection_test.go @@ -0,0 +1,25 @@ +package custom_gas_token + +import ( + "testing" + + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/presets" +) + +// TestCGT_IntrospectionViaL1Block verifies that the L2 L1Block predeploy reports +// that CGT mode is enabled and exposes non-empty token metadata (name, symbol). +func TestCGT_IntrospectionViaL1Block(gt *testing.T) { + t := devtest.SerialT(gt) + sys := presets.NewMinimal(t) + + name, symbol := ensureCGTOrSkip(t, sys) + + // Metadata should be non-empty. + if name == "" { + t.Require().Fail("gasPayingTokenName() returned empty string") + } + if symbol == "" { + t.Require().Fail("gasPayingTokenSymbol() returned empty string") + } +} diff --git a/op-acceptance-tests/tests/custom_gas_token/cgt_l1_portal_introspection_test.go b/op-acceptance-tests/tests/custom_gas_token/cgt_l1_portal_introspection_test.go new file mode 100644 index 0000000000000..173476ca3eafb --- /dev/null +++ b/op-acceptance-tests/tests/custom_gas_token/cgt_l1_portal_introspection_test.go @@ -0,0 +1,53 @@ +package custom_gas_token + +import ( + "context" + "testing" + "time" + + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/presets" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/rpc" + "github.com/lmittmann/w3" +) + +// TestCGT_L1PortalIntrospection checks that the L1 OptimismPortal exposes +// a valid SystemConfig address via its systemConfig() view. +func TestCGT_L1PortalIntrospection(gt *testing.T) { + t := devtest.SerialT(gt) + sys := presets.NewMinimal(t) + + // Skip if this devnet is not CGT-enabled (uses your existing gate). + ensureCGTOrSkip(t, sys) + + l1c := sys.L1EL.EthClient() + portal := sys.L2Chain.DepositContractAddr() + + ctx, cancel := context.WithTimeout(t.Ctx(), 20*time.Second) + defer cancel() + + // Portal exposes systemConfig() -> address + systemConfigFunc := w3.MustNewFunc("systemConfig()", "address") + + data, err := systemConfigFunc.EncodeArgs() + if err != nil { + t.Require().Fail("encode systemConfig() args: %v", err) + } + + out, err := l1c.Call(ctx, ethereum.CallMsg{To: &portal, Data: data}, rpc.LatestBlockNumber) + if err != nil { + t.Require().Fail("portal.systemConfig() call failed: %v", err) + } + + var sysCfg common.Address + if err := systemConfigFunc.DecodeReturns(out, &sysCfg); err != nil { + t.Require().Fail("decode portal.systemConfig() returns: %v", err) + } + + if sysCfg == (common.Address{}) { + t.Require().Fail("portal.systemConfig() returned zero address") + } +} diff --git a/op-acceptance-tests/tests/custom_gas_token/cgt_native_payment_test.go b/op-acceptance-tests/tests/custom_gas_token/cgt_native_payment_test.go new file mode 100644 index 0000000000000..2d6ab79b5a61c --- /dev/null +++ b/op-acceptance-tests/tests/custom_gas_token/cgt_native_payment_test.go @@ -0,0 +1,48 @@ +package custom_gas_token + +import ( + "testing" + + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/presets" + "github.com/ethereum-optimism/optimism/op-service/eth" +) + +// TestCGT_ValueTransferPaysGasInToken verifies that on CGT chains a simple L2 +// value transfer charges gas in the native ERC-20, and balances reflect +// recipient +amount and sender > amount decrease (amount + gas). +func TestCGT_ValueTransferPaysGasInToken(gt *testing.T) { + t := devtest.SerialT(gt) + sys := presets.NewMinimal(t) + + ensureCGTOrSkip(t, sys) + + sender := sys.FunderL2.NewFundedEOA(eth.OneTenthEther) + recipient := sys.Wallet.NewEOA(sys.L2EL) + + amount := eth.OneHundredthEther + beforeS := sender.GetBalance() + beforeR := recipient.GetBalance() + + // This sends L2 native (CGT) value. + sender.Transfer(recipient.Address(), amount) + + // Wait until recipient reflects the transfer. + // We don't wait on sender balance; it includes gas and is non-deterministic. + recipient.WaitForBalance(beforeR.Add(amount)) + + afterS := sender.GetBalance() + afterR := recipient.GetBalance() + + // Recipient increased by amount + wantR := beforeR.Add(amount) + if afterR != wantR { + t.Require().Fail("recipient balance mismatch: got %s, want %s", afterR, wantR) + } + + // Sender decreased by at least amount (amount + gas). Strict inequality: + if !(beforeS.Sub(afterS)).Gt(amount) { + t.Require().Fail("sender delta must exceed transferred amount (gas must be paid): before=%s after=%s amount=%s", + beforeS, afterS, amount) + } +} diff --git a/op-acceptance-tests/tests/custom_gas_token/cgt_portal_reverts_test.go b/op-acceptance-tests/tests/custom_gas_token/cgt_portal_reverts_test.go new file mode 100644 index 0000000000000..fa1b3a982554b --- /dev/null +++ b/op-acceptance-tests/tests/custom_gas_token/cgt_portal_reverts_test.go @@ -0,0 +1,34 @@ +package custom_gas_token + +import ( + "context" + "testing" + "time" + + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/presets" + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" +) + +// TestCGT_PortalReceiveReverts asserts that sending ETH to the L1 OptimismPortal +// (receive() -> depositTransaction) reverts under CGT, preventing ETH from getting stuck. +func TestCGT_PortalReceiveReverts(gt *testing.T) { + t := devtest.SerialT(gt) + sys := presets.NewMinimal(t) + ensureCGTOrSkip(t, sys) + + l1c := sys.L1EL.EthClient() + portal := sys.L2Chain.DepositContractAddr() + + // Try to send 1 wei to the Portal (receive() -> depositTransaction); should revert in CGT mode. + ctx, cancel := context.WithTimeout(t.Ctx(), 20*time.Second) + defer cancel() + _, err := l1c.EstimateGas(ctx, ethereum.CallMsg{ + To: &portal, + Value: common.Big1, + }) + if err == nil { + t.Require().Fail("expected L1 Portal to revert on direct ETH send in CGT mode, but estimator returned no error") + } +} diff --git a/op-acceptance-tests/tests/custom_gas_token/cgt_reverts_test.go b/op-acceptance-tests/tests/custom_gas_token/cgt_reverts_test.go new file mode 100644 index 0000000000000..30a2277ffafcc --- /dev/null +++ b/op-acceptance-tests/tests/custom_gas_token/cgt_reverts_test.go @@ -0,0 +1,68 @@ +package custom_gas_token + +import ( + "context" + "testing" + "time" + + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/presets" + "github.com/ethereum-optimism/optimism/op-service/eth" + + "math/big" + + "github.com/ethereum/go-ethereum" + "github.com/lmittmann/w3" +) + +// TestCGT_MessengerRejectsValue ensures that sending native value to the +// L2CrossDomainMessenger reverts under CGT (non-payable path). +func TestCGT_MessengerRejectsValue(gt *testing.T) { + t := devtest.SerialT(gt) + sys := presets.NewMinimal(t) + ensureCGTOrSkip(t, sys) + + ctx, cancel := context.WithTimeout(t.Ctx(), 30*time.Second) + defer cancel() + + from := sys.FunderL2.NewFundedEOA(eth.OneHundredthEther).Address() + _, err := sys.L2EL.Escape().L2EthClient().EstimateGas(ctx, ethereum.CallMsg{ + From: from, + To: &l2XDMAddr, + Value: big.NewInt(1), // 1 wei native + Data: nil, + }) + if err == nil { + t.Require().Fail("expected estimation error when sending value to L2CrossDomainMessenger in CGT mode") + } +} + +// TestCGT_L2StandardBridge_LegacyWithdrawReverts verifies that the legacy +// ETH-specific withdraw path on L2StandardBridge reverts under CGT. +func TestCGT_L2StandardBridge_LegacyWithdrawReverts(gt *testing.T) { + t := devtest.SerialT(gt) + sys := presets.NewMinimal(t) + ensureCGTOrSkip(t, sys) + + ctx, cancel := context.WithTimeout(t.Ctx(), 30*time.Second) + defer cancel() + + withdrawFunc := w3.MustNewFunc("withdraw(address,uint256,uint32,bytes)", "") + + // Any address is fine; the ETH-specific legacy path should be disabled under CGT. + anyAddress := l2XDMAddr + data, err := withdrawFunc.EncodeArgs(anyAddress, big.NewInt(1), uint32(100_000), []byte{}) + if err != nil { + t.Require().Fail("%v", err) + } + + from := sys.FunderL2.NewFundedEOA(eth.OneHundredthEther).Address() + _, err = sys.L2EL.Escape().L2EthClient().EstimateGas(ctx, ethereum.CallMsg{ + From: from, + To: &l2BridgeAddr, + Data: data, + }) + if err == nil { + t.Require().Fail("expected estimation error for L2StandardBridge.withdraw under CGT") + } +} diff --git a/op-acceptance-tests/tests/custom_gas_token/cgt_systemconfig_test.go b/op-acceptance-tests/tests/custom_gas_token/cgt_systemconfig_test.go new file mode 100644 index 0000000000000..a350b8d60f6be --- /dev/null +++ b/op-acceptance-tests/tests/custom_gas_token/cgt_systemconfig_test.go @@ -0,0 +1,107 @@ +package custom_gas_token + +import ( + "context" + "testing" + "time" + + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/presets" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/rpc" + "github.com/lmittmann/w3" +) + +// TestCGT_SystemConfigFlagOnL1 checks that the L1 SystemConfig contract reports +// CGT=true via isCustomGasToken(). Skips if the devnet does not wire this flag. +func TestCGT_SystemConfigFlagOnL1(gt *testing.T) { + t := devtest.SerialT(gt) + sys := presets.NewMinimal(t) + ensureCGTOrSkip(t, sys) + + l1c := sys.L1EL.EthClient() + portal := sys.L2Chain.DepositContractAddr() + + systemConfigFunc := w3.MustNewFunc("systemConfig()", "address") + isCustomGasTokenFunc := w3.MustNewFunc("isCustomGasToken()", "bool") + + ctx, cancel := context.WithTimeout(t.Ctx(), 20*time.Second) + defer cancel() + + // Resolve SystemConfig via Portal.systemConfig() + data, _ := systemConfigFunc.EncodeArgs() + out, err := l1c.Call(ctx, ethereum.CallMsg{To: &portal, Data: data}, rpc.LatestBlockNumber) + if err != nil { + t.Require().Fail("portal.systemConfig() call failed: %v", err) + } + var sysCfg common.Address + if err := systemConfigFunc.DecodeReturns(out, &sysCfg); err != nil { + t.Require().Fail("unpack portal.systemConfig() failed: %v", err) + } + if (sysCfg == common.Address{}) { + t.Require().Fail("portal.systemConfig() returned zero address") + } + + // Ask SystemConfig whether CGT is enabled. + data, _ = isCustomGasTokenFunc.EncodeArgs() + out, err = l1c.Call(ctx, ethereum.CallMsg{To: &sysCfg, Data: data}, rpc.LatestBlockNumber) + if err != nil { + t.Require().Fail("SystemConfig.isCustomGasToken() call failed: %v", err) + } + var isCustom bool + if err := isCustomGasTokenFunc.DecodeReturns(out, &isCustom); err != nil { + t.Require().Fail("unpack isCustomGasToken failed: %v", err) + } + if !isCustom { + t.Skip("SystemConfig.isCustomGasToken() = false on this devnet; skipping") + } +} + +// TestCGT_SystemConfigFeatureFlag re-validates the CGT flag on SystemConfig, +// using locally encoded calls (mirrors the previous test structure). Skips on devnets without the flag. +func TestCGT_SystemConfigFeatureFlag(gt *testing.T) { + t := devtest.SerialT(gt) + sys := presets.NewMinimal(t) + + // Skip if not in CGT mode (uses L2 L1Block.isCustomGasToken()). + ensureCGTOrSkip(t, sys) + + l1c := sys.L1EL.EthClient() + portal := sys.L2Chain.DepositContractAddr() + + ctx, cancel := context.WithTimeout(t.Ctx(), 20*time.Second) + defer cancel() + + // Resolve SystemConfig via Portal.systemConfig() + systemConfigFunc := w3.MustNewFunc("systemConfig()", "address") + isCustomGasTokenFunc := w3.MustNewFunc("isCustomGasToken()", "bool") + + data, _ := systemConfigFunc.EncodeArgs() + out, err := l1c.Call(ctx, ethereum.CallMsg{To: &portal, Data: data}, rpc.LatestBlockNumber) + if err != nil { + t.Require().Fail("portal.systemConfig() call failed: %v", err) + } + var sysCfg common.Address + if err := systemConfigFunc.DecodeReturns(out, &sysCfg); err != nil { + t.Require().Fail("unpack portal.systemConfig() failed: %v", err) + } + + // Query the CGT flag on SystemConfig via IGasToken.isCustomGasToken(). + data, _ = isCustomGasTokenFunc.EncodeArgs() + out, err = l1c.Call(ctx, ethereum.CallMsg{ + To: &sysCfg, + Data: data, + }, rpc.LatestBlockNumber) + if err != nil { + t.Require().Fail("SystemConfig.isCustomGasToken() call failed: %v", err) + } + var flag bool + if err := isCustomGasTokenFunc.DecodeReturns(out, &flag); err != nil { + t.Require().Fail("unpack isCustomGasToken failed: %v", err) + } + if !flag { + t.Skip("SystemConfig.isCustomGasToken() = false on this devnet; skipping") + } +} diff --git a/op-acceptance-tests/tests/custom_gas_token/helpers.go b/op-acceptance-tests/tests/custom_gas_token/helpers.go new file mode 100644 index 0000000000000..f76228df3a682 --- /dev/null +++ b/op-acceptance-tests/tests/custom_gas_token/helpers.go @@ -0,0 +1,70 @@ +// op-acceptance-tests/tests/custom_gas_token/helpers.go +package custom_gas_token + +import ( + "context" + "time" + + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/presets" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/rpc" + "github.com/lmittmann/w3" +) + +var ( + // L2 predeploy: L1Block (address is stable across OP Stack chains) + l1BlockAddr = common.HexToAddress("0x4200000000000000000000000000000000000015") + + // L2 predeploy: L2CrossDomainMessenger & L2StandardBridge (for revert checks) + l2XDMAddr = common.HexToAddress("0x4200000000000000000000000000000000000007") + l2BridgeAddr = common.HexToAddress("0x4200000000000000000000000000000000000010") +) + +// ensureCGTOrSkip probes L2 L1Block for CGT mode. If not enabled, the test is skipped. +// Returns (name, symbol). +func ensureCGTOrSkip(t devtest.T, sys *presets.Minimal) (string, string) { + l2 := sys.L2EL.Escape().L2EthClient() + + isCustomGasTokenFunc := w3.MustNewFunc("isCustomGasToken()", "bool") + gasPayingTokenNameFunc := w3.MustNewFunc("gasPayingTokenName()", "string") + gasPayingTokenSymbolFunc := w3.MustNewFunc("gasPayingTokenSymbol()", "string") + + ctx, cancel := context.WithTimeout(t.Ctx(), 20*time.Second) + defer cancel() + + // isCustomGasToken() + data, _ := isCustomGasTokenFunc.EncodeArgs() + out, err := l2.Call(ctx, ethereum.CallMsg{To: &l1BlockAddr, Data: data}, rpc.LatestBlockNumber) + if err != nil { + t.Skipf("CGT not enabled (isCustomGasToken() call failed): %v", err) + } + var isCustom bool + if err := isCustomGasTokenFunc.DecodeReturns(out, &isCustom); err != nil { + t.Require().NoError(err) + } + if !isCustom { + t.Skip("CGT disabled on this devnet (native ETH mode detected)") + } + + // Read metadata (name/symbol) + data, _ = gasPayingTokenNameFunc.EncodeArgs() + out, err = l2.Call(ctx, ethereum.CallMsg{To: &l1BlockAddr, Data: data}, rpc.LatestBlockNumber) + t.Require().NoError(err) + var name string + if err := gasPayingTokenNameFunc.DecodeReturns(out, &name); err != nil { + t.Require().NoError(err) + } + + data, _ = gasPayingTokenSymbolFunc.EncodeArgs() + out, err = l2.Call(ctx, ethereum.CallMsg{To: &l1BlockAddr, Data: data}, rpc.LatestBlockNumber) + t.Require().NoError(err) + var symbol string + if err := gasPayingTokenSymbolFunc.DecodeReturns(out, &symbol); err != nil { + t.Require().NoError(err) + } + + return name, symbol +} diff --git a/op-acceptance-tests/tests/custom_gas_token/init_test.go b/op-acceptance-tests/tests/custom_gas_token/init_test.go new file mode 100644 index 0000000000000..e3aa47385cbf8 --- /dev/null +++ b/op-acceptance-tests/tests/custom_gas_token/init_test.go @@ -0,0 +1,22 @@ +package custom_gas_token + +import ( + "math/big" + "testing" + + "github.com/ethereum-optimism/optimism/op-devstack/presets" + "github.com/ethereum-optimism/optimism/op-devstack/stack" + "github.com/ethereum-optimism/optimism/op-devstack/sysgo" +) + +func TestMain(m *testing.M) { + // Create a CGT-enabled devnet with 1M tokens of liquidity + liq := new(big.Int).Mul(big.NewInt(1_000_000), big.NewInt(1e18)) // 1M tokens * 18 decimals + + presets.DoMain(m, + presets.WithMinimal(), + stack.MakeCommon(sysgo.WithDeployerOptions( + sysgo.WithCustomGasToken(true, "Custom Gas Token", "CGT", liq), + )), + ) +} diff --git a/op-chain-ops/genesis/config.go b/op-chain-ops/genesis/config.go index f3e2052cfa3f9..4e61646a003fb 100644 --- a/op-chain-ops/genesis/config.go +++ b/op-chain-ops/genesis/config.go @@ -275,18 +275,29 @@ func (d *GasPriceOracleDeployConfig) OperatorFeeParams() [32]byte { type GasTokenDeployConfig struct { // UseCustomGasToken is a flag to indicate that a custom gas token should be used UseCustomGasToken bool `json:"useCustomGasToken"` - // CustomGasTokenAddress is the address of the ERC20 token to be used to pay for gas on L2. - CustomGasTokenAddress common.Address `json:"customGasTokenAddress"` + // GasPayingTokenName represents the custom gas token name. + GasPayingTokenName string `json:"gasPayingTokenName"` + // GasPayingTokenSymbol represents the custom gas token symbol. + GasPayingTokenSymbol string `json:"gasPayingTokenSymbol"` + // NativeAssetLiquidityAmount represents the amount of liquidity to pre-fund the NativeAssetLiquidity contract with. + NativeAssetLiquidityAmount *hexutil.Big `json:"nativeAssetLiquidityAmount"` } var _ ConfigChecker = (*GasTokenDeployConfig)(nil) func (d *GasTokenDeployConfig) Check(log log.Logger) error { if d.UseCustomGasToken { - if d.CustomGasTokenAddress == (common.Address{}) { - return fmt.Errorf("%w: CustomGasTokenAddress cannot be address(0)", ErrInvalidDeployConfig) + if d.GasPayingTokenName == "" { + return fmt.Errorf("%w: GasPayingTokenName cannot be empty", ErrInvalidDeployConfig) } - log.Info("Using custom gas token", "address", d.CustomGasTokenAddress) + if d.GasPayingTokenSymbol == "" { + return fmt.Errorf("%w: GasPayingTokenSymbol cannot be empty", ErrInvalidDeployConfig) + } + if d.NativeAssetLiquidityAmount == nil || d.NativeAssetLiquidityAmount.ToInt().Sign() < 0 { + return fmt.Errorf("%w: NativeAssetLiquidityAmount cannot be nil or negative", ErrInvalidDeployConfig) + } + + log.Info("Using custom gas token", "name", d.GasPayingTokenName, "symbol", d.GasPayingTokenSymbol, "nativeAssetLiquidityAmount", d.NativeAssetLiquidityAmount.ToInt()) } return nil } diff --git a/op-chain-ops/genesis/testdata/test-deploy-config-full.json b/op-chain-ops/genesis/testdata/test-deploy-config-full.json index 0f332842204f9..71ac6fae51b9b 100644 --- a/op-chain-ops/genesis/testdata/test-deploy-config-full.json +++ b/op-chain-ops/genesis/testdata/test-deploy-config-full.json @@ -6,7 +6,6 @@ "maxSequencerDrift": 20, "sequencerWindowSize": 100, "channelTimeout": 30, - "customGasTokenAddress": "0x0000000000000000000000000000000000000000", "p2pSequencerAddress": "0x9965507d1a55bcc2695c58ba16fb37d819b0a4dc", "batchInboxAddress": "0x42000000000000000000000000000000000000ff", "batchSenderAddress": "0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc", @@ -85,6 +84,9 @@ "disputeGameFinalityDelaySeconds": 6, "respectedGameType": 0, "useCustomGasToken": false, + "gasPayingTokenName": "", + "gasPayingTokenSymbol": "", + "nativeAssetLiquidityAmount": null, "useFaultProofs": false, "useAltDA": false, "daBondSize": 0, diff --git a/op-chain-ops/interopgen/deploy.go b/op-chain-ops/interopgen/deploy.go index 62d321e1f0f7a..8c368164daf37 100644 --- a/op-chain-ops/interopgen/deploy.go +++ b/op-chain-ops/interopgen/deploy.go @@ -333,6 +333,10 @@ func GenesisL2(l2Host *script.Host, cfg *L2Config, deployment *L2Deployment, mul DeployCrossL2Inbox: multichainDepSet, EnableGovernance: cfg.EnableGovernance, FundDevAccounts: cfg.FundDevAccounts, + UseCustomGasToken: cfg.UseCustomGasToken, + GasPayingTokenName: cfg.GasPayingTokenName, + GasPayingTokenSymbol: cfg.GasPayingTokenSymbol, + NativeAssetLiquidityAmount: cfg.NativeAssetLiquidityAmount.ToInt(), }); err != nil { return fmt.Errorf("failed L2 genesis: %w", err) } diff --git a/op-chain-ops/interopgen/recipe.go b/op-chain-ops/interopgen/recipe.go index 2d40c4451c94d..f780f83f35a13 100644 --- a/op-chain-ops/interopgen/recipe.go +++ b/op-chain-ops/interopgen/recipe.go @@ -243,7 +243,10 @@ func (r *InteropDevL2Recipe) build(l1ChainID uint64, addrs devkeys.Addresses) (* GasPriceOracleBlobBaseFeeScalar: 810949, }, GasTokenDeployConfig: genesis.GasTokenDeployConfig{ - UseCustomGasToken: false, + UseCustomGasToken: false, + GasPayingTokenName: "", + GasPayingTokenSymbol: "", + NativeAssetLiquidityAmount: (*hexutil.Big)(big.NewInt(0)), }, OperatorDeployConfig: genesis.OperatorDeployConfig{ P2PSequencerAddress: sequencerP2P, diff --git a/op-deployer/pkg/deployer/devfeatures.go b/op-deployer/pkg/deployer/devfeatures.go index 1fd2db94c8509..ecb8e3a883a37 100644 --- a/op-deployer/pkg/deployer/devfeatures.go +++ b/op-deployer/pkg/deployer/devfeatures.go @@ -17,6 +17,9 @@ var ( // DeployV2DisputeGames enables deployment of V2 dispute game contracts. DeployV2DisputeGames = common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000100") + + // CustomGasToken enables the custom gas token. + CustomGasToken = common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000001000") ) // 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 76cf010eaa864..e2aeaa4d6db31 100644 --- a/op-deployer/pkg/deployer/integration_test/apply_test.go +++ b/op-deployer/pkg/deployer/integration_test/apply_test.go @@ -13,6 +13,8 @@ import ( "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/bootstrap" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/inspect" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/integration_test/shared" + + "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/params" "github.com/ethereum-optimism/optimism/op-service/testutils" @@ -34,6 +36,7 @@ import ( op_e2e "github.com/ethereum-optimism/optimism/op-e2e" "github.com/holiman/uint256" + "github.com/lmittmann/w3" "github.com/ethereum-optimism/optimism/op-chain-ops/devkeys" "github.com/ethereum-optimism/optimism/op-chain-ops/genesis" @@ -226,6 +229,59 @@ func TestEndToEndApply(t *testing.T) { require.NoError(t, err) } }) + + t.Run("with custom gas token", func(t *testing.T) { + intent, st := shared.NewIntent(t, l1ChainID, dk, l2ChainID1, loc, loc, testCustomGasLimit) + + // CGT config for L2 genesis + amount := new(big.Int) + amount.SetString("1000000000000000000000", 10) + intent.Chains[0].CustomGasToken = state.CustomGasToken{ + Enabled: true, + Name: "Custom Gas Token", + Symbol: "CGT", + InitialLiquidity: (*hexutil.Big)(amount), + } + // CGT config for OPCM + intent.GlobalDeployOverrides = map[string]interface{}{ + "devFeatureBitmap": deployer.CustomGasToken, + } + + 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, + })) + + systemConfig := st.Chains[0].SystemConfigProxy + fn := w3.MustNewFunc("isFeatureEnabled(bytes32)", "bool") + // bytes32("CUSTOM_GAS_TOKEN") + data, err := fn.EncodeArgs(w3.H("0x435553544f4d5f4741535f544f4b454e00000000000000000000000000000000")) + require.NoError(t, err) + + res, err := l1Client.CallContract(ctx, ethereum.CallMsg{ + To: &systemConfig, + Data: data, + }, nil) + require.NoError(t, err) + + var response bool + err = fn.DecodeReturns(res, &response) + require.NoError(t, err) + require.Equal(t, true, response) + + // Check that the native asset liquidity predeploy has the configured amount in L2 genesis + nativeAssetLiquidityAddr := common.HexToAddress("0x4200000000000000000000000000000000000029") + l2Genesis := st.Chains[0].Allocs.Data.Accounts + account, exists := l2Genesis[nativeAssetLiquidityAddr] + 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") + }) } func TestGlobalOverrides(t *testing.T) { diff --git a/op-deployer/pkg/deployer/integration_test/shared/shared.go b/op-deployer/pkg/deployer/integration_test/shared/shared.go index 296d56104a717..702e7a7fe4aeb 100644 --- a/op-deployer/pkg/deployer/integration_test/shared/shared.go +++ b/op-deployer/pkg/deployer/integration_test/shared/shared.go @@ -42,6 +42,11 @@ func NewChainIntent(t *testing.T, dk *devkeys.MnemonicDevKeys, l1ChainID *big.In Proposer: AddrFor(t, dk, devkeys.ProposerRole.Key(l1ChainID)), Challenger: AddrFor(t, dk, devkeys.ChallengerRole.Key(l1ChainID)), }, + CustomGasToken: state.CustomGasToken{ + Enabled: false, + Name: "", + Symbol: "", + }, } } diff --git a/op-deployer/pkg/deployer/manage/testdata/state.json b/op-deployer/pkg/deployer/manage/testdata/state.json index c99554e1e9731..d8e562c51dd83 100755 --- a/op-deployer/pkg/deployer/manage/testdata/state.json +++ b/op-deployer/pkg/deployer/manage/testdata/state.json @@ -31,6 +31,12 @@ "batcher": "0x0000000000000000000000000000000000000006", "proposer": "0x0000000000000000000000000000000000000007", "challenger": "0xfd1d2e729ae8eee2e146c033bf4400fe75284301" + }, + "customGasToken": { + "enabled": false, + "name": "", + "symbol": "", + "initialLiquidity": null } } ], @@ -57,4 +63,3 @@ "l1StateDump": null, "DeploymentCalldata": null } - diff --git a/op-deployer/pkg/deployer/opcm/l2genesis.go b/op-deployer/pkg/deployer/opcm/l2genesis.go index 3f91196cc85fc..92e39307678a1 100644 --- a/op-deployer/pkg/deployer/opcm/l2genesis.go +++ b/op-deployer/pkg/deployer/opcm/l2genesis.go @@ -28,6 +28,10 @@ type L2GenesisInput struct { DeployCrossL2Inbox bool EnableGovernance bool FundDevAccounts bool + UseCustomGasToken bool + GasPayingTokenName string + GasPayingTokenSymbol string + NativeAssetLiquidityAmount *big.Int } type L2GenesisScript script.DeployScriptWithoutOutput[L2GenesisInput] diff --git a/op-deployer/pkg/deployer/pipeline/l2genesis.go b/op-deployer/pkg/deployer/pipeline/l2genesis.go index bb3f514358bce..a9b9a77b5993c 100644 --- a/op-deployer/pkg/deployer/pipeline/l2genesis.go +++ b/op-deployer/pkg/deployer/pipeline/l2genesis.go @@ -22,6 +22,13 @@ import ( ) type l2GenesisOverrides struct { + // ===== CUSTOM GAS TOKEN (CGT) CONFIGURATION ===== + UseCustomGasToken bool `json:"useCustomGasToken"` // CGT: Enable custom gas token mode + GasPayingTokenName string `json:"gasPayingTokenName"` // CGT: Name of the custom gas token + GasPayingTokenSymbol string `json:"gasPayingTokenSymbol"` // CGT: Symbol of the custom gas token + NativeAssetLiquidityAmount *hexutil.Big `json:"nativeAssetLiquidityAmount"` // CGT: Liquidity amount for NativeAssetLiquidity contract + + // ===== GENERAL L2 CONFIGURATION (NON-CGT) ===== FundDevAccounts bool `json:"fundDevAccounts"` BaseFeeVaultMinimumWithdrawalAmount *hexutil.Big `json:"baseFeeVaultMinimumWithdrawalAmount"` L1FeeVaultMinimumWithdrawalAmount *hexutil.Big `json:"l1FeeVaultMinimumWithdrawalAmount"` @@ -94,6 +101,11 @@ func GenerateL2Genesis(pEnv *Env, intent *state.Intent, bundle ArtifactsBundle, DeployCrossL2Inbox: len(intent.Chains) > 1, EnableGovernance: overrides.EnableGovernance, FundDevAccounts: overrides.FundDevAccounts, + // Custom Gas Token (CGT) configuration passed to L2Genesis script + UseCustomGasToken: thisIntent.CustomGasToken.Enabled, // CGT: Enable/disable custom gas token + GasPayingTokenName: thisIntent.CustomGasToken.Name, // CGT: Token name (e.g., "Custom Gas Token") + GasPayingTokenSymbol: thisIntent.CustomGasToken.Symbol, // CGT: Token symbol (e.g., "CGT") + NativeAssetLiquidityAmount: thisIntent.GetInitialLiquidity(), // CGT: Liquidity amount for NativeAssetLiquidity contract }); err != nil { return fmt.Errorf("failed to call L2Genesis script: %w", err) } @@ -142,6 +154,16 @@ func calculateL2GenesisOverrides(intent *state.Intent, thisIntent *state.ChainIn } } + // If CustomGasToken is not enabled, update it with override values + if !thisIntent.CustomGasToken.Enabled { + thisIntent.CustomGasToken = state.CustomGasToken{ + Enabled: overrides.UseCustomGasToken, + Name: overrides.GasPayingTokenName, + Symbol: overrides.GasPayingTokenSymbol, + InitialLiquidity: overrides.NativeAssetLiquidityAmount, + } + } + return overrides, schedule, nil } @@ -156,6 +178,7 @@ func wdNetworkToBig(wd genesis.WithdrawalNetwork) *big.Int { func defaultOverrides() l2GenesisOverrides { return l2GenesisOverrides{ + // ===== GENERAL L2 DEFAULTS ===== FundDevAccounts: false, BaseFeeVaultMinimumWithdrawalAmount: standard.VaultMinWithdrawalAmount, L1FeeVaultMinimumWithdrawalAmount: standard.VaultMinWithdrawalAmount, @@ -165,5 +188,10 @@ func defaultOverrides() l2GenesisOverrides { SequencerFeeVaultWithdrawalNetwork: "local", EnableGovernance: false, GovernanceTokenOwner: standard.GovernanceTokenOwner, + // ===== CGT DEFAULTS ===== + UseCustomGasToken: false, // CGT disabled by default + GasPayingTokenName: "", // Empty when CGT disabled + GasPayingTokenSymbol: "", // Empty when CGT disabled + NativeAssetLiquidityAmount: (*hexutil.Big)(big.NewInt(0)), // Default to 0 when CGT disabled (consistent with "" and false) } } diff --git a/op-deployer/pkg/deployer/pipeline/l2genesis_test.go b/op-deployer/pkg/deployer/pipeline/l2genesis_test.go index aed2a1e782390..65bcf4860cb65 100644 --- a/op-deployer/pkg/deployer/pipeline/l2genesis_test.go +++ b/op-deployer/pkg/deployer/pipeline/l2genesis_test.go @@ -43,17 +43,11 @@ func TestCalculateL2GenesisOverrides(t *testing.T) { }, chainIntent: &state.ChainIntent{}, expectError: false, - expectedOverrides: l2GenesisOverrides{ - FundDevAccounts: true, - BaseFeeVaultMinimumWithdrawalAmount: standard.VaultMinWithdrawalAmount, - L1FeeVaultMinimumWithdrawalAmount: standard.VaultMinWithdrawalAmount, - SequencerFeeVaultMinimumWithdrawalAmount: standard.VaultMinWithdrawalAmount, - BaseFeeVaultWithdrawalNetwork: "local", - L1FeeVaultWithdrawalNetwork: "local", - SequencerFeeVaultWithdrawalNetwork: "local", - EnableGovernance: false, - GovernanceTokenOwner: standard.GovernanceTokenOwner, - }, + expectedOverrides: func() l2GenesisOverrides { + defaults := defaultOverrides() + defaults.FundDevAccounts = true + return defaults + }(), expectedSchedule: func() *genesis.UpgradeScheduleDeployConfig { return standard.DefaultHardforkScheduleForTag("") }, @@ -73,21 +67,28 @@ func TestCalculateL2GenesisOverrides(t *testing.T) { "enableGovernance": true, "governanceTokenOwner": "0x1111111111111111111111111111111111111111", "l2GenesisInteropTimeOffset": "0x1234", + "useCustomGasToken": false, + "gasPayingTokenName": "", + "gasPayingTokenSymbol": "", + "nativeAssetLiquidityAmount": "0x0", }, }, chainIntent: &state.ChainIntent{}, expectError: false, - expectedOverrides: l2GenesisOverrides{ - FundDevAccounts: true, - BaseFeeVaultMinimumWithdrawalAmount: (*hexutil.Big)(hexutil.MustDecodeBig("0x1234")), - L1FeeVaultMinimumWithdrawalAmount: (*hexutil.Big)(hexutil.MustDecodeBig("0x2345")), - SequencerFeeVaultMinimumWithdrawalAmount: (*hexutil.Big)(hexutil.MustDecodeBig("0x3456")), - BaseFeeVaultWithdrawalNetwork: "remote", - L1FeeVaultWithdrawalNetwork: "remote", - SequencerFeeVaultWithdrawalNetwork: "remote", - EnableGovernance: true, - GovernanceTokenOwner: common.HexToAddress("0x1111111111111111111111111111111111111111"), - }, + expectedOverrides: func() l2GenesisOverrides { + defaults := defaultOverrides() + defaults.FundDevAccounts = true + defaults.BaseFeeVaultMinimumWithdrawalAmount = (*hexutil.Big)(hexutil.MustDecodeBig("0x1234")) + defaults.L1FeeVaultMinimumWithdrawalAmount = (*hexutil.Big)(hexutil.MustDecodeBig("0x2345")) + defaults.SequencerFeeVaultMinimumWithdrawalAmount = (*hexutil.Big)(hexutil.MustDecodeBig("0x3456")) + defaults.BaseFeeVaultWithdrawalNetwork = "remote" + defaults.L1FeeVaultWithdrawalNetwork = "remote" + defaults.SequencerFeeVaultWithdrawalNetwork = "remote" + defaults.EnableGovernance = true + defaults.GovernanceTokenOwner = common.HexToAddress("0x1111111111111111111111111111111111111111") + defaults.NativeAssetLiquidityAmount = (*hexutil.Big)(hexutil.MustDecodeBig("0x0")) + return defaults + }(), expectedSchedule: func() *genesis.UpgradeScheduleDeployConfig { sched := standard.DefaultHardforkScheduleForTag("") sched.L2GenesisInteropTimeOffset = op_service.U64UtilPtr(0x1234) @@ -114,20 +115,27 @@ func TestCalculateL2GenesisOverrides(t *testing.T) { "enableGovernance": true, "governanceTokenOwner": "0x1111111111111111111111111111111111111111", "l2GenesisInteropTimeOffset": "0x1234", + "useCustomGasToken": false, + "gasPayingTokenName": "", + "gasPayingTokenSymbol": "", + "nativeAssetLiquidityAmount": "0x0", }, }, expectError: false, - expectedOverrides: l2GenesisOverrides{ - FundDevAccounts: true, - BaseFeeVaultMinimumWithdrawalAmount: (*hexutil.Big)(hexutil.MustDecodeBig("0x1234")), - L1FeeVaultMinimumWithdrawalAmount: (*hexutil.Big)(hexutil.MustDecodeBig("0x2345")), - SequencerFeeVaultMinimumWithdrawalAmount: (*hexutil.Big)(hexutil.MustDecodeBig("0x3456")), - BaseFeeVaultWithdrawalNetwork: "remote", - L1FeeVaultWithdrawalNetwork: "remote", - SequencerFeeVaultWithdrawalNetwork: "remote", - EnableGovernance: true, - GovernanceTokenOwner: common.HexToAddress("0x1111111111111111111111111111111111111111"), - }, + expectedOverrides: func() l2GenesisOverrides { + defaults := defaultOverrides() + defaults.FundDevAccounts = true + defaults.BaseFeeVaultMinimumWithdrawalAmount = (*hexutil.Big)(hexutil.MustDecodeBig("0x1234")) + defaults.L1FeeVaultMinimumWithdrawalAmount = (*hexutil.Big)(hexutil.MustDecodeBig("0x2345")) + defaults.SequencerFeeVaultMinimumWithdrawalAmount = (*hexutil.Big)(hexutil.MustDecodeBig("0x3456")) + defaults.BaseFeeVaultWithdrawalNetwork = "remote" + defaults.L1FeeVaultWithdrawalNetwork = "remote" + defaults.SequencerFeeVaultWithdrawalNetwork = "remote" + defaults.EnableGovernance = true + defaults.GovernanceTokenOwner = common.HexToAddress("0x1111111111111111111111111111111111111111") + defaults.NativeAssetLiquidityAmount = (*hexutil.Big)(hexutil.MustDecodeBig("0x0")) + return defaults + }(), expectedSchedule: func() *genesis.UpgradeScheduleDeployConfig { sched := standard.DefaultHardforkScheduleForTag("") sched.L2GenesisInteropTimeOffset = op_service.U64UtilPtr(0x1234) @@ -140,11 +148,17 @@ func TestCalculateL2GenesisOverrides(t *testing.T) { L1ContractsLocator: &artifacts.Locator{}, GlobalDeployOverrides: map[string]any{ "l2GenesisInteropTimeOffset": "0x0", + "nativeAssetLiquidityAmount": "0x0", }, }, - chainIntent: &state.ChainIntent{}, - expectError: false, - expectedOverrides: defaultOverrides(), + chainIntent: &state.ChainIntent{}, + expectError: false, + expectedOverrides: func() l2GenesisOverrides { + defaults := defaultOverrides() + // Override with the same value that comes from JSON merge to match internal representation + defaults.NativeAssetLiquidityAmount = (*hexutil.Big)(hexutil.MustDecodeBig("0x0")) + return defaults + }(), expectedSchedule: func() *genesis.UpgradeScheduleDeployConfig { schedule := standard.DefaultHardforkScheduleForTag("") schedule.L2GenesisInteropTimeOffset = op_service.U64UtilPtr(0) diff --git a/op-deployer/pkg/deployer/state/chain_intent.go b/op-deployer/pkg/deployer/state/chain_intent.go index 819bc9e3f1f9f..0686e3f3e3c8e 100644 --- a/op-deployer/pkg/deployer/state/chain_intent.go +++ b/op-deployer/pkg/deployer/state/chain_intent.go @@ -2,6 +2,7 @@ package state import ( "fmt" + "math/big" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" @@ -56,6 +57,13 @@ type L2DevGenesisParams struct { Prefund map[common.Address]*hexutil.U256 `json:"prefund" toml:"prefund"` } +type CustomGasToken struct { + Enabled bool `json:"enabled" toml:"enabled"` + Name string `json:"name,omitempty" toml:"name,omitempty"` + Symbol string `json:"symbol,omitempty" toml:"symbol,omitempty"` + InitialLiquidity *hexutil.Big `json:"initialLiquidity,omitempty" toml:"initialLiquidity,omitempty"` +} + type ChainIntent struct { ID common.Hash `json:"id" toml:"id"` BaseFeeVaultRecipient common.Address `json:"baseFeeVaultRecipient" toml:"baseFeeVaultRecipient"` @@ -73,7 +81,7 @@ type ChainIntent struct { OperatorFeeConstant uint64 `json:"operatorFeeConstant,omitempty" toml:"operatorFeeConstant,omitempty"` L1StartBlockHash *common.Hash `json:"l1StartBlockHash,omitempty" toml:"l1StartBlockHash,omitempty"` MinBaseFee uint64 `json:"minBaseFee,omitempty" toml:"minBaseFee,omitempty"` - + CustomGasToken CustomGasToken `json:"customGasToken" toml:"customGasToken"` // Optional. For development purposes only. Only enabled if the operation mode targets a genesis-file output. L2DevGenesisParams *L2DevGenesisParams `json:"l2DevGenesisParams,omitempty" toml:"l2DevGenesisParams,omitempty"` } @@ -119,9 +127,32 @@ func (c *ChainIntent) Check() error { return fmt.Errorf("%w: chainId=%s", ErrFeeVaultZeroAddress, c.ID) } + if c.CustomGasToken.Enabled { + if c.CustomGasToken.Name == "" { + return fmt.Errorf("%w: CustomGasToken.Name cannot be empty when enabled, chainId=%s", ErrIncompatibleValue, c.ID) + } + if c.CustomGasToken.Symbol == "" { + return fmt.Errorf("%w: CustomGasToken.Symbol cannot be empty when enabled, chainId=%s", ErrIncompatibleValue, c.ID) + } + + if c.CustomGasToken.InitialLiquidity == nil || c.CustomGasToken.InitialLiquidity.ToInt().Sign() < 0 { + return fmt.Errorf("%w: CustomGasToken.InitialLiquidity must be set and non-negative when custom gas token is enabled, chainId=%s", ErrIncompatibleValue, c.ID) + } + } + if c.DangerousAltDAConfig.UseAltDA { return c.DangerousAltDAConfig.Check(nil) } return nil } + +// GetInitialLiquidity returns the native asset liquidity amount for the chain. +// If not set, returns the default value of zero. +func (c *ChainIntent) GetInitialLiquidity() *big.Int { + if c.CustomGasToken.InitialLiquidity != nil { + return c.CustomGasToken.InitialLiquidity.ToInt() + } + + return (*hexutil.Big)(big.NewInt(0)).ToInt() +} diff --git a/op-deployer/pkg/deployer/state/deploy_config.go b/op-deployer/pkg/deployer/state/deploy_config.go index d497e0d08eb26..8cc109a872539 100644 --- a/op-deployer/pkg/deployer/state/deploy_config.go +++ b/op-deployer/pkg/deployer/state/deploy_config.go @@ -71,6 +71,13 @@ func CombineDeployConfig(intent *Intent, chainIntent *ChainIntent, state *State, EIP1559Elasticity: chainIntent.Eip1559Elasticity, }, + GasTokenDeployConfig: genesis.GasTokenDeployConfig{ + UseCustomGasToken: chainIntent.CustomGasToken.Enabled, + GasPayingTokenName: chainIntent.CustomGasToken.Name, + GasPayingTokenSymbol: chainIntent.CustomGasToken.Symbol, + NativeAssetLiquidityAmount: chainIntent.CustomGasToken.InitialLiquidity, + }, + // STOP! This struct sets the _default_ upgrade schedule for all chains. // Any upgrades you enable here will be enabled for all new deployments. // In-development hardforks should never be activated here. Instead, they @@ -108,15 +115,11 @@ func CombineDeployConfig(intent *Intent, chainIntent *ChainIntent, state *State, if chainState.StartBlock == nil { // These are dummy variables - see below for rationale. - num := rpc.LatestBlockNumber - cfg.L1StartingBlockTag = &genesis.MarshalableRPCBlockNumberOrHash{ - BlockNumber: &num, - } + blockNumOrHash := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber) + cfg.L1StartingBlockTag = (*genesis.MarshalableRPCBlockNumberOrHash)(&blockNumOrHash) } else { - startHash := chainState.StartBlock.Hash - cfg.L1StartingBlockTag = &genesis.MarshalableRPCBlockNumberOrHash{ - BlockHash: &startHash, - } + blockNumOrHash := rpc.BlockNumberOrHashWithHash(chainState.StartBlock.Hash, false) + cfg.L1StartingBlockTag = (*genesis.MarshalableRPCBlockNumberOrHash)(&blockNumOrHash) } if chainIntent.DangerousAltDAConfig.UseAltDA { diff --git a/op-deployer/pkg/deployer/state/deploy_config_test.go b/op-deployer/pkg/deployer/state/deploy_config_test.go index a91a90a8cbcb8..48c69a19dfdd9 100644 --- a/op-deployer/pkg/deployer/state/deploy_config_test.go +++ b/op-deployer/pkg/deployer/state/deploy_config_test.go @@ -1,6 +1,7 @@ package state import ( + "math/big" "testing" "github.com/ethereum-optimism/optimism/op-chain-ops/addresses" @@ -33,6 +34,12 @@ func TestCombineDeployConfig(t *testing.T) { UnsafeBlockSigner: common.HexToAddress("0xabc"), Batcher: common.HexToAddress("0xdef"), }, + CustomGasToken: CustomGasToken{ + Enabled: false, + Name: "", + Symbol: "", + InitialLiquidity: (*hexutil.Big)(big.NewInt(0)), + }, } state := State{ SuperchainDeployment: &addresses.SuperchainContracts{ProtocolVersionsProxy: common.HexToAddress("0x123")}, diff --git a/op-deployer/pkg/deployer/state/intent.go b/op-deployer/pkg/deployer/state/intent.go index e12c356a21248..ff2e600f14269 100644 --- a/op-deployer/pkg/deployer/state/intent.go +++ b/op-deployer/pkg/deployer/state/intent.go @@ -158,6 +158,9 @@ func (c *Intent) validateStandardValues() error { if len(chain.AdditionalDisputeGames) > 0 { return fmt.Errorf("%w: chainId=%s additionalDisputeGames must be nil", ErrNonStandardValue, chain.ID) } + if chain.CustomGasToken.Enabled { + return fmt.Errorf("%w: chainId=%s custom gas token must be disabled for standard chains", ErrNonStandardValue, chain.ID) + } } challenger, _ := standard.ChallengerAddressFor(c.L1ChainID) @@ -299,6 +302,12 @@ func NewIntentCustom(l1ChainId uint64, l2ChainIds []common.Hash) (Intent, error) intent.Chains = append(intent.Chains, &ChainIntent{ ID: l2ChainID, GasLimit: standard.GasLimit, + CustomGasToken: CustomGasToken{ + Enabled: false, + Name: "", + Symbol: "", + InitialLiquidity: (*hexutil.Big)(big.NewInt(0)), + }, }) } return intent, nil @@ -343,6 +352,12 @@ func NewIntentStandard(l1ChainId uint64, l2ChainIds []common.Hash) (Intent, erro L1ProxyAdminOwner: l1ProxyAdminOwner, L2ProxyAdminOwner: l2ProxyAdminOwner, }, + CustomGasToken: CustomGasToken{ + Enabled: false, + Name: "", + Symbol: "", + InitialLiquidity: (*hexutil.Big)(big.NewInt(0)), + }, }) } return intent, nil diff --git a/op-deployer/pkg/deployer/state/intent_test.go b/op-deployer/pkg/deployer/state/intent_test.go index 8ffb0060db3c5..1a0f1c5feb676 100644 --- a/op-deployer/pkg/deployer/state/intent_test.go +++ b/op-deployer/pkg/deployer/state/intent_test.go @@ -1,10 +1,12 @@ package state import ( + "math/big" "testing" "github.com/ethereum-optimism/optimism/op-chain-ops/addresses" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/stretchr/testify/require" ) @@ -62,6 +64,18 @@ func TestValidateStandardValues(t *testing.T) { }, ErrNonStandardValue, }, + { + "CustomGasToken", + func(intent *Intent) { + intent.Chains[0].CustomGasToken = CustomGasToken{ + Enabled: true, + Name: "Custom Gas Token", + Symbol: "CGT", + InitialLiquidity: (*hexutil.Big)(big.NewInt(1000)), + } + }, + ErrNonStandardValue, + }, { "SuperchainConfigProxy", func(intent *Intent) { @@ -131,6 +145,10 @@ func TestValidateCustomValues(t *testing.T) { err = intent.Check() require.NoError(t, err) + setCustomGasToken(&intent) + err = intent.Check() + require.NoError(t, err) + tests := []struct { name string mutator func(intent *Intent) @@ -155,6 +173,28 @@ func TestValidateCustomValues(t *testing.T) { }, ErrIncompatibleValue, }, + { + "empty custom gas token name when enabled", + func(intent *Intent) { + intent.Chains[0].CustomGasToken = CustomGasToken{ + Enabled: true, + Name: "", + Symbol: "CGT", + } + }, + ErrIncompatibleValue, + }, + { + "empty custom gas token symbol when enabled", + func(intent *Intent) { + intent.Chains[0].CustomGasToken = CustomGasToken{ + Enabled: true, + Name: "Custom Gas Token", + Symbol: "", + } + }, + ErrIncompatibleValue, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -211,3 +251,16 @@ func setFeeAddresses(intent *Intent) { intent.Chains[0].L1FeeVaultRecipient = common.HexToAddress("0x09") intent.Chains[0].SequencerFeeVaultRecipient = common.HexToAddress("0x0A") } + +func setCustomGasToken(intent *Intent) { + // 1000 ETH in wei (1000 * 10^18) + amount := new(big.Int) + amount.SetString("1000000000000000000000", 10) + + intent.Chains[0].CustomGasToken = CustomGasToken{ + Enabled: true, + Name: "Custom Gas Token", + Symbol: "CGT", + InitialLiquidity: (*hexutil.Big)(amount), + } +} diff --git a/op-devstack/sysgo/deployer.go b/op-devstack/sysgo/deployer.go index 4c204d5b9bea6..f95b3132ef6e0 100644 --- a/op-devstack/sysgo/deployer.go +++ b/op-devstack/sysgo/deployer.go @@ -339,6 +339,14 @@ func WithDisputeGameFinalityDelaySeconds(seconds uint64) DeployerOption { } } +func WithCustomGasToken(enabled bool, name, symbol string, initialLiquidity *big.Int) DeployerOption { + return func(p devtest.P, keys devkeys.Keys, builder intentbuilder.Builder) { + for _, l2Cfg := range builder.L2s() { + l2Cfg.WithCustomGasToken(enabled, name, symbol, initialLiquidity) + } + } +} + func (wb *worldBuilder) buildL1Genesis() { wb.require.NotNil(wb.output.L1DevGenesis, "must have L1 genesis outer config") wb.require.NotNil(wb.output.L1StateDump, "must have L1 genesis alloc") diff --git a/op-e2e/config/init.go b/op-e2e/config/init.go index c2b4c8b508685..a29d56977efe5 100644 --- a/op-e2e/config/init.go +++ b/op-e2e/config/init.go @@ -400,6 +400,12 @@ func defaultIntent(root string, loc *artifacts.Locator, deployer common.Address, Proposer: addrs.Proposer, Challenger: common.HexToAddress("0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65"), }, + CustomGasToken: state.CustomGasToken{ + Enabled: false, + Name: "", + Symbol: "", + InitialLiquidity: (*hexutil.Big)(big.NewInt(0)), + }, AdditionalDisputeGames: []state.AdditionalDisputeGame{ { ChainProofParams: state.ChainProofParams{ diff --git a/op-e2e/e2eutils/intentbuilder/builder.go b/op-e2e/e2eutils/intentbuilder/builder.go index 448cb72e1c453..a666b4c9ad7fb 100644 --- a/op-e2e/e2eutils/intentbuilder/builder.go +++ b/op-e2e/e2eutils/intentbuilder/builder.go @@ -2,6 +2,7 @@ package intentbuilder import ( "fmt" + "math/big" "github.com/holiman/uint256" "github.com/stretchr/testify/require" @@ -48,6 +49,7 @@ type L2Configurator interface { WithL1StartBlockHash(hash common.Hash) WithAdditionalDisputeGames(games []state.AdditionalDisputeGame) WithFinalizationPeriodSeconds(value uint64) + WithCustomGasToken(enabled bool, name string, symbol string, initialLiquidity *big.Int) ContractsConfigurator L2VaultsConfigurator L2RolesConfigurator @@ -387,6 +389,15 @@ func (c *l2Configurator) WithEIP1559Denominator(value uint64) { c.builder.intent.Chains[c.chainIndex].Eip1559Denominator = value } +func (c *l2Configurator) WithCustomGasToken(enabled bool, name, symbol string, initialLiquidity *big.Int) { + c.builder.intent.Chains[c.chainIndex].CustomGasToken = state.CustomGasToken{ + Enabled: enabled, + Name: name, + Symbol: symbol, + InitialLiquidity: (*hexutil.Big)(initialLiquidity), + } +} + func (c *l2Configurator) WithEIP1559Elasticity(value uint64) { c.builder.intent.Chains[c.chainIndex].Eip1559Elasticity = value } diff --git a/op-e2e/e2eutils/intentbuilder/builder_test.go b/op-e2e/e2eutils/intentbuilder/builder_test.go index ecf7e19d30738..fa7389dd911d3 100644 --- a/op-e2e/e2eutils/intentbuilder/builder_test.go +++ b/op-e2e/e2eutils/intentbuilder/builder_test.go @@ -2,6 +2,7 @@ package intentbuilder import ( "encoding/json" + "math/big" "net/url" "testing" @@ -69,6 +70,7 @@ func TestBuilder(t *testing.T) { require.Equal(t, eth.ChainIDFromUInt64(420), l2Config.ChainID()) l2Config.WithBlockTime(2) l2Config.WithL1StartBlockHash(common.HexToHash("0x5678")) + l2Config.WithCustomGasToken(false, "", "", (*big.Int)(big.NewInt(0))) // Test ContractsConfigurator methods l2Config.WithL1ContractsLocator("http://l1.example.com") @@ -161,6 +163,12 @@ func TestBuilder(t *testing.T) { GasLimit: standard.GasLimit, OperatorFeeScalar: 100, OperatorFeeConstant: 200, + CustomGasToken: state.CustomGasToken{ + Enabled: false, + Name: "", + Symbol: "", + InitialLiquidity: (*hexutil.Big)(big.NewInt(0)), + }, DeployOverrides: map[string]any{ "l2BlockTime": uint64(2), "l2GenesisRegolithTimeOffset": hexutil.Uint64(0), diff --git a/packages/contracts-bedrock/deploy-config/hardhat.json b/packages/contracts-bedrock/deploy-config/hardhat.json index 736991c93ba26..d330d1345955f 100644 --- a/packages/contracts-bedrock/deploy-config/hardhat.json +++ b/packages/contracts-bedrock/deploy-config/hardhat.json @@ -62,5 +62,9 @@ "daChallengeWindow": 100, "daResolveWindow": 100, "daBondSize": 1000, - "daResolverRefundPercentage": 50 + "daResolverRefundPercentage": 50, + "useCustomGasToken": false, + "gasPayingTokenName": "", + "gasPayingTokenSymbol": "", + "nativeAssetLiquidityAmount": null } diff --git a/packages/contracts-bedrock/deploy-config/internal-devnet.json b/packages/contracts-bedrock/deploy-config/internal-devnet.json index 643c507dc86d5..e74050bb76c70 100644 --- a/packages/contracts-bedrock/deploy-config/internal-devnet.json +++ b/packages/contracts-bedrock/deploy-config/internal-devnet.json @@ -38,5 +38,9 @@ "eip1559Elasticity": 10, "systemConfigStartBlock": 8364212, "requiredProtocolVersion": "0x0000000000000000000000000000000000000000000000000000000000000000", - "recommendedProtocolVersion": "0x0000000000000000000000000000000000000000000000000000000000000000" + "recommendedProtocolVersion": "0x0000000000000000000000000000000000000000000000000000000000000000", + "useCustomGasToken": false, + "gasPayingTokenName": "", + "gasPayingTokenSymbol": "", + "nativeAssetLiquidityAmount": null } diff --git a/packages/contracts-bedrock/deploy-config/mainnet.json b/packages/contracts-bedrock/deploy-config/mainnet.json index cd217ba111538..ae9b04ec3a464 100644 --- a/packages/contracts-bedrock/deploy-config/mainnet.json +++ b/packages/contracts-bedrock/deploy-config/mainnet.json @@ -55,5 +55,9 @@ "proofMaturityDelaySeconds": 604800, "disputeGameFinalityDelaySeconds": 302400, "respectedGameType": 0, - "useFaultProofs": true + "useFaultProofs": true, + "useCustomGasToken": false, + "gasPayingTokenName": "", + "gasPayingTokenSymbol": "", + "nativeAssetLiquidityAmount": null } diff --git a/packages/contracts-bedrock/deploy-config/sepolia-devnet-0.json b/packages/contracts-bedrock/deploy-config/sepolia-devnet-0.json index 2392dcb9281a5..78575879084bf 100644 --- a/packages/contracts-bedrock/deploy-config/sepolia-devnet-0.json +++ b/packages/contracts-bedrock/deploy-config/sepolia-devnet-0.json @@ -79,5 +79,9 @@ "useFaultProofs": true, "fundDevAccounts": false, "requiredProtocolVersion": "0x0000000000000000000000000000000000000005000000000000000000000000", - "recommendedProtocolVersion": "0x0000000000000000000000000000000000000005000000000000000000000000" + "recommendedProtocolVersion": "0x0000000000000000000000000000000000000005000000000000000000000000", + "useCustomGasToken": false, + "gasPayingTokenName": "", + "gasPayingTokenSymbol": "", + "nativeAssetLiquidityAmount": null } diff --git a/packages/contracts-bedrock/deploy-config/sepolia.json b/packages/contracts-bedrock/deploy-config/sepolia.json index 12a1cc392c803..22ad35b14806b 100644 --- a/packages/contracts-bedrock/deploy-config/sepolia.json +++ b/packages/contracts-bedrock/deploy-config/sepolia.json @@ -54,5 +54,9 @@ "proofMaturityDelaySeconds": 604800, "disputeGameFinalityDelaySeconds": 302400, "respectedGameType": 0, - "useFaultProofs": true + "useFaultProofs": true, + "useCustomGasToken": false, + "gasPayingTokenName": "", + "gasPayingTokenSymbol": "", + "nativeAssetLiquidityAmount": null } diff --git a/packages/contracts-bedrock/interfaces/L1/IL1CrossDomainMessenger.sol b/packages/contracts-bedrock/interfaces/L1/IL1CrossDomainMessenger.sol index 75d61233ae242..adb75a42b7cbd 100644 --- a/packages/contracts-bedrock/interfaces/L1/IL1CrossDomainMessenger.sol +++ b/packages/contracts-bedrock/interfaces/L1/IL1CrossDomainMessenger.sol @@ -8,15 +8,10 @@ import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { IProxyAdminOwnedBase } from "interfaces/L1/IProxyAdminOwnedBase.sol"; interface IL1CrossDomainMessenger is ICrossDomainMessenger, IProxyAdminOwnedBase { - error ReinitializableBase_ZeroInitVersion(); function PORTAL() external view returns (IOptimismPortal); - function initialize( - ISystemConfig _systemConfig, - IOptimismPortal _portal - ) - external; + function initialize(ISystemConfig _systemConfig, IOptimismPortal _portal) external; function initVersion() external view returns (uint8); function portal() external view returns (IOptimismPortal); function systemConfig() external view returns (ISystemConfig); diff --git a/packages/contracts-bedrock/interfaces/L1/IL1StandardBridge.sol b/packages/contracts-bedrock/interfaces/L1/IL1StandardBridge.sol index 0e22bb9b9c45c..bca0a79f56287 100644 --- a/packages/contracts-bedrock/interfaces/L1/IL1StandardBridge.sol +++ b/packages/contracts-bedrock/interfaces/L1/IL1StandardBridge.sol @@ -8,7 +8,6 @@ import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { IProxyAdminOwnedBase } from "interfaces/L1/IProxyAdminOwnedBase.sol"; interface IL1StandardBridge is IStandardBridge, IProxyAdminOwnedBase { - error ReinitializableBase_ZeroInitVersion(); event ERC20DepositInitiated( @@ -67,11 +66,7 @@ interface IL1StandardBridge is IStandardBridge, IProxyAdminOwnedBase { ) external payable; - function initialize( - ICrossDomainMessenger _messenger, - ISystemConfig _systemConfig - ) - external; + function initialize(ICrossDomainMessenger _messenger, ISystemConfig _systemConfig) external; function l2TokenBridge() external view returns (address); function systemConfig() external view returns (ISystemConfig); function version() external view returns (string memory); diff --git a/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol b/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol index 987450481d6b6..849249ab269d6 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol @@ -21,6 +21,7 @@ interface IOptimismPortal2 is IProxyAdminOwnedBase { error OptimismPortal_BadTarget(); error OptimismPortal_CallPaused(); error OptimismPortal_CalldataTooLarge(); + error OptimismPortal_NotAllowedOnCGTMode(); error OptimismPortal_GasEstimation(); error OptimismPortal_GasLimitTooLow(); error OptimismPortal_ImproperDisputeGame(); @@ -70,11 +71,7 @@ interface IOptimismPortal2 is IProxyAdminOwnedBase { external; function finalizedWithdrawals(bytes32) external view returns (bool); function guardian() external view returns (address); - function initialize( - ISystemConfig _systemConfig, - IAnchorStateRegistry _anchorStateRegistry - ) - external; + function initialize(ISystemConfig _systemConfig, IAnchorStateRegistry _anchorStateRegistry) external; function initVersion() external view returns (uint8); function l2Sender() external view returns (address); function minimumGasLimit(uint64 _byteCount) external pure returns (uint64); diff --git a/packages/contracts-bedrock/interfaces/L1/ISystemConfig.sol b/packages/contracts-bedrock/interfaces/L1/ISystemConfig.sol index 4abb69a1c1ab1..bc45b690ff846 100644 --- a/packages/contracts-bedrock/interfaces/L1/ISystemConfig.sol +++ b/packages/contracts-bedrock/interfaces/L1/ISystemConfig.sol @@ -98,6 +98,7 @@ interface ISystemConfig is IProxyAdminOwnedBase { function guardian() external view returns (address); function setFeature(bytes32 _feature, bool _enabled) external; function isFeatureEnabled(bytes32) external view returns (bool); + function isCustomGasToken() external view returns (bool); function __constructor__() external; } diff --git a/packages/contracts-bedrock/interfaces/L2/IL1BlockCGT.sol b/packages/contracts-bedrock/interfaces/L2/IL1BlockCGT.sol new file mode 100644 index 0000000000000..1501b74fc3cd8 --- /dev/null +++ b/packages/contracts-bedrock/interfaces/L2/IL1BlockCGT.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IL1BlockCGT { + function DEPOSITOR_ACCOUNT() external pure returns (address addr_); + function number() external view returns (uint64); + function timestamp() external view returns (uint64); + function basefee() external view returns (uint256); + function hash() external view returns (bytes32); + function sequenceNumber() external view returns (uint64); + function blobBaseFeeScalar() external view returns (uint32); + function baseFeeScalar() external view returns (uint32); + function batcherHash() external view returns (bytes32); + function l1FeeOverhead() external view returns (uint256); + function l1FeeScalar() external view returns (uint256); + function blobBaseFee() external view returns (uint256); + function operatorFeeConstant() external view returns (uint64); + function operatorFeeScalar() external view returns (uint32); + function version() external pure returns (string memory); + function isCustomGasToken() external view returns (bool isCustom_); + function gasPayingTokenName() external view returns (string memory name_); + function gasPayingTokenSymbol() external view returns (string memory symbol_); + function setL1BlockValues( + uint64 _number, + uint64 _timestamp, + uint256 _basefee, + bytes32 _hash, + uint64 _sequenceNumber, + bytes32 _batcherHash, + uint256 _l1FeeOverhead, + uint256 _l1FeeScalar + ) + external; + function setL1BlockValuesEcotone() external; + function setL1BlockValuesIsthmus() external; + function setCustomGasToken() external; + function gasPayingToken() external view returns (address, uint8); + + function __constructor__() external; +} diff --git a/packages/contracts-bedrock/interfaces/L2/IL2ToL1MessagePasserCGT.sol b/packages/contracts-bedrock/interfaces/L2/IL2ToL1MessagePasserCGT.sol new file mode 100644 index 0000000000000..cdb94f21d478b --- /dev/null +++ b/packages/contracts-bedrock/interfaces/L2/IL2ToL1MessagePasserCGT.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IL2ToL1MessagePasserCGT { + error L2ToL1MessagePasserCGT_NotAllowedOnCGTMode(); + + event MessagePassed( + uint256 indexed nonce, + address indexed sender, + address indexed target, + uint256 value, + uint256 gasLimit, + bytes data, + bytes32 withdrawalHash + ); + event WithdrawerBalanceBurnt(uint256 indexed amount); + + receive() external payable; + + function MESSAGE_VERSION() external view returns (uint16); + function burn() external; + function initiateWithdrawal(address _target, uint256 _gasLimit, bytes memory _data) external payable; + function messageNonce() external view returns (uint256); + function sentMessages(bytes32) external view returns (bool); + function version() external view returns (string memory); + + function __constructor__() external; +} diff --git a/packages/contracts-bedrock/interfaces/L2/ILiquidityController.sol b/packages/contracts-bedrock/interfaces/L2/ILiquidityController.sol new file mode 100644 index 0000000000000..2039de56a67d7 --- /dev/null +++ b/packages/contracts-bedrock/interfaces/L2/ILiquidityController.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { ISemver } from "interfaces/universal/ISemver.sol"; + +interface ILiquidityController is ISemver { + error LiquidityController_Unauthorized(); + + event Initialized(uint8 version); + + event MinterAuthorized(address indexed minter); + event MinterDeauthorized(address indexed minter); + event LiquidityMinted(address indexed minter, address indexed to, uint256 amount); + event LiquidityBurned(address indexed minter, uint256 amount); + + function authorizeMinter(address _minter) external; + function deauthorizeMinter(address _minter) external; + function mint(address _to, uint256 _amount) external; + function burn() external payable; + function minters(address) external view returns (bool); + function gasPayingTokenName() external view returns (string memory); + function gasPayingTokenSymbol() external view returns (string memory); + function initialize(string memory _gasPayingTokenName, string memory _gasPayingTokenSymbol) external; + + function __constructor__() external; +} diff --git a/packages/contracts-bedrock/interfaces/L2/INativeAssetLiquidity.sol b/packages/contracts-bedrock/interfaces/L2/INativeAssetLiquidity.sol new file mode 100644 index 0000000000000..fc343f4cba1e6 --- /dev/null +++ b/packages/contracts-bedrock/interfaces/L2/INativeAssetLiquidity.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { ISemver } from "interfaces/universal/ISemver.sol"; + +interface INativeAssetLiquidity is ISemver { + error NativeAssetLiquidity_Unauthorized(); + error NativeAssetLiquidity_InsufficientBalance(); + + event LiquidityDeposited(address indexed caller, uint256 value); + event LiquidityWithdrawn(address indexed caller, uint256 value); + + function deposit() external payable; + function withdraw(uint256 _amount) external; +} diff --git a/packages/contracts-bedrock/scripts/L2Genesis.s.sol b/packages/contracts-bedrock/scripts/L2Genesis.s.sol index de75c6b99cf1e..244cdc4c409fc 100644 --- a/packages/contracts-bedrock/scripts/L2Genesis.s.sol +++ b/packages/contracts-bedrock/scripts/L2Genesis.s.sol @@ -30,6 +30,8 @@ import { ICrossDomainMessenger } from "interfaces/universal/ICrossDomainMessenge import { IL2CrossDomainMessenger } from "interfaces/L2/IL2CrossDomainMessenger.sol"; import { IGasPriceOracle } from "interfaces/L2/IGasPriceOracle.sol"; import { IL1Block } from "interfaces/L2/IL1Block.sol"; +import { ILiquidityController } from "interfaces/L2/ILiquidityController.sol"; +import { IL1BlockCGT } from "interfaces/L2/IL1BlockCGT.sol"; /// @title L2Genesis /// @notice Generates the genesis state for the L2 network. @@ -60,6 +62,10 @@ contract L2Genesis is Script { bool deployCrossL2Inbox; bool enableGovernance; bool fundDevAccounts; + bool useCustomGasToken; + string gasPayingTokenName; + string gasPayingTokenSymbol; + uint256 nativeAssetLiquidityAmount; } using ForkUtils for Fork; @@ -118,6 +124,7 @@ contract L2Genesis is Script { if (_input.fundDevAccounts) { fundDevAccounts(); } + vm.stopPrank(); vm.deal(deployer, 0); vm.resetNonce(deployer); @@ -195,7 +202,8 @@ contract L2Genesis is Script { vm.etch(addr, code); EIP1967Helper.setAdmin(addr, Predeploys.PROXY_ADMIN); - if (Predeploys.isSupportedPredeploy(addr, _input.fork, _input.deployCrossL2Inbox)) { + if (Predeploys.isSupportedPredeploy(addr, _input.fork, _input.deployCrossL2Inbox, _input.useCustomGasToken)) + { address implementation = Predeploys.predeployToCodeNamespace(addr); EIP1967Helper.setImplementation(addr, implementation); } @@ -219,8 +227,8 @@ contract L2Genesis is Script { setOptimismMintableERC20Factory(); // 12 setL1BlockNumber(); // 13 setL2ERC721Bridge(_input.l1ERC721BridgeProxy); // 14 - setL1Block(); // 15 - setL2ToL1MessagePasser(); // 16 + setL1Block(_input.useCustomGasToken); // 15 + setL2ToL1MessagePasser(_input.useCustomGasToken); // 16 setOptimismMintableERC721Factory(_input); // 17 setProxyAdmin(_input); // 18 setBaseFeeVault(_input); // 19 @@ -236,6 +244,10 @@ contract L2Genesis is Script { } setL2ToL2CrossDomainMessenger(); // 23 } + if (_input.useCustomGasToken) { + setLiquidityController(_input); // 29 + setNativeAssetLiquidity(_input); // 2A + } } function setInteropPredeployProxies() internal { } @@ -252,8 +264,14 @@ contract L2Genesis is Script { vm.store(impl, _ownerSlot, bytes32(uint256(uint160(_input.opChainProxyAdminOwner)))); } - function setL2ToL1MessagePasser() internal { - _setImplementationCode(Predeploys.L2_TO_L1_MESSAGE_PASSER); + function setL2ToL1MessagePasser(bool _useCustomGasToken) internal { + if (_useCustomGasToken) { + string memory cname = "L2ToL1MessagePasserCGT"; + address impl = Predeploys.predeployToCodeNamespace(Predeploys.L2_TO_L1_MESSAGE_PASSER); + vm.etch(impl, vm.getDeployedCode(string.concat(cname, ".sol:", cname))); + } else { + _setImplementationCode(Predeploys.L2_TO_L1_MESSAGE_PASSER); + } } /// @notice This predeploy is following the safety invariant #1. @@ -289,6 +307,12 @@ contract L2Genesis is Script { /// @notice This predeploy is following the safety invariant #2, function setSequencerFeeVault(Input memory _input) internal { + Types.WithdrawalNetwork withdrawalNetwork = Types.WithdrawalNetwork(_input.sequencerFeeVaultWithdrawalNetwork); + + if (_input.useCustomGasToken && withdrawalNetwork == Types.WithdrawalNetwork.L1) { + revert("SequencerFeeVault: withdrawalNetwork type cannot be L1 when custom gas token is enabled"); + } + ISequencerFeeVault vault = ISequencerFeeVault( DeployUtils.create1({ _name: "SequencerFeeVault", @@ -298,7 +322,7 @@ contract L2Genesis is Script { ( _input.sequencerFeeVaultRecipient, _input.sequencerFeeVaultMinimumWithdrawalAmount, - Types.WithdrawalNetwork(_input.sequencerFeeVaultWithdrawalNetwork) + withdrawalNetwork ) ) ) @@ -346,10 +370,20 @@ contract L2Genesis is Script { } /// @notice This predeploy is following the safety invariant #1. - function setL1Block() internal { - // Note: L1 block attributes are set to 0. - // Before the first user-tx the state is overwritten with actual L1 attributes. - _setImplementationCode(Predeploys.L1_BLOCK_ATTRIBUTES); + function setL1Block(bool _useCustomGasToken) internal { + if (_useCustomGasToken) { + // Set the implementation code for L1BlockCGT + string memory cname = "L1BlockCGT"; + address impl = Predeploys.predeployToCodeNamespace(Predeploys.L1_BLOCK_ATTRIBUTES); + vm.etch(impl, vm.getDeployedCode(string.concat(cname, ".sol:", cname))); + + // Set the custom gas token flag + vm.startPrank(IL1BlockCGT(Predeploys.L1_BLOCK_ATTRIBUTES).DEPOSITOR_ACCOUNT()); + IL1BlockCGT(Predeploys.L1_BLOCK_ATTRIBUTES).setCustomGasToken(); + vm.stopPrank(); + } else { + _setImplementationCode(Predeploys.L1_BLOCK_ATTRIBUTES); + } } /// @notice This predeploy is following the safety invariant #1. @@ -381,17 +415,19 @@ contract L2Genesis is Script { /// @notice This predeploy is following the safety invariant #2. function setBaseFeeVault(Input memory _input) internal { + Types.WithdrawalNetwork withdrawalNetwork = Types.WithdrawalNetwork(_input.baseFeeVaultWithdrawalNetwork); + + if (_input.useCustomGasToken && withdrawalNetwork == Types.WithdrawalNetwork.L1) { + revert("BaseFeeVault: withdrawalNetwork type cannot be L1 when custom gas token is enabled"); + } + IBaseFeeVault vault = IBaseFeeVault( DeployUtils.create1({ _name: "BaseFeeVault", _args: DeployUtils.encodeConstructor( abi.encodeCall( IBaseFeeVault.__constructor__, - ( - _input.baseFeeVaultRecipient, - _input.baseFeeVaultMinimumWithdrawalAmount, - Types.WithdrawalNetwork(_input.baseFeeVaultWithdrawalNetwork) - ) + (_input.baseFeeVaultRecipient, _input.baseFeeVaultMinimumWithdrawalAmount, withdrawalNetwork) ) ) }) @@ -407,17 +443,19 @@ contract L2Genesis is Script { /// @notice This predeploy is following the safety invariant #2. function setL1FeeVault(Input memory _input) internal { + Types.WithdrawalNetwork withdrawalNetwork = Types.WithdrawalNetwork(_input.l1FeeVaultWithdrawalNetwork); + + if (_input.useCustomGasToken && withdrawalNetwork == Types.WithdrawalNetwork.L1) { + revert("L1FeeVault: withdrawalNetwork type cannot be L1 when custom gas token is enabled"); + } + IL1FeeVault vault = IL1FeeVault( DeployUtils.create1({ _name: "L1FeeVault", _args: DeployUtils.encodeConstructor( abi.encodeCall( IL1FeeVault.__constructor__, - ( - _input.l1FeeVaultRecipient, - _input.l1FeeVaultMinimumWithdrawalAmount, - Types.WithdrawalNetwork(_input.l1FeeVaultWithdrawalNetwork) - ) + (_input.l1FeeVaultRecipient, _input.l1FeeVaultMinimumWithdrawalAmount, withdrawalNetwork) ) ) }) @@ -546,6 +584,32 @@ contract L2Genesis is Script { _setImplementationCode(Predeploys.SUPERCHAIN_TOKEN_BRIDGE); } + /// @notice This predeploy is following the safety invariant #1. + function setLiquidityController(Input memory _input) internal { + address impl = _setImplementationCode(Predeploys.LIQUIDITY_CONTROLLER); + + ILiquidityController(impl).initialize({ _gasPayingTokenName: "", _gasPayingTokenSymbol: "" }); + + ILiquidityController(Predeploys.LIQUIDITY_CONTROLLER).initialize({ + _gasPayingTokenName: _input.gasPayingTokenName, + _gasPayingTokenSymbol: _input.gasPayingTokenSymbol + }); + } + + /// @notice This predeploy is following the safety invariant #1. + /// This contract has no initializer. + function setNativeAssetLiquidity(Input memory _input) internal { + _setImplementationCode(Predeploys.NATIVE_ASSET_LIQUIDITY); + + require( + _input.nativeAssetLiquidityAmount <= type(uint248).max, + "L2Genesis: native asset liquidity amount must be less than or equal to type(uint248).max" + ); + + // Pre-fund the liquidity contract with the specified amount + vm.deal(Predeploys.NATIVE_ASSET_LIQUIDITY, _input.nativeAssetLiquidityAmount); + } + /// @notice Sets all the preinstalls. function setPreinstalls() internal { address tmpSetPreinstalls = address(uint160(uint256(keccak256("SetPreinstalls")))); diff --git a/packages/contracts-bedrock/scripts/checks/test-validation/exclusions.toml b/packages/contracts-bedrock/scripts/checks/test-validation/exclusions.toml index 8adb91a6091be..0150b824b7c81 100644 --- a/packages/contracts-bedrock/scripts/checks/test-validation/exclusions.toml +++ b/packages/contracts-bedrock/scripts/checks/test-validation/exclusions.toml @@ -1,4 +1,4 @@ - # Test validation exclusions configuration +# Test validation exclusions configuration # This file contains lists of paths and test names that should be excluded # from various validation checks in the test validation script. @@ -91,4 +91,5 @@ contracts = [ "OptimismPortal2_MigrateLiquidity_Test", # Interop tests hosted in the OptimismPortal2 test file "OptimismPortal2_MigrateToSuperRoots_Test", # Interop tests hosted in the OptimismPortal2 test file "OptimismPortal2_UpgradeInterop_Test", # Interop tests hosted in the OptimismPortal2 test file + "L1Block_SetCustomGasToken_Test", # Custom gas token tests hosted in the L1Block test file ] diff --git a/packages/contracts-bedrock/scripts/deploy/DeployConfig.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployConfig.s.sol index 928faad8129bc..6a5808b91f56b 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployConfig.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployConfig.s.sol @@ -76,6 +76,12 @@ contract DeployConfig is Script { uint256 public daBondSize; uint256 public daResolverRefundPercentage; + // Custom Gas Token Configuration + bool public useCustomGasToken; + string public gasPayingTokenName; + string public gasPayingTokenSymbol; + uint256 public nativeAssetLiquidityAmount; + // V2 Dispute Game Configuration uint256 public faultGameV2MaxGameDepth; uint256 public faultGameV2SplitDepth; @@ -128,6 +134,10 @@ contract DeployConfig is Script { l2GenesisBlockGasLimit = stdJson.readUint(_json, "$.l2GenesisBlockGasLimit"); basefeeScalar = uint32(_readOr(_json, "$.gasPriceOracleBaseFeeScalar", 1368)); blobbasefeeScalar = uint32(_readOr(_json, "$.gasPriceOracleBlobBaseFeeScalar", 810949)); + useCustomGasToken = _readOr(_json, "$.useCustomGasToken", false); + gasPayingTokenName = _readOr(_json, "$.gasPayingTokenName", ""); + gasPayingTokenSymbol = _readOr(_json, "$.gasPayingTokenSymbol", ""); + nativeAssetLiquidityAmount = _readOr(_json, "$.nativeAssetLiquidityAmount", 0); enableGovernance = _readOr(_json, "$.enableGovernance", false); systemConfigStartBlock = stdJson.readUint(_json, "$.systemConfigStartBlock"); @@ -236,6 +246,41 @@ contract DeployConfig is Script { useUpgradedFork = _useUpgradedFork; } + /// @notice Allow the `useCustomGasToken` config to be overridden in testing environments + function setUseCustomGasToken(bool _useCustomGasToken) public { + useCustomGasToken = _useCustomGasToken; + } + + /// @notice Allow the `gasPayingTokenName` config to be overridden in testing environments + function setGasPayingTokenName(string memory _gasPayingTokenName) public { + gasPayingTokenName = _gasPayingTokenName; + } + + /// @notice Allow the `gasPayingTokenSymbol` config to be overridden in testing environments + function setGasPayingTokenSymbol(string memory _gasPayingTokenSymbol) public { + gasPayingTokenSymbol = _gasPayingTokenSymbol; + } + + /// @notice Allow the `nativeAssetLiquidityAmount` config to be overridden in testing environments + function setNativeAssetLiquidityAmount(uint256 _nativeAssetLiquidityAmount) public { + nativeAssetLiquidityAmount = _nativeAssetLiquidityAmount; + } + + /// @notice Allow the `baseFeeVaultWithdrawalNetwork` config to be overridden in testing environments + function setBaseFeeVaultWithdrawalNetwork(uint256 _baseFeeVaultWithdrawalNetwork) public { + baseFeeVaultWithdrawalNetwork = _baseFeeVaultWithdrawalNetwork; + } + + /// @notice Allow the `l1FeeVaultWithdrawalNetwork` config to be overridden in testing environments + function setL1FeeVaultWithdrawalNetwork(uint256 _l1FeeVaultWithdrawalNetwork) public { + l1FeeVaultWithdrawalNetwork = _l1FeeVaultWithdrawalNetwork; + } + + /// @notice Allow the `sequencerFeeVaultWithdrawalNetwork` config to be overridden in testing environments + function setSequencerFeeVaultWithdrawalNetwork(uint256 _sequencerFeeVaultWithdrawalNetwork) public { + sequencerFeeVaultWithdrawalNetwork = _sequencerFeeVaultWithdrawalNetwork; + } + function latestGenesisFork() internal view returns (Fork) { if (l2GenesisJovianTimeOffset == 0) { return Fork.JOVIAN; diff --git a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol index 050dfed7695b9..77326a62a668b 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol @@ -107,13 +107,19 @@ contract DeployOPChainInput is BaseDeployIO { } function set(bytes4 _sel, bytes32 _value) public { - if (_sel == this.disputeAbsolutePrestate.selector) _disputeAbsolutePrestate = Claim.wrap(_value); - else revert("DeployImplementationsInput: unknown selector"); + if (_sel == this.disputeAbsolutePrestate.selector) { + _disputeAbsolutePrestate = Claim.wrap(_value); + } else { + revert("DeployOPChainInput: unknown selector"); + } } function set(bytes4 _sel, bool _value) public { - if (_sel == this.allowCustomDisputeParameters.selector) _allowCustomDisputeParameters = _value; - else revert("DeployOPChainInput: unknown selector"); + if (_sel == this.allowCustomDisputeParameters.selector) { + _allowCustomDisputeParameters = _value; + } else { + revert("DeployOPChainInput: unknown selector"); + } } function opChainProxyAdminOwner() public view returns (address) { diff --git a/packages/contracts-bedrock/scripts/libraries/Config.sol b/packages/contracts-bedrock/scripts/libraries/Config.sol index d21a99a47b0a5..7fa68ab705f77 100644 --- a/packages/contracts-bedrock/scripts/libraries/Config.sol +++ b/packages/contracts-bedrock/scripts/libraries/Config.sol @@ -245,4 +245,9 @@ library Config { function devFeatureCannonKona() internal view returns (bool) { return vm.envOr("DEV_FEATURE__CANNON_KONA", false); } + + /// @notice Returns true if the development feature custom gas token is enabled. + function devFeatureCustomGasToken() internal view returns (bool) { + return vm.envOr("DEV_FEATURE__CUSTOM_GAS_TOKEN", false); + } } diff --git a/packages/contracts-bedrock/snapshots/abi/L1Block.json b/packages/contracts-bedrock/snapshots/abi/L1Block.json index 153d2676cf5bb..cf5e9a890de69 100644 --- a/packages/contracts-bedrock/snapshots/abi/L1Block.json +++ b/packages/contracts-bedrock/snapshots/abi/L1Block.json @@ -105,7 +105,7 @@ "type": "string" } ], - "stateMutability": "pure", + "stateMutability": "view", "type": "function" }, { @@ -118,7 +118,7 @@ "type": "string" } ], - "stateMutability": "pure", + "stateMutability": "view", "type": "function" }, { @@ -144,7 +144,7 @@ "type": "bool" } ], - "stateMutability": "pure", + "stateMutability": "view", "type": "function" }, { diff --git a/packages/contracts-bedrock/snapshots/abi/L1BlockCGT.json b/packages/contracts-bedrock/snapshots/abi/L1BlockCGT.json new file mode 100644 index 0000000000000..1ece2bf86189b --- /dev/null +++ b/packages/contracts-bedrock/snapshots/abi/L1BlockCGT.json @@ -0,0 +1,323 @@ +[ + { + "inputs": [], + "name": "DEPOSITOR_ACCOUNT", + "outputs": [ + { + "internalType": "address", + "name": "addr_", + "type": "address" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "baseFeeScalar", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "basefee", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "batcherHash", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "blobBaseFee", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "blobBaseFeeScalar", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "gasPayingToken", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "gasPayingTokenName", + "outputs": [ + { + "internalType": "string", + "name": "name_", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "gasPayingTokenSymbol", + "outputs": [ + { + "internalType": "string", + "name": "symbol_", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "hash", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isCustomGasToken", + "outputs": [ + { + "internalType": "bool", + "name": "isCustom_", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "l1FeeOverhead", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "l1FeeScalar", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "number", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "operatorFeeConstant", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "operatorFeeScalar", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "sequenceNumber", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "setCustomGasToken", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "_number", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "_timestamp", + "type": "uint64" + }, + { + "internalType": "uint256", + "name": "_basefee", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "_hash", + "type": "bytes32" + }, + { + "internalType": "uint64", + "name": "_sequenceNumber", + "type": "uint64" + }, + { + "internalType": "bytes32", + "name": "_batcherHash", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "_l1FeeOverhead", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_l1FeeScalar", + "type": "uint256" + } + ], + "name": "setL1BlockValues", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "setL1BlockValuesEcotone", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "setL1BlockValuesIsthmus", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "timestamp", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/L2ToL1MessagePasser.json b/packages/contracts-bedrock/snapshots/abi/L2ToL1MessagePasser.json index 77e1cf7596b35..3638283520aab 100644 --- a/packages/contracts-bedrock/snapshots/abi/L2ToL1MessagePasser.json +++ b/packages/contracts-bedrock/snapshots/abi/L2ToL1MessagePasser.json @@ -88,7 +88,7 @@ "type": "string" } ], - "stateMutability": "view", + "stateMutability": "pure", "type": "function" }, { diff --git a/packages/contracts-bedrock/snapshots/abi/L2ToL1MessagePasserCGT.json b/packages/contracts-bedrock/snapshots/abi/L2ToL1MessagePasserCGT.json new file mode 100644 index 0000000000000..a8a57bea938ed --- /dev/null +++ b/packages/contracts-bedrock/snapshots/abi/L2ToL1MessagePasserCGT.json @@ -0,0 +1,161 @@ +[ + { + "stateMutability": "payable", + "type": "receive" + }, + { + "inputs": [], + "name": "MESSAGE_VERSION", + "outputs": [ + { + "internalType": "uint16", + "name": "", + "type": "uint16" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "burn", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_target", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_gasLimit", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "_data", + "type": "bytes" + } + ], + "name": "initiateWithdrawal", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "messageNonce", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "sentMessages", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "gasLimit", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "withdrawalHash", + "type": "bytes32" + } + ], + "name": "MessagePassed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "WithdrawerBalanceBurnt", + "type": "event" + }, + { + "inputs": [], + "name": "L2ToL1MessagePasserCGT_NotAllowedOnCGTMode", + "type": "error" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/LiquidityController.json b/packages/contracts-bedrock/snapshots/abi/LiquidityController.json new file mode 100644 index 0000000000000..3a61fc231c183 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/abi/LiquidityController.json @@ -0,0 +1,222 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_minter", + "type": "address" + } + ], + "name": "authorizeMinter", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "burn", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_minter", + "type": "address" + } + ], + "name": "deauthorizeMinter", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "gasPayingTokenName", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "gasPayingTokenSymbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "_gasPayingTokenName", + "type": "string" + }, + { + "internalType": "string", + "name": "_gasPayingTokenSymbol", + "type": "string" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "mint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "minters", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "minter", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "LiquidityBurned", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "minter", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "LiquidityMinted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "minter", + "type": "address" + } + ], + "name": "MinterAuthorized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "minter", + "type": "address" + } + ], + "name": "MinterDeauthorized", + "type": "event" + }, + { + "inputs": [], + "name": "LiquidityController_Unauthorized", + "type": "error" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/NativeAssetLiquidity.json b/packages/contracts-bedrock/snapshots/abi/NativeAssetLiquidity.json new file mode 100644 index 0000000000000..156ece4cc1361 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/abi/NativeAssetLiquidity.json @@ -0,0 +1,83 @@ +[ + { + "inputs": [], + "name": "deposit", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "withdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "LiquidityDeposited", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "LiquidityWithdrawn", + "type": "event" + }, + { + "inputs": [], + "name": "NativeAssetLiquidity_InsufficientBalance", + "type": "error" + }, + { + "inputs": [], + "name": "NativeAssetLiquidity_Unauthorized", + "type": "error" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json b/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json index 49ae551310b01..9d19d18dbe5db 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json @@ -824,6 +824,11 @@ "name": "OptimismPortal_NoReentrancy", "type": "error" }, + { + "inputs": [], + "name": "OptimismPortal_NotAllowedOnCGTMode", + "type": "error" + }, { "inputs": [], "name": "OptimismPortal_ProofNotOldEnough", diff --git a/packages/contracts-bedrock/snapshots/abi/SystemConfig.json b/packages/contracts-bedrock/snapshots/abi/SystemConfig.json index 10a956622f8f5..fcb9fc5921918 100644 --- a/packages/contracts-bedrock/snapshots/abi/SystemConfig.json +++ b/packages/contracts-bedrock/snapshots/abi/SystemConfig.json @@ -413,6 +413,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [], + "name": "isCustomGasToken", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index a18c5ddb5876f..10bf62a3a942f 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -20,16 +20,16 @@ "sourceCodeHash": "0xfca613b5d055ffc4c3cbccb0773ddb9030abedc1aa6508c9e2e7727cc0cd617b" }, "src/L1/OPContractsManager.sol:OPContractsManager": { - "initCodeHash": "0x90527840505f5c7a74e2c4492dda6fa7f73c90ef68fa5a36bed2ce874b2ea226", - "sourceCodeHash": "0xf022e035aedf6c7979160ef59d18dd9faa8450879eca55447858709c8c521eb0" + "initCodeHash": "0x6dfe2a337e7690287b571c360dc972101fb2c93ab17b2bcd2cb1ef5e6b858e59", + "sourceCodeHash": "0x4e2237a9a11581b3ea9800fe636e201f99d9e4166f026824dfd79fc8f2c1de11" }, "src/L1/OPContractsManagerStandardValidator.sol:OPContractsManagerStandardValidator": { "initCodeHash": "0xcc5dacb9e1c2b9395aa5f9c300f03c18af1ff5a9efd6a7ce4d5135dfbe7b1e2b", "sourceCodeHash": "0x0c63dc35ccf59cc583fbbfc0f2251ba95d026c11540f08806a48cb3934e73ae4" }, "src/L1/OptimismPortal2.sol:OptimismPortal2": { - "initCodeHash": "0x5bf576ea7f566e402a997204988471fc9b971410aa9dff8fe810b10baf6b7456", - "sourceCodeHash": "0xcde7f2a838d13bf3bd2140a686d0c31472b637ab84d18ee46963977f036f80eb" + "initCodeHash": "0x2c01bc6c0a55a1a27263224e05c1b28703ff85c61075bae7ab384b3043820ed2", + "sourceCodeHash": "0x16fb96f4d29a10d03b3b9c70edf56df51e97c2a1a3f0ba36aae79469b446ad5c" }, "src/L1/OptimismPortalInterop.sol:OptimismPortalInterop": { "initCodeHash": "0x087281cd2a48e882648c09fa90bfcca7487d222e16300f9372deba6b2b8ccfad", @@ -44,8 +44,8 @@ "sourceCodeHash": "0xbf344c4369b8cb00ec7a3108f72795747f3bc59ab5b37ac18cf21e72e2979dbf" }, "src/L1/SystemConfig.sol:SystemConfig": { - "initCodeHash": "0x6e1e3cf76f08916bf6a3aed2b68772bd5ade935db4f0f876e682dc7a586334fb", - "sourceCodeHash": "0x006e3560f8b2e17133eb10a116916798ddc4345a7b006f8504dab69e810adb1c" + "initCodeHash": "0x7c560a18cde0e6139c263d61fec2e8be53dc171ccd7f791b56e100bcd90237f8", + "sourceCodeHash": "0x04ef4b7b4248fb06b098370f3e12434aa16723c23efcef73b77bf07e3475c124" }, "src/L2/BaseFeeVault.sol:BaseFeeVault": { "initCodeHash": "0x9b664e3d84ad510091337b4aacaa494b142512e2f6f7fbcdb6210ed62ca9b885", @@ -64,8 +64,12 @@ "sourceCodeHash": "0x4351fe2ac1106c8c220b8cfe7839bc107c24d8084deb21259ac954f5a362725d" }, "src/L2/L1Block.sol:L1Block": { - "initCodeHash": "0xc35734387887a95f611888f3944546c6bcf82fd4c05dcdaa1e019779b628ad68", - "sourceCodeHash": "0x6e5349fd781d5f0127ff29ccea4d86a80240550cfa322364183a0f629abcb43e" + "initCodeHash": "0x03285c88bf59fd17004489269134abf740d8111b9cb55a6c542a5dc27121d988", + "sourceCodeHash": "0x2cad0fa9b950c9306eb7a77622486ebc9aa2c2530a4c167c460819dba4dbaf9a" + }, + "src/L2/L1BlockCGT.sol:L1BlockCGT": { + "initCodeHash": "0x4a12ae06f475c342268b0cd2c1fb8fe5f49210813eb6349b1cd6cd10ec9748c1", + "sourceCodeHash": "0x97b84b125df97ca4ad6fb1bd5c05998115970f37e71d7bccb5b902144fb8f8de" }, "src/L2/L1FeeVault.sol:L1FeeVault": { "initCodeHash": "0x9b664e3d84ad510091337b4aacaa494b142512e2f6f7fbcdb6210ed62ca9b885", @@ -88,13 +92,25 @@ "sourceCodeHash": "0xde724da82ecf3c96b330c2876a7285b6e2b933ac599241eaa3174c443ebbe33a" }, "src/L2/L2ToL1MessagePasser.sol:L2ToL1MessagePasser": { - "initCodeHash": "0x88f7b25f956eceeab9ad84c17e66cded6a1acbb933054ac2c8b336641f70f875", - "sourceCodeHash": "0x83396cbd12a0c5c02e09a4d99c4b62ab4e9d9eb762745e63283e2e818a78a39c" + "initCodeHash": "0xe30675ea6623cd7390dd2cd1e9a523c92c66956dfab86d06e318eb410cd1989b", + "sourceCodeHash": "0xdc7bd63134eeab163a635950f2afd16b59f40f9cf1306f2ed33ad661cc7b4962" + }, + "src/L2/L2ToL1MessagePasserCGT.sol:L2ToL1MessagePasserCGT": { + "initCodeHash": "0xf36eab44c41249b4d8ea95a21b70f1eae55b1a555384870c2f4ca306fa9121b6", + "sourceCodeHash": "0xec1736e67134e22ad9ceb0b8b6c116fd169637aa6729c05d9f0f4b02547aaac0" }, "src/L2/L2ToL2CrossDomainMessenger.sol:L2ToL2CrossDomainMessenger": { "initCodeHash": "0x975fd33a3a386310d54dbb01b56f3a6a8350f55a3b6bd7781e5ccc2166ddf2e6", "sourceCodeHash": "0xbea4229c5c6988243dbc7cf5a086ddd412fe1f2903b8e20d56699fec8de0c2c9" }, + "src/L2/LiquidityController.sol:LiquidityController": { + "initCodeHash": "0x19ce7a691d1f8631bfce9fea002c385c418fc69c2e9a9aa5d44ee0cf348c1026", + "sourceCodeHash": "0x87351013f5bcca917dfdd916b06b8ece6bd447acae146f004a33d88f59ce2487" + }, + "src/L2/NativeAssetLiquidity.sol:NativeAssetLiquidity": { + "initCodeHash": "0x04b176e5d484e54173a5644c833117c5fd9f055dce8678be9ec4cf07c0f01f00", + "sourceCodeHash": "0x79289174e875ead5a6290df9af1951d6c0ff0dc6809601e1c7a80110ceb8e945" + }, "src/L2/OperatorFeeVault.sol:OperatorFeeVault": { "initCodeHash": "0x3d8c0d7736e8767f2f797da1c20c5fe30bd7f48a4cf75f376290481ad7c0f91f", "sourceCodeHash": "0x2022fdb4e32769eb9446dab4aed4b8abb5261fd866f381cccfa7869df1a2adff" diff --git a/packages/contracts-bedrock/snapshots/storageLayout/L1BlockCGT.json b/packages/contracts-bedrock/snapshots/storageLayout/L1BlockCGT.json new file mode 100644 index 0000000000000..2c23f06367859 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/storageLayout/L1BlockCGT.json @@ -0,0 +1,93 @@ +[ + { + "bytes": "8", + "label": "number", + "offset": 0, + "slot": "0", + "type": "uint64" + }, + { + "bytes": "8", + "label": "timestamp", + "offset": 8, + "slot": "0", + "type": "uint64" + }, + { + "bytes": "32", + "label": "basefee", + "offset": 0, + "slot": "1", + "type": "uint256" + }, + { + "bytes": "32", + "label": "hash", + "offset": 0, + "slot": "2", + "type": "bytes32" + }, + { + "bytes": "8", + "label": "sequenceNumber", + "offset": 0, + "slot": "3", + "type": "uint64" + }, + { + "bytes": "4", + "label": "blobBaseFeeScalar", + "offset": 8, + "slot": "3", + "type": "uint32" + }, + { + "bytes": "4", + "label": "baseFeeScalar", + "offset": 12, + "slot": "3", + "type": "uint32" + }, + { + "bytes": "32", + "label": "batcherHash", + "offset": 0, + "slot": "4", + "type": "bytes32" + }, + { + "bytes": "32", + "label": "l1FeeOverhead", + "offset": 0, + "slot": "5", + "type": "uint256" + }, + { + "bytes": "32", + "label": "l1FeeScalar", + "offset": 0, + "slot": "6", + "type": "uint256" + }, + { + "bytes": "32", + "label": "blobBaseFee", + "offset": 0, + "slot": "7", + "type": "uint256" + }, + { + "bytes": "8", + "label": "operatorFeeConstant", + "offset": 0, + "slot": "8", + "type": "uint64" + }, + { + "bytes": "4", + "label": "operatorFeeScalar", + "offset": 8, + "slot": "8", + "type": "uint32" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/L2ToL1MessagePasserCGT.json b/packages/contracts-bedrock/snapshots/storageLayout/L2ToL1MessagePasserCGT.json new file mode 100644 index 0000000000000..09cc3b5440136 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/storageLayout/L2ToL1MessagePasserCGT.json @@ -0,0 +1,16 @@ +[ + { + "bytes": "32", + "label": "sentMessages", + "offset": 0, + "slot": "0", + "type": "mapping(bytes32 => bool)" + }, + { + "bytes": "30", + "label": "msgNonce", + "offset": 0, + "slot": "1", + "type": "uint240" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/LiquidityController.json b/packages/contracts-bedrock/snapshots/storageLayout/LiquidityController.json new file mode 100644 index 0000000000000..d817ac7953d16 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/storageLayout/LiquidityController.json @@ -0,0 +1,37 @@ +[ + { + "bytes": "1", + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "uint8" + }, + { + "bytes": "1", + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "bool" + }, + { + "bytes": "32", + "label": "minters", + "offset": 0, + "slot": "1", + "type": "mapping(address => bool)" + }, + { + "bytes": "32", + "label": "gasPayingTokenName", + "offset": 0, + "slot": "2", + "type": "string" + }, + { + "bytes": "32", + "label": "gasPayingTokenSymbol", + "offset": 0, + "slot": "3", + "type": "string" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/NativeAssetLiquidity.json b/packages/contracts-bedrock/snapshots/storageLayout/NativeAssetLiquidity.json new file mode 100644 index 0000000000000..0637a088a01e8 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/storageLayout/NativeAssetLiquidity.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/L1/OPContractsManager.sol b/packages/contracts-bedrock/src/L1/OPContractsManager.sol index 75f0cbb20b460..b0da7e4079a43 100644 --- a/packages/contracts-bedrock/src/L1/OPContractsManager.sol +++ b/packages/contracts-bedrock/src/L1/OPContractsManager.sol @@ -1084,6 +1084,12 @@ contract OPContractsManagerDeployer is OPContractsManagerBase { output.opChainProxyAdmin, address(output.systemConfigProxy), implementation.systemConfigImpl, data ); + // If the custom gas token feature was requested, enable the custom gas token feature in the SystemConfig + // contract. + if (isDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN)) { + output.systemConfigProxy.setFeature(Features.CUSTOM_GAS_TOKEN, true); + } + // If the interop feature was requested, enable the ETHLockbox feature in the SystemConfig // contract. Only other way to get the ETHLockbox feature as of u16a is to have already had // the ETHLockbox in U16 and then upgrade to U16a. @@ -1296,6 +1302,21 @@ contract OPContractsManagerDeployer is OPContractsManagerBase { (IResourceMetering.ResourceConfig memory referenceResourceConfig, ISystemConfig.Addresses memory opChainAddrs) = defaultSystemConfigParams(_input, _output); + return systemConfigInitializerData(_input, _superchainConfig, referenceResourceConfig, opChainAddrs); + } + + /// @notice Helper method for encoding the call data for the SystemConfig initializer. + function systemConfigInitializerData( + OPContractsManager.DeployInput memory _input, + ISuperchainConfig _superchainConfig, + IResourceMetering.ResourceConfig memory _referenceResourceConfig, + ISystemConfig.Addresses memory _opChainAddrs + ) + internal + view + virtual + returns (bytes memory) + { return abi.encodeCall( ISystemConfig.initialize, ( @@ -1305,9 +1326,9 @@ contract OPContractsManagerDeployer is OPContractsManagerBase { bytes32(uint256(uint160(_input.roles.batcher))), // batcherHash _input.gasLimit, _input.roles.unsafeBlockSigner, - referenceResourceConfig, + _referenceResourceConfig, chainIdToBatchInboxAddress(_input.l2ChainId), - opChainAddrs, + _opChainAddrs, _input.l2ChainId, _superchainConfig ) @@ -1802,9 +1823,9 @@ contract OPContractsManager is ISemver { // -------- Constants and Variables -------- - /// @custom:semver 3.5.0 + /// @custom:semver 3.6.0 function version() public pure virtual returns (string memory) { - return "3.5.0"; + return "3.6.0"; } OPContractsManagerGameTypeAdder public immutable opcmGameTypeAdder; diff --git a/packages/contracts-bedrock/src/L1/OptimismPortal2.sol b/packages/contracts-bedrock/src/L1/OptimismPortal2.sol index 071bf55a88922..7024fad39c7d8 100644 --- a/packages/contracts-bedrock/src/L1/OptimismPortal2.sol +++ b/packages/contracts-bedrock/src/L1/OptimismPortal2.sol @@ -168,6 +168,9 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase /// @notice Thrown when the portal is paused. error OptimismPortal_CallPaused(); + /// @notice Thrown when a CGT withdrawal is not allowed. + error OptimismPortal_NotAllowedOnCGTMode(); + /// @notice Thrown when a gas estimation transaction is being executed. error OptimismPortal_GasEstimation(); @@ -205,9 +208,9 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase error OptimismPortal_InvalidLockboxState(); /// @notice Semantic version. - /// @custom:semver 5.1.1 + /// @custom:semver 5.2.0 function version() public pure virtual returns (string memory) { - return "5.1.1"; + return "5.2.0"; } /// @param _proofMaturityDelaySeconds The proof maturity delay in seconds. @@ -342,14 +345,19 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase // Cannot prove withdrawal transactions while the system is paused. _assertNotPaused(); - // Fetch the dispute game proxy from the `DisputeGameFactory` contract. - (,, IDisputeGame disputeGameProxy) = disputeGameFactory().gameAtIndex(_disputeGameIndex); - // Make sure that the target address is safe. if (_isUnsafeTarget(_tx.target)) { revert OptimismPortal_BadTarget(); } + // Cannot prove withdrawal with value when custom gas token mode is enabled. + if (_isUsingCustomGasToken()) { + if (_tx.value > 0) revert OptimismPortal_NotAllowedOnCGTMode(); + } + + // Fetch the dispute game proxy from the `DisputeGameFactory` contract. + (,, IDisputeGame disputeGameProxy) = disputeGameFactory().gameAtIndex(_disputeGameIndex); + // Game must be a Proper Game. if (!anchorStateRegistry.isGameProper(disputeGameProxy)) { revert OptimismPortal_ImproperDisputeGame(); @@ -438,6 +446,11 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase // Cannot finalize withdrawal transactions while the system is paused. _assertNotPaused(); + // Cannot finalize withdrawal with value when custom gas token mode is enabled. + if (_isUsingCustomGasToken()) { + if (_tx.value > 0) revert OptimismPortal_NotAllowedOnCGTMode(); + } + // Make sure that the l2Sender has not yet been set. The l2Sender is set to a value other // than the default value when a withdrawal transaction is being finalized. This check is // a defacto reentrancy guard. @@ -560,6 +573,10 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase payable metered(_gasLimit) { + if (_isUsingCustomGasToken()) { + if (msg.value > 0) revert OptimismPortal_NotAllowedOnCGTMode(); + } + // If using ETHLockbox, lock the ETH in the ETHLockbox. if (_isUsingLockbox()) { if (msg.value > 0) ethLockbox.lockETH{ value: msg.value }(); @@ -614,6 +631,14 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase return systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX) && address(ethLockbox) != address(0); } + /// @notice Checks if the Custom Gas Token feature is enabled. + /// @return bool True if the Custom Gas Token feature is enabled. + function _isUsingCustomGasToken() internal view returns (bool) { + // NOTE: Chains are not supposed to enable Custom Gas Token (CGT) mode after initial deployment. + // Enabling CGT post-deployment is strongly discouraged and may lead to unexpected behavior. + return systemConfig.isFeatureEnabled(Features.CUSTOM_GAS_TOKEN); + } + /// @notice Asserts that the contract is not paused. function _assertNotPaused() internal view { if (paused()) { diff --git a/packages/contracts-bedrock/src/L1/SystemConfig.sol b/packages/contracts-bedrock/src/L1/SystemConfig.sol index 68b5dba6ef81c..2b6c0f0d77398 100644 --- a/packages/contracts-bedrock/src/L1/SystemConfig.sol +++ b/packages/contracts-bedrock/src/L1/SystemConfig.sol @@ -161,9 +161,9 @@ contract SystemConfig is ProxyAdminOwnedBase, OwnableUpgradeable, Reinitializabl error SystemConfig_InvalidFeatureState(); /// @notice Semantic version. - /// @custom:semver 3.10.0 + /// @custom:semver 3.11.0 function version() public pure virtual returns (string memory) { - return "3.10.0"; + return "3.11.0"; } /// @notice Constructs the SystemConfig contract. @@ -565,4 +565,11 @@ contract SystemConfig is ProxyAdminOwnedBase, OwnableUpgradeable, Reinitializabl function guardian() public view returns (address) { return superchainConfig.guardian(); } + + /// @custom:legacy + /// @notice Returns whether the custom gas token feature is enabled. + /// @return bool True if the custom gas token feature is enabled, false otherwise. + function isCustomGasToken() public view returns (bool) { + return isFeatureEnabled[Features.CUSTOM_GAS_TOKEN]; + } } diff --git a/packages/contracts-bedrock/src/L2/L1Block.sol b/packages/contracts-bedrock/src/L2/L1Block.sol index 31935dfab7eac..5cba32efcc410 100644 --- a/packages/contracts-bedrock/src/L2/L1Block.sol +++ b/packages/contracts-bedrock/src/L2/L1Block.sol @@ -61,13 +61,13 @@ contract L1Block is ISemver { /// @notice The scalar value applied to the operator fee. uint32 public operatorFeeScalar; - /// @custom:semver 1.6.1 + /// @custom:semver 1.6.2 function version() public pure virtual returns (string memory) { - return "1.6.1"; + return "1.6.2"; } /// @notice Returns the gas paying token, its decimals, name and symbol. - function gasPayingToken() public pure returns (address addr_, uint8 decimals_) { + function gasPayingToken() public pure virtual returns (address addr_, uint8 decimals_) { addr_ = Constants.ETHER; decimals_ = 18; } @@ -75,20 +75,20 @@ contract L1Block is ISemver { /// @notice Returns the gas paying token name. /// If nothing is set in state, then it means ether is used. /// This function cannot be removed because WETH depends on it. - function gasPayingTokenName() public pure returns (string memory name_) { + function gasPayingTokenName() public view virtual returns (string memory name_) { name_ = "Ether"; } /// @notice Returns the gas paying token symbol. /// If nothing is set in state, then it means ether is used. /// This function cannot be removed because WETH depends on it. - function gasPayingTokenSymbol() public pure returns (string memory symbol_) { + function gasPayingTokenSymbol() public view virtual returns (string memory symbol_) { symbol_ = "ETH"; } /// @notice Getter for custom gas token paying networks. Returns true if the /// network uses a custom gas token. - function isCustomGasToken() public pure returns (bool is_) { + function isCustomGasToken() public view virtual returns (bool is_) { is_ = false; } diff --git a/packages/contracts-bedrock/src/L2/L1BlockCGT.sol b/packages/contracts-bedrock/src/L2/L1BlockCGT.sol new file mode 100644 index 0000000000000..aac68b7915814 --- /dev/null +++ b/packages/contracts-bedrock/src/L2/L1BlockCGT.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Libraries +import { Constants } from "src/libraries/Constants.sol"; +import { Predeploys } from "src/libraries/Predeploys.sol"; +import { L1Block } from "src/L2/L1Block.sol"; + +// Interfaces +import { ILiquidityController } from "interfaces/L2/ILiquidityController.sol"; + +/// @custom:proxied true +/// @custom:predeploy 0x4200000000000000000000000000000000000015 +/// @title L1BlockCGT +/// @notice The L1BlockCGT predeploy gives users access to information about the last known L1 block. +/// Values within this contract are updated once per epoch (every L1 block) and can only be +/// set by the "depositor" account, a special system address. Depositor account transactions +/// are created by the protocol whenever we move to a new epoch. +contract L1BlockCGT is L1Block { + /// @notice Storage slot for the isCustomGasToken flag + /// @dev bytes32(uint256(keccak256("l1block.isCustomGasToken")) - 1) + bytes32 private constant IS_CUSTOM_GAS_TOKEN_SLOT = + 0xd2ff82c9b477ff6a09f530b1c627ffb4b0b81e2ae2ba427f824162e8dad020aa; + + /// @custom:semver +custom-gas-token + function version() public pure override returns (string memory) { + return string.concat(super.version(), "+custom-gas-token"); + } + + /// @notice Returns whether the gas paying token is custom. + function isCustomGasToken() public view override returns (bool isCustom_) { + bytes32 slot = IS_CUSTOM_GAS_TOKEN_SLOT; + assembly { + isCustom_ := sload(slot) + } + } + + /// @notice Returns the gas paying token, its decimals, name and symbol. + function gasPayingToken() public pure override returns (address, uint8) { + revert("L1BlockCGT: deprecated"); + } + + /// @notice Returns the gas paying token name. + /// If nothing is set in state, then it means ether is used. + /// This function cannot be removed because WETH depends on it. + function gasPayingTokenName() public view override returns (string memory name_) { + name_ = + isCustomGasToken() ? ILiquidityController(Predeploys.LIQUIDITY_CONTROLLER).gasPayingTokenName() : "Ether"; + } + + /// @notice Returns the gas paying token symbol. + /// If nothing is set in state, then it means ether is used. + /// This function cannot be removed because WETH depends on it. + function gasPayingTokenSymbol() public view override returns (string memory symbol_) { + symbol_ = + isCustomGasToken() ? ILiquidityController(Predeploys.LIQUIDITY_CONTROLLER).gasPayingTokenSymbol() : "ETH"; + } + + /// @notice Set chain to use custom gas token (callable by depositor account) + function setCustomGasToken() external { + require( + msg.sender == Constants.DEPOSITOR_ACCOUNT, + "L1Block: only the depositor account can set isCustomGasToken flag" + ); + require(isCustomGasToken() == false, "L1Block: CustomGasToken already active"); + + bytes32 slot = IS_CUSTOM_GAS_TOKEN_SLOT; + assembly { + sstore(slot, 1) + } + } +} diff --git a/packages/contracts-bedrock/src/L2/L2ToL1MessagePasser.sol b/packages/contracts-bedrock/src/L2/L2ToL1MessagePasser.sol index b25a2a1248bc5..682b5ac1b2ccb 100644 --- a/packages/contracts-bedrock/src/L2/L2ToL1MessagePasser.sol +++ b/packages/contracts-bedrock/src/L2/L2ToL1MessagePasser.sol @@ -48,11 +48,13 @@ contract L2ToL1MessagePasser is ISemver { ); /// @notice Emitted when the balance of this contract is burned. - /// @param amount Amount of ETh that was burned. + /// @param amount Amount of ETH that was burned. event WithdrawerBalanceBurnt(uint256 indexed amount); - /// @custom:semver 1.1.2 - string public constant version = "1.1.2"; + /// @custom:semver 1.2.0 + function version() public pure virtual returns (string memory) { + return "1.2.0"; + } /// @notice Allows users to withdraw ETH by sending directly to this contract. receive() external payable { @@ -73,7 +75,7 @@ contract L2ToL1MessagePasser is ISemver { /// @param _target Address to call on L1 execution. /// @param _gasLimit Minimum gas limit for executing the message on L1. /// @param _data Data to forward to L1 target. - function initiateWithdrawal(address _target, uint256 _gasLimit, bytes memory _data) public payable { + function initiateWithdrawal(address _target, uint256 _gasLimit, bytes memory _data) public payable virtual { bytes32 withdrawalHash = Hashing.hashWithdrawal( Types.WithdrawalTransaction({ nonce: messageNonce(), diff --git a/packages/contracts-bedrock/src/L2/L2ToL1MessagePasserCGT.sol b/packages/contracts-bedrock/src/L2/L2ToL1MessagePasserCGT.sol new file mode 100644 index 0000000000000..efd00314559c2 --- /dev/null +++ b/packages/contracts-bedrock/src/L2/L2ToL1MessagePasserCGT.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Libraries +import { Predeploys } from "src/libraries/Predeploys.sol"; +import { L2ToL1MessagePasser } from "src/L2/L2ToL1MessagePasser.sol"; + +// Interfaces +import { IL1Block } from "interfaces/L2/IL1Block.sol"; + +/// @custom:proxied true +/// @custom:predeploy 0x4200000000000000000000000000000000000016 +/// @title L2ToL1MessagePasserCGT +/// @notice The L2ToL1MessagePasserCGT is a dedicated contract where messages that are being sent from +/// L2 to L1 can be stored. The storage root of this contract is pulled up to the top level +/// of the L2 output to reduce the cost of proving the existence of sent messages. +contract L2ToL1MessagePasserCGT is L2ToL1MessagePasser { + /// @notice The error thrown when a withdrawal is initiated with value and custom gas token is used. + error L2ToL1MessagePasserCGT_NotAllowedOnCGTMode(); + + /// @custom:semver +custom-gas-token + function version() public pure override returns (string memory) { + return string.concat(super.version(), "+custom-gas-token"); + } + + /// @notice Sends a message from L2 to L1. + /// @param _target Address to call on L1 execution. + /// @param _gasLimit Minimum gas limit for executing the message on L1. + /// @param _data Data to forward to L1 target. + function initiateWithdrawal(address _target, uint256 _gasLimit, bytes memory _data) public payable override { + if (IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken() && msg.value > 0) { + revert L2ToL1MessagePasserCGT_NotAllowedOnCGTMode(); + } + super.initiateWithdrawal(_target, _gasLimit, _data); + } +} diff --git a/packages/contracts-bedrock/src/L2/LiquidityController.sol b/packages/contracts-bedrock/src/L2/LiquidityController.sol new file mode 100644 index 0000000000000..571932d477a52 --- /dev/null +++ b/packages/contracts-bedrock/src/L2/LiquidityController.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Contracts +import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import { SafeSend } from "src/universal/SafeSend.sol"; + +// Libraries +import { Predeploys } from "src/libraries/Predeploys.sol"; + +// Interfaces +import { INativeAssetLiquidity } from "interfaces/L2/INativeAssetLiquidity.sol"; +import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; +import { ISemver } from "interfaces/universal/ISemver.sol"; + +/// @custom:proxied true +/// @custom:predeploy 0x420000000000000000000000000000000000002A +/// @title LiquidityController +/// @notice The LiquidityController contract is responsible for controlling the liquidity of the native asset on the L2 +/// chain. +contract LiquidityController is ISemver, Initializable { + /// @notice Emitted when an address is authorized to mint/burn liquidity + /// @param minter The address that was authorized + event MinterAuthorized(address indexed minter); + + /// @notice Emitted when an address is deauthorized to mint/burn liquidity + /// @param minter The address that was deauthorized + event MinterDeauthorized(address indexed minter); + + /// @notice Emitted when liquidity is minted + /// @param minter The address that minted the liquidity + /// @param to The address that received the minted liquidity + /// @param amount The amount of liquidity that was minted + event LiquidityMinted(address indexed minter, address indexed to, uint256 amount); + + /// @notice Emitted when liquidity is burned + /// @param minter The address that burned the liquidity + /// @param amount The amount of liquidity that was burned + event LiquidityBurned(address indexed minter, uint256 amount); + + /// @notice Error for when an address is unauthorized to perform liquidity control operations + error LiquidityController_Unauthorized(); + + /// @notice Semantic version. + /// @custom:semver 1.0.0 + string public constant version = "1.0.0"; + + /// @notice Mapping of addresses authorized to control liquidity operations + mapping(address => bool) public minters; + + /// @notice The name of the native asset + string public gasPayingTokenName; + + /// @notice The symbol of the native asset + string public gasPayingTokenSymbol; + + constructor() { + _disableInitializers(); + } + + /// @notice Initializer. + /// @param _gasPayingTokenName The name of the native asset + /// @param _gasPayingTokenSymbol The symbol of the native asset + function initialize(string memory _gasPayingTokenName, string memory _gasPayingTokenSymbol) external initializer { + gasPayingTokenName = _gasPayingTokenName; + gasPayingTokenSymbol = _gasPayingTokenSymbol; + } + + /// @notice Authorizes an address to perform liquidity control operations + /// @param _minter The address to authorize as a minter + function authorizeMinter(address _minter) external { + if (msg.sender != IProxyAdmin(Predeploys.PROXY_ADMIN).owner()) revert LiquidityController_Unauthorized(); + minters[_minter] = true; + emit MinterAuthorized(_minter); + } + + /// @notice Deauthorizes an address from performing liquidity control operations + /// @param _minter The address to deauthorize as a minter + function deauthorizeMinter(address _minter) external { + if (msg.sender != IProxyAdmin(Predeploys.PROXY_ADMIN).owner()) revert LiquidityController_Unauthorized(); + delete minters[_minter]; + emit MinterDeauthorized(_minter); + } + + /// @notice Mints native asset liquidity and sends it to a specified address + /// @param _to The address to receive the minted native asset + /// @param _amount The amount of native asset to mint and send + function mint(address _to, uint256 _amount) external { + if (!minters[msg.sender]) revert LiquidityController_Unauthorized(); + INativeAssetLiquidity(Predeploys.NATIVE_ASSET_LIQUIDITY).withdraw(_amount); + + // This is a forced ETH send to the recipient, the recipient should NOT expect to be called + new SafeSend{ value: _amount }(payable(_to)); + + emit LiquidityMinted(msg.sender, _to, _amount); + } + + /// @notice Burns native asset liquidity by sending ETH to the contract + function burn() external payable { + if (!minters[msg.sender]) revert LiquidityController_Unauthorized(); + INativeAssetLiquidity(Predeploys.NATIVE_ASSET_LIQUIDITY).deposit{ value: msg.value }(); + + emit LiquidityBurned(msg.sender, msg.value); + } +} diff --git a/packages/contracts-bedrock/src/L2/NativeAssetLiquidity.sol b/packages/contracts-bedrock/src/L2/NativeAssetLiquidity.sol new file mode 100644 index 0000000000000..f0ab2bbba95b9 --- /dev/null +++ b/packages/contracts-bedrock/src/L2/NativeAssetLiquidity.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Contracts +import { SafeSend } from "src/universal/SafeSend.sol"; + +// Libraries +import { Predeploys } from "src/libraries/Predeploys.sol"; + +// Interfaces +import { ISemver } from "interfaces/universal/ISemver.sol"; + +/// @custom:predeploy 0x4200000000000000000000000000000000000029 +/// @title NativeAssetLiquidity +/// @notice The NativeAssetLiquidity contract allows other contracts to access native asset liquidity +contract NativeAssetLiquidity is ISemver { + /// @notice Emitted when an address withdraws native asset liquidity. + event LiquidityWithdrawn(address indexed caller, uint256 value); + + /// @notice Emitted when an address deposits native asset liquidity. + event LiquidityDeposited(address indexed caller, uint256 value); + + /// @notice Error for when the contract has insufficient balance. + error NativeAssetLiquidity_InsufficientBalance(); + + /// @notice Error for when an address is unauthorized to perform native asset liquidity operations + error NativeAssetLiquidity_Unauthorized(); + + /// @notice Semantic version. + /// @custom:semver 1.0.0 + string public constant version = "1.0.0"; + + /// @notice Allows an address to lock native asset liquidity into this contract. + function deposit() external payable { + if (msg.sender != Predeploys.LIQUIDITY_CONTROLLER) revert NativeAssetLiquidity_Unauthorized(); + + emit LiquidityDeposited(msg.sender, msg.value); + } + + /// @notice Allows an address to unlock native asset liquidity from this contract. + /// @param _amount The amount of liquidity to unlock. + function withdraw(uint256 _amount) external { + if (msg.sender != Predeploys.LIQUIDITY_CONTROLLER) revert NativeAssetLiquidity_Unauthorized(); + + if (_amount > address(this).balance) revert NativeAssetLiquidity_InsufficientBalance(); + + new SafeSend{ value: _amount }(payable(msg.sender)); + + emit LiquidityWithdrawn(msg.sender, _amount); + } +} diff --git a/packages/contracts-bedrock/src/libraries/DevFeatures.sol b/packages/contracts-bedrock/src/libraries/DevFeatures.sol index 88427ee645b3a..143acef186a5f 100644 --- a/packages/contracts-bedrock/src/libraries/DevFeatures.sol +++ b/packages/contracts-bedrock/src/libraries/DevFeatures.sol @@ -14,12 +14,17 @@ library DevFeatures { bytes32 public constant OPTIMISM_PORTAL_INTEROP = bytes32(0x0000000000000000000000000000000000000000000000000000000000000001); + /// @notice The feature that enables the Cannon Kona chain. bytes32 public constant CANNON_KONA = bytes32(0x0000000000000000000000000000000000000000000000000000000000000010); /// @notice The feature that enables deployment of V2 dispute game contracts. bytes32 public constant DEPLOY_V2_DISPUTE_GAMES = bytes32(0x0000000000000000000000000000000000000000000000000000000000000100); + /// @notice The feature that enables the custom gas token. + bytes32 public constant CUSTOM_GAS_TOKEN = + bytes32(0x0000000000000000000000000000000000000000000000000000000000001000); + /// @notice Checks if a feature is enabled in a bitmap. Note that this function does not check /// that the input feature represents a single feature and the bitwise AND operation /// allows for multiple features to be enabled at once. Users should generally check diff --git a/packages/contracts-bedrock/src/libraries/Features.sol b/packages/contracts-bedrock/src/libraries/Features.sol index 1521b1d1a3307..ffe8b8c3eefa0 100644 --- a/packages/contracts-bedrock/src/libraries/Features.sol +++ b/packages/contracts-bedrock/src/libraries/Features.sol @@ -10,4 +10,9 @@ library Features { /// and the ETHLockbox contract has been configured, the OptimismPortal will use the /// ETHLockbox to store ETH instead of storing ETH directly in the portal itself. bytes32 internal constant ETH_LOCKBOX = "ETH_LOCKBOX"; + + /// @notice The CUSTOM_GAS_TOKEN feature determines if the system is configured to use a custom + /// gas token in the OptimismPortal. When the CUSTOM_GAS_TOKEN feature is active, the + /// deposits and withdrawals of native ETH are disabled. + bytes32 internal constant CUSTOM_GAS_TOKEN = "CUSTOM_GAS_TOKEN"; } diff --git a/packages/contracts-bedrock/src/libraries/Predeploys.sol b/packages/contracts-bedrock/src/libraries/Predeploys.sol index baeb6a143575c..a58ab7c4f0290 100644 --- a/packages/contracts-bedrock/src/libraries/Predeploys.sol +++ b/packages/contracts-bedrock/src/libraries/Predeploys.sol @@ -113,6 +113,12 @@ library Predeploys { /// @notice Address of the SuperchainTokenBridge predeploy. address internal constant SUPERCHAIN_TOKEN_BRIDGE = 0x4200000000000000000000000000000000000028; + /// @notice Address of the NativeAssetLiquidity predeploy. + address internal constant NATIVE_ASSET_LIQUIDITY = 0x4200000000000000000000000000000000000029; + + /// @notice Address of the LiquidityController predeploy. + address internal constant LIQUIDITY_CONTROLLER = 0x420000000000000000000000000000000000002a; + /// @notice Returns the name of the predeploy at the given address. function getName(address _addr) internal pure returns (string memory out_) { require(isPredeployNamespace(_addr), "Predeploys: address must be a predeploy"); @@ -145,6 +151,8 @@ library Predeploys { if (_addr == OPTIMISM_SUPERCHAIN_ERC20_FACTORY) return "OptimismSuperchainERC20Factory"; if (_addr == OPTIMISM_SUPERCHAIN_ERC20_BEACON) return "OptimismSuperchainERC20Beacon"; if (_addr == SUPERCHAIN_TOKEN_BRIDGE) return "SuperchainTokenBridge"; + if (_addr == LIQUIDITY_CONTROLLER) return "LiquidityController"; + if (_addr == NATIVE_ASSET_LIQUIDITY) return "NativeAssetLiquidity"; revert("Predeploys: unnamed predeploy"); } @@ -157,7 +165,8 @@ library Predeploys { function isSupportedPredeploy( address _addr, uint256 _fork, - bool _enableCrossL2Inbox + bool _enableCrossL2Inbox, + bool _isCustomGasToken ) internal pure @@ -171,7 +180,9 @@ library Predeploys { || _addr == L1_FEE_VAULT || _addr == OPERATOR_FEE_VAULT || _addr == SCHEMA_REGISTRY || _addr == EAS || _addr == GOVERNANCE_TOKEN || (_fork >= uint256(Fork.INTEROP) && _enableCrossL2Inbox && _addr == CROSS_L2_INBOX) - || (_fork >= uint256(Fork.INTEROP) && _addr == L2_TO_L2_CROSS_DOMAIN_MESSENGER); + || (_fork >= uint256(Fork.INTEROP) && _addr == L2_TO_L2_CROSS_DOMAIN_MESSENGER) + || (_isCustomGasToken && _addr == LIQUIDITY_CONTROLLER) + || (_isCustomGasToken && _addr == NATIVE_ASSET_LIQUIDITY); } function isPredeployNamespace(address _addr) internal pure returns (bool) { diff --git a/packages/contracts-bedrock/test/L1/L1StandardBridge.t.sol b/packages/contracts-bedrock/test/L1/L1StandardBridge.t.sol index 03ebb72969608..1761a130415f7 100644 --- a/packages/contracts-bedrock/test/L1/L1StandardBridge.t.sol +++ b/packages/contracts-bedrock/test/L1/L1StandardBridge.t.sol @@ -15,6 +15,7 @@ import { Predeploys } from "src/libraries/Predeploys.sol"; import { AddressAliasHelper } from "src/vendor/AddressAliasHelper.sol"; import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; import { Features } from "src/libraries/Features.sol"; +import { DevFeatures } from "src/libraries/DevFeatures.sol"; // Interfaces import { ICrossDomainMessenger } from "interfaces/universal/ICrossDomainMessenger.sol"; @@ -332,6 +333,7 @@ contract L1StandardBridge_Paused_Test is CommonTest { contract L1StandardBridge_Receive_Test is CommonTest { /// @notice Tests receive bridges ETH successfully. function test_receive_succeeds() external { + skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); uint256 portalBalanceBefore = address(optimismPortal2).balance; uint256 ethLockboxBalanceBefore = address(ethLockbox).balance; @@ -371,8 +373,9 @@ contract L1StandardBridge_Receive_Test is CommonTest { vm.etch(alice, hex"ffff"); vm.deal(alice, 100); vm.prank(alice); - vm.expectRevert("StandardBridge: function can only be called from an EOA"); - l1StandardBridge.depositETH{ value: 100 }(50000, hex""); + vm.expectRevert(bytes("StandardBridge: function can only be called from an EOA")); + (bool revertsAsExpected,) = address(l1StandardBridge).call{ value: 100 }(hex""); + assertTrue(revertsAsExpected, "expectRevert: call did not revert"); } } @@ -385,6 +388,7 @@ contract L1StandardBridge_DepositETH_Test is L1StandardBridge_TestInit { /// Only EOA can call depositETH. /// ETH ends up in the optimismPortal. function test_depositETH_fromEOA_succeeds() external { + skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); _preBridgeETH({ isLegacy: true, value: 500 }); uint256 portalBalanceBefore = address(optimismPortal2).balance; uint256 ethLockboxBalanceBefore = address(ethLockbox).balance; @@ -400,6 +404,7 @@ contract L1StandardBridge_DepositETH_Test is L1StandardBridge_TestInit { /// @notice Tests that depositing ETH succeeds for an EOA using 7702 delegation. function test_depositETH_fromEOA7702_succeeds() external { + skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); // Set alice to have 7702 code. vm.etch(alice, abi.encodePacked(hex"EF0100", address(0))); @@ -434,6 +439,7 @@ contract L1StandardBridge_DepositETHTo_Test is L1StandardBridge_TestInit { /// EOA or contract can call depositETHTo. /// ETH ends up in the optimismPortal. function test_depositETHTo_succeeds() external { + skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); _preBridgeETHTo({ isLegacy: true, value: 600 }); uint256 portalBalanceBefore = address(optimismPortal2).balance; uint256 ethLockboxBalanceBefore = address(ethLockbox).balance; @@ -451,6 +457,7 @@ contract L1StandardBridge_DepositETHTo_Test is L1StandardBridge_TestInit { /// @param _to Random recipient address /// @param _amount Random ETH amount to deposit function testFuzz_depositETHTo_randomRecipient_succeeds(address _to, uint256 _amount) external { + skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); vm.assume(_to != address(0)); _amount = bound(_amount, 1, 10 ether); @@ -779,6 +786,7 @@ contract L1StandardBridge_Uncategorized_Test is L1StandardBridge_TestInit { /// Only EOA can call bridgeETH. /// ETH ends up in the optimismPortal. function test_bridgeETH_succeeds() external { + skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); _preBridgeETH({ isLegacy: false, value: 500 }); uint256 portalBalanceBefore = address(optimismPortal2).balance; uint256 ethLockboxBalanceBefore = address(ethLockbox).balance; @@ -798,6 +806,7 @@ contract L1StandardBridge_Uncategorized_Test is L1StandardBridge_TestInit { /// Only EOA can call bridgeETHTo. /// ETH ends up in the optimismPortal. function test_bridgeETHTo_succeeds() external { + skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); _preBridgeETHTo({ isLegacy: false, value: 600 }); uint256 portalBalanceBefore = address(optimismPortal2).balance; uint256 ethLockboxBalanceBefore = address(ethLockbox).balance; diff --git a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol index 90691b95afa8f..754a459f5707c 100644 --- a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol +++ b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol @@ -62,6 +62,10 @@ contract OptimismPortal2_TestInit is DisputeGameFactory_TestInit { data: hex"aa" // includes calldata for ERC20 withdrawal test }); + if (isUsingCustomGasToken()) { + _defaultTx.value = 0; + } + // Get withdrawal proof data we can use for testing. (_stateRoot, _storageRoot, _outputRoot, _withdrawalHash, _withdrawalProof) = ffi.getProveWithdrawalTransactionInputs(_defaultTx); @@ -161,6 +165,12 @@ contract OptimismPortal2_TestInit is DisputeGameFactory_TestInit { systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX) && address(optimismPortal2.ethLockbox()) != address(0); } + /// @notice Checks if the Custom Gas Token feature is enabled. + /// @return bool True if the Custom Gas Token feature is enabled. + function isUsingCustomGasToken() public view returns (bool) { + return isDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); + } + /// @notice Enables the ETHLockbox feature if not enabled. /// @param _lockbox Address of the lockbox to enable. function forceEnableLockbox(address _lockbox) public { @@ -178,6 +188,12 @@ contract OptimismPortal2_TestInit is DisputeGameFactory_TestInit { vm.etch(address(_lockbox), hex"00"); } } + + /// @notice Sets the useCustomGasToken variable + function setUseCustomGasToken(bool _useCustomGasToken) public { + vm.prank(address(proxyAdmin)); + systemConfig.setFeature(Features.CUSTOM_GAS_TOKEN, _useCustomGasToken); + } } /// @title OptimismPortal2_Version_Test @@ -212,7 +228,7 @@ contract OptimismPortal2_Initialize_Test is OptimismPortal2_TestInit { /// @notice Tests that the initializer sets the correct values. /// @dev Marked virtual to be overridden in /// test/kontrol/deployment/DeploymentSummary.t.sol - function test_initialize_succeeds() external virtual { + function test_initialize_succeeds() public virtual { assertEq(address(optimismPortal2.anchorStateRegistry()), address(anchorStateRegistry)); assertEq(address(optimismPortal2.disputeGameFactory()), address(disputeGameFactory)); assertEq(address(optimismPortal2.superchainConfig()), address(superchainConfig)); @@ -225,6 +241,11 @@ contract OptimismPortal2_Initialize_Test is OptimismPortal2_TestInit { } else { assertEq(address(optimismPortal2.ethLockbox()), address(0)); } + if (isUsingCustomGasToken()) { + assertTrue(optimismPortal2.systemConfig().isFeatureEnabled(Features.CUSTOM_GAS_TOKEN)); + } else if (!isUsingLockbox()) { + assertFalse(optimismPortal2.systemConfig().isFeatureEnabled(Features.CUSTOM_GAS_TOKEN)); + } returnIfForkTest( "OptimismPortal2_Initialize_Test: Do not check guardian and respectedGameType on forked networks" @@ -252,31 +273,9 @@ contract OptimismPortal2_Initialize_Test is OptimismPortal2_TestInit { // Assert that the initializer value matches the expected value. assertEq(val, optimismPortal2.initVersion()); } - /// @notice Tests that the initialize function reverts if called by a non-proxy admin or owner. /// @param _sender The address of the sender to test. - function testFuzz_initialize_notProxyAdminOrProxyAdminOwner_reverts(address _sender) public { - skipIfDevFeatureEnabled(DevFeatures.OPTIMISM_PORTAL_INTEROP); - // Prank as the not ProxyAdmin or ProxyAdmin owner. - vm.assume(_sender != address(proxyAdmin) && _sender != proxyAdminOwner); - - // Get the slot for _initialized. - StorageSlot memory slot = ForgeArtifacts.getSlot("OptimismPortal2", "_initialized"); - - // Set the initialized slot to 0. - vm.store(address(optimismPortal2), bytes32(slot.slot), bytes32(0)); - - // Expect the revert with `ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner` selector. - vm.expectRevert(IProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner.selector); - - // Call the `initialize` function with the sender - vm.prank(_sender); - optimismPortal2.initialize(systemConfig, anchorStateRegistry); - } - - /// @notice Tests that the initialize function reverts if called by a non-proxy admin or owner. - /// @param _sender The address of the sender to test. function testFuzz_initialize_interopNotProxyAdminOrProxyAdminOwner_reverts(address _sender) public { skipIfDevFeatureDisabled(DevFeatures.OPTIMISM_PORTAL_INTEROP); @@ -324,6 +323,28 @@ contract OptimismPortal2_Initialize_Test is OptimismPortal2_TestInit { vm.prank(address(proxyAdmin)); optimismPortal2.initialize(systemConfig, anchorStateRegistry); } + + /// @notice Tests that the initialize function reverts if called by a non-proxy admin or owner. + /// @param _sender The address of the sender to test. + function testFuzz_initialize_notProxyAdminOrProxyAdminOwner_reverts(address _sender) public { + skipIfDevFeatureEnabled(DevFeatures.OPTIMISM_PORTAL_INTEROP); + + // Prank as the not ProxyAdmin or ProxyAdmin owner. + vm.assume(_sender != address(proxyAdmin) && _sender != proxyAdminOwner); + + // Get the slot for _initialized. + StorageSlot memory slot = ForgeArtifacts.getSlot("OptimismPortal2", "_initialized"); + + // Set the initialized slot to 0. + vm.store(address(optimismPortal2), bytes32(slot.slot), bytes32(0)); + + // Expect the revert with `ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner` selector. + vm.expectRevert(IProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner.selector); + + // Call the `initialize` function with the sender + vm.prank(_sender); + optimismPortal2.initialize(systemConfig, anchorStateRegistry); + } } /// @title OptimismPortal2_UpgradeInterop_Test @@ -619,8 +640,9 @@ contract OptimismPortal2_NumProofSubmitters_Test is OptimismPortal2_TestInit { /// @title OptimismPortal2_Receive_Test /// @notice Test contract for OptimismPortal2 `receive` function. contract OptimismPortal2_Receive_Test is OptimismPortal2_TestInit { - /// @notice Tests that `receive` successdully deposits ETH. + /// @notice Tests that `receive` successfully deposits ETH. function testFuzz_receive_succeeds(uint256 _value) external { + skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); // Prevent overflow on an upgrade context _value = bound(_value, 0, type(uint256).max - address(ethLockbox).balance); uint256 balanceBefore = address(optimismPortal2).balance; @@ -659,6 +681,7 @@ contract OptimismPortal2_Receive_Test is OptimismPortal2_TestInit { } function testFuzz_receive_withLockbox_succeeds(uint256 _value) external { + skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); // Prevent overflow on an upgrade context. // We use a dummy lockbox here because the real one won't work for upgrade tests. address dummyLockbox = address(0xdeadbeef); @@ -694,6 +717,18 @@ contract OptimismPortal2_Receive_Test is OptimismPortal2_TestInit { assertEq(address(optimismPortal2).balance, balanceBefore); assertEq(address(dummyLockbox).balance, lockboxBalanceBefore + _value); } + + /// @notice Tests that `receive` reverts when custom gas token is enabled + function testFuzz_receive_customGasToken_reverts(uint256 _value) external { + skipIfDevFeatureDisabled(DevFeatures.CUSTOM_GAS_TOKEN); + _value = bound(_value, 1, type(uint128).max); + vm.deal(alice, _value); + + vm.prank(alice); + vm.expectRevert(IOptimismPortal.OptimismPortal_NotAllowedOnCGTMode.selector); + (bool revertsAsExpected,) = address(optimismPortal2).call{ value: _value }(hex""); + assertTrue(revertsAsExpected, "expectRevert: call did not revert"); + } } /// @title OptimismPortal2_DonateETH_Test @@ -1367,6 +1402,26 @@ contract OptimismPortal2_ProveWithdrawalTransaction_Test is OptimismPortal2_Test _withdrawalProof: _withdrawalProof }); } + + /// @notice Tests that `proveWithdrawalTransaction` reverts when the custom gas token mode + /// is enabled and the withdrawal transaction has a value. + function test_proveWithdrawalTransaction_withValueAndCustomGasToken_reverts() external { + skipIfDevFeatureDisabled(DevFeatures.CUSTOM_GAS_TOKEN); + skipIfForkTest( + "OptimismPortal2_ProveWithdrawalTransaction_Test: isCustomGasToken() not available on forked networks" + ); + // Set the withdrawal transaction value to a non-zero value. + _defaultTx.value = bound(uint256(1), 1, type(uint256).max); + + // Prove the withdrawal transaction. This should revert. + vm.expectRevert(IOptimismPortal.OptimismPortal_NotAllowedOnCGTMode.selector); + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + } } /// @title OptimismPortal2_FinalizeWithdrawalTransaction_Test @@ -1408,14 +1463,7 @@ contract OptimismPortal2_FinalizeWithdrawalTransaction_Test is OptimismPortal2_T /// @notice Tests that `finalizeWithdrawalTransaction` succeeds when _tx.data is empty. function test_finalizeWithdrawalTransaction_noTxData_succeeds() external { - Types.WithdrawalTransaction memory _defaultTx_noData = Types.WithdrawalTransaction({ - nonce: 0, - sender: alice, - target: bob, - value: 100, - gasLimit: 100_000, - data: hex"" - }); + _defaultTx.data = hex""; // Get withdrawal proof data we can use for testing. ( @@ -1424,7 +1472,7 @@ contract OptimismPortal2_FinalizeWithdrawalTransaction_Test is OptimismPortal2_T bytes32 _outputRoot_noData, bytes32 _withdrawalHash_noData, bytes[] memory _withdrawalProof_noData - ) = ffi.getProveWithdrawalTransactionInputs(_defaultTx_noData); + ) = ffi.getProveWithdrawalTransactionInputs(_defaultTx); // Setup a dummy output root proof for reuse. Types.OutputRootProof memory _outputRootProof_noData = Types.OutputRootProof({ @@ -1463,7 +1511,7 @@ contract OptimismPortal2_FinalizeWithdrawalTransaction_Test is OptimismPortal2_T vm.expectEmit(address(optimismPortal2)); emit WithdrawalProvenExtension1(_withdrawalHash_noData, address(this)); optimismPortal2.proveWithdrawalTransaction({ - _tx: _defaultTx_noData, + _tx: _defaultTx, _disputeGameIndex: _proposedGameIndex_noData, _outputRootProof: _outputRootProof_noData, _withdrawalProof: _withdrawalProof_noData @@ -1476,9 +1524,9 @@ contract OptimismPortal2_FinalizeWithdrawalTransaction_Test is OptimismPortal2_T vm.expectEmit(true, true, false, true); emit WithdrawalFinalized(_withdrawalHash_noData, true); - optimismPortal2.finalizeWithdrawalTransaction(_defaultTx_noData); + optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); - assert(bob.balance == bobBalanceBefore + 100); + assert(bob.balance == bobBalanceBefore + _defaultTx.value); } /// @notice Tests that `finalizeWithdrawalTransaction` succeeds. @@ -1505,7 +1553,7 @@ contract OptimismPortal2_FinalizeWithdrawalTransaction_Test is OptimismPortal2_T emit WithdrawalFinalized(_withdrawalHash, true); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); - assert(address(bob).balance == bobBalanceBefore + 100); + assert(address(bob).balance == bobBalanceBefore + _defaultTx.value); } /// @notice Tests that `finalizeWithdrawalTransaction` succeeds using a different proof than an @@ -1566,7 +1614,7 @@ contract OptimismPortal2_FinalizeWithdrawalTransaction_Test is OptimismPortal2_T emit WithdrawalFinalized(_withdrawalHash, true); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); - assert(address(bob).balance == bobBalanceBefore + 100); + assert(address(bob).balance == bobBalanceBefore + _defaultTx.value); } /// @notice Tests that `finalizeWithdrawalTransaction` reverts if the contract is paused. @@ -1778,19 +1826,12 @@ contract OptimismPortal2_FinalizeWithdrawalTransaction_Test is OptimismPortal2_T /// does not have enough gas to execute. function test_finalizeWithdrawalTransaction_onInsufficientGas_reverts() external { // This number was identified through trial and error. - uint256 gasLimit = 150_000; - Types.WithdrawalTransaction memory insufficientGasTx = Types.WithdrawalTransaction({ - nonce: 0, - sender: alice, - target: bob, - value: 100, - gasLimit: gasLimit, - data: hex"" - }); + _defaultTx.gasLimit = 150_000; + _defaultTx.data = hex""; // Get updated proof inputs. (bytes32 stateRoot, bytes32 storageRoot,,, bytes[] memory withdrawalProof) = - ffi.getProveWithdrawalTransactionInputs(insufficientGasTx); + ffi.getProveWithdrawalTransactionInputs(_defaultTx); Types.OutputRootProof memory outputRootProof = Types.OutputRootProof({ version: bytes32(0), stateRoot: stateRoot, @@ -1803,7 +1844,7 @@ contract OptimismPortal2_FinalizeWithdrawalTransaction_Test is OptimismPortal2_T ); optimismPortal2.proveWithdrawalTransaction({ - _tx: insufficientGasTx, + _tx: _defaultTx, _disputeGameIndex: _proposedGameIndex, _outputRootProof: outputRootProof, _withdrawalProof: withdrawalProof @@ -1815,7 +1856,7 @@ contract OptimismPortal2_FinalizeWithdrawalTransaction_Test is OptimismPortal2_T vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1); vm.expectRevert("SafeCall: Not enough gas"); - optimismPortal2.finalizeWithdrawalTransaction{ gas: gasLimit }(insufficientGasTx); + optimismPortal2.finalizeWithdrawalTransaction{ gas: _defaultTx.gasLimit }(_defaultTx); } /// @notice Tests that `finalizeWithdrawalTransaction` reverts if a sub-call attempts to @@ -1867,6 +1908,21 @@ contract OptimismPortal2_FinalizeWithdrawalTransaction_Test is OptimismPortal2_T assert(address(bob).balance == bobBalanceBefore); } + /// @notice Tests that `finalizeWithdrawalTransaction` reverts when the custom gas token mode + /// is enabled and the withdrawal transaction has a value. + function test_finalizeWithdrawalTransaction_withValueAndCustomGasToken_reverts() external { + skipIfDevFeatureDisabled(DevFeatures.CUSTOM_GAS_TOKEN); + skipIfForkTest( + "OptimismPortal2_FinalizeWithdrawalTransaction_Test: isCustomGasToken() not available on forked networks" + ); + // Set the withdrawal transaction value to a non-zero value. + _defaultTx.value = bound(uint256(1), 1, type(uint256).max); + + // Finalize the withdrawal transaction. This should revert. + vm.expectRevert(IOptimismPortal.OptimismPortal_NotAllowedOnCGTMode.selector); + optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); + } + /// @notice Tests that `finalizeWithdrawalTransaction` succeeds. function testDiff_finalizeWithdrawalTransaction_succeeds( address _sender, @@ -1878,6 +1934,9 @@ contract OptimismPortal2_FinalizeWithdrawalTransaction_Test is OptimismPortal2_T external { skipIfForkTest("Skipping on forked tests because of the L2ToL1MessageParser call below"); + if (isUsingCustomGasToken()) { + _value = 0; + } vm.assume( _target != address(optimismPortal2) // Cannot call the optimism portal or a contract @@ -1959,6 +2018,9 @@ contract OptimismPortal2_FinalizeWithdrawalTransaction_Test is OptimismPortal2_T external { skipIfForkTest("Skipping on forked tests because of the L2ToL1MessageParser call below"); + if (isUsingCustomGasToken()) { + _value = 0; + } vm.assume( _target != address(optimismPortal2) // Cannot call the optimism portal or a contract @@ -2278,7 +2340,7 @@ contract OptimismPortal2_FinalizeWithdrawalTransactionExternalProof_Test is Opti vm.expectRevert(IOptimismPortal.OptimismPortal_AlreadyFinalized.selector); optimismPortal2.finalizeWithdrawalTransactionExternalProof(_defaultTx, address(this)); - assert(address(bob).balance == bobBalanceBefore + 100); + assert(address(bob).balance == bobBalanceBefore + _defaultTx.value); } } @@ -2452,6 +2514,28 @@ contract OptimismPortal2_DepositTransaction_Test is OptimismPortal2_TestInit { optimismPortal2.depositTransaction({ _to: address(1), _value: 0, _gasLimit: 0, _isCreation: false, _data: hex"" }); } + /// @notice Tests that `depositTransaction` reverts when the value is greater than 0 and the + /// custom gas token is active. + function test_depositTransaction_withCustomGasTokenAndValue_reverts(bytes memory _data, uint256 _value) external { + skipIfDevFeatureDisabled(DevFeatures.CUSTOM_GAS_TOKEN); + skipIfForkTest("OptimismPortal2_DepositTransaction_Test: isCustomGasToken() not available on forked networks"); + + // Prevent overflow on an upgrade context + _value = bound(_value, 1, type(uint256).max - address(optimismPortal2).balance); + uint64 gasLimit = optimismPortal2.minimumGasLimit(uint64(_data.length)); + + vm.deal(alice, _value); + vm.prank(alice); + vm.expectRevert(IOptimismPortal.OptimismPortal_NotAllowedOnCGTMode.selector); + optimismPortal2.depositTransaction{ value: _value }({ + _to: address(0x40), + _value: _value, + _gasLimit: gasLimit, + _isCreation: false, + _data: _data + }); + } + /// @notice Tests that `depositTransaction` succeeds for small, but sufficient, gas limits. function testFuzz_depositTransaction_smallGasLimit_succeeds(bytes memory _data, bool _shouldFail) external { uint64 gasLimit = optimismPortal2.minimumGasLimit(uint64(_data.length)); @@ -2481,7 +2565,17 @@ contract OptimismPortal2_DepositTransaction_Test is OptimismPortal2_TestInit { external { // Prevent overflow on an upgrade context - _mint = bound(_mint, 0, type(uint256).max - address(ethLockbox).balance); + // Since the value always goes through the portal + _mint = bound(_mint, 0, type(uint256).max - address(optimismPortal2).balance); + + if (isUsingLockbox() && address(optimismPortal2).balance > address(ethLockbox).balance) { + _mint = bound(_mint, 0, type(uint256).max - address(ethLockbox).balance); + } + + if (isUsingCustomGasToken()) { + _mint = 0; + } + _gasLimit = uint64( bound( _gasLimit, @@ -2493,7 +2587,6 @@ contract OptimismPortal2_DepositTransaction_Test is OptimismPortal2_TestInit { uint256 balanceBefore = address(optimismPortal2).balance; uint256 lockboxBalanceBefore = address(ethLockbox).balance; - _mint = bound(_mint, 0, type(uint256).max - balanceBefore); // EOA emulation vm.expectEmit(address(optimismPortal2)); @@ -2547,6 +2640,10 @@ contract OptimismPortal2_DepositTransaction_Test is OptimismPortal2_TestInit { // Prevent overflow on an upgrade context _mint = bound(_mint, 0, type(uint256).max - address(ethLockbox).balance); + if (isUsingCustomGasToken()) { + _mint = 0; + } + _gasLimit = uint64( bound( _gasLimit, @@ -2606,6 +2703,9 @@ contract OptimismPortal2_DepositTransaction_Test is OptimismPortal2_TestInit { { // Prevent overflow on an upgrade context _mint = bound(_mint, 0, type(uint256).max - address(ethLockbox).balance); + if (isUsingCustomGasToken()) { + _mint = 0; + } _gasLimit = uint64( bound( _gasLimit, diff --git a/packages/contracts-bedrock/test/L1/SystemConfig.t.sol b/packages/contracts-bedrock/test/L1/SystemConfig.t.sol index cfa090517b154..fd3426638b65e 100644 --- a/packages/contracts-bedrock/test/L1/SystemConfig.t.sol +++ b/packages/contracts-bedrock/test/L1/SystemConfig.t.sol @@ -11,6 +11,7 @@ import { ForgeArtifacts, StorageSlot } from "scripts/libraries/ForgeArtifacts.so import { Constants } from "src/libraries/Constants.sol"; import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; import { Features } from "src/libraries/Features.sol"; +import { DevFeatures } from "src/libraries/DevFeatures.sol"; // Interfaces import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; @@ -907,3 +908,19 @@ contract SystemConfig_SetMinBaseFee_Test is SystemConfig_TestInit { assertEq(systemConfig.minBaseFee(), newMinBaseFee); } } + +/// @title SystemConfig_IsCustomGasToken_Test +/// @notice Test contract for SystemConfig `isCustomGasToken` function. +contract SystemConfig_IsCustomGasToken_Test is SystemConfig_TestInit { + /// @notice Tests that `isCustomGasToken` returns the correct value. + function test_isCustomGasToken_enabled_succeeds() external { + skipIfDevFeatureDisabled(DevFeatures.CUSTOM_GAS_TOKEN); + assertTrue(systemConfig.isCustomGasToken()); + } + + /// @notice Tests that `isCustomGasToken` returns the correct value. + function test_isCustomGasToken_disabled_succeeds() external { + skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); + assertFalse(systemConfig.isCustomGasToken()); + } +} diff --git a/packages/contracts-bedrock/test/L2/BaseFeeVault.t.sol b/packages/contracts-bedrock/test/L2/BaseFeeVault.t.sol index ce1a0fe77d35b..1a2019cf061ff 100644 --- a/packages/contracts-bedrock/test/L2/BaseFeeVault.t.sol +++ b/packages/contracts-bedrock/test/L2/BaseFeeVault.t.sol @@ -4,9 +4,6 @@ pragma solidity 0.8.15; // Testing utilities import { CommonTest } from "test/setup/CommonTest.sol"; -// Libraries -import { Types } from "src/libraries/Types.sol"; - /// @title BaseFeeVault_Constructor_Test /// @notice Tests the `constructor` of the `BaseFeeVault` contract. contract BaseFeeVault_Constructor_Test is CommonTest { @@ -16,7 +13,7 @@ contract BaseFeeVault_Constructor_Test is CommonTest { assertEq(baseFeeVault.recipient(), deploy.cfg().baseFeeVaultRecipient()); assertEq(baseFeeVault.MIN_WITHDRAWAL_AMOUNT(), deploy.cfg().baseFeeVaultMinimumWithdrawalAmount()); assertEq(baseFeeVault.minWithdrawalAmount(), deploy.cfg().baseFeeVaultMinimumWithdrawalAmount()); - assertEq(uint8(baseFeeVault.WITHDRAWAL_NETWORK()), uint8(Types.WithdrawalNetwork.L1)); - assertEq(uint8(baseFeeVault.withdrawalNetwork()), uint8(Types.WithdrawalNetwork.L1)); + assertEq(uint8(baseFeeVault.WITHDRAWAL_NETWORK()), deploy.cfg().baseFeeVaultWithdrawalNetwork()); + assertEq(uint8(baseFeeVault.withdrawalNetwork()), deploy.cfg().baseFeeVaultWithdrawalNetwork()); } } diff --git a/packages/contracts-bedrock/test/L2/L1Block.t.sol b/packages/contracts-bedrock/test/L2/L1Block.t.sol index 7a276b94df5d6..b46df4bae4d8d 100644 --- a/packages/contracts-bedrock/test/L2/L1Block.t.sol +++ b/packages/contracts-bedrock/test/L2/L1Block.t.sol @@ -3,11 +3,16 @@ pragma solidity 0.8.15; // Testing import { CommonTest } from "test/setup/CommonTest.sol"; +import { stdStorage, StdStorage } from "forge-std/Test.sol"; // Libraries import { Encoding } from "src/libraries/Encoding.sol"; import { Constants } from "src/libraries/Constants.sol"; import "src/libraries/L1BlockErrors.sol"; +import { DevFeatures } from "src/libraries/DevFeatures.sol"; + +// Interfaces +import { IL1BlockCGT } from "interfaces/L2/IL1BlockCGT.sol"; /// @title L1Block_ TestInit /// @notice Reusable test initialization for `L1Block` tests. @@ -21,34 +26,68 @@ contract L1Block_TestInit is CommonTest { } } +/// @title L1Block_Version_Test +/// @notice Test contract for L1Block `version` function. +contract L1Block_Version_Test is L1Block_TestInit { + /// @notice Tests that the version function returns a valid string. We avoid testing the + /// specific value of the string as it changes frequently. + function test_version_succeeds() external view { + assert(bytes(l1Block.version()).length > 0); + } +} + /// @title L1Block_GasPayingToken_Test /// @notice Tests the `gasPayingToken` function of the `L1Block` contract. contract L1Block_GasPayingToken_Test is L1Block_TestInit { /// @notice Tests that the `gasPayingToken` function returns the correct token address and /// decimals. - function test_gasPayingToken_succeeds() external view { + function test_gasPayingToken_succeeds() external { + skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); (address token, uint8 decimals) = l1Block.gasPayingToken(); assertEq(token, Constants.ETHER); assertEq(uint256(decimals), uint256(18)); } + + /// @notice Tests that the `gasPayingToken` function reverts when custom gas token is enabled. + function test_gasPayingToken_customGasToken_reverts() external { + skipIfDevFeatureDisabled(DevFeatures.CUSTOM_GAS_TOKEN); + vm.expectRevert("L1BlockCGT: deprecated"); + l1Block.gasPayingToken(); + } } /// @title L1Block_GasPayingTokenName_Test /// @notice Tests the `gasPayingTokenName` function of the `L1Block` contract. contract L1Block_GasPayingTokenName_Test is L1Block_TestInit { /// @notice Tests that the `gasPayingTokenName` function returns the correct token name. - function test_gasPayingTokenName_succeeds() external view { + function test_gasPayingTokenName_succeeds() external { + skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); assertEq("Ether", l1Block.gasPayingTokenName()); } + + /// @notice Tests that the `gasPayingTokenName` function returns the correct token name when custom gas token is + /// enabled. + function test_gasPayingTokenName_customGasToken_succeeds() external { + skipIfDevFeatureDisabled(DevFeatures.CUSTOM_GAS_TOKEN); + assertEq(liquidityController.gasPayingTokenName(), l1Block.gasPayingTokenName()); + } } /// @title L1Block_GasPayingTokenSymbol_Test /// @notice Tests the `gasPayingTokenSymbol` function of the `L1Block` contract. contract L1Block_GasPayingTokenSymbol_Test is L1Block_TestInit { /// @notice Tests that the `gasPayingTokenSymbol` function returns the correct token symbol. - function test_gasPayingTokenSymbol_succeeds() external view { + function test_gasPayingTokenSymbol_succeeds() external { + skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); assertEq("ETH", l1Block.gasPayingTokenSymbol()); } + + /// @notice Tests that the `gasPayingTokenSymbol` function returns the correct token symbol when custom gas token is + /// enabled. + function test_gasPayingTokenSymbol_customGasToken_succeeds() external { + skipIfDevFeatureDisabled(DevFeatures.CUSTOM_GAS_TOKEN); + assertEq(liquidityController.gasPayingTokenSymbol(), l1Block.gasPayingTokenSymbol()); + } } /// @title L1Block_IsCustomGasToken_Test @@ -56,9 +95,17 @@ contract L1Block_GasPayingTokenSymbol_Test is L1Block_TestInit { contract L1Block_IsCustomGasToken_Test is L1Block_TestInit { /// @notice Tests that the `isCustomGasToken` function returns false when no custom gas token /// is used. - function test_isCustomGasToken_succeeds() external view { + function test_isCustomGasToken_succeeds() external { + skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); assertFalse(l1Block.isCustomGasToken()); } + + /// @notice Tests that the `isCustomGasToken` function returns true when custom gas token + /// is used. + function test_isCustomGasToken_customGasToken_succeeds() external { + skipIfDevFeatureDisabled(DevFeatures.CUSTOM_GAS_TOKEN); + assertTrue(l1Block.isCustomGasToken()); + } } /// @title L1Block_SetL1BlockValues_Test @@ -315,3 +362,48 @@ contract L1Block_SetL1BlockValuesIsthmus_Test is L1Block_TestInit { assertEq(data, expReturn); } } + +/// @title L1Block_SetCustomGasToken_Test +/// @notice Tests the `setCustomGasToken` function of the `L1Block` contract. +contract L1Block_SetCustomGasToken_Test is L1Block_TestInit { + using stdStorage for StdStorage; + + IL1BlockCGT l1BlockCGT; + + function setUp() public override { + super.setUp(); + skipIfDevFeatureDisabled(DevFeatures.CUSTOM_GAS_TOKEN); + l1BlockCGT = IL1BlockCGT(address(l1Block)); + } + + /// @notice Tests that `setCustomGasToken` reverts if called twice. + function test_setCustomGasToken_alreadyActive_reverts() external { + // This test uses the setUp that already activates custom gas token + assertTrue(l1BlockCGT.isCustomGasToken()); + + vm.expectRevert("L1Block: CustomGasToken already active"); + vm.prank(depositor); + IL1BlockCGT(address(l1BlockCGT)).setCustomGasToken(); + } + + /// @notice Tests that `setCustomGasToken` updates the flag correctly when called by depositor. + function test_setCustomGasToken_succeeds() external { + stdstore.target(address(l1BlockCGT)).sig("isCustomGasToken()").checked_write(false); + // This test uses the setUp that already activates custom gas token + assertFalse(l1BlockCGT.isCustomGasToken()); + + vm.prank(depositor); + l1BlockCGT.setCustomGasToken(); + + assertTrue(l1BlockCGT.isCustomGasToken()); + } + + /// @notice Tests that `setCustomGasToken` reverts if sender address is not the depositor. + function test_setCustomGasToken_notDepositor_reverts(address nonDepositor) external { + stdstore.target(address(l1BlockCGT)).sig("isCustomGasToken()").checked_write(false); + vm.assume(nonDepositor != depositor); + vm.expectRevert("L1Block: only the depositor account can set isCustomGasToken flag"); + vm.prank(nonDepositor); + l1BlockCGT.setCustomGasToken(); + } +} diff --git a/packages/contracts-bedrock/test/L2/L1FeeVault.t.sol b/packages/contracts-bedrock/test/L2/L1FeeVault.t.sol index f61eb99927db7..b98774339d615 100644 --- a/packages/contracts-bedrock/test/L2/L1FeeVault.t.sol +++ b/packages/contracts-bedrock/test/L2/L1FeeVault.t.sol @@ -4,9 +4,6 @@ pragma solidity 0.8.15; // Testing utilities import { CommonTest } from "test/setup/CommonTest.sol"; -// Libraries -import { Types } from "src/libraries/Types.sol"; - /// @title L1FeeVault_Constructor_Test /// @notice Tests the `constructor` of the `L1FeeVault` contract. contract L1FeeVault_Constructor_Test is CommonTest { @@ -16,7 +13,7 @@ contract L1FeeVault_Constructor_Test is CommonTest { assertEq(l1FeeVault.recipient(), deploy.cfg().l1FeeVaultRecipient()); assertEq(l1FeeVault.MIN_WITHDRAWAL_AMOUNT(), deploy.cfg().l1FeeVaultMinimumWithdrawalAmount()); assertEq(l1FeeVault.minWithdrawalAmount(), deploy.cfg().l1FeeVaultMinimumWithdrawalAmount()); - assertEq(uint8(l1FeeVault.WITHDRAWAL_NETWORK()), uint8(Types.WithdrawalNetwork.L1)); - assertEq(uint8(l1FeeVault.withdrawalNetwork()), uint8(Types.WithdrawalNetwork.L1)); + assertEq(uint8(l1FeeVault.WITHDRAWAL_NETWORK()), deploy.cfg().l1FeeVaultWithdrawalNetwork()); + assertEq(uint8(l1FeeVault.withdrawalNetwork()), deploy.cfg().l1FeeVaultWithdrawalNetwork()); } } diff --git a/packages/contracts-bedrock/test/L2/L2StandardBridge.t.sol b/packages/contracts-bedrock/test/L2/L2StandardBridge.t.sol index b7aedc1ac6713..51bdd1227f21f 100644 --- a/packages/contracts-bedrock/test/L2/L2StandardBridge.t.sol +++ b/packages/contracts-bedrock/test/L2/L2StandardBridge.t.sol @@ -14,6 +14,7 @@ import { OptimismMintableERC20 } from "src/universal/OptimismMintableERC20.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; import { Hashing } from "src/libraries/Hashing.sol"; import { Types } from "src/libraries/Types.sol"; +import { DevFeatures } from "src/libraries/DevFeatures.sol"; // Interfaces import { ICrossDomainMessenger } from "interfaces/universal/ICrossDomainMessenger.sol"; @@ -231,6 +232,7 @@ contract L2StandardBridge_Initialize_Test is L2StandardBridge_TestInit { contract L2StandardBridge_Receive_Test is L2StandardBridge_TestInit { /// @notice Tests that the bridge receives ETH and successfully initiates a withdrawal. function test_receive_succeeds() external { + skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); assertEq(address(l2ToL1MessagePasser).balance, 0); uint256 nonce = l2CrossDomainMessenger.messageNonce(); @@ -323,6 +325,7 @@ contract L2StandardBridge_Withdraw_Test is L2StandardBridge_TestInit { /// @notice Tests that the legacy `withdraw` interface on the L2StandardBridge sucessfully /// initiates a withdrawal. function test_withdraw_ether_succeeds() external { + skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); assertTrue(alice.balance >= 100); assertEq(Predeploys.L2_TO_L1_MESSAGE_PASSER.balance, 0); @@ -465,6 +468,7 @@ contract L2StandardBridge_Unclassified_Test is L2StandardBridge_TestInit { /// @notice Tests that bridging ETH succeeds. function testFuzz_bridgeETH_succeeds(uint256 _value, uint32 _minGasLimit, bytes calldata _extraData) external { + skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); uint256 nonce = l2CrossDomainMessenger.messageNonce(); bytes memory message = abi.encodeCall(IStandardBridge.finalizeBridgeETH, (alice, alice, _value, _extraData)); @@ -498,6 +502,7 @@ contract L2StandardBridge_Unclassified_Test is L2StandardBridge_TestInit { /// @notice Tests that bridging ETH to a different address succeeds. function testFuzz_bridgeETHTo_succeeds(uint256 _value, uint32 _minGasLimit, bytes calldata _extraData) external { + skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); uint256 nonce = l2CrossDomainMessenger.messageNonce(); vm.expectCall( diff --git a/packages/contracts-bedrock/test/L2/L2ToL1MessagePasser.t.sol b/packages/contracts-bedrock/test/L2/L2ToL1MessagePasser.t.sol index 8ce02b6aad74b..c4c911a4937b3 100644 --- a/packages/contracts-bedrock/test/L2/L2ToL1MessagePasser.t.sol +++ b/packages/contracts-bedrock/test/L2/L2ToL1MessagePasser.t.sol @@ -7,6 +7,20 @@ import { CommonTest } from "test/setup/CommonTest.sol"; // Libraries import { Types } from "src/libraries/Types.sol"; import { Hashing } from "src/libraries/Hashing.sol"; +import { DevFeatures } from "src/libraries/DevFeatures.sol"; + +// Interfaces +import { IL2ToL1MessagePasserCGT } from "interfaces/L2/IL2ToL1MessagePasserCGT.sol"; + +/// @title L2ToL1MessagePasser_Version_Test +/// @notice Tests the `version` function of the `L2ToL1MessagePasser` contract. +contract L2ToL1MessagePasser_Version_Test is CommonTest { + /// @notice Tests that the `version` function returns the correct string. We avoid testing the + /// specific value of the string as it changes frequently. + function test_version_succeeds() external view { + assert(bytes(l2ToL1MessagePasser.version()).length > 0); + } +} /// @title L2ToL1MessagePasser_InitiateWithdrawal_Test /// @notice Tests the `initiateWithdrawal` function of the `L2ToL1MessagePasser` contract. @@ -22,6 +36,9 @@ contract L2ToL1MessagePasser_InitiateWithdrawal_Test is CommonTest { ) external { + if (isDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN)) { + _value = 0; + } uint256 nonce = l2ToL1MessagePasser.messageNonce(); bytes32 withdrawalHash = Hashing.hashWithdrawal( @@ -59,6 +76,7 @@ contract L2ToL1MessagePasser_InitiateWithdrawal_Test is CommonTest { ) external { + skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); bytes32 withdrawalHash = Hashing.hashWithdrawal( Types.WithdrawalTransaction({ nonce: l2ToL1MessagePasser.messageNonce(), @@ -89,6 +107,7 @@ contract L2ToL1MessagePasser_InitiateWithdrawal_Test is CommonTest { ) external { + skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); uint256 nonce = l2ToL1MessagePasser.messageNonce(); // EOA emulation @@ -107,6 +126,25 @@ contract L2ToL1MessagePasser_InitiateWithdrawal_Test is CommonTest { // the nonce increments assertEq(nonce + 1, l2ToL1MessagePasser.messageNonce()); } + + /// @notice Tests that `initiateWithdrawal` fails when called with value and custom gas token + /// is enabled. + function testFuzz_initiateWithdrawal_withValueAndCustomGasToken_fails( + address _randomAddress, + uint256 _value + ) + external + { + skipIfDevFeatureDisabled(DevFeatures.CUSTOM_GAS_TOKEN); + // Set initial state + _value = bound(_value, 1, type(uint256).max); + vm.deal(_randomAddress, _value); + + // Expect revert with NotAllowedOnCGTMode + vm.prank(_randomAddress); + vm.expectRevert(IL2ToL1MessagePasserCGT.L2ToL1MessagePasserCGT_NotAllowedOnCGTMode.selector); + l2ToL1MessagePasser.initiateWithdrawal{ value: _value }({ _target: address(0), _gasLimit: 1, _data: "" }); + } } /// @title L2ToL1MessagePasser_Burn_Test @@ -114,6 +152,7 @@ contract L2ToL1MessagePasser_InitiateWithdrawal_Test is CommonTest { contract L2ToL1MessagePasser_Burn_Test is CommonTest { /// @notice Tests that `burn` succeeds and destroys the ETH held in the contract. function testFuzz_burn_succeeds(uint256 _value, address _target, uint256 _gasLimit, bytes memory _data) external { + skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); vm.deal(address(this), _value); l2ToL1MessagePasser.initiateWithdrawal{ value: _value }({ _target: _target, _gasLimit: _gasLimit, _data: _data }); diff --git a/packages/contracts-bedrock/test/L2/LiquidityController.t.sol b/packages/contracts-bedrock/test/L2/LiquidityController.t.sol new file mode 100644 index 0000000000000..c43d3e3ee6eda --- /dev/null +++ b/packages/contracts-bedrock/test/L2/LiquidityController.t.sol @@ -0,0 +1,279 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Testing utilities +import { CommonTest } from "test/setup/CommonTest.sol"; +import { stdStorage, StdStorage } from "forge-std/Test.sol"; + +// Libraries +import { Predeploys } from "src/libraries/Predeploys.sol"; +import { DevFeatures } from "src/libraries/DevFeatures.sol"; + +// Contracts +import { LiquidityController } from "src/L2/LiquidityController.sol"; +import { NativeAssetLiquidity } from "src/L2/NativeAssetLiquidity.sol"; + +// Interfaces +import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; + +/// @title LiquidityController_TestInit +/// @notice Reusable test initialization for `LiquidityController` tests. +contract LiquidityController_TestInit is CommonTest { + using stdStorage for StdStorage; + + /// @notice Emitted when an address withdraws native asset liquidity. + event LiquidityWithdrawn(address indexed caller, uint256 value); + + /// @notice Emitted when an address deposits native asset liquidity. + event LiquidityDeposited(address indexed caller, uint256 value); + + /// @notice Emitted when an address is deauthorized to mint/burn liquidity + event MinterDeauthorized(address indexed minter); + + /// @notice Emitted when an address is authorized to mint/burn liquidity + event MinterAuthorized(address indexed minter); + + /// @notice Emitted when liquidity is minted + event LiquidityMinted(address indexed minter, address indexed to, uint256 amount); + + /// @notice Emitted when liquidity is burned + event LiquidityBurned(address indexed minter, uint256 amount); + + /// @notice Test setup. + function setUp() public virtual override { + super.setUp(); + skipIfDevFeatureDisabled(DevFeatures.CUSTOM_GAS_TOKEN); + } + + /// @notice Helper function to authorize a minter. + function _authorizeMinter(address _minter) internal { + assumeNotForgeAddress(_minter); + assumeNotZeroAddress(_minter); + // Authorize the minter + stdstore.target(address(liquidityController)).sig(liquidityController.minters.selector).with_key(_minter) + .checked_write(true); + } +} + +/// @title LiquidityController_Version_Test +/// @notice Tests the `version` function of the `LiquidityController` contract. +contract LiquidityController_Version_Test is LiquidityController_TestInit { + /// @notice Tests that the version function returns a valid string. + function test_version_succeeds() public view { + assert(bytes(liquidityController.version()).length > 0); + } +} + +/// @title LiquidityController_GasPayingTokenName_Test +/// @notice Tests the `gasPayingTokenName` function of the `LiquidityController` contract. +contract LiquidityController_GasPayingTokenName_Test is LiquidityController_TestInit { + /// @notice Tests that the `version` function returns the correct string. We avoid testing the + /// specific value of the string as it changes frequently. + function test_gasPayingTokenName_succeeds() public view { + assertTrue(bytes(liquidityController.gasPayingTokenName()).length > 0); + } +} + +/// @title LiquidityController_GasPayingTokenSymbol_Test +/// @notice Tests the `gasPayingTokenSymbol` function of the `LiquidityController` contract. +contract LiquidityController_GasPayingTokenSymbol_Test is LiquidityController_TestInit { + /// @notice Tests that the gasPayingTokenSymbol function returns a valid string. + function test_gasPayingTokenSymbol_succeeds() public view { + assertTrue(bytes(liquidityController.gasPayingTokenSymbol()).length > 0); + } +} + +/// @title LiquidityController_AuthorizeMinter_Test +/// @notice Tests the `authorizeMinter` function of the `LiquidityController` contract. +contract LiquidityController_AuthorizeMinter_Test is LiquidityController_TestInit { + /// @notice Tests that the authorizeMinter function can be called by the owner. + function testFuzz_authorizeMinter_fromOwner_succeeds(address _minter) public { + // Expect emit MinterAuthorized event + vm.expectEmit(address(liquidityController)); + emit MinterAuthorized(_minter); + // Call the authorizeMinter function with owner as the caller + vm.prank(IProxyAdmin(Predeploys.PROXY_ADMIN).owner()); + liquidityController.authorizeMinter(_minter); + + // Assert minter is authorized + assertTrue(liquidityController.minters(_minter)); + } + + /// @notice Tests that the authorizeMinter function reverts when called by non-owner. + function testFuzz_authorizeMinter_fromNonOwner_fails(address _caller, address _minter) public { + vm.assume(_caller != IProxyAdmin(Predeploys.PROXY_ADMIN).owner()); + + // Call the authorizeMinter function with non-owner as the caller + vm.prank(_caller); + vm.expectRevert(LiquidityController.LiquidityController_Unauthorized.selector); + liquidityController.authorizeMinter(_minter); + + // Assert minter is not authorized + assertFalse(liquidityController.minters(_minter)); + } +} + +/// @title LiquidityController_DeauthorizeMinter_Test +/// @notice Tests the `deauthorizeMinter` function of the `LiquidityController` contract. +contract LiquidityController_DeauthorizeMinter_Test is LiquidityController_TestInit { + /// @notice Tests that the deauthorizeMinter function can be called by the owner. + function testFuzz_deauthorizeMinter_fromOwner_succeeds(address _minter) public { + // Set minter to authorized + _authorizeMinter(_minter); + + // Expect emit MinterDeauthorized event + vm.expectEmit(address(liquidityController)); + emit MinterDeauthorized(_minter); + // Call the deauthorizeMinter function with owner as the caller + vm.prank(IProxyAdmin(Predeploys.PROXY_ADMIN).owner()); + liquidityController.deauthorizeMinter(_minter); + + // Assert minter is deauthorized + assertFalse(liquidityController.minters(_minter)); + } + + /// @notice Tests that the deauthorizeMinter function reverts when called by non-owner. + function testFuzz_deauthorizeMinter_fromNonOwner_fails(address _caller, address _minter) public { + vm.assume(_caller != IProxyAdmin(Predeploys.PROXY_ADMIN).owner()); + + // Set minter to authorized + _authorizeMinter(_minter); + + // Call the deauthorizeMinter function with non-owner as the caller + vm.prank(_caller); + vm.expectRevert(LiquidityController.LiquidityController_Unauthorized.selector); + liquidityController.deauthorizeMinter(_minter); + + // Assert minter is still authorized + assertTrue(liquidityController.minters(_minter)); + } +} + +/// @title LiquidityController_Mint_Test +/// @notice Tests the `mint` function of the `LiquidityController` contract. +contract LiquidityController_Mint_Test is LiquidityController_TestInit { + /// @notice Tests that the mint function can be called by an authorized minter. + function testFuzz_mint_fromAuthorizedMinter_succeeds(address _to, uint256 _amount, address _minter) public { + _authorizeMinter(_minter); + vm.assume(_to != address(nativeAssetLiquidity)); + _amount = bound(_amount, 1, address(nativeAssetLiquidity).balance); + + // Record initial balances + uint256 nativeAssetBalanceBefore = address(nativeAssetLiquidity).balance; + uint256 toBalanceBefore = _to.balance; + + // Expect emit LiquidityWithdrawn event and call the mint function + vm.expectEmit(address(nativeAssetLiquidity)); + emit LiquidityWithdrawn(address(liquidityController), _amount); + // Expect emit LiquidityMinted event + vm.expectEmit(address(liquidityController)); + emit LiquidityMinted(_minter, _to, _amount); + vm.prank(_minter); + liquidityController.mint(_to, _amount); + + // Assert recipient and NativeAssetLiquidity balances are updated correctly + assertEq(_to.balance, toBalanceBefore + _amount); + assertEq(address(nativeAssetLiquidity).balance, nativeAssetBalanceBefore - _amount); + } + + /// @notice Tests that the mint function reverts when called by unauthorized address. + function testFuzz_mint_fromUnauthorizedCaller_fails(address _caller, address _to, uint256 _amount) public { + _amount = bound(_amount, 1, address(nativeAssetLiquidity).balance); + + uint256 nativeAssetBalanceBefore = address(nativeAssetLiquidity).balance; + uint256 toBalanceBefore = _to.balance; + + // Call the mint function with unauthorized caller + vm.prank(_caller); + vm.expectRevert(LiquidityController.LiquidityController_Unauthorized.selector); + liquidityController.mint(_to, _amount); + + // Assert recipient and NativeAssetLiquidity balances remain unchanged + assertEq(_to.balance, toBalanceBefore); + assertEq(address(nativeAssetLiquidity).balance, nativeAssetBalanceBefore); + } + + /// @notice Tests that the mint function reverts when contract has insufficient balance. + function test_mint_insufficientBalance_fails(address _minter) public { + _authorizeMinter(_minter); + // Try to mint more than available balance + uint256 contractBalance = address(nativeAssetLiquidity).balance; + uint256 amount = bound(contractBalance, contractBalance + 1, type(uint256).max); + address to = makeAddr("recipient"); + + // Call the mint function with insufficient balance + vm.prank(_minter); + // Should revert due to insufficient balance in NativeAssetLiquidity + vm.expectRevert(NativeAssetLiquidity.NativeAssetLiquidity_InsufficientBalance.selector); + + liquidityController.mint(to, amount); + + // Assert recipient and NativeAssetLiquidity balances remain unchanged + assertEq(to.balance, 0); + assertEq(address(nativeAssetLiquidity).balance, contractBalance); + } +} + +/// @title LiquidityController_Burn_Test +/// @notice Tests the `burn` function of the `LiquidityController` contract. +contract LiquidityController_Burn_Test is LiquidityController_TestInit { + /// @notice Tests that the burn function can be called by an authorized minter. + function testFuzz_burn_fromAuthorizedMinter_succeeds(uint256 _amount, address _minter) public { + _authorizeMinter(_minter); + _amount = bound(_amount, 0, address(nativeAssetLiquidity).balance); + + // Deal the authorized minter with the amount to burn + vm.deal(_minter, _amount); + uint256 nativeAssetBalanceBefore = address(nativeAssetLiquidity).balance; + uint256 minterBalanceBefore = _minter.balance; + + // Expect emit LiquidityDeposited event and call the burn function + vm.expectEmit(address(nativeAssetLiquidity)); + emit LiquidityDeposited(address(liquidityController), _amount); + // Expect emit LiquidityBurned event + vm.expectEmit(address(liquidityController)); + emit LiquidityBurned(_minter, _amount); + vm.prank(_minter); + liquidityController.burn{ value: _amount }(); + + // Assert minter and NativeAssetLiquidity balances are updated correctly + assertEq(_minter.balance, minterBalanceBefore - _amount); + assertEq(address(nativeAssetLiquidity).balance, nativeAssetBalanceBefore + _amount); + } + + /// @notice Tests that the burn function reverts when called by unauthorized address. + function testFuzz_burn_fromUnauthorizedCaller_fails(address _caller, uint256 _amount, address _minter) public { + _authorizeMinter(_minter); + vm.assume(_caller != _minter); + _amount = bound(_amount, 0, address(nativeAssetLiquidity).balance); + + // Deal the unauthorized caller with the amount to burn + vm.deal(_caller, _amount); + uint256 nativeAssetBalanceBefore = address(nativeAssetLiquidity).balance; + uint256 callerBalanceBefore = _caller.balance; + + // Call the burn function with unauthorized caller + vm.prank(_caller); + vm.expectRevert(LiquidityController.LiquidityController_Unauthorized.selector); + liquidityController.burn{ value: _amount }(); + + // Assert caller and NativeAssetLiquidity balances remain unchanged + assertEq(_caller.balance, callerBalanceBefore); + assertEq(address(nativeAssetLiquidity).balance, nativeAssetBalanceBefore); + } +} + +/// @title LiquidityController_Initialize_Test +/// @notice Tests the `initialize` function of the `LiquidityController` contract. +contract LiquidityController_Initialize_Test is LiquidityController_TestInit { + /// @notice Tests that calling initialize on the implementation contract reverts. + function test_initialize_implementation_reverts() public { + // Deploy a new implementation contract directly (not through proxy) + LiquidityController implementation = new LiquidityController(); + + // Try to initialize the implementation contract directly + // This should revert because _disableInitializers() was called in the constructor + vm.expectRevert("Initializable: contract is already initialized"); + implementation.initialize("Test Token", "TEST"); + } +} diff --git a/packages/contracts-bedrock/test/L2/NativeAssetLiquidity.t.sol b/packages/contracts-bedrock/test/L2/NativeAssetLiquidity.t.sol new file mode 100644 index 0000000000000..2b5869b556596 --- /dev/null +++ b/packages/contracts-bedrock/test/L2/NativeAssetLiquidity.t.sol @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Testing utilities +import { CommonTest } from "test/setup/CommonTest.sol"; + +// Libraries +import { DevFeatures } from "src/libraries/DevFeatures.sol"; +import { NativeAssetLiquidity } from "src/L2/NativeAssetLiquidity.sol"; + +/// @title NativeAssetLiquidity_TestInit +/// @notice Reusable test initialization for `NativeAssetLiquidity` tests. +contract NativeAssetLiquidity_TestInit is CommonTest { + /// @notice Emitted when an address withdraws native asset liquidity. + event LiquidityWithdrawn(address indexed caller, uint256 value); + + /// @notice Emitted when an address deposits native asset liquidity. + event LiquidityDeposited(address indexed caller, uint256 value); + + /// @notice Emitted when an address funds the contract. + event LiquidityFunded(address indexed funder, uint256 value); + + /// @notice Test setup. + function setUp() public virtual override { + super.setUp(); + skipIfDevFeatureDisabled(DevFeatures.CUSTOM_GAS_TOKEN); + } +} + +/// @title NativeAssetLiquidity_Version_Test +/// @notice Tests the `version` function of the `NativeAssetLiquidity` contract. +contract NativeAssetLiquidity_Version_Test is NativeAssetLiquidity_TestInit { + /// @notice Tests that the `version` function returns the correct string. We avoid testing the + /// specific value of the string as it changes frequently. + function test_version_succeeds() public view { + assert(bytes(nativeAssetLiquidity.version()).length > 0); + } +} + +/// @title NativeAssetLiquidity_Deposit_Test +/// @notice Tests the `deposit` function of the `NativeAssetLiquidity` contract. +contract NativeAssetLiquidity_Deposit_Test is NativeAssetLiquidity_TestInit { + /// @notice Tests that the deposit function can be called by the authorized caller. + /// @param _amount Amount of native asset (in wei) to call the deposit function with. + function testFuzz_deposit_fromAuthorizedCaller_succeeds(uint256 _amount) public { + _amount = bound(_amount, 0, type(uint248).max); + + // Deal the LiquidityController with the amount to deposit + vm.deal(address(liquidityController), _amount); + uint256 nativeAssetBalanceBefore = address(nativeAssetLiquidity).balance; + + // Expect emit LiquidityDeposited event + vm.expectEmit(address(nativeAssetLiquidity)); + emit LiquidityDeposited(address(liquidityController), _amount); + + // Call the deposit function with LiquidityController as the caller + vm.prank(address(liquidityController)); + nativeAssetLiquidity.deposit{ value: _amount }(); + + // Assert LiquidityController and NativeAssetLiquidity balances are updated correctly + assertEq(address(liquidityController).balance, 0); + assertEq(address(nativeAssetLiquidity).balance, nativeAssetBalanceBefore + _amount); + } + + /// @notice Tests that the deposit function always reverts when called by an unauthorized caller. + /// @param _amount Amount of native asset (in wei) to call the deposit function with. + /// @param _caller Address of the caller to call the deposit function with. + function testFuzz_deposit_fromUnauthorizedCaller_fails(uint256 _amount, address _caller) public { + vm.assume(_caller != address(liquidityController)); + _amount = bound(_amount, 0, type(uint248).max); + + // Deal the unauthorized caller with the amount to deposit + vm.deal(_caller, _amount); + uint256 nativeAssetBalanceBefore = address(nativeAssetLiquidity).balance; + + // Call the deposit function with unauthorized caller + vm.prank(_caller); + // Expect revert with Unauthorized + vm.expectRevert(NativeAssetLiquidity.NativeAssetLiquidity_Unauthorized.selector); + nativeAssetLiquidity.deposit{ value: _amount }(); + + // Assert caller and NativeAssetLiquidity balances remain unchanged + assertEq(_caller.balance, _amount); + assertEq(address(nativeAssetLiquidity).balance, nativeAssetBalanceBefore); + } +} + +/// @title NativeAssetLiquidity_Withdraw_Test +/// @notice Tests the `withdraw` function of the `NativeAssetLiquidity` contract. +contract NativeAssetLiquidity_Withdraw_Test is NativeAssetLiquidity_TestInit { + /// @notice Tests that the withdraw function can be called by the authorized caller. + /// @param _amount Amount of native asset (in wei) to call the withdraw function with. + function testFuzz_withdraw_fromAuthorizedCaller_succeeds(uint256 _amount) public { + _amount = bound(_amount, 1, type(uint248).max); + + // Deal NativeAssetLiquidity with the amount to withdraw + vm.deal(address(nativeAssetLiquidity), _amount); + uint256 nativeAssetBalanceBefore = address(nativeAssetLiquidity).balance; + uint256 controllerBalanceBefore = address(liquidityController).balance; + + // Expect emit LiquidityWithdrawn event + vm.expectEmit(address(nativeAssetLiquidity)); + emit LiquidityWithdrawn(address(liquidityController), _amount); + vm.prank(address(liquidityController)); + nativeAssetLiquidity.withdraw(_amount); + + // Assert LiquidityController and NativeAssetLiquidity balances are updated correctly + assertEq(address(liquidityController).balance, controllerBalanceBefore + _amount); + assertEq(address(nativeAssetLiquidity).balance, nativeAssetBalanceBefore - _amount); + } + + /// @notice Tests that the withdraw function always reverts when called by an unauthorized caller. + /// @param _amount Amount of native asset (in wei) to call the withdraw function with. + /// @param _caller Address of the caller to call the withdraw function with. + function testFuzz_withdraw_fromUnauthorizedCaller_fails(uint256 _amount, address _caller) public { + vm.assume(_caller != address(liquidityController)); + _amount = bound(_amount, 1, type(uint248).max); + + // Deal NativeAssetLiquidity with the amount to withdraw + vm.deal(address(nativeAssetLiquidity), _amount); + uint256 nativeAssetBalanceBefore = address(nativeAssetLiquidity).balance; + uint256 callerBalanceBefore = _caller.balance; + + // Call the withdraw function with unauthorized caller + vm.prank(_caller); + // Expect revert with Unauthorized + vm.expectRevert(NativeAssetLiquidity.NativeAssetLiquidity_Unauthorized.selector); + nativeAssetLiquidity.withdraw(_amount); + + // Assert caller and NativeAssetLiquidity balances remain unchanged + assertEq(_caller.balance, callerBalanceBefore); + assertEq(address(nativeAssetLiquidity).balance, nativeAssetBalanceBefore); + } + + /// @notice Tests that the withdraw function reverts when contract has insufficient balance. + function test_withdraw_insufficientBalance_fails() public { + // Try to withdraw more than available balance + uint256 contractBalance = address(nativeAssetLiquidity).balance; + uint256 amount = bound(contractBalance, contractBalance + 1, type(uint256).max); + + // Call the withdraw function with insufficient balance + vm.prank(address(liquidityController)); + // Expect revert with NativeAssetLiquidity_InsufficientBalance + vm.expectRevert(NativeAssetLiquidity.NativeAssetLiquidity_InsufficientBalance.selector); + nativeAssetLiquidity.withdraw(amount); + + // Assert contract and controller balances remain unchanged + assertEq(address(nativeAssetLiquidity).balance, contractBalance); + assertEq(address(liquidityController).balance, 0); + } +} diff --git a/packages/contracts-bedrock/test/L2/SequencerFeeVault.t.sol b/packages/contracts-bedrock/test/L2/SequencerFeeVault.t.sol index 55c5a7eb72991..ab808f1bac122 100644 --- a/packages/contracts-bedrock/test/L2/SequencerFeeVault.t.sol +++ b/packages/contracts-bedrock/test/L2/SequencerFeeVault.t.sol @@ -14,6 +14,7 @@ import { Hashing } from "src/libraries/Hashing.sol"; import { Types } from "src/libraries/Types.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; +import { DevFeatures } from "src/libraries/DevFeatures.sol"; /// @title SequencerFeeVault_TestInit /// @notice Reusable test initialization for `SequencerFeeVault` tests. @@ -37,8 +38,8 @@ contract SequencerFeeVault_Constructor_Test is SequencerFeeVault_TestInit { assertEq(sequencerFeeVault.recipient(), recipient); assertEq(sequencerFeeVault.MIN_WITHDRAWAL_AMOUNT(), deploy.cfg().sequencerFeeVaultMinimumWithdrawalAmount()); assertEq(sequencerFeeVault.minWithdrawalAmount(), deploy.cfg().sequencerFeeVaultMinimumWithdrawalAmount()); - assertEq(uint8(sequencerFeeVault.WITHDRAWAL_NETWORK()), uint8(Types.WithdrawalNetwork.L1)); - assertEq(uint8(sequencerFeeVault.withdrawalNetwork()), uint8(Types.WithdrawalNetwork.L1)); + assertEq(uint8(sequencerFeeVault.WITHDRAWAL_NETWORK()), deploy.cfg().sequencerFeeVaultWithdrawalNetwork()); + assertEq(uint8(sequencerFeeVault.withdrawalNetwork()), deploy.cfg().sequencerFeeVaultWithdrawalNetwork()); } } @@ -96,6 +97,7 @@ contract SequencerFeeVault_Withdraw_Test is SequencerFeeVault_TestInit { /// @notice Tests that `withdraw` successfully initiates a withdrawal to L1. function test_withdraw_toL1_succeeds() external { + skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); uint256 amount = sequencerFeeVault.MIN_WITHDRAWAL_AMOUNT() + 1; vm.deal(address(sequencerFeeVault), amount); @@ -105,7 +107,12 @@ contract SequencerFeeVault_Withdraw_Test is SequencerFeeVault_TestInit { vm.expectEmit(address(Predeploys.SEQUENCER_FEE_WALLET)); emit Withdrawal(address(sequencerFeeVault).balance, recipient, address(this)); vm.expectEmit(address(Predeploys.SEQUENCER_FEE_WALLET)); - emit Withdrawal(address(sequencerFeeVault).balance, recipient, address(this), Types.WithdrawalNetwork.L1); + emit Withdrawal( + address(sequencerFeeVault).balance, + recipient, + address(this), + Types.WithdrawalNetwork(deploy.cfg().sequencerFeeVaultWithdrawalNetwork()) + ); // The entire vault's balance is withdrawn vm.expectCall(Predeploys.L2_TO_L1_MESSAGE_PASSER, address(sequencerFeeVault).balance, hex""); diff --git a/packages/contracts-bedrock/test/L2/WETH.t.sol b/packages/contracts-bedrock/test/L2/WETH.t.sol index 4b1cfa1751d13..694183115e0a0 100644 --- a/packages/contracts-bedrock/test/L2/WETH.t.sol +++ b/packages/contracts-bedrock/test/L2/WETH.t.sol @@ -16,7 +16,7 @@ contract WETH_Name_Test is CommonTest { /// @notice Tests that the `name` function returns 'Wrapped Ether' by default. function test_name_ether_succeeds() external view { - assertEq("Wrapped Ether", weth.name()); + assertEq(string.concat("Wrapped ", l1Block.gasPayingTokenName()), weth.name()); } } @@ -34,6 +34,6 @@ contract WETH_Symbol_Test is CommonTest { /// @notice Tests that the `symbol` function returns 'WETH' by default. function test_symbol_ether_succeeds() external view { - assertEq("WETH", weth.symbol()); + assertEq(string.concat("W", l1Block.gasPayingTokenSymbol()), weth.symbol()); } } diff --git a/packages/contracts-bedrock/test/invariants/CustomGasToken.t.sol b/packages/contracts-bedrock/test/invariants/CustomGasToken.t.sol new file mode 100644 index 0000000000000..fe96f009e3023 --- /dev/null +++ b/packages/contracts-bedrock/test/invariants/CustomGasToken.t.sol @@ -0,0 +1,258 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Testing +import { StdUtils } from "forge-std/Test.sol"; +import { Vm } from "forge-std/Vm.sol"; +import { CommonTest } from "test/setup/CommonTest.sol"; + +// Libraries +import { Predeploys } from "src/libraries/Predeploys.sol"; +import { DevFeatures } from "src/libraries/DevFeatures.sol"; + +// Contracts +import { ILiquidityController } from "interfaces/L2/ILiquidityController.sol"; +import { INativeAssetLiquidity } from "interfaces/L2/INativeAssetLiquidity.sol"; +import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; + +/// @title CGT_Minter +/// @notice An actor with the minter role (can mint and burn) +contract LiquidityController_Minter is StdUtils { + /// @notice The Vm contract. + Vm internal vm; + + /// @notice The LiquidityController contract. + ILiquidityController internal liquidityController; + + /// @notice The RandomActor contract. + RandomActor internal randomActor; + + /// @notice Ghost accounting + uint256 public totalAmountMinted; + uint256 public totalAmountBurned; + bool public deltaBalanceAndMint; // NativeAssetLiquidity balance change != amount minted? + bool public deltaBalanceAndBurn; // NativeAssetLiquidity balance change != amount burned? + + /// @param _vm The Vm contract. + /// @param _liquidityController The LiquidityController contract. + /// @param _randomActor The RandomActor contract. + constructor(Vm _vm, ILiquidityController _liquidityController, RandomActor _randomActor) { + vm = _vm; + liquidityController = _liquidityController; + randomActor = _randomActor; + } + + /// @notice Mint custom gas token to the random actor. + /// @param _amount The amount of CGT to mint. + /// @dev Accounting invariants are leveraging the balance difference between pre and post-condition + function mint(uint256 _amount) public { + // precondition: nil - update ghost variables + uint256 _preBalance = payable(Predeploys.NATIVE_ASSET_LIQUIDITY).balance; + + // action: mint to the random actor + liquidityController.mint(address(randomActor), _amount); + + // postcondition: is the NativeAssetLiquidity contract's balance changed by an amount different than minted? + deltaBalanceAndMint = _amount != (_preBalance - uint256(payable(Predeploys.NATIVE_ASSET_LIQUIDITY).balance)); + totalAmountMinted += _amount; + } + + /// @notice Burn custom gas token. + /// @param _amount The amount of CGT to burn, which is bounded to the actor's balance (avoid trivial revert) + /// @dev Accounting invariant are leveraging the balance difference between pre and post-condition + function burn(uint256 _amount) public { + // precondition: amount to burn has an upper bound (this contract's balance) + _amount = bound(_amount, 0, address(this).balance); + uint256 _preBalance = payable(Predeploys.NATIVE_ASSET_LIQUIDITY).balance; + + // action: burn _amount + liquidityController.burn{ value: _amount }(); + + // postcondition: update ghost variables by tracking an accounting difference + deltaBalanceAndBurn = _preBalance + _amount != uint256(payable(Predeploys.NATIVE_ASSET_LIQUIDITY).balance); + totalAmountBurned += _amount; + } + + /// @dev Receive needed to receive CGT from the random actor + receive() external payable { } +} + +/// @notice An actor which funds the NativeAssetLiquidity contract +/// @dev There is no underlying access control to this +contract NativeAssetLiquidity_Fundooor is StdUtils { + /// @notice The Vm contract. + Vm internal vm; + + /// @notice The NativeAssetLiquidity contract. + INativeAssetLiquidity internal nativeAssetLiquidity; + + /// @notice Ghost accounting + uint256 public totalAmountFunded; + + /// @param _vm The Vm contract. + constructor(Vm _vm) { + vm = _vm; + nativeAssetLiquidity = INativeAssetLiquidity(Predeploys.NATIVE_ASSET_LIQUIDITY); + } + + /// @notice Wrap fund() calls on the NativeAssetLiquidity contract. + /// @param _amount The amount of CGT to fund. + /// @dev The amount is bounded to the actor's balance (avoid trivial revert) + function fund(uint256 _amount) public { + // precondition: amount to fund has an upper bound (this contract's balance) + ghost accounting + _amount = bound(_amount, 0, address(this).balance); + + // action: fund _amount + vm.deal(address(nativeAssetLiquidity), _amount); + + // postcondition: nil here (in the invariant tests) + // update ghost variables + totalAmountFunded += _amount; + } + + receive() external payable { } +} + +/// @notice actor which receives fund and send them to either the minter or the funder actor, +/// keeping a closed loop (no vm.deal). It receive() function always revert, to insure mint()/safeSend is +/// always successfully sending the CGT. +contract RandomActor is StdUtils { + address internal liquidityController_Minter; + address internal nativeAssetLiquidity_Fundooor; + + /// @notice Flag to indicate if the actor has been called via receive() + bool public hasBeenCalled = false; + + /// @notice Error thrown when sending CGT to minter fails. + error RandomActor_SendCGTToMinterFailed(); + + /// @notice Error thrown when sending CGT to funder fails. + error RandomActor_SendCGTtoFunderFailed(); + + /// @notice Initialize the addresses of the minter and funder actors. + /// @param _liquidityController_Minter The address of the minter actor. + /// @param _nativeAssetLiquidity_Fundooor The address of the funder actor. + /// @dev This function selector is excluded from the invariant tests + function initAddresses(address _liquidityController_Minter, address _nativeAssetLiquidity_Fundooor) public { + liquidityController_Minter = _liquidityController_Minter; + nativeAssetLiquidity_Fundooor = _nativeAssetLiquidity_Fundooor; + } + + /// @notice Send CGT to the minter actor. + /// @param _amount The amount of CGT to send. + /// @dev The amount is bounded to the actor's balance (avoid trivial revert) + function sendCGTtoMinter(uint256 _amount) public { + // precondition: amount to send has an upper bound (this contract's balance) + uint256 _amountToSend = bound(_amount, 0, address(this).balance); + + // action: send _amountToSend to the minter actor + (bool success,) = payable(address(liquidityController_Minter)).call{ value: _amountToSend }(""); + + // postcondition: the call must succeed (test suite sanity check) + if (!success) revert RandomActor_SendCGTToMinterFailed(); + } + + /// @notice Send CGT to the funder actor. + /// @param _amount The amount of CGT to send. + /// @dev The amount is bounded to the actor's balance (avoid trivial revert) + function sendCGTtoFunder(uint256 _amount) public { + // precondition: amount to send has an upper bound (this contract's balance) + uint256 _amountToSend = bound(_amount, 0, address(this).balance); + + // action: send _amountToSend to the funder actor + (bool success,) = payable(address(nativeAssetLiquidity_Fundooor)).call{ value: _amountToSend }(""); + + // postcondition: the call must succeed (test suite sanity check) + if (!success) revert RandomActor_SendCGTtoFunderFailed(); + } + + /// @dev We track if the SafeSend triggers a logic on the receiver via a ghost variable + receive() external payable { + hasBeenCalled = true; + } + + fallback() external payable { + hasBeenCalled = true; + } +} + +/// @title ETHLiquidity_MintBurn_Invariant +/// @notice Invariant that checks that the NativeAssetLiquidity contract's balance is always equal +/// to the sum of the initial supply, the deposits, the funds, and minus the withdrawals. +/// NAL Balance = Initial Supply + Deposits + Funds - Withdrawals +contract CustomGasToken_Invariants_Test is CommonTest { + /// @notice Starting balance of the contract - arbitrary value (cf Config change) + uint256 internal constant STARTING_BALANCE = type(uint248).max / 5; + + LiquidityController_Minter internal actor_minter; + NativeAssetLiquidity_Fundooor internal actor_funder; + RandomActor internal randomActor; + + /// @notice Test setup. + function setUp() public override { + skipIfDevFeatureDisabled(DevFeatures.CUSTOM_GAS_TOKEN); + super.setUp(); + + randomActor = new RandomActor(); + actor_funder = new NativeAssetLiquidity_Fundooor(vm); + actor_minter = new LiquidityController_Minter(vm, liquidityController, randomActor); + + // Initialize the addresses of the minter and funder actors + randomActor.initAddresses(address(actor_minter), address(actor_funder)); + + // Authorize the minter actor (simple access control in unit tests) + vm.prank(IProxyAdmin(Predeploys.PROXY_ADMIN).owner()); + liquidityController.authorizeMinter(address(actor_minter)); + + // Create the initial supply + vm.deal(address(nativeAssetLiquidity), STARTING_BALANCE); + + // Set the target contract. + targetContract(address(actor_minter)); + targetContract(address(actor_funder)); + + // Set the target selectors (exclude the initAddresses function) + bytes4[] memory selectors = new bytes4[](2); + selectors[0] = RandomActor.sendCGTtoMinter.selector; + selectors[1] = RandomActor.sendCGTtoFunder.selector; + FuzzSelector memory selector = FuzzSelector({ addr: address(randomActor), selectors: selectors }); + targetSelector(selector); + } + + /// @notice Invariant that checks that the NativeAssetLiquidity contract's balance is always equal + /// to the sum of the initial supply, the deposits, the funds, and minus the withdrawals. + /// NAL Balance = Initial Supply + Deposits + Funds - Withdrawals + /// @dev liquidityController.burn() calls deposit, liquidityController.mint() calls withdraw + function invariant_supplyConservation() public view { + assertEq( + address(nativeAssetLiquidity).balance, + STARTING_BALANCE + actor_funder.totalAmountFunded() + actor_minter.totalAmountBurned() + - actor_minter.totalAmountMinted(), + "NativeAssetLiquidity balance is not equal to the sum of the initial supply, the deposits, the funds, and minus the withdrawals" + ); + } + + /// @notice Invariant that checks that the minted amount is equal to the withdrawn amount + /// @dev Checks if the amount minted equals the amount transferred *outside* the NativeAssetLiquidity contract + function invariant_mintedEqualsWithdrawn() public view { + assertFalse(actor_minter.deltaBalanceAndMint(), "Minted amount is not equal to the withdrawn amount"); + } + + /// @notice Invariant that checks that the burned amount is equal to the deposited amount + /// @dev Checks if the amount burned equals the amount transferred *to* the NativeAssetLiquidity contract + function invariant_burnedEqualsDeposited() public view { + assertFalse(actor_minter.deltaBalanceAndBurn(), "Burned amount is not equal to the deposited amount"); + } + + /// @notice Invariant that checks that the LiquidityController contract's balance is always 0 + /// @dev Checks if the LiquidityController there is no CGT being trapped in the LiquidityController contract + function invariant_noDustLiquidityController() public view { + assertEq(address(liquidityController).balance, 0, "LiquidityController balance is not 0"); + } + + /// @notice Invariant that checks that the mint function never calls back to the RandomActor contract + /// @dev Checks if the mint function never calls back to the RandomActor contract (test SafeSend) + function invariant_mintNeverCallsBack() public view { + assertFalse(randomActor.hasBeenCalled(), "RandomActor receive() function has been triggered"); + } +} diff --git a/packages/contracts-bedrock/test/invariants/OptimismPortal2.t.sol b/packages/contracts-bedrock/test/invariants/OptimismPortal2.t.sol index 63d05ad7a4f94..f826e0262914a 100644 --- a/packages/contracts-bedrock/test/invariants/OptimismPortal2.t.sol +++ b/packages/contracts-bedrock/test/invariants/OptimismPortal2.t.sol @@ -14,6 +14,7 @@ import { ResourceMetering } from "src/L1/ResourceMetering.sol"; // Libraries import { Constants } from "src/libraries/Constants.sol"; import { Types } from "src/libraries/Types.sol"; +import { Features } from "src/libraries/Features.sol"; import "src/dispute/lib/Types.sol"; // Interfaces @@ -67,6 +68,11 @@ contract OptimismPortal2_Depositor is StdUtils, ResourceMetering { uint256 preDepositBalance = address(this).balance; uint256 value = bound(preDepositvalue, 0, preDepositBalance); + // If custom gas token is enabled, set deposit value to 0 + if (portal.systemConfig().isFeatureEnabled(Features.CUSTOM_GAS_TOKEN)) { + value = 0; + } + (, uint64 cachedPrevBoughtGas,) = ResourceMetering(address(portal)).params(); ResourceMetering.ResourceConfig memory rcfg = resourceConfig(); uint256 maxResourceLimit = uint64(rcfg.maxResourceLimit); @@ -106,6 +112,12 @@ contract OptimismPortal2_Invariant_Harness is DisputeGameFactory_TestInit { gasLimit: 100_000, data: hex"" }); + + // If custom gas token is enabled, set deposit value to 0 + if (systemConfig.isFeatureEnabled(Features.CUSTOM_GAS_TOKEN)) { + _defaultTx.value = 0; + } + // Get withdrawal proof data we can use for testing. (_stateRoot, _storageRoot, _outputRoot, _withdrawalHash, _withdrawalProof) = ffi.getProveWithdrawalTransactionInputs(_defaultTx); diff --git a/packages/contracts-bedrock/test/libraries/Predeploys.t.sol b/packages/contracts-bedrock/test/libraries/Predeploys.t.sol index ff9cacefa5dbe..cd6ca72fd7efb 100644 --- a/packages/contracts-bedrock/test/libraries/Predeploys.t.sol +++ b/packages/contracts-bedrock/test/libraries/Predeploys.t.sol @@ -9,6 +9,7 @@ import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; import { ForgeArtifacts } from "scripts/libraries/ForgeArtifacts.sol"; import { Fork } from "scripts/libraries/Config.sol"; +import { DevFeatures } from "src/libraries/DevFeatures.sol"; /// @title Predeploys_TestInit /// @notice Reusable test initialization for `Predeploys` tests. @@ -23,6 +24,12 @@ contract Predeploys_TestInit is CommonTest { return _addr == Predeploys.L1_BLOCK_ATTRIBUTES || _addr == Predeploys.L2_STANDARD_BRIDGE; } + /// @notice Returns true if the address is a predeploy that has a different code in the + /// custom gas token mode. + function _customGasTokenCodeDiffer(address _addr) internal pure returns (bool) { + return _addr == Predeploys.L1_BLOCK_ATTRIBUTES || _addr == Predeploys.L2_TO_L1_MESSAGE_PASSER; + } + /// @notice Returns true if the account is not meant to be in the L2 genesis anymore. function _isOmitted(address _addr) internal pure returns (bool) { return _addr == Predeploys.L1_MESSAGE_SENDER; @@ -42,7 +49,7 @@ contract Predeploys_TestInit is CommonTest { } /// @notice Internal test function for predeploys validation across different forks. - function _test_predeploys(Fork _fork, bool _enableCrossL2Inbox) internal { + function _test_predeploys(Fork _fork, bool _enableCrossL2Inbox, bool _isCustomGasToken) internal { uint256 count = 2048; uint160 prefix = uint160(0x420) << 148; @@ -57,7 +64,8 @@ contract Predeploys_TestInit is CommonTest { continue; } - bool isPredeploy = Predeploys.isSupportedPredeploy(addr, uint256(_fork), _enableCrossL2Inbox); + bool isPredeploy = + Predeploys.isSupportedPredeploy(addr, uint256(_fork), _enableCrossL2Inbox, _isCustomGasToken); bytes memory code = addr.code; if (isPredeploy) assertTrue(code.length > 0); @@ -93,7 +101,7 @@ contract Predeploys_TestInit is CommonTest { string.concat("Implementation mismatch for ", vm.toString(addr)) ); assertNotEq(implAddr.code.length, 0, "predeploy implementation account must have code"); - if (!_usesImmutables(addr) && !_interopCodeDiffer(addr)) { + if (!_usesImmutables(addr) && !_interopCodeDiffer(addr) && !_customGasTokenCodeDiffer(addr)) { // can't check bytecode if it's modified with immutables in genesis. assertEq(implAddr.code, supposedCode, "proxy implementation contract should match contract source"); } @@ -133,7 +141,14 @@ contract Predeploys_Unclassified_Test is Predeploys_TestInit { /// @notice Tests that the predeploy addresses are set correctly. They have code /// and the proxied accounts have the correct admin. function test_predeploys_succeeds() external { - _test_predeploys(Fork.ISTHMUS, false); + _test_predeploys(Fork.ISTHMUS, false, false); + } + + /// @notice Tests that the predeploy addresses are set correctly. They have code + /// and the proxied accounts have the correct admin. Using custom gas token. + function test_predeploys_customGasToken_succeeds() external { + skipIfDevFeatureDisabled(DevFeatures.CUSTOM_GAS_TOKEN); + _test_predeploys(Fork.ISTHMUS, false, true); } } @@ -150,12 +165,12 @@ contract Predeploys_UnclassifiedInterop_Test is Predeploys_TestInit { /// @notice Tests that the predeploy addresses are set correctly. They have code and the /// proxied accounts have the correct admin. Using interop with inbox. function test_predeploysWithInbox_succeeds() external { - _test_predeploys(Fork.INTEROP, true); + _test_predeploys(Fork.INTEROP, true, false); } /// @notice Tests that the predeploy addresses are set correctly. They have code and the /// proxied accounts have the correct admin. Using interop without inbox. function test_predeploysWithoutInbox_succeeds() external { - _test_predeploys(Fork.INTEROP, false); + _test_predeploys(Fork.INTEROP, false, false); } } diff --git a/packages/contracts-bedrock/test/scripts/L2Genesis.t.sol b/packages/contracts-bedrock/test/scripts/L2Genesis.t.sol index 400f3285bf50b..843706aa3888a 100644 --- a/packages/contracts-bedrock/test/scripts/L2Genesis.t.sol +++ b/packages/contracts-bedrock/test/scripts/L2Genesis.t.sol @@ -15,6 +15,8 @@ import { IOptimismMintableERC721Factory } from "interfaces/L2/IOptimismMintableE import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; import { IGovernanceToken } from "interfaces/governance/IGovernanceToken.sol"; import { IGasPriceOracle } from "interfaces/L2/IGasPriceOracle.sol"; +import { ILiquidityController } from "interfaces/L2/ILiquidityController.sol"; +import { INativeAssetLiquidity } from "interfaces/L2/INativeAssetLiquidity.sol"; /// @title L2Genesis_TestInit /// @notice Reusable test initialization for `L2Genesis` tests. @@ -23,12 +25,22 @@ contract L2Genesis_TestInit is Test { L2Genesis internal genesis; - function setUp() public { + function setUp() public virtual { genesis = new L2Genesis(); } function testProxyAdmin() internal view { + // Verify owner in the proxy assertEq(input.opChainProxyAdminOwner, IProxyAdmin(Predeploys.PROXY_ADMIN).owner()); + + // Verify owner in the implementation to catch storage shifting issues + // The implementation is stored in the code namespace + address proxyAdminImpl = Predeploys.predeployToCodeNamespace(Predeploys.PROXY_ADMIN); + assertEq( + input.opChainProxyAdminOwner, + IProxyAdmin(proxyAdminImpl).owner(), + "ProxyAdmin implementation owner should match expected" + ); } function testPredeploys() internal view { @@ -47,7 +59,7 @@ contract L2Genesis_TestInit is Test { assertEq(Predeploys.PROXY_ADMIN, EIP1967Helper.getAdmin(addr)); // If it's not a supported predeploy, skip next checks. - if (!Predeploys.isSupportedPredeploy(addr, uint256(LATEST_FORK), true)) { + if (!Predeploys.isSupportedPredeploy(addr, uint256(LATEST_FORK), true, input.useCustomGasToken)) { continue; } @@ -81,7 +93,14 @@ contract L2Genesis_TestInit is Test { function testGovernance() internal view { IGovernanceToken token = IGovernanceToken(payable(Predeploys.GOVERNANCE_TOKEN)); + + // Verify owner (existing check) assertEq(token.owner(), input.governanceTokenOwner); + + // Verify name and symbol to catch storage shifting issues + // These should match the values hardcoded in GovernanceToken constructor + assertEq(token.name(), "Optimism", "GovernanceToken name should be 'Optimism'"); + assertEq(token.symbol(), "OP", "GovernanceToken symbol should be 'OP'"); } function testFactories() internal view { @@ -102,12 +121,29 @@ contract L2Genesis_TestInit is Test { assertEq(gasPriceOracle.isFjord(), true); assertEq(gasPriceOracle.isIsthmus(), true); } + + function testCGT() internal view { + // Test LiquidityController deployment + ILiquidityController controller = ILiquidityController(Predeploys.LIQUIDITY_CONTROLLER); + assertEq(controller.gasPayingTokenName(), input.gasPayingTokenName); + assertEq(controller.gasPayingTokenSymbol(), input.gasPayingTokenSymbol); + + // Test NativeAssetLiquidity deployment and funding + INativeAssetLiquidity liquidity = INativeAssetLiquidity(Predeploys.NATIVE_ASSET_LIQUIDITY); + assertEq(address(liquidity).balance, type(uint248).max); + + // Verify predeploys have code + assertGt(Predeploys.LIQUIDITY_CONTROLLER.code.length, 0); + assertGt(Predeploys.NATIVE_ASSET_LIQUIDITY.code.length, 0); + } } /// @title L2Genesis_Run_Test /// @notice Tests the `run` function of the `L2Genesis` contract. contract L2Genesis_Run_Test is L2Genesis_TestInit { - function test_run_succeeds() external { + function setUp() public override { + super.setUp(); + // Set up default input configuration input = L2Genesis.Input({ l1ChainID: 1, l2ChainID: 2, @@ -128,8 +164,15 @@ contract L2Genesis_Run_Test is L2Genesis_TestInit { fork: uint256(LATEST_FORK), deployCrossL2Inbox: true, enableGovernance: true, - fundDevAccounts: true + fundDevAccounts: true, + useCustomGasToken: false, + gasPayingTokenName: "", + gasPayingTokenSymbol: "", + nativeAssetLiquidityAmount: type(uint248).max }); + } + + function test_run_succeeds() external { genesis.run(input); testProxyAdmin(); @@ -139,4 +182,58 @@ contract L2Genesis_Run_Test is L2Genesis_TestInit { testFactories(); testForks(); } + + /// @notice Helper function to configure input for CGT enabled tests. + function _setInputCGTEnabled() internal { + input.useCustomGasToken = true; + input.gasPayingTokenName = "Custom Gas Token"; + input.gasPayingTokenSymbol = "CGT"; + } + + /// @notice Tests that the run function succeeds when CGT is enabled. + /// @dev Tests that LiquidityController and NativeAssetLiquidity are deployed. + function test_run_cgt_succeeds() external { + _setInputCGTEnabled(); + genesis.run(input); + + testProxyAdmin(); + testPredeploys(); + testVaults(); + testGovernance(); + testFactories(); + testForks(); + testCGT(); + } + + /// @notice Tests that the run function reverts when CGT is enabled and sequencerFeeVault withdrawal network is L1. + function test_cgt_sequencerVault_reverts() external { + _setInputCGTEnabled(); + input.sequencerFeeVaultWithdrawalNetwork = 0; + vm.expectRevert("SequencerFeeVault: withdrawalNetwork type cannot be L1 when custom gas token is enabled"); + genesis.run(input); + } + + /// @notice Tests that the run function reverts when CGT is enabled and baseFeeVault withdrawal network is L1. + function test_cgt_baseFeeVault_reverts() external { + _setInputCGTEnabled(); + input.baseFeeVaultWithdrawalNetwork = 0; + vm.expectRevert("BaseFeeVault: withdrawalNetwork type cannot be L1 when custom gas token is enabled"); + genesis.run(input); + } + + /// @notice Tests that the run function reverts when CGT is enabled and l1FeeVault withdrawal network is L1. + function test_cgt_l1FeeVault_reverts() external { + _setInputCGTEnabled(); + input.l1FeeVaultWithdrawalNetwork = 0; + vm.expectRevert("L1FeeVault: withdrawalNetwork type cannot be L1 when custom gas token is enabled"); + genesis.run(input); + } + + /// @notice Tests that the run function reverts when nativeAssetLiquidityAmount exceeds type(uint248).max. + function test_cgt_liquidityAmount_reverts() external { + _setInputCGTEnabled(); + input.nativeAssetLiquidityAmount = uint256(type(uint248).max) + 1; + vm.expectRevert("L2Genesis: native asset liquidity amount must be less than or equal to type(uint248).max"); + genesis.run(input); + } } diff --git a/packages/contracts-bedrock/test/setup/CommonTest.sol b/packages/contracts-bedrock/test/setup/CommonTest.sol index a117be29a50b1..280a7326d1c25 100644 --- a/packages/contracts-bedrock/test/setup/CommonTest.sol +++ b/packages/contracts-bedrock/test/setup/CommonTest.sol @@ -14,6 +14,7 @@ import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; // Contracts import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { DevFeatures } from "src/libraries/DevFeatures.sol"; // Libraries import { console } from "forge-std/console.sol"; @@ -34,6 +35,7 @@ contract CommonTest is Test, Setup, Events { bool useAltDAOverride; bool useInteropOverride; + bool useCustomGasToken; /// @dev This value is only used in forked tests. During forked tests, the default is to perform the upgrade before /// running the tests. @@ -74,6 +76,16 @@ contract CommonTest is Test, Setup, Events { if (useUpgradedFork) { deploy.cfg().setUseUpgradedFork(true); } + if (isDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN)) { + console.log("CommonTest: enabling custom gas token"); + deploy.cfg().setUseCustomGasToken(true); + deploy.cfg().setGasPayingTokenName("Custom Gas Token"); + deploy.cfg().setGasPayingTokenSymbol("CGT"); + deploy.cfg().setNativeAssetLiquidityAmount(type(uint248).max); + deploy.cfg().setBaseFeeVaultWithdrawalNetwork(1); + deploy.cfg().setL1FeeVaultWithdrawalNetwork(1); + deploy.cfg().setSequencerFeeVaultWithdrawalNetwork(1); + } if (isForkTest()) { // Skip any test suite which uses a nonstandard configuration. diff --git a/packages/contracts-bedrock/test/setup/FeatureFlags.sol b/packages/contracts-bedrock/test/setup/FeatureFlags.sol index da0c6d6e3a570..2461b76170a64 100644 --- a/packages/contracts-bedrock/test/setup/FeatureFlags.sol +++ b/packages/contracts-bedrock/test/setup/FeatureFlags.sol @@ -40,6 +40,10 @@ contract FeatureFlags { console.log("Setup: DEV_FEATURE__CANNON_KONA is enabled"); devFeatureBitmap |= DevFeatures.CANNON_KONA; } + if (Config.devFeatureCustomGasToken()) { + console.log("Setup: DEV_FEATURE__CUSTOM_GAS_TOKEN is enabled"); + devFeatureBitmap |= DevFeatures.CUSTOM_GAS_TOKEN; + } } /// @notice Enables a feature. diff --git a/packages/contracts-bedrock/test/setup/Setup.sol b/packages/contracts-bedrock/test/setup/Setup.sol index 4d6453932799e..b80c45e597e40 100644 --- a/packages/contracts-bedrock/test/setup/Setup.sol +++ b/packages/contracts-bedrock/test/setup/Setup.sol @@ -62,6 +62,8 @@ import { ISuperchainTokenBridge } from "interfaces/L2/ISuperchainTokenBridge.sol import { IPermissionedDisputeGame } from "interfaces/dispute/IPermissionedDisputeGame.sol"; import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; import { ICrossL2Inbox } from "interfaces/L2/ICrossL2Inbox.sol"; +import { ILiquidityController } from "interfaces/L2/ILiquidityController.sol"; +import { INativeAssetLiquidity } from "interfaces/L2/INativeAssetLiquidity.sol"; /// @title Setup /// @dev This contact is responsible for setting up the contracts in state. It currently @@ -146,6 +148,8 @@ contract Setup is FeatureFlags { ISuperchainTokenBridge superchainTokenBridge = ISuperchainTokenBridge(Predeploys.SUPERCHAIN_TOKEN_BRIDGE); IOptimismSuperchainERC20Factory l2OptimismSuperchainERC20Factory = IOptimismSuperchainERC20Factory(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_FACTORY); + ILiquidityController liquidityController = ILiquidityController(Predeploys.LIQUIDITY_CONTROLLER); + INativeAssetLiquidity nativeAssetLiquidity = INativeAssetLiquidity(Predeploys.NATIVE_ASSET_LIQUIDITY); /// @notice Indicates whether a test is running against a forked production network. function isForkTest() public view returns (bool) { @@ -334,7 +338,11 @@ contract Setup is FeatureFlags { fork: uint256(l2Fork), deployCrossL2Inbox: deploy.cfg().useInterop(), enableGovernance: deploy.cfg().enableGovernance(), - fundDevAccounts: deploy.cfg().fundDevAccounts() + fundDevAccounts: deploy.cfg().fundDevAccounts(), + useCustomGasToken: deploy.cfg().useCustomGasToken(), + gasPayingTokenName: deploy.cfg().gasPayingTokenName(), + gasPayingTokenSymbol: deploy.cfg().gasPayingTokenSymbol(), + nativeAssetLiquidityAmount: deploy.cfg().nativeAssetLiquidityAmount() }) ); @@ -366,6 +374,8 @@ contract Setup is FeatureFlags { labelPredeploy(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_FACTORY); labelPredeploy(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_BEACON); labelPredeploy(Predeploys.SUPERCHAIN_TOKEN_BRIDGE); + labelPredeploy(Predeploys.NATIVE_ASSET_LIQUIDITY); + labelPredeploy(Predeploys.LIQUIDITY_CONTROLLER); // L2 Preinstalls labelPreinstall(Preinstalls.MultiCall3);