From 9245bd39aa6702a8412d7b6fac3444067805d1ea Mon Sep 17 00:00:00 2001 From: Kyuhyeon Choi Date: Thu, 14 Aug 2025 13:40:45 +0900 Subject: [PATCH 01/10] feat(x/precisebank): add FracionalBalanceChange event --- x/precisebank/keeper/burn.go | 14 +++------- x/precisebank/keeper/mint.go | 14 +++------- x/precisebank/keeper/send.go | 27 +++----------------- x/precisebank/types/events.go | 31 +++++++++++++++++++++++ x/precisebank/types/fractional_balance.go | 6 +++++ 5 files changed, 47 insertions(+), 45 deletions(-) create mode 100644 x/precisebank/types/events.go diff --git a/x/precisebank/keeper/burn.go b/x/precisebank/keeper/burn.go index 73c5e54f7..0f284f151 100644 --- a/x/precisebank/keeper/burn.go +++ b/x/precisebank/keeper/burn.go @@ -12,7 +12,6 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" - banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" ) // BurnCoins burns coins deletes coins from the balance of the module account. @@ -65,16 +64,6 @@ func (k Keeper) BurnCoins(goCtx context.Context, moduleName string, amt sdk.Coin } } - fullEmissionCoins := sdk.NewCoins(types.SumExtendedCoin(amt)) - if fullEmissionCoins.IsZero() { - return nil - } - - ctx.EventManager().EmitEvents(sdk.Events{ - banktypes.NewCoinBurnEvent(acc.GetAddress(), fullEmissionCoins), - banktypes.NewCoinSpentEvent(acc.GetAddress(), fullEmissionCoins), - }) - return nil } @@ -197,5 +186,8 @@ func (k Keeper) burnExtendedCoin( // Update remainder for burned fractional coins k.SetRemainderAmount(ctx, newRemainder) + // Emit event for fractional balance change + types.EmitFractionalBalanceChange(ctx, moduleAddr, prevFractionalBalance, newFractionalBalance) + return nil } diff --git a/x/precisebank/keeper/mint.go b/x/precisebank/keeper/mint.go index efaec12f6..1c42f1b03 100644 --- a/x/precisebank/keeper/mint.go +++ b/x/precisebank/keeper/mint.go @@ -12,7 +12,6 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" - banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" ) // MintCoins creates new coins from thin air and adds it to the module account. @@ -67,16 +66,6 @@ func (k Keeper) MintCoins(goCtx context.Context, moduleName string, amt sdk.Coin } } - fullEmissionCoins := sdk.NewCoins(types.SumExtendedCoin(amt)) - if fullEmissionCoins.IsZero() { - return nil - } - - ctx.EventManager().EmitEvents(sdk.Events{ - banktypes.NewCoinMintEvent(acc.GetAddress(), fullEmissionCoins), - banktypes.NewCoinReceivedEvent(acc.GetAddress(), fullEmissionCoins), - }) - return nil } @@ -226,5 +215,8 @@ func (k Keeper) mintExtendedCoin( k.SetRemainderAmount(ctx, newRemainder) + // Emit event for fractional balance change + types.EmitFractionalBalanceChange(ctx, moduleAddr, fractionalAmount, newFractionalBalance) + return nil } diff --git a/x/precisebank/keeper/send.go b/x/precisebank/keeper/send.go index 3d3ec54dd..cd5350433 100644 --- a/x/precisebank/keeper/send.go +++ b/x/precisebank/keeper/send.go @@ -90,29 +90,6 @@ func (k Keeper) SendCoins( } } - // Get a full extended coin amount (passthrough integer + fractional) ONLY - // for event attributes. - fullEmissionCoins := sdk.NewCoins(types.SumExtendedCoin(amt)) - - // If no passthrough integer nor fractional coins, then no event emission. - // We also want to emit the event with the whole equivalent extended coin - // if only integer coins are sent. - if fullEmissionCoins.IsZero() { - return nil - } - - // Emit transfer event of extended denom for the FULL equivalent value. - ctx.EventManager().EmitEvents(sdk.Events{ - sdk.NewEvent( - banktypes.EventTypeTransfer, - sdk.NewAttribute(banktypes.AttributeKeyRecipient, to.String()), - sdk.NewAttribute(banktypes.AttributeKeySender, from.String()), - sdk.NewAttribute(sdk.AttributeKeyAmount, fullEmissionCoins.String()), - ), - banktypes.NewCoinSpentEvent(from, fullEmissionCoins), - banktypes.NewCoinReceivedEvent(to, fullEmissionCoins), - }) - return nil } @@ -265,6 +242,10 @@ func (k Keeper) sendExtendedCoins( k.SetFractionalBalance(ctx, from, senderNewFracBal) k.SetFractionalBalance(ctx, to, recipientNewFracBal) + // Emit event for fractional balance change + types.EmitFractionalBalanceChange(ctx, from, senderFracBal, senderNewFracBal) + types.EmitFractionalBalanceChange(ctx, to, recipientFracBal, recipientNewFracBal) + return nil } diff --git a/x/precisebank/types/events.go b/x/precisebank/types/events.go new file mode 100644 index 000000000..f3649eb83 --- /dev/null +++ b/x/precisebank/types/events.go @@ -0,0 +1,31 @@ +package types + +import ( + sdkmath "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +const ( + // Event types for precisebank operations + EventTypeFractionalBalanceChange = "fractional_balance_change" + + // Attribute keys + AttributeKeyAddress = "address" + AttributeKeyDelta = "delta" +) + +func EmitFractionalBalanceChange( + ctx sdk.Context, + address sdk.AccAddress, + beforeAmount sdkmath.Int, + afterAmount sdkmath.Int, +) { + delta := afterAmount.Sub(beforeAmount) + + ctx.EventManager().EmitEvent(sdk.NewEvent( + EventTypeFractionalBalanceChange, + sdk.NewAttribute(AttributeKeyAddress, address.String()), + sdk.NewAttribute(AttributeKeyDelta, delta.String()), + )) +} diff --git a/x/precisebank/types/fractional_balance.go b/x/precisebank/types/fractional_balance.go index 72c9e821a..6b2f883ff 100644 --- a/x/precisebank/types/fractional_balance.go +++ b/x/precisebank/types/fractional_balance.go @@ -32,6 +32,12 @@ func ExtendedCoinDenom() string { return evmtypes.GetEVMCoinExtendedDenom() } +// IsExtendedDenomSameAsIntegerDenom returns true if the extended denom is the same as the integer denom +// This happens in 18-decimal chains where both denoms are identical +func IsExtendedDenomSameAsIntegerDenom() bool { + return IntegerCoinDenom() == ExtendedCoinDenom() +} + // FractionalBalance returns a new FractionalBalance with the given address and // amount. func NewFractionalBalance(address string, amount sdkmath.Int) FractionalBalance { From 001a3283d3223df224a4c1b1b774b9c6d39fc6ab Mon Sep 17 00:00:00 2001 From: Kyuhyeon Choi Date: Thu, 14 Aug 2025 13:54:08 +0900 Subject: [PATCH 02/10] feat(precmopile/common): Add FractionalBalanceChangeEvent to balanceHandler --- precompiles/common/balance_handler.go | 62 +++++++++++++-- precompiles/common/balance_handler_test.go | 92 ++++++++++++---------- precompiles/common/interfaces.go | 1 + precompiles/common/mocks/BankKeeper.go | 21 ++++- precompiles/common/precompile.go | 7 +- precompiles/common/utils.go | 28 +++++-- 6 files changed, 155 insertions(+), 56 deletions(-) diff --git a/precompiles/common/balance_handler.go b/precompiles/common/balance_handler.go index 6f48bbe89..36a0d3c0e 100644 --- a/precompiles/common/balance_handler.go +++ b/precompiles/common/balance_handler.go @@ -2,12 +2,14 @@ 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" @@ -17,12 +19,14 @@ import ( // 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, } } @@ -37,13 +41,28 @@ 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) } @@ -53,20 +72,53 @@ func (bh *BalanceHandler) AfterBalanceChange(ctx sdk.Context, stateDB *statedb.S 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) 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) + + 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 + } + + delta, err := ParseFractionalAmount(event) + if err != nil { + return fmt.Errorf("failed to parse amount from event %q: %w", precisebanktypes.EventTypeFractionalBalanceChange, err) + } + + deltaAbs, err := utils.Uint256FromBigInt(new(big.Int).Abs(delta)) + if err != nil { + return fmt.Errorf("failed to convert delta to Uint256: %w", err) + } + + 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) + } + + default: + continue } } diff --git a/precompiles/common/balance_handler_test.go b/precompiles/common/balance_handler_test.go index d9e94b357..1466e97aa 100644 --- a/precompiles/common/balance_handler_test.go +++ b/precompiles/common/balance_handler_test.go @@ -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" @@ -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" ) @@ -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 := ðsecp256k1.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, @@ -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) }) } } @@ -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 @@ -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)) @@ -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 diff --git a/precompiles/common/interfaces.go b/precompiles/common/interfaces.go index 631f91344..2993bfd45 100644 --- a/precompiles/common/interfaces.go +++ b/precompiles/common/interfaces.go @@ -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 } diff --git a/precompiles/common/mocks/BankKeeper.go b/precompiles/common/mocks/BankKeeper.go index 2ab1c6083..a3c72593c 100644 --- a/precompiles/common/mocks/BankKeeper.go +++ b/precompiles/common/mocks/BankKeeper.go @@ -17,6 +17,24 @@ type BankKeeper struct { mock.Mock } +// BlockedAddr provides a mock function with given fields: addr +func (_m *BankKeeper) BlockedAddr(addr types.AccAddress) bool { + ret := _m.Called(addr) + + if len(ret) == 0 { + panic("no return value specified for BlockedAddr") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(types.AccAddress) bool); ok { + r0 = rf(addr) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + // GetBalance provides a mock function with given fields: ctx, addr, denom func (_m *BankKeeper) GetBalance(ctx context.Context, addr types.AccAddress, denom string) types.Coin { ret := _m.Called(ctx, addr, denom) @@ -137,8 +155,7 @@ func (_m *BankKeeper) SpendableCoin(ctx context.Context, addr types.AccAddress, func NewBankKeeper(t interface { mock.TestingT Cleanup(func()) -}, -) *BankKeeper { +}) *BankKeeper { mock := &BankKeeper{} mock.Mock.Test(t) diff --git a/precompiles/common/precompile.go b/precompiles/common/precompile.go index 0ea006fd8..a1fb80668 100644 --- a/precompiles/common/precompile.go +++ b/precompiles/common/precompile.go @@ -226,8 +226,9 @@ func (p Precompile) standardCallData(contract *vm.Contract) (method *abi.Method, } func (p *Precompile) GetBalanceHandler() *BalanceHandler { - if p.balanceHandler == nil { - p.balanceHandler = NewBalanceHandler() - } return p.balanceHandler } + +func (p *Precompile) SetBalanceHandler(bankKeeper BankKeeper) { + p.balanceHandler = NewBalanceHandler(bankKeeper) +} diff --git a/precompiles/common/utils.go b/precompiles/common/utils.go index 53b8c7867..520dc5e09 100644 --- a/precompiles/common/utils.go +++ b/precompiles/common/utils.go @@ -2,29 +2,33 @@ package common import ( "fmt" + "math/big" - "github.com/ethereum/go-ethereum/common" "github.com/holiman/uint256" "github.com/cosmos/evm/utils" + precisebanktypes "github.com/cosmos/evm/x/precisebank/types" evmtypes "github.com/cosmos/evm/x/vm/types" + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" ) -func ParseHexAddress(event sdk.Event, key string) (common.Address, error) { +// ParseAddress parses the address from the event attributes +func ParseAddress(event sdk.Event, key string) (sdk.AccAddress, error) { attr, ok := event.GetAttribute(key) if !ok { - return common.Address{}, fmt.Errorf("event %q missing attribute %q", event.Type, key) + return sdk.AccAddress{}, 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 sdk.AccAddress{}, fmt.Errorf("invalid address %q: %w", attr.Value, err) } - return common.BytesToAddress(accAddr), nil + return accAddr, nil } func ParseAmount(event sdk.Event) (*uint256.Int, error) { @@ -45,3 +49,17 @@ func ParseAmount(event sdk.Event) (*uint256.Int, error) { } return amount, nil } + +func ParseFractionalAmount(event sdk.Event) (*big.Int, error) { + deltaAttr, ok := event.GetAttribute(precisebanktypes.AttributeKeyDelta) + if !ok { + return nil, fmt.Errorf("event %q missing attribute %q", precisebanktypes.EventTypeFractionalBalanceChange, sdk.AttributeKeyAmount) + } + + delta, ok := sdkmath.NewIntFromString(deltaAttr.Value) + if !ok { + return nil, fmt.Errorf("failed to parse coins from %q", deltaAttr.Value) + } + + return delta.BigInt(), nil +} From fbd1575a8f088d431b2af2a0dfe62411daccc541 Mon Sep 17 00:00:00 2001 From: Kyuhyeon Choi Date: Thu, 14 Aug 2025 14:19:36 +0900 Subject: [PATCH 03/10] feat(precompilse): apply changed balance handler --- precompiles/distribution/distribution.go | 8 ++++---- precompiles/erc20/erc20.go | 12 ++++++++++++ precompiles/erc20/tx.go | 18 ------------------ precompiles/gov/gov.go | 4 ++++ precompiles/ics20/ics20.go | 3 +++ precompiles/slashing/slashing.go | 4 ++++ precompiles/staking/staking.go | 4 ++++ precompiles/werc20/tx.go | 7 ------- precompiles/werc20/werc20.go | 8 ++++++++ 9 files changed, 39 insertions(+), 29 deletions(-) diff --git a/precompiles/distribution/distribution.go b/precompiles/distribution/distribution.go index 2c0f235a9..076ce0021 100644 --- a/precompiles/distribution/distribution.go +++ b/precompiles/distribution/distribution.go @@ -10,7 +10,6 @@ import ( "github.com/ethereum/go-ethereum/core/vm" cmn "github.com/cosmos/evm/precompiles/common" - evmkeeper "github.com/cosmos/evm/x/vm/keeper" evmtypes "github.com/cosmos/evm/x/vm/types" "cosmossdk.io/core/address" @@ -32,7 +31,6 @@ type Precompile struct { cmn.Precompile distributionKeeper distributionkeeper.Keeper stakingKeeper stakingkeeper.Keeper - evmKeeper *evmkeeper.Keeper addrCdc address.Codec } @@ -41,7 +39,7 @@ type Precompile struct { func NewPrecompile( distributionKeeper distributionkeeper.Keeper, stakingKeeper stakingkeeper.Keeper, - evmKeeper *evmkeeper.Keeper, + bankKeeper cmn.BankKeeper, addrCdc address.Codec, ) (*Precompile, error) { newAbi, err := cmn.LoadABI(f, "abi.json") @@ -57,13 +55,15 @@ func NewPrecompile( }, stakingKeeper: stakingKeeper, distributionKeeper: distributionKeeper, - evmKeeper: evmKeeper, addrCdc: addrCdc, } // SetAddress defines the address of the distribution compile contract. p.SetAddress(common.HexToAddress(evmtypes.DistributionPrecompileAddress)) + // Set the balance handler for the precompile. + p.SetBalanceHandler(bankKeeper) + return p, nil } diff --git a/precompiles/erc20/erc20.go b/precompiles/erc20/erc20.go index c7d2a401f..9ac290cd3 100644 --- a/precompiles/erc20/erc20.go +++ b/precompiles/erc20/erc20.go @@ -82,6 +82,10 @@ func NewPrecompile( } // Address defines the address of the ERC-20 precompile contract. p.SetAddress(p.tokenPair.GetERC20Contract()) + + // Set the balance handler for the precompile. + p.SetBalanceHandler(bankKeeper) + return p, nil } @@ -151,6 +155,9 @@ func (p Precompile) run(evm *vm.EVM, contract *vm.Contract, readOnly bool) (bz [ return nil, err } + // Start the balance change handler before executing the precompile. + p.GetBalanceHandler().BeforeBalanceChange(ctx) + // This handles any out of gas errors that may occur during the execution of a precompile tx or query. // It avoids panics and returns the out of gas error so the EVM can continue gracefully. defer cmn.HandleGasError(ctx, contract, initialGas, &err)() @@ -166,6 +173,11 @@ func (p Precompile) run(evm *vm.EVM, contract *vm.Contract, readOnly bool) (bz [ return nil, vm.ErrOutOfGas } + // Process the native balance changes after the method execution. + if err = p.GetBalanceHandler().AfterBalanceChange(ctx, stateDB); err != nil { + return nil, err + } + return bz, nil } diff --git a/precompiles/erc20/tx.go b/precompiles/erc20/tx.go index 54bffdaf9..9df843c32 100644 --- a/precompiles/erc20/tx.go +++ b/precompiles/erc20/tx.go @@ -5,12 +5,8 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/vm" - "github.com/cosmos/evm/utils" - evmtypes "github.com/cosmos/evm/x/vm/types" - "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" @@ -127,20 +123,6 @@ func (p *Precompile) transfer( return nil, ConvertErrToERC20Error(err) } - // TODO: Properly handle native balance changes via the balance handler. - // Currently, decimal conversion issues exist with the precisebank module. - // As a temporary workaround, balances are adjusted directly using add/sub operations. - evmDenom := evmtypes.GetEVMCoinDenom() - if p.tokenPair.Denom == evmDenom { - convertedAmount, err := utils.Uint256FromBigInt(evmtypes.ConvertAmountTo18DecimalsBigInt(amount)) - if err != nil { - return nil, err - } - - stateDB.SubBalance(from, convertedAmount, tracing.BalanceChangeUnspecified) - stateDB.AddBalance(to, convertedAmount, tracing.BalanceChangeUnspecified) - } - if err = p.EmitTransferEvent(ctx, stateDB, from, to, amount); err != nil { return nil, err } diff --git a/precompiles/gov/gov.go b/precompiles/gov/gov.go index a7a5506b4..ca1aeada8 100644 --- a/precompiles/gov/gov.go +++ b/precompiles/gov/gov.go @@ -46,6 +46,7 @@ func LoadABI() (abi.ABI, error) { // PrecompiledContract interface. func NewPrecompile( govKeeper govkeeper.Keeper, + bankKeeper cmn.BankKeeper, codec codec.Codec, addrCdc address.Codec, ) (*Precompile, error) { @@ -68,6 +69,9 @@ func NewPrecompile( // SetAddress defines the address of the gov precompiled contract. p.SetAddress(common.HexToAddress(evmtypes.GovPrecompileAddress)) + // Set the balance handler for the precompile. + p.SetBalanceHandler(bankKeeper) + return p, nil } diff --git a/precompiles/ics20/ics20.go b/precompiles/ics20/ics20.go index 88d840a0e..4ed33272e 100644 --- a/precompiles/ics20/ics20.go +++ b/precompiles/ics20/ics20.go @@ -69,6 +69,9 @@ func NewPrecompile( // SetAddress defines the address of the ICS-20 compile contract. p.SetAddress(common.HexToAddress(evmtypes.ICS20PrecompileAddress)) + // Set the balance handler for the precompile. + p.SetBalanceHandler(bankKeeper) + return p, nil } diff --git a/precompiles/slashing/slashing.go b/precompiles/slashing/slashing.go index 2bd268c5f..3a9c96950 100644 --- a/precompiles/slashing/slashing.go +++ b/precompiles/slashing/slashing.go @@ -46,6 +46,7 @@ func LoadABI() (abi.ABI, error) { // PrecompiledContract interface. func NewPrecompile( slashingKeeper slashingkeeper.Keeper, + bankKeeper cmn.BankKeeper, valCdc, consCdc address.Codec, ) (*Precompile, error) { abi, err := LoadABI() @@ -67,6 +68,9 @@ func NewPrecompile( // SetAddress defines the address of the slashing precompiled contract. p.SetAddress(common.HexToAddress(evmtypes.SlashingPrecompileAddress)) + // Set the balance handler for the precompile. + p.SetBalanceHandler(bankKeeper) + return p, nil } diff --git a/precompiles/staking/staking.go b/precompiles/staking/staking.go index cb4d29049..383e7ca6a 100644 --- a/precompiles/staking/staking.go +++ b/precompiles/staking/staking.go @@ -43,6 +43,7 @@ func LoadABI() (abi.ABI, error) { // PrecompiledContract interface. func NewPrecompile( stakingKeeper stakingkeeper.Keeper, + bankKeeper cmn.BankKeeper, addrCdc address.Codec, ) (*Precompile, error) { abi, err := LoadABI() @@ -62,6 +63,9 @@ func NewPrecompile( // SetAddress defines the address of the staking precompiled contract. p.SetAddress(common.HexToAddress(evmtypes.StakingPrecompileAddress)) + // Set the balance handler for the precompile. + p.SetBalanceHandler(bankKeeper) + return p, nil } diff --git a/precompiles/werc20/tx.go b/precompiles/werc20/tx.go index 1b5dd06db..0b69f71da 100644 --- a/precompiles/werc20/tx.go +++ b/precompiles/werc20/tx.go @@ -4,7 +4,6 @@ import ( "fmt" "math/big" - "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/vm" "github.com/cosmos/evm/x/precisebank/types" @@ -50,12 +49,6 @@ func (p Precompile) Deposit( return nil, err } - // TODO: Properly handle native balance changes via the balance handler. - // Currently, decimal conversion issues exist with the precisebank module. - // As a temporary workaround, balances are adjusted directly using add/sub operations. - stateDB.SubBalance(p.Address(), depositedAmount, tracing.BalanceChangeUnspecified) - stateDB.AddBalance(caller, depositedAmount, tracing.BalanceChangeUnspecified) - if err := p.EmitDepositEvent(ctx, stateDB, caller, depositedAmount.ToBig()); err != nil { return nil, err } diff --git a/precompiles/werc20/werc20.go b/precompiles/werc20/werc20.go index de6ecc24d..0f1312305 100644 --- a/precompiles/werc20/werc20.go +++ b/precompiles/werc20/werc20.go @@ -119,6 +119,9 @@ func (p Precompile) run(evm *vm.EVM, contract *vm.Contract, readOnly bool) (bz [ return nil, err } + // Start the balance change handler before executing the precompile. + p.GetBalanceHandler().BeforeBalanceChange(ctx) + // This handles any out of gas errors that may occur during the execution of // a precompile tx or query. It avoids panics and returns the out of gas error so // the EVM can continue gracefully. @@ -146,6 +149,11 @@ func (p Precompile) run(evm *vm.EVM, contract *vm.Contract, readOnly bool) (bz [ return nil, vm.ErrOutOfGas } + // Process the native balance changes after the method execution. + if err = p.GetBalanceHandler().AfterBalanceChange(ctx, stateDB); err != nil { + return nil, err + } + return bz, nil } From 841c403f7385a0c9c0e4661557cef96c982819e4 Mon Sep 17 00:00:00 2001 From: Kyuhyeon Choi Date: Thu, 14 Aug 2025 16:04:07 +0900 Subject: [PATCH 04/10] fix(precompiles/common): balance handler logic --- precompiles/common/balance_handler.go | 43 ++++----------------------- 1 file changed, 6 insertions(+), 37 deletions(-) diff --git a/precompiles/common/balance_handler.go b/precompiles/common/balance_handler.go index 36a0d3c0e..29292430a 100644 --- a/precompiles/common/balance_handler.go +++ b/precompiles/common/balance_handler.go @@ -6,12 +6,10 @@ import ( "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" @@ -66,8 +64,12 @@ func (bh *BalanceHandler) AfterBalanceChange(ctx sdk.Context, stateDB *statedb.S 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) } @@ -84,7 +86,7 @@ func (bh *BalanceHandler) AfterBalanceChange(ctx sdk.Context, stateDB *statedb.S 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) } @@ -124,36 +126,3 @@ func (bh *BalanceHandler) AfterBalanceChange(ctx sdk.Context, stateDB *statedb.S 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.BytesToAddress(accAddr), 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 -} From ea0d43f3253c5ac527ed1aed574b9a5c976883fc Mon Sep 17 00:00:00 2001 From: Kyuhyeon Choi Date: Thu, 14 Aug 2025 16:05:20 +0900 Subject: [PATCH 05/10] test(precompiles): apply changed balance handler to evmd & integration test suite --- evmd/precompiles.go | 8 +++---- .../precompiles/distribution/test_setup.go | 2 +- .../precompiles/distribution/test_utils.go | 1 + .../integration/precompiles/gov/test_setup.go | 1 + .../precompiles/slashing/test_setup.go | 1 + .../precompiles/staking/test_setup.go | 1 + testutil/integration/base/grpc/grpc.go | 5 +++++ testutil/integration/base/grpc/precisebank.go | 21 +++++++++++++++++++ testutil/integration/base/network/network.go | 2 ++ 9 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 testutil/integration/base/grpc/precisebank.go diff --git a/evmd/precompiles.go b/evmd/precompiles.go index b84977762..30165a0e9 100644 --- a/evmd/precompiles.go +++ b/evmd/precompiles.go @@ -101,7 +101,7 @@ 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)) } @@ -109,7 +109,7 @@ func NewAvailableStaticPrecompiles( distributionPrecompile, err := distprecompile.NewPrecompile( distributionKeeper, stakingKeeper, - evmKeeper, + bankKeeper, options.AddressCodec, ) if err != nil { @@ -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)) } diff --git a/tests/integration/precompiles/distribution/test_setup.go b/tests/integration/precompiles/distribution/test_setup.go index 2fe56f434..e611fea6c 100644 --- a/tests/integration/precompiles/distribution/test_setup.go +++ b/tests/integration/precompiles/distribution/test_setup.go @@ -132,7 +132,7 @@ func (s *PrecompileTestSuite) SetupTest() { s.precompile, err = distribution.NewPrecompile( s.network.App.GetDistrKeeper(), *s.network.App.GetStakingKeeper(), - s.network.App.GetEVMKeeper(), + s.network.App.GetBankKeeper(), address.NewBech32Codec(sdk.GetConfig().GetBech32AccountAddrPrefix()), ) if err != nil { diff --git a/tests/integration/precompiles/distribution/test_utils.go b/tests/integration/precompiles/distribution/test_utils.go index fe252380e..d5f03c6a5 100644 --- a/tests/integration/precompiles/distribution/test_utils.go +++ b/tests/integration/precompiles/distribution/test_utils.go @@ -84,6 +84,7 @@ func (s *PrecompileTestSuite) fundAccountWithBaseDenom(ctx sdk.Context, addr sdk func (s *PrecompileTestSuite) getStakingPrecompile() (*staking.Precompile, error) { return staking.NewPrecompile( *s.network.App.GetStakingKeeper(), + s.network.App.GetBankKeeper(), address.NewBech32Codec(sdk.GetConfig().GetBech32AccountAddrPrefix()), ) } diff --git a/tests/integration/precompiles/gov/test_setup.go b/tests/integration/precompiles/gov/test_setup.go index 4e7ec6eef..1c71b3f4f 100644 --- a/tests/integration/precompiles/gov/test_setup.go +++ b/tests/integration/precompiles/gov/test_setup.go @@ -138,6 +138,7 @@ func (s *PrecompileTestSuite) SetupTest() { if s.precompile, err = gov.NewPrecompile( s.network.App.GetGovKeeper(), + s.network.App.GetBankKeeper(), s.network.App.AppCodec(), address.NewBech32Codec(sdk.GetConfig().GetBech32AccountAddrPrefix()), ); err != nil { diff --git a/tests/integration/precompiles/slashing/test_setup.go b/tests/integration/precompiles/slashing/test_setup.go index f64ee11a6..7558a34f5 100644 --- a/tests/integration/precompiles/slashing/test_setup.go +++ b/tests/integration/precompiles/slashing/test_setup.go @@ -56,6 +56,7 @@ func (s *PrecompileTestSuite) SetupTest() { if s.precompile, err = slashing.NewPrecompile( s.network.App.GetSlashingKeeper(), + s.network.App.GetBankKeeper(), address.NewBech32Codec(sdk.GetConfig().GetBech32ValidatorAddrPrefix()), address.NewBech32Codec(sdk.GetConfig().GetBech32ConsensusAddrPrefix()), ); err != nil { diff --git a/tests/integration/precompiles/staking/test_setup.go b/tests/integration/precompiles/staking/test_setup.go index c20d7d42e..2e7aedfa5 100644 --- a/tests/integration/precompiles/staking/test_setup.go +++ b/tests/integration/precompiles/staking/test_setup.go @@ -82,6 +82,7 @@ func (s *PrecompileTestSuite) SetupTest() { if s.precompile, err = staking.NewPrecompile( *s.network.App.GetStakingKeeper(), + s.network.App.GetBankKeeper(), address.NewBech32Codec(sdk.GetConfig().GetBech32AccountAddrPrefix()), ); err != nil { panic(err) diff --git a/testutil/integration/base/grpc/grpc.go b/testutil/integration/base/grpc/grpc.go index af27a4d40..539a94a04 100644 --- a/testutil/integration/base/grpc/grpc.go +++ b/testutil/integration/base/grpc/grpc.go @@ -2,6 +2,7 @@ package grpc import ( "github.com/cosmos/evm/testutil/integration/base/network" + precisebanktypes "github.com/cosmos/evm/x/precisebank/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/authz" @@ -30,6 +31,10 @@ type Handler interface { GetAllBalances(address sdk.AccAddress) (*banktypes.QueryAllBalancesResponse, error) GetTotalSupply() (*banktypes.QueryTotalSupplyResponse, error) + // PreciseBank methods + Remainder() (*precisebanktypes.QueryRemainderResponse, error) + FractionalBalance(address sdk.AccAddress) (*precisebanktypes.QueryFractionalBalanceResponse, error) + // Staking methods GetDelegation(delegatorAddress string, validatorAddress string) (*stakingtypes.QueryDelegationResponse, error) GetDelegatorDelegations(delegatorAddress string) (*stakingtypes.QueryDelegatorDelegationsResponse, error) diff --git a/testutil/integration/base/grpc/precisebank.go b/testutil/integration/base/grpc/precisebank.go new file mode 100644 index 000000000..ca3ad4e2b --- /dev/null +++ b/testutil/integration/base/grpc/precisebank.go @@ -0,0 +1,21 @@ +package grpc + +import ( + "context" + + precisebanktypes "github.com/cosmos/evm/x/precisebank/types" + + sdktypes "github.com/cosmos/cosmos-sdk/types" +) + +func (gqh *IntegrationHandler) Remainder() (*precisebanktypes.QueryRemainderResponse, error) { + preciseBankClient := gqh.network.GetPreciseBankClient() + return preciseBankClient.Remainder(context.Background(), &precisebanktypes.QueryRemainderRequest{}) +} + +func (gqh *IntegrationHandler) FractionalBalance(address sdktypes.AccAddress) (*precisebanktypes.QueryFractionalBalanceResponse, error) { + preciseBankClient := gqh.network.GetPreciseBankClient() + return preciseBankClient.FractionalBalance(context.Background(), &precisebanktypes.QueryFractionalBalanceRequest{ + Address: address.String(), + }) +} diff --git a/testutil/integration/base/network/network.go b/testutil/integration/base/network/network.go index c03a3642d..37d11b747 100644 --- a/testutil/integration/base/network/network.go +++ b/testutil/integration/base/network/network.go @@ -6,6 +6,7 @@ import ( abcitypes "github.com/cometbft/cometbft/abci/types" + precisebanktypes "github.com/cosmos/evm/x/precisebank/types" ibctesting "github.com/cosmos/ibc-go/v10/testing" sdktypes "github.com/cosmos/cosmos-sdk/types" @@ -37,6 +38,7 @@ type Network interface { GetAuthClient() authtypes.QueryClient GetAuthzClient() authz.QueryClient GetBankClient() banktypes.QueryClient + GetPreciseBankClient() precisebanktypes.QueryClient GetStakingClient() stakingtypes.QueryClient GetDistrClient() distrtypes.QueryClient From 9827fb13c70618e4c1d2dac3c52ef7a5232e8ea8 Mon Sep 17 00:00:00 2001 From: Kyuhyeon Choi Date: Thu, 14 Aug 2025 16:05:54 +0900 Subject: [PATCH 06/10] test(precompiles/werc20): fix integration test --- .../precompiles/werc20/test_integration.go | 370 ++++++++++-------- 1 file changed, 214 insertions(+), 156 deletions(-) diff --git a/tests/integration/precompiles/werc20/test_integration.go b/tests/integration/precompiles/werc20/test_integration.go index 82f257028..9e726d346 100644 --- a/tests/integration/precompiles/werc20/test_integration.go +++ b/tests/integration/precompiles/werc20/test_integration.go @@ -1,6 +1,7 @@ package werc20 import ( + "fmt" "math/big" "testing" @@ -30,6 +31,7 @@ import ( "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" ) // ------------------------------------------------------------------------------------------------- @@ -44,11 +46,17 @@ type PrecompileIntegrationTestSuite struct { wrappedCoinDenom string - // WEVMOS related fields + // WERC20 precompile instance and configuration precompile *werc20.Precompile precompileAddrHex string } +// BalanceSnapshot represents a snapshot of account balances for testing +type BalanceSnapshot struct { + IntegerBalance *big.Int + FractionalBalance *big.Int +} + func TestPrecompileIntegrationTestSuite(t *testing.T, create network.CreateEvmApp, options ...network.ConfigOption) { _ = DescribeTableSubtree("a user interact with the WEVMOS precompiled contract", func(chainId testconstants.ChainID) { var ( @@ -61,13 +69,111 @@ func TestPrecompileIntegrationTestSuite(t *testing.T, create network.CreateEvmAp txSender, user keyring.Key revertContractAddr common.Address + + precisebankModuleAccAddr sdk.AccAddress + + // Shared balance snapshots for common test scenarios + senderBeforeSnapshot *BalanceSnapshot + receiverBeforeSnapshot *BalanceSnapshot + precompileBeforeSnapshot *BalanceSnapshot + contractBeforeSnapshot *BalanceSnapshot + precisebankModuleBeforeSnapshot *BalanceSnapshot + + // Expected balance changes (set by individual tests) + senderIntegerDelta *big.Int + senderFractionalDelta *big.Int + receiverIntegerDelta *big.Int + receiverFractionalDelta *big.Int + precompileIntegerDelta *big.Int + precompileFractionalDelta *big.Int + contractIntegerDelta *big.Int + contractFractionalDelta *big.Int + precisebankIntegerDelta *big.Int + precisebankFractionalDelta *big.Int + precisebankRemainder *big.Int ) - depositAmount := big.NewInt(1e18) - depositFractional := big.NewInt(1) + // Configure deposit amounts with integer and fractional components to test + // precise balance handling across different decimal configurations + var conversionFactor *big.Int + switch chainId { + case testconstants.SixDecimalsChainID: + conversionFactor = big.NewInt(1e12) // For 6-decimal chains + case testconstants.TwelveDecimalsChainID: + conversionFactor = big.NewInt(1e6) // For 12-decimal chains + default: + conversionFactor = big.NewInt(1) // For 18-decimal chains + } + + // Create deposit with 1000 integer units + fractional part + depositAmount := big.NewInt(1000) + depositAmount = depositAmount.Mul(depositAmount, conversionFactor) // 1000 integer units + depositFractional := new(big.Int).Div(new(big.Int).Mul(conversionFactor, big.NewInt(3)), big.NewInt(10)) // 0.3 * conversion factor as fractional depositAmount = depositAmount.Add(depositAmount, depositFractional) + withdrawAmount := depositAmount - transferAmount := depositAmount + transferAmount := big.NewInt(10) // Start with 10 integer units + + // Helper functions for balance snapshot management + resetExpectedDeltas := func() { + senderIntegerDelta = big.NewInt(0) + senderFractionalDelta = big.NewInt(0) + + receiverIntegerDelta = big.NewInt(0) + receiverFractionalDelta = big.NewInt(0) + + precompileIntegerDelta = big.NewInt(0) + precompileFractionalDelta = big.NewInt(0) + + contractIntegerDelta = big.NewInt(0) + contractFractionalDelta = big.NewInt(0) + + precisebankIntegerDelta = big.NewInt(0) + precisebankFractionalDelta = big.NewInt(0) + precisebankRemainder = big.NewInt(0) + } + + takeSnapshots := func() { + var err error + + senderBeforeSnapshot, err = is.getBalanceSnapshot(txSender.AccAddr) + Expect(err).ToNot(HaveOccurred(), "failed to get sender balance snapshot") + + receiverBeforeSnapshot, err = is.getBalanceSnapshot(user.AccAddr) + Expect(err).ToNot(HaveOccurred(), "failed to get receiver balance snapshot") + + precompileBeforeSnapshot, err = is.getBalanceSnapshot(callsData.precompileAddr.Bytes()) + Expect(err).ToNot(HaveOccurred(), "failed to get precompile balance snapshot") + + contractBeforeSnapshot, err = is.getBalanceSnapshot(revertContractAddr.Bytes()) + Expect(err).ToNot(HaveOccurred(), "failed to get contract balance snapshot") + + precisebankModuleAccAddr = authtypes.NewModuleAddress(precisebanktypes.ModuleName) + precisebankModuleBeforeSnapshot, err = is.getBalanceSnapshot(precisebankModuleAccAddr) + Expect(err).ToNot(HaveOccurred(), "failed to get precisebank module balance snapshot") + } + + verifyBalanceChanges := func() { + is.expectBalanceChange(txSender.AccAddr, senderBeforeSnapshot, + senderIntegerDelta, senderFractionalDelta, "sender") + + is.expectBalanceChange(user.AccAddr, receiverBeforeSnapshot, + receiverIntegerDelta, receiverFractionalDelta, "receiver") + + is.expectBalanceChange(callsData.precompileAddr.Bytes(), precompileBeforeSnapshot, + precompileIntegerDelta, precompileFractionalDelta, "precompile") + + is.expectBalanceChange(revertContractAddr.Bytes(), contractBeforeSnapshot, + contractIntegerDelta, contractFractionalDelta, "contract") + + is.expectBalanceChange(precisebankModuleAccAddr, precisebankModuleBeforeSnapshot, + precisebankIntegerDelta, precisebankFractionalDelta, "precisebank module") + + res, err := is.grpcHandler.Remainder() + Expect(err).ToNot(HaveOccurred(), "failed to get precisebank module remainder") + actualRemainder := res.Remainder.Amount.BigInt() + Expect(actualRemainder).To(Equal(precisebankRemainder)) + } BeforeEach(func() { is = new(PrecompileIntegrationTestSuite) @@ -183,20 +289,25 @@ func TestPrecompileIntegrationTestSuite(t *testing.T, create network.CreateEvmAp withdrawCheck = passCheck.WithExpEvents(werc20.EventTypeWithdrawal) depositCheck = passCheck.WithExpEvents(werc20.EventTypeDeposit) transferCheck = passCheck.WithExpEvents(erc20.EventTypeTransfer) + + // Reset balance tracking state for each test + resetExpectedDeltas() }) + + // JustBeforeEach takes snapshots after individual test setup + JustBeforeEach(func() { + takeSnapshots() + }) + + // AfterEach verifies balance changes + AfterEach(func() { + verifyBalanceChanges() + }) + Context("calling a specific wrapped coin method", func() { Context("and funds are part of the transaction", func() { When("the method is deposit", func() { It("it should return funds to sender and emit the event", func() { - // Store initial balance to verify that sender - // balance remains the same after the contract call. - ctx := is.network.GetContext() - bk := is.network.App.GetBankKeeper() - pbk := is.network.App.GetPreciseBankKeeper() - initBalance := pbk.GetBalance(ctx, user.Addr.Bytes(), precisebanktypes.ExtendedCoinDenom()) - Expect(is.network.NextBlock()).ToNot(HaveOccurred(), "error on NextBlock") - - initAllBalances := bk.GetAllBalances(ctx, user.Addr.Bytes()) txArgs, callArgs := callsData.getTxAndCallArgs(directCall, werc20.DepositMethod) txArgs.Amount = depositAmount @@ -204,62 +315,39 @@ func TestPrecompileIntegrationTestSuite(t *testing.T, create network.CreateEvmAp _, _, err := is.factory.CallContractAndCheckLogs(user.Priv, txArgs, callArgs, depositCheck) Expect(err).ToNot(HaveOccurred(), "unexpected error calling the precompile") Expect(is.network.NextBlock()).ToNot(HaveOccurred(), "error on NextBlock") - - finalBalance := pbk.GetBalance(ctx, user.Addr.Bytes(), precisebanktypes.ExtendedCoinDenom()) - precompileBalance := pbk.GetBalance(ctx, callsData.precompileAddr.Bytes(), precisebanktypes.ExtendedCoinDenom()) - Expect(precompileBalance.Amount.String()).To(Equal("0")) - Expect(finalBalance.String()).To(Equal(initBalance.String())) - - finalAllBalances := bk.GetAllBalances(ctx, user.Addr.Bytes()) - Expect(finalAllBalances).To(Equal(initAllBalances)) }) It("it should consume at least the deposit requested gas", func() { txArgs, callArgs := callsData.getTxAndCallArgs(directCall, werc20.DepositMethod) txArgs.Amount = depositAmount - _, ethRes, _ := is.factory.CallContractAndCheckLogs(user.Priv, txArgs, callArgs, depositCheck) + _, ethRes, err := is.factory.CallContractAndCheckLogs(user.Priv, txArgs, callArgs, depositCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected error calling the precompile") Expect(is.network.NextBlock()).ToNot(HaveOccurred(), "error on NextBlock") - Expect(ethRes.GasUsed).To(BeNumerically(">=", werc20.DepositRequiredGas), "expected different gas used for deposit") }) }) //nolint:dupl When("no calldata is provided", func() { It("it should call the receive which behave like deposit", func() { - ctx := is.network.GetContext() - pbk := is.network.App.GetPreciseBankKeeper() - initBalance := pbk.GetBalance(ctx, user.Addr.Bytes(), precisebanktypes.ExtendedCoinDenom()) - Expect(is.network.NextBlock()).ToNot(HaveOccurred(), "error on NextBlock") - txArgs, callArgs := callsData.getTxAndCallArgs(directCall, "") txArgs.Amount = depositAmount _, _, err := is.factory.CallContractAndCheckLogs(user.Priv, txArgs, callArgs, depositCheck) Expect(err).ToNot(HaveOccurred(), "unexpected error calling the precompile") Expect(is.network.NextBlock()).ToNot(HaveOccurred(), "error on NextBlock") - - finalBalance := pbk.GetBalance(ctx, user.Addr.Bytes(), precisebanktypes.ExtendedCoinDenom()) - precompileBalance := pbk.GetBalance(ctx, callsData.precompileAddr.Bytes(), precisebanktypes.ExtendedCoinDenom()) - Expect(precompileBalance.Amount.String()).To(Equal("0")) - Expect(finalBalance).To(Equal(initBalance)) }) It("it should consume at least the deposit requested gas", func() { txArgs, callArgs := callsData.getTxAndCallArgs(directCall, werc20.DepositMethod) txArgs.Amount = depositAmount - _, ethRes, _ := is.factory.CallContractAndCheckLogs(user.Priv, txArgs, callArgs, depositCheck) + _, ethRes, err := is.factory.CallContractAndCheckLogs(user.Priv, txArgs, callArgs, depositCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected error calling the precompile") Expect(is.network.NextBlock()).ToNot(HaveOccurred(), "error on NextBlock") - Expect(ethRes.GasUsed).To(BeNumerically(">=", werc20.DepositRequiredGas), "expected different gas used for receive") }) }) When("the specified method is too short", func() { It("it should call the fallback which behave like deposit", func() { - ctx := is.network.GetContext() - pbk := is.network.App.GetPreciseBankKeeper() - initBalance := pbk.GetBalance(ctx, user.Addr.Bytes(), precisebanktypes.ExtendedCoinDenom()) - Expect(is.network.NextBlock()).ToNot(HaveOccurred(), "error on NextBlock") - txArgs, callArgs := callsData.getTxAndCallArgs(directCall, "") txArgs.Amount = depositAmount // Short method is directly set in the input to skip ABI validation @@ -268,11 +356,6 @@ func TestPrecompileIntegrationTestSuite(t *testing.T, create network.CreateEvmAp _, _, err := is.factory.CallContractAndCheckLogs(user.Priv, txArgs, callArgs, depositCheck) Expect(err).ToNot(HaveOccurred(), "unexpected error calling the precompile") Expect(is.network.NextBlock()).ToNot(HaveOccurred(), "error on NextBlock") - - finalBalance := pbk.GetBalance(ctx, user.Addr.Bytes(), precisebanktypes.ExtendedCoinDenom()) - precompileBalance := pbk.GetBalance(ctx, callsData.precompileAddr.Bytes(), precisebanktypes.ExtendedCoinDenom()) - Expect(precompileBalance.Amount.String()).To(Equal("0")) - Expect(finalBalance).To(Equal(initBalance)) }) It("it should consume at least the deposit requested gas", func() { txArgs, callArgs := callsData.getTxAndCallArgs(directCall, "") @@ -280,19 +363,14 @@ func TestPrecompileIntegrationTestSuite(t *testing.T, create network.CreateEvmAp // Short method is directly set in the input to skip ABI validation txArgs.Input = []byte{1, 2, 3} - _, ethRes, _ := is.factory.CallContractAndCheckLogs(user.Priv, txArgs, callArgs, depositCheck) + _, ethRes, err := is.factory.CallContractAndCheckLogs(user.Priv, txArgs, callArgs, depositCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected error calling the precompile") Expect(is.network.NextBlock()).ToNot(HaveOccurred(), "error on NextBlock") - Expect(ethRes.GasUsed).To(BeNumerically(">=", werc20.DepositRequiredGas), "expected different gas used for fallback") }) }) When("the specified method does not exist", func() { It("it should call the fallback which behave like deposit", func() { - ctx := is.network.GetContext() - pbk := is.network.App.GetPreciseBankKeeper() - initBalance := pbk.GetBalance(ctx, user.Addr.Bytes(), precisebanktypes.ExtendedCoinDenom()) - Expect(is.network.NextBlock()).ToNot(HaveOccurred(), "error on NextBlock") - txArgs, callArgs := callsData.getTxAndCallArgs(directCall, "") txArgs.Amount = depositAmount // Wrong method is directly set in the input to skip ABI validation @@ -301,11 +379,6 @@ func TestPrecompileIntegrationTestSuite(t *testing.T, create network.CreateEvmAp _, _, err := is.factory.CallContractAndCheckLogs(user.Priv, txArgs, callArgs, depositCheck) Expect(err).ToNot(HaveOccurred(), "unexpected error calling the precompile") Expect(is.network.NextBlock()).ToNot(HaveOccurred(), "error on NextBlock") - - finalBalance := pbk.GetBalance(ctx, user.Addr.Bytes(), precisebanktypes.ExtendedCoinDenom()) - precompileBalance := pbk.GetBalance(ctx, callsData.precompileAddr.Bytes(), precisebanktypes.ExtendedCoinDenom()) - Expect(precompileBalance.Amount.String()).To(Equal("0")) - Expect(finalBalance).To(Equal(initBalance)) }) It("it should consume at least the deposit requested gas", func() { txArgs, callArgs := callsData.getTxAndCallArgs(directCall, "") @@ -313,9 +386,9 @@ func TestPrecompileIntegrationTestSuite(t *testing.T, create network.CreateEvmAp // Wrong method is directly set in the input to skip ABI validation txArgs.Input = []byte("nonExistingMethod") - _, ethRes, _ := is.factory.CallContractAndCheckLogs(user.Priv, txArgs, callArgs, depositCheck) + _, ethRes, err := is.factory.CallContractAndCheckLogs(user.Priv, txArgs, callArgs, depositCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected error calling the precompile") Expect(is.network.NextBlock()).ToNot(HaveOccurred(), "error on NextBlock") - Expect(ethRes.GasUsed).To(BeNumerically(">=", werc20.DepositRequiredGas), "expected different gas used for fallback") }) }) @@ -323,13 +396,6 @@ func TestPrecompileIntegrationTestSuite(t *testing.T, create network.CreateEvmAp Context("and funds are NOT part of the transaction", func() { When("the method is withdraw", func() { It("it should fail if user doesn't have enough funds", func() { - // Store initial balance to verify withdraw is a no-op and sender - // balance remains the same after the contract call. - ctx := is.network.GetContext() - pbk := is.network.App.GetPreciseBankKeeper() - initBalance := pbk.GetBalance(ctx, user.Addr.Bytes(), precisebanktypes.ExtendedCoinDenom()) - Expect(is.network.NextBlock()).ToNot(HaveOccurred(), "error on NextBlock") - newUserAcc, newUserPriv := utiltx.NewAccAddressAndKey() newUserBalance := sdk.Coins{sdk.Coin{ Denom: evmtypes.GetEVMCoinDenom(), @@ -344,75 +410,44 @@ func TestPrecompileIntegrationTestSuite(t *testing.T, create network.CreateEvmAp _, _, err = is.factory.CallContractAndCheckLogs(newUserPriv, txArgs, callArgs, withdrawCheck) Expect(err).To(HaveOccurred(), "expected an error because not enough funds") Expect(is.network.NextBlock()).ToNot(HaveOccurred(), "error on NextBlock") - - finalBalance := pbk.GetBalance(ctx, user.Addr.Bytes(), precisebanktypes.ExtendedCoinDenom()) - precompileBalance := pbk.GetBalance(ctx, callsData.precompileAddr.Bytes(), precisebanktypes.ExtendedCoinDenom()) - Expect(precompileBalance.Amount.String()).To(Equal("0")) - Expect(finalBalance).To(Equal(initBalance)) }) It("it should be a no-op and emit the event", func() { - ctx := is.network.GetContext() - pbk := is.network.App.GetPreciseBankKeeper() - initBalance := pbk.GetBalance(ctx, user.Addr.Bytes(), precisebanktypes.ExtendedCoinDenom()) - Expect(is.network.NextBlock()).ToNot(HaveOccurred(), "error on NextBlock") - txArgs, callArgs := callsData.getTxAndCallArgs(directCall, werc20.WithdrawMethod, withdrawAmount) _, _, err := is.factory.CallContractAndCheckLogs(user.Priv, txArgs, callArgs, withdrawCheck) Expect(err).ToNot(HaveOccurred(), "unexpected error calling the precompile") Expect(is.network.NextBlock()).ToNot(HaveOccurred(), "error on NextBlock") - - finalBalance := pbk.GetBalance(ctx, user.Addr.Bytes(), precisebanktypes.ExtendedCoinDenom()) - precompileBalance := pbk.GetBalance(ctx, callsData.precompileAddr.Bytes(), precisebanktypes.ExtendedCoinDenom()) - Expect(precompileBalance.Amount.String()).To(Equal("0")) - Expect(finalBalance).To(Equal(initBalance)) }) It("it should consume at least the withdraw requested gas", func() { txArgs, callArgs := callsData.getTxAndCallArgs(directCall, werc20.WithdrawMethod, withdrawAmount) _, ethRes, _ := is.factory.CallContractAndCheckLogs(user.Priv, txArgs, callArgs, withdrawCheck) Expect(is.network.NextBlock()).ToNot(HaveOccurred(), "error on NextBlock") - Expect(ethRes.GasUsed).To(BeNumerically(">=", werc20.WithdrawRequiredGas), "expected different gas used for withdraw") }) }) //nolint:dupl When("no calldata is provided", func() { It("it should call the fallback which behave like deposit", func() { - ctx := is.network.GetContext() - pbk := is.network.App.GetPreciseBankKeeper() - initBalance := pbk.GetBalance(ctx, user.Addr.Bytes(), precisebanktypes.ExtendedCoinDenom()) - Expect(is.network.NextBlock()).ToNot(HaveOccurred(), "error on NextBlock") - txArgs, callArgs := callsData.getTxAndCallArgs(directCall, "") txArgs.Amount = depositAmount _, _, err := is.factory.CallContractAndCheckLogs(user.Priv, txArgs, callArgs, depositCheck) Expect(err).ToNot(HaveOccurred(), "unexpected error calling the precompile") Expect(is.network.NextBlock()).ToNot(HaveOccurred(), "error on NextBlock") - - finalBalance := pbk.GetBalance(ctx, user.Addr.Bytes(), precisebanktypes.ExtendedCoinDenom()) - precompileBalance := pbk.GetBalance(ctx, callsData.precompileAddr.Bytes(), precisebanktypes.ExtendedCoinDenom()) - Expect(precompileBalance.Amount.String()).To(Equal("0")) - Expect(finalBalance).To(Equal(initBalance)) }) It("it should consume at least the deposit requested gas", func() { txArgs, callArgs := callsData.getTxAndCallArgs(directCall, werc20.DepositMethod) txArgs.Amount = depositAmount - _, ethRes, _ := is.factory.CallContractAndCheckLogs(user.Priv, txArgs, callArgs, depositCheck) + _, ethRes, err := is.factory.CallContractAndCheckLogs(user.Priv, txArgs, callArgs, depositCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected error calling the precompile") Expect(is.network.NextBlock()).ToNot(HaveOccurred(), "error on NextBlock") - Expect(ethRes.GasUsed).To(BeNumerically(">=", werc20.DepositRequiredGas), "expected different gas used for receive") }) }) When("the specified method is too short", func() { It("it should call the fallback which behave like deposit", func() { - ctx := is.network.GetContext() - pbk := is.network.App.GetPreciseBankKeeper() - initBalance := pbk.GetBalance(ctx, user.Addr.Bytes(), precisebanktypes.ExtendedCoinDenom()) - Expect(is.network.NextBlock()).ToNot(HaveOccurred(), "error on NextBlock") - txArgs, callArgs := callsData.getTxAndCallArgs(directCall, "") txArgs.Amount = depositAmount // Short method is directly set in the input to skip ABI validation @@ -421,11 +456,6 @@ func TestPrecompileIntegrationTestSuite(t *testing.T, create network.CreateEvmAp _, _, err := is.factory.CallContractAndCheckLogs(user.Priv, txArgs, callArgs, depositCheck) Expect(err).ToNot(HaveOccurred(), "unexpected error calling the precompile") Expect(is.network.NextBlock()).ToNot(HaveOccurred(), "error on NextBlock") - - finalBalance := pbk.GetBalance(ctx, user.Addr.Bytes(), precisebanktypes.ExtendedCoinDenom()) - precompileBalance := pbk.GetBalance(ctx, callsData.precompileAddr.Bytes(), precisebanktypes.ExtendedCoinDenom()) - Expect(precompileBalance.Amount.String()).To(Equal("0")) - Expect(finalBalance).To(Equal(initBalance)) }) It("it should consume at least the deposit requested gas", func() { txArgs, callArgs := callsData.getTxAndCallArgs(directCall, "") @@ -433,19 +463,14 @@ func TestPrecompileIntegrationTestSuite(t *testing.T, create network.CreateEvmAp // Short method is directly set in the input to skip ABI validation txArgs.Input = []byte{1, 2, 3} - _, ethRes, _ := is.factory.CallContractAndCheckLogs(user.Priv, txArgs, callArgs, depositCheck) + _, ethRes, err := is.factory.CallContractAndCheckLogs(user.Priv, txArgs, callArgs, depositCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected error calling the precompile") Expect(is.network.NextBlock()).ToNot(HaveOccurred(), "error on NextBlock") - Expect(ethRes.GasUsed).To(BeNumerically(">=", werc20.DepositRequiredGas), "expected different gas used for fallback") }) }) When("the specified method does not exist", func() { It("it should call the fallback which behave like deposit", func() { - ctx := is.network.GetContext() - pbk := is.network.App.GetPreciseBankKeeper() - initBalance := pbk.GetBalance(ctx, user.Addr.Bytes(), precisebanktypes.ExtendedCoinDenom()) - Expect(is.network.NextBlock()).ToNot(HaveOccurred(), "error on NextBlock") - txArgs, callArgs := callsData.getTxAndCallArgs(directCall, "") txArgs.Amount = depositAmount // Wrong method is directly set in the input to skip ABI validation @@ -454,11 +479,6 @@ func TestPrecompileIntegrationTestSuite(t *testing.T, create network.CreateEvmAp _, _, err := is.factory.CallContractAndCheckLogs(user.Priv, txArgs, callArgs, depositCheck) Expect(err).ToNot(HaveOccurred(), "unexpected error calling the precompile") Expect(is.network.NextBlock()).ToNot(HaveOccurred(), "error on NextBlock") - - finalBalance := pbk.GetBalance(ctx, user.Addr.Bytes(), precisebanktypes.ExtendedCoinDenom()) - precompileBalance := pbk.GetBalance(ctx, callsData.precompileAddr.Bytes(), precisebanktypes.ExtendedCoinDenom()) - Expect(precompileBalance.Amount.String()).To(Equal("0")) - Expect(finalBalance).To(Equal(initBalance)) }) It("it should consume at least the deposit requested gas", func() { txArgs, callArgs := callsData.getTxAndCallArgs(directCall, "") @@ -466,9 +486,9 @@ func TestPrecompileIntegrationTestSuite(t *testing.T, create network.CreateEvmAp // Wrong method is directly set in the input to skip ABI validation txArgs.Input = []byte("nonExistingMethod") - _, ethRes, _ := is.factory.CallContractAndCheckLogs(user.Priv, txArgs, callArgs, depositCheck) + _, ethRes, err := is.factory.CallContractAndCheckLogs(user.Priv, txArgs, callArgs, depositCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected error calling the precompile") Expect(is.network.NextBlock()).ToNot(HaveOccurred(), "error on NextBlock") - Expect(ethRes.GasUsed).To(BeNumerically(">=", werc20.DepositRequiredGas), "expected different gas used for fallback") }) }) @@ -477,7 +497,18 @@ func TestPrecompileIntegrationTestSuite(t *testing.T, create network.CreateEvmAp Context("calling a reverter contract", func() { When("to call the deposit", func() { It("it should return funds to the last sender and emit the event", func() { - ctx := is.network.GetContext() + borrow := big.NewInt(0) + if conversionFactor.Cmp(big.NewInt(1)) != 0 { // 18-decimal chain (conversionFactor = 1) + borrow = big.NewInt(1) + } + + senderIntegerDelta = new(big.Int).Sub(new(big.Int).Neg((new(big.Int).Quo(depositAmount, conversionFactor))), borrow) + senderFractionalDelta = new(big.Int).Mod(new(big.Int).Sub(conversionFactor, depositFractional), conversionFactor) + + contractIntegerDelta = new(big.Int).Quo(depositAmount, conversionFactor) + contractFractionalDelta = depositFractional + + precisebankIntegerDelta = borrow txArgs, callArgs := callsData.getTxAndCallArgs(contractCall, "depositWithRevert", false, false) txArgs.Amount = depositAmount @@ -485,28 +516,15 @@ func TestPrecompileIntegrationTestSuite(t *testing.T, create network.CreateEvmAp _, _, err := is.factory.CallContractAndCheckLogs(txSender.Priv, txArgs, callArgs, depositCheck) Expect(err).ToNot(HaveOccurred(), "unexpected error calling the precompile") Expect(is.network.NextBlock()).ToNot(HaveOccurred(), "error on NextBlock") - - finalBalance := is.network.App.GetPreciseBankKeeper().GetBalance(ctx, revertContractAddr.Bytes(), precisebanktypes.ExtendedCoinDenom()) - Expect(finalBalance.Amount.String()).To(Equal(depositAmount.String()), "expected final balance equal to deposit") - - finalAllBalances := is.network.App.GetBankKeeper().GetAllBalances(ctx, revertContractAddr.Bytes()) - Expect(finalAllBalances).To(Equal(sdk.Coins{sdk.NewCoin(evmtypes.GetEVMCoinDenom(), math.NewIntFromBigInt(depositAmount).Quo(precisebanktypes.ConversionFactor()))})) }) }) DescribeTable("to call the deposit", func(before, after bool) { - ctx := is.network.GetContext() - - initBalance := is.network.App.GetBankKeeper().GetAllBalances(ctx, txSender.AccAddr) - txArgs, callArgs := callsData.getTxAndCallArgs(contractCall, "depositWithRevert", before, after) txArgs.Amount = depositAmount _, _, err := is.factory.CallContractAndCheckLogs(txSender.Priv, txArgs, callArgs, depositCheck) Expect(err).To(HaveOccurred(), "execution should have reverted") Expect(is.network.NextBlock()).ToNot(HaveOccurred(), "error on NextBlock") - - finalBalance := is.network.App.GetBankKeeper().GetAllBalances(ctx, txSender.AccAddr) - Expect(finalBalance.String()).To(Equal(initBalance.String()), "expected final balance equal to initial") }, Entry("it should not move funds and dont emit the event reverting before changing state", true, false), Entry("it should not move funds and dont emit the event reverting after changing state", false, true), @@ -515,39 +533,34 @@ func TestPrecompileIntegrationTestSuite(t *testing.T, create network.CreateEvmAp Context("calling an erc20 method", func() { When("transferring tokens", func() { It("it should transfer tokens to a receiver using `transfer`", func() { - ctx := is.network.GetContext() - - senderBalance := is.network.App.GetBankKeeper().GetAllBalances(ctx, txSender.AccAddr) - receiverBalance := is.network.App.GetBankKeeper().GetAllBalances(ctx, user.AccAddr) - transferAmount = transferAmount.Quo(transferAmount, big.NewInt(precisebanktypes.ConversionFactor().Int64())) + senderIntegerDelta = new(big.Int).Neg(transferAmount) + senderFractionalDelta = big.NewInt(0) + receiverIntegerDelta = transferAmount + receiverFractionalDelta = big.NewInt(0) + + // First, sender needs to deposit to get WERC20 tokens + // Use a larger deposit amount to ensure sufficient balance for transfer + depositForTransfer := new(big.Int).Mul(transferAmount, big.NewInt(10)) // 10x transfer amount + txArgs, callArgs := callsData.getTxAndCallArgs(directCall, werc20.DepositMethod) + txArgs.Amount = depositForTransfer + _, _, err := is.factory.CallContractAndCheckLogs(txSender.Priv, txArgs, callArgs, depositCheck) + Expect(err).ToNot(HaveOccurred(), "failed to deposit before transfer") + Expect(is.network.NextBlock()).ToNot(HaveOccurred(), "error on NextBlock after deposit") + // Now perform the transfer txArgs, transferArgs := callsData.getTxAndCallArgs(directCall, erc20.TransferMethod, user.Addr, transferAmount) - transferCoins := sdk.Coins{sdk.NewInt64Coin(is.wrappedCoinDenom, transferAmount.Int64())} - _, _, err := is.factory.CallContractAndCheckLogs(txSender.Priv, txArgs, transferArgs, transferCheck) + _, _, err = is.factory.CallContractAndCheckLogs(txSender.Priv, txArgs, transferArgs, transferCheck) Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") - - senderBalanceAfter := is.network.App.GetBankKeeper().GetAllBalances(ctx, txSender.AccAddr) - receiverBalanceAfter := is.network.App.GetBankKeeper().GetAllBalances(ctx, user.AccAddr) - Expect(senderBalanceAfter).To(Equal(senderBalance.Sub(transferCoins...))) - Expect(receiverBalanceAfter).To(Equal(receiverBalance.Add(transferCoins...))) + Expect(is.network.NextBlock()).ToNot(HaveOccurred(), "error on NextBlock after transfer") }) It("it should fail to transfer tokens to a receiver using `transferFrom`", func() { - ctx := is.network.GetContext() - - senderBalance := is.network.App.GetBankKeeper().GetAllBalances(ctx, txSender.AccAddr) - receiverBalance := is.network.App.GetBankKeeper().GetAllBalances(ctx, user.AccAddr) - txArgs, transferArgs := callsData.getTxAndCallArgs(directCall, erc20.TransferFromMethod, txSender.Addr, user.Addr, transferAmount) insufficientAllowanceCheck := failCheck.WithErrContains(erc20.ErrInsufficientAllowance.Error()) _, _, err := is.factory.CallContractAndCheckLogs(txSender.Priv, txArgs, transferArgs, insufficientAllowanceCheck) Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") - - senderBalanceAfter := is.network.App.GetBankKeeper().GetAllBalances(ctx, txSender.AccAddr) - receiverBalanceAfter := is.network.App.GetBankKeeper().GetAllBalances(ctx, user.AccAddr) - Expect(senderBalanceAfter).To(Equal(senderBalance)) - Expect(receiverBalanceAfter).To(Equal(receiverBalance)) + Expect(is.network.NextBlock()).ToNot(HaveOccurred(), "error on NextBlock after transfer") }) }) When("querying information", func() { @@ -559,12 +572,14 @@ func TestPrecompileIntegrationTestSuite(t *testing.T, create network.CreateEvmAp _, ethRes, err := is.factory.CallContractAndCheckLogs(txSender.Priv, txArgs, balancesArgs, passCheck) Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") - expBalance := is.network.App.GetBankKeeper().GetBalance(is.network.GetContext(), txSender.AccAddr, is.wrappedCoinDenom) + // Get expected balance using grpcHandler for accurate state + expBalanceRes, err := is.grpcHandler.GetBalanceFromBank(txSender.AccAddr, is.wrappedCoinDenom) + Expect(err).ToNot(HaveOccurred(), "failed to get balance from grpcHandler") var balance *big.Int err = is.precompile.UnpackIntoInterface(&balance, erc20.BalanceOfMethod, ethRes.Ret) Expect(err).ToNot(HaveOccurred(), "failed to unpack result") - Expect(balance).To(Equal(expBalance.Amount.BigInt()), "expected different balance") + Expect(balance).To(Equal(expBalanceRes.Balance.Amount.BigInt()), "expected different balance") }) It("should return 0 for a new account", func() { // Query the balance @@ -632,3 +647,46 @@ func TestPrecompileIntegrationTestSuite(t *testing.T, create network.CreateEvmAp RegisterFailHandler(Fail) RunSpecs(t, "WEVMOS precompile test suite") } + +// expectBalanceChange verifies expected balance changes after operations +func (is *PrecompileIntegrationTestSuite) expectBalanceChange( + addr sdk.AccAddress, + beforeSnapshot *BalanceSnapshot, + expectedIntegerDelta *big.Int, + expectedFractionalDelta *big.Int, + description string, +) { + afterSnapshot, err := is.getBalanceSnapshot(addr) + Expect(err).ToNot(HaveOccurred(), "failed to get balance snapshot for %s", description) + + actualIntegerDelta := new(big.Int).Sub(afterSnapshot.IntegerBalance, beforeSnapshot.IntegerBalance) + actualFractionalDelta := new(big.Int).Sub(afterSnapshot.FractionalBalance, beforeSnapshot.FractionalBalance) + + Expect(actualIntegerDelta.Cmp(expectedIntegerDelta)).To(Equal(0), + "integer balance delta mismatch for %s: expected %s, got %s", + description, expectedIntegerDelta.String(), actualIntegerDelta.String()) + + Expect(actualFractionalDelta.Cmp(expectedFractionalDelta)).To(Equal(0), + "fractional balance delta mismatch for %s: expected %s, got %s", + description, expectedFractionalDelta.String(), actualFractionalDelta.String()) +} + +// getBalanceSnapshot gets complete balance information using grpcHandler +func (is *PrecompileIntegrationTestSuite) getBalanceSnapshot(addr sdk.AccAddress) (*BalanceSnapshot, error) { + // Get integer balance (uatom) + intRes, err := is.grpcHandler.GetBalanceFromBank(addr, evmtypes.GetEVMCoinDenom()) + if err != nil { + return nil, fmt.Errorf("failed to get integer balance: %w", err) + } + + // Get fractional balance using the new grpcHandler method + fracRes, err := is.grpcHandler.FractionalBalance(addr) + if err != nil { + return nil, fmt.Errorf("failed to get fractional balance: %w", err) + } + + return &BalanceSnapshot{ + IntegerBalance: intRes.Balance.Amount.BigInt(), + FractionalBalance: fracRes.FractionalBalance.Amount.BigInt(), + }, nil +} From 2f2c7875c069532f2eca1bf79d96823653b7010d Mon Sep 17 00:00:00 2001 From: Kyuhyeon Choi Date: Thu, 14 Aug 2025 20:26:21 +0900 Subject: [PATCH 07/10] test(x/precisebank): fix integration tests --- .../x/precisebank/test_burn_integration.go | 34 ++++------- .../x/precisebank/test_mint_integration.go | 39 ++++--------- .../x/precisebank/test_send_integration.go | 56 ++++++++----------- x/precisebank/types/events.go | 20 +++++-- 4 files changed, 61 insertions(+), 88 deletions(-) diff --git a/tests/integration/x/precisebank/test_burn_integration.go b/tests/integration/x/precisebank/test_burn_integration.go index c0f35ea2f..65b57f5da 100644 --- a/tests/integration/x/precisebank/test_burn_integration.go +++ b/tests/integration/x/precisebank/test_burn_integration.go @@ -16,7 +16,6 @@ import ( 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" ) func (s *KeeperIntegrationTestSuite) TestBurnCoinsMatchingErrors() { @@ -177,6 +176,9 @@ func (s *KeeperIntegrationTestSuite) TestBurnCoins() { err := s.network.App.GetPreciseBankKeeper().MintCoins(s.network.GetContext(), moduleName, tt.startBalance) s.Require().NoError(err) + // Get fractional balance before burn + fracBalBefore := s.network.App.GetPreciseBankKeeper().GetFractionalBalance(s.network.GetContext(), recipientAddr) + // Burn err = s.network.App.GetPreciseBankKeeper().BurnCoins(s.network.GetContext(), moduleName, tt.burnCoins) if tt.wantErr != "" { @@ -187,6 +189,9 @@ func (s *KeeperIntegrationTestSuite) TestBurnCoins() { s.Require().NoError(err) + // Get fractional balance after burn + fracBalAfter := s.network.App.GetPreciseBankKeeper().GetFractionalBalance(s.network.GetContext(), recipientAddr) + // ------------------------------------------------------------- // Check FULL balances // x/bank balances + x/precisebank balance @@ -199,28 +204,11 @@ func (s *KeeperIntegrationTestSuite) TestBurnCoins() { "unexpected balance after minting %s to %s", ) - intCoinAmt := tt.burnCoins.AmountOf(types.IntegerCoinDenom()). - Mul(types.ConversionFactor()) - - fraCoinAmt := tt.burnCoins.AmountOf(types.ExtendedCoinDenom()) - - totalExtCoinAmt := intCoinAmt.Add(fraCoinAmt) - spentCoins := sdk.NewCoins(sdk.NewCoin( - types.ExtendedCoinDenom(), - totalExtCoinAmt, - )) - - events := s.network.GetContext().EventManager().Events() - - expBurnEvent := banktypes.NewCoinBurnEvent(recipientAddr, spentCoins) - expSpendEvent := banktypes.NewCoinSpentEvent(recipientAddr, spentCoins) - - if totalExtCoinAmt.IsZero() { - s.Require().NotContains(events, expBurnEvent) - s.Require().NotContains(events, expSpendEvent) - } else { - s.Require().Contains(events, expBurnEvent) - s.Require().Contains(events, expSpendEvent) + // Check fractinoal balance change event + if !fracBalAfter.Sub(fracBalBefore).Equal(sdkmath.ZeroInt()) { + expEvent := types.NewEventFractionalBalanceChange(recipientAddr, fracBalBefore, fracBalAfter) + events := s.network.GetContext().EventManager().Events() + s.Require().Contains(events, expEvent) } }) } diff --git a/tests/integration/x/precisebank/test_mint_integration.go b/tests/integration/x/precisebank/test_mint_integration.go index d9d00f665..0458f1299 100644 --- a/tests/integration/x/precisebank/test_mint_integration.go +++ b/tests/integration/x/precisebank/test_mint_integration.go @@ -283,6 +283,9 @@ func (s *KeeperIntegrationTestSuite) TestMintCoins() { recipientAddr := s.network.App.GetAccountKeeper().GetModuleAddress(tt.recipientModule) for _, mt := range tt.mints { + // Get fractional balance before mint + fracBalBefore := s.network.App.GetPreciseBankKeeper().GetFractionalBalance(s.network.GetContext(), recipientAddr) + err := s.network.App.GetPreciseBankKeeper().MintCoins(s.network.GetContext(), tt.recipientModule, mt.mintAmount) s.Require().NoError(err) @@ -292,6 +295,9 @@ func (s *KeeperIntegrationTestSuite) TestMintCoins() { // Exclude "uatom" as x/precisebank balance will include it bankCoins := s.network.App.GetBankKeeper().GetAllBalances(s.network.GetContext(), recipientAddr) + // Get fractional balance after mint + fracBalAfter := s.network.App.GetPreciseBankKeeper().GetFractionalBalance(s.network.GetContext(), recipientAddr) + // Only use x/bank balances for non-uatom denoms var denoms []string for _, coin := range bankCoins { @@ -321,34 +327,11 @@ func (s *KeeperIntegrationTestSuite) TestMintCoins() { "unexpected balance after minting %s to %s", ) - // Get event for minted coins - intCoinAmt := mt.mintAmount.AmountOf(types.IntegerCoinDenom()). - Mul(types.ConversionFactor()) - - fraCoinAmt := mt.mintAmount.AmountOf(types.ExtendedCoinDenom()) - - totalExtCoinAmt := intCoinAmt.Add(fraCoinAmt) - extCoins := sdk.NewCoins(sdk.NewCoin(types.ExtendedCoinDenom(), totalExtCoinAmt)) - - // Check for mint event - events := s.network.GetContext().EventManager().Events() - - expMintEvent := banktypes.NewCoinMintEvent( - recipientAddr, - extCoins, - ) - - expReceivedEvent := banktypes.NewCoinReceivedEvent( - recipientAddr, - extCoins, - ) - - if totalExtCoinAmt.IsZero() { - s.Require().NotContains(events, expMintEvent) - s.Require().NotContains(events, expReceivedEvent) - } else { - s.Require().Contains(events, expMintEvent) - s.Require().Contains(events, expReceivedEvent) + // Check fractinoal balance change event + if !fracBalAfter.Sub(fracBalBefore).Equal(sdkmath.ZeroInt()) { + expEvent := types.NewEventFractionalBalanceChange(recipientAddr, fracBalBefore, fracBalAfter) + events := s.network.GetContext().EventManager().Events() + s.Require().Contains(events, expEvent) } } }) diff --git a/tests/integration/x/precisebank/test_send_integration.go b/tests/integration/x/precisebank/test_send_integration.go index c24362026..d819bf650 100644 --- a/tests/integration/x/precisebank/test_send_integration.go +++ b/tests/integration/x/precisebank/test_send_integration.go @@ -401,6 +401,9 @@ func (s *KeeperIntegrationTestSuite) TestSendCoins() { senderBalBefore := s.GetAllBalances(sender) recipientBalBefore := s.GetAllBalances(recipient) + senderFracBalBefore := s.network.App.GetPreciseBankKeeper().GetFractionalBalance(s.network.GetContext(), sender) + recipientFracBalBefore := s.network.App.GetPreciseBankKeeper().GetFractionalBalance(s.network.GetContext(), recipient) + err := s.network.App.GetPreciseBankKeeper().SendCoins(s.network.GetContext(), sender, recipient, tt.giveAmt) if tt.wantErr != "" { s.Require().Error(err) @@ -414,6 +417,9 @@ func (s *KeeperIntegrationTestSuite) TestSendCoins() { senderBalAfter := s.GetAllBalances(sender) recipientBalAfter := s.GetAllBalances(recipient) + senderFracBalAfter := s.network.App.GetPreciseBankKeeper().GetFractionalBalance(s.network.GetContext(), sender) + recipientFracBalAfter := s.network.App.GetPreciseBankKeeper().GetFractionalBalance(s.network.GetContext(), recipient) + // Convert send amount coins to extended coins. i.e. if send coins // includes uatom, convert it so that its the equivalent aatom // amount so its easier to compare. Compare extended coins only. @@ -440,41 +446,27 @@ func (s *KeeperIntegrationTestSuite) TestSendCoins() { ) // Check events - - // FULL aatom equivalent, including uatom only/mixed sends - sendExtendedAmount := sdk.NewCoin( - types.ExtendedCoinDenom(), - sendAmountFullExtended.AmountOf(types.ExtendedCoinDenom()), - ) - extCoins := sdk.NewCoins(sendExtendedAmount) - - // No extra events if not sending aatom - if sendExtendedAmount.IsZero() { - return + events := s.network.GetContext().EventManager().Events() + targetEvents := []sdk.Event{} + for _, event := range events { + if event.Type == types.EventTypeFractionalBalanceChange { + targetEvents = append(targetEvents, event) + } } - extendedEvent := sdk.NewEvent( - banktypes.EventTypeTransfer, - sdk.NewAttribute(banktypes.AttributeKeyRecipient, recipient.String()), - sdk.NewAttribute(banktypes.AttributeKeySender, sender.String()), - sdk.NewAttribute(sdk.AttributeKeyAmount, sendExtendedAmount.String()), - ) - - expReceivedEvent := banktypes.NewCoinReceivedEvent( - recipient, - extCoins, - ) - - expSentEvent := banktypes.NewCoinSpentEvent( - sender, - extCoins, - ) - - events := s.network.GetContext().EventManager().Events() + if !senderFracBalAfter.Sub(senderFracBalBefore).Equal(sdkmath.ZeroInt()) { + expSenderFracBalChangeEvent := types.NewEventFractionalBalanceChange( + sender, senderFracBalBefore, senderFracBalAfter, + ) + s.Require().Contains(targetEvents, expSenderFracBalChangeEvent) + } - s.Require().Contains(events, extendedEvent) - s.Require().Contains(events, expReceivedEvent) - s.Require().Contains(events, expSentEvent) + if !recipientFracBalAfter.Sub(recipientFracBalBefore).Equal(sdkmath.ZeroInt()) { + expRecipientFracBalChangeEvent := types.NewEventFractionalBalanceChange( + recipient, recipientFracBalBefore, recipientFracBalAfter, + ) + s.Require().Contains(targetEvents, expRecipientFracBalChangeEvent) + } }) } } diff --git a/x/precisebank/types/events.go b/x/precisebank/types/events.go index f3649eb83..186ecf604 100644 --- a/x/precisebank/types/events.go +++ b/x/precisebank/types/events.go @@ -15,17 +15,27 @@ const ( AttributeKeyDelta = "delta" ) -func EmitFractionalBalanceChange( - ctx sdk.Context, +func NewEventFractionalBalanceChange( address sdk.AccAddress, beforeAmount sdkmath.Int, afterAmount sdkmath.Int, -) { +) sdk.Event { delta := afterAmount.Sub(beforeAmount) - ctx.EventManager().EmitEvent(sdk.NewEvent( + return sdk.NewEvent( EventTypeFractionalBalanceChange, sdk.NewAttribute(AttributeKeyAddress, address.String()), sdk.NewAttribute(AttributeKeyDelta, delta.String()), - )) + ) +} + +func EmitFractionalBalanceChange( + ctx sdk.Context, + address sdk.AccAddress, + beforeAmount sdkmath.Int, + afterAmount sdkmath.Int, +) { + ctx.EventManager().EmitEvent( + NewEventFractionalBalanceChange(address, beforeAmount, afterAmount), + ) } From 0953fe8c63ab4d1974fcc2d617da4385c1370c1a Mon Sep 17 00:00:00 2001 From: Kyuhyeon Choi Date: Thu, 14 Aug 2025 20:26:42 +0900 Subject: [PATCH 08/10] test(precompiles/werc20): fix integration tests --- .../precompiles/werc20/test_integration.go | 170 +++--------------- .../precompiles/werc20/test_utils.go | 160 +++++++++++++++++ 2 files changed, 186 insertions(+), 144 deletions(-) diff --git a/tests/integration/precompiles/werc20/test_integration.go b/tests/integration/precompiles/werc20/test_integration.go index 9e726d346..36ae15bc3 100644 --- a/tests/integration/precompiles/werc20/test_integration.go +++ b/tests/integration/precompiles/werc20/test_integration.go @@ -1,7 +1,6 @@ package werc20 import ( - "fmt" "math/big" "testing" @@ -31,7 +30,6 @@ import ( "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" - authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" ) // ------------------------------------------------------------------------------------------------- @@ -51,12 +49,6 @@ type PrecompileIntegrationTestSuite struct { precompileAddrHex string } -// BalanceSnapshot represents a snapshot of account balances for testing -type BalanceSnapshot struct { - IntegerBalance *big.Int - FractionalBalance *big.Int -} - func TestPrecompileIntegrationTestSuite(t *testing.T, create network.CreateEvmApp, options ...network.ConfigOption) { _ = DescribeTableSubtree("a user interact with the WEVMOS precompiled contract", func(chainId testconstants.ChainID) { var ( @@ -70,27 +62,9 @@ func TestPrecompileIntegrationTestSuite(t *testing.T, create network.CreateEvmAp revertContractAddr common.Address - precisebankModuleAccAddr sdk.AccAddress - - // Shared balance snapshots for common test scenarios - senderBeforeSnapshot *BalanceSnapshot - receiverBeforeSnapshot *BalanceSnapshot - precompileBeforeSnapshot *BalanceSnapshot - contractBeforeSnapshot *BalanceSnapshot - precisebankModuleBeforeSnapshot *BalanceSnapshot - - // Expected balance changes (set by individual tests) - senderIntegerDelta *big.Int - senderFractionalDelta *big.Int - receiverIntegerDelta *big.Int - receiverFractionalDelta *big.Int - precompileIntegerDelta *big.Int - precompileFractionalDelta *big.Int - contractIntegerDelta *big.Int - contractFractionalDelta *big.Int - precisebankIntegerDelta *big.Int - precisebankFractionalDelta *big.Int - precisebankRemainder *big.Int + // Account balance tracking + accountBalances []*AccountBalanceInfo + precisebankRemainder *big.Int ) // Configure deposit amounts with integer and fractional components to test @@ -114,65 +88,9 @@ func TestPrecompileIntegrationTestSuite(t *testing.T, create network.CreateEvmAp withdrawAmount := depositAmount transferAmount := big.NewInt(10) // Start with 10 integer units - // Helper functions for balance snapshot management - resetExpectedDeltas := func() { - senderIntegerDelta = big.NewInt(0) - senderFractionalDelta = big.NewInt(0) - - receiverIntegerDelta = big.NewInt(0) - receiverFractionalDelta = big.NewInt(0) - - precompileIntegerDelta = big.NewInt(0) - precompileFractionalDelta = big.NewInt(0) - - contractIntegerDelta = big.NewInt(0) - contractFractionalDelta = big.NewInt(0) - - precisebankIntegerDelta = big.NewInt(0) - precisebankFractionalDelta = big.NewInt(0) - precisebankRemainder = big.NewInt(0) - } - - takeSnapshots := func() { - var err error - - senderBeforeSnapshot, err = is.getBalanceSnapshot(txSender.AccAddr) - Expect(err).ToNot(HaveOccurred(), "failed to get sender balance snapshot") - - receiverBeforeSnapshot, err = is.getBalanceSnapshot(user.AccAddr) - Expect(err).ToNot(HaveOccurred(), "failed to get receiver balance snapshot") - - precompileBeforeSnapshot, err = is.getBalanceSnapshot(callsData.precompileAddr.Bytes()) - Expect(err).ToNot(HaveOccurred(), "failed to get precompile balance snapshot") - - contractBeforeSnapshot, err = is.getBalanceSnapshot(revertContractAddr.Bytes()) - Expect(err).ToNot(HaveOccurred(), "failed to get contract balance snapshot") - - precisebankModuleAccAddr = authtypes.NewModuleAddress(precisebanktypes.ModuleName) - precisebankModuleBeforeSnapshot, err = is.getBalanceSnapshot(precisebankModuleAccAddr) - Expect(err).ToNot(HaveOccurred(), "failed to get precisebank module balance snapshot") - } - - verifyBalanceChanges := func() { - is.expectBalanceChange(txSender.AccAddr, senderBeforeSnapshot, - senderIntegerDelta, senderFractionalDelta, "sender") - - is.expectBalanceChange(user.AccAddr, receiverBeforeSnapshot, - receiverIntegerDelta, receiverFractionalDelta, "receiver") - - is.expectBalanceChange(callsData.precompileAddr.Bytes(), precompileBeforeSnapshot, - precompileIntegerDelta, precompileFractionalDelta, "precompile") - - is.expectBalanceChange(revertContractAddr.Bytes(), contractBeforeSnapshot, - contractIntegerDelta, contractFractionalDelta, "contract") - - is.expectBalanceChange(precisebankModuleAccAddr, precisebankModuleBeforeSnapshot, - precisebankIntegerDelta, precisebankFractionalDelta, "precisebank module") - - res, err := is.grpcHandler.Remainder() - Expect(err).ToNot(HaveOccurred(), "failed to get precisebank module remainder") - actualRemainder := res.Remainder.Amount.BigInt() - Expect(actualRemainder).To(Equal(precisebankRemainder)) + // Helper function to get account balance info by type + balanceOf := func(accountType AccountType) *AccountBalanceInfo { + return GetAccountBalance(accountBalances, accountType) } BeforeEach(func() { @@ -290,18 +208,25 @@ func TestPrecompileIntegrationTestSuite(t *testing.T, create network.CreateEvmAp depositCheck = passCheck.WithExpEvents(werc20.EventTypeDeposit) transferCheck = passCheck.WithExpEvents(erc20.EventTypeTransfer) - // Reset balance tracking state for each test - resetExpectedDeltas() + // Initialize and reset balance tracking state for each test + accountBalances = InitializeAccountBalances( + txSender.AccAddr, user.AccAddr, + callsData.precompileAddr, revertContractAddr, + ) + + // Reset expected balance change of accounts + ResetExpectedDeltas(accountBalances) + precisebankRemainder = big.NewInt(0) }) // JustBeforeEach takes snapshots after individual test setup JustBeforeEach(func() { - takeSnapshots() + TakeBalanceSnapshots(accountBalances, is.grpcHandler) }) // AfterEach verifies balance changes AfterEach(func() { - verifyBalanceChanges() + VerifyBalanceChanges(accountBalances, is.grpcHandler, precisebankRemainder) }) Context("calling a specific wrapped coin method", func() { @@ -502,13 +427,13 @@ func TestPrecompileIntegrationTestSuite(t *testing.T, create network.CreateEvmAp borrow = big.NewInt(1) } - senderIntegerDelta = new(big.Int).Sub(new(big.Int).Neg((new(big.Int).Quo(depositAmount, conversionFactor))), borrow) - senderFractionalDelta = new(big.Int).Mod(new(big.Int).Sub(conversionFactor, depositFractional), conversionFactor) + balanceOf(Sender).IntegerDelta = new(big.Int).Sub(new(big.Int).Neg((new(big.Int).Quo(depositAmount, conversionFactor))), borrow) + balanceOf(Sender).FractionalDelta = new(big.Int).Mod(new(big.Int).Sub(conversionFactor, depositFractional), conversionFactor) - contractIntegerDelta = new(big.Int).Quo(depositAmount, conversionFactor) - contractFractionalDelta = depositFractional + balanceOf(Contract).IntegerDelta = new(big.Int).Quo(depositAmount, conversionFactor) + balanceOf(Contract).FractionalDelta = depositFractional - precisebankIntegerDelta = borrow + balanceOf(PrecisebankModule).IntegerDelta = borrow txArgs, callArgs := callsData.getTxAndCallArgs(contractCall, "depositWithRevert", false, false) txArgs.Amount = depositAmount @@ -533,10 +458,10 @@ func TestPrecompileIntegrationTestSuite(t *testing.T, create network.CreateEvmAp Context("calling an erc20 method", func() { When("transferring tokens", func() { It("it should transfer tokens to a receiver using `transfer`", func() { - senderIntegerDelta = new(big.Int).Neg(transferAmount) - senderFractionalDelta = big.NewInt(0) - receiverIntegerDelta = transferAmount - receiverFractionalDelta = big.NewInt(0) + balanceOf(Sender).IntegerDelta = new(big.Int).Neg(transferAmount) + balanceOf(Sender).FractionalDelta = big.NewInt(0) + balanceOf(Receiver).IntegerDelta = transferAmount + balanceOf(Receiver).FractionalDelta = big.NewInt(0) // First, sender needs to deposit to get WERC20 tokens // Use a larger deposit amount to ensure sufficient balance for transfer @@ -647,46 +572,3 @@ func TestPrecompileIntegrationTestSuite(t *testing.T, create network.CreateEvmAp RegisterFailHandler(Fail) RunSpecs(t, "WEVMOS precompile test suite") } - -// expectBalanceChange verifies expected balance changes after operations -func (is *PrecompileIntegrationTestSuite) expectBalanceChange( - addr sdk.AccAddress, - beforeSnapshot *BalanceSnapshot, - expectedIntegerDelta *big.Int, - expectedFractionalDelta *big.Int, - description string, -) { - afterSnapshot, err := is.getBalanceSnapshot(addr) - Expect(err).ToNot(HaveOccurred(), "failed to get balance snapshot for %s", description) - - actualIntegerDelta := new(big.Int).Sub(afterSnapshot.IntegerBalance, beforeSnapshot.IntegerBalance) - actualFractionalDelta := new(big.Int).Sub(afterSnapshot.FractionalBalance, beforeSnapshot.FractionalBalance) - - Expect(actualIntegerDelta.Cmp(expectedIntegerDelta)).To(Equal(0), - "integer balance delta mismatch for %s: expected %s, got %s", - description, expectedIntegerDelta.String(), actualIntegerDelta.String()) - - Expect(actualFractionalDelta.Cmp(expectedFractionalDelta)).To(Equal(0), - "fractional balance delta mismatch for %s: expected %s, got %s", - description, expectedFractionalDelta.String(), actualFractionalDelta.String()) -} - -// getBalanceSnapshot gets complete balance information using grpcHandler -func (is *PrecompileIntegrationTestSuite) getBalanceSnapshot(addr sdk.AccAddress) (*BalanceSnapshot, error) { - // Get integer balance (uatom) - intRes, err := is.grpcHandler.GetBalanceFromBank(addr, evmtypes.GetEVMCoinDenom()) - if err != nil { - return nil, fmt.Errorf("failed to get integer balance: %w", err) - } - - // Get fractional balance using the new grpcHandler method - fracRes, err := is.grpcHandler.FractionalBalance(addr) - if err != nil { - return nil, fmt.Errorf("failed to get fractional balance: %w", err) - } - - return &BalanceSnapshot{ - IntegerBalance: intRes.Balance.Amount.BigInt(), - FractionalBalance: fracRes.FractionalBalance.Amount.BigInt(), - }, nil -} diff --git a/tests/integration/precompiles/werc20/test_utils.go b/tests/integration/precompiles/werc20/test_utils.go index 82eccfc29..a82bd824c 100644 --- a/tests/integration/precompiles/werc20/test_utils.go +++ b/tests/integration/precompiles/werc20/test_utils.go @@ -1,14 +1,23 @@ package werc20 import ( + "fmt" "math/big" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" + //nolint:revive // dot imports are fine for Ginkgo + . "github.com/onsi/gomega" + + "github.com/cosmos/evm/testutil/integration/evm/grpc" "github.com/cosmos/evm/testutil/keyring" testutiltypes "github.com/cosmos/evm/testutil/types" + precisebanktypes "github.com/cosmos/evm/x/precisebank/types" evmtypes "github.com/cosmos/evm/x/vm/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" ) // callType constants to differentiate between @@ -65,3 +74,154 @@ func (cd CallsData) getTxAndCallArgs( return txArgs, callArgs } + +// ------------------------------------------------------------------------------------------------- +// Balance management utilities +// ------------------------------------------------------------------------------------------------- + +// AccountType represents different account types in the test +type AccountType int + +const ( + Sender AccountType = iota + Receiver + Precompile + Contract + PrecisebankModule +) + +// String returns the string representation of AccountType +func (at AccountType) String() string { + switch at { + case Sender: + return "sender" + case Receiver: + return "receiver" + case Precompile: + return "precompile" + case Contract: + return "contract" + case PrecisebankModule: + return "precisebank module" + default: + return "unknown" + } +} + +// BalanceSnapshot represents a snapshot of account balances for testing +type BalanceSnapshot struct { + IntegerBalance *big.Int + FractionalBalance *big.Int +} + +// AccountBalanceInfo holds balance tracking information for a test account +type AccountBalanceInfo struct { + AccountType AccountType + Address sdk.AccAddress + BeforeSnapshot *BalanceSnapshot + IntegerDelta *big.Int + FractionalDelta *big.Int +} + +// InitializeAccountBalances creates the account balance tracking slice with proper addresses +func InitializeAccountBalances( + senderAddr, receiverAddr sdk.AccAddress, + precompileAddr, contractAddr common.Address, +) []*AccountBalanceInfo { + precisebankModuleAddr := authtypes.NewModuleAddress(precisebanktypes.ModuleName) + return []*AccountBalanceInfo{ + {AccountType: Sender, Address: senderAddr}, + {AccountType: Receiver, Address: receiverAddr}, + {AccountType: Precompile, Address: precompileAddr.Bytes()}, + {AccountType: Contract, Address: contractAddr.Bytes()}, + {AccountType: PrecisebankModule, Address: precisebankModuleAddr}, + } +} + +// ResetExpectedDeltas resets all account balance deltas to zero +func ResetExpectedDeltas(accounts []*AccountBalanceInfo) { + for _, account := range accounts { + account.IntegerDelta = big.NewInt(0) + account.FractionalDelta = big.NewInt(0) + } +} + +// TakeBalanceSnapshots captures current balance states for all accounts +func TakeBalanceSnapshots(accounts []*AccountBalanceInfo, grpcHandler grpc.Handler) { + for _, account := range accounts { + snapshot, err := GetBalanceSnapshot(account.Address, grpcHandler) + Expect(err).ToNot(HaveOccurred(), "failed to take balance snapshots") + account.BeforeSnapshot = snapshot + } +} + +// VerifyBalanceChanges verifies expected balance changes for all accounts +func VerifyBalanceChanges( + accounts []*AccountBalanceInfo, + grpcHandler grpc.Handler, + expectedRemainder *big.Int, +) { + for _, account := range accounts { + ExpectBalanceChange(account.Address, account.BeforeSnapshot, + account.IntegerDelta, account.FractionalDelta, account.AccountType.String(), grpcHandler) + } + + res, err := grpcHandler.Remainder() + Expect(err).ToNot(HaveOccurred(), "failed to get precisebank module remainder") + actualRemainder := res.Remainder.Amount.BigInt() + Expect(actualRemainder).To(Equal(expectedRemainder)) +} + +// GetAccountBalance returns the AccountBalanceInfo for a given account type +func GetAccountBalance(accounts []*AccountBalanceInfo, accountType AccountType) *AccountBalanceInfo { + for _, account := range accounts { + if account.AccountType == accountType { + return account + } + } + return nil +} + +// GetBalanceSnapshot gets complete balance information using grpcHandler +func GetBalanceSnapshot(addr sdk.AccAddress, grpcHandler grpc.Handler) (*BalanceSnapshot, error) { + // Get integer balance (uatom) + intRes, err := grpcHandler.GetBalanceFromBank(addr, evmtypes.GetEVMCoinDenom()) + if err != nil { + return nil, fmt.Errorf("failed to get integer balance: %w", err) + } + + // Get fractional balance using the new grpcHandler method + fracRes, err := grpcHandler.FractionalBalance(addr) + if err != nil { + return nil, fmt.Errorf("failed to get fractional balance: %w", err) + } + + return &BalanceSnapshot{ + IntegerBalance: intRes.Balance.Amount.BigInt(), + FractionalBalance: fracRes.FractionalBalance.Amount.BigInt(), + }, nil +} + +// ExpectBalanceChange verifies expected balance changes after operations +func ExpectBalanceChange( + addr sdk.AccAddress, + beforeSnapshot *BalanceSnapshot, + expectedIntegerDelta *big.Int, + expectedFractionalDelta *big.Int, + description string, + grpcHandler grpc.Handler, +) { + afterSnapshot, err := GetBalanceSnapshot(addr, grpcHandler) + Expect(err).ToNot(HaveOccurred(), "failed to get balance snapshot for %s", description) + + actualIntegerDelta := new(big.Int).Sub(afterSnapshot.IntegerBalance, beforeSnapshot.IntegerBalance) + actualFractionalDelta := new(big.Int).Sub(afterSnapshot.FractionalBalance, beforeSnapshot.FractionalBalance) + + Expect(actualIntegerDelta.Cmp(expectedIntegerDelta)).To(Equal(0), + "integer balance delta mismatch for %s: expected %s, got %s", + description, expectedIntegerDelta.String(), actualIntegerDelta.String()) + + Expect(actualFractionalDelta.Cmp(expectedFractionalDelta)).To(Equal(0), + "fractional balance delta mismatch for %s: expected %s, got %s", + description, expectedFractionalDelta.String(), actualFractionalDelta.String()) +} From 86bef8d5da75d7b17a1ade6023be260de6018810 Mon Sep 17 00:00:00 2001 From: Kyuhyeon Choi Date: Thu, 14 Aug 2025 20:30:08 +0900 Subject: [PATCH 09/10] chore(x/precisebank): modify method name --- x/precisebank/keeper/burn.go | 2 +- x/precisebank/keeper/mint.go | 2 +- x/precisebank/keeper/send.go | 4 ++-- x/precisebank/types/events.go | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/x/precisebank/keeper/burn.go b/x/precisebank/keeper/burn.go index 0f284f151..f971f4d26 100644 --- a/x/precisebank/keeper/burn.go +++ b/x/precisebank/keeper/burn.go @@ -187,7 +187,7 @@ func (k Keeper) burnExtendedCoin( k.SetRemainderAmount(ctx, newRemainder) // Emit event for fractional balance change - types.EmitFractionalBalanceChange(ctx, moduleAddr, prevFractionalBalance, newFractionalBalance) + types.EmitEventFractionalBalanceChange(ctx, moduleAddr, prevFractionalBalance, newFractionalBalance) return nil } diff --git a/x/precisebank/keeper/mint.go b/x/precisebank/keeper/mint.go index 1c42f1b03..2b3ca4496 100644 --- a/x/precisebank/keeper/mint.go +++ b/x/precisebank/keeper/mint.go @@ -216,7 +216,7 @@ func (k Keeper) mintExtendedCoin( k.SetRemainderAmount(ctx, newRemainder) // Emit event for fractional balance change - types.EmitFractionalBalanceChange(ctx, moduleAddr, fractionalAmount, newFractionalBalance) + types.EmitEventFractionalBalanceChange(ctx, moduleAddr, fractionalAmount, newFractionalBalance) return nil } diff --git a/x/precisebank/keeper/send.go b/x/precisebank/keeper/send.go index cd5350433..938622925 100644 --- a/x/precisebank/keeper/send.go +++ b/x/precisebank/keeper/send.go @@ -243,8 +243,8 @@ func (k Keeper) sendExtendedCoins( k.SetFractionalBalance(ctx, to, recipientNewFracBal) // Emit event for fractional balance change - types.EmitFractionalBalanceChange(ctx, from, senderFracBal, senderNewFracBal) - types.EmitFractionalBalanceChange(ctx, to, recipientFracBal, recipientNewFracBal) + types.EmitEventFractionalBalanceChange(ctx, from, senderFracBal, senderNewFracBal) + types.EmitEventFractionalBalanceChange(ctx, to, recipientFracBal, recipientNewFracBal) return nil } diff --git a/x/precisebank/types/events.go b/x/precisebank/types/events.go index 186ecf604..070b103cf 100644 --- a/x/precisebank/types/events.go +++ b/x/precisebank/types/events.go @@ -29,7 +29,7 @@ func NewEventFractionalBalanceChange( ) } -func EmitFractionalBalanceChange( +func EmitEventFractionalBalanceChange( ctx sdk.Context, address sdk.AccAddress, beforeAmount sdkmath.Int, From d5a83c76f22a46c7e774204f215049ad070fb577 Mon Sep 17 00:00:00 2001 From: Kyuhyeon Choi Date: Thu, 14 Aug 2025 20:36:51 +0900 Subject: [PATCH 10/10] chore: fix lint --- tests/integration/precompiles/werc20/test_integration.go | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/integration/precompiles/werc20/test_integration.go b/tests/integration/precompiles/werc20/test_integration.go index 36ae15bc3..686bdb65f 100644 --- a/tests/integration/precompiles/werc20/test_integration.go +++ b/tests/integration/precompiles/werc20/test_integration.go @@ -233,7 +233,6 @@ func TestPrecompileIntegrationTestSuite(t *testing.T, create network.CreateEvmAp Context("and funds are part of the transaction", func() { When("the method is deposit", func() { It("it should return funds to sender and emit the event", func() { - txArgs, callArgs := callsData.getTxAndCallArgs(directCall, werc20.DepositMethod) txArgs.Amount = depositAmount