-
Notifications
You must be signed in to change notification settings - Fork 16
Implement alt token fee #205
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6ce1d31
872680b
eeb0611
f2c680f
b68a8bf
8b6dc3d
9242114
163825d
d045bc5
d5625f0
4415b48
43e4446
dada931
895b64d
5769551
eb4c252
5f1f513
d9597a0
e3cae2b
e9b6088
472d7a0
e30824d
8907321
d04e496
321f89e
080fb68
868b12e
43a36b0
9991d26
82206cd
456073d
ed34e3c
b0eb667
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -48,3 +48,4 @@ profile.cov | |
|
|
||
| **/yarn-error.log | ||
| /build/db*/ | ||
| local-test | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -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, | ||
| } | ||
| 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") | ||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate consistency between FeeTokenID and FeeLimit. The routing logic only checks 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
🤖 Prompt for AI Agents |
||
| } else { | ||
| // Chain is not London ready -> use legacy transaction | ||
| rawTx, err = c.createLegacyTx(opts, contract, input) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,8 +17,10 @@ | |
| package core | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "errors" | ||
| "fmt" | ||
| "math" | ||
| math "math" | ||
| "math/big" | ||
| "time" | ||
|
|
||
|
|
@@ -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 | ||
|
|
@@ -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. | ||
|
|
@@ -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) | ||
|
|
@@ -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() | ||
| } | ||
|
|
||
|
|
@@ -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) | ||
|
|
||
|
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) | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainVerify gas pool rollback on transfer failure. Based on past review comments, a rollback mechanism was added to restore the gas pool if 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 -100Length of output: 2580 🏁 Script executed: #!/bin/bash
# Search for AddGas usage patterns in the codebase
rg -n "AddGas" core/state_transition.go -B3 -A3Length 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 -A2Length 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 Add a defer block immediately after // 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 |
||
|
|
||
| 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) | ||
|
|
@@ -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() | ||
| } | ||
|
curryxbo marked this conversation as resolved.
|
||
| return st.buyGas() | ||
| } | ||
|
|
||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Populate FeeRate/TokenScale/FeeUsed in result. Result fields are unset (FeeUsed TODO). Record the token rate/scale at purchase and compute FeeUsed = purchased − refunded.
No behavioral change to execution; only metrics correctness. |
||
|
|
||
| // validateAuthorization validates an EIP-7702 authorization against the state. | ||
|
|
@@ -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) | ||
| } | ||
|
Kukoomomo marked this conversation as resolved.
|
||
|
|
||
| // gasUsed returns the amount of gas used up by the state transition. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Validate FeeLimit before creating the transaction.
FeeLimitis assigned directly fromopts.FeeLimitwithout checking if it'snilor 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
🤖 Prompt for AI Agents