diff --git a/op-chain-ops/genesis/config.go b/op-chain-ops/genesis/config.go index df8a0efbd19dd..151849df2dccb 100644 --- a/op-chain-ops/genesis/config.go +++ b/op-chain-ops/genesis/config.go @@ -273,20 +273,24 @@ func (d *GasPriceOracleDeployConfig) OperatorFeeParams() [32]byte { // GasTokenDeployConfig configures the optional custom gas token functionality. 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"` + // IsCustomGasToken is a flag to indicate that a custom gas token should be used + IsCustomGasToken bool `json:"isCustomGasToken"` + // GasPayingTokenName represents the custom gas token name. + GasPayingTokenName string `json:"gasPayingTokenName"` + // GasPayingTokenSymbol represents the custom gas token symbol. + GasPayingTokenSymbol string `json:"gasPayingTokenSymbol"` } 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.IsCustomGasToken { + if d.GasPayingTokenName == "" { + return fmt.Errorf("%w: GasPayingTokenName cannot be empty", ErrInvalidDeployConfig) + } + if d.GasPayingTokenSymbol == "" { + return fmt.Errorf("%w: GasPayingTokenSymbol cannot be empty", ErrInvalidDeployConfig) } - log.Info("Using custom gas token", "address", d.CustomGasTokenAddress) } 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..1e2c6b2d7111d 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", @@ -84,7 +83,7 @@ "proofMaturityDelaySeconds": 12, "disputeGameFinalityDelaySeconds": 6, "respectedGameType": 0, - "useCustomGasToken": false, + "isCustomGasToken": false, "useFaultProofs": false, "useAltDA": false, "daBondSize": 0, diff --git a/op-chain-ops/interopgen/deploy.go b/op-chain-ops/interopgen/deploy.go index b5c1aab406872..6028da8c89d54 100644 --- a/op-chain-ops/interopgen/deploy.go +++ b/op-chain-ops/interopgen/deploy.go @@ -242,6 +242,7 @@ func DeployL2ToL1(l1Host *script.Host, superCfg *SuperchainConfig, superDeployme AllowCustomDisputeParameters: true, OperatorFeeScalar: cfg.GasPriceOracleOperatorFeeScalar, OperatorFeeConstant: cfg.GasPriceOracleOperatorFeeConstant, + IsCustomGasToken: cfg.IsCustomGasToken, }) if err != nil { return nil, fmt.Errorf("failed to deploy L2 OP chain: %w", err) @@ -324,6 +325,9 @@ func GenesisL2(l2Host *script.Host, cfg *L2Config, deployment *L2Deployment, mul DeployCrossL2Inbox: multichainDepSet, EnableGovernance: cfg.EnableGovernance, FundDevAccounts: cfg.FundDevAccounts, + IsCustomGasToken: cfg.IsCustomGasToken, + GasPayingTokenName: cfg.GasPayingTokenName, + GasPayingTokenSymbol: cfg.GasPayingTokenSymbol, }); 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..87bf2921181a1 100644 --- a/op-chain-ops/interopgen/recipe.go +++ b/op-chain-ops/interopgen/recipe.go @@ -243,7 +243,9 @@ func (r *InteropDevL2Recipe) build(l1ChainID uint64, addrs devkeys.Addresses) (* GasPriceOracleBlobBaseFeeScalar: 810949, }, GasTokenDeployConfig: genesis.GasTokenDeployConfig{ - UseCustomGasToken: false, + IsCustomGasToken: false, + GasPayingTokenName: "Custom Gas Token", + GasPayingTokenSymbol: "CGT", }, OperatorDeployConfig: genesis.OperatorDeployConfig{ P2PSequencerAddress: sequencerP2P, diff --git a/op-deployer/pkg/deployer/integration_test/apply_test.go b/op-deployer/pkg/deployer/integration_test/apply_test.go index 39d29ed6d8865..1c8c1d6c8fc2c 100644 --- a/op-deployer/pkg/deployer/integration_test/apply_test.go +++ b/op-deployer/pkg/deployer/integration_test/apply_test.go @@ -713,6 +713,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: standard.CustomGasTokenEnabled, + Name: standard.CustomGasTokenName, + Symbol: standard.CustomGasTokenSymbol, + }, } } diff --git a/op-deployer/pkg/deployer/opcm/l2genesis.go b/op-deployer/pkg/deployer/opcm/l2genesis.go index 3f91196cc85fc..ff52f208b504d 100644 --- a/op-deployer/pkg/deployer/opcm/l2genesis.go +++ b/op-deployer/pkg/deployer/opcm/l2genesis.go @@ -28,6 +28,9 @@ type L2GenesisInput struct { DeployCrossL2Inbox bool EnableGovernance bool FundDevAccounts bool + IsCustomGasToken bool + GasPayingTokenName string + GasPayingTokenSymbol string } type L2GenesisScript script.DeployScriptWithoutOutput[L2GenesisInput] diff --git a/op-deployer/pkg/deployer/opcm/opchain.go b/op-deployer/pkg/deployer/opcm/opchain.go index 9d87fb4979a4e..af40a7ea71623 100644 --- a/op-deployer/pkg/deployer/opcm/opchain.go +++ b/op-deployer/pkg/deployer/opcm/opchain.go @@ -40,6 +40,7 @@ type DeployOPChainInput struct { DisputeClockExtension uint64 DisputeMaxClockDuration uint64 AllowCustomDisputeParameters bool + IsCustomGasToken bool OperatorFeeScalar uint32 OperatorFeeConstant uint64 diff --git a/op-deployer/pkg/deployer/pipeline/l2genesis.go b/op-deployer/pkg/deployer/pipeline/l2genesis.go index bb3f514358bce..566b6532451f5 100644 --- a/op-deployer/pkg/deployer/pipeline/l2genesis.go +++ b/op-deployer/pkg/deployer/pipeline/l2genesis.go @@ -22,6 +22,9 @@ import ( ) type l2GenesisOverrides struct { + IsCustomGasToken bool `json:"isCustomGasToken"` + GasPayingTokenName string `json:"gasPayingTokenName"` + GasPayingTokenSymbol string `json:"gasPayingTokenSymbol"` FundDevAccounts bool `json:"fundDevAccounts"` BaseFeeVaultMinimumWithdrawalAmount *hexutil.Big `json:"baseFeeVaultMinimumWithdrawalAmount"` L1FeeVaultMinimumWithdrawalAmount *hexutil.Big `json:"l1FeeVaultMinimumWithdrawalAmount"` @@ -94,6 +97,9 @@ func GenerateL2Genesis(pEnv *Env, intent *state.Intent, bundle ArtifactsBundle, DeployCrossL2Inbox: len(intent.Chains) > 1, EnableGovernance: overrides.EnableGovernance, FundDevAccounts: overrides.FundDevAccounts, + IsCustomGasToken: thisIntent.CustomGasToken.Enabled, + GasPayingTokenName: thisIntent.CustomGasToken.Name, + GasPayingTokenSymbol: thisIntent.CustomGasToken.Symbol, }); err != nil { return fmt.Errorf("failed to call L2Genesis script: %w", err) } @@ -156,6 +162,9 @@ func wdNetworkToBig(wd genesis.WithdrawalNetwork) *big.Int { func defaultOverrides() l2GenesisOverrides { return l2GenesisOverrides{ + IsCustomGasToken: false, + GasPayingTokenName: "Custom Gas Token", + GasPayingTokenSymbol: "CGT", FundDevAccounts: false, BaseFeeVaultMinimumWithdrawalAmount: standard.VaultMinWithdrawalAmount, L1FeeVaultMinimumWithdrawalAmount: standard.VaultMinWithdrawalAmount, diff --git a/op-deployer/pkg/deployer/pipeline/l2genesis_test.go b/op-deployer/pkg/deployer/pipeline/l2genesis_test.go index aed2a1e782390..19493e51cba5b 100644 --- a/op-deployer/pkg/deployer/pipeline/l2genesis_test.go +++ b/op-deployer/pkg/deployer/pipeline/l2genesis_test.go @@ -53,6 +53,9 @@ func TestCalculateL2GenesisOverrides(t *testing.T) { SequencerFeeVaultWithdrawalNetwork: "local", EnableGovernance: false, GovernanceTokenOwner: standard.GovernanceTokenOwner, + IsCustomGasToken: false, + GasPayingTokenName: "Custom Gas Token", + GasPayingTokenSymbol: "CGT", }, expectedSchedule: func() *genesis.UpgradeScheduleDeployConfig { return standard.DefaultHardforkScheduleForTag("") @@ -73,6 +76,9 @@ func TestCalculateL2GenesisOverrides(t *testing.T) { "enableGovernance": true, "governanceTokenOwner": "0x1111111111111111111111111111111111111111", "l2GenesisInteropTimeOffset": "0x1234", + "isCustomGasToken": false, + "gasPayingTokenName": "Custom Gas Token", + "gasPayingTokenSymbol": "CGT", }, }, chainIntent: &state.ChainIntent{}, @@ -87,6 +93,9 @@ func TestCalculateL2GenesisOverrides(t *testing.T) { SequencerFeeVaultWithdrawalNetwork: "remote", EnableGovernance: true, GovernanceTokenOwner: common.HexToAddress("0x1111111111111111111111111111111111111111"), + IsCustomGasToken: false, + GasPayingTokenName: "Custom Gas Token", + GasPayingTokenSymbol: "CGT", }, expectedSchedule: func() *genesis.UpgradeScheduleDeployConfig { sched := standard.DefaultHardforkScheduleForTag("") @@ -114,6 +123,9 @@ func TestCalculateL2GenesisOverrides(t *testing.T) { "enableGovernance": true, "governanceTokenOwner": "0x1111111111111111111111111111111111111111", "l2GenesisInteropTimeOffset": "0x1234", + "isCustomGasToken": false, + "gasPayingTokenName": "Custom Gas Token", + "gasPayingTokenSymbol": "CGT", }, }, expectError: false, @@ -127,6 +139,9 @@ func TestCalculateL2GenesisOverrides(t *testing.T) { SequencerFeeVaultWithdrawalNetwork: "remote", EnableGovernance: true, GovernanceTokenOwner: common.HexToAddress("0x1111111111111111111111111111111111111111"), + IsCustomGasToken: false, + GasPayingTokenName: "Custom Gas Token", + GasPayingTokenSymbol: "CGT", }, expectedSchedule: func() *genesis.UpgradeScheduleDeployConfig { sched := standard.DefaultHardforkScheduleForTag("") diff --git a/op-deployer/pkg/deployer/pipeline/opchain.go b/op-deployer/pkg/deployer/pipeline/opchain.go index f44721dfd5711..e9657a7a3d7d6 100644 --- a/op-deployer/pkg/deployer/pipeline/opchain.go +++ b/op-deployer/pkg/deployer/pipeline/opchain.go @@ -109,6 +109,7 @@ func makeDCI(intent *state.Intent, thisIntent *state.ChainIntent, chainID common DisputeClockExtension: proofParams.DisputeClockExtension, // 3 hours (input in seconds) DisputeMaxClockDuration: proofParams.DisputeMaxClockDuration, // 3.5 days (input in seconds) AllowCustomDisputeParameters: proofParams.DangerouslyAllowCustomDisputeParameters, + IsCustomGasToken: thisIntent.CustomGasToken.Enabled, OperatorFeeScalar: thisIntent.OperatorFeeScalar, OperatorFeeConstant: thisIntent.OperatorFeeConstant, }, nil diff --git a/op-deployer/pkg/deployer/standard/standard.go b/op-deployer/pkg/deployer/standard/standard.go index 4b3379979593b..7306a7cdd9183 100644 --- a/op-deployer/pkg/deployer/standard/standard.go +++ b/op-deployer/pkg/deployer/standard/standard.go @@ -34,6 +34,9 @@ const ( Eip1559DenominatorCanyon uint64 = 250 Eip1559Denominator uint64 = 50 Eip1559Elasticity uint64 = 6 + CustomGasTokenEnabled bool = false + CustomGasTokenName string = "" + CustomGasTokenSymbol string = "" ContractsV160Tag = "op-contracts/v1.6.0" ContractsV180Tag = "op-contracts/v1.8.0-rc.4" diff --git a/op-deployer/pkg/deployer/state/chain_intent.go b/op-deployer/pkg/deployer/state/chain_intent.go index 3ad1a3dea3b00..c69a11a532718 100644 --- a/op-deployer/pkg/deployer/state/chain_intent.go +++ b/op-deployer/pkg/deployer/state/chain_intent.go @@ -56,6 +56,12 @@ 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" toml:"name"` + Symbol string `json:"symbol" toml:"symbol"` +} + type ChainIntent struct { ID common.Hash `json:"id" toml:"id"` BaseFeeVaultRecipient common.Address `json:"baseFeeVaultRecipient" toml:"baseFeeVaultRecipient"` @@ -70,8 +76,8 @@ type ChainIntent struct { AdditionalDisputeGames []AdditionalDisputeGame `json:"dangerousAdditionalDisputeGames" toml:"dangerousAdditionalDisputeGames,omitempty"` OperatorFeeScalar uint32 `json:"operatorFeeScalar,omitempty" toml:"operatorFeeScalar,omitempty"` OperatorFeeConstant uint64 `json:"operatorFeeConstant,omitempty" toml:"operatorFeeConstant,omitempty"` - L1StartBlockHash *common.Hash `json:"l1StartBlockHash,omitempty" toml:"l1StartBlockHash,omitempty"` - + L1StartBlockHash *common.Hash `json:"l1StartBlockHash,omitempty" toml:"l1StartBlockHash,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"` } @@ -111,6 +117,15 @@ func (c *ChainIntent) Check() error { return fmt.Errorf("%w: chainId=%s", ErrFeeVaultZeroAddress, c.ID) } + if c.CustomGasToken != nil && 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.DangerousAltDAConfig.UseAltDA { return c.DangerousAltDAConfig.Check(nil) } diff --git a/op-deployer/pkg/deployer/state/deploy_config.go b/op-deployer/pkg/deployer/state/deploy_config.go index 05123b143e323..943d9df8c9e0e 100644 --- a/op-deployer/pkg/deployer/state/deploy_config.go +++ b/op-deployer/pkg/deployer/state/deploy_config.go @@ -71,6 +71,12 @@ func CombineDeployConfig(intent *Intent, chainIntent *ChainIntent, state *State, EIP1559Elasticity: chainIntent.Eip1559Elasticity, }, + GasTokenDeployConfig: genesis.GasTokenDeployConfig{ + IsCustomGasToken: chainIntent.CustomGasToken.Enabled, + GasPayingTokenName: chainIntent.CustomGasToken.Name, + GasPayingTokenSymbol: chainIntent.CustomGasToken.Symbol, + }, + // 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 diff --git a/op-deployer/pkg/deployer/state/deploy_config_test.go b/op-deployer/pkg/deployer/state/deploy_config_test.go index 53d33d5874f2b..d8b7247df252c 100644 --- a/op-deployer/pkg/deployer/state/deploy_config_test.go +++ b/op-deployer/pkg/deployer/state/deploy_config_test.go @@ -31,6 +31,11 @@ func TestCombineDeployConfig(t *testing.T) { UnsafeBlockSigner: common.HexToAddress("0xabc"), Batcher: common.HexToAddress("0xdef"), }, + CustomGasToken: &CustomGasToken{ + Enabled: false, + Name: "Test", + Symbol: "TEST", + }, } 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 410f0bd4369db..70fe84abd78c4 100644 --- a/op-deployer/pkg/deployer/state/intent.go +++ b/op-deployer/pkg/deployer/state/intent.go @@ -154,6 +154,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 != nil && (chain.CustomGasToken.Enabled != standard.CustomGasTokenEnabled) { + return fmt.Errorf("%w: chainId=%s custom gas token not allowed in standard configuration", ErrNonStandardValue, chain.ID) + } } challenger, _ := standard.ChallengerAddressFor(c.L1ChainID) @@ -294,6 +297,11 @@ func NewIntentCustom(l1ChainId uint64, l2ChainIds []common.Hash) (Intent, error) for _, l2ChainID := range l2ChainIds { intent.Chains = append(intent.Chains, &ChainIntent{ ID: l2ChainID, + CustomGasToken: &CustomGasToken{ + Enabled: standard.CustomGasTokenEnabled, + Name: standard.CustomGasTokenName, + Symbol: standard.CustomGasTokenSymbol, + }, }) } return intent, nil @@ -337,6 +345,11 @@ func NewIntentStandard(l1ChainId uint64, l2ChainIds []common.Hash) (Intent, erro L1ProxyAdminOwner: l1ProxyAdminOwner, L2ProxyAdminOwner: l2ProxyAdminOwner, }, + CustomGasToken: &CustomGasToken{ + Enabled: standard.CustomGasTokenEnabled, + Name: standard.CustomGasTokenName, + Symbol: standard.CustomGasTokenSymbol, + }, }) } 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..ca0ec71735fa2 100644 --- a/op-deployer/pkg/deployer/state/intent_test.go +++ b/op-deployer/pkg/deployer/state/intent_test.go @@ -87,6 +87,17 @@ func TestValidateStandardValues(t *testing.T) { }, ErrIncompatibleValue, }, + { + "CustomGasToken", + func(intent *Intent) { + intent.Chains[0].CustomGasToken = &CustomGasToken{ + Enabled: true, + Name: "Custom Gas Token", + Symbol: "CGT", + } + }, + ErrNonStandardValue, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -131,6 +142,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 +170,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 +248,11 @@ func setFeeAddresses(intent *Intent) { intent.Chains[0].L1FeeVaultRecipient = common.HexToAddress("0x09") intent.Chains[0].SequencerFeeVaultRecipient = common.HexToAddress("0x0A") } + +func setCustomGasToken(intent *Intent) { + intent.Chains[0].CustomGasToken = &CustomGasToken{ + Enabled: true, + Name: "Custom Gas Token", + Symbol: "CGT", + } +} diff --git a/op-devstack/sysgo/deployer.go b/op-devstack/sysgo/deployer.go index f01bb2240ed2d..aca99b2e81c1b 100644 --- a/op-devstack/sysgo/deployer.go +++ b/op-devstack/sysgo/deployer.go @@ -304,6 +304,14 @@ func WithDisputeGameFinalityDelaySeconds(seconds uint64) DeployerOption { } } +func WithCustomGasToken(enabled bool, name, symbol string) DeployerOption { + return func(p devtest.P, keys devkeys.Keys, builder intentbuilder.Builder) { + for _, l2Cfg := range builder.L2s() { + l2Cfg.WithCustomGasToken(enabled, name, symbol) + } + } +} + 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 04aa0fd620925..962a24f389a13 100644 --- a/op-e2e/config/init.go +++ b/op-e2e/config/init.go @@ -396,6 +396,11 @@ func defaultIntent(root string, loc *artifacts.Locator, deployer common.Address, Proposer: addrs.Proposer, Challenger: common.HexToAddress("0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65"), }, + CustomGasToken: &state.CustomGasToken{ + Enabled: false, + Name: "", + Symbol: "", + }, AdditionalDisputeGames: []state.AdditionalDisputeGame{ { ChainProofParams: state.ChainProofParams{ diff --git a/op-e2e/e2eutils/intentbuilder/builder.go b/op-e2e/e2eutils/intentbuilder/builder.go index 88482bbc11ad0..8b8598b9ed773 100644 --- a/op-e2e/e2eutils/intentbuilder/builder.go +++ b/op-e2e/e2eutils/intentbuilder/builder.go @@ -48,6 +48,7 @@ type L2Configurator interface { WithL1StartBlockHash(hash common.Hash) WithAdditionalDisputeGames(games []state.AdditionalDisputeGame) WithFinalizationPeriodSeconds(value uint64) + WithCustomGasToken(enabled bool, name, symbol string) ContractsConfigurator L2VaultsConfigurator L2RolesConfigurator @@ -386,6 +387,14 @@ func (c *l2Configurator) WithEIP1559Denominator(value uint64) { c.builder.intent.Chains[c.chainIndex].Eip1559Denominator = value } +func (c *l2Configurator) WithCustomGasToken(enabled bool, name, symbol string) { + c.builder.intent.Chains[c.chainIndex].CustomGasToken = &state.CustomGasToken{ + Enabled: enabled, + Name: name, + Symbol: symbol, + } +} + 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 eeffb15a14d9e..39abaf38d6098 100644 --- a/op-e2e/e2eutils/intentbuilder/builder_test.go +++ b/op-e2e/e2eutils/intentbuilder/builder_test.go @@ -68,6 +68,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, "Custom Gas Token", "CGT") // Test ContractsConfigurator methods l2Config.WithL1ContractsLocator("http://l1.example.com") @@ -159,6 +160,11 @@ func TestBuilder(t *testing.T) { Eip1559Elasticity: 10, OperatorFeeScalar: 100, OperatorFeeConstant: 200, + CustomGasToken: &state.CustomGasToken{ + Enabled: false, + Name: "Custom Gas Token", + Symbol: "CGT", + }, 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 339a98da80ded..e81d125314e0f 100644 --- a/packages/contracts-bedrock/deploy-config/hardhat.json +++ b/packages/contracts-bedrock/deploy-config/hardhat.json @@ -62,5 +62,6 @@ "daChallengeWindow": 100, "daResolveWindow": 100, "daBondSize": 1000, - "daResolverRefundPercentage": 50 + "daResolverRefundPercentage": 50, + "isCustomGasToken": false } diff --git a/packages/contracts-bedrock/deploy-config/internal-devnet.json b/packages/contracts-bedrock/deploy-config/internal-devnet.json index 643c507dc86d5..ba86edaee7d2c 100644 --- a/packages/contracts-bedrock/deploy-config/internal-devnet.json +++ b/packages/contracts-bedrock/deploy-config/internal-devnet.json @@ -38,5 +38,6 @@ "eip1559Elasticity": 10, "systemConfigStartBlock": 8364212, "requiredProtocolVersion": "0x0000000000000000000000000000000000000000000000000000000000000000", - "recommendedProtocolVersion": "0x0000000000000000000000000000000000000000000000000000000000000000" + "recommendedProtocolVersion": "0x0000000000000000000000000000000000000000000000000000000000000000", + "isCustomGasToken": false } diff --git a/packages/contracts-bedrock/deploy-config/mainnet.json b/packages/contracts-bedrock/deploy-config/mainnet.json index cd217ba111538..f81c4d131cb9a 100644 --- a/packages/contracts-bedrock/deploy-config/mainnet.json +++ b/packages/contracts-bedrock/deploy-config/mainnet.json @@ -55,5 +55,6 @@ "proofMaturityDelaySeconds": 604800, "disputeGameFinalityDelaySeconds": 302400, "respectedGameType": 0, - "useFaultProofs": true + "useFaultProofs": true, + "isCustomGasToken": false } diff --git a/packages/contracts-bedrock/deploy-config/sepolia-devnet-0.json b/packages/contracts-bedrock/deploy-config/sepolia-devnet-0.json index 2392dcb9281a5..5f97a743f17f4 100644 --- a/packages/contracts-bedrock/deploy-config/sepolia-devnet-0.json +++ b/packages/contracts-bedrock/deploy-config/sepolia-devnet-0.json @@ -79,5 +79,6 @@ "useFaultProofs": true, "fundDevAccounts": false, "requiredProtocolVersion": "0x0000000000000000000000000000000000000005000000000000000000000000", - "recommendedProtocolVersion": "0x0000000000000000000000000000000000000005000000000000000000000000" + "recommendedProtocolVersion": "0x0000000000000000000000000000000000000005000000000000000000000000", + "isCustomGasToken": false } diff --git a/packages/contracts-bedrock/deploy-config/sepolia.json b/packages/contracts-bedrock/deploy-config/sepolia.json index 12a1cc392c803..31d0867938a8c 100644 --- a/packages/contracts-bedrock/deploy-config/sepolia.json +++ b/packages/contracts-bedrock/deploy-config/sepolia.json @@ -54,5 +54,6 @@ "proofMaturityDelaySeconds": 604800, "disputeGameFinalityDelaySeconds": 302400, "respectedGameType": 0, - "useFaultProofs": true + "useFaultProofs": true, + "isCustomGasToken": false } diff --git a/packages/contracts-bedrock/interfaces/L1/IL1CrossDomainMessenger.sol b/packages/contracts-bedrock/interfaces/L1/IL1CrossDomainMessenger.sol index 81d7bcd22abb1..df5054860bdc2 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 4ea5e42f1edfe..0e11d6a2ceca0 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( @@ -68,11 +67,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/IOPContractsManager.sol b/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol index 63273248e3fb4..7ed838e9273ac 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol @@ -134,6 +134,7 @@ interface IOPContractsManager { uint32 basefeeScalar; uint32 blobBasefeeScalar; uint256 l2ChainId; + bool isCustomGasToken; // The correct type is OutputRoot memory but OP Deployer does not yet support structs. bytes startingAnchorRoot; // The salt mixer is used as part of making the resulting salt unique. diff --git a/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol b/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol index eb73b2956cc24..d9a527aad944d 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol @@ -22,6 +22,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(); @@ -50,7 +51,12 @@ interface IOptimismPortal2 is IProxyAdminOwnedBase { event WithdrawalProven(bytes32 indexed withdrawalHash, address indexed from, address indexed to); event WithdrawalProvenExtension1(bytes32 indexed withdrawalHash, address indexed proofSubmitter); event ETHMigrated(address indexed lockbox, uint256 ethBalance); - event PortalMigrated(IETHLockbox oldLockbox, IETHLockbox newLockbox, IAnchorStateRegistry oldAnchorStateRegistry, IAnchorStateRegistry newAnchorStateRegistry); + event PortalMigrated( + IETHLockbox oldLockbox, + IETHLockbox newLockbox, + IAnchorStateRegistry oldAnchorStateRegistry, + IAnchorStateRegistry newAnchorStateRegistry + ); receive() external payable; @@ -83,10 +89,12 @@ interface IOptimismPortal2 is IProxyAdminOwnedBase { function initialize( ISystemConfig _systemConfig, IAnchorStateRegistry _anchorStateRegistry, - IETHLockbox _ethLockbox + IETHLockbox _ethLockbox, + bool _isCustomGasToken ) external; function initVersion() external view returns (uint8); + function isCustomGasToken() external view returns (bool); function l2Sender() external view returns (address); function minimumGasLimit(uint64 _byteCount) external pure returns (uint64); function numProofSubmitters(bytes32 _withdrawalHash) external view returns (uint256); diff --git a/packages/contracts-bedrock/interfaces/L1/ISystemConfig.sol b/packages/contracts-bedrock/interfaces/L1/ISystemConfig.sol index ca2c3ebe1444e..fee2c5bed82d7 100644 --- a/packages/contracts-bedrock/interfaces/L1/ISystemConfig.sol +++ b/packages/contracts-bedrock/interfaces/L1/ISystemConfig.sol @@ -66,6 +66,7 @@ interface ISystemConfig is IProxyAdminOwnedBase { function l1ERC721Bridge() external view returns (address addr_); function l1StandardBridge() external view returns (address addr_); function l2ChainId() external view returns (uint256); + function isCustomGasToken() external view returns (bool); function maximumGasLimit() external pure returns (uint64); function minimumGasLimit() external view returns (uint64); function operatorFeeConstant() external view returns (uint64); diff --git a/packages/contracts-bedrock/interfaces/L2/IL1Block.sol b/packages/contracts-bedrock/interfaces/L2/IL1Block.sol index 30c42275adf9a..c8c2c9119ceb1 100644 --- a/packages/contracts-bedrock/interfaces/L2/IL1Block.sol +++ b/packages/contracts-bedrock/interfaces/L2/IL1Block.sol @@ -3,22 +3,23 @@ pragma solidity ^0.8.0; interface IL1Block { function DEPOSITOR_ACCOUNT() external pure returns (address addr_); - function baseFeeScalar() external view returns (uint32); + function number() external view returns (uint64); + function timestamp() external view returns (uint64); function basefee() external view returns (uint256); - function batcherHash() external view returns (bytes32); - function blobBaseFee() external view returns (uint256); - function blobBaseFeeScalar() external view returns (uint32); - function gasPayingToken() external pure returns (address addr_, uint8 decimals_); - function gasPayingTokenName() external pure returns (string memory name_); - function gasPayingTokenSymbol() external pure returns (string memory symbol_); function hash() external view returns (bytes32); - function isCustomGasToken() external pure returns (bool is_); + 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 number() external view returns (uint64); - function operatorFeeScalar() external view returns (uint32); + function blobBaseFee() external view returns (uint256); function operatorFeeConstant() external view returns (uint64); - function sequenceNumber() 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, @@ -32,8 +33,7 @@ interface IL1Block { external; function setL1BlockValuesEcotone() external; function setL1BlockValuesIsthmus() external; - function timestamp() external view returns (uint64); - function version() external pure returns (string memory); + function setCustomGasToken() external; function __constructor__() external; } diff --git a/packages/contracts-bedrock/interfaces/L2/IL2ToL1MessagePasser.sol b/packages/contracts-bedrock/interfaces/L2/IL2ToL1MessagePasser.sol index 4629dbaba8d09..912e2e7be02cf 100644 --- a/packages/contracts-bedrock/interfaces/L2/IL2ToL1MessagePasser.sol +++ b/packages/contracts-bedrock/interfaces/L2/IL2ToL1MessagePasser.sol @@ -2,6 +2,8 @@ pragma solidity ^0.8.0; interface IL2ToL1MessagePasser { + error NotAllowedOnCGTMode(); + event MessagePassed( uint256 indexed nonce, address indexed sender, diff --git a/packages/contracts-bedrock/interfaces/L2/ILiquidityController.sol b/packages/contracts-bedrock/interfaces/L2/ILiquidityController.sol new file mode 100644 index 0000000000000..b20214702abb0 --- /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 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..003ac4d7bddce --- /dev/null +++ b/packages/contracts-bedrock/interfaces/L2/INativeAssetLiquidity.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { ISemver } from "interfaces/universal/ISemver.sol"; + +interface INativeAssetLiquidity is ISemver { + error Unauthorized(); + error InvalidAmount(); + + event LiquidityDeposited(address indexed caller, uint256 value); + event LiquidityWithdrawn(address indexed caller, uint256 value); + event LiquidityFunded(address indexed funder, uint256 value); + + function deposit() external payable; + function withdraw(uint256 _amount) external; + function fund() external payable; +} diff --git a/packages/contracts-bedrock/scripts/L2Genesis.s.sol b/packages/contracts-bedrock/scripts/L2Genesis.s.sol index de75c6b99cf1e..0da11d9afd987 100644 --- a/packages/contracts-bedrock/scripts/L2Genesis.s.sol +++ b/packages/contracts-bedrock/scripts/L2Genesis.s.sol @@ -30,6 +30,7 @@ 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"; /// @title L2Genesis /// @notice Generates the genesis state for the L2 network. @@ -60,6 +61,9 @@ contract L2Genesis is Script { bool deployCrossL2Inbox; bool enableGovernance; bool fundDevAccounts; + bool isCustomGasToken; + string gasPayingTokenName; + string gasPayingTokenSymbol; } using ForkUtils for Fork; @@ -195,7 +199,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.isCustomGasToken)) + { address implementation = Predeploys.predeployToCodeNamespace(addr); EIP1967Helper.setImplementation(addr, implementation); } @@ -236,6 +241,10 @@ contract L2Genesis is Script { } setL2ToL2CrossDomainMessenger(); // 23 } + if (_input.isCustomGasToken) { + setLiquidityController(_input); // 29 + setNativeAssetLiquidity(); // 2A + } } function setInteropPredeployProxies() internal { } @@ -289,6 +298,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.isCustomGasToken && 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 +313,7 @@ contract L2Genesis is Script { ( _input.sequencerFeeVaultRecipient, _input.sequencerFeeVaultMinimumWithdrawalAmount, - Types.WithdrawalNetwork(_input.sequencerFeeVaultWithdrawalNetwork) + withdrawalNetwork ) ) ) @@ -347,8 +362,6 @@ 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); } @@ -381,17 +394,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.isCustomGasToken && 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 +422,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.isCustomGasToken && 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 +563,27 @@ 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() internal { + _setImplementationCode(Predeploys.NATIVE_ASSET_LIQUIDITY); + + // Pre-fund the liquidity contract with the specified amount + vm.deal(Predeploys.NATIVE_ASSET_LIQUIDITY, type(uint248).max); + } + /// @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/main.go b/packages/contracts-bedrock/scripts/checks/test-validation/main.go index 4b32827ab3dfc..b260dfe2379dd 100644 --- a/packages/contracts-bedrock/scripts/checks/test-validation/main.go +++ b/packages/contracts-bedrock/scripts/checks/test-validation/main.go @@ -323,18 +323,20 @@ var excludedPaths = []string{ // Resolving these naming inconsistencies is outside the script's scope, but they are // documented here to avoid false validation failures while maintaining the validation rules // for standard contract tests. - "test/invariants/", // Invariant testing framework - no direct src counterpart - "test/opcm/", // OP Chain Manager tests - may have different structure - "test/scripts/", // Script tests - test deployment/utility scripts, not contracts - "test/integration/", // Integration tests - test multiple contracts together - "test/cannon/MIPS64Memory.t.sol", // Tests external MIPS implementation - "test/dispute/lib/LibClock.t.sol", // Tests library utilities - "test/dispute/lib/LibGameId.t.sol", // Tests library utilities - "test/setup/DeployVariations.t.sol", // Tests deployment variations - "test/universal/BenchmarkTest.t.sol", // Performance benchmarking tests - "test/universal/ExtendedPause.t.sol", // Tests extended functionality - "test/vendor/Initializable.t.sol", // Tests external vendor code - "test/vendor/InitializableOZv5.t.sol", // Tests external vendor code + "test/invariants/", // Invariant testing framework - no direct src counterpart + "test/opcm/", // OP Chain Manager tests - may have different structure + "test/scripts/", // Script tests - test deployment/utility scripts, not contracts + "test/integration/", // Integration tests - test multiple contracts together + "test/cannon/MIPS64Memory.t.sol", // Tests external MIPS implementation + "test/dispute/lib/LibClock.t.sol", // Tests library utilities + "test/dispute/lib/LibGameId.t.sol", // Tests library utilities + "test/setup/DeployVariations.t.sol", // Tests deployment variations + "test/universal/BenchmarkTest.t.sol", // Performance benchmarking tests + "test/universal/ExtendedPause.t.sol", // Tests extended functionality + "test/vendor/Initializable.t.sol", // Tests external vendor code + "test/vendor/InitializableOZv5.t.sol", // Tests external vendor code + "test/L2/L1BlockCGT.t.sol", // Tests L1Block with custom gas token + "test/L2/L2ToL1MessagePasserCGT.t.sol", // Tests L2ToL1MessagePasser with custom gas token // PATHS EXCLUDED FROM CONTRACT NAME FILE PATH VALIDATION: // These paths are excluded because they don't follow the standard naming convention where the @@ -359,6 +361,7 @@ var excludedPaths = []string{ "test/L2/CrossDomainOwnable3.t.sol", // Contains contracts not matching CrossDomainOwnable3 base name "test/L2/GasPriceOracle.t.sol", // Contains contracts not matching GasPriceOracle base name "test/universal/StandardBridge.t.sol", // Contains contracts not matching StandardBridge base name + "test/L1/OptimismPortal2.t.sol", // Contains contracts not matching OptimismPortal2 base name // PATHS EXCLUDED FROM FUNCTION NAME VALIDATION: // These paths are excluded because they don't pass the function name validation, which checks diff --git a/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol b/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol index 8c693f0ca8871..c5f5fa5e1277b 100644 --- a/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol +++ b/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol @@ -91,6 +91,8 @@ library ChainAssertions { require(config.l1StandardBridge() == _contracts.L1StandardBridge, "CHECK-SCFG-180"); require(config.optimismPortal() == _contracts.OptimismPortal, "CHECK-SCFG-200"); require(config.optimismMintableERC20Factory() == _contracts.OptimismMintableERC20Factory, "CHECK-SCFG-210"); + // Check custom gas token + require(config.isCustomGasToken() == _doi.isCustomGasToken(), "CHECK-SCFG-220"); } else { require(config.owner() == address(0), "CHECK-SCFG-220"); require(config.overhead() == 0, "CHECK-SCFG-230"); diff --git a/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol b/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol index b3afc5ac39c05..6916175bdcb25 100644 --- a/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol @@ -422,6 +422,7 @@ contract Deploy is Deployer { basefeeScalar: cfg.basefeeScalar(), blobBasefeeScalar: cfg.blobbasefeeScalar(), l2ChainId: cfg.l2ChainID(), + isCustomGasToken: cfg.isCustomGasToken(), startingAnchorRoot: abi.encode( Proposal({ root: Hash.wrap(cfg.faultGameGenesisOutputRoot()), l2SequenceNumber: cfg.faultGameGenesisBlock() }) ), diff --git a/packages/contracts-bedrock/scripts/deploy/DeployConfig.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployConfig.s.sol index 041dadd898268..22acab2414294 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployConfig.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployConfig.s.sol @@ -74,7 +74,7 @@ contract DeployConfig is Script { uint256 public daResolveWindow; uint256 public daBondSize; uint256 public daResolverRefundPercentage; - + bool public isCustomGasToken; bool public useInterop; bool public useUpgradedFork; @@ -119,6 +119,7 @@ contract DeployConfig is Script { l2GenesisBlockGasLimit = stdJson.readUint(_json, "$.l2GenesisBlockGasLimit"); basefeeScalar = uint32(_readOr(_json, "$.gasPriceOracleBaseFeeScalar", 1368)); blobbasefeeScalar = uint32(_readOr(_json, "$.gasPriceOracleBlobBaseFeeScalar", 810949)); + isCustomGasToken = _readOr(_json, "$.isCustomGasToken", false); enableGovernance = _readOr(_json, "$.enableGovernance", false); systemConfigStartBlock = stdJson.readUint(_json, "$.systemConfigStartBlock"); @@ -217,6 +218,26 @@ contract DeployConfig is Script { useUpgradedFork = _useUpgradedFork; } + /// @notice Allow the `isCustomGasToken` config to be overridden in testing environments + function setIsCustomGasToken(bool _isCustomGasToken) public { + isCustomGasToken = _isCustomGasToken; + } + + /// @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 (l2GenesisHoloceneTimeOffset == 0) { return Fork.HOLOCENE; diff --git a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol index 050dfed7695b9..90fbf6173de03 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol @@ -55,6 +55,7 @@ contract DeployOPChainInput is BaseDeployIO { Duration internal _disputeClockExtension; Duration internal _disputeMaxClockDuration; bool internal _allowCustomDisputeParameters; + bool internal _isCustomGasToken; uint32 internal _operatorFeeScalar; uint64 internal _operatorFeeConstant; @@ -107,13 +108,21 @@ 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("DeployImplementationsInput: 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 if (_sel == this.isCustomGasToken.selector) { + _isCustomGasToken = _value; + } else { + revert("DeployOPChainInput: unknown selector"); + } } function opChainProxyAdminOwner() public view returns (address) { @@ -162,6 +171,10 @@ contract DeployOPChainInput is BaseDeployIO { return _l2ChainId; } + function isCustomGasToken() public view returns (bool) { + return _isCustomGasToken; + } + function startingAnchorRoot() public pure returns (bytes memory) { // WARNING: For now always hardcode the starting permissioned game anchor root to 0xdead, // and we do not set anything for the permissioned game. This is because we currently only @@ -381,7 +394,8 @@ contract DeployOPChain is Script { disputeMaxGameDepth: _doi.disputeMaxGameDepth(), disputeSplitDepth: _doi.disputeSplitDepth(), disputeClockExtension: _doi.disputeClockExtension(), - disputeMaxClockDuration: _doi.disputeMaxClockDuration() + disputeMaxClockDuration: _doi.disputeMaxClockDuration(), + isCustomGasToken: _doi.isCustomGasToken() }); vm.broadcast(msg.sender); diff --git a/packages/contracts-bedrock/snapshots/abi/L1Block.json b/packages/contracts-bedrock/snapshots/abi/L1Block.json index 153d2676cf5bb..55b23f0699bc4 100644 --- a/packages/contracts-bedrock/snapshots/abi/L1Block.json +++ b/packages/contracts-bedrock/snapshots/abi/L1Block.json @@ -77,24 +77,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [], - "name": "gasPayingToken", - "outputs": [ - { - "internalType": "address", - "name": "addr_", - "type": "address" - }, - { - "internalType": "uint8", - "name": "decimals_", - "type": "uint8" - } - ], - "stateMutability": "pure", - "type": "function" - }, { "inputs": [], "name": "gasPayingTokenName", @@ -105,7 +87,7 @@ "type": "string" } ], - "stateMutability": "pure", + "stateMutability": "view", "type": "function" }, { @@ -118,7 +100,7 @@ "type": "string" } ], - "stateMutability": "pure", + "stateMutability": "view", "type": "function" }, { @@ -140,11 +122,11 @@ "outputs": [ { "internalType": "bool", - "name": "is_", + "name": "isCustom_", "type": "bool" } ], - "stateMutability": "pure", + "stateMutability": "view", "type": "function" }, { @@ -225,6 +207,13 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "setCustomGasToken", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { diff --git a/packages/contracts-bedrock/snapshots/abi/L2ToL1MessagePasser.json b/packages/contracts-bedrock/snapshots/abi/L2ToL1MessagePasser.json index 77e1cf7596b35..6b7cd4244752a 100644 --- a/packages/contracts-bedrock/snapshots/abi/L2ToL1MessagePasser.json +++ b/packages/contracts-bedrock/snapshots/abi/L2ToL1MessagePasser.json @@ -152,5 +152,10 @@ ], "name": "WithdrawerBalanceBurnt", "type": "event" + }, + { + "inputs": [], + "name": "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..e44eeb5b0f2c0 --- /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": "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..9bbeb8427231b --- /dev/null +++ b/packages/contracts-bedrock/snapshots/abi/NativeAssetLiquidity.json @@ -0,0 +1,109 @@ +[ + { + "inputs": [], + "name": "deposit", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "fund", + "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": "funder", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "LiquidityFunded", + "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": "InvalidAmount", + "type": "error" + }, + { + "inputs": [], + "name": "Unauthorized", + "type": "error" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json index d7d8a6551ec1b..eb06c17836534 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json @@ -303,6 +303,11 @@ "name": "l2ChainId", "type": "uint256" }, + { + "internalType": "bool", + "name": "isCustomGasToken", + "type": "bool" + }, { "internalType": "bytes", "name": "startingAnchorRoot", diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployer.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployer.json index 81fd1cef7bb3f..2118ad58e6606 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployer.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployer.json @@ -191,6 +191,11 @@ "name": "l2ChainId", "type": "uint256" }, + { + "internalType": "bool", + "name": "isCustomGasToken", + "type": "bool" + }, { "internalType": "bytes", "name": "startingAnchorRoot", diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json b/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json index 39d02adf5ec39..f9751ab7443ae 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json @@ -299,6 +299,11 @@ "internalType": "contract IETHLockbox", "name": "_ethLockbox", "type": "address" + }, + { + "internalType": "bool", + "name": "_isCustomGasToken", + "type": "bool" } ], "name": "initialize", @@ -306,6 +311,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [], + "name": "isCustomGasToken", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "l2Sender", @@ -1081,6 +1099,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 a295b986db223..59caaf7559228 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": [], "name": "l1CrossDomainMessenger", diff --git a/packages/contracts-bedrock/snapshots/abi/WETH.json b/packages/contracts-bedrock/snapshots/abi/WETH.json index 0f97edfd828d3..03cf2880ccce0 100644 --- a/packages/contracts-bedrock/snapshots/abi/WETH.json +++ b/packages/contracts-bedrock/snapshots/abi/WETH.json @@ -104,7 +104,7 @@ "type": "string" } ], - "stateMutability": "pure", + "stateMutability": "view", "type": "function" }, { @@ -117,7 +117,7 @@ "type": "string" } ], - "stateMutability": "pure", + "stateMutability": "view", "type": "function" }, { diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 83294cb7a89c9..55d2e5ab1325c 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -20,16 +20,16 @@ "sourceCodeHash": "0x11b35ee81f797b30ee834e2ffad52686d2100d7ee139db4299b7d854dba25550" }, "src/L1/OPContractsManager.sol:OPContractsManager": { - "initCodeHash": "0x8c209f938d6aa21f1dbc93d50bed559c4cfe3d1b7b4f2cb81c4ea46e880b443c", - "sourceCodeHash": "0xb1264c7af50b6134c98cb82d1ffc7891adf97068fa7048ee70992fb94bc15bd1" + "initCodeHash": "0x1c67b4c8c3768e1ec81fcc4056084bafe170d29fba70f6a4492d953f1902195b", + "sourceCodeHash": "0x112a8f4d62699eddc103c8167b94cee8cb61b37fb5847c31a2956344d510a7ab" }, "src/L1/OPContractsManagerStandardValidator.sol:OPContractsManagerStandardValidator": { "initCodeHash": "0x4bf3bbdaf08989de57408b2ea88995e2f477b98add164dbf82e0dceb01417ef6", "sourceCodeHash": "0x36861c793b247f4922ecd77b1b153a0f2a47a129117fbe59e7e1f6498ef46c42" }, "src/L1/OptimismPortal2.sol:OptimismPortal2": { - "initCodeHash": "0x785b09610b2da65d248b49150fafc85b8369c921ddae95b0ea45608b1ce5cbc6", - "sourceCodeHash": "0x925821e7ca59f1799a900fbf5ce7d2c6bef35fc2636c306977d9889f60a987bb" + "initCodeHash": "0xf81abd502df268c9dd61d722e80c143dc6973c8a11dae8bd6de5b47bfbaf8d1f", + "sourceCodeHash": "0x5c84380f6622a41392dd4cc22b0c0e28672831ff77a1a1d4026d36c50f3e07d6" }, "src/L1/ProtocolVersions.sol:ProtocolVersions": { "initCodeHash": "0x5a76c8530cb24cf23d3baacc6eefaac226382af13f1e2a35535d2ec2b0573b29", @@ -40,8 +40,8 @@ "sourceCodeHash": "0xad12c20a00dc20683bd3f68e6ee254f968da6cc2d98930be6534107ee5cb11d9" }, "src/L1/SystemConfig.sol:SystemConfig": { - "initCodeHash": "0x07b7039de5b8a4dc57642ee9696e949d70516b7f6dce41dde4920efb17105ef2", - "sourceCodeHash": "0x997212ceadabb306c2abd31918b09bccbba0b21662c1d8930a3599831c374b13" + "initCodeHash": "0x31ee221cef680b15cc25013ae2142bef541c26b2ecc7665ec28fd4585f469cad", + "sourceCodeHash": "0xb53da5831a507d1ce896b18b3b2cd9091afa7200b9a0b3dea06f7475660cfd95" }, "src/L2/BaseFeeVault.sol:BaseFeeVault": { "initCodeHash": "0x9b664e3d84ad510091337b4aacaa494b142512e2f6f7fbcdb6210ed62ca9b885", @@ -60,8 +60,8 @@ "sourceCodeHash": "0x4351fe2ac1106c8c220b8cfe7839bc107c24d8084deb21259ac954f5a362725d" }, "src/L2/L1Block.sol:L1Block": { - "initCodeHash": "0xc35734387887a95f611888f3944546c6bcf82fd4c05dcdaa1e019779b628ad68", - "sourceCodeHash": "0x6e5349fd781d5f0127ff29ccea4d86a80240550cfa322364183a0f629abcb43e" + "initCodeHash": "0x77f825c0828c113a550022989cdab9c5a0d324c80a421abf8ca27022e4766609", + "sourceCodeHash": "0xca797382d9075577ba2aa9542e12d21b4e5061c95096cb0f630ed8229c396bc3" }, "src/L2/L1FeeVault.sol:L1FeeVault": { "initCodeHash": "0x9b664e3d84ad510091337b4aacaa494b142512e2f6f7fbcdb6210ed62ca9b885", @@ -84,13 +84,21 @@ "sourceCodeHash": "0xde724da82ecf3c96b330c2876a7285b6e2b933ac599241eaa3174c443ebbe33a" }, "src/L2/L2ToL1MessagePasser.sol:L2ToL1MessagePasser": { - "initCodeHash": "0x88f7b25f956eceeab9ad84c17e66cded6a1acbb933054ac2c8b336641f70f875", - "sourceCodeHash": "0x83396cbd12a0c5c02e09a4d99c4b62ab4e9d9eb762745e63283e2e818a78a39c" + "initCodeHash": "0xdad71cc062cab0edf07d7bcc9c10b0522f0ad5af4ac2a1b2d3830e5884468404", + "sourceCodeHash": "0xd3c6fc68785e2342be06a35efbadd17510acb780c6d11dac55567cb9d4698e26" }, "src/L2/L2ToL2CrossDomainMessenger.sol:L2ToL2CrossDomainMessenger": { "initCodeHash": "0x975fd33a3a386310d54dbb01b56f3a6a8350f55a3b6bd7781e5ccc2166ddf2e6", "sourceCodeHash": "0xbea4229c5c6988243dbc7cf5a086ddd412fe1f2903b8e20d56699fec8de0c2c9" }, + "src/L2/LiquidityController.sol:LiquidityController": { + "initCodeHash": "0x8a21d16da3377aa61375755d37010c0c37c99817fdce9d16f78f0ab9bc3a84c5", + "sourceCodeHash": "0xb4fca8d4532e88e0dd53d3643aa4022953e37c745753009b3c8f9bcca5806f57" + }, + "src/L2/NativeAssetLiquidity.sol:NativeAssetLiquidity": { + "initCodeHash": "0x2c50c7cac8eab6867ffb969a65a8aa3026d415f2e9464726683ff6cd5da0b8f3", + "sourceCodeHash": "0x9432883dd4aa4d5ffc733ad99fa7bcc9cc8c319e654b385b8cd093a37a4c94cb" + }, "src/L2/OperatorFeeVault.sol:OperatorFeeVault": { "initCodeHash": "0x3d8c0d7736e8767f2f797da1c20c5fe30bd7f48a4cf75f376290481ad7c0f91f", "sourceCodeHash": "0x2022fdb4e32769eb9446dab4aed4b8abb5261fd866f381cccfa7869df1a2adff" @@ -132,8 +140,8 @@ "sourceCodeHash": "0x0ff7c1f0264d784fac5d69b792c6bc9d064d4a09701c1bafa808388685c8c4f1" }, "src/L2/WETH.sol:WETH": { - "initCodeHash": "0xbc2cd025153720943e51b79822c2dc374d270a78b92cf47d49548c468e218e46", - "sourceCodeHash": "0x734a6b2aa6406bc145d848ad6071d3af1d40852aeb8f4b2f6f51beaad476e2d3" + "initCodeHash": "0x0d3777864ed564365ec6aee93ab11816424f2e435bf51a58fcbcc99ac5975361", + "sourceCodeHash": "0xb7bccbe1cb0ac6ac7c4a82eeb5e3175d46617cd579e8506e037d42f97502155d" }, "src/cannon/MIPS64.sol:MIPS64": { "initCodeHash": "0xbc7c3c50e8c3679576f87d79c2dae05dd1174e64bdaa4c1e0857314618e415a3", 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/snapshots/storageLayout/OptimismPortal2.json b/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortal2.json index 8dc6639f30366..7bd556d4d55a4 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortal2.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortal2.json @@ -145,5 +145,12 @@ "offset": 20, "slot": "63", "type": "bool" + }, + { + "bytes": "1", + "label": "isCustomGasToken", + "offset": 21, + "slot": "63", + "type": "bool" } ] \ 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 03d997b15a23a..00098ca0cde10 100644 --- a/packages/contracts-bedrock/src/L1/OPContractsManager.sol +++ b/packages/contracts-bedrock/src/L1/OPContractsManager.sol @@ -1077,7 +1077,7 @@ contract OPContractsManagerDeployer is OPContractsManagerBase { output.opChainProxyAdmin, address(output.l1ERC721BridgeProxy), implementation.l1ERC721BridgeImpl, data ); - data = encodeOptimismPortalInitializer(output); + data = encodeOptimismPortalInitializer(output, _input); upgradeToAndCall( output.opChainProxyAdmin, address(output.optimismPortalProxy), implementation.optimismPortalImpl, data ); @@ -1229,7 +1229,10 @@ contract OPContractsManagerDeployer is OPContractsManagerBase { } /// @notice Helper method for encoding the OptimismPortal initializer data. - function encodeOptimismPortalInitializer(OPContractsManager.DeployOutput memory _output) + function encodeOptimismPortalInitializer( + OPContractsManager.DeployOutput memory _output, + OPContractsManager.DeployInput memory _input + ) internal view virtual @@ -1237,7 +1240,12 @@ contract OPContractsManagerDeployer is OPContractsManagerBase { { return abi.encodeCall( IOptimismPortal.initialize, - (_output.systemConfigProxy, _output.anchorStateRegistryProxy, _output.ethLockboxProxy) + ( + _output.systemConfigProxy, + _output.anchorStateRegistryProxy, + _output.ethLockboxProxy, + _input.isCustomGasToken + ) ); } @@ -1268,6 +1276,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, ( @@ -1658,6 +1681,7 @@ contract OPContractsManager is ISemver { uint32 basefeeScalar; uint32 blobBasefeeScalar; uint256 l2ChainId; + bool isCustomGasToken; // The correct type is Proposal memory but OP Deployer does not yet support structs. bytes startingAnchorRoot; // The salt mixer is used as part of making the resulting salt unique. @@ -1760,9 +1784,9 @@ contract OPContractsManager is ISemver { // -------- Constants and Variables -------- - /// @custom:semver 3.0.0 + /// @custom:semver 3.0.1 function version() public pure virtual returns (string memory) { - return "3.0.0"; + return "3.0.1"; } OPContractsManagerGameTypeAdder public immutable opcmGameTypeAdder; diff --git a/packages/contracts-bedrock/src/L1/OptimismPortal2.sol b/packages/contracts-bedrock/src/L1/OptimismPortal2.sol index ef2f8c8cb0e07..3b72a0ac9c892 100644 --- a/packages/contracts-bedrock/src/L1/OptimismPortal2.sol +++ b/packages/contracts-bedrock/src/L1/OptimismPortal2.sol @@ -125,6 +125,9 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase /// @notice Whether the OptimismPortal is using Super Roots or Output Roots. bool public superRootsActive; + /// @notice Whether the gas token is custom. + bool public isCustomGasToken; + /// @notice Emitted when a transaction is deposited from L1 to L2. The parameters of this event /// are read by the rollup node and used to derive deposit transactions on L2. /// @param from Address that triggered the deposit transaction. @@ -180,6 +183,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(); @@ -232,9 +238,9 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase error OptimismPortal_MigratingToSameRegistry(); /// @notice Semantic version. - /// @custom:semver 4.6.0 + /// @custom:semver 4.6.1 function version() public pure virtual returns (string memory) { - return "4.6.0"; + return "4.6.1"; } /// @param _proofMaturityDelaySeconds The proof maturity delay in seconds. @@ -247,10 +253,12 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase /// @param _systemConfig Address of the SystemConfig. /// @param _anchorStateRegistry Address of the AnchorStateRegistry. /// @param _ethLockbox Contract of the ETHLockbox. + /// @param _isCustomGasToken Whether the gas token is custom. function initialize( ISystemConfig _systemConfig, IAnchorStateRegistry _anchorStateRegistry, - IETHLockbox _ethLockbox + IETHLockbox _ethLockbox, + bool _isCustomGasToken ) external reinitializer(initVersion()) @@ -262,6 +270,7 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase systemConfig = _systemConfig; anchorStateRegistry = _anchorStateRegistry; ethLockbox = _ethLockbox; + isCustomGasToken = _isCustomGasToken; // Set the l2Sender slot, only if it is currently empty. This signals the first // initialization of the contract. @@ -627,6 +636,11 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase ) public { + // Cannot finalize withdrawal with value when custom gas token mode is enabled. + if (isCustomGasToken && _tx.value > 0) { + revert OptimismPortal_NotAllowedOnCGTMode(); + } + // Cannot finalize withdrawal transactions while the system is paused. _assertNotPaused(); @@ -748,8 +762,13 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase payable metered(_gasLimit) { - // Lock the ETH in the ETHLockbox. - if (msg.value > 0) ethLockbox.lockETH{ value: msg.value }(); + // Handle ETH deposits: prevent when custom gas token is active, otherwise lock in ETHLockbox. + if (msg.value > 0) { + if (isCustomGasToken) { + revert OptimismPortal_NotAllowedOnCGTMode(); + } + ethLockbox.lockETH{ value: msg.value }(); + } // Just to be safe, make sure that people specify address(0) as the target when doing // contract creations. diff --git a/packages/contracts-bedrock/src/L1/SystemConfig.sol b/packages/contracts-bedrock/src/L1/SystemConfig.sol index 604a9936d3aef..4d22c27b39cb1 100644 --- a/packages/contracts-bedrock/src/L1/SystemConfig.sol +++ b/packages/contracts-bedrock/src/L1/SystemConfig.sol @@ -142,9 +142,9 @@ contract SystemConfig is ProxyAdminOwnedBase, OwnableUpgradeable, Reinitializabl event ConfigUpdate(uint256 indexed version, UpdateType indexed updateType, bytes data); /// @notice Semantic version. - /// @custom:semver 3.4.0 + /// @custom:semver 3.4.1 function version() public pure virtual returns (string memory) { - return "3.4.0"; + return "3.4.1"; } /// @notice Constructs the SystemConfig contract. @@ -497,4 +497,10 @@ contract SystemConfig is ProxyAdminOwnedBase, OwnableUpgradeable, Reinitializabl function guardian() public view returns (address) { return superchainConfig.guardian(); } + + /// @notice Returns whether the gas token is custom by reading from the OptimismPortal. + /// @return bool True if the gas token is custom, false otherwise. + function isCustomGasToken() public view returns (bool) { + return IOptimismPortal2(payable(optimismPortal())).isCustomGasToken(); + } } diff --git a/packages/contracts-bedrock/src/L2/L1Block.sol b/packages/contracts-bedrock/src/L2/L1Block.sol index 31935dfab7eac..5fa53b0c48790 100644 --- a/packages/contracts-bedrock/src/L2/L1Block.sol +++ b/packages/contracts-bedrock/src/L2/L1Block.sol @@ -3,9 +3,11 @@ pragma solidity 0.8.15; // Libraries import { Constants } from "src/libraries/Constants.sol"; +import { Predeploys } from "src/libraries/Predeploys.sol"; // Interfaces import { ISemver } from "interfaces/universal/ISemver.sol"; +import { ILiquidityController } from "interfaces/L2/ILiquidityController.sol"; /// @custom:proxied true /// @custom:predeploy 0x4200000000000000000000000000000000000015 @@ -15,6 +17,11 @@ import { ISemver } from "interfaces/universal/ISemver.sol"; /// 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 L1Block is ISemver { + /// @notice Storage slot for the isCustomGasToken flag + /// @dev bytes32(uint256(keccak256("l1block.isCustomGasToken")) - 1) + bytes32 private constant IS_CUSTOM_GAS_TOKEN_SLOT = + 0xd2ff82c9b477ff6a09f530b1c627ffb4b0b81e2ae2ba427f824162e8dad020aa; + /// @notice Address of the special depositor account. function DEPOSITOR_ACCOUNT() public pure returns (address addr_) { addr_ = Constants.DEPOSITOR_ACCOUNT; @@ -61,35 +68,33 @@ 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_) { - addr_ = Constants.ETHER; - decimals_ = 18; + /// @notice Returns whether the gas paying token is custom. + function isCustomGasToken() public view returns (bool isCustom_) { + bytes32 slot = IS_CUSTOM_GAS_TOKEN_SLOT; + assembly { + isCustom_ := sload(slot) + } } /// @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_) { - name_ = "Ether"; + function gasPayingTokenName() public view 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 pure 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_) { - is_ = false; + function gasPayingTokenSymbol() public view returns (string memory symbol_) { + symbol_ = + isCustomGasToken() ? ILiquidityController(Predeploys.LIQUIDITY_CONTROLLER).gasPayingTokenSymbol() : "ETH"; } /// @custom:legacy @@ -212,4 +217,18 @@ contract L1Block is ISemver { sstore(operatorFeeConstant.slot, shr(160, calldataload(164))) } } + + /// @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..92662c71d13ac 100644 --- a/packages/contracts-bedrock/src/L2/L2ToL1MessagePasser.sol +++ b/packages/contracts-bedrock/src/L2/L2ToL1MessagePasser.sol @@ -6,9 +6,11 @@ import { Types } from "src/libraries/Types.sol"; import { Hashing } from "src/libraries/Hashing.sol"; import { Encoding } from "src/libraries/Encoding.sol"; import { Burn } from "src/libraries/Burn.sol"; +import { Predeploys } from "src/libraries/Predeploys.sol"; // Interfaces import { ISemver } from "interfaces/universal/ISemver.sol"; +import { IL1Block } from "interfaces/L2/IL1Block.sol"; /// @custom:proxied true /// @custom:predeploy 0x4200000000000000000000000000000000000016 @@ -29,6 +31,9 @@ contract L2ToL1MessagePasser is ISemver { /// @notice A unique value hashed with each withdrawal. uint240 internal msgNonce; + /// @notice The error thrown when a withdrawal is initiated with value and custom gas token is used. + error NotAllowedOnCGTMode(); + /// @notice Emitted any time a withdrawal is initiated. /// @param nonce Unique value corresponding to each withdrawal. /// @param sender The L2 account address which initiated the withdrawal. @@ -51,8 +56,8 @@ contract L2ToL1MessagePasser is ISemver { /// @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.1.3 + string public constant version = "1.1.3"; /// @notice Allows users to withdraw ETH by sending directly to this contract. receive() external payable { @@ -74,6 +79,10 @@ contract L2ToL1MessagePasser is ISemver { /// @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 { + if (IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken() && msg.value > 0) { + revert NotAllowedOnCGTMode(); + } + bytes32 withdrawalHash = Hashing.hashWithdrawal( Types.WithdrawalTransaction({ nonce: messageNonce(), diff --git a/packages/contracts-bedrock/src/L2/LiquidityController.sol b/packages/contracts-bedrock/src/L2/LiquidityController.sol new file mode 100644 index 0000000000000..fa92db0f984b6 --- /dev/null +++ b/packages/contracts-bedrock/src/L2/LiquidityController.sol @@ -0,0 +1,103 @@ +// 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 { Unauthorized } from "src/libraries/errors/CommonErrors.sol"; +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 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 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 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 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 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..859818ad6cfda --- /dev/null +++ b/packages/contracts-bedrock/src/L2/NativeAssetLiquidity.sol @@ -0,0 +1,57 @@ +// 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"; + +// Errors +import { Unauthorized, InvalidAmount } from "src/libraries/errors/CommonErrors.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 Emitted when funds are received. + event LiquidityFunded(address indexed funder, uint256 value); + + /// @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 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 Unauthorized(); + + new SafeSend{ value: _amount }(payable(msg.sender)); + + emit LiquidityWithdrawn(msg.sender, _amount); + } + + /// @notice Fund the contract by sending native asset. + /// @dev The function is payable to accept native asset. + function fund() external payable { + if (msg.value == 0) revert InvalidAmount(); + + emit LiquidityFunded(msg.sender, msg.value); + } +} diff --git a/packages/contracts-bedrock/src/L2/WETH.sol b/packages/contracts-bedrock/src/L2/WETH.sol index 42b51261a9838..3711bcec10f15 100644 --- a/packages/contracts-bedrock/src/L2/WETH.sol +++ b/packages/contracts-bedrock/src/L2/WETH.sol @@ -15,18 +15,18 @@ import { IL1Block } from "interfaces/L2/IL1Block.sol"; /// Allows for nice rendering of token names for chains using custom gas token. /// This contract is not proxied and contains calls to the custom gas token methods. contract WETH is WETH98, ISemver { - /// @custom:semver 1.1.1 - string public constant version = "1.1.1"; + /// @custom:semver 1.1.2 + string public constant version = "1.1.2"; /// @notice Returns the name of the wrapped native asset. Will be "Wrapped Ether" /// if the native asset is Ether. - function name() external pure override returns (string memory name_) { + function name() external view override returns (string memory name_) { name_ = string.concat("Wrapped ", IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).gasPayingTokenName()); } /// @notice Returns the symbol of the wrapped native asset. Will be "WETH" if the /// native asset is Ether. - function symbol() external pure override returns (string memory symbol_) { + function symbol() external view override returns (string memory symbol_) { symbol_ = string.concat("W", IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).gasPayingTokenSymbol()); } } 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 a67aeb6f7148f..508b238ed7e25 100644 --- a/packages/contracts-bedrock/test/L1/L1StandardBridge.t.sol +++ b/packages/contracts-bedrock/test/L1/L1StandardBridge.t.sol @@ -430,8 +430,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"); } } diff --git a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol index 92113e5d473f7..775834fcf87cb 100644 --- a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol +++ b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol @@ -745,7 +745,8 @@ contract OPContractsManager_TestInit is Test { disputeMaxGameDepth: 73, disputeSplitDepth: 30, disputeClockExtension: Duration.wrap(10800), - disputeMaxClockDuration: Duration.wrap(302400) + disputeMaxClockDuration: Duration.wrap(302400), + isCustomGasToken: false }) ); } @@ -1942,7 +1943,8 @@ contract OPContractsManager_Deploy_Test is DeployOPChain_TestBase { disputeMaxGameDepth: _doi.disputeMaxGameDepth(), disputeSplitDepth: _doi.disputeSplitDepth(), disputeClockExtension: _doi.disputeClockExtension(), - disputeMaxClockDuration: _doi.disputeMaxClockDuration() + disputeMaxClockDuration: _doi.disputeMaxClockDuration(), + isCustomGasToken: _doi.isCustomGasToken() }); } diff --git a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol index ab80ffb6777cb..74fccd788ce1c 100644 --- a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol +++ b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol @@ -9,6 +9,8 @@ import { CommonTest } from "test/setup/CommonTest.sol"; import { NextImpl } from "test/mocks/NextImpl.sol"; import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; import { DisputeGameFactory_TestInit } from "test/dispute/DisputeGameFactory.t.sol"; +import { stdStorage, StdStorage } from "forge-std/StdStorage.sol"; +import { OptimismPortal2 } from "src/L1/OptimismPortal2.sol"; // Scripts import { ForgeArtifacts, StorageSlot } from "scripts/libraries/ForgeArtifacts.sol"; @@ -32,6 +34,8 @@ import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; import { IProxyAdminOwnedBase } from "interfaces/L1/IProxyAdminOwnedBase.sol"; contract OptimismPortal2_TestInit is DisputeGameFactory_TestInit { + using stdStorage for StdStorage; + address depositor; Types.WithdrawalTransaction _defaultTx; @@ -147,6 +151,13 @@ contract OptimismPortal2_TestInit is DisputeGameFactory_TestInit { // Store the new value at the correct slot/offset. vm.store(address(optimismPortal2), bytes32(slot.slot), newValue); } + + /// @notice Sets the isCustomGasToken variable to true. + function setIsCustomGasToken(bool _isCustomGasToken) public { + stdstore.enable_packed_slots().target(address(optimismPortal2)).sig("isCustomGasToken()").checked_write( + _isCustomGasToken + ); + } } /// @title OptimismPortal2_Version_Test @@ -189,6 +200,7 @@ contract OptimismPortal2_Initialize_Test is OptimismPortal2_TestInit { assertEq(optimismPortal2.paused(), false); assertEq(address(optimismPortal2.systemConfig()), address(systemConfig)); assertEq(address(optimismPortal2.ethLockbox()), address(ethLockbox)); + assertFalse(OptimismPortal2(payable(address(optimismPortal2))).isCustomGasToken()); returnIfForkTest( "OptimismPortal2_Initialize_Test: Do not check guardian and respectedGameType on forked networks" @@ -234,7 +246,7 @@ contract OptimismPortal2_Initialize_Test is OptimismPortal2_TestInit { // Call the `initialize` function with the sender vm.prank(_sender); - optimismPortal2.initialize(systemConfig, anchorStateRegistry, ethLockbox); + optimismPortal2.initialize(systemConfig, anchorStateRegistry, ethLockbox, false); } } @@ -1994,6 +2006,95 @@ contract OptimismPortal2_FinalizeWithdrawalTransaction_Test is OptimismPortal2_T optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); assertTrue(optimismPortal2.finalizedWithdrawals(_withdrawalHash)); } + + /// @notice Tests that `finalizeWithdrawalTransaction` succeeds when the custom gas token mode + /// is enabled and the withdrawal transaction has no value. + function test_finalizeWithdrawalTransaction_withoutValueAndCustomGasToken_succeeds( + address _sender, + address _target, + uint256 _gasLimit, + bytes memory _data + ) + external + { + skipIfForkTest("Skipping on forked tests because of the L2ToL1MessageParser call below"); + + vm.assume( + _target != address(optimismPortal2) // Cannot call the optimism portal or a contract + && _target.code.length == 0 // No accounts with code + && _target != CONSOLE // The console has no code but behaves like a contract + && uint160(_target) > 9 // No precompiles (or zero address) + ); + + // Set the custom gas token to true. + setIsCustomGasToken(true); + + uint256 gasLimit = bound(_gasLimit, 0, 50_000_000); + uint256 nonce = l2ToL1MessagePasser.messageNonce(); + + // Get a withdrawal transaction and mock proof from the differential testing script. + Types.WithdrawalTransaction memory _tx = Types.WithdrawalTransaction({ + nonce: nonce, + sender: _sender, + target: _target, + value: 0, + gasLimit: gasLimit, + data: _data + }); + ( + bytes32 stateRoot, + bytes32 storageRoot, + bytes32 outputRoot, + bytes32 withdrawalHash, + bytes[] memory withdrawalProof + ) = ffi.getProveWithdrawalTransactionInputs(_tx); + + // Create the output root proof + Types.OutputRootProof memory proof = Types.OutputRootProof({ + version: bytes32(uint256(0)), + stateRoot: stateRoot, + messagePasserStorageRoot: storageRoot, + latestBlockhash: bytes32(uint256(0)) + }); + + // Ensure the values returned from ffi are correct + assertEq(outputRoot, Hashing.hashOutputRootProof(proof)); + assertEq(withdrawalHash, Hashing.hashWithdrawal(_tx)); + + // Setup the dispute game to return the output root + vm.mockCall(address(game), abi.encodeCall(game.rootClaim, ()), abi.encode(outputRoot)); + + // Prove the withdrawal transaction + optimismPortal2.proveWithdrawalTransaction(_tx, _proposedGameIndex, proof, withdrawalProof); + (IDisputeGame _game,) = optimismPortal2.provenWithdrawals(withdrawalHash, address(this)); + assertTrue(_game.rootClaim().raw() != bytes32(0)); + + // Resolve the dispute game + game.resolveClaim(0, 0); + game.resolve(); + + // Warp past the finalization period + vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1); + + // Finalize the withdrawal transaction + vm.expectCallMinGas(_tx.target, _tx.value, uint64(_tx.gasLimit), _tx.data); + optimismPortal2.finalizeWithdrawalTransaction(_tx); + assertTrue(optimismPortal2.finalizedWithdrawals(withdrawalHash)); + } + + /// @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 { + // Set the custom gas token to true. + setIsCustomGasToken(true); + + // 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); + } } /// @title OptimismPortal2_FinalizeWithdrawalTransactionExternalProof_Test @@ -2215,6 +2316,27 @@ 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_customGasToken_reverts(bytes memory _data, uint256 _value) external { + // Prevent overflow on an upgrade context + _value = bound(_value, 1, type(uint256).max - address(ethLockbox).balance); + // Set the custom gas token to true. + setIsCustomGasToken(true); + uint64 gasLimit = optimismPortal2.minimumGasLimit(uint64(_data.length)); + + vm.deal(depositor, _value); + vm.prank(depositor); + 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)); @@ -2532,3 +2654,64 @@ contract OptimismPortal2_Params_Test is CommonTest { assertEq(slot21Expected, slot21After); } } + +/// @title OptimismPortal2_CustomGasToken_Test +/// @notice Test suite for OptimismPortal2 with custom gas token enabled. +contract OptimismPortal2_CustomGasToken_Test is OptimismPortal2_TestInit { + /// @notice Sets up a portal with custom gas token enabled + function setUp() public override { + super.setUp(); + + // Set isCustomGasToken to true. + setIsCustomGasToken(true); + } + + /// @notice Tests that isCustomGasToken storage is set correctly + function test_isCustomGasToken_succeeds() external view { + // Check that the public getter returns true + assertTrue(OptimismPortal2(payable(address(optimismPortal2))).isCustomGasToken()); + } + + /// @notice Tests that depositTransaction reverts when value > 0 and custom gas token is enabled + function testFuzz_depositTransaction_withValue_reverts(uint256 value) external { + value = bound(value, 1, type(uint128).max); + vm.deal(depositor, value); + + vm.prank(depositor); + vm.expectRevert(IOptimismPortal.OptimismPortal_NotAllowedOnCGTMode.selector); + optimismPortal2.depositTransaction{ value: value }({ + _to: address(0x40), + _value: value, + _gasLimit: 100_000, + _isCreation: false, + _data: hex"" + }); + } + + /// @notice Tests that depositTransaction succeeds when value = 0 and custom gas token is enabled + function test_depositTransaction_withZeroValue_succeeds() external { + vm.prank(depositor); + optimismPortal2.depositTransaction({ + _to: address(0x40), + _value: 0, + _gasLimit: 100_000, + _isCreation: false, + _data: hex"" + }); + // No revert expected + } + + /// @notice Tests that receive() reverts when custom gas token is enabled + function testFuzz_receive_withCustomGasToken_reverts(uint256 value) external { + value = bound(value, 1, type(uint128).max); + vm.deal(depositor, value); + + address portal = address(optimismPortal2); + + vm.prank(depositor); + vm.expectRevert(IOptimismPortal.OptimismPortal_NotAllowedOnCGTMode.selector); + assembly { + pop(call(gas(), portal, value, 0, 0, 0, 0)) + } + } +} diff --git a/packages/contracts-bedrock/test/L1/SystemConfig.t.sol b/packages/contracts-bedrock/test/L1/SystemConfig.t.sol index 9b1e1c3d813e5..0f814a49c6200 100644 --- a/packages/contracts-bedrock/test/L1/SystemConfig.t.sol +++ b/packages/contracts-bedrock/test/L1/SystemConfig.t.sol @@ -80,6 +80,7 @@ contract SystemConfig_Constructor_Test is SystemConfig_TestInit { assertEq(actual.maximumBaseFee, 0); assertEq(impl.startBlock(), type(uint256).max); assertEq(address(impl.batchInbox()), address(0)); + assertEq(impl.isCustomGasToken(), false); // Check addresses assertEq(address(impl.l1CrossDomainMessenger()), address(0)); assertEq(address(impl.l1ERC721Bridge()), address(0)); diff --git a/packages/contracts-bedrock/test/L2/L1Block.t.sol b/packages/contracts-bedrock/test/L2/L1Block.t.sol index 7a276b94df5d6..166f0982baacc 100644 --- a/packages/contracts-bedrock/test/L2/L1Block.t.sol +++ b/packages/contracts-bedrock/test/L2/L1Block.t.sol @@ -6,7 +6,6 @@ import { CommonTest } from "test/setup/CommonTest.sol"; // Libraries import { Encoding } from "src/libraries/Encoding.sol"; -import { Constants } from "src/libraries/Constants.sol"; import "src/libraries/L1BlockErrors.sol"; /// @title L1Block_ TestInit @@ -21,18 +20,6 @@ contract L1Block_TestInit is CommonTest { } } -/// @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 { - (address token, uint8 decimals) = l1Block.gasPayingToken(); - assertEq(token, Constants.ETHER); - assertEq(uint256(decimals), uint256(18)); - } -} - /// @title L1Block_GasPayingTokenName_Test /// @notice Tests the `gasPayingTokenName` function of the `L1Block` contract. contract L1Block_GasPayingTokenName_Test is L1Block_TestInit { diff --git a/packages/contracts-bedrock/test/L2/L1BlockCGT.t.sol b/packages/contracts-bedrock/test/L2/L1BlockCGT.t.sol new file mode 100644 index 0000000000000..f1a39c5e735c4 --- /dev/null +++ b/packages/contracts-bedrock/test/L2/L1BlockCGT.t.sol @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Testing +import { CommonTest } from "test/setup/CommonTest.sol"; +import { + L1Block_SetL1BlockValues_Test, + L1Block_SetL1BlockValuesEcotone_Test, + L1Block_SetL1BlockValuesIsthmus_Test +} from "test/L2/L1Block.t.sol"; + +// Libraries +import "src/libraries/L1BlockErrors.sol"; + +/// @title L1BlockCGT_TestInit +/// @notice Reusable test initialization for `L1Block` tests with custom gas token enabled. +contract L1BlockCGT_TestInit is CommonTest { + address depositor; + + /// @notice Sets up the test suite. + function setUp() public virtual override { + super.enableCustomGasToken(); + super.setUp(); + depositor = l1Block.DEPOSITOR_ACCOUNT(); + + // Manually activate custom gas token since we removed the constructor parameter + vm.prank(depositor); + l1Block.setCustomGasToken(); + } +} + +/// @title L1Block_CGT_GasPayingTokenName_Test +/// @notice Tests the `gasPayingTokenName` function of the `L1Block` contract with custom gas +/// token enabled. +contract L1Block_CGT_GasPayingTokenName_Test is L1BlockCGT_TestInit { + /// @notice Tests that the `gasPayingTokenName` function returns the correct token name. + function test_gasPayingTokenName_succeeds() external view { + assertEq(liquidityController.gasPayingTokenName(), l1Block.gasPayingTokenName()); + } +} + +/// @title L1Block_CGT_GasPayingTokenSymbol_Test +/// @notice Tests the `gasPayingTokenSymbol` function of the `L1Block` contract with custom gas +/// token enabled. +contract L1Block_CGT_GasPayingTokenSymbol_Test is L1BlockCGT_TestInit { + /// @notice Tests that the `gasPayingTokenSymbol` function returns the correct token symbol. + function test_gasPayingTokenSymbol_succeeds() external view { + assertEq(liquidityController.gasPayingTokenSymbol(), l1Block.gasPayingTokenSymbol()); + } +} + +/// @title L1Block_CGT_IsCustomGasToken_Test +/// @notice Tests the `isCustomGasToken` function of the `L1Block` contract with custom gas token +/// enabled. +contract L1Block_CGT_IsCustomGasToken_Test is L1BlockCGT_TestInit { + /// @notice Tests that the `isCustomGasToken` function returns false when no custom gas token + /// is used. + function test_isCustomGasToken_succeeds() external view { + assertTrue(l1Block.isCustomGasToken()); + } +} + +/// @title L1Block_CGT_SetL1BlockValues_Test +/// @notice Tests the `setL1BlockValues` function of the `L1Block` contract with custom gas token +/// enabled. +contract L1Block_CGT_SetL1BlockValues_Test is L1Block_SetL1BlockValues_Test { + // Override setUp to enable custom gas token + // Re-use the test from L1Block.t.sol + function setUp() public override { + super.enableCustomGasToken(); + super.setUp(); + // Manually activate custom gas token since we removed the constructor parameter + vm.prank(depositor); + l1Block.setCustomGasToken(); + } +} + +/// @title L1Block_CGT_SetL1BlockValuesEcotone_Test +/// @notice Tests the `setL1BlockValuesEcotone` function of the `L1Block` contract with custom gas +/// token enabled. +contract L1Block_CGT_SetL1BlockValuesEcotone_Test is L1Block_SetL1BlockValuesEcotone_Test { + // Override setUp to enable custom gas token + // Re-use the test from L1Block.t.sol + function setUp() public override { + super.enableCustomGasToken(); + super.setUp(); + // Manually activate custom gas token since we removed the constructor parameter + vm.prank(depositor); + l1Block.setCustomGasToken(); + } +} + +/// @title L1Block_CGTSetL1BlockValuesIsthmus_Test +/// @notice Tests the `setL1BlockValuesIsthmus` function of the `L1Block` contract with custom gas +/// token enabled. +contract L1Block_CGT_SetL1BlockValuesIsthmus_Test is L1Block_SetL1BlockValuesIsthmus_Test { + // Override setUp to enable custom gas token + // Re-use the test from L1Block.t.sols + function setUp() public override { + super.enableCustomGasToken(); + super.setUp(); + // Manually activate custom gas token since we removed the constructor parameter + vm.prank(depositor); + l1Block.setCustomGasToken(); + } +} + +/// @title L1Block_CGT_SetCustomGasToken_Test +/// @notice Tests the `setCustomGasToken` function of the `L1Block` contract. +contract L1Block_CGT_SetCustomGasToken_Test is L1BlockCGT_TestInit { + /// @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(l1Block.isCustomGasToken()); + + vm.expectRevert("L1Block: CustomGasToken already active"); + vm.prank(depositor); + l1Block.setCustomGasToken(); + } +} + +/// @title L1Block_SetCustomGasToken_Test +/// @notice Tests the `setCustomGasToken` function of the `L1Block` contract without CGT enabled. +contract L1Block_SetCustomGasToken_Test is CommonTest { + address depositor; + + /// @notice Sets up the test suite. + function setUp() public virtual override { + // Don't enable custom gas token - test the activation process + super.setUp(); + depositor = l1Block.DEPOSITOR_ACCOUNT(); + } + + /// @notice Tests that `setCustomGasToken` updates the flag correctly when called by depositor. + function test_setCustomGasToken_succeeds() external { + assertFalse(l1Block.isCustomGasToken()); + + vm.prank(depositor); + l1Block.setCustomGasToken(); + + assertTrue(l1Block.isCustomGasToken()); + } + + /// @notice Tests that `setCustomGasToken` reverts if sender address is not the depositor. + function test_setCustomGasToken_notDepositor_reverts(address nonDepositor) external { + vm.assume(nonDepositor != depositor); + vm.expectRevert("L1Block: only the depositor account can set isCustomGasToken flag"); + vm.prank(nonDepositor); + l1Block.setCustomGasToken(); + } +} diff --git a/packages/contracts-bedrock/test/L2/L2ToL1MessagePasserCGT.t.sol b/packages/contracts-bedrock/test/L2/L2ToL1MessagePasserCGT.t.sol new file mode 100644 index 0000000000000..9f014af6169ca --- /dev/null +++ b/packages/contracts-bedrock/test/L2/L2ToL1MessagePasserCGT.t.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Testing utilities +import { CommonTest } from "test/setup/CommonTest.sol"; + +// Libraries +import { Types } from "src/libraries/Types.sol"; +import { Hashing } from "src/libraries/Hashing.sol"; + +// Interfaces +import { IL2ToL1MessagePasser } from "interfaces/L2/IL2ToL1MessagePasser.sol"; + +/// @title L2ToL1MessagePasserCGT_TestInit +/// @notice Tests the `L2ToL1MessagePasser` contract with a custom gas token enabled. +contract L2ToL1MessagePasserCGT_TestInit is CommonTest { + /// @notice Sets up the test suite with custom gas token enabled. + function setUp() public override { + super.enableCustomGasToken(); + super.setUp(); + + // Manually activate custom gas token since we removed the constructor parameter + address depositor = l1Block.DEPOSITOR_ACCOUNT(); + vm.prank(depositor); + l1Block.setCustomGasToken(); + } +} + +/// @title L2ToL1MessagePasserCGT_InitiateWithdrawal_Test +/// @notice Tests the `initiateWithdrawal` function of the `L2ToL1MessagePasser` contract with +/// custom gas token enabled. +contract L2ToL1MessagePasserCGT_InitiateWithdrawal_Test is L2ToL1MessagePasserCGT_TestInit { + /// @notice Tests that `initiateWithdrawal` succeeds and correctly sets the state of the + /// message passer for the withdrawal hash. + function testFuzz_initiateWithdrawal_withZeroValue_succeeds( + address _sender, + address _target, + uint256 _gasLimit, + bytes memory _data + ) + external + { + uint256 nonce = l2ToL1MessagePasser.messageNonce(); + + bytes32 withdrawalHash = Hashing.hashWithdrawal( + Types.WithdrawalTransaction({ + nonce: nonce, + sender: _sender, + target: _target, + value: 0, + gasLimit: _gasLimit, + data: _data + }) + ); + + vm.expectEmit(address(l2ToL1MessagePasser)); + emit MessagePassed(nonce, _sender, _target, 0, _gasLimit, _data, withdrawalHash); + + vm.prank(_sender); + l2ToL1MessagePasser.initiateWithdrawal{ value: 0 }(_target, _gasLimit, _data); + + assertEq(l2ToL1MessagePasser.sentMessages(withdrawalHash), true); + + bytes32 slot = keccak256(bytes.concat(withdrawalHash, bytes32(0))); + + assertEq(vm.load(address(l2ToL1MessagePasser), slot), bytes32(uint256(1))); + } + + /// @notice Tests that `initiateWithdrawal` fails when called with value and custom gas token + /// is enabled. + function testFuzz_initiateWithdrawal_withValue_fails(address _randomAddress, uint256 _value) external { + // Set initial state + _value = bound(_value, 1, type(uint256).max); + vm.deal(_randomAddress, _value); + + // Expect revert with NotAllowedOnCGTMode + vm.prank(_randomAddress); + vm.expectRevert(IL2ToL1MessagePasser.NotAllowedOnCGTMode.selector); + l2ToL1MessagePasser.initiateWithdrawal{ value: _value }({ _target: address(0), _gasLimit: 1, _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..730d471c5022e --- /dev/null +++ b/packages/contracts-bedrock/test/L2/LiquidityController.t.sol @@ -0,0 +1,252 @@ +// 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"; + +// Error imports +import { Unauthorized } from "src/libraries/errors/CommonErrors.sol"; + +// Libraries +import { Predeploys } from "src/libraries/Predeploys.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 { + enableCustomGasToken(); + super.setUp(); + } + + /// @notice Tests that contract is set up correctly. + function test_setup_succeeds() public view { + assertEq(liquidityController.version(), "1.0.0"); + assertEq(liquidityController.gasPayingTokenName(), "Custom Gas Token"); + assertEq(liquidityController.gasPayingTokenSymbol(), "CGT"); + } + + /// @notice Shared modifier to authorize a minter. + modifier isAuthorizedMinter(address _minter) { + // Authorize the minter + stdstore.target(address(liquidityController)).sig(liquidityController.minters.selector).with_key(_minter) + .checked_write(true); + _; + } +} + +/// @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(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 { + using stdStorage for StdStorage; + + /// @notice Tests that the deauthorizeMinter function can be called by the owner. + function testFuzz_deauthorizeMinter_fromOwner_succeeds(address _minter) public { + // Set minter to authorized + stdstore.target(address(liquidityController)).sig(liquidityController.minters.selector).with_key(_minter) + .checked_write(true); + + // 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 + stdstore.target(address(liquidityController)).sig(liquidityController.minters.selector).with_key(_minter) + .checked_write(true); + + // Call the deauthorizeMinter function with non-owner as the caller + vm.prank(_caller); + vm.expectRevert(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 { + address authorizedMinter = makeAddr("authorizedMinter"); + + /// @notice Tests that the mint function can be called by an authorized minter. + function testFuzz_mint_fromAuthorizedMinter_succeeds( + address _to, + uint256 _amount + ) + public + isAuthorizedMinter(authorizedMinter) + { + _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(authorizedMinter, _to, _amount); + vm.prank(authorizedMinter); + 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, type(uint248).max); + + uint256 nativeAssetBalanceBefore = address(nativeAssetLiquidity).balance; + uint256 toBalanceBefore = _to.balance; + + // Call the mint function with unauthorized caller + vm.prank(_caller); + vm.expectRevert(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() public isAuthorizedMinter(authorizedMinter) { + // 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(authorizedMinter); + vm.expectRevert(); // Should revert due to insufficient balance in NativeAssetLiquidity + 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 { + address authorizedMinter = makeAddr("authorizedMinter"); + + /// @notice Tests that the burn function can be called by an authorized minter. + function testFuzz_burn_fromAuthorizedMinter_succeeds(uint256 _amount) public isAuthorizedMinter(authorizedMinter) { + _amount = bound(_amount, 0, type(uint248).max); + + // Deal the authorized minter with the amount to burn + vm.deal(authorizedMinter, _amount); + uint256 nativeAssetBalanceBefore = address(nativeAssetLiquidity).balance; + uint256 minterBalanceBefore = authorizedMinter.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(authorizedMinter, _amount); + vm.prank(authorizedMinter); + liquidityController.burn{ value: _amount }(); + + // Assert minter and NativeAssetLiquidity balances are updated correctly + assertEq(authorizedMinter.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 + ) + public + isAuthorizedMinter(authorizedMinter) + { + _amount = bound(_amount, 0, type(uint248).max); + + // 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(Unauthorized.selector); + liquidityController.burn{ value: _amount }(); + + // Assert caller and NativeAssetLiquidity balances remain unchanged + assertEq(_caller.balance, callerBalanceBefore); + assertEq(address(nativeAssetLiquidity).balance, nativeAssetBalanceBefore); + } +} 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..c32a7a0eb3c9b --- /dev/null +++ b/packages/contracts-bedrock/test/L2/NativeAssetLiquidity.t.sol @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Testing utilities +import { CommonTest } from "test/setup/CommonTest.sol"; + +// Error imports +import { Unauthorized, InvalidAmount } from "src/libraries/errors/CommonErrors.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 { + enableCustomGasToken(); + super.setUp(); + } + + /// @notice Tests that contract is set up correctly. + function test_setup_succeeds() public view { + // Assert + assertEq(nativeAssetLiquidity.version(), "1.0.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(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(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 OutOfFunds + vm.expectRevert(); + nativeAssetLiquidity.withdraw(amount); + + // Assert contract and controller balances remain unchanged + assertEq(address(nativeAssetLiquidity).balance, contractBalance); + assertEq(address(liquidityController).balance, 0); + } +} + +/// @title NativeAssetLiquidity_Fund_Test +/// @notice Tests the `fund` function of the `NativeAssetLiquidity` contract. +contract NativeAssetLiquidity_Fund_Test is NativeAssetLiquidity_TestInit { + /// @notice Tests that the fund function succeeds when called with a non-zero value. + /// @param _amount Amount of native asset (in wei) to call the fund function with. + /// @param _caller Address of the caller to call the fund function with. + function testFuzz_fund_succeeds(uint256 _amount, address _caller) public { + _amount = bound(_amount, 1, 1000 ether); + vm.assume(_caller != address(0)); + vm.assume(_caller != address(nativeAssetLiquidity)); // Prevent contract from calling itself + + // Deal caller with the amount to fund + vm.deal(_caller, _amount); + uint256 initialContractBalance = address(nativeAssetLiquidity).balance; + + // Expect emit LiquidityFunded event + vm.expectEmit(address(nativeAssetLiquidity)); + emit LiquidityFunded(_caller, _amount); + vm.prank(_caller); + nativeAssetLiquidity.fund{ value: _amount }(); + + // Assert caller and contract balances are updated correctly + assertEq(_caller.balance, 0); + assertEq(address(nativeAssetLiquidity).balance, initialContractBalance + _amount); + } + + /// @notice Tests that the fund function reverts when called with zero value. + function test_fund_zeroAmount_reverts() public { + uint256 initialContractBalance = address(nativeAssetLiquidity).balance; + // Expect revert with InvalidAmount + vm.expectRevert(InvalidAmount.selector); + nativeAssetLiquidity.fund{ value: 0 }(); + + // Assert contract balance does not change + assertEq(address(nativeAssetLiquidity).balance, initialContractBalance); + } +} diff --git a/packages/contracts-bedrock/test/libraries/Predeploys.t.sol b/packages/contracts-bedrock/test/libraries/Predeploys.t.sol index ff9cacefa5dbe..89bcb1434d041 100644 --- a/packages/contracts-bedrock/test/libraries/Predeploys.t.sol +++ b/packages/contracts-bedrock/test/libraries/Predeploys.t.sol @@ -42,7 +42,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 +57,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); @@ -133,7 +134,7 @@ 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); } } @@ -150,12 +151,28 @@ 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); + } +} + +/// @title Predeploys_CustomGasToken_Test +/// @notice Tests the `Predeploys` contract with custom gas token. +contract Predeploys_CustomGasToken_Test is Predeploys_TestInit { + /// @notice Test setup. Enabling custom gas token. + function setUp() public virtual override { + super.enableCustomGasToken(); + super.setUp(); + } + + /// @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_predeploysWithCustomGasToken_succeeds() external { + _test_predeploys(Fork.ISTHMUS, false, true); } } diff --git a/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol index faa3f466f4085..cc82f7518d6a5 100644 --- a/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol @@ -56,6 +56,7 @@ contract DeployOPChainInput_Test is Test { doi.set(doi.blobBaseFeeScalar.selector, blobBaseFeeScalar); doi.set(doi.l2ChainId.selector, l2ChainId); doi.set(doi.allowCustomDisputeParameters.selector, true); + doi.set(doi.isCustomGasToken.selector, false); doi.set(doi.opcm.selector, opcm); vm.etch(opcm, hex"01"); @@ -71,6 +72,7 @@ contract DeployOPChainInput_Test is Test { assertEq(l2ChainId, doi.l2ChainId(), "1000"); assertEq(opcm, address(doi.opcm()), "1100"); assertEq(true, doi.allowCustomDisputeParameters(), "1200"); + assertEq(false, doi.isCustomGasToken(), "1300"); } function test_getters_whenNotSet_reverts() public { @@ -328,6 +330,7 @@ contract DeployOPChain_TestBase is Test { IOPContractsManager opcm = IOPContractsManager(address(0)); string saltMixer = "defaultSaltMixer"; uint64 gasLimit = 60_000_000; + bool isCustomGasToken = false; // Configurable dispute game parameters. uint32 disputeGameType = GameType.unwrap(GameTypes.PERMISSIONED_CANNON); bytes32 disputeAbsolutePrestate = hex"038512e02c4c3f7bdaec27d00edf55b7155e0905301e1a88083e4e0a6764d54c"; @@ -400,6 +403,7 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { basefeeScalar = uint32(uint256(hash(_seed, 6))); blobBaseFeeScalar = uint32(uint256(hash(_seed, 7))); l2ChainId = uint256(hash(_seed, 8)); + isCustomGasToken = bool(uint256(hash(_seed, 9)) % 2 == 0); doi.set(doi.opChainProxyAdminOwner.selector, opChainProxyAdminOwner); doi.set(doi.systemConfigOwner.selector, systemConfigOwner); @@ -419,6 +423,7 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { doi.set(doi.disputeSplitDepth.selector, disputeSplitDepth); doi.set(doi.disputeClockExtension.selector, disputeClockExtension); doi.set(doi.disputeMaxClockDuration.selector, disputeMaxClockDuration); + doi.set(doi.isCustomGasToken.selector, isCustomGasToken); deployOPChain.run(doi, doo); @@ -442,6 +447,7 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { assertEq(disputeSplitDepth, doi.disputeSplitDepth(), "1500"); assertEq(disputeClockExtension, Duration.unwrap(doi.disputeClockExtension()), "1600"); assertEq(disputeMaxClockDuration, Duration.unwrap(doi.disputeMaxClockDuration()), "1700"); + assertEq(isCustomGasToken, doi.isCustomGasToken(), "1800"); // Assert inputs were properly passed through to the contract initializers. assertEq(address(doo.opChainProxyAdmin().owner()), opChainProxyAdminOwner, "2100"); @@ -486,6 +492,20 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { assertEq(doo.permissionedDisputeGame().splitDepth(), disputeSplitDepth + 1); } + function test_isCustomGasToken_whenTrue_succeeds() public { + setDOI(); + doi.set(doi.isCustomGasToken.selector, true); + deployOPChain.run(doi, doo); + assertEq(doi.isCustomGasToken(), true, "CGT-300"); + } + + function test_isCustomGasToken_whenFalse_succeeds() public { + setDOI(); + doi.set(doi.isCustomGasToken.selector, false); + deployOPChain.run(doi, doo); + assertEq(doi.isCustomGasToken(), false, "CGT-400"); + } + function setDOI() internal { doi.set(doi.opChainProxyAdminOwner.selector, opChainProxyAdminOwner); doi.set(doi.systemConfigOwner.selector, systemConfigOwner); @@ -505,5 +525,6 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { doi.set(doi.disputeSplitDepth.selector, disputeSplitDepth); doi.set(doi.disputeClockExtension.selector, disputeClockExtension); doi.set(doi.disputeMaxClockDuration.selector, disputeMaxClockDuration); + doi.set(doi.isCustomGasToken.selector, false); } } diff --git a/packages/contracts-bedrock/test/scripts/L2Genesis.t.sol b/packages/contracts-bedrock/test/scripts/L2Genesis.t.sol index 400f3285bf50b..9f7f86a14ded8 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. @@ -47,7 +49,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.isCustomGasToken)) { continue; } @@ -102,6 +104,21 @@ 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 @@ -128,8 +145,54 @@ contract L2Genesis_Run_Test is L2Genesis_TestInit { fork: uint256(LATEST_FORK), deployCrossL2Inbox: true, enableGovernance: true, - fundDevAccounts: true + fundDevAccounts: true, + isCustomGasToken: false, + gasPayingTokenName: "", + gasPayingTokenSymbol: "" + }); + genesis.run(input); + + testProxyAdmin(); + testPredeploys(); + testVaults(); + testGovernance(); + testFactories(); + testForks(); + } + + /// @dev Modifier to set up the input for L2Genesis with CGT enabled. + modifier setInputCGTEnabled() { + input = L2Genesis.Input({ + l1ChainID: 1, + l2ChainID: 2, + l1CrossDomainMessengerProxy: payable(address(0x0000000000000000000000000000000000000001)), + l1StandardBridgeProxy: payable(address(0x0000000000000000000000000000000000000002)), + l1ERC721BridgeProxy: payable(address(0x0000000000000000000000000000000000000003)), + opChainProxyAdminOwner: address(0x0000000000000000000000000000000000000004), + sequencerFeeVaultRecipient: address(0x0000000000000000000000000000000000000005), + sequencerFeeVaultMinimumWithdrawalAmount: 1, + sequencerFeeVaultWithdrawalNetwork: 1, + baseFeeVaultRecipient: address(0x0000000000000000000000000000000000000006), + baseFeeVaultMinimumWithdrawalAmount: 1, + baseFeeVaultWithdrawalNetwork: 1, + l1FeeVaultRecipient: address(0x0000000000000000000000000000000000000007), + l1FeeVaultMinimumWithdrawalAmount: 1, + l1FeeVaultWithdrawalNetwork: 1, + governanceTokenOwner: address(0x0000000000000000000000000000000000000008), + fork: uint256(LATEST_FORK), + deployCrossL2Inbox: true, + enableGovernance: true, + fundDevAccounts: true, + isCustomGasToken: true, + gasPayingTokenName: "Custom Gas Token", + 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(); @@ -138,5 +201,29 @@ contract L2Genesis_Run_Test is L2Genesis_TestInit { testGovernance(); testFactories(); testForks(); + testCGT(); + } + + /// @notice Tests that the run function reverts when CGT is enabled and the withdrawal network type of the FeeVaults + /// is L1. + function test_run_cgt_reverts() external setInputCGTEnabled { + // Expect revert when sequencerFeeVaultWithdrawalNetwork is L1 + input.sequencerFeeVaultWithdrawalNetwork = 0; + vm.expectRevert("SequencerFeeVault: withdrawalNetwork type cannot be L1 when custom gas token is enabled"); + genesis.run(input); + // Reset sequencerFeeVaultWithdrawalNetwork input to L2 + input.sequencerFeeVaultWithdrawalNetwork = 1; + + // Expect revert when baseFeeVaultWithdrawalNetwork is L1 + input.baseFeeVaultWithdrawalNetwork = 0; + vm.expectRevert("BaseFeeVault: withdrawalNetwork type cannot be L1 when custom gas token is enabled"); + genesis.run(input); + // Reset baseFeeVaultWithdrawalNetwork input to L2 + input.baseFeeVaultWithdrawalNetwork = 1; + + // Expect revert when l1FeeVaultWithdrawalNetwork is L1 + input.l1FeeVaultWithdrawalNetwork = 0; + vm.expectRevert("L1FeeVault: withdrawalNetwork type cannot be L1 when custom gas token is enabled"); + genesis.run(input); } } diff --git a/packages/contracts-bedrock/test/setup/CommonTest.sol b/packages/contracts-bedrock/test/setup/CommonTest.sol index 5e7b1b2d0c396..fad13a485af28 100644 --- a/packages/contracts-bedrock/test/setup/CommonTest.sol +++ b/packages/contracts-bedrock/test/setup/CommonTest.sol @@ -34,6 +34,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. @@ -69,6 +70,12 @@ contract CommonTest is Test, Setup, Events { if (useUpgradedFork) { deploy.cfg().setUseUpgradedFork(true); } + if (useCustomGasToken) { + deploy.cfg().setIsCustomGasToken(true); + deploy.cfg().setBaseFeeVaultWithdrawalNetwork(1); + deploy.cfg().setL1FeeVaultWithdrawalNetwork(1); + deploy.cfg().setSequencerFeeVaultWithdrawalNetwork(1); + } if (isForkTest()) { // Skip any test suite which uses a nonstandard configuration. @@ -203,4 +210,9 @@ contract CommonTest is Test, Setup, Events { useUpgradedFork = false; } + + function enableCustomGasToken() public { + _checkNotDeployed("custom gas token"); + useCustomGasToken = true; + } } diff --git a/packages/contracts-bedrock/test/setup/Setup.sol b/packages/contracts-bedrock/test/setup/Setup.sol index ab79c92c1a500..9e5c9e200b41d 100644 --- a/packages/contracts-bedrock/test/setup/Setup.sol +++ b/packages/contracts-bedrock/test/setup/Setup.sol @@ -61,6 +61,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 @@ -145,6 +147,8 @@ contract Setup { 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) { @@ -323,7 +327,10 @@ contract Setup { fork: uint256(l2Fork), deployCrossL2Inbox: deploy.cfg().useInterop(), enableGovernance: deploy.cfg().enableGovernance(), - fundDevAccounts: deploy.cfg().fundDevAccounts() + fundDevAccounts: deploy.cfg().fundDevAccounts(), + isCustomGasToken: deploy.cfg().isCustomGasToken(), + gasPayingTokenName: "Custom Gas Token", + gasPayingTokenSymbol: "CGT" }) ); @@ -355,6 +362,8 @@ contract Setup { 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); diff --git a/packages/contracts-bedrock/test/vendor/Initializable.t.sol b/packages/contracts-bedrock/test/vendor/Initializable.t.sol index e7460729bc6f4..8b75fa1afeed5 100644 --- a/packages/contracts-bedrock/test/vendor/Initializable.t.sol +++ b/packages/contracts-bedrock/test/vendor/Initializable.t.sol @@ -123,7 +123,9 @@ contract Initializer_Test is CommonTest { InitializeableContract({ name: "OptimismPortal2Impl", target: EIP1967Helper.getImplementation(address(optimismPortal2)), - initCalldata: abi.encodeCall(optimismPortal2.initialize, (systemConfig, anchorStateRegistry, ethLockbox)) + initCalldata: abi.encodeCall( + optimismPortal2.initialize, (systemConfig, anchorStateRegistry, ethLockbox, false) + ) }) ); // OptimismPortal2Proxy @@ -131,7 +133,9 @@ contract Initializer_Test is CommonTest { InitializeableContract({ name: "OptimismPortal2Proxy", target: address(optimismPortal2), - initCalldata: abi.encodeCall(optimismPortal2.initialize, (systemConfig, anchorStateRegistry, ethLockbox)) + initCalldata: abi.encodeCall( + optimismPortal2.initialize, (systemConfig, anchorStateRegistry, ethLockbox, false) + ) }) );