Skip to content
Merged
8 changes: 4 additions & 4 deletions evmd/precompiles.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,15 +101,15 @@ func NewAvailableStaticPrecompiles(
panic(fmt.Errorf("failed to instantiate bech32 precompile: %w", err))
}

stakingPrecompile, err := stakingprecompile.NewPrecompile(stakingKeeper, options.AddressCodec)
stakingPrecompile, err := stakingprecompile.NewPrecompile(stakingKeeper, bankKeeper, options.AddressCodec)
if err != nil {
panic(fmt.Errorf("failed to instantiate staking precompile: %w", err))
}

distributionPrecompile, err := distprecompile.NewPrecompile(
distributionKeeper,
stakingKeeper,
evmKeeper,
bankKeeper,
options.AddressCodec,
)
if err != nil {
Expand All @@ -132,12 +132,12 @@ func NewAvailableStaticPrecompiles(
panic(fmt.Errorf("failed to instantiate bank precompile: %w", err))
}

govPrecompile, err := govprecompile.NewPrecompile(govKeeper, codec, options.AddressCodec)
govPrecompile, err := govprecompile.NewPrecompile(govKeeper, bankKeeper, codec, options.AddressCodec)
if err != nil {
panic(fmt.Errorf("failed to instantiate gov precompile: %w", err))
}

slashingPrecompile, err := slashingprecompile.NewPrecompile(slashingKeeper, options.ValidatorAddrCodec, options.ConsensusAddrCodec)
slashingPrecompile, err := slashingprecompile.NewPrecompile(slashingKeeper, bankKeeper, options.ValidatorAddrCodec, options.ConsensusAddrCodec)
if err != nil {
panic(fmt.Errorf("failed to instantiate slashing precompile: %w", err))
}
Expand Down
99 changes: 60 additions & 39 deletions precompiles/common/balance_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,29 @@ package common

import (
"fmt"
"math/big"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/holiman/uint256"

"github.com/cosmos/evm/utils"
precisebanktypes "github.com/cosmos/evm/x/precisebank/types"
"github.com/cosmos/evm/x/vm/statedb"
evmtypes "github.com/cosmos/evm/x/vm/types"

sdk "github.com/cosmos/cosmos-sdk/types"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
)

// BalanceHandler is a struct that handles balance changes in the Cosmos SDK context.
type BalanceHandler struct {
bankKeeper BankKeeper
prevEventsLen int
}

// NewBalanceHandler creates a new BalanceHandler instance.
func NewBalanceHandler() *BalanceHandler {
func NewBalanceHandler(bankKeeper BankKeeper) *BalanceHandler {
return &BalanceHandler{
bankKeeper: bankKeeper,
prevEventsLen: 0,
}
}
Expand All @@ -37,71 +39,90 @@ func (bh *BalanceHandler) BeforeBalanceChange(ctx sdk.Context) {
// AfterBalanceChange processes the recorded events and updates the stateDB accordingly.
// It handles the bank events for coin spent and coin received, updating the balances
// of the spender and receiver addresses respectively.
//
// NOTES: Balance change events involving BlockedAddresses are bypassed.
// Native balances are handled separately to prevent cases where a bank coin transfer
// initiated by a precompile is unintentionally overwritten by balance changes from within a contract.

// Typically, accounts registered as BlockedAddresses in app.go—such as module accounts—are not expected to receive coins.
// However, in modules like precisebank, it is common to borrow and repay integer balances
// from the module account to support fractional balance handling.
//
// As a result, even if a module account is marked as a BlockedAddress, a keeper-level SendCoins operation
// can emit an x/bank event in which the module account appears as a spender or receiver.
// If such events are parsed and used to invoke StateDB.AddBalance or StateDB.SubBalance, authorization errors can occur.
//
// To prevent this, balance changes from events involving blocked addresses are not applied to the StateDB.
// Instead, the state changes resulting from the precompile call are applied directly via the MultiStore.
func (bh *BalanceHandler) AfterBalanceChange(ctx sdk.Context, stateDB *statedb.StateDB) error {
events := ctx.EventManager().Events()

for _, event := range events[bh.prevEventsLen:] {
switch event.Type {
case banktypes.EventTypeCoinSpent:
spenderHexAddr, err := parseHexAddress(event, banktypes.AttributeKeySpender)
spenderAddr, err := ParseAddress(event, banktypes.AttributeKeySpender)
if err != nil {
return fmt.Errorf("failed to parse spender address from event %q: %w", banktypes.EventTypeCoinSpent, err)
}
if bh.bankKeeper.BlockedAddr(spenderAddr) {
// Bypass blocked addresses
continue
}

amount, err := parseAmount(event)
amount, err := ParseAmount(event)
if err != nil {
return fmt.Errorf("failed to parse amount from event %q: %w", banktypes.EventTypeCoinSpent, err)
}

stateDB.SubBalance(spenderHexAddr, amount, tracing.BalanceChangeUnspecified)
stateDB.SubBalance(common.BytesToAddress(spenderAddr.Bytes()), amount, tracing.BalanceChangeUnspecified)

case banktypes.EventTypeCoinReceived:
receiverHexAddr, err := parseHexAddress(event, banktypes.AttributeKeyReceiver)
receiverAddr, err := ParseAddress(event, banktypes.AttributeKeyReceiver)
if err != nil {
return fmt.Errorf("failed to parse receiver address from event %q: %w", banktypes.EventTypeCoinReceived, err)
}
if bh.bankKeeper.BlockedAddr(receiverAddr) {
// Bypass blocked addresses
continue
}

amount, err := parseAmount(event)
amount, err := ParseAmount(event)
if err != nil {
return fmt.Errorf("failed to parse amount from event %q: %w", banktypes.EventTypeCoinReceived, err)
}

stateDB.AddBalance(receiverHexAddr, amount, tracing.BalanceChangeUnspecified)
}
}
stateDB.AddBalance(common.BytesToAddress(receiverAddr.Bytes()), amount, tracing.BalanceChangeUnspecified)

return nil
}

func parseHexAddress(event sdk.Event, key string) (common.Address, error) {
attr, ok := event.GetAttribute(key)
if !ok {
return common.Address{}, fmt.Errorf("event %q missing attribute %q", event.Type, key)
}
case precisebanktypes.EventTypeFractionalBalanceChange:
addr, err := ParseAddress(event, precisebanktypes.AttributeKeyAddress)
if err != nil {
return fmt.Errorf("failed to parse address from event %q: %w", precisebanktypes.EventTypeFractionalBalanceChange, err)
}
if bh.bankKeeper.BlockedAddr(addr) {
// Bypass blocked addresses
continue
}

accAddr, err := sdk.AccAddressFromBech32(attr.Value)
if err != nil {
return common.Address{}, fmt.Errorf("invalid address %q: %w", attr.Value, err)
}
delta, err := ParseFractionalAmount(event)
if err != nil {
return fmt.Errorf("failed to parse amount from event %q: %w", precisebanktypes.EventTypeFractionalBalanceChange, err)
}

return common.BytesToAddress(accAddr), nil
}
deltaAbs, err := utils.Uint256FromBigInt(new(big.Int).Abs(delta))
if err != nil {
return fmt.Errorf("failed to convert delta to Uint256: %w", err)
}

func parseAmount(event sdk.Event) (*uint256.Int, error) {
amountAttr, ok := event.GetAttribute(sdk.AttributeKeyAmount)
if !ok {
return nil, fmt.Errorf("event %q missing attribute %q", banktypes.EventTypeCoinSpent, sdk.AttributeKeyAmount)
}
if delta.Sign() == 1 {
stateDB.AddBalance(common.BytesToAddress(addr.Bytes()), deltaAbs, tracing.BalanceChangeUnspecified)
} else if delta.Sign() == -1 {
stateDB.SubBalance(common.BytesToAddress(addr.Bytes()), deltaAbs, tracing.BalanceChangeUnspecified)
}

amountCoins, err := sdk.ParseCoinsNormalized(amountAttr.Value)
if err != nil {
return nil, fmt.Errorf("failed to parse coins from %q: %w", amountAttr.Value, err)
default:
continue
}
}

amountBigInt := amountCoins.AmountOf(evmtypes.GetEVMCoinDenom()).BigInt()
amount, err := utils.Uint256FromBigInt(evmtypes.ConvertAmountTo18DecimalsBigInt(amountBigInt))
if err != nil {
return nil, fmt.Errorf("failed to convert coin amount to Uint256: %w", err)
}
return amount, nil
return nil
}
92 changes: 51 additions & 41 deletions precompiles/common/balance_handler_test.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
package common
package common_test

import (
"testing"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/crypto"
"github.com/holiman/uint256"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"

"github.com/cosmos/evm/crypto/ethsecp256k1"
cmn "github.com/cosmos/evm/precompiles/common"
cmnmocks "github.com/cosmos/evm/precompiles/common/mocks"
testutil "github.com/cosmos/evm/testutil"
testconstants "github.com/cosmos/evm/testutil/constants"
precisebanktypes "github.com/cosmos/evm/x/precisebank/types"
"github.com/cosmos/evm/x/vm/statedb"
evmtypes "github.com/cosmos/evm/x/vm/types"
"github.com/cosmos/evm/x/vm/types/mocks"
Expand All @@ -20,6 +22,7 @@ import (

sdktestutil "github.com/cosmos/cosmos-sdk/testutil"
sdk "github.com/cosmos/cosmos-sdk/types"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
)

Expand All @@ -32,50 +35,43 @@ func setupBalanceHandlerTest(t *testing.T) {
require.NoError(t, configurator.WithEVMCoinInfo(testconstants.ExampleChainCoinInfo[testconstants.ExampleChainID]).Configure())
}

func TestParseHexAddress(t *testing.T) {
// account key, use a constant account to keep unit test deterministic.
priv, err := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
require.NoError(t, err)
privKey := &ethsecp256k1.PrivKey{Key: crypto.FromECDSA(priv)}
accAddr := sdk.AccAddress(privKey.PubKey().Address().Bytes())

func TestParseAddress(t *testing.T) {
testCases := []struct {
name string
maleate func() sdk.Event
key string
expAddr common.Address
expError bool
name string
maleate func() (sdk.AccAddress, sdk.Event)
key string
expBypass bool
expError bool
}{
{
name: "valid address",
maleate: func() sdk.Event {
return sdk.NewEvent("bank", sdk.NewAttribute(banktypes.AttributeKeySpender, accAddr.String()))
maleate: func() (sdk.AccAddress, sdk.Event) {
_, addrs, err := testutil.GeneratePrivKeyAddressPairs(1)
require.NoError(t, err)

return addrs[0], sdk.NewEvent(
banktypes.EventTypeCoinSpent,
sdk.NewAttribute(banktypes.AttributeKeySpender, addrs[0].String()),
)
},
key: banktypes.AttributeKeySpender,
expAddr: common.BytesToAddress(accAddr),
expError: false,
},
{
name: "valid address - BytesToAddress",
maleate: func() sdk.Event {
return sdk.NewEvent("bank", sdk.NewAttribute(banktypes.AttributeKeySpender, "cosmos1ddjhjcmgv95kutgqqqqqqqqqqqqsjugwrg"))
},
key: banktypes.AttributeKeySpender,
expAddr: common.HexToAddress("0x0000006B6579636861696e2d0000000000000001"),
expError: false,
},
{
name: "missing attribute",
maleate: func() sdk.Event {
return sdk.NewEvent("bank")
maleate: func() (sdk.AccAddress, sdk.Event) {
return sdk.AccAddress{}, sdk.NewEvent(banktypes.EventTypeCoinSpent)
},
key: banktypes.AttributeKeySpender,
expError: true,
},
{
name: "invalid address",
maleate: func() sdk.Event {
return sdk.NewEvent("bank", sdk.NewAttribute(banktypes.AttributeKeySpender, "invalid"))
maleate: func() (sdk.AccAddress, sdk.Event) {
return sdk.AccAddress{}, sdk.NewEvent(
banktypes.EventTypeCoinSpent,
sdk.NewAttribute(banktypes.AttributeKeySpender, "invalid"),
)
},
key: banktypes.AttributeKeySpender,
expError: true,
Expand All @@ -86,16 +82,15 @@ func TestParseHexAddress(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
setupBalanceHandlerTest(t)

event := tc.maleate()
ethAddr, event := tc.maleate()

addr, err := parseHexAddress(event, tc.key)
addr, err := cmn.ParseAddress(event, tc.key)
if tc.expError {
require.Error(t, err)
return
} else {
require.NoError(t, err)
require.Equal(t, addr, ethAddr)
}

require.NoError(t, err)
require.Equal(t, tc.expAddr, addr)
})
}
}
Expand Down Expand Up @@ -135,7 +130,7 @@ func TestParseAmount(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
setupBalanceHandlerTest(t)

amt, err := parseAmount(tc.maleate())
amt, err := cmn.ParseAmount(tc.maleate())
if tc.expError {
require.Error(t, err)
return
Expand All @@ -160,14 +155,21 @@ func TestAfterBalanceChange(t *testing.T) {
require.NoError(t, err)
spenderAcc := addrs[0]
receiverAcc := addrs[1]

spender := common.BytesToAddress(spenderAcc)
receiver := common.BytesToAddress(receiverAcc)

// initial balance for spender
stateDB.AddBalance(spender, uint256.NewInt(5), tracing.BalanceChangeUnspecified)

bh := NewBalanceHandler()
bankKeeper := cmnmocks.NewBankKeeper(t)
precisebankModuleAccAddr := authtypes.NewModuleAddress(precisebanktypes.ModuleName)
bankKeeper.Mock.On("BlockedAddr", mock.AnythingOfType("types.AccAddress")).Return(func(addr sdk.AccAddress) bool {
// NOTE: In principle, all blockedAddresses configured in app.go should be checked.
// However, for the sake of simplicity in this test, we assume a scenario where
// only the precisebank module account is treated as a blockedAddress.
return addr.Equals(precisebankModuleAccAddr)
})
bh := cmn.NewBalanceHandler(bankKeeper)
bh.BeforeBalanceChange(ctx)

coins := sdk.NewCoins(sdk.NewInt64Coin(evmtypes.GetEVMCoinDenom(), 3))
Expand Down Expand Up @@ -195,7 +197,15 @@ func TestAfterBalanceChangeErrors(t *testing.T) {
require.NoError(t, err)
addr := addrs[0]

bh := NewBalanceHandler()
bankKeeper := cmnmocks.NewBankKeeper(t)
precisebankModuleAccAddr := authtypes.NewModuleAddress(precisebanktypes.ModuleName)
bankKeeper.Mock.On("BlockedAddr", mock.AnythingOfType("types.AccAddress")).Return(func(addr sdk.AccAddress) bool {
// NOTE: In principle, all blockedAddresses configured in app.go should be checked.
// However, for the sake of simplicity in this test, we assume a scenario where
// only the precisebank module account is treated as a blockedAddress.
return addr.Equals(precisebankModuleAccAddr)
})
bh := cmn.NewBalanceHandler(bankKeeper)
bh.BeforeBalanceChange(ctx)

// invalid address in event
Expand Down
1 change: 1 addition & 0 deletions precompiles/common/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ type BankKeeper interface {
GetBalance(ctx context.Context, addr sdk.AccAddress, denom string) sdk.Coin
SendCoins(ctx context.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) error
SpendableCoin(ctx context.Context, addr sdk.AccAddress, denom string) sdk.Coin
BlockedAddr(addr sdk.AccAddress) bool
}
Loading
Loading