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/precompiles/common/balance_handler.go b/precompiles/common/balance_handler.go index 6f48bbe89..29292430a 100644 --- a/precompiles/common/balance_handler.go +++ b/precompiles/common/balance_handler.go @@ -2,14 +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" sdk "github.com/cosmos/cosmos-sdk/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" @@ -17,12 +17,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,71 +39,90 @@ 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) } + 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) } - 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) + 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) - 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) - } + 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 + } - accAddr, err := sdk.AccAddressFromBech32(attr.Value) - if err != nil { - return common.Address{}, fmt.Errorf("invalid address %q: %w", attr.Value, err) - } + delta, err := ParseFractionalAmount(event) + if err != nil { + return fmt.Errorf("failed to parse amount from event %q: %w", precisebanktypes.EventTypeFractionalBalanceChange, err) + } - return common.BytesToAddress(accAddr), nil -} + deltaAbs, err := utils.Uint256FromBigInt(new(big.Int).Abs(delta)) + if err != nil { + return fmt.Errorf("failed to convert delta to Uint256: %w", err) + } -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) - } + 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) + } - amountCoins, err := sdk.ParseCoinsNormalized(amountAttr.Value) - if err != nil { - return nil, fmt.Errorf("failed to parse coins from %q: %w", amountAttr.Value, err) + default: + continue + } } - 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 + return nil } 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 +} 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 450999171..173ca6cc0 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" @@ -121,20 +117,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 } 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/tests/integration/precompiles/werc20/test_integration.go b/tests/integration/precompiles/werc20/test_integration.go index 82f257028..686bdb65f 100644 --- a/tests/integration/precompiles/werc20/test_integration.go +++ b/tests/integration/precompiles/werc20/test_integration.go @@ -44,7 +44,7 @@ type PrecompileIntegrationTestSuite struct { wrappedCoinDenom string - // WEVMOS related fields + // WERC20 precompile instance and configuration precompile *werc20.Precompile precompileAddrHex string } @@ -61,13 +61,37 @@ func TestPrecompileIntegrationTestSuite(t *testing.T, create network.CreateEvmAp txSender, user keyring.Key revertContractAddr common.Address + + // Account balance tracking + accountBalances []*AccountBalanceInfo + 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 function to get account balance info by type + balanceOf := func(accountType AccountType) *AccountBalanceInfo { + return GetAccountBalance(accountBalances, accountType) + } BeforeEach(func() { is = new(PrecompileIntegrationTestSuite) @@ -183,83 +207,71 @@ func TestPrecompileIntegrationTestSuite(t *testing.T, create network.CreateEvmAp withdrawCheck = passCheck.WithExpEvents(werc20.EventTypeWithdrawal) depositCheck = passCheck.WithExpEvents(werc20.EventTypeDeposit) transferCheck = passCheck.WithExpEvents(erc20.EventTypeTransfer) + + // 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() { + TakeBalanceSnapshots(accountBalances, is.grpcHandler) + }) + + // AfterEach verifies balance changes + AfterEach(func() { + VerifyBalanceChanges(accountBalances, is.grpcHandler, precisebankRemainder) }) + 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 _, _, 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 +280,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 +287,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 +303,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 +310,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 +320,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 +334,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 +380,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 +387,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 +403,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 +410,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 +421,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) + } + + 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) + + balanceOf(Contract).IntegerDelta = new(big.Int).Quo(depositAmount, conversionFactor) + balanceOf(Contract).FractionalDelta = depositFractional + + balanceOf(PrecisebankModule).IntegerDelta = borrow txArgs, callArgs := callsData.getTxAndCallArgs(contractCall, "depositWithRevert", false, false) txArgs.Amount = depositAmount @@ -485,28 +440,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 +457,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())) + 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 + 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 +496,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 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()) +} 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/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 diff --git a/x/precisebank/keeper/burn.go b/x/precisebank/keeper/burn.go index 73c5e54f7..f971f4d26 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.EmitEventFractionalBalanceChange(ctx, moduleAddr, prevFractionalBalance, newFractionalBalance) + return nil } diff --git a/x/precisebank/keeper/mint.go b/x/precisebank/keeper/mint.go index efaec12f6..2b3ca4496 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.EmitEventFractionalBalanceChange(ctx, moduleAddr, fractionalAmount, newFractionalBalance) + return nil } diff --git a/x/precisebank/keeper/send.go b/x/precisebank/keeper/send.go index 3d3ec54dd..938622925 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.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 new file mode 100644 index 000000000..070b103cf --- /dev/null +++ b/x/precisebank/types/events.go @@ -0,0 +1,41 @@ +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 NewEventFractionalBalanceChange( + address sdk.AccAddress, + beforeAmount sdkmath.Int, + afterAmount sdkmath.Int, +) sdk.Event { + delta := afterAmount.Sub(beforeAmount) + + return sdk.NewEvent( + EventTypeFractionalBalanceChange, + sdk.NewAttribute(AttributeKeyAddress, address.String()), + sdk.NewAttribute(AttributeKeyDelta, delta.String()), + ) +} + +func EmitEventFractionalBalanceChange( + ctx sdk.Context, + address sdk.AccAddress, + beforeAmount sdkmath.Int, + afterAmount sdkmath.Int, +) { + ctx.EventManager().EmitEvent( + NewEventFractionalBalanceChange(address, beforeAmount, afterAmount), + ) +} 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 {