-
Notifications
You must be signed in to change notification settings - Fork 104
feat(precompiles): add BalanceHandler to handle native balance change #201
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
cloudgray
merged 27 commits into
cosmos:main
from
cloudgray:poc/precompiles-balance-handler
Jul 7, 2025
Merged
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 04fd359
refactor: remove parts of calling SetBalanceChangeEntries
cloudgray 723754e
chore: fix lint
cloudgray aa2e71f
chore(precompiles/distribution): remove unused helper function
cloudgray 794089e
chore(precompiles): modify comments
cloudgray 958f7bd
chore: restore modification to be applied later
cloudgray 3933be1
chore: fix typo
cloudgray 2251c30
Merge branch 'main' into poc/precompiles-balance-handler
cloudgray 6553235
Merge branch 'main' into poc/precompiles-balance-handler
cloudgray a62d502
chore: resolve conflict
cloudgray 0ccd712
chore: fix lint
cloudgray c8f3fcb
Merge branch 'main' into poc/precompiles-balance-handler
cloudgray e744bd9
test(precompiles/common) add unit test cases
cloudgray 5239be2
chore: fix lint
cloudgray bf9f101
Merge branch 'main' into poc/precompiles-balance-handler
cloudgray 62aa9f1
fix(test): precompile test case that intermittently fails
cloudgray 17c9466
Merge branch 'main' into poc/precompiles-balance-handler
cloudgray 2ce685a
refactor: move mock evm keeper to x/vm/types/mocks
cloudgray 31e6f8a
Merge remote-tracking branch 'upstream/main' into poc/precompiles-bal…
cloudgray ea887e0
chore: add KVStoreKeys() method to mock evmKeeper
cloudgray 7c5ff7b
refactoring balance handling
zsystm b532fd5
test(precompile/common): improve unit test for balance handler
cloudgray 46cd527
Merge pull request #1 from zsystm/poc/precompiles-balance-handler
cloudgray 25b89f3
refactor(precompiles): separate common logic
cloudgray 36ac731
Revert "refactor(precompiles): separate common logic"
cloudgray b938988
Revert "Merge pull request #1 from zsystm/poc/precompiles-balance-han…
cloudgray 70ea450
Merge branch 'main' into poc/precompiles-balance-handler
vladjdk File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) { | ||
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ditto There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
applied! b532fd5