Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
0844194
fix!: multiply operatorFeeScalar by 100 instead of dividing by 1e6
fakedev9999 Sep 11, 2025
1c7b2ee
chore: bump to 1.5.0
fakedev9999 Sep 11, 2025
3881631
chore: bump to 2.0.0
fakedev9999 Sep 11, 2025
c104e98
refac: gate at Jovian
fakedev9999 Sep 13, 2025
887699c
missing changes
fakedev9999 Sep 13, 2025
7ea9b9e
update
fakedev9999 Sep 14, 2025
dea4318
reduce diff
fakedev9999 Sep 14, 2025
bf5cabb
snapshot abi + storage layout + remove unncessary change
fakedev9999 Sep 14, 2025
fb6cec8
apply comments
fakedev9999 Sep 18, 2025
dc89d06
make ci green
fakedev9999 Sep 23, 2025
6c247c4
chore: update go.mod
fakedev9999 Sep 25, 2025
d7edb95
fix: activate Jovian in e2e tests
leruaa Sep 25, 2025
cf895c1
feat: add Jovian upgrate transactions
leruaa Sep 25, 2025
827be39
fix: update GPO bytecode
leruaa Sep 25, 2025
6361ca8
use IsOperatorFeeFix
fakedev9999 Sep 27, 2025
c30e0cf
add jovian operator fee acceptance test
fakedev9999 Sep 27, 2025
e8098ad
chore: add commit hash for GPO bytecode
leruaa Oct 1, 2025
edaa994
chore: remove useless replace in go.mod
leruaa Oct 1, 2025
21f1632
acceptance-tests: push common code for operator fee into dsl
geoknee Oct 9, 2025
dd20852
tweak
geoknee Oct 9, 2025
8b3d4a1
refactor IsthmusCostOracle
geoknee Oct 9, 2025
f4f2b9e
fix link
geoknee Oct 10, 2025
165c19b
tag TODO for followup
geoknee Oct 10, 2025
dd390af
update op-geth
geoknee Oct 10, 2025
13821d1
lint
geoknee Oct 10, 2025
e4d4e8f
lint
geoknee Oct 10, 2025
4a85283
revert changes to op-service/txinclude
geoknee Oct 10, 2025
d0d0098
Merge remote-tracking branch 'origin/develop' into taehoon/update-ope…
geoknee Oct 15, 2025
b3299e0
Merge remote-tracking branch 'origin/develop' into taehoon/update-ope…
geoknee Oct 15, 2025
82a7658
regenerate l1block bytecode and resolve nonce clash
geoknee Oct 15, 2025
34045b6
decouple upgrade transactions, change API to getter and fix tests
geoknee Oct 15, 2025
c0a62c0
update op-geth reference
geoknee Oct 15, 2025
bac564f
Update packages/contracts-bedrock/test/L2/GasPriceOracle.t.sol
geoknee Oct 15, 2025
1fbc84c
jovian upgrade transactions: reorder deployer addresses
geoknee Oct 15, 2025
866499f
just update-op-geth 5c6d276814f2cce1d4dda25ed9e5a3a1c52e59a4
geoknee Oct 16, 2025
d35ba6a
explicitly disallow user transactions in jovian activation block
geoknee Oct 16, 2025
82aad4d
assert on receipts in operator fee acceptance test
geoknee Oct 16, 2025
b719641
op-e2e/actions: remove jovianActivationBlock test case
geoknee Oct 16, 2025
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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ require (
lukechampine.com/blake3 v1.3.0 // indirect
)

replace github.com/ethereum/go-ethereum => github.com/ethereum-optimism/op-geth v1.101603.2-0.20251014161648-ded9c88e417b
replace github.com/ethereum/go-ethereum => github.com/ethereum-optimism/op-geth v1.101603.2-0.20251016091451-5c6d276814f2

