diff --git a/.circleci/continue/main.yml b/.circleci/continue/main.yml index 7ce6c49f7eb..d670aa52c88 100644 --- a/.circleci/continue/main.yml +++ b/.circleci/continue/main.yml @@ -3198,6 +3198,20 @@ workflows: - cannon-prestate - rust-binaries-for-sysgo - go-binaries-for-sysgo + # IN-MEMORY (celo) — dedicated job for the Celo CIP-64 acceptance gate so + # failures are visible independently of the gateless memory-all job. + - op-acceptance-tests: + name: memory-celo + gate: celo + context: + - circleci-repo-readonly-authenticated-github-token + - slack + - discord + requires: + - contracts-bedrock-build + - cannon-prestate + - rust-binaries-for-sysgo + - go-binaries-for-sysgo # Generate flaky test report - generate-flaky-report: name: generate-flaky-tests-report diff --git a/op-acceptance-tests/acceptance-tests.yaml b/op-acceptance-tests/acceptance-tests.yaml index f6f6c5237ec..764bb4fbb1e 100644 --- a/op-acceptance-tests/acceptance-tests.yaml +++ b/op-acceptance-tests/acceptance-tests.yaml @@ -185,6 +185,12 @@ gates: - package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/custom_gas_token timeout: 10m + - id: celo + description: "Celo-specific tests (CIP-64 fee currencies, etc.)." + tests: + - package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/celo + timeout: 10m + - id: supernode description: "Supernode tests - tests for the op-supernode multi-chain consensus layer." tests: diff --git a/op-acceptance-tests/justfile b/op-acceptance-tests/justfile index 34faa2e14f8..e39cbaeab5c 100644 --- a/op-acceptance-tests/justfile +++ b/op-acceptance-tests/justfile @@ -17,6 +17,9 @@ interop: cgt: @just acceptance-test "" cgt +celo: + @just acceptance-test "" celo + # Run acceptance tests with mise-managed binary # Usage: just acceptance-test [devnet] [gate] diff --git a/op-acceptance-tests/tests/celo/cip64_test.go b/op-acceptance-tests/tests/celo/cip64_test.go new file mode 100644 index 00000000000..85a19705741 --- /dev/null +++ b/op-acceptance-tests/tests/celo/cip64_test.go @@ -0,0 +1,84 @@ +package celo + +import ( + "context" + "math/big" + "testing" + "time" + + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + + "github.com/ethereum-optimism/optimism/op-core/predeploys" + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/presets" +) + +// TestCIP64_PayGasInTestFeeCurrency sends a CeloDynamicFeeTxV2 (CIP-64) with +// FeeCurrency set to the registered test fee currency. devAccounts[0] is the +// foundry test mnemonic account that L2Genesis pre-funds with 100k TEST and +// zero native CELO; the only way the tx can be paid for is via op-geth's +// fee-currency machinery. +func TestCIP64_PayGasInTestFeeCurrency(gt *testing.T) { + t := devtest.SerialT(gt) + sys := presets.NewMinimal(t) + ensureCeloFeeCurrencyOrSkip(t, sys) + + priv, err := crypto.HexToECDSA(foundryDevAccount0PrivKeyHex) + t.Require().NoError(err, "parse foundry dev priv key") + sender := crypto.PubkeyToAddress(priv.PublicKey) + + l2 := sys.L2EL.Escape().L2EthClient() + chainID := sys.L2Chain.ChainID().ToBig() + recipient := sys.Wallet.NewEOA(sys.L2EL).Address() + + ctx, cancel := context.WithTimeout(t.Ctx(), 60*time.Second) + defer cancel() + + nativeBefore, err := l2.BalanceAt(ctx, sender, nil) + t.Require().NoError(err, "get native balance") + testBefore := erc20BalanceOf(t, sys, predeploys.TestFeeCurrencyAddr, sender) + t.Require().Truef(testBefore.Sign() > 0, "sender must have a positive TEST balance, got %s", testBefore) + + nonce, err := l2.PendingNonceAt(ctx, sender) + t.Require().NoError(err, "get nonce") + + tx := types.NewTx(&types.CeloDynamicFeeTxV2{ + ChainID: chainID, + Nonce: nonce, + GasTipCap: big.NewInt(1_000_000_000), // 1 gwei + GasFeeCap: big.NewInt(10_000_000_000), // 10 gwei + Gas: 200_000, + To: &recipient, + Value: big.NewInt(0), + FeeCurrency: &predeploys.TestFeeCurrencyAddr, + }) + signed, err := types.SignTx(tx, types.LatestSignerForChainID(chainID), priv) + t.Require().NoError(err, "sign CIP-64 tx") + t.Require().NoError(l2.SendTransaction(ctx, signed), "send CIP-64 tx") + + var receipt *types.Receipt + t.Require().Eventually(func() bool { + r, err := l2.TransactionReceipt(ctx, signed.Hash()) + if err != nil { + return false + } + receipt = r + return true + }, sys.L2EL.Escape().TransactionTimeout(), time.Second, "tx receipt") + + t.Require().Equal(uint64(1), receipt.Status, "CIP-64 tx must succeed") + + nativeAfter, err := l2.BalanceAt(ctx, sender, nil) + t.Require().NoError(err, "get native balance after") + testAfter := erc20BalanceOf(t, sys, predeploys.TestFeeCurrencyAddr, sender) + + // Native balance must not change: gas was paid in TEST, no value transferred. + t.Require().Equal(0, nativeBefore.Cmp(nativeAfter), + "native balance changed: before=%s after=%s", nativeBefore, nativeAfter) + + // TEST balance must drop — the fee-currency machinery debited gas + intrinsic-gas + // surcharge in TEST tokens. + t.Require().Truef(testAfter.Cmp(testBefore) < 0, + "TEST balance must drop, got before=%s after=%s", testBefore, testAfter) +} diff --git a/op-acceptance-tests/tests/celo/helpers.go b/op-acceptance-tests/tests/celo/helpers.go new file mode 100644 index 00000000000..a644649b167 --- /dev/null +++ b/op-acceptance-tests/tests/celo/helpers.go @@ -0,0 +1,76 @@ +package celo + +import ( + "context" + "math/big" + "time" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/rpc" + "github.com/lmittmann/w3" + + "github.com/ethereum-optimism/optimism/op-core/predeploys" + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/presets" +) + +// Foundry "test test test test test test test test test test test junk" mnemonic, +// path m/44'/60'/0'/0/0 — same address that L2Genesis.s.sol pre-funds with 100k TEST. +const foundryDevAccount0PrivKeyHex = "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" + +// ensureCeloFeeCurrencyOrSkip checks that the FeeCurrencyDirectory predeploy is in +// place and that the test fee currency is registered. Skips the test otherwise so +// the gate can be run against environments without the Celo predeploys. +func ensureCeloFeeCurrencyOrSkip(t devtest.T, sys *presets.Minimal) { + l2 := sys.L2EL.Escape().L2EthClient() + + ctx, cancel := context.WithTimeout(t.Ctx(), 20*time.Second) + defer cancel() + + // Skip if the FeeCurrencyDirectory has no code: eth_call on an empty account + // silently returns empty bytes rather than reverting, so we have to check code + // length explicitly. + header, err := l2.InfoByLabel(ctx, "latest") + t.Require().NoError(err, "fetch latest L2 header") + code, err := l2.CodeAtHash(ctx, predeploys.FeeCurrencyDirectoryAddr, header.Hash()) + t.Require().NoError(err, "eth_getCode FeeCurrencyDirectory") + if len(code) == 0 { + t.Skip("FeeCurrencyDirectory predeploy missing — chain is not running with Celo predeploys") + } + + // Confirm the test fee currency is actually registered. An unregistered token + // returns a zero-valued config (oracle = address(0)) without reverting, so the + // presence of a non-zero oracle is what proves registration. The first 32 bytes + // of the ABI-encoded return are the oracle address (left-padded). + getCurrencyConfig := w3.MustNewFunc( + "getCurrencyConfig(address)", + "(address oracle, uint256 intrinsicGas)", + ) + data, err := getCurrencyConfig.EncodeArgs(predeploys.TestFeeCurrencyAddr) + t.Require().NoError(err, "encode getCurrencyConfig") + out, err := l2.Call(ctx, ethereum.CallMsg{To: &predeploys.FeeCurrencyDirectoryAddr, Data: data}, rpc.LatestBlockNumber) + t.Require().NoError(err, "FeeCurrencyDirectory.getCurrencyConfig") + if len(out) < 32 || common.BytesToAddress(out[:32]) == (common.Address{}) { + t.Skip("test fee currency not registered with FeeCurrencyDirectory") + } +} + +// erc20BalanceOf reads the ERC-20 balance of `account` for the token at `token`. +func erc20BalanceOf(t devtest.T, sys *presets.Minimal, token, account common.Address) *big.Int { + l2 := sys.L2EL.Escape().L2EthClient() + + ctx, cancel := context.WithTimeout(t.Ctx(), 20*time.Second) + defer cancel() + + balanceOf := w3.MustNewFunc("balanceOf(address)", "uint256") + data, err := balanceOf.EncodeArgs(account) + t.Require().NoError(err, "encode balanceOf") + + out, err := l2.Call(ctx, ethereum.CallMsg{To: &token, Data: data}, rpc.LatestBlockNumber) + t.Require().NoError(err, "call balanceOf") + + var bal *big.Int + t.Require().NoError(balanceOf.DecodeReturns(out, &bal), "decode balanceOf") + return bal +} diff --git a/op-acceptance-tests/tests/celo/init_test.go b/op-acceptance-tests/tests/celo/init_test.go new file mode 100644 index 00000000000..9c8cbcbbc7c --- /dev/null +++ b/op-acceptance-tests/tests/celo/init_test.go @@ -0,0 +1,18 @@ +package celo + +import ( + "testing" + + "github.com/ethereum-optimism/optimism/op-devstack/presets" + "github.com/ethereum-optimism/optimism/op-devstack/stack" + "github.com/ethereum-optimism/optimism/op-devstack/sysgo" +) + +func TestMain(m *testing.M) { + presets.DoMain(m, + presets.WithMinimal(), + stack.MakeCommon(sysgo.WithDeployerOptions( + sysgo.WithCelo(), + )), + ) +} diff --git a/op-chain-ops/interopgen/deploy.go b/op-chain-ops/interopgen/deploy.go index 634177b022e..90e88fba982 100644 --- a/op-chain-ops/interopgen/deploy.go +++ b/op-chain-ops/interopgen/deploy.go @@ -257,6 +257,7 @@ func DeployL2ToL1(l1Host *script.Host, superCfg *SuperchainConfig, superDeployme OperatorFeeConstant: cfg.GasPriceOracleOperatorFeeConstant, SuperchainConfig: superDeployment.SuperchainConfigProxy, UseCustomGasToken: cfg.UseCustomGasToken, + DeployCeloContracts: false, }) if err != nil { return nil, fmt.Errorf("failed to deploy L2 OP chain: %w", err) @@ -356,6 +357,7 @@ func GenesisL2(l2Host *script.Host, cfg *L2Config, deployment *L2Deployment, mul NativeAssetLiquidityAmount: cfg.NativeAssetLiquidityAmount.ToInt(), LiquidityControllerOwner: cfg.LiquidityControllerOwner, UseL2CM: false, // TODO(#19102): add support for L2CM + DeployCeloContracts: false, }); err != nil { return fmt.Errorf("failed L2 genesis: %w", err) } diff --git a/op-core/predeploys/addresses.go b/op-core/predeploys/addresses.go index 89ca5f2f1a2..153fd859c14 100644 --- a/op-core/predeploys/addresses.go +++ b/op-core/predeploys/addresses.go @@ -57,6 +57,14 @@ const ( SortedOracles = "0xefb84935239dacdecf7c5ba76d8de40b077b7b33" AddressSortedLinkedListWithMedian = "0xED477A99035d0c1e11369F1D7A4e587893cc002B" FeeCurrency = "0x4200000000000000000000000000000000001022" + // FeeCurrencyDirectory matches the celo-mainnet address that op-geth's + // addresses.MainnetAddresses defaults to when no chain-specific entry exists. + FeeCurrencyDirectory = "0x15F344b9E6c3Cb6F0376A36A64928b13F62C6276" + // TestFeeCurrency is a non-production fee currency installed by L2Genesis when + // deployCeloContracts is set, registered with the FeeCurrencyDirectory so CIP-64 + // transactions can pay gas in it. The address is the celo-mainnet cUSD address + // (kept for a deterministic predeploy slot). + TestFeeCurrency = "0x765DE816845861e75A25fCA122bb6898B8B1282a" ) var ( @@ -114,6 +122,8 @@ var ( SortedOraclesAddr = common.HexToAddress(SortedOracles) AddressSortedLinkedListWithMedianAddr = common.HexToAddress(AddressSortedLinkedListWithMedian) FeeCurrencyAddr = common.HexToAddress(FeeCurrency) + FeeCurrencyDirectoryAddr = common.HexToAddress(FeeCurrencyDirectory) + TestFeeCurrencyAddr = common.HexToAddress(TestFeeCurrency) CeloPredeploys = make(map[string]*Predeploy) ) @@ -213,6 +223,8 @@ func init() { CeloPredeploys["SortedOracles"] = &Predeploy{Address: SortedOraclesAddr} CeloPredeploys["AddressSortedLinkedListWithMedian"] = &Predeploy{Address: AddressSortedLinkedListWithMedianAddr} CeloPredeploys["FeeCurrency"] = &Predeploy{Address: FeeCurrencyAddr} + CeloPredeploys["FeeCurrencyDirectory"] = &Predeploy{Address: FeeCurrencyDirectoryAddr} + CeloPredeploys["TestFeeCurrency"] = &Predeploy{Address: TestFeeCurrencyAddr} for key, predeploy := range CeloPredeploys { Predeploys[key] = predeploy } diff --git a/op-deployer/pkg/deployer/integration_test/apply_test.go b/op-deployer/pkg/deployer/integration_test/apply_test.go index cae2024ae68..e429a5f61be 100644 --- a/op-deployer/pkg/deployer/integration_test/apply_test.go +++ b/op-deployer/pkg/deployer/integration_test/apply_test.go @@ -535,6 +535,40 @@ func TestApplyGenesisStrategy(t *testing.T) { }) } +func TestApplyDeployCeloContracts(t *testing.T) { + op_e2e.InitParallel(t) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + opts, intent, st := setupGenesisChain(t, devnet.DefaultChainID) + intent.Chains[0].DeployCeloContracts = true + + require.NoError(t, deployer.ApplyPipeline(ctx, opts)) + + allocs := st.Chains[0].Allocs.Data.Accounts + + // Predeploys (proxies — code is the EIP-1967 Proxy bytecode at these addresses). + proxied := []common.Address{ + predeploys.CeloRegistryAddr, + predeploys.GoldTokenAddr, + predeploys.FeeHandlerAddr, + predeploys.SortedOraclesAddr, + predeploys.FeeCurrencyDirectoryAddr, + predeploys.TestFeeCurrencyAddr, // StableTokenV2 instance registered as a fee currency + } + for _, addr := range proxied { + acc, ok := allocs[addr] + require.Truef(t, ok, "no account at %s", addr) + require.NotEmpty(t, acc.Code, "no code at %s", addr) + } + + // Library — etched directly, not proxied. + acc, ok := allocs[predeploys.AddressSortedLinkedListWithMedianAddr] + require.True(t, ok, "no account at AddressSortedLinkedListWithMedian") + require.NotEmpty(t, acc.Code) +} + func TestProofParamOverrides(t *testing.T) { op_e2e.InitParallel(t) diff --git a/op-deployer/pkg/deployer/opcm/l2genesis.go b/op-deployer/pkg/deployer/opcm/l2genesis.go index 29deabfd740..921da36e9db 100644 --- a/op-deployer/pkg/deployer/opcm/l2genesis.go +++ b/op-deployer/pkg/deployer/opcm/l2genesis.go @@ -40,6 +40,7 @@ type L2GenesisInput struct { NativeAssetLiquidityAmount *big.Int LiquidityControllerOwner common.Address UseL2CM bool + DeployCeloContracts bool } type L2GenesisScript script.DeployScriptWithoutOutput[L2GenesisInput] diff --git a/op-deployer/pkg/deployer/opcm/opchain.go b/op-deployer/pkg/deployer/opcm/opchain.go index 89cef911c5f..72d9a068b2d 100644 --- a/op-deployer/pkg/deployer/opcm/opchain.go +++ b/op-deployer/pkg/deployer/opcm/opchain.go @@ -47,6 +47,8 @@ type DeployOPChainInput struct { SuperchainConfig common.Address UseCustomGasToken bool + + DeployCeloContracts bool } type DeployOPChainOutput struct { diff --git a/op-deployer/pkg/deployer/pipeline/l2genesis.go b/op-deployer/pkg/deployer/pipeline/l2genesis.go index 08e3f10b26c..f0680a935ab 100644 --- a/op-deployer/pkg/deployer/pipeline/l2genesis.go +++ b/op-deployer/pkg/deployer/pipeline/l2genesis.go @@ -127,6 +127,7 @@ func GenerateL2Genesis(pEnv *Env, intent *state.Intent, bundle ArtifactsBundle, NativeAssetLiquidityAmount: cgt.NativeAssetLiquidityAmount, LiquidityControllerOwner: cgt.LiquidityControllerOwner, UseL2CM: useL2CM, + DeployCeloContracts: thisIntent.DeployCeloContracts, }); err != nil { return fmt.Errorf("failed to call L2Genesis script: %w", err) } diff --git a/op-deployer/pkg/deployer/pipeline/opchain.go b/op-deployer/pkg/deployer/pipeline/opchain.go index c923bc54aeb..741f24011c3 100644 --- a/op-deployer/pkg/deployer/pipeline/opchain.go +++ b/op-deployer/pkg/deployer/pipeline/opchain.go @@ -151,8 +151,8 @@ func makeDCI(intent *state.Intent, thisIntent *state.ChainIntent, chainID common UnsafeBlockSigner: thisIntent.Roles.UnsafeBlockSigner, Proposer: thisIntent.Roles.Proposer, Challenger: thisIntent.Roles.Challenger, - BasefeeScalar: standard.BasefeeScalar, - BlobBaseFeeScalar: standard.BlobBaseFeeScalar, + BasefeeScalar: celoOrDefaultScalar(thisIntent.DeployCeloContracts, standard.BasefeeScalar), + BlobBaseFeeScalar: celoOrDefaultScalar(thisIntent.DeployCeloContracts, standard.BlobBaseFeeScalar), L2ChainId: chainID.Big(), Opcm: opcmAddr, SaltMixer: st.Create2Salt.String(), // passing through salt generated at state initialization @@ -168,9 +168,19 @@ func makeDCI(intent *state.Intent, thisIntent *state.ChainIntent, chainID common OperatorFeeConstant: thisIntent.OperatorFeeConstant, SuperchainConfig: st.SuperchainDeployment.SuperchainConfigProxy, UseCustomGasToken: thisIntent.IsCustomGasTokenEnabled(), + DeployCeloContracts: thisIntent.DeployCeloContracts, }, nil } +// celoOrDefaultScalar returns 0 when Celo contracts are deployed (so the chain operator +// pays all L1 DA cost on behalf of users) and the standard default otherwise. +func celoOrDefaultScalar(celo bool, def uint32) uint32 { + if celo { + return 0 + } + return def +} + func makeChainState(chainID common.Hash, impls opcm.ReadImplementationAddressesOutput, dco opcm.DeployOPChainOutput) *state.ChainState { opChainContracts := addresses.OpChainContracts{} opChainContracts.OpChainProxyAdminImpl = dco.OpChainProxyAdmin diff --git a/op-deployer/pkg/deployer/state/chain_intent.go b/op-deployer/pkg/deployer/state/chain_intent.go index 101aac45b2b..84e2125bc36 100644 --- a/op-deployer/pkg/deployer/state/chain_intent.go +++ b/op-deployer/pkg/deployer/state/chain_intent.go @@ -83,6 +83,7 @@ type ChainIntent struct { MinBaseFee uint64 `json:"minBaseFee,omitempty" toml:"minBaseFee,omitempty"` DAFootprintGasScalar uint16 `json:"daFootprintGasScalar,omitempty" toml:"daFootprintGasScalar,omitempty"` CustomGasToken CustomGasToken `json:"customGasToken" toml:"customGasToken"` + DeployCeloContracts bool `json:"deployCeloContracts,omitempty" toml:"deployCeloContracts,omitempty"` // Optional. For development purposes only. Only enabled if the operation mode targets a genesis-file output. L2DevGenesisParams *L2DevGenesisParams `json:"l2DevGenesisParams,omitempty" toml:"l2DevGenesisParams,omitempty"` diff --git a/op-deployer/pkg/deployer/state/deploy_config.go b/op-deployer/pkg/deployer/state/deploy_config.go index 72d92746ae5..967daf33cc8 100644 --- a/op-deployer/pkg/deployer/state/deploy_config.go +++ b/op-deployer/pkg/deployer/state/deploy_config.go @@ -62,8 +62,8 @@ func CombineDeployConfig(intent *Intent, chainIntent *ChainIntent, state *State, GovernanceTokenOwner: standard.GovernanceTokenOwner, }, GasPriceOracleDeployConfig: genesis.GasPriceOracleDeployConfig{ - GasPriceOracleBaseFeeScalar: standard.BasefeeScalar, - GasPriceOracleBlobBaseFeeScalar: standard.BlobBaseFeeScalar, + GasPriceOracleBaseFeeScalar: celoOrDefaultScalar(chainIntent.DeployCeloContracts, standard.BasefeeScalar), + GasPriceOracleBlobBaseFeeScalar: celoOrDefaultScalar(chainIntent.DeployCeloContracts, standard.BlobBaseFeeScalar), GasPriceOracleOperatorFeeScalar: chainIntent.OperatorFeeScalar, GasPriceOracleOperatorFeeConstant: chainIntent.OperatorFeeConstant, }, @@ -184,3 +184,12 @@ func calculateBatchInboxAddr(chainID common.Hash) common.Address { copy(out[1:], crypto.Keccak256(chainID[:])[:19]) return out } + +// celoOrDefaultScalar returns 0 when Celo contracts are deployed (so the chain operator +// pays all L1 DA cost on behalf of users) and the standard default otherwise. +func celoOrDefaultScalar(celo bool, def uint32) uint32 { + if celo { + return 0 + } + return def +} diff --git a/op-devstack/sysgo/deployer.go b/op-devstack/sysgo/deployer.go index 94627337386..faef7e89163 100644 --- a/op-devstack/sysgo/deployer.go +++ b/op-devstack/sysgo/deployer.go @@ -406,6 +406,17 @@ func WithCustomGasToken(name, symbol string, initialLiquidity *big.Int, liquidit } } +// WithCelo enables Celo predeploys (CeloRegistry, FeeCurrencyDirectory, cUSD, ...) in the +// L2 genesis allocs. cUSD is registered as a fee currency so CIP-64 (CeloDynamicFeeTxV2) +// transactions can be paid in cUSD. devAccounts[0] is funded with 100k cUSD. +func WithCelo() DeployerOption { + return func(p devtest.P, keys devkeys.Keys, builder intentbuilder.Builder) { + for _, l2Cfg := range builder.L2s() { + l2Cfg.WithDeployCeloContracts() + } + } +} + 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-devstack/sysgo/l2_el_opgeth.go b/op-devstack/sysgo/l2_el_opgeth.go index 5b2be235e7e..6b60f6d6d16 100644 --- a/op-devstack/sysgo/l2_el_opgeth.go +++ b/op-devstack/sysgo/l2_el_opgeth.go @@ -5,14 +5,17 @@ import ( "strings" "sync" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/eth/ethconfig" "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/miner" gn "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p/enode" "github.com/ethereum/go-ethereum/rpc" + "github.com/ethereum-optimism/optimism/op-core/predeploys" "github.com/ethereum-optimism/optimism/op-devstack/devtest" "github.com/ethereum-optimism/optimism/op-devstack/shim" "github.com/ethereum-optimism/optimism/op-devstack/stack" @@ -118,6 +121,21 @@ func (n *OpGeth) Start() { ethCfg.InteropMessageRPC = n.supervisorRPC ethCfg.InteropMempoolFiltering = true + // If WithCelo() was used, the L2 genesis has the FeeCurrencyDirectory at + // the celo-mainnet address. Configure op-geth's MultiGasPool so CIP-64 txs + // don't get silently popped at block-building time: + // - FeeCurrencyDefault: default per-currency fraction of the block gas limit + // (mirrors miner.DefaultConfig). + // - FeeCurrencyLimits: explicit 0.9 for the registered test fee currency, + // mirroring celo-mainnet's cUSD/USDT/USDC config. + if alloc, ok := n.l2Net.genesis.Alloc[predeploys.FeeCurrencyDirectoryAddr]; ok && len(alloc.Code) > 0 { + ethCfg.Miner.FeeCurrencyDefault = miner.DefaultFeeCurrencyLimit + if ethCfg.Miner.FeeCurrencyLimits == nil { + ethCfg.Miner.FeeCurrencyLimits = map[common.Address]float64{} + } + ethCfg.Miner.FeeCurrencyLimits[predeploys.TestFeeCurrencyAddr] = 0.9 + } + listenAddr := n.cfg.P2PAddr port := n.cfg.P2PPort listenAddr = listenAddr + ":" + strconv.Itoa(port) diff --git a/op-e2e/e2eutils/intentbuilder/builder.go b/op-e2e/e2eutils/intentbuilder/builder.go index 8ad9b4a03b6..26445e0c7fa 100644 --- a/op-e2e/e2eutils/intentbuilder/builder.go +++ b/op-e2e/e2eutils/intentbuilder/builder.go @@ -56,6 +56,7 @@ type L2Configurator interface { WithFinalizationPeriodSeconds(value uint64) WithRevenueShare(enabled bool, chainFeesRecipient common.Address) WithCustomGasToken(name string, symbol string, initialLiquidity *big.Int, liquidityControllerOwner common.Address) + WithDeployCeloContracts() ContractsConfigurator L2VaultsConfigurator L2RolesConfigurator @@ -480,6 +481,10 @@ func (c *l2Configurator) WithCustomGasToken(name, symbol string, initialLiquidit } } +func (c *l2Configurator) WithDeployCeloContracts() { + c.builder.intent.Chains[c.chainIndex].DeployCeloContracts = true +} + func (c *l2Configurator) WithEIP1559Elasticity(value uint64) { c.builder.intent.Chains[c.chainIndex].Eip1559Elasticity = value } diff --git a/packages/contracts-bedrock/justfile b/packages/contracts-bedrock/justfile index 56ab962526e..b94f54ed77b 100644 --- a/packages/contracts-bedrock/justfile +++ b/packages/contracts-bedrock/justfile @@ -16,9 +16,14 @@ dep-status: ######################################################## # Core forge build command. - +# +# AddressSortedLinkedListWithMedian is a library used by Celo predeploys; without +# the explicit address forge writes link placeholders into the bytecode and +# downstream artifact loaders fail. Configuring this in foundry.toml's libraries +# array works for forge but writes a metadata format that breaks Go-side artifact +# parsers, so we set it on the CLI instead — applied to every forge build. forge-build *ARGS: - forge build {{ARGS}} + forge build --libraries "src/celo/common/linkedlists/AddressSortedLinkedListWithMedian.sol:AddressSortedLinkedListWithMedian:0xED477A99035d0c1e11369F1D7A4e587893cc002B" {{ARGS}} @# Forge build compiles only the src/ graph; the scripts/ graph is compiled by `forge script`. @# On the first invocation, `forge script` may compile a small set of dependencies. diff --git a/packages/contracts-bedrock/scripts/L2Genesis.s.sol b/packages/contracts-bedrock/scripts/L2Genesis.s.sol index 2385d962c43..43e178cf27c 100644 --- a/packages/contracts-bedrock/scripts/L2Genesis.s.sol +++ b/packages/contracts-bedrock/scripts/L2Genesis.s.sol @@ -34,6 +34,19 @@ import { IFeeVault } from "interfaces/L2/IFeeVault.sol"; import { IL1Withdrawer } from "interfaces/L2/IL1Withdrawer.sol"; import { ISuperchainRevSharesCalculator } from "interfaces/L2/ISuperchainRevSharesCalculator.sol"; +// Celo predeploys +import { CeloPredeploys } from "src/celo/CeloPredeploys.sol"; +import { CeloRegistry } from "src/celo/CeloRegistry.sol"; +import { GoldToken } from "src/celo/GoldToken.sol"; +import { FeeHandler } from "src/celo/FeeHandler.sol"; +import { MentoFeeHandlerSeller } from "src/celo/MentoFeeHandlerSeller.sol"; +import { UniswapFeeHandlerSeller } from "src/celo/UniswapFeeHandlerSeller.sol"; +import { SortedOracles } from "src/celo/stability/SortedOracles.sol"; +import { FeeCurrencyDirectory } from "src/celo/FeeCurrencyDirectory.sol"; +import { FeeCurrency } from "src/celo/testing/FeeCurrency.sol"; +import { StableTokenV2 } from "src/celo/StableTokenV2.sol"; +import { AddressSortedLinkedListWithMedian } from "src/celo/common/linkedlists/AddressSortedLinkedListWithMedian.sol"; + /// @title L2Genesis /// @notice Generates the genesis state for the L2 network. /// The following safety invariants are used when setting state: @@ -82,6 +95,7 @@ contract L2Genesis is Script { uint256 nativeAssetLiquidityAmount; address liquidityControllerOwner; bool useL2CM; + bool deployCeloContracts; } using ForkUtils for Fork; @@ -142,6 +156,9 @@ contract L2Genesis is Script { if (_input.fundDevAccounts) { fundDevAccounts(); } + if (_input.deployCeloContracts) { + setCeloPredeploys(deployer); + } vm.stopPrank(); vm.deal(deployer, 0); @@ -763,4 +780,139 @@ contract L2Genesis is Script { vm.deal(devAccounts[i], DEV_ACCOUNT_FUND_AMT); } } + + /// @notice Etches all Celo predeploy contracts and registers a test fee currency. + /// devAccounts[0] receives 100k TEST tokens and owns the FeeCurrencyDirectory. + function setCeloPredeploys(address _deployer) internal { + setCeloRegistry(); + setCeloGoldToken(); + setCeloFeeHandler(); + setCeloMentoFeeHandlerSeller(); + setCeloUniswapFeeHandlerSeller(); + setCeloAddressSortedLinkedListWithMedian(); + setCeloSortedOracles(); + setCeloFeeCurrency(); + setFeeCurrencyDirectory(); + + address[] memory initialBalanceAddresses = new address[](2); + initialBalanceAddresses[0] = devAccounts[0]; + // Seed the FeeHandler with 1 wei to avoid the cold-SSTORE penalty on its first transfer. + initialBalanceAddresses[1] = CeloPredeploys.FEE_HANDLER; + + uint256[] memory initialBalances = new uint256[](2); + initialBalances[0] = 100_000 ether; + initialBalances[1] = 1; + + deployTestFeeCurrency(_deployer, initialBalanceAddresses, initialBalances, 2); + } + + function _setupCeloProxy(address _addr, address _impl) internal { + bytes memory code = vm.getDeployedCode("Proxy.sol:Proxy"); + vm.etch(_addr, code); + EIP1967Helper.setAdmin(_addr, Predeploys.PROXY_ADMIN); + EIP1967Helper.setImplementation(_addr, _impl); + } + + function setCeloRegistry() internal { + CeloRegistry impl = new CeloRegistry({ test: false }); + vm.resetNonce(address(impl)); + _setupCeloProxy(CeloPredeploys.CELO_REGISTRY, address(impl)); + } + + function setCeloGoldToken() internal { + GoldToken impl = new GoldToken({ test: false }); + vm.resetNonce(address(impl)); + _setupCeloProxy(CeloPredeploys.GOLD_TOKEN, address(impl)); + } + + function setCeloFeeHandler() internal { + FeeHandler impl = new FeeHandler({ test: false }); + vm.resetNonce(address(impl)); + _setupCeloProxy(CeloPredeploys.FEE_HANDLER, address(impl)); + } + + function setCeloMentoFeeHandlerSeller() internal { + MentoFeeHandlerSeller impl = new MentoFeeHandlerSeller({ test: false }); + vm.resetNonce(address(impl)); + _setupCeloProxy(CeloPredeploys.MENTO_FEE_HANDLER_SELLER, address(impl)); + } + + function setCeloUniswapFeeHandlerSeller() internal { + UniswapFeeHandlerSeller impl = new UniswapFeeHandlerSeller({ test: false }); + vm.resetNonce(address(impl)); + _setupCeloProxy(CeloPredeploys.UNISWAP_FEE_HANDLER_SELLER, address(impl)); + } + + function setCeloAddressSortedLinkedListWithMedian() internal { + bytes memory runtimeCode = type(AddressSortedLinkedListWithMedian).runtimeCode; + vm.etch(CeloPredeploys.ADDRESS_SORTED_LINKED_LIST_WITH_MEDIAN, runtimeCode); + } + + function setCeloSortedOracles() internal { + SortedOracles impl = new SortedOracles({ test: false }); + vm.resetNonce(address(impl)); + _setupCeloProxy(CeloPredeploys.SORTED_ORACLES, address(impl)); + // Initialize so owner() is non-zero and reportExpirySeconds is set. 1 hour is + // generous enough for tests; production deployments configure their own value. + SortedOracles(CeloPredeploys.SORTED_ORACLES).initialize(1 hours); + } + + function setCeloFeeCurrency() internal { + FeeCurrency impl = new FeeCurrency({ name_: "Test", symbol_: "TST" }); + vm.resetNonce(address(impl)); + _setupCeloProxy(CeloPredeploys.FEE_CURRENCY, address(impl)); + } + + function setFeeCurrencyDirectory() internal { + FeeCurrencyDirectory impl = new FeeCurrencyDirectory({ test: false }); + vm.resetNonce(address(impl)); + _setupCeloProxy(CeloPredeploys.FEE_CURRENCY_DIRECTORY, address(impl)); + + // Initialize the proxy and transfer ownership to devAccounts[0] so devnets can + // edit the directory at runtime. + vm.prank(devAccounts[0]); + FeeCurrencyDirectory(CeloPredeploys.FEE_CURRENCY_DIRECTORY).initialize(); + } + + /// @notice Deploys a test fee currency (StableTokenV2) at its predeploy address, mints + /// initial balances, registers _deployer as a price oracle, reports a price, + /// and registers the token with the FeeCurrencyDirectory so the EL accepts + /// CIP-64 transactions paying gas in it. + function deployTestFeeCurrency( + address _deployer, + address[] memory _initialBalanceAddresses, + uint256[] memory _initialBalances, + uint256 _price + ) + internal + { + StableTokenV2 impl = new StableTokenV2({ disable: false }); + vm.resetNonce(address(impl)); + _setupCeloProxy(CeloPredeploys.TEST_FEE_CURRENCY, address(impl)); + + StableTokenV2(CeloPredeploys.TEST_FEE_CURRENCY).initialize( + "Test Fee Currency", "TEST", _initialBalanceAddresses, _initialBalances + ); + + // Register _deployer as an oracle for the test currency. addOracle is onlyOwner. + SortedOracles sortedOracles = SortedOracles(CeloPredeploys.SORTED_ORACLES); + vm.prank(sortedOracles.owner()); + sortedOracles.addOracle(CeloPredeploys.TEST_FEE_CURRENCY, _deployer); + + // Report a price as the registered oracle. + if (_price != 0) { + vm.prank(_deployer); + sortedOracles.report(CeloPredeploys.TEST_FEE_CURRENCY, _price * 1e24, address(0), address(0)); + } + + // Register the test currency with the FeeCurrencyDirectory so the EL recognizes it + // as a fee currency. Intrinsic gas is the per-tx surcharge added to cover the + // debitGasFees / creditGasFees calls into the fee-currency contract. + uint256 intrinsicGas = 80_000; + FeeCurrencyDirectory feeCurrencyDirectory = FeeCurrencyDirectory(CeloPredeploys.FEE_CURRENCY_DIRECTORY); + vm.prank(feeCurrencyDirectory.owner()); + feeCurrencyDirectory.setCurrencyConfig( + CeloPredeploys.TEST_FEE_CURRENCY, address(sortedOracles), intrinsicGas + ); + } } diff --git a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol index 2faa6cdcb11..bdb437c198f 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol @@ -290,8 +290,11 @@ contract DeployOPChain is Script { require(_i.proposer != address(0), "DeployOPChainInput: proposer not set"); require(_i.challenger != address(0), "DeployOPChainInput: challenger not set"); - require(_i.blobBaseFeeScalar != 0, "DeployOPChainInput: blobBaseFeeScalar not set"); - require(_i.basefeeScalar != 0, "DeployOPChainInput: basefeeScalar not set"); + // Zero scalars are valid for Celo-style chains where the operator pays all L1 DA cost. + if (!_i.deployCeloContracts) { + require(_i.blobBaseFeeScalar != 0, "DeployOPChainInput: blobBaseFeeScalar not set"); + require(_i.basefeeScalar != 0, "DeployOPChainInput: basefeeScalar not set"); + } require(_i.gasLimit != 0, "DeployOPChainInput: gasLimit not set"); require(_i.l2ChainId != 0, "DeployOPChainInput: l2ChainId not set"); diff --git a/packages/contracts-bedrock/scripts/libraries/Types.sol b/packages/contracts-bedrock/scripts/libraries/Types.sol index bec2f022e91..ede1d2268f6 100644 --- a/packages/contracts-bedrock/scripts/libraries/Types.sol +++ b/packages/contracts-bedrock/scripts/libraries/Types.sol @@ -54,5 +54,8 @@ library Types { ISuperchainConfig superchainConfig; // Whether to use the custom gas token. bool useCustomGasToken; + // Whether the chain is configured as a Celo chain (operator pays all L1 DA cost, + // basefeeScalar/blobBaseFeeScalar may be zero). + bool deployCeloContracts; } } diff --git a/packages/contracts-bedrock/src/celo/CeloPredeploys.sol b/packages/contracts-bedrock/src/celo/CeloPredeploys.sol index 2ca38a84576..e51143e37b4 100644 --- a/packages/contracts-bedrock/src/celo/CeloPredeploys.sol +++ b/packages/contracts-bedrock/src/celo/CeloPredeploys.sol @@ -12,8 +12,12 @@ library CeloPredeploys { address internal constant SORTED_ORACLES = 0xefB84935239dAcdecF7c5bA76d8dE40b077B7b33; address internal constant ADDRESS_SORTED_LINKED_LIST_WITH_MEDIAN = 0xED477A99035d0c1e11369F1D7A4e587893cc002B; address internal constant FEE_CURRENCY = 0x4200000000000000000000000000000000001022; - address internal constant FEE_CURRENCY_DIRECTORY = 0x9212Fb72ae65367A7c887eC4Ad9bE310BAC611BF; - address internal constant cUSD = 0x765DE816845861e75A25fCA122bb6898B8B1282a; + address internal constant FEE_CURRENCY_DIRECTORY = 0x15F344b9E6c3Cb6F0376A36A64928b13F62C6276; + /// @notice A test fee currency deployed only when `deployCeloContracts` is set. + /// Registered with the FeeCurrencyDirectory so that CIP-64 transactions can + /// pay gas in this token. Address is the (now meaningless) Celo mainnet cUSD + /// address kept for deterministic predeploy placement. + address internal constant TEST_FEE_CURRENCY = 0x765DE816845861e75A25fCA122bb6898B8B1282a; /// @notice Returns the name of the predeploy at the given address. function getName(address _addr) internal pure returns (string memory out_) { @@ -28,7 +32,7 @@ library CeloPredeploys { if (_addr == ADDRESS_SORTED_LINKED_LIST_WITH_MEDIAN) return "AddressSortedLinkedListWithMedian"; if (_addr == FEE_CURRENCY) return "FeeCurrency"; if (_addr == FEE_CURRENCY_DIRECTORY) return "FeeCurrencyDirectory"; - if (_addr == cUSD) return "cUSD"; + if (_addr == TEST_FEE_CURRENCY) return "TestFeeCurrency"; revert("Predeploys: unnamed predeploy"); } diff --git a/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol index 961e05cccd1..38d474df00a 100644 --- a/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol @@ -141,7 +141,8 @@ contract DeployOPChain_TestBase is Test, FeatureFlags { operatorFeeScalar: 0, operatorFeeConstant: 0, superchainConfig: superchainConfig, - useCustomGasToken: useCustomGasToken + useCustomGasToken: useCustomGasToken, + deployCeloContracts: false }); } } diff --git a/packages/contracts-bedrock/test/scripts/L2Genesis.t.sol b/packages/contracts-bedrock/test/scripts/L2Genesis.t.sol index 7750eff501e..f1bf5354c5e 100644 --- a/packages/contracts-bedrock/test/scripts/L2Genesis.t.sol +++ b/packages/contracts-bedrock/test/scripts/L2Genesis.t.sol @@ -265,7 +265,8 @@ contract L2Genesis_Run_Test is L2Genesis_TestInit { gasPayingTokenSymbol: "", nativeAssetLiquidityAmount: type(uint248).max, liquidityControllerOwner: address(0x000000000000000000000000000000000000000d), - useL2CM: false + useL2CM: false, + deployCeloContracts: false }); } diff --git a/packages/contracts-bedrock/test/setup/Setup.sol b/packages/contracts-bedrock/test/setup/Setup.sol index b67e7105e33..747edf630d3 100644 --- a/packages/contracts-bedrock/test/setup/Setup.sol +++ b/packages/contracts-bedrock/test/setup/Setup.sol @@ -408,7 +408,8 @@ abstract contract Setup is FeatureFlags { gasPayingTokenSymbol: deploy.cfg().gasPayingTokenSymbol(), nativeAssetLiquidityAmount: deploy.cfg().nativeAssetLiquidityAmount(), liquidityControllerOwner: deploy.cfg().liquidityControllerOwner(), - useL2CM: deploy.cfg().useL2CM() + useL2CM: deploy.cfg().useL2CM(), + deployCeloContracts: deploy.cfg().deployCeloContracts() }) );