Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
55a7624
op-deployer,contracts: Add deployCeloContracts flag to L2Genesis
palango Apr 29, 2026
e3fad4a
op-devstack,op-e2e: Add sysgo.WithCelo() option
palango Apr 29, 2026
e37ba5b
op-deployer,op-e2e,contracts: Land CIP-64 txs on sysgo Celo chains
palango Apr 29, 2026
4b29439
op-acceptance-tests: Add Celo CIP-64 fee-currency test
palango Apr 29, 2026
f1f9c6f
contracts: Initialize SortedOracles in setCeloPredeploys
palango Apr 29, 2026
fc01165
op-devstack: Move Celo miner config out of generic geth.InitL2
palango Apr 29, 2026
a0c19f2
contracts,op-deployer: Make zero-scalar guard conditional on deployCe…
palango Apr 29, 2026
fec5c02
op-core,op-devstack,op-acceptance-tests: Single source of truth for F…
palango Apr 29, 2026
674d72c
op-deployer: Use predeploys constants in TestApplyDeployCeloContracts
palango Apr 29, 2026
fbc2ab1
op-deployer: Rename celoOrDefault to celoOrDefaultScalar for consistency
palango Apr 29, 2026
b137861
contracts: Use vm.prank for single-call pranks in setCeloPredeploys
palango Apr 29, 2026
5eeab6b
op-devstack,op-e2e: Drop unused bool param from WithDeployCeloContracts
palango Apr 29, 2026
df36775
contracts,op-chain-ops: Update remaining L2GenesisInput / DeployOPCha…
palango Apr 29, 2026
5ffb3fb
contracts: Move AddressSortedLinkedListWithMedian library link to fou…
palango Apr 29, 2026
021c193
op-acceptance-tests: Make ensureCeloFeeCurrencyOrSkip actually skip n…
palango Apr 29, 2026
6840eb5
contracts: Move AddressSortedLinkedListWithMedian library link to for…
palango Apr 29, 2026
999df5f
contracts: Update test struct literals for new deployCeloContracts field
palango Apr 29, 2026
ae871db
ci: Add dedicated memory-celo acceptance-test job
palango Apr 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .circleci/continue/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions op-acceptance-tests/acceptance-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions op-acceptance-tests/justfile
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
84 changes: 84 additions & 0 deletions op-acceptance-tests/tests/celo/cip64_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
76 changes: 76 additions & 0 deletions op-acceptance-tests/tests/celo/helpers.go
Original file line number Diff line number Diff line change
@@ -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
}
18 changes: 18 additions & 0 deletions op-acceptance-tests/tests/celo/init_test.go
Original file line number Diff line number Diff line change
@@ -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(),
)),
)
}
2 changes: 2 additions & 0 deletions op-chain-ops/interopgen/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
Expand Down
12 changes: 12 additions & 0 deletions op-core/predeploys/addresses.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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)
)
Expand Down Expand Up @@ -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
}
Expand Down
34 changes: 34 additions & 0 deletions op-deployer/pkg/deployer/integration_test/apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions op-deployer/pkg/deployer/opcm/l2genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ type L2GenesisInput struct {
NativeAssetLiquidityAmount *big.Int
LiquidityControllerOwner common.Address
UseL2CM bool
DeployCeloContracts bool
}

type L2GenesisScript script.DeployScriptWithoutOutput[L2GenesisInput]
Expand Down
2 changes: 2 additions & 0 deletions op-deployer/pkg/deployer/opcm/opchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ type DeployOPChainInput struct {
SuperchainConfig common.Address

UseCustomGasToken bool

DeployCeloContracts bool
}

type DeployOPChainOutput struct {
Expand Down
1 change: 1 addition & 0 deletions op-deployer/pkg/deployer/pipeline/l2genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
14 changes: 12 additions & 2 deletions op-deployer/pkg/deployer/pipeline/opchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions op-deployer/pkg/deployer/state/chain_intent.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
Loading
Loading