// replace github.com/ethereum/go-ethereum => ../op-geth

Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -238,8 +238,8 @@ github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A=
github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s=
github.com/ethereum-optimism/go-ethereum-hdwallet v0.1.4-0.20251001155152-4eb15ccedf7e h1:iy1vBIzACYUyOVyoADUwvAiq2eOPC0yVsDUdolPwQjk=
github.com/ethereum-optimism/go-ethereum-hdwallet v0.1.4-0.20251001155152-4eb15ccedf7e/go.mod h1:DYj7+vYJ4cIB7zera9mv4LcAynCL5u4YVfoeUu6Wa+w=
github.com/ethereum-optimism/op-geth v1.101603.2-0.20251014161648-ded9c88e417b h1:5FhpB8a0wXp/11jd6OnUIX3Blwy2Wh9aa+z+AA9amUM=
github.com/ethereum-optimism/op-geth v1.101603.2-0.20251014161648-ded9c88e417b/go.mod h1:Ct2QjqZ2UKgvvgKLLYzoh/DBicJZB8DXsv45DgEjcco=
github.com/ethereum-optimism/op-geth v1.101603.2-0.20251016091451-5c6d276814f2 h1:fvYTR+KOcvSDd/gJuh+ALG/Fx7Y0xU3ZaDkgT/kqVi0=
github.com/ethereum-optimism/op-geth v1.101603.2-0.20251016091451-5c6d276814f2/go.mod h1:Ct2QjqZ2UKgvvgKLLYzoh/DBicJZB8DXsv45DgEjcco=
github.com/ethereum-optimism/superchain-registry/validation v0.0.0-20251009180028-9b4658b9b7af h1:WWz0gJM/boaUImtJnROecPirAerKCLpAU4m6Tx0ArOg=
github.com/ethereum-optimism/superchain-registry/validation v0.0.0-20251009180028-9b4658b9b7af/go.mod h1:NZ816PzLU1TLv1RdAvYAb6KWOj4Zm5aInT0YpDVml2Y=
github.com/ethereum/c-kzg-4844/v2 v2.1.0 h1:gQropX9YFBhl3g4HYhwE70zq3IHFRgbbNPw0Shwzf5w=
Expand Down
Original file line number Diff line number Diff line change
@@ -1,65 +1,18 @@
package operatorfee

import (
"math/big"
"testing"

"github.com/ethereum-optimism/optimism/op-devstack/devtest"
"github.com/ethereum-optimism/optimism/op-devstack/dsl"
"github.com/ethereum-optimism/optimism/op-devstack/presets"
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-service/eth"
)

func TestOperatorFee(gt *testing.T) {
t := devtest.SerialT(gt)
sys := presets.NewMinimal(t)
require := t.Require()

err := dsl.RequiresL2Fork(t.Ctx(), sys, 0, rollup.Isthmus)
require.NoError(err, "Isthmus fork must be active for this test")

fundAmount := eth.OneTenthEther
alice := sys.FunderL2.NewFundedEOA(fundAmount)

alice.WaitForBalance(fundAmount)
bob := sys.Wallet.NewEOA(sys.L2EL)

operatorFee := dsl.NewOperatorFee(t, sys.L2Chain, sys.L1EL)

operatorFee.CheckCompatibility()
systemOwner := operatorFee.GetSystemOwner()
sys.FunderL1.FundAtLeast(systemOwner, fundAmount)

// First, ensure L2 is synced with current L1 state before starting tests
t.Log("Ensuring L2 is synced with current L1 state...")
operatorFee.WaitForL2SyncWithCurrentL1State()

testCases := []struct {
name string
scalar uint32
constant uint64
}{
{"ZeroFees", 0, 0},
{"NonZeroFees", 300, 400},
}

for _, tc := range testCases {
t.Run(tc.name, func(t devtest.T) {
operatorFee.SetOperatorFee(tc.scalar, tc.constant)
operatorFee.WaitForL2Sync(tc.scalar, tc.constant)
operatorFee.VerifyL2Config(tc.scalar, tc.constant)

result := operatorFee.ValidateTransactionFees(alice, bob, big.NewInt(1000), tc.scalar, tc.constant)

t.Log("Test completed successfully:",
"testCase", tc.name,
"gasUsed", result.TransactionReceipt.GasUsed,
"actualTotalFee", result.ActualTotalFee.String(),
"expectedOperatorFee", result.ExpectedOperatorFee.String(),
"vaultBalanceIncrease", result.VaultBalanceIncrease.String())
})
}

operatorFee.RestoreOriginalConfig()
t.Require().NoError(err, "Isthmus fork must be active for this test")
dsl.RunOperatorFeeTest(t, sys.L2Chain, sys.L1EL, sys.FunderL1, sys.FunderL2)
}
18 changes: 18 additions & 0 deletions op-acceptance-tests/tests/jovian/operator_fee_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package jovian

import (
"testing"

"github.com/ethereum-optimism/optimism/op-devstack/devtest"
"github.com/ethereum-optimism/optimism/op-devstack/dsl"
"github.com/ethereum-optimism/optimism/op-devstack/presets"
"github.com/ethereum-optimism/optimism/op-node/rollup"
)

