Skip to content

[Misc] Calculating Base Fee for next block. #2121

@WorldCypher

Description

@WorldCypher

Seid version

% seid version --long | head
name: sei
server_name: <appd>
version: v6.0.5-6-g620c03b1
commit: 620c03b10312874c52c96c6c9c07e87f23b9ae0a
build_tags: netgo,ledger
go: go version go1.23.0 darwin/arm64
build_deps:
- cosmossdk.io/[email protected]
- filippo.io/[email protected]
- github.com/99designs/[email protected]

Chain ID
1329

Describe the Issue
I’m trying to integrate Sei’s EVM base fee calculation into my project and have extracted the function from this code.

For most blocks, my implementation gives the expected result, but for some, I see a discrepancy of -1, and for others, the difference is much larger. I’m using the mainnet parameters retrieved from seid.

Could someone provide an example of how the calculation should be done step by step, or help me verify if I’m using the correct parameters?

Additional context

Here are some example blocks where I noticed a larger difference in calculations:

Block        | Parent Base Fee      | Calculated Fee       | Actual Fee           | Diff (Wei) | Parent Gas Limit | Diff (%)                       |
----------------------------------------------------------------------------------------------------
140310769    | 1000000000           | 1006650013           | 1006581340           | 68673       | 10000000        | 0.0068223995%
140310772    | 1002075274           | 1002293288           | 1000170285           | 2123003     | 10000000        | 0.2122641546%
140310774    | 1000753349           | 1001336255           | 1001258747           | 77508       | 10000000        | 0.0077410560%
140310803    | 1000000000           | 1000339254           | 1000277950           | 61304       | 10000000        | 0.0061286965%
140310819    | 1000000000           | 1000554712           | 1000492828           | 61884       | 10000000        | 0.0061853517%
140311951    | 1000312343           | 1000494563           | 1000432665           | 61898       | 10000000        | 0.0061871230%

Here are all of the results ~400 blocks: results.txt

Here is the script:

package main

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"log/slog"
	"math/big"
	"net/http"
	"os"
	"strconv"
	"strings"
	"time"

	sdk "github.com/cosmos/cosmos-sdk/types"

	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/common/hexutil"
	tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
)

const (
	publicEndpoint  = "https://evm-rpc.sei-apis.com/"
	publicEndpoint2 = "https://evm-rpc.pacific-1.sei.io/"
	ourEndpoint     = "http://eth-rpc.sei.mainnet.prod"
	rpcEndpoint     = publicEndpoint2
	sleepTime       = 100 * time.Millisecond
	retry           = 3
)

