From b9d15c8895a7cdb446a7c865f05c93c8b669c4f9 Mon Sep 17 00:00:00 2001 From: matteyu Date: Thu, 11 Jun 2026 16:15:07 -0700 Subject: [PATCH] fix: distribution precompile 32-byte withdraw address inflates native supply --- precompiles/common/balance_handler.go | 24 +++++++++++++ precompiles/common/balance_handler_test.go | 40 ++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/precompiles/common/balance_handler.go b/precompiles/common/balance_handler.go index 54aa248f..01b52172 100644 --- a/precompiles/common/balance_handler.go +++ b/precompiles/common/balance_handler.go @@ -85,6 +85,10 @@ func (bh *BalanceHandler) AfterBalanceChange(ctx sdk.Context, stateDB *statedb.S // Bypass blocked addresses continue } + if !isMirrorableEVMAddress(spenderAddr) { + // Non-20-byte accounts have no bijective EVM mapping; skip mirroring. + continue + } amount, err := ParseAmount(event) if err != nil { @@ -102,6 +106,12 @@ func (bh *BalanceHandler) AfterBalanceChange(ctx sdk.Context, stateDB *statedb.S // Bypass blocked addresses continue } + if !isMirrorableEVMAddress(receiverAddr) { + // Non-20-byte accounts have no bijective EVM mapping; skip mirroring. + // A 32-byte withdraw address would otherwise be truncated to its trailing + // 20 bytes here and minted a duplicate balance on StateDB commit. + continue + } amount, err := ParseAmount(event) if err != nil { @@ -119,6 +129,10 @@ func (bh *BalanceHandler) AfterBalanceChange(ctx sdk.Context, stateDB *statedb.S // Bypass blocked addresses continue } + if !isMirrorableEVMAddress(addr) { + // Non-20-byte accounts have no bijective EVM mapping; skip mirroring. + continue + } delta, err := ParseFractionalAmount(event) if err != nil { @@ -144,3 +158,13 @@ func (bh *BalanceHandler) AfterBalanceChange(ctx sdk.Context, stateDB *statedb.S return nil } + +// isMirrorableEVMAddress reports whether the SDK account address maps 1:1 to an +// EVM address. Only exactly-20-byte accounts have a bijective mapping; longer +// accounts (e.g. 32-byte module, CosmWasm contract, or bech32m accounts) would be +// truncated by common.BytesToAddress to their trailing 20 bytes. Mirroring such a +// balance change into the StateDB would mint or burn a duplicate balance on commit +// and break the native bank supply invariant, so those events must be skipped. +func isMirrorableEVMAddress(addr sdk.AccAddress) bool { + return len(addr) == common.AddressLength +} diff --git a/precompiles/common/balance_handler_test.go b/precompiles/common/balance_handler_test.go index 0e6c7993..8ea9139e 100644 --- a/precompiles/common/balance_handler_test.go +++ b/precompiles/common/balance_handler_test.go @@ -1,6 +1,7 @@ package common_test import ( + "bytes" "testing" "github.com/ethereum/go-ethereum/common" @@ -186,6 +187,45 @@ func TestAfterBalanceChange(t *testing.T) { require.Equal(t, "3", stateDB.GetBalance(receiver).String()) } +// TestAfterBalanceChangeSkips32ByteAddress is a regression test for the distribution +// precompile 32-byte withdraw address supply inflation. A 32-byte SDK account (e.g. a +// bech32 withdraw address, module, or CosmWasm contract account) must NOT be mirrored +// into the StateDB, because common.BytesToAddress would truncate it to its trailing 20 +// bytes and the StateDB commit would mint a duplicate balance to that EVM account, +// inflating the native token supply. +func TestAfterBalanceChangeSkips32ByteAddress(t *testing.T) { + setupBalanceHandlerTest(t) + + storeKey := storetypes.NewKVStoreKey("test") + tKey := storetypes.NewTransientStoreKey("test_t") + ctx := sdktestutil.DefaultContext(storeKey, tKey) + + stateDB := statedb.New(ctx, mocks.NewEVMKeeper(), statedb.NewEmptyTxConfig()) + + // 32-byte account; its trailing 20 bytes are what common.BytesToAddress would derive. + receiverAcc := sdk.AccAddress(bytes.Repeat([]byte{0xAB}, 32)) + require.Len(t, receiverAcc, 32) + trailing20 := common.BytesToAddress(receiverAcc.Bytes()) + + bankKeeper := cmnmocks.NewBankKeeper(t) + precisebankModuleAccAddr := authtypes.NewModuleAddress(precisebanktypes.ModuleName) + bankKeeper.Mock.On("BlockedAddr", mock.AnythingOfType("types.AccAddress")).Return(func(addr sdk.AccAddress) bool { + return addr.Equals(precisebankModuleAccAddr) + }) + bhf := cmn.NewBalanceHandlerFactory(bankKeeper) + bh := bhf.NewBalanceHandler() + bh.BeforeBalanceChange(ctx) + + coins := sdk.NewCoins(sdk.NewInt64Coin(evmtypes.GetEVMCoinDenom(), 7)) + ctx.EventManager().EmitEvent(banktypes.NewCoinReceivedEvent(receiverAcc, coins)) + + err := bh.AfterBalanceChange(ctx, stateDB) + require.NoError(t, err) + + // The 32-byte receiver must not be mirrored to its trailing-20-byte EVM account. + require.Equal(t, "0", stateDB.GetBalance(trailing20).String()) +} + func TestAfterBalanceChangeErrors(t *testing.T) { setupBalanceHandlerTest(t)