From 06ac58b26aaf9c3cd7f671f65f3122c4e886f97a Mon Sep 17 00:00:00 2001 From: "Masih H. Derkani" Date: Fri, 22 May 2026 16:20:20 +0100 Subject: [PATCH] Refine address mapping for delegations and rebinds Simplify the address-mapping and code-storage helpers in x/evm/keeper and the parallel giga/deps/xevm/keeper package. Comments document the non-obvious invariants (the three-way GetCodeHash semantics, the bidirectional index contract, the cast-address receive rule), magic constants are named, and a few stale branches in SetCode and SetAddressMapping are simplified along the way. Expand the keeper test suites on both sides to cover behaviours that were previously implicit: SetCode with delegation-shaped input, SetAddressMapping when either side is rebound to a new partner, and GetCodeHash for accounts with a nonce but no balance. The two keeper packages share underlying storage via GigaEvmKeeper.UseRegularStore=true, so coverage is mirrored to keep them from drifting. Mirror both fixes into giga/deps/xevm/keeper. --- giga/deps/xevm/keeper/address.go | 96 +++++++++++----- giga/deps/xevm/keeper/address_test.go | 156 ++++++++++++++++---------- giga/deps/xevm/keeper/code.go | 88 +++++++++++---- giga/deps/xevm/keeper/code_test.go | 44 +++++++- x/evm/keeper/address.go | 96 +++++++++++----- x/evm/keeper/address_test.go | 156 ++++++++++++++++---------- x/evm/keeper/code.go | 88 +++++++++++---- x/evm/keeper/code_test.go | 45 +++++++- 8 files changed, 543 insertions(+), 226 deletions(-) diff --git a/giga/deps/xevm/keeper/address.go b/giga/deps/xevm/keeper/address.go index 68bdc9212f..db2162a81c 100644 --- a/giga/deps/xevm/keeper/address.go +++ b/giga/deps/xevm/keeper/address.go @@ -1,16 +1,49 @@ package keeper import ( + "bytes" + "github.com/ethereum/go-ethereum/common" "github.com/sei-protocol/sei-chain/giga/deps/xevm/types" "github.com/sei-protocol/sei-chain/sei-cosmos/store/prefix" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" ) +// SetAddressMapping binds a Sei address to an EVM address bidirectionally. +// +// The mapping is maintained as two keys (forward: sei→evm, reverse: evm→sei). +// When either side is being rebound to a new partner, we must also clear the +// OLD partner's stale half of the mapping — otherwise the old address would +// continue to resolve to its former partner via one direction while the new +// binding wins in the other, leaving the index permanently inconsistent. +// +// The defensive `bytes.Equal` / `Equals` checks before deletion guard against +// already-inconsistent state: we only delete the stale half if it still +// points back to the address we're rebinding away from. func (k *Keeper) SetAddressMapping(ctx sdk.Context, seiAddress sdk.AccAddress, evmAddress common.Address) { store := k.GetKVStore(ctx) - store.Set(types.EVMAddressToSeiAddressKey(evmAddress), seiAddress) - store.Set(types.SeiAddressToEVMAddressKey(seiAddress), evmAddress[:]) + evmKey := types.EVMAddressToSeiAddressKey(evmAddress) + seiKey := types.SeiAddressToEVMAddressKey(seiAddress) + + // If evmAddress was previously bound to a different Sei address, + // clear that Sei address's stale forward mapping. + if prevSei := store.Get(evmKey); prevSei != nil && !sdk.AccAddress(prevSei).Equals(seiAddress) { + prevSeiKey := types.SeiAddressToEVMAddressKey(prevSei) + if bytes.Equal(store.Get(prevSeiKey), evmAddress[:]) { + store.Delete(prevSeiKey) + } + } + // If seiAddress was previously bound to a different EVM address, + // clear that EVM address's stale reverse mapping. + if prevEvm := store.Get(seiKey); prevEvm != nil && !bytes.Equal(prevEvm, evmAddress[:]) { + prevEvmKey := types.EVMAddressToSeiAddressKey(common.BytesToAddress(prevEvm)) + if prevReverseSei := store.Get(prevEvmKey); prevReverseSei != nil && sdk.AccAddress(prevReverseSei).Equals(seiAddress) { + store.Delete(prevEvmKey) + } + } + + store.Set(evmKey, seiAddress) + store.Set(seiKey, evmAddress[:]) if !k.accountKeeper.HasAccount(ctx, seiAddress) { k.accountKeeper.SetAccount(ctx, k.accountKeeper.NewAccountWithAddress(ctx, seiAddress)) } @@ -21,6 +54,9 @@ func (k *Keeper) SetAddressMapping(ctx sdk.Context, seiAddress sdk.AccAddress, e )) } +// DeleteAddressMapping removes both directions of the binding. +// The caller is responsible for passing a (sei, evm) pair that was actually +// bound together — passing a mismatched pair will corrupt the indexes. func (k *Keeper) DeleteAddressMapping(ctx sdk.Context, seiAddress sdk.AccAddress, evmAddress common.Address) { store := k.GetKVStore(ctx) store.Delete(types.EVMAddressToSeiAddressKey(evmAddress)) @@ -28,61 +64,67 @@ func (k *Keeper) DeleteAddressMapping(ctx sdk.Context, seiAddress sdk.AccAddress } func (k *Keeper) GetEVMAddress(ctx sdk.Context, seiAddress sdk.AccAddress) (common.Address, bool) { - store := k.GetKVStore(ctx) - bz := store.Get(types.SeiAddressToEVMAddressKey(seiAddress)) - addr := common.Address{} + bz := k.GetKVStore(ctx).Get(types.SeiAddressToEVMAddressKey(seiAddress)) if bz == nil { - return addr, false + return common.Address{}, false } + var addr common.Address copy(addr[:], bz) return addr, true } func (k *Keeper) GetEVMAddressOrDefault(ctx sdk.Context, seiAddress sdk.AccAddress) common.Address { - addr, ok := k.GetEVMAddress(ctx, seiAddress) - if ok { + if addr, ok := k.GetEVMAddress(ctx, seiAddress); ok { return addr } return common.BytesToAddress(seiAddress) } func (k *Keeper) GetSeiAddress(ctx sdk.Context, evmAddress common.Address) (sdk.AccAddress, bool) { - store := k.GetKVStore(ctx) - bz := store.Get(types.EVMAddressToSeiAddressKey(evmAddress)) + bz := k.GetKVStore(ctx).Get(types.EVMAddressToSeiAddressKey(evmAddress)) if bz == nil { - return []byte{}, false + return nil, false } return bz, true } func (k *Keeper) GetSeiAddressOrDefault(ctx sdk.Context, evmAddress common.Address) sdk.AccAddress { - addr, ok := k.GetSeiAddress(ctx, evmAddress) - if ok { + if addr, ok := k.GetSeiAddress(ctx, evmAddress); ok { return addr } return sdk.AccAddress(evmAddress[:]) } -func (k *Keeper) IterateSeiAddressMapping(ctx sdk.Context, cb func(evmAddr common.Address, seiAddr sdk.AccAddress) bool) { +// IterateSeiAddressMapping walks every (evm, sei) pair in the address index. +// The callback returns `stop=true` to halt iteration early. +func (k *Keeper) IterateSeiAddressMapping(ctx sdk.Context, cb func(evmAddr common.Address, seiAddr sdk.AccAddress) (stop bool)) { iter := prefix.NewStore(k.GetKVStore(ctx), types.EVMAddressToSeiAddressKeyPrefix).Iterator(nil, nil) defer func() { _ = iter.Close() }() for ; iter.Valid(); iter.Next() { evmAddr := common.BytesToAddress(iter.Key()) seiAddr := sdk.AccAddress(iter.Value()) if cb(evmAddr, seiAddr) { - break + return } } } -// A sdk.AccAddress may not receive funds from bank if it's the result of direct-casting -// from an EVM address AND the originating EVM address has already been associated with -// a true (i.e. derived from the same pubkey) sdk.AccAddress. +// CanAddressReceive reports whether bank may credit `addr`. +// +// EVM and Sei addresses are both 20 bytes, so any Sei address can be +// interpreted as the byte-cast of an EVM address. That cast is permitted +// as a recipient EXCEPT when the underlying EVM address has already been +// associated with a real, pubkey-derived Sei address — in that case, funds +// sent to the cast form would be stranded outside the real account, so we +// reject it. The carve-out for `associatedAddr.Equals(addr)` permits EVM +// contracts and other cases where the cast IS the canonical Sei side. func (k *Keeper) CanAddressReceive(ctx sdk.Context, addr sdk.AccAddress) bool { - directCast := common.BytesToAddress(addr) // casting goes both directions since both address formats have 20 bytes + directCast := common.BytesToAddress(addr) associatedAddr, isAssociated := k.GetSeiAddress(ctx, directCast) - // if the associated address is the cast address itself, allow the address to receive (e.g. EVM contract addresses) - return associatedAddr.Equals(addr) || !isAssociated // this means it's either a cast address that's not associated yet, or not a cast address at all. + if !isAssociated { + return true // not a cast form, or a cast that hasn't been associated yet + } + return associatedAddr.Equals(addr) } type EvmAddressHandler struct { @@ -93,14 +135,12 @@ func NewEvmAddressHandler(evmKeeper *Keeper) EvmAddressHandler { return EvmAddressHandler{evmKeeper: evmKeeper} } +// GetSeiAddressFromString resolves a string-encoded address (either 0x-hex +// or bech32) to its Sei address form. Hex inputs go through the keeper's +// associated/cast resolution; bech32 inputs are returned as-is. func (h EvmAddressHandler) GetSeiAddressFromString(ctx sdk.Context, address string) (sdk.AccAddress, error) { if common.IsHexAddress(address) { - parsedAddress := common.HexToAddress(address) - return h.evmKeeper.GetSeiAddressOrDefault(ctx, parsedAddress), nil - } - parsedAddress, err := sdk.AccAddressFromBech32(address) - if err != nil { - return nil, err + return h.evmKeeper.GetSeiAddressOrDefault(ctx, common.HexToAddress(address)), nil } - return parsedAddress, nil + return sdk.AccAddressFromBech32(address) } diff --git a/giga/deps/xevm/keeper/address_test.go b/giga/deps/xevm/keeper/address_test.go index a54a151814..c07b10eb49 100644 --- a/giga/deps/xevm/keeper/address_test.go +++ b/giga/deps/xevm/keeper/address_test.go @@ -1,7 +1,6 @@ package keeper_test import ( - "bytes" "testing" "github.com/sei-protocol/sei-chain/giga/deps/testutil/keeper" @@ -13,11 +12,16 @@ import ( func TestSetGetAddressMapping(t *testing.T) { k, ctx := keeper.MockEVMKeeper(t) seiAddr, evmAddr := keeper.MockAddressPair() + + // Before the mapping is set, neither direction resolves. _, ok := k.GetEVMAddress(ctx, seiAddr) require.False(t, ok) _, ok = k.GetSeiAddress(ctx, evmAddr) require.False(t, ok) + k.SetAddressMapping(ctx, seiAddr, evmAddr) + + // Both directions now resolve, and the underlying Sei account exists. foundEVM, ok := k.GetEVMAddress(ctx, seiAddr) require.True(t, ok) require.Equal(t, evmAddr, foundEVM) @@ -27,9 +31,52 @@ func TestSetGetAddressMapping(t *testing.T) { require.Equal(t, seiAddr, k.AccountKeeper().GetAccount(ctx, seiAddr).GetAddress()) } +func TestSetAddressMappingReplacesExistingIndexes(t *testing.T) { + // Re-binding either side of the mapping must clear the OLD reverse + // index, not just overwrite the forward one — otherwise stale entries + // would let an old address still resolve to its former partner. + + t.Run("rebinding evm address to a new sei address", func(t *testing.T) { + k, ctx := keeper.MockEVMKeeper(t) + oldSeiAddr, evmAddr := keeper.MockAddressPair() + newSeiAddr, _ := keeper.MockAddressPair() + + k.SetAddressMapping(ctx, oldSeiAddr, evmAddr) + k.SetAddressMapping(ctx, newSeiAddr, evmAddr) + + _, ok := k.GetEVMAddress(ctx, oldSeiAddr) + require.False(t, ok, "old sei address must no longer map to evm address") + foundEVM, ok := k.GetEVMAddress(ctx, newSeiAddr) + require.True(t, ok) + require.Equal(t, evmAddr, foundEVM) + foundSei, ok := k.GetSeiAddress(ctx, evmAddr) + require.True(t, ok) + require.Equal(t, newSeiAddr, foundSei) + }) + + t.Run("rebinding sei address to a new evm address", func(t *testing.T) { + k, ctx := keeper.MockEVMKeeper(t) + seiAddr, oldEvmAddr := keeper.MockAddressPair() + _, newEvmAddr := keeper.MockAddressPair() + + k.SetAddressMapping(ctx, seiAddr, oldEvmAddr) + k.SetAddressMapping(ctx, seiAddr, newEvmAddr) + + _, ok := k.GetSeiAddress(ctx, oldEvmAddr) + require.False(t, ok, "old evm address must no longer map to sei address") + foundEVM, ok := k.GetEVMAddress(ctx, seiAddr) + require.True(t, ok) + require.Equal(t, newEvmAddr, foundEVM) + foundSei, ok := k.GetSeiAddress(ctx, newEvmAddr) + require.True(t, ok) + require.Equal(t, seiAddr, foundSei) + }) +} + func TestDeleteAddressMapping(t *testing.T) { k, ctx := keeper.MockEVMKeeper(t) seiAddr, evmAddr := keeper.MockAddressPair() + k.SetAddressMapping(ctx, seiAddr, evmAddr) foundEVM, ok := k.GetEVMAddress(ctx, seiAddr) require.True(t, ok) @@ -37,6 +84,8 @@ func TestDeleteAddressMapping(t *testing.T) { foundSei, ok := k.GetSeiAddress(ctx, evmAddr) require.True(t, ok) require.Equal(t, seiAddr, foundSei) + + // Deletion must clear both directions of the index. k.DeleteAddressMapping(ctx, seiAddr, evmAddr) _, ok = k.GetEVMAddress(ctx, seiAddr) require.False(t, ok) @@ -47,10 +96,14 @@ func TestDeleteAddressMapping(t *testing.T) { func TestGetAddressOrDefault(t *testing.T) { k, ctx := keeper.MockEVMKeeper(t) seiAddr, evmAddr := keeper.MockAddressPair() + + // With no mapping set, the defaults are the raw byte cast in each + // direction: the Sei address bytes become the EVM address, and the + // EVM address bytes become the Sei address. defaultEvmAddr := k.GetEVMAddressOrDefault(ctx, seiAddr) - require.True(t, bytes.Equal(seiAddr, defaultEvmAddr[:])) + require.Equal(t, seiAddr.Bytes(), defaultEvmAddr[:]) defaultSeiAddr := k.GetSeiAddressOrDefault(ctx, evmAddr) - require.True(t, bytes.Equal(defaultSeiAddr, evmAddr[:])) + require.Equal(t, sdk.AccAddress(evmAddr[:]), defaultSeiAddr) } func TestSendingToCastAddress(t *testing.T) { @@ -58,17 +111,29 @@ func TestSendingToCastAddress(t *testing.T) { seiAddr, evmAddr := keeper.MockAddressPair() castAddr := sdk.AccAddress(evmAddr[:]) sourceAddr, _ := keeper.MockAddressPair() - require.Nil(t, a.BankKeeper.MintCoins(ctx, "evm", sdk.NewCoins(sdk.NewCoin("usei", sdk.NewInt(10))))) - require.Nil(t, a.BankKeeper.SendCoinsFromModuleToAccount(ctx, "evm", sourceAddr, sdk.NewCoins(sdk.NewCoin("usei", sdk.NewInt(5))))) - amt := sdk.NewCoins(sdk.NewCoin("usei", sdk.NewInt(1))) - require.Nil(t, a.BankKeeper.SendCoinsFromModuleToAccount(ctx, "evm", castAddr, amt)) - require.Nil(t, a.BankKeeper.SendCoins(ctx, sourceAddr, castAddr, amt)) - require.Nil(t, a.BankKeeper.SendCoinsAndWei(ctx, sourceAddr, castAddr, sdk.OneInt(), sdk.OneInt())) + // Fund the evm module and a source account. + require.NoError(t, a.BankKeeper.MintCoins(ctx, evmModule, + sdk.NewCoins(sdk.NewCoin(baseDenom, sdk.NewInt(10))))) + require.NoError(t, a.BankKeeper.SendCoinsFromModuleToAccount(ctx, evmModule, sourceAddr, + sdk.NewCoins(sdk.NewCoin(baseDenom, sdk.NewInt(5))))) + + amt := sdk.NewCoins(sdk.NewCoin(baseDenom, sdk.NewInt(1))) + + // Before any mapping exists, the bech32 cast of an EVM address is just + // an unowned account — bank can send to it freely. + require.NoError(t, a.BankKeeper.SendCoinsFromModuleToAccount(ctx, evmModule, castAddr, amt)) + require.NoError(t, a.BankKeeper.SendCoins(ctx, sourceAddr, castAddr, amt)) + require.NoError(t, a.BankKeeper.SendCoinsAndWei(ctx, sourceAddr, castAddr, sdk.OneInt(), sdk.OneInt())) + + // Once the EVM address is associated with a real Sei address, sending + // to the cast form MUST be rejected. Otherwise a sender could bypass + // the mapping by addressing the unmapped cast directly and stranding + // funds outside the associated Sei account. a.EvmKeeper.SetAddressMapping(ctx, seiAddr, evmAddr) - require.NotNil(t, a.BankKeeper.SendCoinsFromModuleToAccount(ctx, "evm", castAddr, amt)) - require.NotNil(t, a.BankKeeper.SendCoins(ctx, sourceAddr, castAddr, amt)) - require.NotNil(t, a.BankKeeper.SendCoinsAndWei(ctx, sourceAddr, castAddr, sdk.OneInt(), sdk.OneInt())) + require.Error(t, a.BankKeeper.SendCoinsFromModuleToAccount(ctx, evmModule, castAddr, amt)) + require.Error(t, a.BankKeeper.SendCoins(ctx, sourceAddr, castAddr, amt)) + require.Error(t, a.BankKeeper.SendCoinsAndWei(ctx, sourceAddr, castAddr, sdk.OneInt(), sdk.OneInt())) } func TestEvmAddressHandler_GetSeiAddressFromString(t *testing.T) { @@ -79,71 +144,48 @@ func TestEvmAddressHandler_GetSeiAddressFromString(t *testing.T) { _, notAssociatedEvmAddr := keeper.MockAddressPair() castAddr := sdk.AccAddress(notAssociatedEvmAddr[:]) - type args struct { - ctx sdk.Context - address string - } tests := []struct { name string - args args + address string want sdk.AccAddress - wantErr bool wantErrMsg string }{ { - name: "returns associated Sei address if input address is a valid 0x and associated", - args: args{ - ctx: ctx, - address: evmAddr.String(), - }, - want: seiAddr, + name: "valid 0x address, associated → returns mapped Sei address", + address: evmAddr.String(), + want: seiAddr, }, { - name: "returns default Sei address if input address is a valid 0x not associated", - args: args{ - ctx: ctx, - address: notAssociatedEvmAddr.String(), - }, - want: castAddr, + name: "valid 0x address, not associated → returns cast Sei address", + address: notAssociatedEvmAddr.String(), + want: castAddr, }, { - name: "returns Sei address if input address is a valid bech32 address", - args: args{ - ctx: ctx, - address: seiAddr.String(), - }, - want: seiAddr, + name: "valid bech32 address → returns itself", + address: seiAddr.String(), + want: seiAddr, }, { - name: "returns error if address is invalid", - args: args{ - ctx: ctx, - address: "invalid", - }, - wantErr: true, + name: "invalid address", + address: "invalid", wantErrMsg: "decoding bech32 failed: invalid bech32 string length 7", - }, { - name: "returns error if address is empty", - args: args{ - ctx: ctx, - address: "", - }, - wantErr: true, + }, + { + name: "empty address", + address: "", wantErrMsg: "empty address string is not allowed", }, } + h := evmkeeper.NewEvmAddressHandler(&a.GigaEvmKeeper) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := evmkeeper.NewEvmAddressHandler(&a.GigaEvmKeeper) - got, err := h.GetSeiAddressFromString(tt.args.ctx, tt.args.address) - if tt.wantErr { - require.NotNil(t, err) - require.Equal(t, tt.wantErrMsg, err.Error()) + got, err := h.GetSeiAddressFromString(ctx, tt.address) + if tt.wantErrMsg != "" { + require.EqualError(t, err, tt.wantErrMsg) return - } else { - require.NoError(t, err) - require.Equal(t, tt.want, got) } + require.NoError(t, err) + require.Equal(t, tt.want, got) }) } } diff --git a/giga/deps/xevm/keeper/code.go b/giga/deps/xevm/keeper/code.go index 410f629347..0866d6aec8 100644 --- a/giga/deps/xevm/keeper/code.go +++ b/giga/deps/xevm/keeper/code.go @@ -12,6 +12,14 @@ import ( "github.com/sei-protocol/sei-chain/utils" ) +// codeSizeEncodedLen is the fixed width of the big-endian uint64 encoding +// used for the cached code-size lookup. +const codeSizeEncodedLen = 8 + +// GetCode returns the contract bytecode stored at addr, or nil if no code +// is set. An explicitly-stored empty byte slice is also returned as nil; +// callers that need to distinguish "no account" from "account with empty +// code" should consult GetCodeHash instead. func (k *Keeper) GetCode(ctx sdk.Context, addr common.Address) []byte { code := k.PrefixStore(ctx, types.CodeKeyPrefix).Get(addr[:]) if len(code) == 0 { @@ -20,51 +28,85 @@ func (k *Keeper) GetCode(ctx sdk.Context, addr common.Address) []byte { return code } +// SetCode writes addr's bytecode and the derived code-size and code-hash +// indexes. Three values are persisted to avoid loading the full bytecode +// for size/hash lookups. +// +// Side effect: unless `code` is an EIP-7702 delegation designator, SetCode +// also installs an address mapping between addr and its cast Sei address +// (if one does not already exist). This is what binds a newly-deployed +// contract to a Sei account; delegations are deliberately excluded because +// the delegating account already has its own pubkey-derived Sei address +// and creating a cast mapping would conflict with it. +// +// Calling SetCode with nil/empty code is permitted and still creates the +// address mapping — it is the normal path for marking a contract account +// as existing without bytecode (e.g. CREATE before code deployment). func (k *Keeper) SetCode(ctx sdk.Context, addr common.Address, code []byte) { if code == nil { code = []byte{} } - k.PrefixStore(ctx, types.CodeKeyPrefix).Set(addr[:], code) - length := make([]byte, 8) - binary.BigEndian.PutUint64(length, uint64(len(code))) - k.PrefixStore(ctx, types.CodeSizeKeyPrefix).Set(addr[:], length) - h := crypto.Keccak256Hash(code) - k.PrefixStore(ctx, types.CodeHashKeyPrefix).Set(addr[:], h[:]) - // set association with direct cast Sei address for the contract address - if _, ok := k.GetSeiAddress(ctx, addr); !ok { + key := addr[:] + + k.PrefixStore(ctx, types.CodeKeyPrefix).Set(key, code) + + sizeBz := make([]byte, codeSizeEncodedLen) + binary.BigEndian.PutUint64(sizeBz, uint64(len(code))) + k.PrefixStore(ctx, types.CodeSizeKeyPrefix).Set(key, sizeBz) + + hash := crypto.Keccak256Hash(code) + k.PrefixStore(ctx, types.CodeHashKeyPrefix).Set(key, hash[:]) + + // EIP-7702 delegations are stored verbatim but must NOT create a + // cast-address mapping — see function doc. + if _, isDelegation := ethtypes.ParseDelegation(code); isDelegation { + return + } + if _, alreadyMapped := k.GetSeiAddress(ctx, addr); !alreadyMapped { k.SetAddressMapping(ctx, k.GetSeiAddressOrDefault(ctx, addr), addr) } } +// GetCodeHash returns addr's code hash following Ethereum's three-way +// account semantics: +// +// - Account does not exist (no code, no balance, no nonce) → Hash{}. +// - Account exists but has no code (balance > 0 or nonce > 0) → EmptyCodeHash. +// - Account has code → Keccak256(code). +// +// The distinction matters: EXTCODEHASH returning Hash{} signals a +// nonexistent account to the EVM, while EmptyCodeHash signals an existing +// EOA or empty contract. See `TestGetCodeHashWithNonceButZeroBalance`. func (k *Keeper) GetCodeHash(ctx sdk.Context, addr common.Address) common.Hash { - store := k.PrefixStore(ctx, types.CodeHashKeyPrefix) - bz := store.Get(addr[:]) - if bz == nil { - // per Ethereum behavior, if an address has no code, balance, or nonce, return Hash(0) - if k.GetBalance(ctx, k.GetSeiAddressOrDefault(ctx, addr)).Cmp(utils.Big0) == 0 && k.GetNonce(ctx, addr) == 0 { - return common.Hash{} - } - // if an address has no code but has balance or nonce, return EmptyCodeHash - return ethtypes.EmptyCodeHash + if bz := k.PrefixStore(ctx, types.CodeHashKeyPrefix).Get(addr[:]); bz != nil { + return common.BytesToHash(bz) + } + + balance := k.GetBalance(ctx, k.GetSeiAddressOrDefault(ctx, addr)) + if balance.Cmp(utils.Big0) == 0 && k.GetNonce(ctx, addr) == 0 { + return common.Hash{} } - return common.BytesToHash(bz) + return ethtypes.EmptyCodeHash } +// GetCodeSize returns the byte length of addr's code. Reads the cached +// size index rather than loading the full bytecode. func (k *Keeper) GetCodeSize(ctx sdk.Context, addr common.Address) int { bz := k.PrefixStore(ctx, types.CodeSizeKeyPrefix).Get(addr[:]) if bz == nil { return 0 } - return int(binary.BigEndian.Uint64(bz)) //nolint:gosec + return int(binary.BigEndian.Uint64(bz)) //nolint:gosec // bounded by code size } -func (k *Keeper) IterateAllCode(ctx sdk.Context, cb func(addr common.Address, code []byte) bool) { +// IterateAllCode walks every (address, code) pair in the code store. +// The callback returns `stop=true` to halt iteration early. +func (k *Keeper) IterateAllCode(ctx sdk.Context, cb func(addr common.Address, code []byte) (stop bool)) { iter := prefix.NewStore(k.GetKVStore(ctx), types.CodeKeyPrefix).Iterator(nil, nil) defer func() { _ = iter.Close() }() for ; iter.Valid(); iter.Next() { - evmAddr := common.BytesToAddress(iter.Key()) - if cb(evmAddr, iter.Value()) { - break + if cb(common.BytesToAddress(iter.Key()), iter.Value()) { + return } } } diff --git a/giga/deps/xevm/keeper/code_test.go b/giga/deps/xevm/keeper/code_test.go index 3561cff03e..6172043dc7 100644 --- a/giga/deps/xevm/keeper/code_test.go +++ b/giga/deps/xevm/keeper/code_test.go @@ -11,30 +11,62 @@ import ( "github.com/stretchr/testify/require" ) +const ( + evmModule = "evm" + baseDenom = "usei" +) + func TestCode(t *testing.T) { k, ctx := keeper.MockEVMKeeper(t) _, addr := keeper.MockAddressPair() + // Untouched address: code hash is the zero hash, not EmptyCodeHash. require.Equal(t, common.Hash{}, k.GetCodeHash(ctx, addr)) - k.BankKeeper().MintCoins(ctx, "evm", sdk.NewCoins(sdk.NewCoin("usei", sdk.OneInt()))) - k.BankKeeper().SendCoinsFromModuleToAccount(ctx, "evm", sdk.AccAddress(addr[:]), sdk.NewCoins(sdk.NewCoin("usei", sdk.OneInt()))) + // Funding the address creates the underlying account, after which the + // code hash becomes EmptyCodeHash (account exists, has no code). + oneUsei := sdk.NewCoins(sdk.NewCoin(baseDenom, sdk.OneInt())) + require.NoError(t, k.BankKeeper().MintCoins(ctx, evmModule, oneUsei)) + require.NoError(t, k.BankKeeper().SendCoinsFromModuleToAccount(ctx, evmModule, sdk.AccAddress(addr[:]), oneUsei)) + require.Equal(t, ethtypes.EmptyCodeHash, k.GetCodeHash(ctx, addr)) require.Nil(t, k.GetCode(ctx, addr)) require.Equal(t, 0, k.GetCodeSize(ctx, addr)) + // After SetCode, hash, bytes, and size all reflect the new code. code := []byte{1, 2, 3, 4, 5} k.SetCode(ctx, addr, code) require.Equal(t, crypto.Keccak256Hash(code), k.GetCodeHash(ctx, addr)) require.Equal(t, code, k.GetCode(ctx, addr)) - require.Equal(t, 5, k.GetCodeSize(ctx, addr)) - require.Equal(t, sdk.AccAddress(addr[:]), k.AccountKeeper().GetAccount(ctx, k.GetSeiAddressOrDefault(ctx, addr)).GetAddress()) + require.Equal(t, len(code), k.GetCodeSize(ctx, addr)) + + // SetCode must also associate a Sei account with the EVM address. + seiAddr := k.GetSeiAddressOrDefault(ctx, addr) + acct := k.AccountKeeper().GetAccount(ctx, seiAddr) + require.Equal(t, sdk.AccAddress(addr[:]), acct.GetAddress()) +} + +func TestCodeDelegation(t *testing.T) { + k, ctx := keeper.MockEVMKeeper(t) + _, addr := keeper.MockAddressPair() + _, target := keeper.MockAddressPair() + + // EIP-7702 delegation is stored verbatim and must NOT create a + // Sei-address mapping for the delegating account. + code := ethtypes.AddressToDelegation(target) + k.SetCode(ctx, addr, code) + + require.Equal(t, code, k.GetCode(ctx, addr)) + _, found := k.GetSeiAddress(ctx, addr) + require.False(t, found) } func TestNilCode(t *testing.T) { k, ctx := keeper.MockEVMKeeper(t) _, addr := keeper.MockAddressPair() + // Writing nil code stores no bytes but normalises the hash to + // EmptyCodeHash rather than leaving it as the zero hash. k.SetCode(ctx, addr, nil) require.Nil(t, k.GetCode(ctx, addr)) require.Equal(t, 0, k.GetCodeSize(ctx, addr)) @@ -47,8 +79,10 @@ func TestGetCodeHashWithNonceButZeroBalance(t *testing.T) { require.Equal(t, common.Hash{}, k.GetCodeHash(ctx, addr)) + // Bumping the nonce marks the account as existing even with a zero + // balance, so the code hash flips from zero to EmptyCodeHash. k.SetNonce(ctx, addr, 1) require.Equal(t, ethtypes.EmptyCodeHash, k.GetCodeHash(ctx, addr)) - require.True(t, k.GetBalance(ctx, k.GetSeiAddressOrDefault(ctx, addr)).Sign() == 0) + require.Zero(t, k.GetBalance(ctx, k.GetSeiAddressOrDefault(ctx, addr)).Sign()) } diff --git a/x/evm/keeper/address.go b/x/evm/keeper/address.go index d097325115..f62f2dd808 100644 --- a/x/evm/keeper/address.go +++ b/x/evm/keeper/address.go @@ -1,16 +1,49 @@ package keeper import ( + "bytes" + "github.com/ethereum/go-ethereum/common" "github.com/sei-protocol/sei-chain/sei-cosmos/store/prefix" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" "github.com/sei-protocol/sei-chain/x/evm/types" ) +// SetAddressMapping binds a Sei address to an EVM address bidirectionally. +// +// The mapping is maintained as two keys (forward: sei→evm, reverse: evm→sei). +// When either side is being rebound to a new partner, we must also clear the +// OLD partner's stale half of the mapping — otherwise the old address would +// continue to resolve to its former partner via one direction while the new +// binding wins in the other, leaving the index permanently inconsistent. +// +// The defensive `bytes.Equal` / `Equals` checks before deletion guard against +// already-inconsistent state: we only delete the stale half if it still +// points back to the address we're rebinding away from. func (k *Keeper) SetAddressMapping(ctx sdk.Context, seiAddress sdk.AccAddress, evmAddress common.Address) { store := ctx.KVStore(k.storeKey) - store.Set(types.EVMAddressToSeiAddressKey(evmAddress), seiAddress) - store.Set(types.SeiAddressToEVMAddressKey(seiAddress), evmAddress[:]) + evmKey := types.EVMAddressToSeiAddressKey(evmAddress) + seiKey := types.SeiAddressToEVMAddressKey(seiAddress) + + // If evmAddress was previously bound to a different Sei address, + // clear that Sei address's stale forward mapping. + if prevSei := store.Get(evmKey); prevSei != nil && !sdk.AccAddress(prevSei).Equals(seiAddress) { + prevSeiKey := types.SeiAddressToEVMAddressKey(prevSei) + if bytes.Equal(store.Get(prevSeiKey), evmAddress[:]) { + store.Delete(prevSeiKey) + } + } + // If seiAddress was previously bound to a different EVM address, + // clear that EVM address's stale reverse mapping. + if prevEvm := store.Get(seiKey); prevEvm != nil && !bytes.Equal(prevEvm, evmAddress[:]) { + prevEvmKey := types.EVMAddressToSeiAddressKey(common.BytesToAddress(prevEvm)) + if prevReverseSei := store.Get(prevEvmKey); prevReverseSei != nil && sdk.AccAddress(prevReverseSei).Equals(seiAddress) { + store.Delete(prevEvmKey) + } + } + + store.Set(evmKey, seiAddress) + store.Set(seiKey, evmAddress[:]) if !k.accountKeeper.HasAccount(ctx, seiAddress) { k.accountKeeper.SetAccount(ctx, k.accountKeeper.NewAccountWithAddress(ctx, seiAddress)) } @@ -21,6 +54,9 @@ func (k *Keeper) SetAddressMapping(ctx sdk.Context, seiAddress sdk.AccAddress, e )) } +// DeleteAddressMapping removes both directions of the binding. +// The caller is responsible for passing a (sei, evm) pair that was actually +// bound together — passing a mismatched pair will corrupt the indexes. func (k *Keeper) DeleteAddressMapping(ctx sdk.Context, seiAddress sdk.AccAddress, evmAddress common.Address) { store := ctx.KVStore(k.storeKey) store.Delete(types.EVMAddressToSeiAddressKey(evmAddress)) @@ -28,61 +64,67 @@ func (k *Keeper) DeleteAddressMapping(ctx sdk.Context, seiAddress sdk.AccAddress } func (k *Keeper) GetEVMAddress(ctx sdk.Context, seiAddress sdk.AccAddress) (common.Address, bool) { - store := ctx.KVStore(k.storeKey) - bz := store.Get(types.SeiAddressToEVMAddressKey(seiAddress)) - addr := common.Address{} + bz := ctx.KVStore(k.storeKey).Get(types.SeiAddressToEVMAddressKey(seiAddress)) if bz == nil { - return addr, false + return common.Address{}, false } + var addr common.Address copy(addr[:], bz) return addr, true } func (k *Keeper) GetEVMAddressOrDefault(ctx sdk.Context, seiAddress sdk.AccAddress) common.Address { - addr, ok := k.GetEVMAddress(ctx, seiAddress) - if ok { + if addr, ok := k.GetEVMAddress(ctx, seiAddress); ok { return addr } return common.BytesToAddress(seiAddress) } func (k *Keeper) GetSeiAddress(ctx sdk.Context, evmAddress common.Address) (sdk.AccAddress, bool) { - store := ctx.KVStore(k.storeKey) - bz := store.Get(types.EVMAddressToSeiAddressKey(evmAddress)) + bz := ctx.KVStore(k.storeKey).Get(types.EVMAddressToSeiAddressKey(evmAddress)) if bz == nil { - return []byte{}, false + return nil, false } return bz, true } func (k *Keeper) GetSeiAddressOrDefault(ctx sdk.Context, evmAddress common.Address) sdk.AccAddress { - addr, ok := k.GetSeiAddress(ctx, evmAddress) - if ok { + if addr, ok := k.GetSeiAddress(ctx, evmAddress); ok { return addr } return sdk.AccAddress(evmAddress[:]) } -func (k *Keeper) IterateSeiAddressMapping(ctx sdk.Context, cb func(evmAddr common.Address, seiAddr sdk.AccAddress) bool) { +// IterateSeiAddressMapping walks every (evm, sei) pair in the address index. +// The callback returns `stop=true` to halt iteration early. +func (k *Keeper) IterateSeiAddressMapping(ctx sdk.Context, cb func(evmAddr common.Address, seiAddr sdk.AccAddress) (stop bool)) { iter := prefix.NewStore(ctx.KVStore(k.storeKey), types.EVMAddressToSeiAddressKeyPrefix).Iterator(nil, nil) defer func() { _ = iter.Close() }() for ; iter.Valid(); iter.Next() { evmAddr := common.BytesToAddress(iter.Key()) seiAddr := sdk.AccAddress(iter.Value()) if cb(evmAddr, seiAddr) { - break + return } } } -// A sdk.AccAddress may not receive funds from bank if it's the result of direct-casting -// from an EVM address AND the originating EVM address has already been associated with -// a true (i.e. derived from the same pubkey) sdk.AccAddress. +// CanAddressReceive reports whether bank may credit `addr`. +// +// EVM and Sei addresses are both 20 bytes, so any Sei address can be +// interpreted as the byte-cast of an EVM address. That cast is permitted +// as a recipient EXCEPT when the underlying EVM address has already been +// associated with a real, pubkey-derived Sei address — in that case, funds +// sent to the cast form would be stranded outside the real account, so we +// reject it. The carve-out for `associatedAddr.Equals(addr)` permits EVM +// contracts and other cases where the cast IS the canonical Sei side. func (k *Keeper) CanAddressReceive(ctx sdk.Context, addr sdk.AccAddress) bool { - directCast := common.BytesToAddress(addr) // casting goes both directions since both address formats have 20 bytes + directCast := common.BytesToAddress(addr) associatedAddr, isAssociated := k.GetSeiAddress(ctx, directCast) - // if the associated address is the cast address itself, allow the address to receive (e.g. EVM contract addresses) - return associatedAddr.Equals(addr) || !isAssociated // this means it's either a cast address that's not associated yet, or not a cast address at all. + if !isAssociated { + return true // not a cast form, or a cast that hasn't been associated yet + } + return associatedAddr.Equals(addr) } type EvmAddressHandler struct { @@ -93,14 +135,12 @@ func NewEvmAddressHandler(evmKeeper *Keeper) EvmAddressHandler { return EvmAddressHandler{evmKeeper: evmKeeper} } +// GetSeiAddressFromString resolves a string-encoded address (either 0x-hex +// or bech32) to its Sei address form. Hex inputs go through the keeper's +// associated/cast resolution; bech32 inputs are returned as-is. func (h EvmAddressHandler) GetSeiAddressFromString(ctx sdk.Context, address string) (sdk.AccAddress, error) { if common.IsHexAddress(address) { - parsedAddress := common.HexToAddress(address) - return h.evmKeeper.GetSeiAddressOrDefault(ctx, parsedAddress), nil - } - parsedAddress, err := sdk.AccAddressFromBech32(address) - if err != nil { - return nil, err + return h.evmKeeper.GetSeiAddressOrDefault(ctx, common.HexToAddress(address)), nil } - return parsedAddress, nil + return sdk.AccAddressFromBech32(address) } diff --git a/x/evm/keeper/address_test.go b/x/evm/keeper/address_test.go index f0077c5cfc..f58e10e4c2 100644 --- a/x/evm/keeper/address_test.go +++ b/x/evm/keeper/address_test.go @@ -1,7 +1,6 @@ package keeper_test import ( - "bytes" "testing" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" @@ -14,11 +13,16 @@ func TestSetGetAddressMapping(t *testing.T) { k := &keeper.EVMTestApp.EvmKeeper ctx := keeper.EVMTestApp.GetContextForDeliverTx([]byte{}) seiAddr, evmAddr := keeper.MockAddressPair() + + // Before the mapping is set, neither direction resolves. _, ok := k.GetEVMAddress(ctx, seiAddr) require.False(t, ok) _, ok = k.GetSeiAddress(ctx, evmAddr) require.False(t, ok) + k.SetAddressMapping(ctx, seiAddr, evmAddr) + + // Both directions now resolve, and the underlying Sei account exists. foundEVM, ok := k.GetEVMAddress(ctx, seiAddr) require.True(t, ok) require.Equal(t, evmAddr, foundEVM) @@ -28,10 +32,53 @@ func TestSetGetAddressMapping(t *testing.T) { require.Equal(t, seiAddr, k.AccountKeeper().GetAccount(ctx, seiAddr).GetAddress()) } +func TestSetAddressMappingReplacesExistingIndexes(t *testing.T) { + // Re-binding either side of the mapping must clear the OLD reverse + // index, not just overwrite the forward one — otherwise stale entries + // would let an old address still resolve to its former partner. + k := &keeper.EVMTestApp.EvmKeeper + ctx := keeper.EVMTestApp.GetContextForDeliverTx([]byte{}) + + t.Run("rebinding evm address to a new sei address", func(t *testing.T) { + oldSeiAddr, evmAddr := keeper.MockAddressPair() + newSeiAddr, _ := keeper.MockAddressPair() + + k.SetAddressMapping(ctx, oldSeiAddr, evmAddr) + k.SetAddressMapping(ctx, newSeiAddr, evmAddr) + + _, ok := k.GetEVMAddress(ctx, oldSeiAddr) + require.False(t, ok, "old sei address must no longer map to evm address") + foundEVM, ok := k.GetEVMAddress(ctx, newSeiAddr) + require.True(t, ok) + require.Equal(t, evmAddr, foundEVM) + foundSei, ok := k.GetSeiAddress(ctx, evmAddr) + require.True(t, ok) + require.Equal(t, newSeiAddr, foundSei) + }) + + t.Run("rebinding sei address to a new evm address", func(t *testing.T) { + seiAddr, oldEvmAddr := keeper.MockAddressPair() + _, newEvmAddr := keeper.MockAddressPair() + + k.SetAddressMapping(ctx, seiAddr, oldEvmAddr) + k.SetAddressMapping(ctx, seiAddr, newEvmAddr) + + _, ok := k.GetSeiAddress(ctx, oldEvmAddr) + require.False(t, ok, "old evm address must no longer map to sei address") + foundEVM, ok := k.GetEVMAddress(ctx, seiAddr) + require.True(t, ok) + require.Equal(t, newEvmAddr, foundEVM) + foundSei, ok := k.GetSeiAddress(ctx, newEvmAddr) + require.True(t, ok) + require.Equal(t, seiAddr, foundSei) + }) +} + func TestDeleteAddressMapping(t *testing.T) { k := &keeper.EVMTestApp.EvmKeeper ctx := keeper.EVMTestApp.GetContextForDeliverTx([]byte{}) seiAddr, evmAddr := keeper.MockAddressPair() + k.SetAddressMapping(ctx, seiAddr, evmAddr) foundEVM, ok := k.GetEVMAddress(ctx, seiAddr) require.True(t, ok) @@ -39,6 +86,8 @@ func TestDeleteAddressMapping(t *testing.T) { foundSei, ok := k.GetSeiAddress(ctx, evmAddr) require.True(t, ok) require.Equal(t, seiAddr, foundSei) + + // Deletion must clear both directions of the index. k.DeleteAddressMapping(ctx, seiAddr, evmAddr) _, ok = k.GetEVMAddress(ctx, seiAddr) require.False(t, ok) @@ -50,10 +99,14 @@ func TestGetAddressOrDefault(t *testing.T) { k := &keeper.EVMTestApp.EvmKeeper ctx := keeper.EVMTestApp.GetContextForDeliverTx([]byte{}) seiAddr, evmAddr := keeper.MockAddressPair() + + // With no mapping set, the defaults are the raw byte cast in each + // direction: the Sei address bytes become the EVM address, and the + // EVM address bytes become the Sei address. defaultEvmAddr := k.GetEVMAddressOrDefault(ctx, seiAddr) - require.True(t, bytes.Equal(seiAddr, defaultEvmAddr[:])) + require.Equal(t, seiAddr.Bytes(), defaultEvmAddr[:]) defaultSeiAddr := k.GetSeiAddressOrDefault(ctx, evmAddr) - require.True(t, bytes.Equal(defaultSeiAddr, evmAddr[:])) + require.Equal(t, sdk.AccAddress(evmAddr[:]), defaultSeiAddr) } func TestSendingToCastAddress(t *testing.T) { @@ -62,17 +115,29 @@ func TestSendingToCastAddress(t *testing.T) { seiAddr, evmAddr := keeper.MockAddressPair() castAddr := sdk.AccAddress(evmAddr[:]) sourceAddr, _ := keeper.MockAddressPair() - require.Nil(t, a.BankKeeper.MintCoins(ctx, "evm", sdk.NewCoins(sdk.NewCoin("usei", sdk.NewInt(10))))) - require.Nil(t, a.BankKeeper.SendCoinsFromModuleToAccount(ctx, "evm", sourceAddr, sdk.NewCoins(sdk.NewCoin("usei", sdk.NewInt(5))))) - amt := sdk.NewCoins(sdk.NewCoin("usei", sdk.NewInt(1))) - require.Nil(t, a.BankKeeper.SendCoinsFromModuleToAccount(ctx, "evm", castAddr, amt)) - require.Nil(t, a.BankKeeper.SendCoins(ctx, sourceAddr, castAddr, amt)) - require.Nil(t, a.BankKeeper.SendCoinsAndWei(ctx, sourceAddr, castAddr, sdk.OneInt(), sdk.OneInt())) + // Fund the evm module and a source account. + require.NoError(t, a.BankKeeper.MintCoins(ctx, evmModule, + sdk.NewCoins(sdk.NewCoin(baseDenom, sdk.NewInt(10))))) + require.NoError(t, a.BankKeeper.SendCoinsFromModuleToAccount(ctx, evmModule, sourceAddr, + sdk.NewCoins(sdk.NewCoin(baseDenom, sdk.NewInt(5))))) + + amt := sdk.NewCoins(sdk.NewCoin(baseDenom, sdk.NewInt(1))) + + // Before any mapping exists, the bech32 cast of an EVM address is just + // an unowned account — bank can send to it freely. + require.NoError(t, a.BankKeeper.SendCoinsFromModuleToAccount(ctx, evmModule, castAddr, amt)) + require.NoError(t, a.BankKeeper.SendCoins(ctx, sourceAddr, castAddr, amt)) + require.NoError(t, a.BankKeeper.SendCoinsAndWei(ctx, sourceAddr, castAddr, sdk.OneInt(), sdk.OneInt())) + + // Once the EVM address is associated with a real Sei address, sending + // to the cast form MUST be rejected. Otherwise a sender could bypass + // the mapping by addressing the unmapped cast directly and stranding + // funds outside the associated Sei account. a.EvmKeeper.SetAddressMapping(ctx, seiAddr, evmAddr) - require.NotNil(t, a.BankKeeper.SendCoinsFromModuleToAccount(ctx, "evm", castAddr, amt)) - require.NotNil(t, a.BankKeeper.SendCoins(ctx, sourceAddr, castAddr, amt)) - require.NotNil(t, a.BankKeeper.SendCoinsAndWei(ctx, sourceAddr, castAddr, sdk.OneInt(), sdk.OneInt())) + require.Error(t, a.BankKeeper.SendCoinsFromModuleToAccount(ctx, evmModule, castAddr, amt)) + require.Error(t, a.BankKeeper.SendCoins(ctx, sourceAddr, castAddr, amt)) + require.Error(t, a.BankKeeper.SendCoinsAndWei(ctx, sourceAddr, castAddr, sdk.OneInt(), sdk.OneInt())) } func TestEvmAddressHandler_GetSeiAddressFromString(t *testing.T) { @@ -84,71 +149,48 @@ func TestEvmAddressHandler_GetSeiAddressFromString(t *testing.T) { _, notAssociatedEvmAddr := keeper.MockAddressPair() castAddr := sdk.AccAddress(notAssociatedEvmAddr[:]) - type args struct { - ctx sdk.Context - address string - } tests := []struct { name string - args args + address string want sdk.AccAddress - wantErr bool wantErrMsg string }{ { - name: "returns associated Sei address if input address is a valid 0x and associated", - args: args{ - ctx: ctx, - address: evmAddr.String(), - }, - want: seiAddr, + name: "valid 0x address, associated → returns mapped Sei address", + address: evmAddr.String(), + want: seiAddr, }, { - name: "returns default Sei address if input address is a valid 0x not associated", - args: args{ - ctx: ctx, - address: notAssociatedEvmAddr.String(), - }, - want: castAddr, + name: "valid 0x address, not associated → returns cast Sei address", + address: notAssociatedEvmAddr.String(), + want: castAddr, }, { - name: "returns Sei address if input address is a valid bech32 address", - args: args{ - ctx: ctx, - address: seiAddr.String(), - }, - want: seiAddr, + name: "valid bech32 address → returns itself", + address: seiAddr.String(), + want: seiAddr, }, { - name: "returns error if address is invalid", - args: args{ - ctx: ctx, - address: "invalid", - }, - wantErr: true, + name: "invalid address", + address: "invalid", wantErrMsg: "decoding bech32 failed: invalid bech32 string length 7", - }, { - name: "returns error if address is empty", - args: args{ - ctx: ctx, - address: "", - }, - wantErr: true, + }, + { + name: "empty address", + address: "", wantErrMsg: "empty address string is not allowed", }, } + h := evmkeeper.NewEvmAddressHandler(&a.EvmKeeper) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := evmkeeper.NewEvmAddressHandler(&a.EvmKeeper) - got, err := h.GetSeiAddressFromString(tt.args.ctx, tt.args.address) - if tt.wantErr { - require.NotNil(t, err) - require.Equal(t, tt.wantErrMsg, err.Error()) + got, err := h.GetSeiAddressFromString(ctx, tt.address) + if tt.wantErrMsg != "" { + require.EqualError(t, err, tt.wantErrMsg) return - } else { - require.NoError(t, err) - require.Equal(t, tt.want, got) } + require.NoError(t, err) + require.Equal(t, tt.want, got) }) } } diff --git a/x/evm/keeper/code.go b/x/evm/keeper/code.go index 9b729888f0..c7ca4e950f 100644 --- a/x/evm/keeper/code.go +++ b/x/evm/keeper/code.go @@ -12,6 +12,14 @@ import ( "github.com/sei-protocol/sei-chain/x/evm/types" ) +// codeSizeEncodedLen is the fixed width of the big-endian uint64 encoding +// used for the cached code-size lookup. +const codeSizeEncodedLen = 8 + +// GetCode returns the contract bytecode stored at addr, or nil if no code +// is set. An explicitly-stored empty byte slice is also returned as nil; +// callers that need to distinguish "no account" from "account with empty +// code" should consult GetCodeHash instead. func (k *Keeper) GetCode(ctx sdk.Context, addr common.Address) []byte { code := k.PrefixStore(ctx, types.CodeKeyPrefix).Get(addr[:]) if len(code) == 0 { @@ -20,51 +28,85 @@ func (k *Keeper) GetCode(ctx sdk.Context, addr common.Address) []byte { return code } +// SetCode writes addr's bytecode and the derived code-size and code-hash +// indexes. Three values are persisted to avoid loading the full bytecode +// for size/hash lookups. +// +// Side effect: unless `code` is an EIP-7702 delegation designator, SetCode +// also installs an address mapping between addr and its cast Sei address +// (if one does not already exist). This is what binds a newly-deployed +// contract to a Sei account; delegations are deliberately excluded because +// the delegating account already has its own pubkey-derived Sei address +// and creating a cast mapping would conflict with it. +// +// Calling SetCode with nil/empty code is permitted and still creates the +// address mapping — it is the normal path for marking a contract account +// as existing without bytecode (e.g. CREATE before code deployment). func (k *Keeper) SetCode(ctx sdk.Context, addr common.Address, code []byte) { if code == nil { code = []byte{} } - k.PrefixStore(ctx, types.CodeKeyPrefix).Set(addr[:], code) - length := make([]byte, 8) - binary.BigEndian.PutUint64(length, uint64(len(code))) - k.PrefixStore(ctx, types.CodeSizeKeyPrefix).Set(addr[:], length) - h := crypto.Keccak256Hash(code) - k.PrefixStore(ctx, types.CodeHashKeyPrefix).Set(addr[:], h[:]) - // set association with direct cast Sei address for the contract address - if _, ok := k.GetSeiAddress(ctx, addr); !ok { + key := addr[:] + + k.PrefixStore(ctx, types.CodeKeyPrefix).Set(key, code) + + sizeBz := make([]byte, codeSizeEncodedLen) + binary.BigEndian.PutUint64(sizeBz, uint64(len(code))) + k.PrefixStore(ctx, types.CodeSizeKeyPrefix).Set(key, sizeBz) + + hash := crypto.Keccak256Hash(code) + k.PrefixStore(ctx, types.CodeHashKeyPrefix).Set(key, hash[:]) + + // EIP-7702 delegations are stored verbatim but must NOT create a + // cast-address mapping — see function doc. + if _, isDelegation := ethtypes.ParseDelegation(code); isDelegation { + return + } + if _, alreadyMapped := k.GetSeiAddress(ctx, addr); !alreadyMapped { k.SetAddressMapping(ctx, k.GetSeiAddressOrDefault(ctx, addr), addr) } } +// GetCodeHash returns addr's code hash following Ethereum's three-way +// account semantics: +// +// - Account does not exist (no code, no balance, no nonce) → Hash{}. +// - Account exists but has no code (balance > 0 or nonce > 0) → EmptyCodeHash. +// - Account has code → Keccak256(code). +// +// The distinction matters: EXTCODEHASH returning Hash{} signals a +// nonexistent account to the EVM, while EmptyCodeHash signals an existing +// EOA or empty contract. See `TestGetCodeHashWithNonceButZeroBalance`. func (k *Keeper) GetCodeHash(ctx sdk.Context, addr common.Address) common.Hash { - store := k.PrefixStore(ctx, types.CodeHashKeyPrefix) - bz := store.Get(addr[:]) - if bz == nil { - // per Ethereum behavior, if an address has no code, balance, or nonce, return Hash(0) - if k.GetBalance(ctx, k.GetSeiAddressOrDefault(ctx, addr)).Cmp(utils.Big0) == 0 && k.GetNonce(ctx, addr) == 0 { - return common.Hash{} - } - // if an address has no code but has balance or nonce, return EmptyCodeHash - return ethtypes.EmptyCodeHash + if bz := k.PrefixStore(ctx, types.CodeHashKeyPrefix).Get(addr[:]); bz != nil { + return common.BytesToHash(bz) + } + + balance := k.GetBalance(ctx, k.GetSeiAddressOrDefault(ctx, addr)) + if balance.Cmp(utils.Big0) == 0 && k.GetNonce(ctx, addr) == 0 { + return common.Hash{} } - return common.BytesToHash(bz) + return ethtypes.EmptyCodeHash } +// GetCodeSize returns the byte length of addr's code. Reads the cached +// size index rather than loading the full bytecode. func (k *Keeper) GetCodeSize(ctx sdk.Context, addr common.Address) int { bz := k.PrefixStore(ctx, types.CodeSizeKeyPrefix).Get(addr[:]) if bz == nil { return 0 } - return int(binary.BigEndian.Uint64(bz)) //nolint:gosec + return int(binary.BigEndian.Uint64(bz)) //nolint:gosec // bounded by code size } -func (k *Keeper) IterateAllCode(ctx sdk.Context, cb func(addr common.Address, code []byte) bool) { +// IterateAllCode walks every (address, code) pair in the code store. +// The callback returns `stop=true` to halt iteration early. +func (k *Keeper) IterateAllCode(ctx sdk.Context, cb func(addr common.Address, code []byte) (stop bool)) { iter := prefix.NewStore(ctx.KVStore(k.storeKey), types.CodeKeyPrefix).Iterator(nil, nil) defer func() { _ = iter.Close() }() for ; iter.Valid(); iter.Next() { - evmAddr := common.BytesToAddress(iter.Key()) - if cb(evmAddr, iter.Value()) { - break + if cb(common.BytesToAddress(iter.Key()), iter.Value()) { + return } } } diff --git a/x/evm/keeper/code_test.go b/x/evm/keeper/code_test.go index 5977b2d9df..4c25f720ef 100644 --- a/x/evm/keeper/code_test.go +++ b/x/evm/keeper/code_test.go @@ -11,25 +11,56 @@ import ( "github.com/stretchr/testify/require" ) +const ( + evmModule = "evm" + baseDenom = "usei" +) + func TestCode(t *testing.T) { k := &keeper.EVMTestApp.EvmKeeper ctx := keeper.EVMTestApp.GetContextForDeliverTx([]byte{}) _, addr := keeper.MockAddressPair() + // Untouched address: code hash is the zero hash, not EmptyCodeHash. require.Equal(t, common.Hash{}, k.GetCodeHash(ctx, addr)) - k.BankKeeper().MintCoins(ctx, "evm", sdk.NewCoins(sdk.NewCoin("usei", sdk.OneInt()))) - k.BankKeeper().SendCoinsFromModuleToAccount(ctx, "evm", sdk.AccAddress(addr[:]), sdk.NewCoins(sdk.NewCoin("usei", sdk.OneInt()))) + // Funding the address creates the underlying account, after which the + // code hash becomes EmptyCodeHash (account exists, has no code). + oneUsei := sdk.NewCoins(sdk.NewCoin(baseDenom, sdk.OneInt())) + require.NoError(t, k.BankKeeper().MintCoins(ctx, evmModule, oneUsei)) + require.NoError(t, k.BankKeeper().SendCoinsFromModuleToAccount(ctx, evmModule, addr[:], oneUsei)) + require.Equal(t, ethtypes.EmptyCodeHash, k.GetCodeHash(ctx, addr)) require.Nil(t, k.GetCode(ctx, addr)) require.Equal(t, 0, k.GetCodeSize(ctx, addr)) + // After SetCode, hash, bytes, and size all reflect the new code. code := []byte{1, 2, 3, 4, 5} k.SetCode(ctx, addr, code) require.Equal(t, crypto.Keccak256Hash(code), k.GetCodeHash(ctx, addr)) require.Equal(t, code, k.GetCode(ctx, addr)) - require.Equal(t, 5, k.GetCodeSize(ctx, addr)) - require.Equal(t, sdk.AccAddress(addr[:]), k.AccountKeeper().GetAccount(ctx, k.GetSeiAddressOrDefault(ctx, addr)).GetAddress()) + require.Equal(t, len(code), k.GetCodeSize(ctx, addr)) + + // SetCode must also associate a Sei account with the EVM address. + seiAddr := k.GetSeiAddressOrDefault(ctx, addr) + acct := k.AccountKeeper().GetAccount(ctx, seiAddr) + require.Equal(t, sdk.AccAddress(addr[:]), acct.GetAddress()) +} + +func TestCodeDelegation(t *testing.T) { + k := &keeper.EVMTestApp.EvmKeeper + ctx := keeper.EVMTestApp.GetContextForDeliverTx([]byte{}) + _, addr := keeper.MockAddressPair() + _, target := keeper.MockAddressPair() + + // EIP-7702 delegation is stored verbatim and must NOT create a + // Sei-address mapping for the delegating account. + code := ethtypes.AddressToDelegation(target) + k.SetCode(ctx, addr, code) + + require.Equal(t, code, k.GetCode(ctx, addr)) + _, found := k.GetSeiAddress(ctx, addr) + require.False(t, found) } func TestNilCode(t *testing.T) { @@ -37,6 +68,8 @@ func TestNilCode(t *testing.T) { ctx := keeper.EVMTestApp.GetContextForDeliverTx([]byte{}) _, addr := keeper.MockAddressPair() + // Writing nil code stores no bytes but normalises the hash to + // EmptyCodeHash rather than leaving it as the zero hash. k.SetCode(ctx, addr, nil) require.Nil(t, k.GetCode(ctx, addr)) require.Equal(t, 0, k.GetCodeSize(ctx, addr)) @@ -50,8 +83,10 @@ func TestGetCodeHashWithNonceButZeroBalance(t *testing.T) { require.Equal(t, common.Hash{}, k.GetCodeHash(ctx, addr)) + // Bumping the nonce marks the account as existing even with a zero + // balance, so the code hash flips from zero to EmptyCodeHash. k.SetNonce(ctx, addr, 1) require.Equal(t, ethtypes.EmptyCodeHash, k.GetCodeHash(ctx, addr)) - require.True(t, k.GetBalance(ctx, k.GetSeiAddressOrDefault(ctx, addr)).Sign() == 0) + require.Zero(t, k.GetBalance(ctx, k.GetSeiAddressOrDefault(ctx, addr)).Sign()) }