diff --git a/app/app.go b/app/app.go index c251f19791..2df2ea706e 100644 --- a/app/app.go +++ b/app/app.go @@ -142,6 +142,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-cosmos/storev2/rootmulti" "github.com/sei-protocol/sei-chain/utils" + "github.com/sei-protocol/sei-chain/utils/helpers" utilmetrics "github.com/sei-protocol/sei-chain/utils/metrics" "github.com/sei-protocol/sei-chain/wasmbinding" epochmodule "github.com/sei-protocol/sei-chain/x/epoch" @@ -2045,6 +2046,15 @@ func (app *App) executeEVMTxWithGigaExecutor(ctx sdk.Context, msg *evmtypes.MsgE return nil, gigaprecompiles.ErrBalanceMigrationRequired } + // EIP-7702 authorization authorities must be associated with their true (pubkey-derived) + // Sei address before SetCode installs delegation code, otherwise SetCode creates a mutable + // direct-cast mapping that a later associatePubKey can remap (orphaning staking/distribution + // state). That pre-association is done in the V2 ante handler and requires balance migration, + // which giga's cachekv cannot perform, so defer such transactions to V2. + if app.setCodeTxRequiresAuthorityAssociation(ctx, ethTx) { + return nil, gigaprecompiles.ErrBalanceMigrationRequired + } + // Create state DB for this transaction (only for valid transactions) stateDB := gigaevmstate.NewDBImpl(ctx, &app.GigaEvmKeeper, false) defer stateDB.Cleanup() @@ -2247,6 +2257,26 @@ func (app *App) executeEVMTxWithGigaExecutor(ctx sdk.Context, msg *evmtypes.MsgE }, nil } +// setCodeTxRequiresAuthorityAssociation reports whether an EIP-7702 transaction has any +// authorization authority that is not yet associated with its true (pubkey-derived) Sei +// address. Such an authority must be associated (with balance migration) before SetCode +// runs, which the V2 ante handler does but the giga executor cannot; callers use this to +// defer the transaction to V2. Authorities with unrecoverable signatures are ignored: +// EVM execution skips them too, so no direct-cast mapping is created for them. It returns +// false for non-SetCode transactions (which carry no authorizations). +func (app *App) setCodeTxRequiresAuthorityAssociation(ctx sdk.Context, ethTx *ethtypes.Transaction) bool { + for _, auth := range ethTx.SetCodeAuthorizations() { + _, seiAddr, _, err := helpers.RecoverAddressesFromAuthorization(auth) + if err != nil { + continue + } + if _, associated := app.GigaEvmKeeper.GetEVMAddress(ctx, seiAddr); !associated { + return true + } + } + return false +} + // gigaDeliverTx is the OCC-compatible deliverTx function for the giga executor. // makeGigaDeliverTx returns an OCC-compatible deliverTx callback that captures the given // block cache, avoiding mutable state on App for cache lifecycle management. diff --git a/app/setcode_authority_test.go b/app/setcode_authority_test.go new file mode 100644 index 0000000000..1431680776 --- /dev/null +++ b/app/setcode_authority_test.go @@ -0,0 +1,72 @@ +package app + +import ( + "math/big" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/holiman/uint256" + tmproto "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/types" + "github.com/sei-protocol/sei-chain/utils/helpers" + "github.com/stretchr/testify/require" +) + +// TestSetCodeTxRequiresAuthorityAssociation verifies the Giga-path guard for the EIP-7702 +// root-cause fix: a SetCode transaction whose authorization authority is not yet associated +// with its true Sei address must be deferred to V2 (where the ante handler pre-associates +// it), while an already-associated authority or a non-SetCode transaction need not. +func TestSetCodeTxRequiresAuthorityAssociation(t *testing.T) { + a := Setup(t, false, false, false) + ctx := a.NewContext(false, tmproto.Header{Height: 1, ChainID: "sei-test", Time: time.Now()}) + chainID := a.EvmKeeper.ChainID(ctx) + + victimKey, err := crypto.GenerateKey() + require.NoError(t, err) + victimEVM := crypto.PubkeyToAddress(victimKey.PublicKey) + _, victimTrueSei, _, err := helpers.GetAddressesFromPubkeyBytes(crypto.FromECDSAPub(&victimKey.PublicKey)) + require.NoError(t, err) + + auth, err := ethtypes.SignSetCode(victimKey, ethtypes.SetCodeAuthorization{ + ChainID: *uint256.MustFromBig(chainID), + Address: common.HexToAddress("0x000000000000000000000000000000000000c0de"), + Nonce: 0, + }) + require.NoError(t, err) + + sponsorKey, err := crypto.GenerateKey() + require.NoError(t, err) + to := common.HexToAddress("0x00000000000000000000000000000000000000aa") + setCodeTx, err := ethtypes.SignNewTx(sponsorKey, ethtypes.NewPragueSigner(chainID), ðtypes.SetCodeTx{ + ChainID: uint256.MustFromBig(chainID), + Nonce: 0, + GasTipCap: uint256.NewInt(1), + GasFeeCap: uint256.NewInt(1), + Gas: 100000, + To: to, + Value: uint256.NewInt(0), + AuthList: []ethtypes.SetCodeAuthorization{auth}, + }) + require.NoError(t, err) + + // Unassociated authority => giga must defer to V2 so the ante can pre-associate it. + require.True(t, a.setCodeTxRequiresAuthorityAssociation(ctx, setCodeTx)) + + // Once the authority is associated to its true Sei address, giga can execute directly + // (SetCode will see the association and skip the direct-cast mapping). + a.GigaEvmKeeper.SetAddressMapping(ctx, victimTrueSei, victimEVM) + require.False(t, a.setCodeTxRequiresAuthorityAssociation(ctx, setCodeTx)) + + // A non-SetCode transaction carries no authorizations and never needs deferral. + legacyTx, err := ethtypes.SignNewTx(sponsorKey, ethtypes.NewPragueSigner(chainID), ðtypes.LegacyTx{ + Nonce: 0, + GasPrice: big.NewInt(1), + Gas: 21000, + To: &to, + Value: big.NewInt(0), + }) + require.NoError(t, err) + require.False(t, a.setCodeTxRequiresAuthorityAssociation(ctx, legacyTx)) +} diff --git a/sei-cosmos/x/distribution/keeper/hooks.go b/sei-cosmos/x/distribution/keeper/hooks.go index 7b5e3d44f4..ee00bc6f63 100644 --- a/sei-cosmos/x/distribution/keeper/hooks.go +++ b/sei-cosmos/x/distribution/keeper/hooks.go @@ -46,8 +46,25 @@ func (h Hooks) AfterValidatorRemoved(ctx sdk.Context, _ sdk.ConsAddress, valAddr accAddr := sdk.AccAddress(valAddr) withdrawAddr := h.k.GetDelegatorWithdrawAddr(ctx, accAddr) - if err := h.k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, withdrawAddr, coins); err != nil { - panic(err) + // GetDelegatorWithdrawAddr falls back to the delegator (accAddr) when the + // configured withdraw address cannot receive funds, but that fallback can + // itself be unable to receive — e.g. accAddr is an EVM address whose Sei + // mapping was re-associated to a different address, so CanAddressReceive + // rejects it. This hook runs in EndBlock, so attempting the send and + // panicking on the resulting bank error would halt the chain. Check + // receivability first: when the recipient cannot receive, route the + // commission to the community pool instead. The coins already back the + // distribution module account (where community pool funds are held), so this + // conserves value and avoids the partial module-account debit that a failed + // SendCoins leaves behind. + if h.k.canReceiveWithdrawAddr(ctx, withdrawAddr) { + if err := h.k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, withdrawAddr, coins); err != nil { + panic(err) + } + } else { + feePool := h.k.GetFeePool(ctx) + feePool.CommunityPool = feePool.CommunityPool.Add(sdk.NewDecCoinsFromCoins(coins...)...) + h.k.SetFeePool(ctx, feePool) } } } diff --git a/sei-cosmos/x/distribution/keeper/keeper_test.go b/sei-cosmos/x/distribution/keeper/keeper_test.go index ad0dc3a002..22d8c53d07 100644 --- a/sei-cosmos/x/distribution/keeper/keeper_test.go +++ b/sei-cosmos/x/distribution/keeper/keeper_test.go @@ -82,6 +82,61 @@ func TestAfterValidatorRemovedFallsBackForInvalidWithdrawAddress(t *testing.T) { require.Equal(t, balanceBefore.Amount.Add(sdk.NewInt(10)), balanceAfter.Amount) } +// TestAfterValidatorRemovedRoutesToCommunityPoolForUnreceivableValidator covers the +// case where the validator operator address itself cannot receive funds — its EVM +// address was re-associated (e.g. via associatePubKey) away from the direct-cast Sei +// address it was created under. The withdraw-address fallback in GetDelegatorWithdrawAddr +// resolves back to that same unreceivable operator address, so the commission +// force-withdraw fails. AfterValidatorRemoved runs during EndBlock, so it must not panic: +// the commission is routed to the community pool instead, which conserves value because +// the coins already back the distribution module account. +func TestAfterValidatorRemovedRoutesToCommunityPoolForUnreceivableValidator(t *testing.T) { + app := seiapp.Setup(t, false, false, false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + + // The validator operator is the direct-cast Sei address of an EVM address. + evmAddr := common.HexToAddress("0x3333333333333333333333333333333333333333") + castAddr := sdk.AccAddress(evmAddr[:]) + valAddr := sdk.ValAddress(castAddr) + valAccAddr := sdk.AccAddress(valAddr) // == castAddr + + require.True(t, app.BankKeeper.CanSendTo(ctx, castAddr)) + + // Re-associate the EVM address to a different true Sei address, mirroring + // associatePubKey after a validator was created under the direct-cast address. + associatedAddr := seiapp.AddTestAddrs(app, ctx, 1, sdk.NewInt(1000000000))[0] + app.EvmKeeper.SetAddressMapping(ctx, associatedAddr, evmAddr) + + // The operator/delegator address can no longer receive funds, and the + // withdraw-address fallback resolves back to that same unreceivable address. + require.False(t, app.BankKeeper.CanSendTo(ctx, castAddr)) + require.Equal(t, valAccAddr.String(), app.DistrKeeper.GetDelegatorWithdrawAddr(ctx, valAccAddr).String()) + + commission := sdk.DecCoins{sdk.NewDecCoin("usei", sdk.NewInt(10))} + coins := sdk.NewCoins(sdk.NewCoin("usei", sdk.NewInt(10))) + distrAcc := app.DistrKeeper.GetDistributionAccount(ctx) + require.NoError(t, apptesting.FundModuleAccount(app.BankKeeper, ctx, distrAcc.GetName(), coins)) + app.AccountKeeper.SetModuleAccount(ctx, distrAcc) + + app.DistrKeeper.SetValidatorOutstandingRewards(ctx, valAddr, types.ValidatorOutstandingRewards{Rewards: commission}) + app.DistrKeeper.SetValidatorAccumulatedCommission(ctx, valAddr, types.ValidatorAccumulatedCommission{Commission: commission}) + + communityBefore := app.DistrKeeper.GetFeePool(ctx).CommunityPool.AmountOf("usei") + moduleBalanceBefore := app.BankKeeper.GetBalance(ctx, distrAcc.GetAddress(), "usei") + + require.NotPanics(t, func() { + app.DistrKeeper.Hooks().AfterValidatorRemoved(ctx, sdk.ConsAddress{}, valAddr) + }) + + // The commission could not be paid out, so it stays in the distribution module + // account and is accounted to the community pool. No value leaves the module and + // the unreceivable operator address receives nothing. + communityAfter := app.DistrKeeper.GetFeePool(ctx).CommunityPool.AmountOf("usei") + require.Equal(t, communityBefore.Add(sdk.NewDec(10)), communityAfter) + require.True(t, app.BankKeeper.GetBalance(ctx, castAddr, "usei").IsZero()) + require.Equal(t, moduleBalanceBefore, app.BankKeeper.GetBalance(ctx, distrAcc.GetAddress(), "usei")) +} + func TestWithdrawValidatorCommission(t *testing.T) { app := seiapp.Setup(t, false, false, false) ctx := app.BaseApp.NewContext(false, tmproto.Header{}) diff --git a/utils/helpers/address.go b/utils/helpers/address.go index 5ee042ca6d..4dac1a565f 100644 --- a/utils/helpers/address.go +++ b/utils/helpers/address.go @@ -1,6 +1,7 @@ package helpers import ( + "bytes" "errors" "math/big" @@ -8,11 +9,16 @@ import ( "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/rlp" "github.com/sei-protocol/sei-chain/sei-cosmos/crypto/keys/secp256k1" cryptotypes "github.com/sei-protocol/sei-chain/sei-cosmos/crypto/types" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" ) +// eip7702MagicPrefix is the domain-separator byte prepended to the RLP-encoded +// authorization tuple before hashing, per EIP-7702 (MAGIC = 0x05). +const eip7702MagicPrefix byte = 0x05 + var ( big2 = big.NewInt(2) big8 = big.NewInt(8) @@ -106,3 +112,21 @@ func RecoverAddressesFromTx(ethTx *ethtypes.Transaction, signer ethtypes.Signer, adjustedV := AdjustV(V, ethTx.Type(), chainID) return GetAddresses(adjustedV, R, S, txHash) } + +// RecoverAddressesFromAuthorization recovers the EVM address, Sei address, and public +// key of the account that signed an EIP-7702 SetCode authorization (the "authority"). +// The authorization sig hash is keccak256(0x05 || rlp([chainId, address, nonce])) and +// the recovery id is carried directly in auth.V (yParity, 0 or 1), which GetAddresses +// expects bumped by 27. This mirrors go-ethereum's SetCodeAuthorization.Authority(), but +// additionally returns the recovered public key so the authority can be associated with +// its true Sei address. +func RecoverAddressesFromAuthorization(auth ethtypes.SetCodeAuthorization) (common.Address, sdk.AccAddress, cryptotypes.PubKey, error) { + var buf bytes.Buffer + buf.WriteByte(eip7702MagicPrefix) + if err := rlp.Encode(&buf, []any{auth.ChainID, auth.Address, auth.Nonce}); err != nil { + return common.Address{}, sdk.AccAddress{}, nil, err + } + sigHash := crypto.Keccak256Hash(buf.Bytes()) + v := new(big.Int).SetUint64(uint64(auth.V) + 27) + return GetAddresses(v, auth.R.ToBig(), auth.S.ToBig(), sigHash) +} diff --git a/utils/helpers/address_test.go b/utils/helpers/address_test.go index 88daf4c49b..91d3062495 100644 --- a/utils/helpers/address_test.go +++ b/utils/helpers/address_test.go @@ -8,11 +8,76 @@ import ( "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" + "github.com/holiman/uint256" "github.com/sei-protocol/sei-chain/sei-cosmos/crypto/keys/secp256k1" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" "github.com/stretchr/testify/require" ) +// TestRecoverAddressesFromAuthorization verifies that our EIP-7702 authorization sig-hash +// replication recovers the same account go-ethereum's own SignSetCode/Authority() does. +// If the sig hash drifts from go-ethereum, RecoverAddressesFromAuthorization would recover +// the wrong (or no) address and the ante-handler pre-association would silently no-op, so +// this test pins the encoding across several chain IDs, nonces and delegation targets. +func TestRecoverAddressesFromAuthorization(t *testing.T) { + cases := []struct { + name string + chainID *uint256.Int + address common.Address + nonce uint64 + }{ + {"typical", uint256.NewInt(1329), common.HexToAddress("0x00000000000000000000000000000000000000aa"), 7}, + {"zero chain id (valid for any chain)", uint256.NewInt(0), common.HexToAddress("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"), 0}, + {"large nonce", uint256.NewInt(11155111), common.Address{}, 1<<63 + 123}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + privKey, err := crypto.GenerateKey() + require.NoError(t, err) + expectedEVM := crypto.PubkeyToAddress(privKey.PublicKey) + + signed, err := ethtypes.SignSetCode(privKey, ethtypes.SetCodeAuthorization{ + ChainID: *tc.chainID, + Address: tc.address, + Nonce: tc.nonce, + }) + require.NoError(t, err) + + // go-ethereum's authoritative recovery (used during EVM execution). + authority, err := signed.Authority() + require.NoError(t, err) + require.Equal(t, expectedEVM, authority) + + evmAddr, seiAddr, pubkey, err := RecoverAddressesFromAuthorization(signed) + require.NoError(t, err) + require.Equal(t, expectedEVM, evmAddr) + require.Equal(t, authority, evmAddr) + + // The Sei address and pubkey must match those derived directly from the key. + expEVM, expSei, expPub, err := GetAddressesFromPubkeyBytes(crypto.FromECDSAPub(&privKey.PublicKey)) + require.NoError(t, err) + require.Equal(t, expEVM, evmAddr) + require.Equal(t, expSei, seiAddr) + require.Equal(t, expPub, pubkey) + }) + } +} + +// TestRecoverAddressesFromAuthorizationInvalidSig ensures a malformed authorization +// signature yields an error rather than a bogus address (the ante handler skips these). +func TestRecoverAddressesFromAuthorizationInvalidSig(t *testing.T) { + _, _, _, err := RecoverAddressesFromAuthorization(ethtypes.SetCodeAuthorization{ + ChainID: *uint256.NewInt(1329), + Address: common.HexToAddress("0x00000000000000000000000000000000000000aa"), + Nonce: 1, + V: 0, + R: *uint256.NewInt(0), + S: *uint256.NewInt(0), + }) + require.Error(t, err) +} + func TestPubkeyToEVMAddress(t *testing.T) { tests := []struct { name string diff --git a/x/evm/ante/preprocess.go b/x/evm/ante/preprocess.go index eb8f13a3c2..1b41a68eb4 100644 --- a/x/evm/ante/preprocess.go +++ b/x/evm/ante/preprocess.go @@ -102,9 +102,56 @@ func (p *EVMPreprocessDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate } } + // EIP-7702 authorization authorities are distinct accounts from the tx sender, so the + // sender association above does not cover them. Associate each authority to its true + // (pubkey-derived) Sei address before EVM execution installs delegation code for it. + // Otherwise SetCode creates a mutable direct-cast EVM->Sei mapping that a later + // associatePubKey call can remap, orphaning any staking/distribution state created + // under the direct-cast identity (which can then halt the chain via the distribution + // validator-removal hook). + p.associateAuthorizationAuthorities(ctx, msg, associateHelper) + return next(ctx, tx, simulate) } +// associateAuthorizationAuthorities associates every EIP-7702 authorization authority in +// the transaction with its true (pubkey-derived) Sei address, so that a subsequent +// SetCode for the authority does not create a mutable direct-cast mapping. It is +// best-effort: authorities with invalid signatures (which EVM execution would also skip) +// or that are already associated are left untouched, and an association failure skips +// only that authority rather than rejecting the transaction, which go-ethereum would +// still accept. +func (p *EVMPreprocessDecorator) associateAuthorizationAuthorities(ctx sdk.Context, msg *evmtypes.MsgEVMTransaction, associateHelper *helpers.AssociationHelper) { + txData, err := evmtypes.UnpackTxData(msg.Data) + if err != nil { + return + } + setCodeTx, ok := txData.(*ethtx.SetCodeTx) + if !ok { + // Only SetCode (EIP-7702) transactions carry authorizations. + return + } + ethTx := ethtypes.NewTx(setCodeTx.AsEthereumData()) + for _, auth := range ethTx.SetCodeAuthorizations() { + evmAddr, seiAddr, pubkey, err := helpers.RecoverAddressesFromAuthorization(auth) + if err != nil { + continue + } + // Cross-check against go-ethereum's authoritative recovery so we only ever + // associate the exact address that SetCode will target during execution. + if authAddr, aerr := auth.Authority(); aerr != nil || authAddr != evmAddr { + continue + } + if _, associated := p.evmKeeper.GetEVMAddress(ctx, seiAddr); associated { + continue + } + cacheCtx, write := ctx.CacheContext() + if err := associateHelper.AssociateAddresses(cacheCtx, seiAddr, evmAddr, pubkey, false); err == nil { + write() + } + } +} + func (p *EVMPreprocessDecorator) IsAccountBalancePositive(ctx sdk.Context, seiAddr sdk.AccAddress, evmAddr common.Address) bool { baseDenom := p.evmKeeper.GetBaseDenom(ctx) if amt := p.evmKeeper.BankKeeper().GetBalance(ctx, seiAddr, baseDenom).Amount; amt.IsPositive() { diff --git a/x/evm/ante/preprocess_test.go b/x/evm/ante/preprocess_test.go index 35227a5370..59b1f6f131 100644 --- a/x/evm/ante/preprocess_test.go +++ b/x/evm/ante/preprocess_test.go @@ -14,6 +14,7 @@ import ( ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" + "github.com/holiman/uint256" "github.com/sei-protocol/sei-chain/sei-cosmos/crypto/keys/secp256k1" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" authtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/auth/types" @@ -27,6 +28,74 @@ import ( "github.com/stretchr/testify/require" ) +// TestPreprocessAssociatesSetCodeAuthorities verifies the EIP-7702 root-cause fix: when a +// sponsored SetCode transaction is preprocessed, each authorization authority is associated +// with its true (pubkey-derived) Sei address before EVM execution. This ensures SetCode +// will not create a mutable direct-cast mapping for the authority that a later +// associatePubKey could remap. +func TestPreprocessAssociatesSetCodeAuthorities(t *testing.T) { + k := &testkeeper.EVMTestApp.EvmKeeper + ctx := testkeeper.EVMTestApp.GetContextForDeliverTx(nil) + handler := ante.NewEVMPreprocessDecorator(k, k.AccountKeeper()) + chainID := k.ChainID(ctx) + + sponsorKey, err := crypto.GenerateKey() + require.NoError(t, err) + victimKey, err := crypto.GenerateKey() + require.NoError(t, err) + + victimEVM := crypto.PubkeyToAddress(victimKey.PublicKey) + victimCast := sdk.AccAddress(victimEVM[:]) + _, victimTrueSei, _, err := helpers.GetAddressesFromPubkeyBytes(crypto.FromECDSAPub(&victimKey.PublicKey)) + require.NoError(t, err) + // Sanity: the true Sei address is distinct from the direct-cast address. + require.NotEqual(t, victimCast.String(), victimTrueSei.String()) + + // The victim is unassociated before the transaction. + _, associated := k.GetSeiAddress(ctx, victimEVM) + require.False(t, associated) + + // Victim signs an EIP-7702 authorization; the sponsor submits the SetCode tx. + auth, err := ethtypes.SignSetCode(victimKey, ethtypes.SetCodeAuthorization{ + ChainID: *uint256.MustFromBig(chainID), + Address: common.HexToAddress("0x000000000000000000000000000000000000c0de"), + Nonce: 0, + }) + require.NoError(t, err) + + to := common.HexToAddress("0x00000000000000000000000000000000000000aa") + tx, err := ethtypes.SignNewTx(sponsorKey, ethtypes.NewPragueSigner(chainID), ðtypes.SetCodeTx{ + ChainID: uint256.MustFromBig(chainID), + Nonce: 0, + GasTipCap: uint256.NewInt(1), + GasFeeCap: uint256.NewInt(1), + Gas: 100000, + To: to, + Value: uint256.NewInt(0), + AuthList: []ethtypes.SetCodeAuthorization{auth}, + }) + require.NoError(t, err) + + typedTx, err := ethtx.NewSetCodeTx(tx) + require.NoError(t, err) + msg, err := types.NewMsgEVMTransaction(typedTx) + require.NoError(t, err) + + ctx, err = handler.AnteHandle(ctx, mockTx{msgs: []sdk.Msg{msg}}, false, func(ctx sdk.Context, _ sdk.Tx, _ bool) (sdk.Context, error) { + return ctx, nil + }) + require.NoError(t, err) + + // The authority is now associated with its TRUE Sei address, not the direct-cast one. + gotSei, associated := k.GetSeiAddress(ctx, victimEVM) + require.True(t, associated) + require.Equal(t, victimTrueSei.String(), gotSei.String()) + require.NotEqual(t, victimCast.String(), gotSei.String()) + gotEVM, ok := k.GetEVMAddress(ctx, victimTrueSei) + require.True(t, ok) + require.Equal(t, victimEVM, gotEVM) +} + func TestPreprocessAnteHandler(t *testing.T) { k := &testkeeper.EVMTestApp.EvmKeeper ctx := testkeeper.EVMTestApp.GetContextForDeliverTx(nil)