Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
f1f2017
feat(precompiles): add BalanceHandler to handle native balance change
cloudgray Jun 9, 2025
04fd359
refactor: remove parts of calling SetBalanceChangeEntries
cloudgray Jun 9, 2025
723754e
chore: fix lint
cloudgray Jun 9, 2025
aa2e71f
chore(precompiles/distribution): remove unused helper function
cloudgray Jun 9, 2025
794089e
chore(precompiles): modify comments
cloudgray Jun 9, 2025
958f7bd
chore: restore modification to be applied later
cloudgray Jun 9, 2025
3933be1
chore: fix typo
cloudgray Jun 9, 2025
2251c30
Merge branch 'main' into poc/precompiles-balance-handler
cloudgray Jun 12, 2025
6553235
Merge branch 'main' into poc/precompiles-balance-handler
cloudgray Jun 13, 2025
a62d502
chore: resolve conflict
cloudgray Jun 13, 2025
0ccd712
chore: fix lint
cloudgray Jun 14, 2025
c8f3fcb
Merge branch 'main' into poc/precompiles-balance-handler
cloudgray Jun 26, 2025
e744bd9
test(precompiles/common) add unit test cases
cloudgray Jun 26, 2025
5239be2
chore: fix lint
cloudgray Jun 26, 2025
bf9f101
Merge branch 'main' into poc/precompiles-balance-handler
cloudgray Jun 27, 2025
62aa9f1
fix(test): precompile test case that intermittently fails
cloudgray Jun 27, 2025
17c9466
Merge branch 'main' into poc/precompiles-balance-handler
cloudgray Jun 30, 2025
2ce685a
refactor: move mock evm keeper to x/vm/types/mocks
cloudgray Jun 30, 2025
31e6f8a
Merge remote-tracking branch 'upstream/main' into poc/precompiles-bal…
cloudgray Jul 3, 2025
ea887e0
chore: add KVStoreKeys() method to mock evmKeeper
cloudgray Jul 3, 2025
7c5ff7b
refactoring balance handling
zsystm Jul 3, 2025
b532fd5
test(precompile/common): improve unit test for balance handler
cloudgray Jul 3, 2025
46cd527
Merge pull request #1 from zsystm/poc/precompiles-balance-handler
cloudgray Jul 3, 2025
25b89f3
refactor(precompiles): separate common logic
cloudgray Jul 3, 2025
36ac731
Revert "refactor(precompiles): separate common logic"
cloudgray Jul 3, 2025
b938988
Revert "Merge pull request #1 from zsystm/poc/precompiles-balance-han…
cloudgray Jul 3, 2025
70ea450
Merge branch 'main' into poc/precompiles-balance-handler
vladjdk Jul 4, 2025
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
9 changes: 0 additions & 9 deletions evmd/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -448,15 +448,6 @@ func NewExampleApp(
runtime.ProvideCometInfoService(),
)
// If evidence needs to be handled for the app, set routes in router here and seal
// Note: The evidence precompile allows evidence to be submitted through an EVM transaction.
// If you implement a custom evidence handler in the router that changes token balances (e.g. penalizing
// addresses, deducting fees, etc.), be aware that the precompile logic (e.g. SetBalanceChangeEntries)
// must be properly integrated to reflect these balance changes in the EVM state. Otherwise, there is a risk
// of desynchronization between the Cosmos SDK state and the EVM state when evidence is submitted via the EVM.
//
// For example, if your custom evidence handler deducts tokens from a user’s account, ensure that the evidence
// precompile also applies these deductions through the EVM’s balance tracking. Failing to do so may cause
// inconsistencies in reported balances and break state synchronization.
app.EvidenceKeeper = *evidenceKeeper

