diff --git a/app/app.go b/app/app.go index d335cc432d..99e3e57174 100644 --- a/app/app.go +++ b/app/app.go @@ -1911,12 +1911,16 @@ func (app *App) executeEVMTxWithGigaExecutor(ctx sdk.Context, msg *evmtypes.MsgE vmError = execResult.Err.Error() } - // Create core.Message from ethTx for WriteReceipt - // WriteReceipt needs msg for GasPrice, To, From, Data, Nonce fields + // Create core.Message from ethTx for WriteReceipt. + // GasPrice must be the EIP-1559 effective gas price (min(baseFee+tip, + // maxFee)) — that's what the chain actually charges (see line 1866) + // and what the receipt's EffectiveGasPrice field needs to report. + // ethTx.GasPrice() returns GasFeeCap for dynamic-fee txs, which puts + // the wrong value on the receipt and breaks EIP-1559 clients. evmMsg := &core.Message{ Nonce: ethTx.Nonce(), GasLimit: ethTx.Gas(), - GasPrice: ethTx.GasPrice(), + GasPrice: effectiveGasPrice, GasFeeCap: ethTx.GasFeeCap(), GasTipCap: ethTx.GasTipCap(), To: ethTx.To(), diff --git a/giga/deps/xevm/keeper/receipt_test.go b/giga/deps/xevm/keeper/receipt_test.go index a11fdf1584..a6ea19e48f 100644 --- a/giga/deps/xevm/keeper/receipt_test.go +++ b/giga/deps/xevm/keeper/receipt_test.go @@ -1,11 +1,14 @@ package keeper_test import ( + "math/big" "testing" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" ethtypes "github.com/ethereum/go-ethereum/core/types" testkeeper "github.com/sei-protocol/sei-chain/giga/deps/testutil/keeper" + "github.com/sei-protocol/sei-chain/giga/deps/xevm/state" "github.com/sei-protocol/sei-chain/giga/deps/xevm/types" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" "github.com/stretchr/testify/require" @@ -39,3 +42,49 @@ func TestDeleteTransientReceipt(t *testing.T) { require.Nil(t, receipt) require.Equal(t, "receipt not found", err.Error()) } + +// TestWriteReceiptStoresMsgGasPriceAsEffectiveGasPrice documents the contract +// between WriteReceipt and its caller for EIP-1559 dynamic-fee txs: +// receipt.EffectiveGasPrice is taken from msg.GasPrice verbatim, so the caller +// is responsible for setting msg.GasPrice = min(baseFee + tipCap, feeCap) +// (the actual effective gas price the chain charged). +// +// The bug this guards against: passing ethTx.GasPrice() into msg for a +// dynamic-fee tx returns GasFeeCap (i.e., maxFee) — the receipt would then +// report maxFee even when the tx actually paid baseFee+tip < maxFee, breaking +// EIP-1559 RPC semantics for clients (e.g. ethers, hardhat-ethers). +func TestWriteReceiptStoresMsgGasPriceAsEffectiveGasPrice(t *testing.T) { + k, ctx := testkeeper.MockEVMKeeper(t) + stateDB := state.NewDBImpl(ctx, k, false) + txHash := common.HexToHash("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef") + + // Scenario: tip=1gwei, maxFee=10gwei, baseFee=1gwei → + // effectiveGasPrice = baseFee + tip = 2gwei (additive branch). + const ( + effectiveGasPrice uint64 = 2_000_000_000 // 2 gwei + feeCap uint64 = 10_000_000_000 // 10 gwei + tipCap uint64 = 1_000_000_000 // 1 gwei + gasUsed uint64 = 21_000 + ) + + msg := &core.Message{ + Nonce: 0, + GasLimit: gasUsed, + GasPrice: new(big.Int).SetUint64(effectiveGasPrice), + GasFeeCap: new(big.Int).SetUint64(feeCap), + GasTipCap: new(big.Int).SetUint64(tipCap), + To: nil, + Value: big.NewInt(0), + From: common.HexToAddress("0x000000000000000000000000000000000000beef"), + } + + r, err := k.WriteReceipt(ctx, stateDB, msg, uint32(ethtypes.DynamicFeeTxType), txHash, gasUsed, "") + require.NoError(t, err) + require.NotNil(t, r) + require.Equal(t, effectiveGasPrice, r.EffectiveGasPrice, + "receipt.EffectiveGasPrice must equal msg.GasPrice; if the caller passes "+ + "ethTx.GasPrice() for a dynamic-fee tx, the receipt would wrongly report GasFeeCap (%d) "+ + "instead of the EIP-1559 effective gas price (%d)", feeCap, effectiveGasPrice) + require.Equal(t, uint32(ethtypes.ReceiptStatusSuccessful), r.Status) + require.Equal(t, gasUsed, r.GasUsed) +}