Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 pkg/relay/ethereum/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ type IChainClient interface {
ethereum.ChainReader
ethereum.GasPricer
ethereum.FeeHistoryReader
ethereum.GasPricer1559

GetMinimumRequiredFee(ctx context.Context, address common.Address, nonce uint64, priceBump uint64) (*txpool.RPCTransaction, *big.Int, *big.Int, error)
}
Expand Down
109 changes: 89 additions & 20 deletions pkg/relay/ethereum/gas.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
"github.com/ethereum/go-ethereum/common"
)

const basefeeWiggleMultiplier = 2

type GasFeeCalculator struct {
client IChainClient
config *ChainConfig
Expand All @@ -35,15 +37,9 @@ func (m *GasFeeCalculator) Apply(ctx context.Context, txOpts *bind.TransactOpts)
}
switch m.config.TxType {
case TxTypeLegacy:
gasPrice, err := m.client.SuggestGasPrice(ctx)
gasPrice, err := m.calculateGasPrice(ctx, oldTx, minFeeCap)
if err != nil {
return fmt.Errorf("failed to suggest gas price: %v", err)
}
if oldTx != nil && oldTx.GasPrice != nil && oldTx.GasPrice.ToInt().Cmp(gasPrice) > 0 {
return fmt.Errorf("old tx's gasPrice(%v) is higher than suggestion(%v)", oldTx.GasPrice.ToInt(), gasPrice)
}
if gasPrice.Cmp(minFeeCap) < 0 {
gasPrice = minFeeCap
return fmt.Errorf("failed to calculate gas price: %v", err)
}
txOpts.GasPrice = gasPrice
return nil
Expand All @@ -59,22 +55,14 @@ func (m *GasFeeCalculator) Apply(ctx context.Context, txOpts *bind.TransactOpts)
m.config.DynamicTxGasConfig.BaseFeeRate.Mul(gasFeeCap)
gasFeeCap.Add(gasFeeCap, gasTipCap)

if oldTx != nil && oldTx.GasFeeCap != nil && oldTx.GasTipCap != nil {
if oldTx.GasFeeCap.ToInt().Cmp(gasFeeCap) >= 0 && oldTx.GasTipCap.ToInt().Cmp(gasTipCap) >= 0 {
return fmt.Errorf("old tx's gasFeeCap(%v) and gasTipCap(%v) are greater than or equal to suggestion(%v, %v)", oldTx.GasFeeCap.ToInt(), oldTx.GasTipCap.ToInt(), gasFeeCap, gasTipCap)
}
gasTipCap, gasFeeCap, err = m.applyMinGasCaps(oldTx, gasTipCap, gasFeeCap, minTipCap, minFeeCap)
if err != nil {
return fmt.Errorf("failed to apply min gas caps: %v", err)
}

if gasTipCap.Cmp(minTipCap) < 0 {
gasTipCap = minTipCap
}
if l := m.config.DynamicTxGasConfig.GetLimitPriorityFeePerGas(); l.Sign() > 0 && gasTipCap.Cmp(l) > 0 {
gasTipCap = l
}

if gasFeeCap.Cmp(minFeeCap) < 0 {
gasFeeCap = minFeeCap
}
if l := m.config.DynamicTxGasConfig.GetLimitFeePerGas(); l.Sign() > 0 && gasFeeCap.Cmp(l) > 0 {
gasFeeCap = l
}
Expand All @@ -85,11 +73,62 @@ func (m *GasFeeCalculator) Apply(ctx context.Context, txOpts *bind.TransactOpts)
txOpts.GasFeeCap = gasFeeCap
txOpts.GasTipCap = gasTipCap
return nil
case TxTypeAuto:
// Calculate gas options in the same way as bind.BoundContract.transact
head, err := m.client.HeaderByNumber(ctx, nil)
if err != nil {
return fmt.Errorf("failed to get latest header: %v", err)
}

if head.BaseFee == nil {
gasPrice, err := m.calculateGasPrice(ctx, oldTx, minFeeCap)
if err != nil {
return fmt.Errorf("failed to calculate gas price: %v", err)
}
txOpts.GasPrice = gasPrice
return nil
} else {
gasTipCap, err := m.client.SuggestGasTipCap(ctx)
if err != nil {
return fmt.Errorf("failed to suggest gas tip cap: %v", err)
}
gasFeeCap := new(big.Int).Add(
gasTipCap,
new(big.Int).Mul(head.BaseFee, big.NewInt(basefeeWiggleMultiplier)),
)

gasTipCap, gasFeeCap, err = m.applyMinGasCaps(oldTx, gasTipCap, gasFeeCap, minTipCap, minFeeCap)
if err != nil {
return fmt.Errorf("failed to apply min gas caps: %v", err)
}
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

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

Missing validation to check if the old transaction's gas values already exceed the suggestions. The TxTypeDynamic case includes this check (lines 58-62):

if oldTx != nil && oldTx.GasFeeCap != nil && oldTx.GasTipCap != nil {
    if oldTx.GasFeeCap.ToInt().Cmp(gasFeeCap) >= 0 && oldTx.GasTipCap.ToInt().Cmp(gasTipCap) >= 0 {
        return fmt.Errorf("old tx's gasFeeCap(%v) and gasTipCap(%v) are greater than or equal to suggestion(%v, %v)", ...)
    }
}

This validation should be added before line 118 to ensure that when both old tx values are already at or above the suggested values, an error is returned instead of attempting to use potentially stale values.

Suggested change
}
}
if oldTx != nil && oldTx.GasFeeCap != nil && oldTx.GasTipCap != nil {
if oldTx.GasFeeCap.ToInt().Cmp(gasFeeCap) >= 0 && oldTx.GasTipCap.ToInt().Cmp(gasTipCap) >= 0 {
return fmt.Errorf("old tx's gasFeeCap(%v) and gasTipCap(%v) are greater than or equal to suggestion(%v, %v)", oldTx.GasFeeCap.ToInt(), oldTx.GasTipCap.ToInt(), gasFeeCap, gasTipCap)
}
}

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've added the validation in the commit 5c4ff80


txOpts.GasFeeCap = gasFeeCap
txOpts.GasTipCap = gasTipCap
return nil
}
default:
return nil
panic("unsupported tx type")
}
}

