Skip to content
Closed
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
288 changes: 177 additions & 111 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"sync"
"time"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/tracing"
ethtypes "github.com/ethereum/go-ethereum/core/types"
Expand Down Expand Up @@ -151,6 +152,7 @@ import (
"github.com/sei-protocol/sei-chain/x/evm/querier"
"github.com/sei-protocol/sei-chain/x/evm/replay"
evmtypes "github.com/sei-protocol/sei-chain/x/evm/types"
evmethtx "github.com/sei-protocol/sei-chain/x/evm/types/ethtx"
"github.com/sei-protocol/sei-chain/x/mint"
mintclient "github.com/sei-protocol/sei-chain/x/mint/client/cli"
mintkeeper "github.com/sei-protocol/sei-chain/x/mint/keeper"
Expand Down Expand Up @@ -178,7 +180,6 @@ import (
gigabankkeeper "github.com/sei-protocol/sei-chain/giga/deps/xbank/keeper"
gigaevmkeeper "github.com/sei-protocol/sei-chain/giga/deps/xevm/keeper"
gigaevmstate "github.com/sei-protocol/sei-chain/giga/deps/xevm/state"
"github.com/sei-protocol/sei-chain/giga/deps/xevm/types/ethtx"
)

// this line is used by starport scaffolding # stargate/wasm/app/enabledProposals
Expand Down Expand Up @@ -1747,122 +1748,25 @@ func (app *App) executeEVMTxWithGigaExecutor(ctx sdk.Context, msg *evmtypes.MsgE

_, isAssociated := app.GigaEvmKeeper.GetEVMAddress(ctx, seiAddr)

// ============================================================================
// Nonce validation (mirrors V2's ante handler check in x/evm/ante/sig.go)
// V2 rejects with ErrWrongSequence if txNonce != expectedNonce, with NO state changes.
// ============================================================================
expectedNonce := app.GigaEvmKeeper.GetNonce(ctx, sender)
txNonce := ethTx.Nonce()
if txNonce != expectedNonce {
nonceDirection := "too high"
if txNonce < expectedNonce {
nonceDirection = "too low"
}
return &abci.ExecTxResult{
Code: sdkerrors.ErrWrongSequence.ABCICode(),
GasWanted: int64(ethTx.Gas()), //nolint:gosec
Log: fmt.Sprintf("nonce %s: address %s, tx: %d state: %d: %s", nonceDirection, sender.Hex(), txNonce, expectedNonce, sdkerrors.ErrWrongSequence.Error()),
}, nil
}

// ============================================================================
// Fee validation (mirrors V2's ante handler checks in evm_checktx.go)
// NOTE: In V2, failed transactions still increment nonce and charge gas.
// We track validation errors here but don't return early - we still need to
// create stateDB, increment nonce, and finalize state to match V2 behavior.
// ============================================================================
baseFee := app.GigaEvmKeeper.GetBaseFee(ctx)
if baseFee == nil {
baseFee = new(big.Int) // default to 0 when base fee is unset
}

// Track validation errors - we'll skip execution but still finalize state
var validationErr *abci.ExecTxResult

// 1. Fee cap < base fee check (INSUFFICIENT_MAX_FEE_PER_GAS)
// V2: evm_checktx.go line 284-286
if txData.GetGasFeeCap().Cmp(baseFee) < 0 {
validationErr = &abci.ExecTxResult{
Code: sdkerrors.ErrInsufficientFee.ABCICode(),
Log: "max fee per gas less than block base fee",
}
}

// 2. Tip > fee cap check (PRIORITY_GREATER_THAN_MAX_FEE_PER_GAS)
// This is checked in txData.Validate() for DynamicFeeTx, but we also check here
// to ensure consistent rejection before execution.
if validationErr == nil && txData.GetGasTipCap().Cmp(txData.GetGasFeeCap()) > 0 {
validationErr = &abci.ExecTxResult{
Code: 1,
Log: "max priority fee per gas higher than max fee per gas",
}
}

// 3. Gas limit * gas price overflow check (GASLIMIT_PRICE_PRODUCT_OVERFLOW)
// V2: Uses IsValidInt256(tx.Fee()) in dynamic_fee_tx.go Validate()
// Fee = GasFeeCap * GasLimit, must fit in 256 bits
if validationErr == nil && !ethtx.IsValidInt256(txData.Fee()) {
validationErr = &abci.ExecTxResult{
Code: 1,
Log: "fee out of bound",
}
}

// 4. TX gas limit > block gas limit check (GAS_ALLOWANCE_EXCEEDED)
// V2: x/evm/ante/basic.go lines 63-68
if validationErr == nil {
if cp := ctx.ConsensusParams(); cp != nil && cp.Block != nil {
if cp.Block.MaxGas > 0 && ethTx.Gas() > uint64(cp.Block.MaxGas) { //nolint:gosec
validationErr = &abci.ExecTxResult{
Code: sdkerrors.ErrOutOfGas.ABCICode(),
Log: fmt.Sprintf("tx gas limit %d exceeds block max gas %d", ethTx.Gas(), cp.Block.MaxGas),
}
}
}
}

// 5. Insufficient balance check for gas * price + value (INSUFFICIENT_FUNDS_FOR_TRANSFER)
if validationErr == nil {
// BuyGas checks balance against GasLimit * GasFeeCap + Value (see go-ethereum/core/state_transition.go:264-291)
balanceCheck := new(big.Int).Mul(new(big.Int).SetUint64(ethTx.Gas()), ethTx.GasFeeCap())
balanceCheck.Add(balanceCheck, ethTx.Value())

senderBalance := app.GigaEvmKeeper.GetBalance(ctx, seiAddr)

// For unassociated addresses, V2's PreprocessDecorator migrates the cast address balance
// BEFORE the fee check (in a CacheMultiStore). We need to include the cast address balance
// in our check to match V2's behavior, even though we defer the actual migration.
if !isAssociated {
// Cast address is the EVM address bytes interpreted as a Sei address
castAddr := sdk.AccAddress(sender[:])
castBalance := app.GigaEvmKeeper.GetBalance(ctx, castAddr)
senderBalance = new(big.Int).Add(senderBalance, castBalance)
}

if senderBalance.Cmp(balanceCheck) < 0 {
validationErr = &abci.ExecTxResult{
Code: sdkerrors.ErrInsufficientFunds.ABCICode(),
Log: fmt.Sprintf("insufficient funds for gas * price + value: address %s have %v want %v: insufficient funds", sender.Hex(), senderBalance, balanceCheck),
}
}
}
// Run validation checks (ordered to match V2's priority)
validation := app.validateGigaEVMTx(ctx, txData, sender, seiAddr, isAssociated)

// Prepare context for EVM transaction (set infinite gas meter like original flow)
ctx = ctx.WithGasMeter(sdk.NewInfiniteGasMeterWithMultiplier(ctx))

// If validation failed, increment nonce via keeper (matching V2's DeliverTxCallback behavior
// in x/evm/ante/basic.go). V2 does NOT create stateDB or handle surplus for early failures.
if validationErr != nil {
// Match V2 error handling: bump nonce directly via keeper (not stateDB)
currentNonce := app.GigaEvmKeeper.GetNonce(ctx, sender)
app.GigaEvmKeeper.SetNonce(ctx, sender, currentNonce+1)

if validation.err != nil {
// Validation failed - bump nonce via keeper if it was valid (matches V2's DeliverTxCallback
// behavior where nonce is incremented even on fee validation failures).
// For successful txs, the nonce is bumped by the EVM during execution.
if validation.bumpNonce {
app.GigaEvmKeeper.SetNonce(ctx, sender, validation.currentNonce+1)
}
// V2 reports intrinsic gas as gasUsed even on validation failure (for metrics),
// but no actual balance is deducted
intrinsicGas, _ := core.IntrinsicGas(ethTx.Data(), ethTx.AccessList(), ethTx.SetCodeAuthorizations(), ethTx.To() == nil, true, true, true)
validationErr.GasUsed = int64(intrinsicGas) //nolint:gosec
validationErr.GasWanted = int64(ethTx.Gas()) //nolint:gosec
return validationErr, nil
validation.err.GasUsed = int64(intrinsicGas) //nolint:gosec
validation.err.GasWanted = int64(ethTx.Gas()) //nolint:gosec
return validation.err, nil
}

if !isAssociated {
Expand Down Expand Up @@ -1896,7 +1800,7 @@ func (app *App) executeEVMTxWithGigaExecutor(ctx sdk.Context, msg *evmtypes.MsgE
// V2 charges fees in the ante handler, then runs the EVM with feeAlreadyCharged=true
// which skips buyGas/refundGas/coinbase. Without this, GasUsed differs between Giga
// and V2, causing LastResultsHash → AppHash divergence.
effectiveGasPrice := new(big.Int).Add(new(big.Int).Set(ethTx.GasTipCap()), baseFee)
effectiveGasPrice := new(big.Int).Add(new(big.Int).Set(ethTx.GasTipCap()), validation.baseFee)
if effectiveGasPrice.Cmp(ethTx.GasFeeCap()) > 0 {
effectiveGasPrice.Set(ethTx.GasFeeCap())
}
Expand Down Expand Up @@ -2564,6 +2468,168 @@ func (app *App) inplacetestnetInitializer(pk cryptotypes.PubKey) error {
return nil
}

// gigaValidationResult holds the result of EVM transaction validation.
type gigaValidationResult struct {
err *abci.ExecTxResult // nil if validation passed
bumpNonce bool // true if tx nonce matches expected nonce
currentNonce uint64 // the expected nonce at time of validation
baseFee *big.Int // the base fee used for validation
}

// validateGigaEVMTx validates an EVM tx, returning the first error found.
// Checks are ordered to match V2's EvmDeliverTxAnte flow:
//
// 1. EvmStatelessChecks (BEFORE nonce callback) - errors here do NOT bump nonce
// - Gas limit > block max
// - GasTipCap < 0
//
// 2. DecorateNonceCallback - sets up nonce callback if nonce matches
//
// 3. EvmDeliverChargeFees (AFTER nonce callback) - errors here DO bump nonce
// - Fee cap < base fee
// - Balance check
func (app *App) validateGigaEVMTx(
ctx sdk.Context,
txData evmethtx.TxData,
sender common.Address,
seiAddr sdk.AccAddress,
isAssociated bool,
) gigaValidationResult {
baseFee := app.GigaEvmKeeper.GetBaseFee(ctx)
if baseFee == nil {
baseFee = new(big.Int)
}

bumpNonce := false

// ========================================================================
// Phase 1: EvmStatelessChecks (BEFORE nonce callback in V2)
// If these fail, return immediately with nonceValid=false → no nonce bump
// ========================================================================

// 1. Gas limit exceeds block max
if cp := ctx.ConsensusParams(); cp != nil && cp.Block != nil {
if cp.Block.MaxGas > 0 && txData.GetGas() > uint64(cp.Block.MaxGas) { //nolint:gosec
return gigaValidationResult{
err: &abci.ExecTxResult{
Code: sdkerrors.ErrOutOfGas.ABCICode(),
Log: fmt.Sprintf("tx gas limit %d exceeds block max gas %d", txData.GetGas(), cp.Block.MaxGas),
},
bumpNonce: bumpNonce,
currentNonce: 0,
baseFee: baseFee,
}
}
}

// 2. Negative gas tip cap
if txData.GetGasTipCap().Sign() < 0 {
return gigaValidationResult{
err: &abci.ExecTxResult{
Code: sdkerrors.ErrInvalidRequest.ABCICode(),
Log: "gas tip cap cannot be negative",
},
bumpNonce: bumpNonce,
currentNonce: 0,
baseFee: baseFee,
}
}

// ========================================================================
// Phase 2: DecorateNonceCallback equivalent - check nonce validity
// ========================================================================
currentNonce := app.GigaEvmKeeper.GetNonce(ctx, sender)
txNonce := txData.GetNonce()
bumpNonce = txNonce == currentNonce

// ========================================================================
// Phase 3: EvmDeliverChargeFees (AFTER nonce callback in V2)
// Calls EvmCheckAndChargeFees which checks in order:
// 1. Fee cap < base fee
// 2. Fee cap < minimum fee
// 3. StatelessChecks (nonce)
// 4. BuyGas (balance)
// If these fail AND nonce was valid → nonce will bump
// ========================================================================

// 3. Fee cap below base fee
if txData.GetGasFeeCap().Cmp(baseFee) < 0 {
return gigaValidationResult{
err: &abci.ExecTxResult{
Code: sdkerrors.ErrInsufficientFee.ABCICode(),
Log: "max fee per gas less than block base fee",
},
bumpNonce: bumpNonce,
currentNonce: currentNonce,
baseFee: baseFee,
}
}

// 4. Fee cap below minimum fee
minimumFee := app.GigaEvmKeeper.GetMinimumFeePerGas(ctx).TruncateInt().BigInt()
if txData.GetGasFeeCap().Cmp(minimumFee) < 0 {
return gigaValidationResult{
err: &abci.ExecTxResult{
Code: sdkerrors.ErrInsufficientFee.ABCICode(),
Log: "max fee per gas less than minimum fee",
},
bumpNonce: bumpNonce,
currentNonce: currentNonce,
baseFee: baseFee,
}
}

// 5. Invalid nonce (via StatelessChecks in V2)
if txNonce != currentNonce {
nonceDirection := "too high"
if txNonce < currentNonce {
nonceDirection = "too low"
}
return gigaValidationResult{
err: &abci.ExecTxResult{
Code: sdkerrors.ErrWrongSequence.ABCICode(),
Log: fmt.Sprintf("nonce %s: address %s, tx: %d state: %d: %s", nonceDirection, sender.Hex(), txNonce, currentNonce, sdkerrors.ErrWrongSequence.Error()),
},
bumpNonce: bumpNonce,
currentNonce: currentNonce,
baseFee: baseFee,
}
}

// 6. Insufficient balance for gas + value (via BuyGas in V2)
balanceCheck := new(big.Int).Mul(new(big.Int).SetUint64(txData.GetGas()), txData.GetGasFeeCap())
balanceCheck.Add(balanceCheck, txData.GetValue())

senderBalance := app.GigaEvmKeeper.GetBalance(ctx, seiAddr)

// Include cast address balance for unassociated addresses (matches V2 PreprocessDecorator)
if !isAssociated {
castAddr := sdk.AccAddress(sender[:])
castBalance := app.GigaEvmKeeper.GetBalance(ctx, castAddr)
senderBalance = new(big.Int).Add(senderBalance, castBalance)
}

if senderBalance.Cmp(balanceCheck) < 0 {
return gigaValidationResult{
err: &abci.ExecTxResult{
Code: sdkerrors.ErrInsufficientFunds.ABCICode(),
Log: fmt.Sprintf("insufficient funds for gas * price + value: address %s have %v want %v: insufficient funds", sender.Hex(), senderBalance, balanceCheck),
},
bumpNonce: bumpNonce,
currentNonce: currentNonce,
baseFee: baseFee,
}
}

// All checks passed
return gigaValidationResult{
err: nil,
bumpNonce: bumpNonce,
currentNonce: currentNonce,
baseFee: baseFee,
}
}

func init() {
// override max wasm size to 2MB
wasmtypes.MaxWasmSize = 2 * 1024 * 1024
Expand Down
Loading