func main() {
	logger := slog.New(slog.NewTextHandler(os.Stdout, nil))

	ctx := context.Background()

	blocksToCheck := 1000
	nextHeight := int64(0)
	var (
		latestBlock *Block
		err         error
	)

	// Initialize the parameters from the memory
	params := DefaultParams()
	logger.Info("Using parameters",
		"priorityNormalizer", params.PriorityNormalizer.String(),
		"baseFeePerGas", params.BaseFeePerGas.String(),
		"maxDynamicBaseFeeUpwardAdjustment", params.MaxDynamicBaseFeeUpwardAdjustment.String(),
		"maxDynamicBaseFeeDownwardAdjustment", params.MaxDynamicBaseFeeDownwardAdjustment.String(),
		"minimumFeePerGas", params.MinimumFeePerGas.String(),
		"deliverTxHookWasmGasLimit", params.DeliverTxHookWasmGasLimit,
		"targetGasUsedPerBlock", params.TargetGasUsedPerBlock,
		"maximumFeePerGas", params.MaximumFeePerGas.String())

	// Create a table header for the results
	fmt.Printf("%-12s | %-20s | %-20s | %-20s | %-10s | %-15s | %-10s\n",
		"Block", "Parent Base Fee", "Calculated Fee", "Actual Fee", "Diff (Wei)", "Parent Gas Limit", "Diff (%)")
	fmt.Println(strings.Repeat("-", 100))

	for i := 0; i < blocksToCheck; i++ {
		if nextHeight == 0 {
			latestBlock, err = fetchLatestBlock(ctx, "latest")
			if err != nil {
				logger.Error("Failed to fetch latest block", "error", err)
				return
			}
		} else {
			latestBlock, err = fetchBlockByNumber(ctx, fmt.Sprintf("0x%x", nextHeight))
			if err != nil {
				logger.Error("Failed to fetch block by number", "error", err)
				return
			}
		}
		nextHeight = latestBlock.Number.ToInt().Int64() + 1

		latestBlockInt := latestBlock.Number.ToInt()
		parentBlockInt := new(big.Int).Sub(latestBlockInt, big.NewInt(1))
		parentBlockNumberHex := hexutil.EncodeBig(parentBlockInt)

		parentBlock, err := fetchBlockByNumber(ctx, parentBlockNumberHex)
		if err != nil {
			logger.Error("Failed to fetch parent block", "error", err)
			return
		}

		// Create a new context and keeper for each block
		keeper := NewKeeper()
		var sdkCtx *Context
		// Set the parent block's base fee as the next base fee
		if parentBlock.BaseFeePerGas != nil {
			weiValue := parentBlock.BaseFeePerGas.ToInt()
			prevBaseFee := sdk.NewDecFromBigInt(weiValue)
			sdkCtx = NewContext(parentBlockInt.Int64(), int64(parentBlock.GasLimit))
			keeper.SetNextBaseFeePerGas(sdkCtx, prevBaseFee)
			keeper.SetParams(sdkCtx, params)
		}

		// Calculate the new base fee using the original implementation
		newBaseFee := keeper.AdjustDynamicBaseFeePerGas(sdkCtx, uint64(parentBlock.GasUsed))
		if newBaseFee == nil {
			logger.Error("Failed to calculate new base fee")
			return
		}

		// Convert to integer for comparison (same as in the chain)
		newBaseFeeInt := newBaseFee.TruncateInt().BigInt()
		actualBaseFee := latestBlock.BaseFeePerGas.ToInt()

		// Calculate difference
		difference := new(big.Int).Sub(newBaseFeeInt, actualBaseFee)
		percentDiff := "0%"
		if actualBaseFee.Sign() > 0 {
			diffFloat := new(big.Float).SetInt(difference)
			actualFloat := new(big.Float).SetInt(actualBaseFee)

			// Calculate percentage: (diff / actual) * 100
			percent := new(big.Float).Quo(diffFloat, actualFloat)
			percent = percent.Mul(percent, big.NewFloat(100))

			// Convert to string with 2 decimal places
			percentVal, _ := percent.Float64()
			percentDiff = fmt.Sprintf("%.10f%%", percentVal)
			time.Sleep(sleepTime)
		}

		// Print the results in table format
		fmt.Printf("%-12s | %-20s | %-20s | %-20s | %-10s  | %-15d | %-10s\n",
			latestBlock.Number.ToInt().String(),
			parentBlock.BaseFeePerGas.ToInt().String(),
			newBaseFeeInt.String(),
			actualBaseFee.String(),
			difference.String(),
			parentBlock.GasLimit,
			percentDiff)
	}
}

// Block represents the Ethereum block structure we need
type Block struct {
	Number        hexutil.Big    `json:"number"`
	Hash          common.Hash    `json:"hash"`
	ParentHash    common.Hash    `json:"parentHash"`
	GasUsed       hexutil.Uint64 `json:"gasUsed"`
	GasLimit      hexutil.Uint64 `json:"gasLimit"`
	BaseFeePerGas *hexutil.Big   `json:"baseFeePerGas,omitempty"`
}

// RPCResponse represents the JSON-RPC response
type RPCResponse struct {
	JSONRPC string      `json:"jsonrpc"`
	ID      int         `json:"id"`
	Result  interface{} `json:"result,omitempty"`
	Error   *RPCError   `json:"error,omitempty"`
}

// RPCError represents the JSON-RPC error
type RPCError struct {
	Code    int    `json:"code"`
	Message string `json:"message"`
}

// RPCRequest represents the JSON-RPC request
type RPCRequest struct {
	JSONRPC string        `json:"jsonrpc"`
	Method  string        `json:"method"`
	Params  []interface{} `json:"params"`
	ID      int           `json:"id"`
}