func (m *GasFeeCalculator) calculateGasPrice(ctx context.Context, oldTx *txpool.RPCTransaction, minFeeCap *big.Int) (*big.Int, error) {
gasPrice, err := m.client.SuggestGasPrice(ctx)
if err != nil {
return nil, fmt.Errorf("failed to suggest gas price: %v", err)
}
if oldTx != nil && oldTx.GasPrice != nil && oldTx.GasPrice.ToInt().Cmp(gasPrice) > 0 {
// Since the old tx's gas price is already higher than the suggested value,
// the gas price is not the reason the old tx has not been processed.
// To avoid raising it indefinitely, return an error.
return nil, fmt.Errorf("old tx's gasPrice(%v) is higher than suggestion(%v)", oldTx.GasPrice.ToInt(), gasPrice)
}
if gasPrice.Cmp(minFeeCap) < 0 {
gasPrice = minFeeCap
}

return gasPrice, nil
}

func (m *GasFeeCalculator) feeHistory(ctx context.Context) (*big.Int, *big.Int, error) {
rewardPercentile := float64(m.config.DynamicTxGasConfig.FeeHistoryRewardPercentile)
maxRetry := m.config.DynamicTxGasConfig.MaxRetryForFeeHistory
Expand Down Expand Up @@ -125,3 +164,33 @@ func getFeeInfo(v *ethereum.FeeHistory) (*big.Int, *big.Int, bool) {
baseFee := v.BaseFee[0]
return gasTipCap, baseFee, true
}

func (m *GasFeeCalculator) applyMinGasCaps(
oldTx *txpool.RPCTransaction,
gasTipCap *big.Int,
gasFeeCap *big.Int,
minTipCap *big.Int,
minFeeCap *big.Int,
) (*big.Int, *big.Int, error) {
if oldTx != nil && oldTx.GasFeeCap != nil && oldTx.GasTipCap != nil {
if oldTx.GasFeeCap.ToInt().Cmp(gasFeeCap) >= 0 && oldTx.GasTipCap.ToInt().Cmp(gasTipCap) >= 0 {
// Since the old tx's gas parameters are already higher than the suggested values,
// the gas parameters are not the reason the old tx has not been processed.
// To avoid raising them indefinitely, return an error.
return nil, nil, fmt.Errorf("old tx's gasFeeCap(%v) and gasTipCap(%v) are greater than or equal to suggestion(%v, %v)", oldTx.GasFeeCap.ToInt(), oldTx.GasTipCap.ToInt(), gasFeeCap, gasTipCap)
}
}

if gasTipCap.Cmp(minTipCap) < 0 {
gasTipCap = minTipCap
}
if gasFeeCap.Cmp(minFeeCap) < 0 {
gasFeeCap = minFeeCap
}

if gasFeeCap.Cmp(gasTipCap) < 0 {
return nil, nil, fmt.Errorf("maxFeePerGas (%v) < maxPriorityFeePerGas (%v)", gasFeeCap, gasTipCap)
}

return gasTipCap, gasFeeCap, nil
}
Loading