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
85 changes: 85 additions & 0 deletions devnet-sdk/system/periphery/go-ethereum/fees.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package goethereum

import (
"context"
"math/big"

"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
)

var (
// Ensure that the feeEstimator implements the FeeEstimator interface
_ FeeEstimator = (*EIP1559FeeEstimator)(nil)

// Ensure that the EIP1159FeeEthClient implements the EIP1159FeeEthClient interface
_ EIP1159FeeEthClient = (*ethclient.Client)(nil)
)

// FeeEstimator is a generic fee estimation interface (not specific to EIP-1559)
type FeeEstimator interface {
EstimateFees(ctx context.Context, opts *bind.TransactOpts) (*bind.TransactOpts, error)
}

// EIP1559FeeEstimator is a fee estimator that uses EIP-1559 fee estimation
type EIP1559FeeEstimator struct {
// Access to the Ethereum client is needed to get the fee information from the chain
client EIP1159FeeEthClient

// The tip multiplier is used to increase the maxPriorityFeePerGas (GasTipCap) by a factor
tipMultiplier *big.Int
}

func NewEIP1559FeeEstimator(client EIP1159FeeEthClient) *EIP1559FeeEstimator {
return &EIP1559FeeEstimator{
client: client,
tipMultiplier: big.NewInt(1),
}
}

func (f *EIP1559FeeEstimator) WithTipMultiplier(multiplier *big.Int) *EIP1559FeeEstimator {
newF := *f
newF.tipMultiplier = multiplier

return &newF
}

func (f *EIP1559FeeEstimator) EstimateFees(ctx context.Context, opts *bind.TransactOpts) (*bind.TransactOpts, error) {
newOpts := *opts

// Add a gas tip cap if needed
if newOpts.GasTipCap == nil {
tipCap, err := f.client.SuggestGasTipCap(ctx)

if err != nil {
return nil, err
}

// GasTipCap represents the maxPriorityFeePerGas
newOpts.GasTipCap = big.NewInt(0).Mul(tipCap, f.tipMultiplier)
}

// Add a gas fee cap if needed
if newOpts.GasFeeCap == nil {
block, err := f.client.BlockByNumber(ctx, nil)

if err != nil {
return nil, err
}

baseFee := block.BaseFee()
if baseFee != nil {
// The total fee (maxFeePerGas) is the sum of the base fee and the tip
newOpts.GasFeeCap = big.NewInt(0).Add(block.BaseFee(), newOpts.GasTipCap)
}
}

return &newOpts, nil
}

// EIP1159FeeEthClient is a subset of the ethclient.Client interface required for fee estimation
type EIP1159FeeEthClient interface {
BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error)
SuggestGasTipCap(ctx context.Context) (*big.Int, error)
}
201 changes: 201 additions & 0 deletions devnet-sdk/system/periphery/go-ethereum/fees_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
package goethereum