// Cosmos EVM keepers
Expand Down
5 changes: 1 addition & 4 deletions precompiles/bank/bank.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ func (p Precompile) RequiredGas(input []byte) uint64 {

// Run executes the precompiled contract bank query methods defined in the ABI.
func (p Precompile) Run(evm *vm.EVM, contract *vm.Contract, readOnly bool) (bz []byte, err error) {
ctx, stateDB, method, initialGas, args, err := p.RunSetup(evm, contract, readOnly, p.IsTransaction)
ctx, _, method, initialGas, args, err := p.RunSetup(evm, contract, readOnly, p.IsTransaction)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -134,9 +134,6 @@ func (p Precompile) Run(evm *vm.EVM, contract *vm.Contract, readOnly bool) (bz [
if !contract.UseGas(cost, nil, tracing.GasChangeCallPrecompiledContract) {
return nil, vm.ErrOutOfGas
}
if err = p.AddJournalEntries(stateDB); err != nil {
return nil, err
}

return bz, nil
}
Expand Down
107 changes: 107 additions & 0 deletions precompiles/common/balance_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package common

import (
"fmt"

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

"github.com/cosmos/evm/utils"
"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 {
prevEventsLen int
}

// NewBalanceHandler creates a new BalanceHandler instance.
func NewBalanceHandler() *BalanceHandler {
return &BalanceHandler{
prevEventsLen: 0,
}
}

// BeforeBalanceChange is called before any balance changes by precompile methods.
// It records the current number of events in the context to later process balance changes
// using the recorded events.
func (bh *BalanceHandler) BeforeBalanceChange(ctx sdk.Context) {
bh.prevEventsLen = len(ctx.EventManager().Events())
}

// 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.
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)
if err != nil {
return fmt.Errorf("failed to parse spender address from event %q: %w", banktypes.EventTypeCoinSpent, err)
}

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)

case banktypes.EventTypeCoinReceived:
receiverHexAddr, err := parseHexAddress(event, banktypes.AttributeKeyReceiver)
if err != nil {
return fmt.Errorf("failed to parse receiver address from event %q: %w", banktypes.EventTypeCoinReceived, err)
}

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)
}
}

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)
}

accAddr, err := sdk.AccAddressFromBech32(attr.Value)
if err != nil {
return common.Address{}, fmt.Errorf("invalid address %q: %w", attr.Value, err)
}

return common.Address(accAddr.Bytes()), nil
}

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)
}

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

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
}
206 changes: 206 additions & 0 deletions precompiles/common/balance_handler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
package common

import (
"testing"

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

testutil "github.com/cosmos/evm/testutil"
testconstants "github.com/cosmos/evm/testutil/constants"
"github.com/cosmos/evm/x/vm/statedb"
evmtypes "github.com/cosmos/evm/x/vm/types"
"github.com/cosmos/evm/x/vm/types/mocks"

storetypes "cosmossdk.io/store/types"

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

func setupBalanceHandlerTest(t *testing.T) {
t.Helper()

sdk.GetConfig().SetBech32PrefixForAccount(testconstants.ExampleBech32Prefix, "")
configurator := evmtypes.NewEVMConfigurator()
configurator.ResetTestConfig()
require.NoError(t, configurator.WithEVMCoinInfo(testconstants.ExampleChainCoinInfo[testconstants.ExampleChainID]).Configure())
}

func TestParseHexAddress(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to turn this into a table-driven test? Since it includes both success and failure cases, I think a table test format would help improve maintainability and readability.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

applied! b532fd5

var accAddr sdk.AccAddress

testCases := []struct {
name string
maleate func() sdk.Event
key string
expAddr common.Address
expError bool
}{
{
name: "valid address",
maleate: func() sdk.Event {
return sdk.NewEvent("bank", sdk.NewAttribute(banktypes.AttributeKeySpender, accAddr.String()))
},
key: banktypes.AttributeKeySpender,
expError: false,
},
{
name: "missing attribute",
maleate: func() sdk.Event {
return sdk.NewEvent("bank")
},
key: banktypes.AttributeKeySpender,
expError: true,
},
{
name: "invalid address",
maleate: func() sdk.Event {
return sdk.NewEvent("bank", sdk.NewAttribute(banktypes.AttributeKeySpender, "invalid"))
},
key: banktypes.AttributeKeySpender,
expError: true,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
setupBalanceHandlerTest(t)

_, addrs, err := testutil.GeneratePrivKeyAddressPairs(1)
require.NoError(t, err)
accAddr = addrs[0]

event := tc.maleate()

addr, err := parseHexAddress(event, tc.key)
if tc.expError {
require.Error(t, err)
return
}

require.NoError(t, err)
require.Equal(t, common.Address(accAddr.Bytes()), addr)
})
}
}

