Skip to content

Commit cbd04f6

Browse files
committed
Squashed commit of the following:
commit 6da91be7d6aa437ec72bb24b20a447030cd08e89 Author: Kyuhyeon Choi <[email protected]> Date: Tue Jul 8 09:53:15 2025 +0900 chore: fix lint commit 7b2332537fac21ead05b6f193cb2a27588f0bfaa Author: Kyuhyeon Choi <[email protected]> Date: Mon Jul 7 23:23:08 2025 +0900 test(precompiles/gov): modify test case for audit issue Co-Authored-By: zsystm <[email protected]> Co-Authored-By: Vlad J <[email protected]> commit c32a6e5128c592d5fc791fc13733fc52e18553bc Author: Kyuhyeon Choi <[email protected]> Date: Mon Jul 7 23:09:24 2025 +0900 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]> commit 37e0f9a8cdd362c78d411bc4291e61b64bbc3dbc Author: Haber <[email protected]> Date: Mon Jul 7 10:28:01 2025 +0900 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]> commit a5bf7d1f255ab43a28ac19d3348d56f20d3dbe29 Author: Haber <[email protected]> Date: Thu Jun 12 11:17:44 2025 +0900 refactor(precompiles): apply journal-based revert approach (#205) * refactor(precompiles): apply journal-based revert approach * refactor: remove unused Snapshot type * chore: fix lint
1 parent 5d57339 commit cbd04f6

File tree

26 files changed

+935
-664
lines changed

26 files changed

+935
-664
lines changed

evmd/app.go

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -471,15 +471,6 @@ func NewExampleApp(
471471
runtime.ProvideCometInfoService(),
472472
)
473473
// If evidence needs to be handled for the app, set routes in router here and seal
474-
// Note: The evidence precompile allows evidence to be submitted through an EVM transaction.
475-
// If you implement a custom evidence handler in the router that changes token balances (e.g. penalizing
476-
// addresses, deducting fees, etc.), be aware that the precompile logic (e.g. SetBalanceChangeEntries)
477-
// must be properly integrated to reflect these balance changes in the EVM state. Otherwise, there is a risk
478-
// of desynchronization between the Cosmos SDK state and the EVM state when evidence is submitted via the EVM.
479-
//
480-
// For example, if your custom evidence handler deducts tokens from a user’s account, ensure that the evidence
481-
// precompile also applies these deductions through the EVM’s balance tracking. Failing to do so may cause
482-
// inconsistencies in reported balances and break state synchronization.
483474
app.EvidenceKeeper = *evidenceKeeper
484475

485476
// Cosmos EVM keepers

precompiles/bank/bank.go

Lines changed: 26 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -104,47 +104,38 @@ 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, snapshot, 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
}
111111

112112
// This handles any out of gas errors that may occur during the execution of a precompile query.
113113
// It avoids panics and returns the out of gas error so the EVM can continue gracefully.
114-
defer cmn.HandleGasError(ctx, contract, initialGas, &err, stateDB, snapshot)()
115-
116-
return p.RunAtomic(
117-
snapshot,
118-
stateDB,
119-
func() ([]byte, error) {
120-
switch method.Name {
121-
// Bank queries
122-
case BalancesMethod:
123-
bz, err = p.Balances(ctx, contract, method, args)
124-
case TotalSupplyMethod:
125-
bz, err = p.TotalSupply(ctx, contract, method, args)
126-
case SupplyOfMethod:
127-
bz, err = p.SupplyOf(ctx, contract, method, args)
128-
default:
129-
return nil, fmt.Errorf(cmn.ErrUnknownMethod, method.Name)
130-
}
131-
132-
if err != nil {
133-
return nil, err
134-
}
135-
136-
cost := ctx.GasMeter().GasConsumed() - initialGas
137-
138-
if !contract.UseGas(cost, nil, tracing.GasChangeCallPrecompiledContract) {
139-
return nil, vm.ErrOutOfGas
140-
}
141-
if err := p.AddJournalEntries(stateDB, snapshot); err != nil {
142-
return nil, err
143-
}
144-
145-
return bz, nil
146-
},
147-
)
114+
defer cmn.HandleGasError(ctx, contract, initialGas, &err)()
115+
116+
switch method.Name {
117+
// Bank queries
118+
case BalancesMethod:
119+
bz, err = p.Balances(ctx, contract, method, args)
120+
case TotalSupplyMethod:
121+
bz, err = p.TotalSupply(ctx, contract, method, args)
122+
case SupplyOfMethod:
123+
bz, err = p.SupplyOf(ctx, contract, method, args)
124+
default:
125+
return nil, fmt.Errorf(cmn.ErrUnknownMethod, method.Name)
126+
}
127+
128+
if err != nil {
129+
return nil, err
130+
}
131+
132+
cost := ctx.GasMeter().GasConsumed() - initialGas
133+
134+
if !contract.UseGas(cost, nil, tracing.GasChangeCallPrecompiledContract) {
135+
return nil, vm.ErrOutOfGas
136+
}
137+
138+
return bz, nil
148139
}
149140

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

0 commit comments

Comments
 (0)