import (
"context"
"fmt"
"math/big"
"testing"

"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestEstimateEIP1559Fees(t *testing.T) {
t.Run("if GasFeeCap and GasTipCap are not nil", func(t *testing.T) {
opts := &bind.TransactOpts{
GasFeeCap: big.NewInt(1),
GasTipCap: big.NewInt(2),
}

t.Run("should not modify the options", func(t *testing.T) {
feeEstimator := NewEIP1559FeeEstimator(&mockFeeEthClientImpl{})
newOpts, err := feeEstimator.EstimateFees(context.Background(), opts)
require.NoError(t, err)

require.Equal(t, opts, newOpts)

// We make sure that we get a copy of the object to prevent mutating the original
assert.NotSame(t, opts, newOpts)
})
})

t.Run("if the GasTipCap is nil", func(t *testing.T) {
defaultOpts := &bind.TransactOpts{
GasFeeCap: big.NewInt(1),
From: common.Address{},
Nonce: big.NewInt(64),
}

t.Run("should return an error if the client returns an error", func(t *testing.T) {
tipCapErr := fmt.Errorf("tip cap error")
feeEstimator := NewEIP1559FeeEstimator(&mockFeeEthClientImpl{
tipCapErr: tipCapErr,
})
_, err := feeEstimator.EstimateFees(context.Background(), defaultOpts)
require.Equal(t, tipCapErr, err)
})

t.Run("with default tip multiplier", func(t *testing.T) {
t.Run("should set the GasTipCap to the client's suggested tip cap", func(t *testing.T) {
tipCapValue := big.NewInt(5)
feeEstimator := NewEIP1559FeeEstimator(&mockFeeEthClientImpl{
tipCapValue: tipCapValue,
})

newOpts, err := feeEstimator.EstimateFees(context.Background(), defaultOpts)
require.NoError(t, err)

// We create a new opts with the expected tip cap added
expectedOpts := *defaultOpts
expectedOpts.GasTipCap = tipCapValue

// We check that the tip has been added
require.Equal(t, &expectedOpts, newOpts)

// We make sure that we get a copy of the object to prevent mutating the original
assert.NotSame(t, defaultOpts, newOpts)
})
})

t.Run("with custom tip multiplier", func(t *testing.T) {
t.Run("should set the GasTipCap to the client's suggested tip cap multplied by the tip multiplier", func(t *testing.T) {
tipCapValue := big.NewInt(5)
tipMultiplier := big.NewInt(10)
// The expected tip is a product of the tip cap and the tip multiplier
expectedTip := big.NewInt(50)

// We create a fee estimator with a custom tip multiplier
feeEstimator := NewEIP1559FeeEstimator(&mockFeeEthClientImpl{
tipCapValue: tipCapValue,
}).WithTipMultiplier(tipMultiplier)

newOpts, err := feeEstimator.EstimateFees(context.Background(), defaultOpts)
require.NoError(t, err)

// We create a new opts with the expected tip cap added
expectedOpts := *defaultOpts
expectedOpts.GasTipCap = expectedTip

// We check that the tip has been added
require.Equal(t, &expectedOpts, newOpts)

// We make sure that we get a copy of the object to prevent mutating the original
assert.NotSame(t, defaultOpts, newOpts)
})
})
})

t.Run("if the GasFeeCap is nil", func(t *testing.T) {
defaultOpts := &bind.TransactOpts{
GasTipCap: big.NewInt(1),
From: common.Address{},
Nonce: big.NewInt(64),
}

t.Run("should return an error if the client returns an error", func(t *testing.T) {
blockErr := fmt.Errorf("tip cap error")
feeEstimator := NewEIP1559FeeEstimator(&mockFeeEthClientImpl{
blockErr: blockErr,
})
_, err := feeEstimator.EstimateFees(context.Background(), defaultOpts)
require.Equal(t, blockErr, err)
})

t.Run("should set the GasFeeCap to the sum of block base fee and tip", func(t *testing.T) {
baseFeeValue := big.NewInt(5)
blockValue := types.NewBlock(&types.Header{
BaseFee: baseFeeValue,
Time: 0,
}, nil, nil, nil, &mockBlockType{})

// We expect the total gas cap to be the base fee plus the tip cap
expectedGas := big.NewInt(0).Add(baseFeeValue, defaultOpts.GasTipCap)

feeEstimator := NewEIP1559FeeEstimator(&mockFeeEthClientImpl{
blockValue: blockValue,
})

newOpts, err := feeEstimator.EstimateFees(context.Background(), defaultOpts)
require.NoError(t, err)

// We create a new opts with the expected fee cap added
expectedOpts := *defaultOpts
expectedOpts.GasFeeCap = expectedGas

// We check that the tip has been added
require.Equal(t, &expectedOpts, newOpts)

// We make sure that we get a copy of the object to prevent mutating the original
assert.NotSame(t, defaultOpts, newOpts)
})

t.Run("should set the GasFeeCap to nil if the base fee is nil", func(t *testing.T) {
blockValue := types.NewBlock(&types.Header{
BaseFee: nil,
Time: 0,
}, nil, nil, nil, &mockBlockType{})

feeEstimator := NewEIP1559FeeEstimator(&mockFeeEthClientImpl{
blockValue: blockValue,
})

newOpts, err := feeEstimator.EstimateFees(context.Background(), defaultOpts)
require.NoError(t, err)

// We create a new opts with the expected fee cap added
expectedOpts := *defaultOpts
expectedOpts.GasFeeCap = nil

// We check that the tip has been added
require.Equal(t, &expectedOpts, newOpts)

// We make sure that we get a copy of the object to prevent mutating the original
assert.NotSame(t, defaultOpts, newOpts)
})
})
}

var (
_ EIP1159FeeEthClient = (*mockFeeEthClientImpl)(nil)

_ types.BlockType = (*mockBlockType)(nil)
)

type mockFeeEthClientImpl struct {
blockValue *types.Block
blockErr error

tipCapValue *big.Int
tipCapErr error
}

func (m *mockFeeEthClientImpl) BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) {
return m.blockValue, m.blockErr
}

func (m *mockFeeEthClientImpl) SuggestGasTipCap(ctx context.Context) (*big.Int, error) {
return m.tipCapValue, m.tipCapErr
}

type mockBlockType struct{}

func (m *mockBlockType) HasOptimismWithdrawalsRoot(blkTime uint64) bool {
return false
}

func (m *mockBlockType) IsIsthmus(blkTime uint64) bool {
return false
}