func TestParseAmount(t *testing.T) {
Copy link
Contributor

@zsystm zsystm Jul 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

applied! b532fd5

testCases := []struct {
name string
maleate func() sdk.Event
expAmt *uint256.Int
expError bool
}{
{
name: "valid amount",
maleate: func() sdk.Event {
coinStr := sdk.NewCoins(sdk.NewInt64Coin(evmtypes.GetEVMCoinDenom(), 5)).String()
return sdk.NewEvent("bank", sdk.NewAttribute(sdk.AttributeKeyAmount, coinStr))
},
expAmt: uint256.NewInt(5),
},
{
name: "missing amount",
maleate: func() sdk.Event {
return sdk.NewEvent("bank")
},
expError: true,
},
{
name: "invalid coins",
maleate: func() sdk.Event {
return sdk.NewEvent("bank", sdk.NewAttribute(sdk.AttributeKeyAmount, "invalid"))
},
expError: true,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
setupBalanceHandlerTest(t)

amt, err := parseAmount(tc.maleate())
if tc.expError {
require.Error(t, err)
return
}

require.NoError(t, err)
require.True(t, amt.Eq(tc.expAmt))
})
}
}

func TestAfterBalanceChange(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(common.BytesToHash(ctx.HeaderHash())))

_, addrs, err := testutil.GeneratePrivKeyAddressPairs(2)
require.NoError(t, err)
spenderAcc := addrs[0]
receiverAcc := addrs[1]
spender := common.Address(spenderAcc.Bytes())
receiver := common.Address(receiverAcc.Bytes())

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

bh := NewBalanceHandler()
bh.BeforeBalanceChange(ctx)

coins := sdk.NewCoins(sdk.NewInt64Coin(evmtypes.GetEVMCoinDenom(), 3))
ctx.EventManager().EmitEvents(sdk.Events{
banktypes.NewCoinSpentEvent(spenderAcc, coins),
banktypes.NewCoinReceivedEvent(receiverAcc, coins),
})

err = bh.AfterBalanceChange(ctx, stateDB)
require.NoError(t, err)

require.Equal(t, "2", stateDB.GetBalance(spender).String())
require.Equal(t, "3", stateDB.GetBalance(receiver).String())
}

func TestAfterBalanceChangeErrors(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(common.BytesToHash(ctx.HeaderHash())))

_, addrs, err := testutil.GeneratePrivKeyAddressPairs(1)
require.NoError(t, err)
addr := addrs[0]

bh := NewBalanceHandler()
bh.BeforeBalanceChange(ctx)

// invalid address in event
coins := sdk.NewCoins(sdk.NewInt64Coin(evmtypes.GetEVMCoinDenom(), 1))
ctx.EventManager().EmitEvent(banktypes.NewCoinSpentEvent(addr, coins))
ctx.EventManager().Events()[len(ctx.EventManager().Events())-1].Attributes[0].Value = "invalid"
err = bh.AfterBalanceChange(ctx, stateDB)
require.Error(t, err)

// reset events
ctx = ctx.WithEventManager(sdk.NewEventManager())
bh.BeforeBalanceChange(ctx)

// invalid amount
ev := sdk.NewEvent(banktypes.EventTypeCoinSpent,
sdk.NewAttribute(banktypes.AttributeKeySpender, addr.String()),
sdk.NewAttribute(sdk.AttributeKeyAmount, "invalid"))
ctx.EventManager().EmitEvent(ev)
err = bh.AfterBalanceChange(ctx, stateDB)
require.Error(t, err)
}
Loading
Loading