Skip to content
Closed
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
24 changes: 24 additions & 0 deletions precompiles/common/balance_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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
}
40 changes: 40 additions & 0 deletions precompiles/common/balance_handler_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package common_test

import (
"bytes"
"testing"

"github.com/ethereum/go-ethereum/common"
Expand Down Expand Up @@ -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)

Expand Down
Loading