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
26 changes: 4 additions & 22 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,6 @@ import (
tmtypes "github.com/sei-protocol/sei-chain/sei-tendermint/types"
wasmkeeper "github.com/sei-protocol/sei-chain/sei-wasmd/x/wasm/keeper"
"github.com/sei-protocol/sei-chain/utils"
"github.com/sei-protocol/sei-chain/utils/helpers"
"github.com/sei-protocol/sei-chain/utils/metrics"
"github.com/sei-protocol/sei-chain/wasmbinding"
epochmodule "github.com/sei-protocol/sei-chain/x/epoch"
Expand Down Expand Up @@ -1809,7 +1808,7 @@ func (app *App) executeEVMTxWithGigaExecutor(ctx sdk.Context, msg *evmtypes.MsgE
chainID := cache.chainID

// Recover sender using the same logic as preprocess.go (version-based signer selection)
sender, seiAddr, pubkey, recoverErr := evmante.RecoverSenderFromEthTx(ctx, ethTx, chainID)
sender, seiAddr, _, recoverErr := evmante.RecoverSenderFromEthTx(ctx, ethTx, chainID)
if recoverErr != nil {
return &abci.ExecTxResult{
Code: 1,
Expand Down Expand Up @@ -1841,26 +1840,9 @@ func (app *App) executeEVMTxWithGigaExecutor(ctx sdk.Context, msg *evmtypes.MsgE
}

if !isAssociated {
// Set address mapping
app.GigaEvmKeeper.SetAddressMapping(ctx, seiAddr, sender)
// Set pubkey on account if not already set
if acc := app.AccountKeeper.GetAccount(ctx, seiAddr); acc != nil && acc.GetPubKey() == nil {
if err := acc.SetPubKey(pubkey); err != nil {
return &abci.ExecTxResult{
Code: 1,
Log: fmt.Sprintf("failed to set pubkey: %v", err),
}, nil
}
app.AccountKeeper.SetAccount(ctx, acc)
}
// Migrate balance from cast address
associateHelper := helpers.NewAssociationHelper(&app.GigaEvmKeeper, app.GigaBankKeeper, &app.AccountKeeper)
if err := associateHelper.MigrateBalance(ctx, sender, seiAddr, false); err != nil {
return &abci.ExecTxResult{
Code: 1,
Log: fmt.Sprintf("failed to migrate balance: %v", err),
}, nil
}
// Unassociated addresses require balance migration (iterating all balances),
// which giga's cachekv doesn't support. Fall back to v2 for this tx.
return nil, gigaprecompiles.ErrBalanceMigrationRequired
}

// Create state DB for this transaction (only for valid transactions)
Expand Down
68 changes: 1 addition & 67 deletions giga/deps/store/cachekv.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,76 +183,10 @@ func (store *Store) GetAllKeyStrsInRange(start, end []byte) (res []string) {
}

func (store *Store) Iterator(start, end []byte) types.Iterator {
// Get all keys in range (merges parent + cache)
keyStrs := store.GetAllKeyStrsInRange(start, end)
sort.Strings(keyStrs)

// Build key-value pairs
items := make([]kv, 0, len(keyStrs))
for _, k := range keyStrs {
v := store.Get([]byte(k))
if v != nil { // Skip deleted keys
items = append(items, kv{key: []byte(k), value: v})
}
}

return newMemIterator(items)
panic("unexpected iterator call on cachekv store")
}

// ReverseIterator implements types.KVStore.
func (store *Store) ReverseIterator(start, end []byte) types.Iterator {
panic("unexpected reverse iterator call on cachekv store")
}

// kv represents a key-value pair for iteration
type kv struct {
key []byte
value []byte
}

// memIterator iterates over an in-memory slice of key-value pairs
type memIterator struct {
items []kv
index int
}

func newMemIterator(items []kv) *memIterator {
return &memIterator{
items: items,
index: 0,
}
}

func (mi *memIterator) Domain() (start []byte, end []byte) {
return nil, nil
}

func (mi *memIterator) Valid() bool {
return mi.index < len(mi.items)
}

func (mi *memIterator) Next() {
mi.index++
}

func (mi *memIterator) Key() []byte {
if !mi.Valid() {
panic("iterator is invalid")
}
return mi.items[mi.index].key
}

func (mi *memIterator) Value() []byte {
if !mi.Valid() {
panic("iterator is invalid")
}
return mi.items[mi.index].value
}

func (mi *memIterator) Close() error {
return nil
}

func (mi *memIterator) Error() error {
return nil
}
27 changes: 5 additions & 22 deletions giga/deps/xbank/keeper/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,35 +89,18 @@ func (k BaseViewKeeper) LockedCoins(ctx sdk.Context, addr sdk.AccAddress) sdk.Co
return sdk.NewCoins()
}

// GetAllBalances returns all the balances for a given account address.
func (k BaseViewKeeper) GetAllBalances(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins {
accountStore := k.getAccountStore(ctx, addr)

balances := sdk.NewCoins()
iterator := accountStore.Iterator(nil, nil)
defer func() { _ = iterator.Close() }()

for ; iterator.Valid(); iterator.Next() {
var balance sdk.Coin
k.cdc.MustUnmarshal(iterator.Value(), &balance)
balances = append(balances, balance)
}

return balances.Sort()
}

// SpendableCoins returns the total balances of spendable coins for an account
// by address. If the account has no spendable coins, an empty Coins slice is
// returned.
func (k BaseViewKeeper) SpendableCoins(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins {
total := k.GetAllBalances(ctx, addr)
locked := k.LockedCoins(ctx, addr)
total := k.GetBalance(ctx, addr, "usei").Amount
locked := k.LockedCoins(ctx, addr).AmountOf("usei")

spendable, hasNeg := total.SafeSub(locked)
if hasNeg {
spendable := total.Sub(locked)
if spendable.IsNegative() {
return sdk.NewCoins()
}
return spendable
return sdk.NewCoins(sdk.NewCoin("usei", spendable))
}

// getAccountStore gets the account store of the given address.
Expand Down
15 changes: 15 additions & 0 deletions giga/executor/precompiles/failfast.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,21 @@ func (e *InvalidPrecompileCallError) IsAbortError() bool {
// It implements vm.AbortError to ensure it propagates through the call stack.
var ErrInvalidPrecompileCall error = &InvalidPrecompileCallError{}

// BalanceMigrationAbortError signals that the transaction requires balance
// migration (unassociated address), which giga cannot handle. The caller
// should fall back to v2.
type BalanceMigrationAbortError struct{}

func (e *BalanceMigrationAbortError) Error() string {
return "balance migration required for unassociated address"
}

func (e *BalanceMigrationAbortError) IsAbortError() bool {
return true
}

var ErrBalanceMigrationRequired error = &BalanceMigrationAbortError{}

type FailFastPrecompile struct{}

var FailFastSingleton vm.PrecompiledContract = &FailFastPrecompile{}
Expand Down
111 changes: 111 additions & 0 deletions giga/tests/giga_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2322,3 +2322,114 @@ func TestGigaValidation_AllModes(t *testing.T) {
})
}
}

// createUnassociatedEVMTx creates a signed EVM transfer without calling SetAddressMapping,
// so the sender remains unassociated and triggers balance migration on first use.
func createUnassociatedEVMTx(t testing.TB, tCtx *GigaTestContext, signer utils.TestAcct, to common.Address, value *big.Int, nonce uint64) []byte {
tc := app.MakeEncodingConfig().TxConfig

signedTx, err := ethtypes.SignTx(ethtypes.NewTx(&ethtypes.DynamicFeeTx{
GasFeeCap: new(big.Int).SetUint64(100000000000),
GasTipCap: new(big.Int).SetUint64(100000000000),
Gas: 21000,
ChainID: big.NewInt(config.DefaultChainID),
To: &to,
Value: value,
Nonce: nonce,
}), signer.EvmSigner, signer.EvmPrivateKey)
require.NoError(t, err)

txData, err := ethtx.NewTxDataFromTx(signedTx)
require.NoError(t, err)

msg, err := types.NewMsgEVMTransaction(txData)
require.NoError(t, err)

txBuilder := tc.NewTxBuilder()
err = txBuilder.SetMsgs(msg)
require.NoError(t, err)
txBuilder.SetGasLimit(10000000000)

txBytes, err := tc.TxEncoder()(txBuilder.GetTx())
require.NoError(t, err)

return txBytes
}

// TestGigaVsGeth_BalanceMigrationMultipleDenoms verifies that when giga encounters an
// unassociated address holding multiple token denominations, it correctly aborts and
// falls back to v2, which performs the full balance migration. The results must match
// a pure v2 execution for consensus parity.
func TestGigaVsGeth_BalanceMigrationMultipleDenoms(t *testing.T) {
blockTime := time.Now()
accts := utils.NewTestAccounts(5)

signer := utils.NewSigner()
recipient := utils.NewSigner()

// The cast address is the EVM address bytes interpreted as a Cosmos address.
// Before association, funds sent to this "address" need to be migrated.
castAddr := sdk.AccAddress(signer.EvmAddress[:])

// Fund the cast address with MULTIPLE denominations — this is the key scenario.
// Balance migration must move all denoms, not just usei.
multiDenomCoins := sdk.NewCoins(
sdk.NewCoin("usei", sdk.NewInt(1000000000000000000)), // 1e18 usei (enough for gas + transfer)
sdk.NewCoin("uusdc", sdk.NewInt(500000000)), // 500 uusdc
)

// --- V2 baseline (handles migration natively) ---
v2Ctx := NewGigaTestContext(t, accts, blockTime, 1, ModeV2Sequential)
err := v2Ctx.TestApp.BankKeeper.MintCoins(v2Ctx.Ctx, "mint", multiDenomCoins)
require.NoError(t, err)
err = v2Ctx.TestApp.BankKeeper.SendCoinsFromModuleToAccount(v2Ctx.Ctx, "mint", castAddr, multiDenomCoins)
require.NoError(t, err)
// DO NOT associate — this forces the preprocess ante handler to migrate balances

v2Tx := createUnassociatedEVMTx(t, v2Ctx, signer, recipient.EvmAddress, big.NewInt(1000), 0)
_, v2Results, v2Err := RunBlock(t, v2Ctx, [][]byte{v2Tx})
require.NoError(t, v2Err)
require.Len(t, v2Results, 1)

// --- Giga Sequential (should abort and fall back to v2) ---
gigaCtx := NewGigaTestContext(t, accts, blockTime, 1, ModeGigaSequential)
err = gigaCtx.TestApp.BankKeeper.MintCoins(gigaCtx.Ctx, "mint", multiDenomCoins)
require.NoError(t, err)
err = gigaCtx.TestApp.BankKeeper.SendCoinsFromModuleToAccount(gigaCtx.Ctx, "mint", castAddr, multiDenomCoins)
require.NoError(t, err)
// DO NOT associate — giga should detect this and abort

gigaTx := createUnassociatedEVMTx(t, gigaCtx, signer, recipient.EvmAddress, big.NewInt(1000), 0)
_, gigaResults, gigaErr := RunBlock(t, gigaCtx, [][]byte{gigaTx})
require.NoError(t, gigaErr)
require.Len(t, gigaResults, 1)

// Both should succeed — giga fell back to v2 which handled migration
require.Equal(t, uint32(0), v2Results[0].Code, "V2 tx should succeed: %s", v2Results[0].Log)
require.Equal(t, uint32(0), gigaResults[0].Code, "Giga tx should succeed (via v2 fallback): %s", gigaResults[0].Log)

// Consensus-critical: deterministic fields must match
require.Equal(t, v2Results[0].Code, gigaResults[0].Code, "Code mismatch")
require.Equal(t, v2Results[0].GasUsed, gigaResults[0].GasUsed, "GasUsed mismatch")
require.Equal(t, v2Results[0].GasWanted, gigaResults[0].GasWanted, "GasWanted mismatch")
CompareLastResultsHash(t, "BalanceMigrationMultipleDenoms", v2Results, gigaResults)

// Verify migration happened in V2: cast address should be empty,
// real sei address should hold the non-usei denoms.
v2CastUsdc := v2Ctx.TestApp.BankKeeper.GetBalance(v2Ctx.Ctx, castAddr, "uusdc")
v2RealUsdc := v2Ctx.TestApp.BankKeeper.GetBalance(v2Ctx.Ctx, signer.AccountAddress, "uusdc")
require.True(t, v2CastUsdc.IsZero(), "V2: cast address uusdc should be 0 after migration, got %s", v2CastUsdc)
require.Equal(t, int64(500000000), v2RealUsdc.Amount.Int64(),
"V2: real sei address should hold migrated uusdc")

// Verify migration also happened in the Giga (v2-fallback) path
gigaCastUsdc := gigaCtx.TestApp.BankKeeper.GetBalance(gigaCtx.Ctx, castAddr, "uusdc")
gigaRealUsdc := gigaCtx.TestApp.BankKeeper.GetBalance(gigaCtx.Ctx, signer.AccountAddress, "uusdc")
require.True(t, gigaCastUsdc.IsZero(), "Giga: cast address uusdc should be 0 after migration, got %s", gigaCastUsdc)
require.Equal(t, int64(500000000), gigaRealUsdc.Amount.Int64(),
"Giga: real sei address should hold migrated uusdc")

t.Logf("Balance migration with multiple denoms verified: V2 and Giga (v2-fallback) produce identical results")
t.Logf(" V2 cast uusdc: %s, real uusdc: %s", v2CastUsdc, v2RealUsdc)
t.Logf(" Giga cast uusdc: %s, real uusdc: %s", gigaCastUsdc, gigaRealUsdc)
}
Loading