// Params represents the EVM module parameters
type Params struct {
	PriorityNormalizer                     sdk.Dec  `json:"priority_normalizer"`
	BaseFeePerGas                          sdk.Dec  `json:"base_fee_per_gas"`
	MaxDynamicBaseFeeUpwardAdjustment      sdk.Dec  `json:"max_dynamic_base_fee_upward_adjustment"`
	MaxDynamicBaseFeeDownwardAdjustment    sdk.Dec  `json:"max_dynamic_base_fee_downward_adjustment"`
	MinimumFeePerGas                       sdk.Dec  `json:"minimum_fee_per_gas"`
	DeliverTxHookWasmGasLimit              uint64   `json:"deliver_tx_hook_wasm_gas_limit"`
	WhitelistedCwCodeHashesForDelegateCall [][]byte `json:"whitelisted_cw_code_hashes_for_delegate_call"`
	TargetGasUsedPerBlock                  uint64   `json:"target_gas_used_per_block"`
	MaximumFeePerGas                       sdk.Dec  `json:"maximum_fee_per_gas"`
}

// DefaultParams returns the default parameters used in Sei mainnet
func DefaultParams() Params {
	return Params{
		PriorityNormalizer:                     sdk.MustNewDecFromStr("1.000000000000000000"),
		BaseFeePerGas:                          sdk.MustNewDecFromStr("0.000000000000000000"),
		MaxDynamicBaseFeeUpwardAdjustment:      sdk.MustNewDecFromStr("0.007500000000000000"),
		MaxDynamicBaseFeeDownwardAdjustment:    sdk.MustNewDecFromStr("0.003900000000000000"),
		MinimumFeePerGas:                       sdk.NewDec(1000000000),
		DeliverTxHookWasmGasLimit:              300000,
		WhitelistedCwCodeHashesForDelegateCall: [][]byte(nil),
		TargetGasUsedPerBlock:                  850000,
		MaximumFeePerGas:                       sdk.MustNewDecFromStr("1000000000000.000000000000000000"),
	}
}

// Context represents a simplified version of the Cosmos SDK context
type Context struct {
	blockHeight int64
	gasLimit    int64
	params      Params
	store       map[string][]byte
}

// NewContext creates a new context with default values
func NewContext(blockHeight int64, gasLimit int64) *Context {
	return &Context{
		blockHeight: blockHeight,
		gasLimit:    gasLimit,
		store:       make(map[string][]byte),
	}
}

// BlockHeight returns the current block height
func (ctx *Context) BlockHeight() int64 {
	return ctx.blockHeight
}

// WithBlockHeight returns a new context with the given block height
func (ctx *Context) WithBlockHeight(height int64) *Context {
	newCtx := *ctx
	newCtx.blockHeight = height
	return &newCtx
}

// ConsensusParams returns the consensus parameters
func (ctx *Context) ConsensusParams() *tmproto.ConsensusParams {
	return &tmproto.ConsensusParams{
		Block: &tmproto.BlockParams{
			MaxGas: ctx.gasLimit,
		},
	}
}

// KVStore represents a simplified key-value store
type KVStore struct {
	store map[string][]byte
}

// Get returns the value for the given key
func (s *KVStore) Get(key []byte) []byte {
	return s.store[string(key)]
}

// Set sets the value for the given key
func (s *KVStore) Set(key []byte, value []byte) {
	s.store[string(key)] = value
}

// Delete deletes the value for the given key
func (s *KVStore) Delete(key []byte) {
	delete(s.store, string(key))
}

// KVStore returns a key-value store for the given store key
func (ctx *Context) KVStore(storeKey string) *KVStore {
	return &KVStore{
		store: ctx.store,
	}
}

// Keeper represents a simplified version of the EVM keeper
type Keeper struct {
	storeKey          string
	params            Params
	currBaseFeePerGas sdk.Dec
	nextBaseFeePerGas sdk.Dec
}

// NewKeeper creates a new keeper with default values
func NewKeeper() *Keeper {
	return &Keeper{
		storeKey: "evm",
		params:   DefaultParams(),
	}
}

// GetStoreKey returns the store key
func (k *Keeper) GetStoreKey() string {
	return k.storeKey
}

// GetParams returns the module parameters
func (k *Keeper) GetParams(ctx *Context) Params {
	return k.params
}