func TestOperatorFee(gt *testing.T) {
t := devtest.SerialT(gt)
sys := presets.NewMinimal(t)
err := dsl.RequiresL2Fork(t.Ctx(), sys, 0, rollup.Jovian)
t.Require().NoError(err, "Jovian fork must be active for this test")
dsl.RunOperatorFeeTest(t, sys.L2Chain, sys.L1EL, sys.FunderL1, sys.FunderL2)
}
55 changes: 45 additions & 10 deletions op-devstack/dsl/bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,11 +149,15 @@ func (b *StandardBridge) UsesSuperRoots() bool {
}

type Deposit struct {
bridge *StandardBridge
l1Receipt *types.Receipt
}

func (d Deposit) GasCost() eth.ETH {
return gasCost(d.l1Receipt)
if d.bridge == nil {
panic("bridge reference not set on deposit")
}
return d.bridge.gasCost(d.l1Receipt, d.bridge.l1Client)
}

func (b *StandardBridge) Deposit(amount eth.ETH, from *EOA) Deposit {
Expand All @@ -175,6 +179,7 @@ func (b *StandardBridge) Deposit(amount eth.ETH, from *EOA) Deposit {
}, 60*time.Second, 500*time.Millisecond, "L2 Deposit never found")
b.require.Equal(types.ReceiptStatusSuccessful, l2DepositReceipt.Status)
return Deposit{
bridge: b,
l1Receipt: l1DepositReceipt,
}
}
Expand Down Expand Up @@ -220,6 +225,7 @@ func (b *StandardBridge) ERC20Deposit(l1TokenAddr common.Address, l2TokenAddr co
b.require.Equal(types.ReceiptStatusSuccessful, l2DepositReceipt.Status, "L2 ERC20 deposit should succeed")

return &Deposit{
bridge: b,
l1Receipt: depositReceipt,
}
}
Expand Down Expand Up @@ -345,17 +351,17 @@ type Withdrawal struct {
}

func (w *Withdrawal) InitiateGasCost() eth.ETH {
return gasCost(w.initReceipt)
return w.bridge.gasCost(w.initReceipt, w.bridge.l2Client)
}

func (w *Withdrawal) ProveGasCost() eth.ETH {
w.require.NotNil(w.proveReceipt, "Must have proven withdrawal before calculating gas cost")
return gasCost(w.proveReceipt)
return w.bridge.gasCost(w.proveReceipt, w.bridge.l1Client)
}

func (w *Withdrawal) FinalizeGasCost() eth.ETH {
w.require.NotNil(w.finalizeReceipt, "Must have finalized withdrawal before calculating gas cost")
return gasCost(w.finalizeReceipt)
return w.bridge.gasCost(w.finalizeReceipt, w.bridge.l1Client)
}

func (w *Withdrawal) InitiateBlockHash() common.Hash {
Expand Down Expand Up @@ -563,18 +569,47 @@ func (w *Withdrawal) WaitForDisputeGameResolved() {
}, 60*time.Second, 100*time.Millisecond, "wait for dispute game resolved")
}

