Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
67 changes: 58 additions & 9 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 Down Expand Up @@ -85,9 +81,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 err
}
if gasTipCap.Cmp(minTipCap) < 0 {
gasTipCap = minTipCap
}

gasFeeCap := new(big.Int).Add(
gasTipCap,
new(big.Int).Mul(head.BaseFee, big.NewInt(basefeeWiggleMultiplier)),
)
if gasFeeCap.Cmp(minFeeCap) < 0 {
gasFeeCap = minFeeCap
}

if gasFeeCap.Cmp(gasTipCap) < 0 {
return fmt.Errorf("maxFeePerGas (%v) < maxPriorityFeePerGas (%v)", gasFeeCap, gasTipCap)
}
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 {
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) {
Expand Down
136 changes: 112 additions & 24 deletions pkg/relay/ethereum/gas_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,11 @@ func Test_TxOpts_AutoTx(t *testing.T) {
if err = calculator.Apply(context.Background(), txOpts); err != nil {
t.Fatal(err)
}
if txOpts.GasTipCap != nil {
t.Error("gasTipCap must be nil")
if txOpts.GasTipCap == nil || txOpts.GasTipCap.Cmp(big.NewInt(0)) == 0 {
t.Error("gasTipCap must be suggested")
}
if txOpts.GasFeeCap != nil {
t.Error("gasFeeCap must be nil")
if txOpts.GasFeeCap == nil || txOpts.GasFeeCap.Cmp(big.NewInt(0)) == 0 {
t.Error("gasFeeCap must be suggested")
}
if txOpts.GasPrice != nil {
t.Error("gasPrice must be nil")
Expand Down Expand Up @@ -137,15 +137,18 @@ func Test_getFeeInfo(t *testing.T) {

type MockChainClient struct {
IChainClient
MockSuggestGasPrice big.Int
MockPendingTransaction *txpool.RPCTransaction
MockLatestHeaderNumber big.Int
MockHistoryGasTipCap big.Int
MockHistoryGasFeeCap big.Int
MockSuggestGasPrice big.Int
MockSuggestGasTipCap big.Int
MockPendingTransaction *txpool.RPCTransaction
MockLatestHeaderNumber big.Int
MockLatestHeaderBaseFee *big.Int
MockHistoryGasTipCap big.Int
MockHistoryGasFeeCap big.Int
}

func (cl *MockChainClient) SuggestGasPrice(ctx context.Context) (*big.Int, error) {
return &cl.MockSuggestGasPrice, nil
// Copy the big.Int to prevent modification of the original value in tests
return new(big.Int).Set(&cl.MockSuggestGasPrice), nil
}

func inclByPercent(n *big.Int, percent uint64) {
Expand All @@ -170,21 +173,28 @@ func (cl *MockChainClient) HeaderByNumber(ctx context.Context, number *big.Int)
}, nil
} else {
return &gethtypes.Header{
Number: &cl.MockLatestHeaderNumber,
Number: &cl.MockLatestHeaderNumber,
BaseFee: cl.MockLatestHeaderBaseFee,
}, nil
}
}
func (cl *MockChainClient) FeeHistory(ctx context.Context, blockCount uint64, lastBlock *big.Int, rewardPercentiles []float64) (*ethereum.FeeHistory, error) {
return &ethereum.FeeHistory{
Reward: [][]*big.Int{ // gasTipCap
{&cl.MockHistoryGasTipCap},
// Copy the big.Int to prevent modification of the original value in tests
{new(big.Int).Set(&cl.MockHistoryGasTipCap)},
},
BaseFee: []*big.Int{ // baseFee. This is used as gasFeeCap
&cl.MockHistoryGasFeeCap,
// Copy the big.Int to prevent modification of the original value in tests
new(big.Int).Set(&cl.MockHistoryGasFeeCap),
},
}, nil
}

func (cl *MockChainClient) SuggestGasTipCap(ctx context.Context) (*big.Int, error) {
return new(big.Int).Set(&cl.MockSuggestGasTipCap), nil
}

func TestPriceBumpLegacy(t *testing.T) {
cli := MockChainClient{}
config := createConfig()
Expand Down Expand Up @@ -213,11 +223,11 @@ func TestPriceBumpLegacy(t *testing.T) {
}
}

// test that old tx's gasPrice is already exceeds suggestion
// test the case where old tx's gasPrice already exceeds suggestion
{
cli.MockSuggestGasPrice.SetUint64(99)
err := calculator.Apply(context.Background(), txOpts)
if err == nil || err.Error() != "old tx's gasPrice(100) is higher than suggestion(99)" {
if err == nil || err.Error() != "failed to calculate gas price: old tx's gasPrice(100) is higher than suggestion(99)" {
t.Fatal(err)
}
}
Expand Down Expand Up @@ -254,9 +264,9 @@ func TestPriceBumpDynamic(t *testing.T) {

// test that gasTipCap and gasFeeCap are bumped from old tx's one
{
// set suggenstion between old value and bump value to apply bump value
// set suggestion to a value between old value and bump value to apply bump value
cli.MockHistoryGasTipCap.SetUint64(201)
cli.MockHistoryGasFeeCap.SetUint64(301 - 201) // note that gasTipCap is added to gasFeeCap
cli.MockHistoryGasFeeCap.SetUint64(301 - 201) // note that the suggested gasFeeCap is this value plus gasFeeCap above
if err := calculator.Apply(context.Background(), txOpts); err != nil {
t.Fatal(err)
}
Expand All @@ -268,18 +278,19 @@ func TestPriceBumpDynamic(t *testing.T) {
}
}

// test that old only tx's gasTipCap exceeds suggestion
// test the case where only old tx's gasTipCap exceeds suggestion
{
cli.MockHistoryGasTipCap.SetUint64(199) // lower than old tx's tipCap(=200)
cli.MockHistoryGasTipCap.SetUint64(199) // lower than old tx's tipCap(=200)
cli.MockHistoryGasFeeCap.SetUint64(301 - 199) // greater than old tx's feeCap(=300)
err := calculator.Apply(context.Background(), txOpts)
if err != nil {
t.Fatal(err)
}
}

// test that old only tx's gasFeeCap exceeds suggestion
// test the case where only old tx's gasFeeCap exceeds suggestion
{
// Because gasTipCap suggestion is added to gasFeeCap suggenstion,
// Because gasTipCap suggestion is added to gasFeeCap suggestion,
// gasTipCap suggestion should be lower than old tx's gasFeeCap
cli.MockPendingTransaction.GasTipCap = (*hexutil.Big)(big.NewInt(100))
cli.MockPendingTransaction.GasFeeCap = (*hexutil.Big)(big.NewInt(300))
Expand All @@ -291,15 +302,92 @@ func TestPriceBumpDynamic(t *testing.T) {
}
}

// test that old both tx's gasFeeCap and gasTipCap exceed suggestion
// test the case where both old tx's gasFeeCap and gasTipCap exceed suggestion
{
cli.MockPendingTransaction.GasTipCap = (*hexutil.Big)(big.NewInt(100))
cli.MockPendingTransaction.GasFeeCap = (*hexutil.Big)(big.NewInt(300))
cli.MockHistoryGasTipCap.SetUint64(100) // eq to 100
cli.MockHistoryGasFeeCap.SetUint64(300 - 100) // eq to 300
cli.MockHistoryGasTipCap.SetUint64(100) // eq to old tx's tipCap(=100)
cli.MockHistoryGasFeeCap.SetUint64(300 - 100) // eq to old tx's feeCap(=300)
err := calculator.Apply(context.Background(), txOpts)
if err == nil || err.Error() != "old tx's gasFeeCap(300) and gasTipCap(100) are greater than or equal to suggestion(300, 100)" {
t.Fatal(err)
}
}
}

func TestPriceBumpAutoLegacy(t *testing.T) {
cli := MockChainClient{}
config := createConfig()
config.TxType = TxTypeAuto
config.PriceBump = 10
calculator := NewGasFeeCalculator(&cli, config)

txOpts := &bind.TransactOpts{}
txOpts.Nonce = big.NewInt(1)

cli.MockPendingTransaction = &txpool.RPCTransaction{
GasPrice: (*hexutil.Big)(big.NewInt(100)),
GasTipCap: (*hexutil.Big)(big.NewInt(200)),
GasFeeCap: (*hexutil.Big)(big.NewInt(300)),
Nonce: (hexutil.Uint64)(txOpts.Nonce.Uint64()),
}

// test that gasPrice is bumped from old tx's gasFeeCap
{
cli.MockSuggestGasPrice.SetUint64(100)
if err := calculator.Apply(context.Background(), txOpts); err != nil {
t.Fatal(err)
}
if txOpts.GasPrice.Uint64() != 330 { //gasFeeCap * 1.1
t.Errorf("gasPrice should be 330 but %v", txOpts.GasPrice)
}
}

// test the case where old tx's gasPrice already exceeds suggestion
{
cli.MockSuggestGasPrice.SetUint64(99)
err := calculator.Apply(context.Background(), txOpts)
if err == nil || err.Error() != "failed to calculate gas price: old tx's gasPrice(100) is higher than suggestion(99)" {
t.Fatal(err)
}
}
}

func TestPriceBumpAutoDynamic(t *testing.T) {
cli := MockChainClient{}
config := createConfig()
config.TxType = TxTypeAuto
config.PriceBump = 10
calculator := NewGasFeeCalculator(&cli, config)

txOpts := &bind.TransactOpts{}
txOpts.Nonce = big.NewInt(1)

cli.MockLatestHeaderNumber.SetUint64(1000)
cli.MockPendingTransaction = &txpool.RPCTransaction{
GasPrice: (*hexutil.Big)(big.NewInt(100)),
GasTipCap: (*hexutil.Big)(big.NewInt(200)),
GasFeeCap: (*hexutil.Big)(big.NewInt(300)),
Nonce: (hexutil.Uint64)(txOpts.Nonce.Uint64()),
}

// test that gasTipCap and gasFeeCap are bumped from old tx's one
{
// set suggestion to a value lower than bump value (220) to apply bump value
cli.MockSuggestGasTipCap.SetUint64(219)
// set suggestion to a value lower than bump value (330) to apply bump value
// NOTE: bumped gasFeeCap = bumped gasTipCap + baseFee * basefeeWiggleMultiplier
// <=> baseFee = (bumped gasFeeCap - bumped gasTipCap) / basefeeWiggleMultiplier
// = (330 - 220) / 2
cli.MockLatestHeaderBaseFee = big.NewInt((330-220)/2 - 1)
if err := calculator.Apply(context.Background(), txOpts); err != nil {
t.Fatal(err)
}
if txOpts.GasTipCap.Uint64() != 220 {
t.Errorf("gasTipCap should be 220 but %v", txOpts.GasTipCap)
}
if txOpts.GasFeeCap.Uint64() != 330 {
t.Errorf("gasFeeCap should be 330 but %v", txOpts.GasFeeCap)
}
}
}
Loading