// SetParams sets the module parameters
func (k *Keeper) SetParams(ctx *Context, params Params) {
	k.params = params
}

// GetMinimumFeePerGas returns the minimum fee per gas
func (k *Keeper) GetMinimumFeePerGas(ctx *Context) sdk.Dec {
	return k.params.MinimumFeePerGas
}

// GetMaximumFeePerGas returns the maximum fee per gas
func (k *Keeper) GetMaximumFeePerGas(ctx *Context) sdk.Dec {
	return k.params.MaximumFeePerGas
}

// GetTargetGasUsedPerBlock returns the target gas used per block
func (k *Keeper) GetTargetGasUsedPerBlock(ctx *Context) uint64 {
	return k.params.TargetGasUsedPerBlock
}

// GetMaxDynamicBaseFeeUpwardAdjustment returns the maximum dynamic base fee upward adjustment
func (k *Keeper) GetMaxDynamicBaseFeeUpwardAdjustment(ctx *Context) sdk.Dec {
	return k.params.MaxDynamicBaseFeeUpwardAdjustment
}

// GetMaxDynamicBaseFeeDownwardAdjustment returns the maximum dynamic base fee downward adjustment
func (k *Keeper) GetMaxDynamicBaseFeeDownwardAdjustment(ctx *Context) sdk.Dec {
	return k.params.MaxDynamicBaseFeeDownwardAdjustment
}

// GetCurrBaseFeePerGas returns the current base fee per gas
func (k *Keeper) GetCurrBaseFeePerGas(ctx *Context) sdk.Dec {
	return k.currBaseFeePerGas
}

// SetCurrBaseFeePerGas sets the current base fee per gas
func (k *Keeper) SetCurrBaseFeePerGas(ctx *Context, baseFeePerGas sdk.Dec) {
	k.currBaseFeePerGas = baseFeePerGas
}

// SetNextBaseFeePerGas sets the next base fee per gas
func (k *Keeper) SetNextBaseFeePerGas(ctx *Context, baseFeePerGas sdk.Dec) {
	k.nextBaseFeePerGas = baseFeePerGas
}

// GetNextBaseFeePerGas returns the next base fee per gas
func (k *Keeper) GetNextBaseFeePerGas(ctx *Context) sdk.Dec {
	return k.nextBaseFeePerGas
}

// AdjustDynamicBaseFeePerGas adjusts the dynamic base fee per gas
// This is the exact implementation from the Sei mainnet EVM module
func (k *Keeper) AdjustDynamicBaseFeePerGas(ctx *Context, blockGasUsed uint64) *sdk.Dec {
	if ctx.ConsensusParams() == nil || ctx.ConsensusParams().Block == nil {
		return nil
	}
	prevBaseFee := k.GetNextBaseFeePerGas(ctx)
	// set the resulting base fee for block n-1 on block n
	k.SetCurrBaseFeePerGas(ctx, prevBaseFee)
	targetGasUsed := sdk.NewDec(int64(k.GetTargetGasUsedPerBlock(ctx)))
	if targetGasUsed.IsZero() { // avoid division by zero
		return &prevBaseFee // return the previous base fee as is
	}
	minimumFeePerGas := k.GetParams(ctx).MinimumFeePerGas
	maximumFeePerGas := k.GetParams(ctx).MaximumFeePerGas
	blockGasLimit := sdk.NewDec(ctx.ConsensusParams().Block.MaxGas)
	blockGasUsedDec := sdk.NewDec(int64(blockGasUsed))

	// cap block gas used to block gas limit
	if blockGasUsedDec.GT(blockGasLimit) {
		blockGasUsedDec = blockGasLimit
	}

	var newBaseFee sdk.Dec
	if blockGasUsedDec.GT(targetGasUsed) {
		// upward adjustment
		numerator := blockGasUsedDec.Sub(targetGasUsed)
		denominator := blockGasLimit.Sub(targetGasUsed)
		percentageFull := numerator.Quo(denominator)
		adjustmentFactor := k.GetMaxDynamicBaseFeeUpwardAdjustment(ctx).Mul(percentageFull)
		newBaseFee = prevBaseFee.Mul(sdk.NewDec(1).Add(adjustmentFactor))
	} else {
		// downward adjustment
		numerator := targetGasUsed.Sub(blockGasUsedDec)
		denominator := targetGasUsed
		percentageEmpty := numerator.Quo(denominator)
		adjustmentFactor := k.GetMaxDynamicBaseFeeDownwardAdjustment(ctx).Mul(percentageEmpty)
		newBaseFee = prevBaseFee.Mul(sdk.NewDec(1).Sub(adjustmentFactor))
	}

	// Ensure the new base fee is not lower than the minimum fee
	if newBaseFee.LT(minimumFeePerGas) {
		newBaseFee = minimumFeePerGas
	}

	// Ensure the new base fee is not higher than the maximum fee
	if newBaseFee.GT(maximumFeePerGas) {
		newBaseFee = maximumFeePerGas
	}

	// Set the new base fee for the next height
	k.SetNextBaseFeePerGas(ctx, newBaseFee)

	return &newBaseFee
}

