Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
6ce1d31
add erc20FeeTx
curryxbo Oct 13, 2025
872680b
fix ERC20FeeTxType
curryxbo Oct 13, 2025
eeb0611
Generate new type tx (#204)
curryxbo Oct 13, 2025
f2c680f
Clean msg (#206)
curryxbo Oct 15, 2025
b68a8bf
add token gas get and transfer func
Kukoomomo Oct 15, 2025
8b6dc3d
add erc20 token gas transfer and balance check
Kukoomomo Oct 16, 2025
9242114
addd erc20 balance slot operate
Kukoomomo Oct 20, 2025
163825d
Merge pull request #213 from morph-l2/gas_erc20
Kukoomomo Oct 20, 2025
d045bc5
Merge branch 'main' into feature/erc20-fee
curryxbo Oct 23, 2025
d5625f0
Clean l1 fee (#210)
curryxbo Oct 30, 2025
4415b48
Rollback rate (#219)
curryxbo Oct 31, 2025
43e4446
update contract state slot and fix some bug
Kukoomomo Nov 3, 2025
dada931
update
Kukoomomo Nov 3, 2025
895b64d
Merge pull request #223 from morph-l2/gas_contract
Kukoomomo Nov 3, 2025
5769551
Fix and clean (#225)
curryxbo Nov 4, 2025
eb4c252
Optimize (#227)
curryxbo Nov 6, 2025
5f1f513
Storage change tracer (#226)
Kukoomomo Nov 6, 2025
d9597a0
Fix token id (#228)
curryxbo Nov 6, 2025
e3cae2b
Merge remote-tracking branch 'origin/main' into token_fee_main
Kukoomomo Nov 6, 2025
e9b6088
Merge pull request #230 from morph-l2/token_fee_main
Kukoomomo Nov 6, 2025
472d7a0
Add check active (#231)
curryxbo Nov 6, 2025
e30824d
Fix tokenlimit and transfer by evm (#232)
curryxbo Nov 7, 2025
8907321
Fix token limit (#233)
curryxbo Nov 7, 2025
d04e496
Fix receipt (#234)
curryxbo Nov 7, 2025
321f89e
fix and clean (#235)
curryxbo Nov 10, 2025
080fb68
add balance check for evm transfer
Kukoomomo Nov 10, 2025
868b12e
fix some bug
Kukoomomo Nov 10, 2025
43a36b0
Merge remote-tracking branch 'origin/feature/erc20-fee' into balance_…
Kukoomomo Nov 10, 2025
9991d26
Merge pull request #236 from morph-l2/balance_check
Kukoomomo Nov 10, 2025
82206cd
Fix EstimateGas and txPool (#239)
curryxbo Nov 10, 2025
456073d
Fix type of token id (#240)
curryxbo Nov 12, 2025
ed34e3c
Merge pull request #241 from morph-l2/rename_alt
Kukoomomo Nov 12, 2025
b0eb667
Fix func name (#242)
curryxbo Nov 13, 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,4 @@ profile.cov

**/yarn-error.log
/build/db*/
local-test
2 changes: 2 additions & 0 deletions accounts/abi/bind/backends/simulated.go
Original file line number Diff line number Diff line change
Expand Up @@ -918,6 +918,8 @@ func (m callMsg) IsL1MessageTx() bool { return false }
func (m callMsg) SetCodeAuthorizations() []types.SetCodeAuthorization {
return m.CallMsg.AuthorizationList
}
func (m callMsg) FeeTokenID() uint16 { return m.CallMsg.FeeTokenID }
func (m callMsg) FeeLimit() *big.Int { return m.CallMsg.FeeLimit }

// filterBackend implements filters.Backend to support filtering for logs without
// taking bloom-bits acceleration structures into account.
Expand Down
67 changes: 66 additions & 1 deletion accounts/abi/bind/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ type TransactOpts struct {
GasTipCap *big.Int // Gas priority fee cap to use for the 1559 transaction execution (nil = gas price oracle)
GasLimit uint64 // Gas limit to set for the transaction execution (0 = estimate)

FeeTokenID uint16 // alt fee token id of transaction execution
FeeLimit *big.Int // alt fee token limit of transaction execution

Context context.Context // Network context to support cancellation and timeouts (nil = no timeout)

NoSend bool // Do all transact steps but do not send the transaction
Expand Down Expand Up @@ -287,6 +290,64 @@ func (c *BoundContract) createDynamicTx(opts *TransactOpts, contract *common.Add
return types.NewTx(baseTx), nil
}

func (c *BoundContract) createAltFeeTx(opts *TransactOpts, contract *common.Address, input []byte, head *types.Header) (*types.Transaction, error) {
// Normalize value
value := opts.Value
if value == nil {
value = new(big.Int)
}
// Estimate TipCap
gasTipCap := opts.GasTipCap
if gasTipCap == nil {
tip, err := c.transactor.SuggestGasTipCap(ensureContext(opts.Context))
if err != nil {
return nil, err
}
gasTipCap = tip
}
// Estimate FeeCap
gasFeeCap := opts.GasFeeCap
if gasFeeCap == nil {
if head.BaseFee != nil {
gasFeeCap = new(big.Int).Add(
gasTipCap,
new(big.Int).Mul(head.BaseFee, big.NewInt(2)),
)
} else {
gasFeeCap = new(big.Int).Set(gasTipCap)
}
}
if gasFeeCap.Cmp(gasTipCap) < 0 {
return nil, fmt.Errorf("maxFeePerGas (%v) < maxPriorityFeePerGas (%v)", gasFeeCap, gasTipCap)
}
// Estimate GasLimit
gasLimit := opts.GasLimit
if opts.GasLimit == 0 {
var err error
gasLimit, err = c.estimateGasLimit(opts, contract, input, nil, gasTipCap, gasFeeCap, value)
if err != nil {
return nil, err
}
}
// create the transaction
nonce, err := c.getNonce(opts)
if err != nil {
return nil, err
}
baseTx := &types.AltFeeTx{
To: contract,
Nonce: nonce,
GasFeeCap: gasFeeCap,
GasTipCap: gasTipCap,
FeeTokenID: opts.FeeTokenID,
FeeLimit: opts.FeeLimit,
Gas: gasLimit,
Value: value,
Data: input,
}
Comment on lines +337 to +347
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate FeeLimit before creating the transaction.

FeeLimit is assigned directly from opts.FeeLimit without checking if it's nil or non-positive. Since this represents the maximum ERC20 tokens a user is willing to spend on fees, a missing or invalid value could cause transaction failures or unexpected behavior downstream.

Apply this diff to add validation:

 	// create the transaction
 	nonce, err := c.getNonce(opts)
 	if err != nil {
 		return nil, err
 	}
+	if opts.FeeLimit == nil || opts.FeeLimit.Sign() <= 0 {
+		return nil, fmt.Errorf("feeLimit must be positive when using alt fee token")
+	}
 	baseTx := &types.AltFeeTx{
 		To:         contract,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
baseTx := &types.AltFeeTx{
To: contract,
Nonce: nonce,
GasFeeCap: gasFeeCap,
GasTipCap: gasTipCap,
FeeTokenID: opts.FeeTokenID,
FeeLimit: opts.FeeLimit,
Gas: gasLimit,
Value: value,
Data: input,
}
if opts.FeeLimit == nil || opts.FeeLimit.Sign() <= 0 {
return nil, fmt.Errorf("feeLimit must be positive when using alt fee token")
}
baseTx := &types.AltFeeTx{
To: contract,
Nonce: nonce,
GasFeeCap: gasFeeCap,
GasTipCap: gasTipCap,
FeeTokenID: opts.FeeTokenID,
FeeLimit: opts.FeeLimit,
Gas: gasLimit,
Value: value,
Data: input,
}
🤖 Prompt for AI Agents
In accounts/abi/bind/base.go around lines 337 to 347, FeeLimit is taken directly
from opts.FeeLimit without validation; before constructing baseTx check that
opts.FeeLimit is not nil and its value is positive (e.g., > 0); if it is nil or
non-positive return a clear error (or set a sensible default only if allowed by
calling conventions), and only then populate FeeLimit on the AltFeeTx so invalid
fee limits cannot be used.

return types.NewTx(baseTx), nil
}

func (c *BoundContract) createLegacyTx(opts *TransactOpts, contract *common.Address, input []byte) (*types.Transaction, error) {
if opts.GasFeeCap != nil || opts.GasTipCap != nil {
return nil, errors.New("maxFeePerGas or maxPriorityFeePerGas specified but curie is not active yet")
Expand Down Expand Up @@ -377,7 +438,11 @@ func (c *BoundContract) transact(opts *TransactOpts, contract *common.Address, i
if head, errHead := c.transactor.HeaderByNumber(ensureContext(opts.Context), nil); errHead != nil {
return nil, errHead
} else if head.BaseFee != nil {
rawTx, err = c.createDynamicTx(opts, contract, input, head)
if opts.FeeTokenID != 0 {
rawTx, err = c.createAltFeeTx(opts, contract, input, head)
} else {
rawTx, err = c.createDynamicTx(opts, contract, input, head)
}
Comment on lines +441 to +445
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Validate consistency between FeeTokenID and FeeLimit.

The routing logic only checks FeeTokenID != 0, but doesn't ensure FeeLimit is also set. Conversely, if a caller sets FeeLimit but leaves FeeTokenID at zero, the limit will be silently ignored. Validate that both fields are set together or both are unset to catch API misuse early.

Apply this diff to add consistency validation earlier in the function:

 func (c *BoundContract) transact(opts *TransactOpts, contract *common.Address, input []byte) (*types.Transaction, error) {
 	if opts.GasPrice != nil && (opts.GasFeeCap != nil || opts.GasTipCap != nil) {
 		return nil, errors.New("both gasPrice and (maxFeePerGas or maxPriorityFeePerGas) specified")
 	}
+	// Validate alt fee fields are consistent
+	if (opts.FeeTokenID != 0) != (opts.FeeLimit != nil && opts.FeeLimit.Sign() > 0) {
+		return nil, errors.New("feeTokenID and feeLimit must both be set or both be unset")
+	}
 	// Create the transaction

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In accounts/abi/bind/base.go around lines 441 to 445, the routing currently
checks only FeeTokenID to choose createAltFeeTx vs createDynamicTx but doesn't
enforce consistency with FeeLimit; add an early validation that either both
opts.FeeTokenID != 0 and opts.FeeLimit != 0 (or >0 as appropriate) are set, or
both are zero/unset, and return a clear error if they are inconsistent so
callers cannot set one without the other; perform this check before the routing
and only then call createAltFeeTx or createDynamicTx.

} else {
// Chain is not London ready -> use legacy transaction
rawTx, err = c.createLegacyTx(opts, contract, input)
Expand Down
6 changes: 6 additions & 0 deletions accounts/external/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,12 @@ func (api *ExternalSigner) SignTx(account accounts.Account, tx *types.Transactio
case types.DynamicFeeTxType, types.SetCodeTxType:
args.MaxFeePerGas = (*hexutil.Big)(tx.GasFeeCap())
args.MaxPriorityFeePerGas = (*hexutil.Big)(tx.GasTipCap())
case types.AltFeeTxType:
args.MaxFeePerGas = (*hexutil.Big)(tx.GasFeeCap())
args.MaxPriorityFeePerGas = (*hexutil.Big)(tx.GasTipCap())
feeTokenID := hexutil.Uint64(tx.FeeTokenID())
args.FeeTokenID = &feeTokenID
args.FeeLimit = (*hexutil.Big)(tx.FeeLimit())
default:
return nil, fmt.Errorf("unsupported tx type %d", tx.Type())
}
Expand Down
7 changes: 7 additions & 0 deletions cmd/evm/internal/t8ntool/execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,13 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig,
receipt.TxHash = tx.Hash()
receipt.GasUsed = msgResult.UsedGas
receipt.L1Fee = msgResult.L1DataFee
if msg.FeeTokenID() != 0 {
tokenID := msg.FeeTokenID()
receipt.FeeTokenID = &tokenID
receipt.FeeLimit = msg.FeeLimit()
receipt.FeeRate = msgResult.FeeRate
receipt.TokenScale = msgResult.TokenScale
}

// If the transaction created a contract, store the creation address in the receipt.
if msg.To() == nil {
Expand Down
5 changes: 5 additions & 0 deletions core/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ var (
// is higher than the balance of the user's account.
ErrInsufficientFunds = errors.New("insufficient funds for gas * price + value")

// ErrInsufficientValue is returned tx value
ErrInsufficientValue = errors.New("insufficient funds for value")
Comment thread
curryxbo marked this conversation as resolved.

ErrInsufficientGasFee = errors.New("insufficient funds for gas * price + l1Fee")
Comment thread
curryxbo marked this conversation as resolved.

// ErrGasUintOverflow is returned when calculating gas usage.
ErrGasUintOverflow = errors.New("gas uint64 overflow")

Expand Down
8 changes: 8 additions & 0 deletions core/state_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,14 @@ func ApplyTransactionWithEVM(msg Message, config *params.ChainConfig, gp *GasPoo
receipt.BlockNumber = blockNumber
receipt.TransactionIndex = uint(statedb.TxIndex())
receipt.L1Fee = result.L1DataFee
if tx.IsAltFeeTx() {
tokenID := tx.FeeTokenID()
receipt.FeeTokenID = &tokenID
receipt.FeeLimit = tx.FeeLimit()
receipt.FeeRate = result.FeeRate
receipt.TokenScale = result.TokenScale
}

return receipt, err
}

Expand Down
157 changes: 143 additions & 14 deletions core/state_transition.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
package core

import (
"bytes"
"errors"
"fmt"
"math"
math "math"
"math/big"
"time"

Expand All @@ -31,6 +33,7 @@ import (
"github.com/morph-l2/go-ethereum/log"
"github.com/morph-l2/go-ethereum/metrics"
"github.com/morph-l2/go-ethereum/params"
"github.com/morph-l2/go-ethereum/rollup/fees"
)

var emptyKeccakCodeHash = codehash.EmptyKeccakCodeHash
Expand Down Expand Up @@ -73,6 +76,9 @@ type StateTransition struct {
evm *vm.EVM

l1DataFee *big.Int

feeRate *big.Int
tokenScale *big.Int
}

// Message represents a message sent to a contract.
Expand All @@ -92,12 +98,16 @@ type Message interface {
AccessList() types.AccessList
IsL1MessageTx() bool
SetCodeAuthorizations() []types.SetCodeAuthorization
FeeTokenID() uint16
FeeLimit() *big.Int
}

// ExecutionResult includes all output after executing given evm
// message no matter the execution itself is successful or not.
type ExecutionResult struct {
L1DataFee *big.Int
FeeRate *big.Int
TokenScale *big.Int
UsedGas uint64 // Total used gas but include the refunded gas
Err error // Any error encountered during the execution(listed in core/vm/errors.go)
ReturnData []byte // Returned data from evm(function result or data supplied with revert opcode)
Expand Down Expand Up @@ -219,7 +229,6 @@ func ApplyMessage(evm *vm.EVM, msg Message, gp *GasPool, l1DataFee *big.Int) (*E
defer func(t time.Time) {
stateTransitionApplyMessageTimer.Update(time.Since(t))
}(time.Now())

return NewStateTransition(evm, msg, gp, l1DataFee).TransitionDb()
}

Expand Down Expand Up @@ -275,6 +284,76 @@ func (st *StateTransition) buyGas() error {
return nil
}

// buyAltTokenGas handles gas payment using alternative tokens (ERC20, etc.)
func (st *StateTransition) buyAltTokenGas() error {
// Calculate the total ETH fee needed
mgval := new(big.Int).SetUint64(st.msg.Gas())
mgval = mgval.Mul(mgval, st.gasPrice)

if !st.evm.ChainConfig().Morph.FeeVaultEnabled() {
return errors.New("tx need fee vault enable")
}

log.Debug("Adding L1DataFee for alt token gas payment", "l1DataFee", st.l1DataFee)
mgval = mgval.Add(mgval, st.l1DataFee)

Comment thread
coderabbitai[bot] marked this conversation as resolved.
// Check value
if have, want := st.state.GetBalance(st.msg.From()), st.value; have.Cmp(want) < 0 {
return fmt.Errorf("%w: address %v have %v want %v", ErrInsufficientValue, st.msg.From().Hex(), have.String(), want.String())
}
// Check alt token balance
tokenInfo, tokenBalance, err := st.GetAltTokenBalanceHybrid(st.msg.FeeTokenID(), st.msg.From())
if err != nil {
return fmt.Errorf("failed to get alt token balance: %v", err)
}
log.Debug("Alt token gas payment calculation",
"tokenID", st.msg.FeeTokenID(),
"tokenAddress", tokenInfo.TokenAddress.Hex(),
"fee", mgval,
)

tokenFee := types.EthToAlt(mgval, st.feeRate, st.tokenScale)
feeLimit := tokenBalance
if st.msg.FeeLimit() != nil && st.msg.FeeLimit().Sign() > 0 {
feeLimit = cmath.BigMin(tokenBalance, st.msg.FeeLimit())
}
if feeLimit.Cmp(tokenFee) < 0 {
return fmt.Errorf("%w: address %v has insufficient token balance or fee limit, have %v need %v (token %s)",
ErrInsufficientGasFee, st.msg.From().Hex(), feeLimit, mgval, tokenInfo.TokenAddress.Hex())
}

// Check gas pool
if err := st.gp.SubGas(st.msg.Gas()); err != nil {
return err
}

// Transfer alt tokens from user to fee vault
feeVaultAddress := st.evm.ChainConfig().Morph.FeeVaultAddress
if feeVaultAddress == nil || bytes.Equal(feeVaultAddress.Bytes(), common.Address{}.Bytes()) {
return fmt.Errorf("fee vault address is not configured")
}
if err := st.TransferAltTokenHybrid(tokenInfo.TokenAddress, st.msg.From(), *feeVaultAddress, tokenFee, tokenInfo.BalanceSlot, tokenBalance); err != nil {
return fmt.Errorf("failed to transfer alt tokens for gas payment: %v", err)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if st.evm.Config.Tracer != nil && st.evm.Config.Tracer.OnGasChange != nil {
st.evm.Config.Tracer.OnGasChange(0, st.msg.Gas(), tracing.GasChangeTxInitialBalance)
}

st.gas += st.msg.Gas()
st.initialGas = st.msg.Gas()

log.Debug("Alt token gas payment successful",
"tokenID", st.msg.FeeTokenID(),
"tokenAddress", tokenInfo.TokenAddress.String(),
"from", st.msg.From().String(),
"amount", mgval,
"tokenAmount", tokenFee,
"feeVault", feeVaultAddress.String())

return nil
}
Comment on lines +287 to +355
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify gas pool rollback on transfer failure.

Based on past review comments, a rollback mechanism was added to restore the gas pool if TransferAltTokenHybrid fails after SubGas (line 326). However, the current code doesn't show this defer/rollback pattern in buyAltTokenGas.

Please confirm the rollback exists (possibly in a different commit or outside the annotated range), or add it:

If no rollback exists, the gas pool will be permanently reduced if the transfer fails, which can stall subsequent transactions in the block.


🏁 Script executed:

#!/bin/bash
# Check if buyAltTokenGas has a defer or rollback mechanism for gas pool restoration
rg -A 30 "func.*buyAltTokenGas" core/state_transition.go | rg -C5 "defer|rollback|AddGas"

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Get full implementation of buyAltTokenGas
cat core/state_transition.go | sed -n '/func.*buyAltTokenGas/,/^func /p' | head -100

Length of output: 2580


🏁 Script executed:

#!/bin/bash
# Search for AddGas usage patterns in the codebase
rg -n "AddGas" core/state_transition.go -B3 -A3

Length of output: 186


🏁 Script executed:

#!/bin/bash
# Check for deferred cleanup patterns with gas pool in state_transition.go
rg -n "defer.*gp\.|gp\.\w+.*defer" core/state_transition.go -B2 -A2

Length of output: 0


Add defer statement to restore gas pool on transfer failure.

The gas pool rollback mechanism mentioned in past review comments is not present in buyAltTokenGas(). If TransferAltTokenHybrid() fails after st.gp.SubGas() is called (line 326), the gas is permanently lost from the pool, leaving it in an inconsistent state.

Add a defer block immediately after SubGas() to restore gas on error:

// Check gas pool
if err := st.gp.SubGas(st.msg.Gas()); err != nil {
    return err
}
defer func() {
    if err != nil {
        st.gp.AddGas(st.msg.Gas())
    }
}()

This ensures the gas pool is restored if any subsequent operation fails.

🤖 Prompt for AI Agents
In core/state_transition.go around lines 287–355 (buyAltTokenGas), after calling
st.gp.SubGas(st.msg.Gas()) you must add a defer that restores the gas on any
subsequent error to avoid permanently losing gas; add the defer immediately
after the SubGas call and make it conditional on the eventual error (i.e., only
call st.gp.AddGas on failure). To implement this safely either convert the
function to use a named return variable (err error) so the defer can inspect
that err, or introduce a local variable (e.g. opErr) that you set on each error
return and check in the defer; ensure the defer does not AddGas on success and
that all subsequent returns set the error variable so the rollback runs
correctly.


func (st *StateTransition) preCheck() error {
if st.msg.IsL1MessageTx() {
// No fee fields to check, no nonce to check, and no need to check if EOA (L1 already verified it for us)
Expand Down Expand Up @@ -343,6 +422,26 @@ func (st *StateTransition) preCheck() error {
return fmt.Errorf("%w (sender %v)", ErrEmptyAuthList, st.msg.From())
}
}

if st.msg.FeeTokenID() != 0 {
active, err := fees.IsTokenActive(st.state, st.msg.FeeTokenID())
if err != nil {
return fmt.Errorf("get token status failed %v", err)
}
if !active {
return fmt.Errorf("token %v not active", st.msg.FeeTokenID())
}
feeRate, tokenScale, err := fees.TokenRate(st.state, st.msg.FeeTokenID())
if err != nil {
return fmt.Errorf("get token rate failed %v", err)
}
if feeRate == nil || tokenScale == nil || feeRate.Sign() <= 0 || tokenScale.Sign() <= 0 {
return fmt.Errorf("token rate or scale is nil")
}
st.feeRate = feeRate
st.tokenScale = tokenScale
return st.buyAltTokenGas()
}
Comment thread
curryxbo marked this conversation as resolved.
return st.buyGas()
}

Expand Down Expand Up @@ -474,19 +573,26 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) {
// The L2 Fee is the same as the fee that is charged in the normal geth
// codepath. Add the L1DataFee to the L2 fee for the total fee that is sent
// to the sequencer.
l2Fee := new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), effectiveTip)
fee := l2Fee
if st.evm.ChainConfig().Morph.FeeVaultEnabled() {
fee = new(big.Int).Add(st.l1DataFee, l2Fee)
if st.msg.FeeTokenID() == 0 {
l2Fee := new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), effectiveTip)
fee := l2Fee
if st.evm.ChainConfig().Morph.FeeVaultEnabled() {
fee = new(big.Int).Add(st.l1DataFee, l2Fee)
}
st.state.AddBalance(st.evm.FeeRecipient(), fee, tracing.BalanceIncreaseRewardTransactionFee)
}
st.state.AddBalance(st.evm.FeeRecipient(), fee, tracing.BalanceIncreaseRewardTransactionFee)

return &ExecutionResult{
result := &ExecutionResult{
L1DataFee: st.l1DataFee,
UsedGas: st.gasUsed(),
Err: vmerr,
ReturnData: ret,
}, nil
}
if st.msg.FeeTokenID() != 0 {
result.FeeRate = st.feeRate
result.TokenScale = st.tokenScale
}
return result, nil
}
Comment on lines +585 to 596
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Populate FeeRate/TokenScale/FeeUsed in result.

Result fields are unset (FeeUsed TODO). Record the token rate/scale at purchase and compute FeeUsed = purchased − refunded.

  • During buyERC20Gas:
    • set st.feeRate, st.tokenScale from fees.TokenRate(...)
    • set st.feeUsed = new(big.Int).Set(erc20Mgval)
  • In refundGas:
    • when refunding ERC20, subtract refunded tokenAmount from st.feeUsed; clamp at >= 0.

No behavioral change to execution; only metrics correctness.


// validateAuthorization validates an EIP-7702 authorization against the state.
Expand Down Expand Up @@ -559,17 +665,40 @@ func (st *StateTransition) refundGas(refundQuotient uint64) {
}
st.gas += refund

// Return remaining gas to the block gas counter at the end, regardless of refund success
defer func() {
if st.gas > 0 {
st.gp.AddGas(st.gas)
}
}()

// Return ETH for remaining gas, exchanged at the original rate.
remaining := new(big.Int).Mul(new(big.Int).SetUint64(st.gas), st.gasPrice)
st.state.AddBalance(st.msg.From(), remaining, tracing.BalanceIncreaseGasReturn)
if st.msg.FeeTokenID() != 0 {
tokenInfo, err := fees.GetTokenInfo(st.state, st.msg.FeeTokenID())
if err != nil {
log.Error("Failed to get token info for gas refund", "tokenID", st.msg.FeeTokenID(), "error", err)
return
}
tokenAmount := types.EthToAlt(remaining, st.feeRate, st.tokenScale)
if err = st.TransferAltTokenHybrid(
tokenInfo.TokenAddress,
*st.evm.ChainConfig().Morph.FeeVaultAddress,
st.msg.From(),
tokenAmount,
tokenInfo.BalanceSlot,
nil,
); err != nil {
log.Error("Failed to refund alt token gas", "tokenID", st.msg.FeeTokenID(), "tokenAddress", tokenInfo.TokenAddress.Hex(), "amount", tokenAmount, "error", err)
// Continue execution even if refund fails - refund should not cause transaction to fail
}
} else {
st.state.AddBalance(st.msg.From(), remaining, tracing.BalanceIncreaseGasReturn)
}

if st.evm.Config.Tracer != nil && st.evm.Config.Tracer.OnGasChange != nil && st.gas > 0 {
st.evm.Config.Tracer.OnGasChange(st.gas, 0, tracing.GasChangeTxLeftOverReturned)
}

// Also return remaining gas to the block gas counter so it is
// available for the next transaction.
st.gp.AddGas(st.gas)
}
Comment thread
Kukoomomo marked this conversation as resolved.

// gasUsed returns the amount of gas used up by the state transition.
Expand Down
Loading
Loading