func gasCost(rcpt *types.Receipt) eth.ETH {
func (b *StandardBridge) gasCost(rcpt *types.Receipt, client apis.EthClient) eth.ETH {
var blockTimestamp *uint64
if hasOperatorFee(rcpt) {
b.require.NotNil(client, "client is required to resolve operator fee timestamp")
blockTimestamp = b.receiptTimestamp(rcpt, client)
}
return gasCost(rcpt, b.rollupCfg, blockTimestamp)
}

func hasOperatorFee(rcpt *types.Receipt) bool {
return rcpt.OperatorFeeConstant != nil && rcpt.OperatorFeeScalar != nil
}

func (b *StandardBridge) receiptTimestamp(rcpt *types.Receipt, client apis.EthClient) *uint64 {
b.require.NotNil(rcpt.BlockNumber, "receipt missing block number")
blockInfo, err := client.InfoByNumber(b.ctx, rcpt.BlockNumber.Uint64())
b.require.NoError(err, "failed to fetch block info for receipt")
ts := blockInfo.Time()
return &ts
}

func gasCost(rcpt *types.Receipt, rollupCfg *rollup.Config, blockTimestamp *uint64) eth.ETH {
cost := eth.WeiBig(new(big.Int).Mul(new(big.Int).SetUint64(rcpt.GasUsed), rcpt.EffectiveGasPrice))
if rcpt.L1Fee != nil {
cost = cost.Add(eth.WeiBig(rcpt.L1Fee))
}
if rcpt.OperatorFeeConstant != nil && rcpt.OperatorFeeScalar != nil {
// https://github.com/ethereum-optimism/op-geth/blob/6005dd53e1b50fe5a3f59764e3e2056a639eff2f/core/types/rollup_cost.go#L244-L247
// Also see: https://specs.optimism.io/protocol/isthmus/exec-engine.html#operator-operatorCost
if hasOperatorFee(rcpt) {
if rollupCfg == nil {
panic("rollup config is required to compute operator fee")
}
if blockTimestamp == nil {
panic("block timestamp is required to compute operator fee")
}
operatorCost := new(big.Int).SetUint64(rcpt.GasUsed)
operatorCost.Mul(operatorCost, new(big.Int).SetUint64(*rcpt.OperatorFeeScalar))
operatorCost = operatorCost.Div(operatorCost, big.NewInt(1_000_000))
operatorCost = operatorCost.Add(operatorCost, new(big.Int).SetUint64(*rcpt.OperatorFeeConstant))
if rollupCfg.IsOperatorFeeFix(*blockTimestamp) {
operatorCost.Mul(operatorCost, big.NewInt(100))
} else {
operatorCost.Div(operatorCost, big.NewInt(1_000_000))
}
operatorCost.Add(operatorCost, new(big.Int).SetUint64(*rcpt.OperatorFeeConstant))
cost = cost.Add(eth.WeiBig(operatorCost))
}
return cost
Expand Down
4 changes: 4 additions & 0 deletions op-devstack/dsl/l2_network.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,10 @@ func (n *L2Network) IsActivated(timestamp uint64) bool {
return head.Number >= blockNum
}

func (n *L2Network) IsForkActive(forkName rollup.ForkName, timestamp uint64) bool {
return n.Escape().RollupConfig().IsForkActive(forkName, timestamp)
}

// LatestBlockBeforeTimestamp finds the latest block before fork activation
func (n *L2Network) LatestBlockBeforeTimestamp(t devtest.T, timestamp uint64) eth.BlockRef {
require := t.Require()
Expand Down
86 changes: 81 additions & 5 deletions op-devstack/dsl/operator_fee.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/ethereum-optimism/optimism/op-chain-ops/devkeys"
"github.com/ethereum-optimism/optimism/op-devstack/devtest"
"github.com/ethereum-optimism/optimism/op-devstack/stack/match"
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum-optimism/optimism/op-service/predeploys"
"github.com/ethereum-optimism/optimism/op-service/txintent/bindings"
Expand All @@ -17,10 +18,11 @@ import (
type OperatorFee struct {
commonImpl

l1Client *L1ELNode
l2Network *L2Network
systemConfig bindings.SystemConfig
l1Block bindings.L1Block
l1Client *L1ELNode
l2Network *L2Network
systemConfig bindings.SystemConfig
l1Block bindings.L1Block
gasPriceOracle bindings.GasPriceOracle

originalScalar uint32
originalConstant uint64
Expand All @@ -44,6 +46,11 @@ func NewOperatorFee(t devtest.T, l2Network *L2Network, l1EL *L1ELNode) *Operator
bindings.WithTo(predeploys.L1BlockAddr),
bindings.WithTest(t))

gasPriceOracle := bindings.NewBindings[bindings.GasPriceOracle](
bindings.WithClient(l2Network.inner.L2ELNode(match.FirstL2EL).EthClient()),
bindings.WithTo(predeploys.GasPriceOracleAddr),
bindings.WithTest(t))

originalScalar, err := contractio.Read(systemConfig.OperatorFeeScalar(), t.Ctx())
t.Require().NoError(err)
originalConstant, err := contractio.Read(systemConfig.OperatorFeeConstant(), t.Ctx())
Expand All @@ -55,6 +62,7 @@ func NewOperatorFee(t devtest.T, l2Network *L2Network, l1EL *L1ELNode) *Operator
l2Network: l2Network,
systemConfig: systemConfig,
l1Block: l1Block,
gasPriceOracle: gasPriceOracle,
originalScalar: originalScalar,
originalConstant: originalConstant,
}
Expand Down Expand Up @@ -131,6 +139,11 @@ func (of *OperatorFee) ValidateTransactionFees(from *EOA, to *EOA, amount *big.I
of.require.NoError(err)
of.require.Equal(types.ReceiptStatusSuccessful, receipt.Status)

blockHash := receipt.BlockHash
blockRef, err := from.el.stackEL().EthClient().BlockRefByHash(of.ctx, blockHash)
of.require.NoError(err)
isJovian := of.l2Network.IsForkActive(rollup.Jovian, blockRef.Time)

vaultAfter, err := from.el.stackEL().EthClient().BalanceAt(of.ctx, predeploys.OperatorFeeVaultAddr, nil)
of.require.NoError(err)

Expand All @@ -140,8 +153,19 @@ func (of *OperatorFee) ValidateTransactionFees(from *EOA, to *EOA, amount *big.I
if expectedScalar == 0 && expectedConstant == 0 {
expectedOperatorFee = big.NewInt(0)
} else {
isJovianinGPO, err := contractio.Read(of.gasPriceOracle.IsJovian(), of.ctx)
of.require.NoError(err)

operatorFee := new(big.Int).Mul(big.NewInt(int64(receipt.GasUsed)), big.NewInt(int64(expectedScalar)))
operatorFee.Div(operatorFee, big.NewInt(1000000))
if isJovian {
of.require.Equal(isJovianinGPO, true)
// Jovian formula: (gasUsed * operatorFeeScalar * 100) + operatorFeeConstant
operatorFee.Mul(operatorFee, big.NewInt(100))
} else {
of.require.Equal(isJovianinGPO, false)
// Isthmus formula: (gasUsed * operatorFeeScalar / 1e6) + operatorFeeConstant
operatorFee.Div(operatorFee, big.NewInt(1000000))
}
operatorFee.Add(operatorFee, big.NewInt(int64(expectedConstant)))
expectedOperatorFee = operatorFee
}
Expand All @@ -156,6 +180,14 @@ func (of *OperatorFee) ValidateTransactionFees(from *EOA, to *EOA, amount *big.I
actualTotalFee.Add(actualTotalFee, receipt.L1Fee)
}

if expectedScalar != 0 || expectedConstant != 0 {
of.require.NotNil(receipt.OperatorFeeScalar)
of.require.NotNil(receipt.OperatorFeeConstant)

of.require.Equal(expectedScalar, uint32(*receipt.OperatorFeeScalar))
of.require.Equal(expectedConstant, *receipt.OperatorFeeConstant)
}

return OperatorFeeValidationResult{
TransactionReceipt: receipt,
ExpectedOperatorFee: expectedOperatorFee,
Expand All @@ -167,3 +199,47 @@ func (of *OperatorFee) ValidateTransactionFees(from *EOA, to *EOA, amount *big.I
func (of *OperatorFee) RestoreOriginalConfig() {
of.SetOperatorFee(of.originalScalar, of.originalConstant)
}

func RunOperatorFeeTest(t devtest.T, l2Chain *L2Network, l1EL *L1ELNode, funderL1, funderL2 *Funder) {
fundAmount := eth.OneTenthEther
alice := funderL2.NewFundedEOA(fundAmount)
alice.WaitForBalance(fundAmount)
bob := funderL2.NewFundedEOA(eth.ZeroWei)

operatorFee := NewOperatorFee(t, l2Chain, l1EL)
operatorFee.CheckCompatibility()
systemOwner := operatorFee.GetSystemOwner()
funderL1.FundAtLeast(systemOwner, fundAmount)

// First, ensure L2 is synced with current L1 state before starting tests
t.Log("Ensuring L2 is synced with current L1 state...")
operatorFee.WaitForL2SyncWithCurrentL1State()

testCases := []struct {
name string
scalar uint32
constant uint64
}{
{"ZeroFees", 0, 0},
{"NonZeroFees", 300, 400},
}

for _, tc := range testCases {
t.Run(tc.name, func(t devtest.T) {
operatorFee.SetOperatorFee(tc.scalar, tc.constant)
operatorFee.WaitForL2Sync(tc.scalar, tc.constant)
operatorFee.VerifyL2Config(tc.scalar, tc.constant)

result := operatorFee.ValidateTransactionFees(alice, bob, big.NewInt(1000), tc.scalar, tc.constant)

t.Log("Test completed successfully:",
"testCase", tc.name,
"gasUsed", result.TransactionReceipt.GasUsed,
"actualTotalFee", result.ActualTotalFee.String(),
"expectedOperatorFee", result.ExpectedOperatorFee.String(),
"vaultBalanceIncrease", result.VaultBalanceIncrease.String())
})
}

operatorFee.RestoreOriginalConfig()
}
Loading