// fetchLatestBlock fetches the latest block from the RPC endpoint
func fetchLatestBlock(ctx context.Context, blockNum string) (*Block, error) {
	if blockNum != "latest" {
		blockNumParsed, err := strconv.ParseUint(blockNum, 10, 64)
		if err != nil {
			return nil, fmt.Errorf("failed to parse block number: %w", err)
		}
		blockNum = fmt.Sprintf("0x%x", blockNumParsed)
	}

	rpcResp, err := sendRPCRequest(ctx, "eth_getBlockByNumber", []interface{}{blockNum, true})
	i := 0
	for err != nil && i < retry {
		time.Sleep(sleepTime)
		rpcResp, err = sendRPCRequest(ctx, "eth_getBlockByNumber", []interface{}{blockNum, true})
		if err == nil {
			break
		}
		i++
	}

	if err != nil {
		return nil, fmt.Errorf("failed to fetch block by number: %w", err)
	}

	resultJSON, err := json.Marshal(rpcResp.Result)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal result: %w", err)
	}

	var block Block
	if err := json.Unmarshal(resultJSON, &block); err != nil {
		return nil, fmt.Errorf("failed to unmarshal block: %w", err)
	}

	return &block, nil
}

// fetchBlockByNumber fetches a specific block by number
func fetchBlockByNumber(ctx context.Context, number string) (*Block, error) {

	rpcResp, err := sendRPCRequest(ctx, "eth_getBlockByNumber", []interface{}{number, true})
	i := 0
	for err != nil && i < retry {
		time.Sleep(sleepTime)
		rpcResp, err = sendRPCRequest(ctx, "eth_getBlockByNumber", []interface{}{number, true})
		if err == nil {
			break
		}
		i++
	}

	if err != nil {
		return nil, fmt.Errorf("failed to fetch block by number: %w", err)
	}

	resultJSON, err := json.Marshal(rpcResp.Result)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal result: %w", err)
	}

	var block Block
	if err := json.Unmarshal(resultJSON, &block); err != nil {
		return nil, fmt.Errorf("failed to unmarshal block: %w", err)
	}

	return &block, nil
}

// sendRPCRequest sends a JSON-RPC request and returns the response
func sendRPCRequest(ctx context.Context, method string, params []interface{}) (*RPCResponse, error) {
	req := RPCRequest{
		JSONRPC: "2.0",
		Method:  method,
		Params:  params,
		ID:      1,
	}

	reqBody, err := json.Marshal(req)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal request: %w", err)
	}

	httpReq, err := http.NewRequestWithContext(ctx, "POST", rpcEndpoint, bytes.NewBuffer(reqBody))
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}
	httpReq.Header.Set("Content-Type", "application/json")

	client := &http.Client{}
	resp, err := client.Do(httpReq)
	if err != nil {
		return nil, fmt.Errorf("failed to send request: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
	}

	var rpcResp RPCResponse
	if err := json.NewDecoder(resp.Body).Decode(&rpcResp); err != nil {
		return nil, fmt.Errorf("failed to decode response: %w", err)
	}

	if rpcResp.Error != nil {
		return nil, fmt.Errorf("RPC error: %s", rpcResp.Error.Message)
	}

	return &rpcResp, nil
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    linearCreated by Linear-GitHub Sync

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions