Skip to content

Commit af6f8d4

Browse files
cloudgrayzsystmvladjdk
authored
feat(precompiles): add BalanceHandler to handle native balance change (#201)
* feat(precompiles): add BalanceHandler to handle native balance change * refactor: remove parts of calling SetBalanceChangeEntries * chore: fix lint * chore(precompiles/distribution): remove unused helper function * chore(precompiles): modify comments * chore: restore modification to be applied later * chore: fix typo * chore: resolve conflict * chore: fix lint * test(precompiles/common) add unit test cases * chore: fix lint * fix(test): precompile test case that intermittently fails * refactor: move mock evm keeper to x/vm/types/mocks * chore: add KVStoreKeys() method to mock evmKeeper * refactoring balance handling * test(precompile/common): improve unit test for balance handler * refactor(precompiles): separate common logic * Revert "refactor(precompiles): separate common logic" This reverts commit 25b89f3. * Revert "Merge pull request #1 from zsystm/poc/precompiles-balance-handler" This reverts commit 46cd527, reversing changes made to b532fd5. --------- Co-authored-by: zsystm <[email protected]> Co-authored-by: Vlad J <[email protected]>
1 parent 0517dcf commit af6f8d4

File tree

23 files changed

+539
-407
lines changed

23 files changed

+539
-407
lines changed

evmd/app.go

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -448,15 +448,6 @@ func NewExampleApp(
448448
runtime.ProvideCometInfoService(),
449449
)
450450
// If evidence needs to be handled for the app, set routes in router here and seal
451-
// Note: The evidence precompile allows evidence to be submitted through an EVM transaction.
452-
// If you implement a custom evidence handler in the router that changes token balances (e.g. penalizing
453-
// addresses, deducting fees, etc.), be aware that the precompile logic (e.g. SetBalanceChangeEntries)
454-
// must be properly integrated to reflect these balance changes in the EVM state. Otherwise, there is a risk
455-
// of desynchronization between the Cosmos SDK state and the EVM state when evidence is submitted via the EVM.
456-
//
457-
// For example, if your custom evidence handler deducts tokens from a user’s account, ensure that the evidence
458-
// precompile also applies these deductions through the EVM’s balance tracking. Failing to do so may cause
459-
// inconsistencies in reported balances and break state synchronization.
460451
app.EvidenceKeeper = *evidenceKeeper
461452

462453
// Cosmos EVM keepers

precompiles/bank/bank.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ func (p Precompile) RequiredGas(input []byte) uint64 {
104104

105105
// Run executes the precompiled contract bank query methods defined in the ABI.
106106
func (p Precompile) Run(evm *vm.EVM, contract *vm.Contract, readOnly bool) (bz []byte, err error) {
107-
ctx, stateDB, method, initialGas, args, err := p.RunSetup(evm, contract, readOnly, p.IsTransaction)
107+
ctx, _, method, initialGas, args, err := p.RunSetup(evm, contract, readOnly, p.IsTransaction)
108108
if err != nil {
109109
return nil, err
110110
}
@@ -134,9 +134,6 @@ func (p Precompile) Run(evm *vm.EVM, contract *vm.Contract, readOnly bool) (bz [
134134
if !contract.UseGas(cost, nil, tracing.GasChangeCallPrecompiledContract) {
135135
return nil, vm.ErrOutOfGas
136136
}
137-
if err = p.AddJournalEntries(stateDB); err != nil {
138-
return nil, err
139-
}
140137

141138
return bz, nil
142139
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package common
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/ethereum/go-ethereum/common"
7+
"github.com/ethereum/go-ethereum/core/tracing"
8+
"github.com/holiman/uint256"
9+
10+
"github.com/cosmos/evm/utils"
11+
"github.com/cosmos/evm/x/vm/statedb"
12+
evmtypes "github.com/cosmos/evm/x/vm/types"
13+
14+
sdk "github.com/cosmos/cosmos-sdk/types"
15+
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
16+
)
17+
18+
// BalanceHandler is a struct that handles balance changes in the Cosmos SDK context.
19+
type BalanceHandler struct {
20+
prevEventsLen int
21+
}
22+
23+
// NewBalanceHandler creates a new BalanceHandler instance.
24+
func NewBalanceHandler() *BalanceHandler {
25+
return &BalanceHandler{
26+
prevEventsLen: 0,
27+
}
28+
}
29+
30+
// BeforeBalanceChange is called before any balance changes by precompile methods.
31+
// It records the current number of events in the context to later process balance changes
32+
// using the recorded events.
33+
func (bh *BalanceHandler) BeforeBalanceChange(ctx sdk.Context) {
34+
bh.prevEventsLen = len(ctx.EventManager().Events())
35+
}
36+
37+
// AfterBalanceChange processes the recorded events and updates the stateDB accordingly.
38+
// It handles the bank events for coin spent and coin received, updating the balances
39+
// of the spender and receiver addresses respectively.
40+
func (bh *BalanceHandler) AfterBalanceChange(ctx sdk.Context, stateDB *statedb.StateDB) error {
41+
events := ctx.EventManager().Events()
42+
43+
for _, event := range events[bh.prevEventsLen:] {
44+
switch event.Type {
45+
case banktypes.EventTypeCoinSpent:
46+
spenderHexAddr, err := parseHexAddress(event, banktypes.AttributeKeySpender)
47+
if err != nil {
48+
return fmt.Errorf("failed to parse spender address from event %q: %w", banktypes.EventTypeCoinSpent, err)
49+
}
50+
51+
amount, err := parseAmount(event)
52+
if err != nil {
53+
return fmt.Errorf("failed to parse amount from event %q: %w", banktypes.EventTypeCoinSpent, err)
54+
}
55+
56+
stateDB.SubBalance(spenderHexAddr, amount, tracing.BalanceChangeUnspecified)
57+
58+
case banktypes.EventTypeCoinReceived:
59+
receiverHexAddr, err := parseHexAddress(event, banktypes.AttributeKeyReceiver)
60+
if err != nil {
61+
return fmt.Errorf("failed to parse receiver address from event %q: %w", banktypes.EventTypeCoinReceived, err)
62+
}
63+
64+
amount, err := parseAmount(event)
65+
if err != nil {
66+
return fmt.Errorf("failed to parse amount from event %q: %w", banktypes.EventTypeCoinReceived, err)
67+
}
68+
69+
stateDB.AddBalance(receiverHexAddr, amount, tracing.BalanceChangeUnspecified)
70+
}
71+
}
72+
73+
return nil
74+
}
75+
76+
func parseHexAddress(event sdk.Event, key string) (common.Address, error) {
77+
attr, ok := event.GetAttribute(key)
78+
if !ok {
79+
return common.Address{}, fmt.Errorf("event %q missing attribute %q", event.Type, key)
80+
}
81+
82+
accAddr, err := sdk.AccAddressFromBech32(attr.Value)
83+
if err != nil {
84+
return common.Address{}, fmt.Errorf("invalid address %q: %w", attr.Value, err)
85+
}
86+
87+
return common.Address(accAddr.Bytes()), nil
88+
}
89+
90+
func parseAmount(event sdk.Event) (*uint256.Int, error) {
91+
amountAttr, ok := event.GetAttribute(sdk.AttributeKeyAmount)
92+
if !ok {
93+
return nil, fmt.Errorf("event %q missing attribute %q", banktypes.EventTypeCoinSpent, sdk.AttributeKeyAmount)
94+
}
95+
96+
amountCoins, err := sdk.ParseCoinsNormalized(amountAttr.Value)
97+
if err != nil {
98+
return nil, fmt.Errorf("failed to parse coins from %q: %w", amountAttr.Value, err)
99+
}
100+
101+
amountBigInt := amountCoins.AmountOf(evmtypes.GetEVMCoinDenom()).BigInt()
102+
amount, err := utils.Uint256FromBigInt(evmtypes.ConvertAmountTo18DecimalsBigInt(amountBigInt))
103+
if err != nil {
104+
return nil, fmt.Errorf("failed to convert coin amount to Uint256: %w", err)
105+
}
106+
return amount, nil
107+
}
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
package common
2+
3+
import (
4+
"testing"
5+
6+
"github.com/ethereum/go-ethereum/common"
7+
"github.com/ethereum/go-ethereum/core/tracing"
8+
"github.com/holiman/uint256"
9+
"github.com/stretchr/testify/require"
10+
11+
testutil "github.com/cosmos/evm/testutil"
12+
testconstants "github.com/cosmos/evm/testutil/constants"
13+
"github.com/cosmos/evm/x/vm/statedb"
14+
evmtypes "github.com/cosmos/evm/x/vm/types"
15+
"github.com/cosmos/evm/x/vm/types/mocks"
16+
17+
storetypes "cosmossdk.io/store/types"
18+
19+
sdktestutil "github.com/cosmos/cosmos-sdk/testutil"
20+
sdk "github.com/cosmos/cosmos-sdk/types"
21+
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
22+
)
23+
24+
func setupBalanceHandlerTest(t *testing.T) {
25+
t.Helper()
26+
27+
sdk.GetConfig().SetBech32PrefixForAccount(testconstants.ExampleBech32Prefix, "")
28+
configurator := evmtypes.NewEVMConfigurator()
29+
configurator.ResetTestConfig()
30+
require.NoError(t, configurator.WithEVMCoinInfo(testconstants.ExampleChainCoinInfo[testconstants.ExampleChainID]).Configure())
31+
}
32+
33+
func TestParseHexAddress(t *testing.T) {
34+
var accAddr sdk.AccAddress
35+
36+
testCases := []struct {
37+
name string
38+
maleate func() sdk.Event
39+
key string
40+
expAddr common.Address
41+
expError bool
42+
}{
43+
{
44+
name: "valid address",
45+
maleate: func() sdk.Event {
46+
return sdk.NewEvent("bank", sdk.NewAttribute(banktypes.AttributeKeySpender, accAddr.String()))
47+
},
48+
key: banktypes.AttributeKeySpender,
49+
expError: false,
50+
},
51+
{
52+
name: "missing attribute",
53+
maleate: func() sdk.Event {
54+
return sdk.NewEvent("bank")
55+
},
56+
key: banktypes.AttributeKeySpender,
57+
expError: true,
58+
},
59+
{
60+
name: "invalid address",
61+
maleate: func() sdk.Event {
62+
return sdk.NewEvent("bank", sdk.NewAttribute(banktypes.AttributeKeySpender, "invalid"))
63+
},
64+
key: banktypes.AttributeKeySpender,
65+
expError: true,
66+
},
67+
}
68+
69+
for _, tc := range testCases {
70+
t.Run(tc.name, func(t *testing.T) {
71+
setupBalanceHandlerTest(t)
72+
73+
_, addrs, err := testutil.GeneratePrivKeyAddressPairs(1)
74+
require.NoError(t, err)
75+
accAddr = addrs[0]
76+
77+
event := tc.maleate()
78+
79+
addr, err := parseHexAddress(event, tc.key)
80+
if tc.expError {
81+
require.Error(t, err)
82+
return
83+
}
84+
85+
require.NoError(t, err)
86+
require.Equal(t, common.Address(accAddr.Bytes()), addr)
87+
})
88+
}
89+
}
90+
91+
func TestParseAmount(t *testing.T) {
92+
testCases := []struct {
93+
name string
94+
maleate func() sdk.Event
95+
expAmt *uint256.Int
96+
expError bool
97+
}{
98+
{
99+
name: "valid amount",
100+
maleate: func() sdk.Event {
101+
coinStr := sdk.NewCoins(sdk.NewInt64Coin(evmtypes.GetEVMCoinDenom(), 5)).String()
102+
return sdk.NewEvent("bank", sdk.NewAttribute(sdk.AttributeKeyAmount, coinStr))
103+
},
104+
expAmt: uint256.NewInt(5),
105+
},
106+
{
107+
name: "missing amount",
108+
maleate: func() sdk.Event {
109+
return sdk.NewEvent("bank")
110+
},
111+
expError: true,
112+
},
113+
{
114+
name: "invalid coins",
115+
maleate: func() sdk.Event {
116+
return sdk.NewEvent("bank", sdk.NewAttribute(sdk.AttributeKeyAmount, "invalid"))
117+
},
118+
expError: true,
119+
},
120+
}
121+
122+
for _, tc := range testCases {
123+
t.Run(tc.name, func(t *testing.T) {
124+
setupBalanceHandlerTest(t)
125+
126+
amt, err := parseAmount(tc.maleate())
127+
if tc.expError {
128+
require.Error(t, err)
129+
return
130+
}
131+
132+
require.NoError(t, err)
133+
require.True(t, amt.Eq(tc.expAmt))
134+
})
135+
}
136+
}
137+
138+
func TestAfterBalanceChange(t *testing.T) {
139+
setupBalanceHandlerTest(t)
140+
141+
storeKey := storetypes.NewKVStoreKey("test")
142+
tKey := storetypes.NewTransientStoreKey("test_t")
143+
ctx := sdktestutil.DefaultContext(storeKey, tKey)
144+
145+
stateDB := statedb.New(ctx, mocks.NewEVMKeeper(), statedb.NewEmptyTxConfig(common.BytesToHash(ctx.HeaderHash())))
146+
147+
_, addrs, err := testutil.GeneratePrivKeyAddressPairs(2)
148+
require.NoError(t, err)
149+
spenderAcc := addrs[0]
150+
receiverAcc := addrs[1]
151+
spender := common.Address(spenderAcc.Bytes())
152+
receiver := common.Address(receiverAcc.Bytes())
153+
154+
// initial balance for spender
155+
stateDB.AddBalance(spender, uint256.NewInt(5), tracing.BalanceChangeUnspecified)
156+
157+
bh := NewBalanceHandler()
158+
bh.BeforeBalanceChange(ctx)
159+
160+
coins := sdk.NewCoins(sdk.NewInt64Coin(evmtypes.GetEVMCoinDenom(), 3))
161+
ctx.EventManager().EmitEvents(sdk.Events{
162+
banktypes.NewCoinSpentEvent(spenderAcc, coins),
163+
banktypes.NewCoinReceivedEvent(receiverAcc, coins),
164+
})
165+
166+
err = bh.AfterBalanceChange(ctx, stateDB)
167+
require.NoError(t, err)
168+
169+
require.Equal(t, "2", stateDB.GetBalance(spender).String())
170+
require.Equal(t, "3", stateDB.GetBalance(receiver).String())
171+
}
172+
173+
func TestAfterBalanceChangeErrors(t *testing.T) {
174+
setupBalanceHandlerTest(t)
175+
176+
storeKey := storetypes.NewKVStoreKey("test")
177+
tKey := storetypes.NewTransientStoreKey("test_t")
178+
ctx := sdktestutil.DefaultContext(storeKey, tKey)
179+
stateDB := statedb.New(ctx, mocks.NewEVMKeeper(), statedb.NewEmptyTxConfig(common.BytesToHash(ctx.HeaderHash())))
180+
181+
_, addrs, err := testutil.GeneratePrivKeyAddressPairs(1)
182+
require.NoError(t, err)
183+
addr := addrs[0]
184+
185+
bh := NewBalanceHandler()
186+
bh.BeforeBalanceChange(ctx)
187+
188+
// invalid address in event
189+
coins := sdk.NewCoins(sdk.NewInt64Coin(evmtypes.GetEVMCoinDenom(), 1))
190+
ctx.EventManager().EmitEvent(banktypes.NewCoinSpentEvent(addr, coins))
191+
ctx.EventManager().Events()[len(ctx.EventManager().Events())-1].Attributes[0].Value = "invalid"
192+
err = bh.AfterBalanceChange(ctx, stateDB)
193+
require.Error(t, err)
194+
195+
// reset events
196+
ctx = ctx.WithEventManager(sdk.NewEventManager())
197+
bh.BeforeBalanceChange(ctx)
198+
199+
// invalid amount
200+
ev := sdk.NewEvent(banktypes.EventTypeCoinSpent,
201+
sdk.NewAttribute(banktypes.AttributeKeySpender, addr.String()),
202+
sdk.NewAttribute(sdk.AttributeKeyAmount, "invalid"))
203+
ctx.EventManager().EmitEvent(ev)
204+
err = bh.AfterBalanceChange(ctx, stateDB)
205+
require.Error(t, err)
206+
}

0 commit comments

Comments
 (0)