From a220b4f28dfcd72df2dd09dc76d11de46ee0a2db Mon Sep 17 00:00:00 2001 From: Nilesh Gupta Date: Mon, 24 Aug 2026 07:16:17 +0530 Subject: [PATCH 01/15] fix: F-2026-18201 | [Dual Defense] Staking Precompile and Vesting Underflow StateDB Balance Enabling Native Mint and Drain (#40) * fix: guard StateDB SubBalance against underflow (F-2026-18201) Cherry-picks the security-relevant part of upstream cosmos/evm 264aa70 (#1176). * test: cover the precompile balance-handler underflow path (F-2026-18201) --- precompiles/common/balance_handler_test.go | 53 ++++++++++++++++++++++ precompiles/common/utils.go | 14 +++++- x/vm/statedb/state_object.go | 12 ++++- x/vm/statedb/statedb_test.go | 11 ++++- 4 files changed, 86 insertions(+), 4 deletions(-) diff --git a/precompiles/common/balance_handler_test.go b/precompiles/common/balance_handler_test.go index 0e6c79930..a027c9d77 100644 --- a/precompiles/common/balance_handler_test.go +++ b/precompiles/common/balance_handler_test.go @@ -110,6 +110,14 @@ func TestParseAmount(t *testing.T) { }, expAmt: uint256.NewInt(5), }, + { + name: "unrelated denom is ignored", + maleate: func() sdk.Event { + coinStr := sdk.NewCoins(sdk.NewInt64Coin("foobar", 7)).String() + return sdk.NewEvent("bank", sdk.NewAttribute(sdk.AttributeKeyAmount, coinStr)) + }, + expAmt: uint256.NewInt(0), + }, { name: "missing amount", maleate: func() sdk.Event { @@ -186,6 +194,51 @@ func TestAfterBalanceChange(t *testing.T) { require.Equal(t, "3", stateDB.GetBalance(receiver).String()) } +// TestAfterBalanceChangeSpendMoreThanBalancePanics reproduces the F-2026-18201 +// attack path at the precompile boundary: the EVM state view only tracks +// SPENDABLE balance, but a vesting account can delegate LOCKED coins through the +// staking precompile. That emits a coin_spent event for more than the StateDB +// holds. Before the underflow guard the subtraction wrapped to ~2^256, and +// x/vm/keeper/statedb.go reconciled the bogus view back to bank by MINTING the +// difference. It must now hard-fail instead. +func TestAfterBalanceChangeSpendMoreThanBalancePanics(t *testing.T) { + setupBalanceHandlerTest(t) + + storeKey := storetypes.NewKVStoreKey("test") + tKey := storetypes.NewTransientStoreKey("test_t") + ctx := sdktestutil.DefaultContext(storeKey, tKey) + + stateDB := statedb.New(ctx, mocks.NewEVMKeeper(), statedb.NewEmptyTxConfig()) + + _, addrs, err := testutil.GeneratePrivKeyAddressPairs(1) + require.NoError(t, err) + spenderAcc := addrs[0] + spender := common.BytesToAddress(spenderAcc) + + // Spendable balance as seen by the EVM. + stateDB.AddBalance(spender, uint256.NewInt(5), tracing.BalanceChangeUnspecified) + + bankKeeper := cmnmocks.NewBankKeeper(t) + bankKeeper.Mock.On("BlockedAddr", mock.AnythingOfType("types.AccAddress")).Return(false) + + bh := cmn.NewBalanceHandlerFactory(bankKeeper).NewBalanceHandler() + bh.BeforeBalanceChange(ctx) + + // Delegating locked coins spends more than the spendable balance. + ctx.EventManager().EmitEvents(sdk.Events{ + banktypes.NewCoinSpentEvent(spenderAcc, sdk.NewCoins(sdk.NewInt64Coin(evmtypes.GetEVMCoinDenom(), 10))), + }) + + require.PanicsWithValue( + t, + "state balance underflow for "+spender.Hex()+": have=5 sub=10", + func() { _ = bh.AfterBalanceChange(ctx, stateDB) }, + ) + + // The balance must not have wrapped around to ~2^256. + require.Equal(t, "5", stateDB.GetBalance(spender).String()) +} + func TestAfterBalanceChangeErrors(t *testing.T) { setupBalanceHandlerTest(t) diff --git a/precompiles/common/utils.go b/precompiles/common/utils.go index 520dc5e09..4ca23a018 100644 --- a/precompiles/common/utils.go +++ b/precompiles/common/utils.go @@ -42,8 +42,18 @@ func ParseAmount(event sdk.Event) (*uint256.Int, error) { 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)) + baseAmount := amountCoins.AmountOf(evmtypes.GetEVMCoinDenom()).BigInt() + extendedAmount := amountCoins.AmountOf(evmtypes.GetEVMCoinExtendedDenom()).BigInt() + + var amountBigInt *big.Int + if baseAmount.Sign() > 0 { + amountBigInt = evmtypes.ConvertAmountTo18DecimalsBigInt(baseAmount) + } else { + // The extended denom is already represented in 18 decimals. + amountBigInt = extendedAmount + } + + amount, err := utils.Uint256FromBigInt(amountBigInt) if err != nil { return nil, fmt.Errorf("failed to convert coin amount to Uint256: %w", err) } diff --git a/x/vm/statedb/state_object.go b/x/vm/statedb/state_object.go index db52e60c8..28e05c58b 100644 --- a/x/vm/statedb/state_object.go +++ b/x/vm/statedb/state_object.go @@ -2,6 +2,7 @@ package statedb import ( "bytes" + "fmt" "sort" "github.com/ethereum/go-ethereum/common" @@ -122,7 +123,16 @@ func (s *stateObject) SubBalance(amount *uint256.Int) uint256.Int { if amount.IsZero() { return *(s.Balance()) } - return s.SetBalance(new(uint256.Int).Sub(s.Balance(), amount)) + balance := s.Balance() + if balance.Lt(amount) { + panic(fmt.Sprintf( + "state balance underflow for %s: have=%s sub=%s", + s.address.Hex(), + balance.String(), + amount.String(), + )) + } + return s.SetBalance(new(uint256.Int).Sub(balance, amount)) } // SetBalance updates account balance. diff --git a/x/vm/statedb/statedb_test.go b/x/vm/statedb/statedb_test.go index 69bbddc2c..8cea4d214 100644 --- a/x/vm/statedb/statedb_test.go +++ b/x/vm/statedb/statedb_test.go @@ -239,8 +239,17 @@ func (suite *StateDBTestSuite) TestDBError() { } } +func (suite *StateDBTestSuite) TestSubBalanceUnderflowPanics() { + db := statedb.New(sdk.Context{}.WithEventManager(sdk.NewEventManager()), mocks.NewEVMKeeper(), emptyTxConfig) + db.AddBalance(address, uint256.NewInt(1), tracing.BalanceChangeUnspecified) + + expectedPanic := fmt.Sprintf("state balance underflow for %s: have=%s sub=%s", address.Hex(), "1", "2") + suite.Require().PanicsWithValue(expectedPanic, func() { + db.SubBalance(address, uint256.NewInt(2), tracing.BalanceChangeUnspecified) + }) +} + func (suite *StateDBTestSuite) TestBalance() { - // NOTE: no need to test overflow/underflow, that is guaranteed by evm implementation. testCases := []struct { name string malleate func(*statedb.StateDB) From cf393872614d902b3048cebc501d0726a88bff9e Mon Sep 17 00:00:00 2001 From: Nilesh Gupta Date: Mon, 24 Aug 2026 08:05:55 +0530 Subject: [PATCH 02/15] fix: F-2026-18197 | [Dual Defense] Nested Message Dispatch Bypasses EVM Ante for MsgEthereumTx (#41) * fix: verify the eth sender in Keeper.EthereumTx before applying the tx The EVM ante only runs over a tx's top-level messages, so an MsgEthereumTx nested inside a dispatching module reaches the executor with its signature unchecked (F-2026-18197). Require VerifySender in the msg server so From is always the recovered signer, whatever route the message took. * docs: changelog entry for the EthereumTx sender verification fix * fix: reject an MsgEthereumTx whose raw transaction is empty ValidateBasic lives in the same ante path a nested message skips, so the msg server must not dereference a nil transaction. --- CHANGELOG.md | 6 ++ tests/integration/x/vm/test_msg_server.go | 102 ++++++++++++++++++++++ x/vm/keeper/msg_server.go | 34 ++++++++ 3 files changed, 142 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc3ecb5fc..92b279089 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,12 @@ ### BUG FIXES +- Verify the Ethereum sender in `Keeper.EthereumTx` before applying the transaction. The EVM ante + handler only runs over a tx's top-level messages, so an `MsgEthereumTx` nested inside a + dispatching module (`x/authz`, `x/group`, `x/gov`, a CosmWasm stargate/`Any` message, ICA) reached + the executor with its signature never checked, letting a victim-signed transaction be replayed as + the victim. `VerifySender` is now required before `ApplyTransaction`, so `From` is always the + ECDSA-recovered signer regardless of how the message arrived. - [\#690](https://github.com/cosmos/evm/pull/690) Fix Ledger hardware wallet support for coin type 60. - [\#769](https://github.com/cosmos/evm/pull/769) Fix erc20 ibc middleware to not to validate sender address format. - [\#790](https://github.com/cosmos/evm/pull/790) fix panic in historical query due to missing EvmCoinInfo. diff --git a/tests/integration/x/vm/test_msg_server.go b/tests/integration/x/vm/test_msg_server.go index 8f776f9c3..b5f612b36 100644 --- a/tests/integration/x/vm/test_msg_server.go +++ b/tests/integration/x/vm/test_msg_server.go @@ -12,6 +12,7 @@ import ( sdkmath "cosmossdk.io/math" sdktypes "github.com/cosmos/cosmos-sdk/types" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" ) @@ -260,3 +261,104 @@ func (s *KeeperTestSuite) TestRegisterPreinstalls() { s.Require().NoError(err) } } + +// TestEthereumTxSenderVerification is the regression guard for F-2026-18197 +// (nested message dispatch bypasses the EVM ante for MsgEthereumTx). +// +// The EVM ante handler only runs over the top-level messages of a tx. A module +// that unpacks and re-dispatches an embedded sdk.Msg (x/authz MsgExec, x/group +// MsgSubmitProposal/MsgExec, x/gov proposals, a CosmWasm stargate/Any message, +// ICA) hands the nested message to this msg server with the ante already +// behind it, so the signature was never checked. Keeper.EthereumTx therefore +// verifies the sender itself: the declared From must be the ECDSA signer of the +// raw transaction, whatever route the message took to get here. +func (s *KeeperTestSuite) TestEthereumTxSenderVerification() { + testCases := []struct { + name string + mutate func(msg *types.MsgEthereumTx) + expErr bool + }{ + { + // The ante already ran VerifySender for this path, so the keeper + // check is a redundant no-op and normal traffic is unaffected. + name: "pass - untouched victim-signed tx (normal JSON-RPC path)", + mutate: func(*types.MsgEthereumTx) {}, + expErr: false, + }, + { + // The attack: take a victim-signed tx off the mempool and push it + // through a nested dispatcher, declaring the attacker (or a group + // policy / authz grantee account) as the sender so the outer + // permission check passes. Recovery still yields the victim. + name: "fail - From spoofed to a different account", + mutate: func(msg *types.MsgEthereumTx) { + msg.From = s.Keyring.GetAddr(1).Bytes() + }, + expErr: true, + }, + { + // A module account is derived from a name, never from a key pair, + // so no signature can ever recover to it. This is what makes it a + // design invariant that module-driven EVM calls go through + // ApplyMessage* directly and never through this msg server. + name: "fail - From spoofed to a module account", + mutate: func(msg *types.MsgEthereumTx) { + msg.From = authtypes.NewModuleAddress(govtypes.ModuleName).Bytes() + }, + expErr: true, + }, + { + name: "fail - From cleared", + mutate: func(msg *types.MsgEthereumTx) { + msg.From = nil + }, + expErr: true, + }, + } + + s.Run("fail - empty raw transaction", func() { + s.SetupTest() + // Raw unmarshals to a nil transaction when the field is empty, and + // ValidateBasic lives in the same ante path a nested message skips. + _, err := s.Network.App.GetEVMKeeper().EthereumTx(s.Network.GetContext(), &types.MsgEthereumTx{}) + s.Require().Error(err) + s.Require().ErrorIs(err, errortypes.ErrInvalidRequest) + }) + + for _, tc := range testCases { + s.Run(tc.name, func() { + s.SetupTest() + + victim := s.Keyring.GetKey(0) + recipient := s.Keyring.GetAddr(1) + tx, err := s.Factory.GenerateSignedEthTx(victim.Priv, types.EvmTxArgs{ + To: &recipient, + Amount: big.NewInt(1e10), + }) + s.Require().NoError(err) + + msg := tx.GetMsgs()[0].(*types.MsgEthereumTx) + s.Require().Equal(victim.Addr.Bytes(), msg.From, "sanity: tx must be signed by the victim") + tc.mutate(msg) + + ctx := s.Network.GetContext() + res, err := s.Network.App.GetEVMKeeper().EthereumTx(ctx, msg) + + if !tc.expErr { + s.Require().NoError(err) + s.Require().False(res.Failed()) + return + } + + s.Require().Error(err) + s.Require().ErrorIs(err, errortypes.ErrorInvalidSigner) + s.Require().Contains(err.Error(), "signature verification failed") + // The transaction must not have been applied at all. + s.Require().Nil(res) + s.Require().False( + utils.ContainsEventType(ctx.EventManager().Events().ToABCIEvents(), types.EventTypeEthereumTx), + "a rejected tx must not emit an ethereum_tx event", + ) + }) + } +} diff --git a/x/vm/keeper/msg_server.go b/x/vm/keeper/msg_server.go index 27cebf57a..9cc27684c 100644 --- a/x/vm/keeper/msg_server.go +++ b/x/vm/keeper/msg_server.go @@ -4,10 +4,13 @@ import ( "context" "encoding/hex" "fmt" + "math/big" "strconv" "github.com/hashicorp/go-metrics" + ethtypes "github.com/ethereum/go-ethereum/core/types" + cmttypes "github.com/cometbft/cometbft/types" "github.com/cosmos/evm/x/vm/types" @@ -17,6 +20,7 @@ import ( "github.com/cosmos/cosmos-sdk/telemetry" sdk "github.com/cosmos/cosmos-sdk/types" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" ) @@ -30,6 +34,36 @@ func (k *Keeper) EthereumTx(goCtx context.Context, msg *types.MsgEthereumTx) (*t ctx := sdk.UnwrapSDKContext(goCtx) tx := msg.AsTransaction() + // MsgEthereumTx.Raw unmarshals to a nil transaction when the raw field is + // empty, and ValidateBasic is part of the same ante path that a nested + // message skips, so the executor cannot assume it ran. Fail cleanly instead + // of dereferencing nil below. + if tx == nil { + return nil, errorsmod.Wrap(errortypes.ErrInvalidRequest, "invalid transaction: raw ethereum transaction is empty") + } + + // Verify that msg.From really is the ECDSA signer of the transaction. + // + // This is normally established by the EVM ante handler + // (ante/evm/05_signature_verification.go), but the ante chain only runs over + // the *top-level* messages of a tx. Any module that unpacks an embedded + // sdk.Msg and re-dispatches it through the message router - x/authz MsgExec, + // x/group MsgSubmitProposal/MsgExec, x/gov proposals, a CosmWasm + // stargate/Any message, ICA - delivers the nested message here with the ante + // already behind it, so nothing has checked the signature. An attacker could + // then take any victim-signed transaction off the mempool and have it + // executed as the victim. Checking here means the executor never trusts an + // unverified From, whatever route the message took to reach it. + // + // Cost on the normal path is ~zero: msg.AsTransaction() hands back the same + // *ethtypes.Transaction the ante verified, and go-ethereum caches the + // recovered sender on it per signer, so this is a cache hit rather than a + // second ECDSA recovery. + signer := ethtypes.MakeSigner(types.GetEthChainConfig(), big.NewInt(ctx.BlockHeight()), uint64(ctx.BlockTime().Unix())) //#nosec G115 -- int overflow is not a concern here + if err := msg.VerifySender(signer); err != nil { + return nil, errorsmod.Wrapf(errortypes.ErrorInvalidSigner, "signature verification failed: %s", err.Error()) + } + txIndex := k.GetTxIndexTransient(ctx) labels := []metrics.Label{ From 3358532ffeab4caf4cd07183799b3f2852c4c8b6 Mon Sep 17 00:00:00 2001 From: Nilesh Gupta Date: Mon, 24 Aug 2026 10:45:05 +0530 Subject: [PATCH 03/15] fix: F-2026-18187 | [Dual Defense] Gov Precompile getTallyResult Persists SDK Tally Vote Deletions (#42) * fix(gov): discard the store writes of the getTallyResult query The SDK tally deletes the votes it counts; precompiles commit unconditionally. * test(gov): getTallyResult must not delete votes via CALL or STATICCALL * docs: changelog entry for the getTallyResult vote deletion fix --- CHANGELOG.md | 6 + precompiles/gov/query.go | 41 ++++- tests/integration/precompiles/gov/test_gov.go | 164 ++++++++++++++++++ 3 files changed, 210 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92b279089..4064e1bbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,12 @@ ### BUG FIXES +- Run the gov precompile's `getTallyResult` against a throwaway cache. The SDK's `TallyResult` query + delegates to `Keeper.Tally`, which deletes every vote it counts; that is safe on the SDK's own + query paths because a gRPC query runs against a context that is never committed, but precompiles + commit their state DB cache context unconditionally. A plain EVM call to `getTallyResult` on a + proposal in the voting period therefore deleted that proposal's votes permanently. The query's + store writes are now discarded, leaving the tally itself unchanged. - Verify the Ethereum sender in `Keeper.EthereumTx` before applying the transaction. The EVM ante handler only runs over a tx's top-level messages, so an `MsgEthereumTx` nested inside a dispatching module (`x/authz`, `x/group`, `x/gov`, a CosmWasm stargate/`Any` message, ICA) reached diff --git a/precompiles/gov/query.go b/precompiles/gov/query.go index 0b389b9a0..ff7482f4b 100644 --- a/precompiles/gov/query.go +++ b/precompiles/gov/query.go @@ -4,6 +4,8 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/core/vm" + vmstoretypes "github.com/cosmos/evm/x/vm/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -136,7 +138,21 @@ func (p *Precompile) GetTallyResult( return nil, err } - res, err := p.govQuerier.TallyResult(ctx, queryTallyResultReq) + // The SDK's TallyResult query is *not* read-only for a proposal in the voting + // period: it delegates to the gov Keeper.Tally, which deletes every vote it + // counts (x/gov/keeper/tally.go). That is safe on the SDK's own query paths + // because a gRPC query runs against a context whose writes are never committed. + // + // Precompiles break that invariant: RunNativeAction commits the state DB cache + // context unconditionally, so a store write performed here would survive the + // enclosing EVM transaction and a plain CALL to getTallyResult would wipe the + // proposal's votes. Run the query against a throwaway cache instead, so the + // deletions are discarded while the tally itself -- delegations, weighted votes, + // validator defaults -- is still the one the SDK computes. + cacheCtx, discardWrites := newDiscardedCacheContext(ctx) + defer discardWrites() + + res, err := p.govQuerier.TallyResult(cacheCtx, queryTallyResultReq) if err != nil { return nil, err } @@ -145,6 +161,29 @@ func (p *Precompile) GetTallyResult( return method.Outputs.Pack(output.TallyResult) } +// newDiscardedCacheContext returns a context to run a query on, together with the +// function that throws away every store write the query performed. The caller is +// expected to always call it -- the writes are never meant to reach the parent +// store. +// +// sdk.Context.CacheContext alone is not enough inside a precompile. It defers to +// the underlying multi store's CacheMultiStore, and while the SDK's stores return +// a detached cache whose writes only reach the parent through the (here dropped) +// write function, the store the EVM runs precompiles on does not: +// x/vm/store/snapshotmulti pushes a snapshot layer and returns the very same +// store, so the layer stays live and is flushed along with everything else when +// the state DB commits. On that store the layer has to be popped explicitly, +// which is what the snapshot/revert pair below does. +func newDiscardedCacheContext(ctx sdk.Context) (sdk.Context, func()) { + if snapshotter, ok := ctx.MultiStore().(vmstoretypes.Snapshotter); ok { + snapshot := snapshotter.Snapshot() + return ctx, func() { snapshotter.RevertToSnapshot(snapshot) } + } + + cacheCtx, _ := ctx.CacheContext() // write function intentionally discarded + return cacheCtx, func() {} +} + // GetProposal implements the query logic for getting a proposal func (p *Precompile) GetProposal( ctx sdk.Context, diff --git a/tests/integration/precompiles/gov/test_gov.go b/tests/integration/precompiles/gov/test_gov.go index b2a2aee96..fca69a6c4 100644 --- a/tests/integration/precompiles/gov/test_gov.go +++ b/tests/integration/precompiles/gov/test_gov.go @@ -13,6 +13,11 @@ import ( "github.com/cosmos/evm/testutil" "github.com/cosmos/evm/x/vm/statedb" evmtypes "github.com/cosmos/evm/x/vm/types" + + "cosmossdk.io/collections" + + sdk "github.com/cosmos/cosmos-sdk/types" + govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" ) func (s *PrecompileTestSuite) TestIsTransaction() { @@ -136,3 +141,162 @@ func (s *PrecompileTestSuite) TestRun() { }) } } + +// runPrecompile executes the gov precompile with the given calldata, mirroring +// what the EVM does for a delivered transaction: the state DB is committed after +// the call so that every store write performed by the precompile is flushed to +// the underlying context. +func (s *PrecompileTestSuite) runPrecompile(ctx sdk.Context, input []byte, readOnly bool) []byte { + baseFee := s.network.App.GetEVMKeeper().GetBaseFee(ctx) + + contract := vm.NewPrecompile(s.keyring.GetAddr(0), s.precompile.Address(), uint256.NewInt(0), uint64(1e6)) + contract.Input = input + + contractAddr := contract.Address() + // Build and sign the Ethereum transaction backing the call. + txArgs := evmtypes.EvmTxArgs{ + ChainID: evmtypes.GetEthChainConfig().ChainID, + Nonce: 0, + To: &contractAddr, + Amount: nil, + GasLimit: 100000, + GasPrice: testutil.ExampleMinGasPrices, + GasFeeCap: baseFee, + GasTipCap: big.NewInt(1), + Accesses: ðtypes.AccessList{}, + } + msg, err := s.factory.GenerateGethCoreMsg(s.keyring.GetPrivKey(0), txArgs) + s.Require().NoError(err) + + cfg, err := s.network.App.GetEVMKeeper().EVMConfig(ctx, ctx.BlockHeader().ProposerAddress) + s.Require().NoError(err, "failed to instantiate EVM config") + + stDB := statedb.New(ctx, s.network.App.GetEVMKeeper(), statedb.NewEmptyTxConfig()) + evm := s.network.App.GetEVMKeeper().NewEVM(ctx, *msg, cfg, nil, stDB) + + precompiles, found, err := s.network.App.GetEVMKeeper().GetPrecompileInstance(ctx, contractAddr) + s.Require().NoError(err, "failed to instantiate precompile") + s.Require().True(found, "not found precompile") + evm.WithPrecompiles(precompiles.Map) + + bz, err := s.precompile.Run(evm, contract, readOnly) + s.Require().NoError(err, "expected no error when running the precompile") + s.Require().NotNil(bz, "expected returned bytes not to be nil") + + // A delivered tx always commits the state DB, which flushes the precompile's + // cache context onto the underlying multi store. + s.Require().NoError(stDB.Commit(), "failed to commit the state DB") + + return bz +} + +// proposalVoters returns the voters that currently have a vote stored for the +// given proposal. +func (s *PrecompileTestSuite) proposalVoters(ctx sdk.Context, proposalID uint64) []string { + govKeeper := s.network.App.GetGovKeeper() + + voters := []string{} + err := govKeeper.Votes.Walk( + ctx, + collections.NewPrefixedPairRange[uint64, sdk.AccAddress](proposalID), + func(_ collections.Pair[uint64, sdk.AccAddress], vote govv1.Vote) (bool, error) { + voters = append(voters, vote.Voter) + return false, nil + }, + ) + s.Require().NoError(err, "failed to walk the proposal votes") + + return voters +} + +// TestGetTallyResultDoesNotDeleteVotes is the regression test for F-2026-18187. +// +// The SDK's gov TallyResult query delegates to Keeper.Tally, which deletes every +// vote it counts. That is safe on the SDK's own query paths because a gRPC query +// runs against a context whose writes are never committed. Precompiles break that +// invariant: cmn.Precompile.runNativeAction commits the state DB cache context +// unconditionally, so before the fix a plain committing EVM call to +// getTallyResult wiped the votes of any proposal in the voting period. +// +// Query classification does not help here: getTallyResult is absent from +// IsTransaction, but that only feeds the staticcall guard and the gas price, so +// both a CALL and a STATICCALL reach the mutating SDK code. Both are covered. +func (s *PrecompileTestSuite) TestGetTallyResultDoesNotDeleteVotes() { + const proposalID uint64 = 1 + + testcases := []struct { + name string + readOnly bool + }{ + { + name: "call", + readOnly: false, + }, + { + name: "staticcall", + readOnly: true, + }, + } + + for _, tc := range testcases { + s.Run(tc.name, func() { + s.SetupTest() + + ctx := s.network.GetContext() + govKeeper := s.network.App.GetGovKeeper() + + // Account 0 is the only delegator of the test network, so it holds all of + // the voting power (3e18). Split its vote across every option so that the + // positive control below exercises the whole tally result. + s.Require().NoError(govKeeper.AddVote(ctx, proposalID, s.keyring.GetAccAddr(0), govv1.WeightedVoteOptions{ + {Option: govv1.OptionYes, Weight: "0.4"}, + {Option: govv1.OptionAbstain, Weight: "0.3"}, + {Option: govv1.OptionNo, Weight: "0.2"}, + {Option: govv1.OptionNoWithVeto, Weight: "0.1"}, + }, "")) + // Accounts 1 and 2 hold no delegations: they add nothing to the tally but + // are stored votes all the same, and Keeper.Tally removes them too. + s.Require().NoError(govKeeper.AddVote(ctx, proposalID, s.keyring.GetAccAddr(1), govv1.NewNonSplitVoteOption(govv1.OptionNo), "")) + s.Require().NoError(govKeeper.AddVote(ctx, proposalID, s.keyring.GetAccAddr(2), govv1.NewNonSplitVoteOption(govv1.OptionYes), "")) + + expVoters := []string{ + s.keyring.GetAccAddr(0).String(), + s.keyring.GetAccAddr(1).String(), + s.keyring.GetAccAddr(2).String(), + } + s.Require().ElementsMatch(expVoters, s.proposalVoters(ctx, proposalID), "expected the votes to be stored before the query") + + proposal, err := govKeeper.Proposals.Get(ctx, proposalID) + s.Require().NoError(err) + s.Require().Equal(govv1.StatusVotingPeriod, proposal.Status, "the proposal must be in the voting period for the tally to mutate state") + + input, err := s.precompile.Pack(gov.GetTallyResultMethod, proposalID) + s.Require().NoError(err, "failed to pack input") + + bz := s.runPrecompile(ctx, input, tc.readOnly) + + // Regression assertion: the query must leave every vote in place. + s.Require().ElementsMatch(expVoters, s.proposalVoters(ctx, proposalID), "getTallyResult must not delete the proposal's votes") + + // Positive control: the tally must still be the one the SDK computes, so + // we do not trade a mutating query for a wrong one. + expTally := gov.TallyResultData{ + Yes: "1200000000000000000", // 40% of 3e18 + Abstain: "900000000000000000", // 30% of 3e18 + No: "600000000000000000", // 20% of 3e18 + NoWithVeto: "300000000000000000", // 10% of 3e18 + } + + var out gov.TallyResultOutput + s.Require().NoError(s.precompile.UnpackIntoInterface(&out, gov.GetTallyResultMethod, bz)) + s.Require().Equal(expTally, out.TallyResult, "unexpected tally result") + + // And the query must be repeatable: a second call sees the same votes and + // therefore returns the same numbers. + var secondOut gov.TallyResultOutput + s.Require().NoError(s.precompile.UnpackIntoInterface(&secondOut, gov.GetTallyResultMethod, s.runPrecompile(ctx, input, tc.readOnly))) + s.Require().Equal(expTally, secondOut.TallyResult, "the tally result must be stable across calls") + s.Require().ElementsMatch(expVoters, s.proposalVoters(ctx, proposalID), "the votes must survive repeated queries") + }) + } +} From c346f918cf0a616194bb1002f27bb574e514d6a2 Mon Sep 17 00:00:00 2001 From: Nilesh Gupta Date: Wed, 26 Aug 2026 19:27:36 +0530 Subject: [PATCH 04/15] fix: charge parent gas meter for failed EVM calls (F-2026-18824) (#43) CallEVMWithData and DerivedEVMCallWithData returned on res.Failed() before reaching ctx.GasMeter().ConsumeGas(res.GasUsed), so a reverting or deliberately out-of-gas internal call was free at the Cosmos meter. DerivedEVMCallWithData also clamps a caller-supplied gasLimit to config.DefaultGasCap: on an out-of-gas halt res.GasUsed equals the cap, so without the clamp the caller would choose how much gas the enclosing Cosmos tx is forced to consume and could panic it with OutOfGas. --- CHANGELOG.md | 9 + tests/integration/x/vm/test_call_evm.go | 54 ++++++ tests/integration/x/vm/test_derived_call.go | 188 ++++++++++++++++++++ x/vm/keeper/call_evm.go | 30 +++- 4 files changed, 276 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4064e1bbc..6fa9bc933 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,15 @@ the executor with its signature never checked, letting a victim-signed transaction be replayed as the victim. `VerifySender` is now required before `ApplyTransaction`, so `From` is always the ECDSA-recovered signer regardless of how the message arrived. +- Charge the parent Cosmos gas meter for failed EVM calls in `CallEVMWithData` and + `DerivedEVMCallWithData`. Both functions returned on `res.Failed()` before reaching + `ctx.GasMeter().ConsumeGas(res.GasUsed)`, so a reverting — or deliberately out-of-gas — + internal call performed real EVM work that cost the enclosing Cosmos transaction nothing + beyond the incidental KV-store gas. `DerivedEVMCallWithData` additionally clamps a + caller-supplied `gasLimit` to `config.DefaultGasCap`; on an out-of-gas halt `res.GasUsed` + equals the gas cap, so without the clamp the caller would pick how much gas the enclosing + transaction is forced to consume and could panic it with `OutOfGas`. `CallEVMWithData` + needs no clamp — its message gas limit is already the hardcoded `config.DefaultGasCap`. - [\#690](https://github.com/cosmos/evm/pull/690) Fix Ledger hardware wallet support for coin type 60. - [\#769](https://github.com/cosmos/evm/pull/769) Fix erc20 ibc middleware to not to validate sender address format. - [\#790](https://github.com/cosmos/evm/pull/790) fix panic in historical query due to missing EvmCoinInfo. diff --git a/tests/integration/x/vm/test_call_evm.go b/tests/integration/x/vm/test_call_evm.go index 013b200f2..787ef3dd3 100644 --- a/tests/integration/x/vm/test_call_evm.go +++ b/tests/integration/x/vm/test_call_evm.go @@ -10,6 +10,8 @@ import ( utiltx "github.com/cosmos/evm/testutil/tx" "github.com/cosmos/evm/x/erc20/types" evmtypes "github.com/cosmos/evm/x/vm/types" + + storetypes "cosmossdk.io/store/types" ) func (s *KeeperTestSuite) TestCallEVM() { @@ -148,3 +150,55 @@ func (s *KeeperTestSuite) TestCallEVMWithData() { }) } } + +// TestCallEVMWithDataFailedCallChargesParentGas is the CallEVMWithData half of +// F-2026-18824. Like DerivedEVMCallWithData, this function returned on +// res.Failed() before reaching ctx.GasMeter().ConsumeGas, so a reverting call +// performed real EVM work for free at the Cosmos meter. Unlike the derived path +// it needs no clamp: msg.GasLimit here is the hardcoded config.DefaultGasCap, so +// res.GasUsed is already bounded and the caller cannot inflate it. +func (s *KeeperTestSuite) TestCallEVMWithDataFailedCallChargesParentGas() { + s.SetupTest() + + from := s.Keyring.GetAddr(0) + ctx := s.Network.GetContext().WithGasMeter(storetypes.NewGasMeter(parentGasMeterLimit)) + keeper := s.Network.App.GetEVMKeeper() + + burner := s.deployRawCode(ctx, gasBurnerRevertCode) + gasBefore := ctx.GasMeter().GasConsumed() + + res, err := keeper.CallEVMWithData(ctx, from, &burner, nil, true, nil) + s.Require().Error(err, "the burner reverts, so the call must surface an error") + s.Require().NotNil(res) + s.Require().True(res.Failed()) + s.Require().Greater(res.GasUsed, uint64(2_000_000)) + + charged := ctx.GasMeter().GasConsumed() - gasBefore + s.Require().GreaterOrEqual(charged, res.GasUsed, + "a failed CallEVMWithData must charge its EVM gas to the parent Cosmos meter (F-2026-18824)") + s.Require().LessOrEqual(charged, res.GasUsed+1_000_000, + "res.GasUsed must be charged once, not doubled") +} + +// TestCallEVMWithDataSuccessChargesGasUsed pins the CallEVMWithData happy path: +// res.GasUsed is charged exactly once, unchanged by the failure-path fix. +func (s *KeeperTestSuite) TestCallEVMWithDataSuccessChargesGasUsed() { + s.SetupTest() + + from := s.Keyring.GetAddr(0) + ctx := s.Network.GetContext().WithGasMeter(storetypes.NewGasMeter(parentGasMeterLimit)) + keeper := s.Network.App.GetEVMKeeper() + + burner := s.deployRawCode(ctx, gasBurnerSuccessCode) + gasBefore := ctx.GasMeter().GasConsumed() + + res, err := keeper.CallEVMWithData(ctx, from, &burner, nil, true, nil) + s.Require().NoError(err) + s.Require().False(res.Failed()) + s.Require().Greater(res.GasUsed, uint64(2_000_000)) + + charged := ctx.GasMeter().GasConsumed() - gasBefore + s.Require().GreaterOrEqual(charged, res.GasUsed) + s.Require().LessOrEqual(charged, res.GasUsed+1_000_000, + "res.GasUsed must be charged exactly once on the success path") +} diff --git a/tests/integration/x/vm/test_derived_call.go b/tests/integration/x/vm/test_derived_call.go index b1d73f80a..5cacb679b 100644 --- a/tests/integration/x/vm/test_derived_call.go +++ b/tests/integration/x/vm/test_derived_call.go @@ -5,14 +5,21 @@ import ( "strconv" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/holiman/uint256" abcitypes "github.com/cometbft/cometbft/abci/types" "github.com/cosmos/evm/contracts" + "github.com/cosmos/evm/server/config" testconstants "github.com/cosmos/evm/testutil/constants" "github.com/cosmos/evm/testutil/integration/evm/utils" + utiltx "github.com/cosmos/evm/testutil/tx" + "github.com/cosmos/evm/x/vm/statedb" evmtypes "github.com/cosmos/evm/x/vm/types" + storetypes "cosmossdk.io/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -189,3 +196,184 @@ func (s *KeeperTestSuite) TestDerivedEVMCallWithDataRevertDoesNotMutateBloom() { s.Require().Equal(bloomBefore, keeper.GetBlockBloomTransient(ctx).Bytes(), "a reverted derived execution must not mutate the block bloom (F-2026-17738)") } + +// --------------------------------------------------------------------------- +// F-2026-18824: failed EVM calls must charge the parent Cosmos gas meter +// --------------------------------------------------------------------------- + +// gasBurnerRevertCode expands memory to 1 MiB and then REVERTs: +// +// PUSH1 0x00 // MSTORE value +// PUSH3 0x100000 // MSTORE offset -> forces a 1 MiB memory expansion +// MSTORE +// PUSH1 0x00 +// PUSH1 0x00 +// REVERT +// +// The memory expansion costs ~2.2M gas, deterministically, and REVERT hands the +// rest back — so res.GasUsed lands far above the incidental KV-store gas the +// call also draws, which is what makes it a usable signal in these tests. +var gasBurnerRevertCode = []byte{0x60, 0x00, 0x62, 0x10, 0x00, 0x00, 0x52, 0x60, 0x00, 0x60, 0x00, 0xfd} + +// gasBurnerSuccessCode is gasBurnerRevertCode with the trailing REVERT swapped +// for STOP: the same ~2.2M of memory expansion, but a successful execution. +var gasBurnerSuccessCode = []byte{0x60, 0x00, 0x62, 0x10, 0x00, 0x00, 0x52, 0x00} + +// invalidOpcodeCode is a single INVALID opcode. An exceptional halt burns every +// unit of gas handed to the frame, so res.GasUsed comes back equal to the gas +// cap — the same shape as running out of gas, without waiting for a loop to +// actually consume tens of millions of gas. +var invalidOpcodeCode = []byte{0xfe} + +// parentGasMeterLimit bounds the enclosing "Cosmos tx" in these tests. It sits +// above config.DefaultGasCap (25M) so a correctly clamped failure fits, and far +// below an unclamped caller-supplied limit so an unclamped charge panics. +const parentGasMeterLimit = 60_000_000 + +// deployRawCode installs raw runtime bytecode at a fresh address. +func (s *KeeperTestSuite) deployRawCode(ctx sdk.Context, code []byte) common.Address { + addr := utiltx.GenerateAddress() + codeHash := crypto.Keccak256Hash(code) + + k := s.Network.App.GetEVMKeeper() + k.SetCode(ctx, codeHash.Bytes(), code) + s.Require().NoError(k.SetAccount(ctx, addr, statedb.Account{ + Nonce: 1, + Balance: new(uint256.Int), + CodeHash: codeHash.Bytes(), + })) + return addr +} + +// TestDerivedEVMCallWithDataFailedCallChargesParentGas is the regression test for +// F-2026-18824. A reverting derived call used to return before the parent +// GasMeter().ConsumeGas, so the EVM work it had just performed was free at the +// Cosmos meter — only the incidental KV-store gas leaked through. The call now +// charges res.GasUsed on the failure path exactly as it does on success. +func (s *KeeperTestSuite) TestDerivedEVMCallWithDataFailedCallChargesParentGas() { + s.SetupTest() + + from := s.Keyring.GetAddr(0) + ctx := s.Network.GetContext().WithGasMeter(storetypes.NewGasMeter(parentGasMeterLimit)) + keeper := s.Network.App.GetEVMKeeper() + + burner := s.deployRawCode(ctx, gasBurnerRevertCode) + gasBefore := ctx.GasMeter().GasConsumed() + + res, err := keeper.DerivedEVMCallWithData( + ctx, from, &burner, nil, + true, // commit + false, // gasless + false, // isModuleSender + big.NewInt(0), big.NewInt(5_000_000), nil, + ) + s.Require().Error(err, "the burner reverts, so the call must surface an error") + s.Require().NotNil(res) + s.Require().True(res.Failed()) + s.Require().Greater(res.GasUsed, uint64(2_000_000), + "the burner must actually burn multi-million gas for this test to mean anything") + + charged := ctx.GasMeter().GasConsumed() - gasBefore + s.Require().GreaterOrEqual(charged, res.GasUsed, + "a failed derived call must charge its EVM gas to the parent Cosmos meter (F-2026-18824)") + s.Require().LessOrEqual(charged, res.GasUsed+1_000_000, + "res.GasUsed must be charged once, not doubled") +} + +// TestDerivedEVMCallWithDataGaslessFailureChargesParentGas covers the same +// failure path with gasless=true. The flag only zeroes the TxGasUsed event +// attribute; it never exempted the parent meter, and must not become an exemption +// now that failures are charged. +func (s *KeeperTestSuite) TestDerivedEVMCallWithDataGaslessFailureChargesParentGas() { + s.SetupTest() + + from := s.Keyring.GetAddr(0) + ctx := s.Network.GetContext().WithGasMeter(storetypes.NewGasMeter(parentGasMeterLimit)) + keeper := s.Network.App.GetEVMKeeper() + + burner := s.deployRawCode(ctx, gasBurnerRevertCode) + gasBefore := ctx.GasMeter().GasConsumed() + + res, err := keeper.DerivedEVMCallWithData( + ctx, from, &burner, nil, + true, // commit + true, // gasless + false, + big.NewInt(0), big.NewInt(5_000_000), nil, + ) + s.Require().Error(err) + s.Require().True(res.Failed()) + s.Require().GreaterOrEqual(ctx.GasMeter().GasConsumed()-gasBefore, res.GasUsed, + "gasless only zeroes the reported event attribute, not the parent meter") +} + +// TestDerivedEVMCallWithDataClampsCallerGasLimit is the safety half of +// F-2026-18824. Charging res.GasUsed on failure is only sound because the +// effective gas cap is clamped to config.DefaultGasCap: on an exceptional halt +// res.GasUsed equals the cap, so an unclamped caller-supplied gasLimit would let +// the caller choose how much gas the enclosing Cosmos tx is forced to consume and +// panic it with OutOfGas — which, on the MsgVoteInbound path, means a lost +// validator vote. Here the caller asks for 10x DefaultGasCap and the call halts +// on an INVALID opcode; the parent meter must absorb at most the clamped cap. +func (s *KeeperTestSuite) TestDerivedEVMCallWithDataClampsCallerGasLimit() { + s.SetupTest() + + from := s.Keyring.GetAddr(0) + ctx := s.Network.GetContext().WithGasMeter(storetypes.NewGasMeter(parentGasMeterLimit)) + keeper := s.Network.App.GetEVMKeeper() + + halter := s.deployRawCode(ctx, invalidOpcodeCode) + gasBefore := ctx.GasMeter().GasConsumed() + + //nolint:gosec // test-only constant, no overflow + callerGasLimit := big.NewInt(int64(config.DefaultGasCap) * 10) + + var res *evmtypes.MsgEthereumTxResponse + var err error + s.Require().NotPanics(func() { + res, err = keeper.DerivedEVMCallWithData( + ctx, from, &halter, nil, + true, false, false, + big.NewInt(0), callerGasLimit, nil, + ) + }, "a caller-supplied gasLimit above DefaultGasCap must not be able to panic the parent gas meter") + + s.Require().Error(err) + s.Require().True(res.Failed()) + s.Require().Equal(config.DefaultGasCap, res.GasUsed, + "an exceptional halt burns the whole cap, and the cap must be the clamped DefaultGasCap") + + charged := ctx.GasMeter().GasConsumed() - gasBefore + s.Require().GreaterOrEqual(charged, config.DefaultGasCap, + "the clamped cap is still charged to the parent meter") + s.Require().LessOrEqual(charged, config.DefaultGasCap+1_000_000, + "the parent meter must never be charged beyond the clamped cap") +} + +// TestDerivedEVMCallWithDataSuccessChargesGasUsed pins the happy path: a +// successful derived call charges res.GasUsed once, unchanged by the failure-path +// fix. +func (s *KeeperTestSuite) TestDerivedEVMCallWithDataSuccessChargesGasUsed() { + s.SetupTest() + + from := s.Keyring.GetAddr(0) + ctx := s.Network.GetContext().WithGasMeter(storetypes.NewGasMeter(parentGasMeterLimit)) + keeper := s.Network.App.GetEVMKeeper() + + burner := s.deployRawCode(ctx, gasBurnerSuccessCode) + gasBefore := ctx.GasMeter().GasConsumed() + + res, err := keeper.DerivedEVMCallWithData( + ctx, from, &burner, nil, + true, false, false, + big.NewInt(0), big.NewInt(5_000_000), nil, + ) + s.Require().NoError(err) + s.Require().False(res.Failed()) + s.Require().Greater(res.GasUsed, uint64(2_000_000)) + + charged := ctx.GasMeter().GasConsumed() - gasBefore + s.Require().GreaterOrEqual(charged, res.GasUsed) + s.Require().LessOrEqual(charged, res.GasUsed+1_000_000, + "res.GasUsed must be charged exactly once on the success path") +} diff --git a/x/vm/keeper/call_evm.go b/x/vm/keeper/call_evm.go index 3d8668bff..de9507c6b 100644 --- a/x/vm/keeper/call_evm.go +++ b/x/vm/keeper/call_evm.go @@ -72,10 +72,9 @@ func (k Keeper) CallEVMWithData( AccessList: ethtypes.AccessList{}, } - // Use a cache context so that a reverting EVM call does not corrupt the - // parent gas meter. On success we commit the cache and charge the actual - // gas used; on revert we discard the cache and leave the parent meter - // untouched (matching DerivedEVMCallWithData semantics). + // Use a cache context so that a reverting EVM call does not commit its state + // changes. The cache shares the parent gas meter, so gas is metered against + // the caller either way; only the store writes are discarded on failure. tmpCtx, commitState := ctx.CacheContext() res, err := k.ApplyMessage(tmpCtx, msg, nil, commit, true) if err != nil { @@ -83,6 +82,13 @@ func (k Keeper) CallEVMWithData( } if res.Failed() { + // A failed execution still burned real EVM work, so charge it to the parent + // Cosmos gas meter exactly like a successful one. Skipping this made a + // revert (or a deliberate out-of-gas) a free way to consume block compute. + // msg.GasLimit above is the hardcoded config.DefaultGasCap, so res.GasUsed + // — which equals the gas limit on out-of-gas — is bounded by that cap and + // cannot be inflated by the caller. + ctx.GasMeter().ConsumeGas(res.GasUsed, "apply evm message (failed)") return res, errorsmod.Wrap(types.ErrVMExecution, res.VmError) } @@ -191,7 +197,15 @@ func (k Keeper) DerivedEVMCallWithData( gasCap = gasRes.Gas } if gasLimit != nil { - gasCap = gasLimit.Uint64() + // Clamp the caller-supplied limit to DefaultGasCap. The failure path below + // charges res.GasUsed to the parent Cosmos gas meter, and on out-of-gas + // res.GasUsed equals this cap — so an unclamped caller value would let the + // caller choose how much gas the enclosing Cosmos tx is forced to consume, + // and panic it with OutOfGas. DefaultGasCap is already the ceiling on every + // other path into this function: the estimator above runs with + // GasCap: config.DefaultGasCap, and the remaining branch uses the cap + // directly. The clamp therefore only ever narrows an outlier. + gasCap = min(gasLimit.Uint64(), config.DefaultGasCap) } msg := core.Message{ @@ -323,6 +337,12 @@ func (k Keeper) DerivedEVMCallWithData( } if res.Failed() { + // A failed execution still burned real EVM work, so charge it to the parent + // Cosmos gas meter exactly like a successful one. Skipping this made a + // revert (or a deliberate out-of-gas) a free way to consume block compute, + // and left the `gasless` flag zeroing only the reported event attribute. + // res.GasUsed is bounded by gasCap, clamped to config.DefaultGasCap above. + ctx.GasMeter().ConsumeGas(res.GasUsed, "apply evm message (failed)") return res, errorsmod.Wrapf(types.ErrVMExecution, "%s: ret 0x%x", res.VmError, res.Ret) } From 913bc7ccc4472754de8ac7535fa685f60405b106 Mon Sep 17 00:00:00 2001 From: Nilesh Gupta Date: Wed, 26 Aug 2026 19:51:37 +0530 Subject: [PATCH 05/15] fix: roll back the bank escrow when ConvertCoinNativeERC20's EVM call fails (#45) The escrow was applied to the parent context before CallEVM, so a VM failure left the sender's coins stranded on the erc20 module account - permanently on the IBC refund path, which swallows the error. --- CHANGELOG.md | 7 + .../integration/x/erc20/test_ibc_callback.go | 127 ++++++++++++++++++ x/erc20/keeper/ibc_callbacks.go | 19 ++- x/erc20/keeper/msg_server.go | 43 +++++- 4 files changed, 190 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fa9bc933..0be370dd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,13 @@ ### BUG FIXES +- Make `ConvertCoinNativeERC20` atomic. It escrowed the sender's coins onto the erc20 module + account before calling the EVM, and `CallEVMWithData` caches only the EVM execution, so a VM + failure discarded the EVM cache while leaving the bank escrow committed. On the IBC error-ACK / + timeout refund path, which deliberately swallows a conversion error so a failed re-wrap cannot + undo the refund, that stranded the sender's coins on the module account permanently - contradicting + the `OnAcknowledgementPacket` / `OnTimeoutPacket` comments promising the user keeps the bank token. + The escrow, the EVM transfer and the burn now all run on a branched context and commit together. - Run the gov precompile's `getTallyResult` against a throwaway cache. The SDK's `TallyResult` query delegates to `Keeper.Tally`, which deletes every vote it counts; that is safe on the SDK's own query paths because a gRPC query runs against a context that is never committed, but precompiles diff --git a/tests/integration/x/erc20/test_ibc_callback.go b/tests/integration/x/erc20/test_ibc_callback.go index 0086d4b78..7af9916a2 100644 --- a/tests/integration/x/erc20/test_ibc_callback.go +++ b/tests/integration/x/erc20/test_ibc_callback.go @@ -769,3 +769,130 @@ func (s *KeeperTestSuite) TestOnTimeoutPacket() { }) } } + +// setupUnbackedPairAndFundSender registers a native-ERC20 pair and funds the +// sender with the paired bank denom, while deliberately leaving the erc20 module +// account with NO token balance to unescrow from. +// +// That produces the exact failure shape F-2026-18819 needs. Walking +// convertCoinNativeERC20 in order: +// +// balanceOf(receiver) -> succeeds (a plain view) +// SendCoinsFromAccountToModule -> succeeds, coins now on the module +// CallEVM "transfer" -> REVERTS, module holds nothing to send +// +// The failure therefore lands strictly after the bank mutation, which is the +// only ordering that can strand the sender's coins. Pausing the token does not +// work here: it fails the balanceOf read instead, before any escrow happens, and +// the test would pass vacuously. +// +// It returns a context fetched *after* the deployment: the suite's contract +// deployment only becomes visible to EVM reads on a context taken afterwards, so +// reusing an earlier one makes balanceOf return nil and the conversion fail +// before it ever escrows. +func (s *KeeperTestSuite) setupUnbackedPairAndFundSender( + sender sdk.AccAddress, fund math.Int, +) (sdk.Context, types.TokenPair) { + contractAddr, err := s.setupRegisterERC20Pair(contractMinterBurner) + s.Require().NoError(err, "failed to register pair") + + ctx := s.network.GetContext() + + id := s.network.App.GetErc20Keeper().GetTokenPairID(ctx, contractAddr.String()) + pair, found := s.network.App.GetErc20Keeper().GetTokenPair(ctx, id) + s.Require().True(found) + + s.Require().NoError(testutil.FundAccount( + ctx, s.network.App.GetBankKeeper(), sender, + sdk.NewCoins(sdk.NewCoin(pair.Denom, fund)), + )) + + // Sanity: the module must hold no tokens, or the transfer would succeed and + // the test would prove nothing. + moduleTokens := s.network.App.GetErc20Keeper().BalanceOf( + ctx, contracts.ERC20MinterBurnerDecimalsContract.ABI, contractAddr, types.ModuleAddress, + ) + s.Require().NotNil(moduleTokens, "balanceOf must work - the failure has to come from transfer") + s.Require().Zero(moduleTokens.Sign(), "module must have nothing to unescrow") + + return ctx, pair +} + +// TestConvertCoinNativeERC20RollsBackEscrowOnEVMFailure is the direct guard for +// F-2026-18819. +// +// ConvertCoinNativeERC20 escrows the sender's coins onto the erc20 module +// account and only then calls the EVM. CallEVM caches solely the EVM execution, +// so before the fix a VM failure discarded the EVM cache but left the bank +// escrow committed - the sender's coins ended up stranded on a module account +// with no user-facing way to reclaim them. +// +// Asserting on the error alone would not catch that. The assertions that matter +// are the balances. +func (s *KeeperTestSuite) TestConvertCoinNativeERC20RollsBackEscrowOnEVMFailure() { + s.mintFeeCollector = true + defer func() { s.mintFeeCollector = false }() + s.SetupTest() + + bankKeeper := s.network.App.GetBankKeeper() + moduleAcc := authtypes.NewModuleAddress(types.ModuleName) + + senderPk := secp256k1.GenPrivKey() + sender := sdk.AccAddress(senderPk.PubKey().Address()) + + ctx, pair := s.setupUnbackedPairAndFundSender(sender, math.NewInt(100)) + + senderBefore := bankKeeper.GetBalance(ctx, sender, pair.Denom) + moduleBefore := bankKeeper.GetBalance(ctx, moduleAcc, pair.Denom) + supplyBefore := bankKeeper.GetSupply(ctx, pair.Denom) + s.Require().Equal(math.NewInt(100), senderBefore.Amount) + + err := s.network.App.GetErc20Keeper().ConvertCoinNativeERC20( + ctx, pair, math.NewInt(10), common.BytesToAddress(sender), sender, + ) + s.Require().Error(err, "a paused token must fail the unescrow transfer") + + s.Require().Equal(senderBefore, bankKeeper.GetBalance(ctx, sender, pair.Denom), + "sender must keep every coin - the escrow has to roll back with the EVM failure") + s.Require().Equal(moduleBefore, bankKeeper.GetBalance(ctx, moduleAcc, pair.Denom), + "no coins may be stranded on the erc20 module account") + s.Require().Equal(supplyBefore, bankKeeper.GetSupply(ctx, pair.Denom), + "a failed conversion must not burn supply") +} + +// TestConvertCoinToERC20FromPacketLeavesRefundWithSender covers the same defect +// one layer up, on the path that actually made it permanent. +// +// The IBC error-ACK / timeout wrapper deliberately swallows a conversion failure +// so a failed re-wrap cannot undo the refund itself, and the block commits. The +// doc comments on OnAcknowledgementPacket / OnTimeoutPacket promise that the +// user "receives the corresponding bank token from the TokenPair instead" - this +// pins that promise down. +func (s *KeeperTestSuite) TestConvertCoinToERC20FromPacketLeavesRefundWithSender() { + s.mintFeeCollector = true + defer func() { s.mintFeeCollector = false }() + s.SetupTest() + + bankKeeper := s.network.App.GetBankKeeper() + moduleAcc := authtypes.NewModuleAddress(types.ModuleName) + + senderPk := secp256k1.GenPrivKey() + sender := sdk.AccAddress(senderPk.PubKey().Address()) + + ctx, pair := s.setupUnbackedPairAndFundSender(sender, math.NewInt(100)) + + senderBefore := bankKeeper.GetBalance(ctx, sender, pair.Denom) + moduleBefore := bankKeeper.GetBalance(ctx, moduleAcc, pair.Denom) + + data := transfertypes.NewFungibleTokenPacketData(pair.Denom, "10", sender.String(), "", "") + + // The wrapper swallows the conversion error by design; the IBC lifecycle + // must continue. + err := s.network.App.GetErc20Keeper().ConvertCoinToERC20FromPacket(ctx, data) + s.Require().NoError(err, "the refund path must not surface the conversion failure") + + s.Require().Equal(senderBefore, bankKeeper.GetBalance(ctx, sender, pair.Denom), + "the refunded coins must still be with the sender, who can retry the conversion") + s.Require().Equal(moduleBefore, bankKeeper.GetBalance(ctx, moduleAcc, pair.Denom), + "the refund must not be stranded on the erc20 module account") +} diff --git a/x/erc20/keeper/ibc_callbacks.go b/x/erc20/keeper/ibc_callbacks.go index 4848dc10b..6270ed5ae 100644 --- a/x/erc20/keeper/ibc_callbacks.go +++ b/x/erc20/keeper/ibc_callbacks.go @@ -159,8 +159,12 @@ func (k Keeper) OnRecvPacket( // is refunded and then the IBC Coins are converted to ERC20. // If the ERC20 conversion fails for whatever reason, such as an attempt to call // a self-destructed ERC20 contract or an invalid function, OnAcknowledgementPacket -// still succeeds, but the user receives the corresponding bank token from the -// TokenPair instead. A user may then manually re-attempt the conversion. +// still succeeds and the user keeps the corresponding bank token from the +// TokenPair, so they may manually re-attempt the conversion. That holds only +// because ConvertCoinNativeERC20 is atomic: it escrows the coins onto the module +// account before touching the EVM, so without the rollback a VM failure would +// leave the refund stranded on the module account rather than with the sender +// (F-2026-18819). func (k Keeper) OnAcknowledgementPacket( ctx sdk.Context, _ channeltypes.Packet, data transfertypes.FungibleTokenPacketData, @@ -181,8 +185,9 @@ func (k Keeper) OnAcknowledgementPacket( // since the original packet sent was never received and has been timed out. // If the ERC20 conversion fails for whatever reason, such as an attempt to call // a self-destructed ERC20 contract or an invalid function, OnTimeoutPacket still -// succeeds, but the user receives the corresponding bank token from the TokenPair -// instead. A user may then manually re-attempt the conversion. +// succeeds and the user keeps the corresponding bank token from the TokenPair, +// so they may manually re-attempt the conversion. As with OnAcknowledgementPacket +// that depends on ConvertCoinNativeERC20 being atomic (F-2026-18819). func (k Keeper) OnTimeoutPacket(ctx sdk.Context, _ channeltypes.Packet, data transfertypes.FungibleTokenPacketData) error { return k.ConvertCoinToERC20FromPacket(ctx, data) } @@ -235,6 +240,12 @@ func (k Keeper) ConvertCoinToERC20FromPacket(ctx sdk.Context, data transfertypes // Convert from Coin to ERC20 if err := k.ConvertCoinNativeERC20(ctx, pair, coin.Amount, common.BytesToAddress(sender), sender); err != nil { + // Swallowing the error is deliberate: the refund itself already + // succeeded and must not be undone by a failure to re-wrap it into + // the ERC20 representation. This is only safe because + // ConvertCoinNativeERC20 rolls back on failure, so the sender still + // holds the bank coins here (F-2026-18819). + // // We want to record only the failed attempt to reconvert the coins during IBC. defer func() { telemetry.IncrCounter(1, types.ModuleName, "ibc", "error", "total") diff --git a/x/erc20/keeper/msg_server.go b/x/erc20/keeper/msg_server.go index 51c8621e4..65f04aba4 100644 --- a/x/erc20/keeper/msg_server.go +++ b/x/erc20/keeper/msg_server.go @@ -228,13 +228,52 @@ func (k Keeper) ConvertCoin( } // ConvertCoinNativeERC20 handles the coin conversion for a native ERC20 token -// pair: +// pair. It runs the conversion atomically: the bank escrow, the EVM transfer and +// the burn either all commit or none of them do. +// +// The atomicity is load-bearing, not cosmetic. The body escrows coins onto the +// module account before calling the EVM, and CallEVM caches only the EVM +// execution — so on a VM failure the discarded EVM cache used to leave the bank +// escrow applied, stranding the sender's coins on the erc20 module account with +// no user-facing way to reclaim them. The IBC refund path (see +// ConvertCoinToERC20FromPacket) swallows this error and commits the block, which +// made that stranding permanent (F-2026-18819). +func (k Keeper) ConvertCoinNativeERC20( + ctx sdk.Context, + pair types.TokenPair, + amount math.Int, + receiver common.Address, + sender sdk.AccAddress, +) error { + // CacheContext branches the multistore and installs a fresh EventManager; + // writeCache replays those events onto the parent and commits the branch. + // Nothing is written unless the whole conversion succeeded. + // + // Note the gas meter is *shared* with the parent, so work done on a + // discarded branch is still charged. That is intended — a failed conversion + // should not be free. + cacheCtx, writeCache := ctx.CacheContext() + + if err := k.convertCoinNativeERC20(cacheCtx, pair, amount, receiver, sender); err != nil { + // Branch discarded: the escrow, the EVM state and any events go with it. + return err + } + + writeCache() + + return nil +} + +// convertCoinNativeERC20 is the non-atomic body of ConvertCoinNativeERC20: // - escrow Coins on module account // - unescrow Tokens that have been previously escrowed with ConvertERC20 and send to receiver // - burn escrowed Coins // - check if token balance increased by amount // - check for unexpected `Approval` event in logs -func (k Keeper) ConvertCoinNativeERC20( +// +// It mutates ctx directly and must only be called on a branched context — see +// ConvertCoinNativeERC20. +func (k Keeper) convertCoinNativeERC20( ctx sdk.Context, pair types.TokenPair, amount math.Int, From 2e5db83460060937e2927497e225715d32757ba3 Mon Sep 17 00:00:00 2001 From: Nilesh Gupta Date: Wed, 26 Aug 2026 19:53:26 +0530 Subject: [PATCH 06/15] fix: resolve precompile selector before RunNativeAction (F-2026-18786) (#48) Stateful precompiles ran the full native-action preamble - including a state DB commit that replays the caller's dirty set - before SetupABI resolved the selector, and RequiredGas returns 0 for an unresolvable one so none of it was charged. --- CHANGELOG.md | 9 ++ precompiles/bank/bank.go | 9 ++ precompiles/bank/bank_selector_test.go | 92 ++++++++++++++++++ precompiles/common/precompile.go | 39 +++++--- precompiles/distribution/distribution.go | 9 ++ .../distribution_selector_test.go | 92 ++++++++++++++++++ precompiles/erc20/erc20.go | 9 ++ precompiles/erc20/erc20_selector_test.go | 94 +++++++++++++++++++ precompiles/gov/gov.go | 9 ++ precompiles/gov/gov_selector_test.go | 92 ++++++++++++++++++ precompiles/ics20/ics20.go | 9 ++ precompiles/ics20/ics20_selector_test.go | 92 ++++++++++++++++++ precompiles/slashing/slashing.go | 9 ++ .../slashing/slashing_selector_test.go | 92 ++++++++++++++++++ precompiles/staking/staking.go | 9 ++ precompiles/staking/staking_selector_test.go | 92 ++++++++++++++++++ precompiles/werc20/werc20.go | 9 ++ precompiles/werc20/werc20_selector_test.go | 91 ++++++++++++++++++ 18 files changed, 846 insertions(+), 11 deletions(-) create mode 100644 precompiles/bank/bank_selector_test.go create mode 100644 precompiles/distribution/distribution_selector_test.go create mode 100644 precompiles/erc20/erc20_selector_test.go create mode 100644 precompiles/gov/gov_selector_test.go create mode 100644 precompiles/ics20/ics20_selector_test.go create mode 100644 precompiles/slashing/slashing_selector_test.go create mode 100644 precompiles/staking/staking_selector_test.go create mode 100644 precompiles/werc20/werc20_selector_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 0be370dd6..6407d42a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,15 @@ ### BUG FIXES +- Resolve the ABI method at the top of every stateful precompile's `Run`, before `RunNativeAction` + is entered. The native-action preamble takes a cache context, snapshots the multi-store and + commits the state DB cache - replaying the caller's entire dirty account and storage set - and + only then does `SetupABI` get around to resolving the selector and failing. `RequiredGas` returns + 0 for an unresolvable selector, so none of that work was charged: a contract could dirty a large + storage set and then hand `bank`, `gov`, `staking`, `distribution`, `ics20`, `slashing`, `erc20` + or `werc20` four random bytes to have the whole set replayed for free, up to 20 times per + transaction. The check goes through the new `cmn.ResolveMethod`, which `SetupABI` now also uses, + so the fallback/receive special cases WERC20 depends on keep working and the revert is unchanged. - Make `ConvertCoinNativeERC20` atomic. It escrowed the sender's coins onto the erc20 module account before calling the EVM, and `CallEVMWithData` caches only the EVM execution, so a VM failure discarded the EVM cache while leaving the bank escrow committed. On the IBC error-ACK / diff --git a/precompiles/bank/bank.go b/precompiles/bank/bank.go index 376eb5357..82781cac6 100644 --- a/precompiles/bank/bank.go +++ b/precompiles/bank/bank.go @@ -107,6 +107,15 @@ func (p Precompile) RequiredGas(input []byte) uint64 { } func (p Precompile) Run(evm *vm.EVM, contract *vm.Contract, readonly bool) ([]byte, error) { + // Resolve the target method before RunNativeAction touches any state: the + // native-action preamble (cache context, multi-store snapshot, state DB + // commit) replays the caller's entire dirty set and is not paid for when the + // selector turns out to be unresolvable, because RequiredGas returns 0 for + // one. Resolving here keeps the revert identical while skipping that work. + if _, err := cmn.ResolveMethod(p.ABI, contract); err != nil { + return cmn.ReturnRevertError(evm, err) + } + return p.RunNativeAction(evm, contract, func(ctx sdk.Context) ([]byte, error) { return p.Execute(ctx, contract, readonly) }) diff --git a/precompiles/bank/bank_selector_test.go b/precompiles/bank/bank_selector_test.go new file mode 100644 index 000000000..95d8a21b0 --- /dev/null +++ b/precompiles/bank/bank_selector_test.go @@ -0,0 +1,92 @@ +package bank_test + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/params" + "github.com/holiman/uint256" + "github.com/stretchr/testify/require" + + "github.com/cosmos/evm/precompiles/bank" + cmn "github.com/cosmos/evm/precompiles/common" +) + +// runPrecompile drives bank.Precompile.Run with a nil StateDB and returns the +// ABI-encoded revert reason. +// +// The early selector guard never looks at evm.StateDB, whereas the first thing +// RunNativeAction does is type-assert on it. A revert reason carrying +// cmn.ErrNotRunInEvm therefore proves the call reached the native-action +// preamble, and any other reason proves it was rejected before it - which is +// what lets these tests tell the two apart without standing up a full app. +func runPrecompile(t *testing.T, input []byte, value *uint256.Int) string { + t.Helper() + + evm := vm.NewEVM(vm.BlockContext{BlockNumber: big.NewInt(1)}, nil, params.TestChainConfig, vm.Config{}) + contract := vm.NewContract(common.Address{}, common.Address{}, value, 100_000, nil) + contract.Input = input + + bz, err := bank.NewPrecompile(nil, nil).Run(evm, contract, false) + require.ErrorIs(t, err, vm.ErrExecutionReverted, "precompile errors are always surfaced as a revert") + + return string(bz) +} + +// TestRunResolvesSelectorBeforeNativeAction covers F-2026-18786: calldata that +// cannot be dispatched used to run the whole native-action preamble - cache +// context, multi-store snapshot and a state DB commit that replays the caller's +// entire dirty set - before SetupABI got around to failing on the selector, and +// RequiredGas returns 0 for an unresolvable selector so none of it was charged. +func TestRunResolvesSelectorBeforeNativeAction(t *testing.T) { + testCases := []struct { + name string + // input is the calldata handed to the precompile. + input []byte + // reachesNativeAction is true when the calldata is dispatchable and so + // must still be handed to RunNativeAction. + reachesNativeAction bool + // expReason, when set, must appear in the revert reason. + expReason string + }{ + { + name: "unknown selector is rejected before the native action", + input: []byte{0xde, 0xad, 0xbe, 0xef}, + expReason: "no method with id", + }, + { + name: "calldata shorter than a selector is rejected before the native action", + input: []byte{0x01, 0x02, 0x03}, + }, + { + name: "empty calldata is rejected before the native action", + input: nil, + }, + { + name: "a valid selector is still dispatched", + input: bank.ABI.Methods[bank.BalancesMethod].ID, + reachesNativeAction: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + reason := runPrecompile(t, tc.input, uint256.NewInt(0)) + + if tc.reachesNativeAction { + require.Contains(t, reason, cmn.ErrNotRunInEvm, + "a valid selector must still reach RunNativeAction") + return + } + + require.NotContains(t, reason, cmn.ErrNotRunInEvm, + "undispatchable calldata must not reach the native-action preamble (F-2026-18786)") + + if tc.expReason != "" { + require.Contains(t, reason, tc.expReason) + } + }) + } +} diff --git a/precompiles/common/precompile.go b/precompiles/common/precompile.go index 1554fe1b3..81a0d061f 100644 --- a/precompiles/common/precompile.go +++ b/precompiles/common/precompile.go @@ -125,14 +125,19 @@ func (p Precompile) runNativeAction(evm *vm.EVM, contract *vm.Contract, action N return bz, nil } -// SetupABI runs the initial setup required to run a transaction or a query. -// It returns the ABI method, initial gas and calling arguments. -func SetupABI( - api abi.ABI, - contract *vm.Contract, - readOnly bool, - isTransaction func(name *abi.Method) bool, -) (method *abi.Method, args []interface{}, err error) { +// ResolveMethod resolves the ABI method targeted by the contract calldata. +// +// It is the selector-resolution half of SetupABI, exported so that a concrete +// precompile can reject calldata it cannot dispatch at the very top of its Run, +// *before* RunNativeAction sets up the native context, snapshots the multi-store +// and commits the state DB cache. That preamble replays the caller's whole dirty +// set and is not charged for an unresolvable selector, since RequiredGas also +// returns 0 for one. +// +// SetupABI delegates to this function so the early check and the in-action check +// can never disagree - notably on the fallback/receive special cases, which the +// werc20 precompile relies on for empty, short and unknown calldata. +func ResolveMethod(api abi.ABI, contract *vm.Contract) (method *abi.Method, err error) { // NOTE: This is a special case where the calling transaction does not specify a function name. // In this case we default to a `fallback` or `receive` function on the contract. @@ -144,17 +149,29 @@ func SetupABI( switch { // Case 1: Calldata is empty case isEmptyCallData: - method, err = emptyCallData(api, contract) + return emptyCallData(api, contract) // Case 2: calldata is non-empty but less than 4 bytes needed for a method case isShortCallData: - method, err = methodIDCallData(api) + return methodIDCallData(api) // Case 3: calldata is non-empty and contains the minimum 4 bytes needed for a method case isStandardCallData: - method, err = standardCallData(api, contract) + return standardCallData(api, contract) } + return nil, vm.ErrExecutionReverted +} + +// SetupABI runs the initial setup required to run a transaction or a query. +// It returns the ABI method, initial gas and calling arguments. +func SetupABI( + api abi.ABI, + contract *vm.Contract, + readOnly bool, + isTransaction func(name *abi.Method) bool, +) (method *abi.Method, args []interface{}, err error) { + method, err = ResolveMethod(api, contract) if err != nil { return nil, nil, err } diff --git a/precompiles/distribution/distribution.go b/precompiles/distribution/distribution.go index f363fa7c9..d6c0faf17 100644 --- a/precompiles/distribution/distribution.go +++ b/precompiles/distribution/distribution.go @@ -95,6 +95,15 @@ func (p Precompile) RequiredGas(input []byte) uint64 { } func (p Precompile) Run(evm *vm.EVM, contract *vm.Contract, readonly bool) ([]byte, error) { + // Resolve the target method before RunNativeAction touches any state: the + // native-action preamble (cache context, multi-store snapshot, state DB + // commit) replays the caller's entire dirty set and is not paid for when the + // selector turns out to be unresolvable, because RequiredGas returns 0 for + // one. Resolving here keeps the revert identical while skipping that work. + if _, err := cmn.ResolveMethod(p.ABI, contract); err != nil { + return cmn.ReturnRevertError(evm, err) + } + return p.RunNativeAction(evm, contract, func(ctx sdk.Context) ([]byte, error) { return p.Execute(ctx, evm.StateDB, contract, readonly) }) diff --git a/precompiles/distribution/distribution_selector_test.go b/precompiles/distribution/distribution_selector_test.go new file mode 100644 index 000000000..9383c75ea --- /dev/null +++ b/precompiles/distribution/distribution_selector_test.go @@ -0,0 +1,92 @@ +package distribution_test + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/params" + "github.com/holiman/uint256" + "github.com/stretchr/testify/require" + + cmn "github.com/cosmos/evm/precompiles/common" + "github.com/cosmos/evm/precompiles/distribution" +) + +// runPrecompile drives distribution.Precompile.Run with a nil StateDB and returns the +// ABI-encoded revert reason. +// +// The early selector guard never looks at evm.StateDB, whereas the first thing +// RunNativeAction does is type-assert on it. A revert reason carrying +// cmn.ErrNotRunInEvm therefore proves the call reached the native-action +// preamble, and any other reason proves it was rejected before it - which is +// what lets these tests tell the two apart without standing up a full app. +func runPrecompile(t *testing.T, input []byte, value *uint256.Int) string { + t.Helper() + + evm := vm.NewEVM(vm.BlockContext{BlockNumber: big.NewInt(1)}, nil, params.TestChainConfig, vm.Config{}) + contract := vm.NewContract(common.Address{}, common.Address{}, value, 100_000, nil) + contract.Input = input + + bz, err := distribution.NewPrecompile(nil, nil, nil, nil, nil, nil).Run(evm, contract, false) + require.ErrorIs(t, err, vm.ErrExecutionReverted, "precompile errors are always surfaced as a revert") + + return string(bz) +} + +// TestRunResolvesSelectorBeforeNativeAction covers F-2026-18786: calldata that +// cannot be dispatched used to run the whole native-action preamble - cache +// context, multi-store snapshot and a state DB commit that replays the caller's +// entire dirty set - before SetupABI got around to failing on the selector, and +// RequiredGas returns 0 for an unresolvable selector so none of it was charged. +func TestRunResolvesSelectorBeforeNativeAction(t *testing.T) { + testCases := []struct { + name string + // input is the calldata handed to the precompile. + input []byte + // reachesNativeAction is true when the calldata is dispatchable and so + // must still be handed to RunNativeAction. + reachesNativeAction bool + // expReason, when set, must appear in the revert reason. + expReason string + }{ + { + name: "unknown selector is rejected before the native action", + input: []byte{0xde, 0xad, 0xbe, 0xef}, + expReason: "no method with id", + }, + { + name: "calldata shorter than a selector is rejected before the native action", + input: []byte{0x01, 0x02, 0x03}, + }, + { + name: "empty calldata is rejected before the native action", + input: nil, + }, + { + name: "a valid selector is still dispatched", + input: distribution.ABI.Methods[distribution.ValidatorCommissionMethod].ID, + reachesNativeAction: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + reason := runPrecompile(t, tc.input, uint256.NewInt(0)) + + if tc.reachesNativeAction { + require.Contains(t, reason, cmn.ErrNotRunInEvm, + "a valid selector must still reach RunNativeAction") + return + } + + require.NotContains(t, reason, cmn.ErrNotRunInEvm, + "undispatchable calldata must not reach the native-action preamble (F-2026-18786)") + + if tc.expReason != "" { + require.Contains(t, reason, tc.expReason) + } + }) + } +} diff --git a/precompiles/erc20/erc20.go b/precompiles/erc20/erc20.go index 12121b7c4..3d90afcaf 100644 --- a/precompiles/erc20/erc20.go +++ b/precompiles/erc20/erc20.go @@ -140,6 +140,15 @@ func (p Precompile) RequiredGas(input []byte) uint64 { } func (p Precompile) Run(evm *vm.EVM, contract *vm.Contract, readonly bool) ([]byte, error) { + // Resolve the target method before RunNativeAction touches any state: the + // native-action preamble (cache context, multi-store snapshot, state DB + // commit) replays the caller's entire dirty set and is not paid for when the + // selector turns out to be unresolvable, because RequiredGas returns 0 for + // one. Resolving here keeps the revert identical while skipping that work. + if _, err := cmn.ResolveMethod(p.ABI, contract); err != nil { + return cmn.ReturnRevertError(evm, err) + } + return p.RunNativeAction(evm, contract, func(ctx sdk.Context) ([]byte, error) { return p.Execute(ctx, evm.StateDB, contract, readonly) }) diff --git a/precompiles/erc20/erc20_selector_test.go b/precompiles/erc20/erc20_selector_test.go new file mode 100644 index 000000000..87b546e34 --- /dev/null +++ b/precompiles/erc20/erc20_selector_test.go @@ -0,0 +1,94 @@ +package erc20_test + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/params" + "github.com/holiman/uint256" + "github.com/stretchr/testify/require" + + cmn "github.com/cosmos/evm/precompiles/common" + "github.com/cosmos/evm/precompiles/erc20" + + erc20types "github.com/cosmos/evm/x/erc20/types" +) + +// runPrecompile drives erc20.Precompile.Run with a nil StateDB and returns the +// ABI-encoded revert reason. +// +// The early selector guard never looks at evm.StateDB, whereas the first thing +// RunNativeAction does is type-assert on it. A revert reason carrying +// cmn.ErrNotRunInEvm therefore proves the call reached the native-action +// preamble, and any other reason proves it was rejected before it - which is +// what lets these tests tell the two apart without standing up a full app. +func runPrecompile(t *testing.T, input []byte, value *uint256.Int) string { + t.Helper() + + evm := vm.NewEVM(vm.BlockContext{BlockNumber: big.NewInt(1)}, nil, params.TestChainConfig, vm.Config{}) + contract := vm.NewContract(common.Address{}, common.Address{}, value, 100_000, nil) + contract.Input = input + + bz, err := erc20.NewPrecompile(erc20types.TokenPair{}, nil, nil, nil).Run(evm, contract, false) + require.ErrorIs(t, err, vm.ErrExecutionReverted, "precompile errors are always surfaced as a revert") + + return string(bz) +} + +// TestRunResolvesSelectorBeforeNativeAction covers F-2026-18786: calldata that +// cannot be dispatched used to run the whole native-action preamble - cache +// context, multi-store snapshot and a state DB commit that replays the caller's +// entire dirty set - before SetupABI got around to failing on the selector, and +// RequiredGas returns 0 for an unresolvable selector so none of it was charged. +func TestRunResolvesSelectorBeforeNativeAction(t *testing.T) { + testCases := []struct { + name string + // input is the calldata handed to the precompile. + input []byte + // reachesNativeAction is true when the calldata is dispatchable and so + // must still be handed to RunNativeAction. + reachesNativeAction bool + // expReason, when set, must appear in the revert reason. + expReason string + }{ + { + name: "unknown selector is rejected before the native action", + input: []byte{0xde, 0xad, 0xbe, 0xef}, + expReason: "no method with id", + }, + { + name: "calldata shorter than a selector is rejected before the native action", + input: []byte{0x01, 0x02, 0x03}, + }, + { + name: "empty calldata is rejected before the native action", + input: nil, + }, + { + name: "a valid selector is still dispatched", + input: erc20.ABI.Methods[erc20.NameMethod].ID, + reachesNativeAction: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + reason := runPrecompile(t, tc.input, uint256.NewInt(0)) + + if tc.reachesNativeAction { + require.Contains(t, reason, cmn.ErrNotRunInEvm, + "a valid selector must still reach RunNativeAction") + return + } + + require.NotContains(t, reason, cmn.ErrNotRunInEvm, + "undispatchable calldata must not reach the native-action preamble (F-2026-18786)") + + if tc.expReason != "" { + require.Contains(t, reason, tc.expReason) + } + }) + } +} diff --git a/precompiles/gov/gov.go b/precompiles/gov/gov.go index baf84d22b..c157874e6 100644 --- a/precompiles/gov/gov.go +++ b/precompiles/gov/gov.go @@ -90,6 +90,15 @@ func (p Precompile) RequiredGas(input []byte) uint64 { } func (p Precompile) Run(evm *vm.EVM, contract *vm.Contract, readonly bool) ([]byte, error) { + // Resolve the target method before RunNativeAction touches any state: the + // native-action preamble (cache context, multi-store snapshot, state DB + // commit) replays the caller's entire dirty set and is not paid for when the + // selector turns out to be unresolvable, because RequiredGas returns 0 for + // one. Resolving here keeps the revert identical while skipping that work. + if _, err := cmn.ResolveMethod(p.ABI, contract); err != nil { + return cmn.ReturnRevertError(evm, err) + } + return p.RunNativeAction(evm, contract, func(ctx sdk.Context) ([]byte, error) { return p.Execute(ctx, evm.StateDB, contract, readonly) }) diff --git a/precompiles/gov/gov_selector_test.go b/precompiles/gov/gov_selector_test.go new file mode 100644 index 000000000..70e7effcb --- /dev/null +++ b/precompiles/gov/gov_selector_test.go @@ -0,0 +1,92 @@ +package gov_test + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/params" + "github.com/holiman/uint256" + "github.com/stretchr/testify/require" + + cmn "github.com/cosmos/evm/precompiles/common" + "github.com/cosmos/evm/precompiles/gov" +) + +// runPrecompile drives gov.Precompile.Run with a nil StateDB and returns the +// ABI-encoded revert reason. +// +// The early selector guard never looks at evm.StateDB, whereas the first thing +// RunNativeAction does is type-assert on it. A revert reason carrying +// cmn.ErrNotRunInEvm therefore proves the call reached the native-action +// preamble, and any other reason proves it was rejected before it - which is +// what lets these tests tell the two apart without standing up a full app. +func runPrecompile(t *testing.T, input []byte, value *uint256.Int) string { + t.Helper() + + evm := vm.NewEVM(vm.BlockContext{BlockNumber: big.NewInt(1)}, nil, params.TestChainConfig, vm.Config{}) + contract := vm.NewContract(common.Address{}, common.Address{}, value, 100_000, nil) + contract.Input = input + + bz, err := gov.NewPrecompile(nil, nil, nil, nil, nil).Run(evm, contract, false) + require.ErrorIs(t, err, vm.ErrExecutionReverted, "precompile errors are always surfaced as a revert") + + return string(bz) +} + +// TestRunResolvesSelectorBeforeNativeAction covers F-2026-18786: calldata that +// cannot be dispatched used to run the whole native-action preamble - cache +// context, multi-store snapshot and a state DB commit that replays the caller's +// entire dirty set - before SetupABI got around to failing on the selector, and +// RequiredGas returns 0 for an unresolvable selector so none of it was charged. +func TestRunResolvesSelectorBeforeNativeAction(t *testing.T) { + testCases := []struct { + name string + // input is the calldata handed to the precompile. + input []byte + // reachesNativeAction is true when the calldata is dispatchable and so + // must still be handed to RunNativeAction. + reachesNativeAction bool + // expReason, when set, must appear in the revert reason. + expReason string + }{ + { + name: "unknown selector is rejected before the native action", + input: []byte{0xde, 0xad, 0xbe, 0xef}, + expReason: "no method with id", + }, + { + name: "calldata shorter than a selector is rejected before the native action", + input: []byte{0x01, 0x02, 0x03}, + }, + { + name: "empty calldata is rejected before the native action", + input: nil, + }, + { + name: "a valid selector is still dispatched", + input: gov.ABI.Methods[gov.GetProposalMethod].ID, + reachesNativeAction: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + reason := runPrecompile(t, tc.input, uint256.NewInt(0)) + + if tc.reachesNativeAction { + require.Contains(t, reason, cmn.ErrNotRunInEvm, + "a valid selector must still reach RunNativeAction") + return + } + + require.NotContains(t, reason, cmn.ErrNotRunInEvm, + "undispatchable calldata must not reach the native-action preamble (F-2026-18786)") + + if tc.expReason != "" { + require.Contains(t, reason, tc.expReason) + } + }) + } +} diff --git a/precompiles/ics20/ics20.go b/precompiles/ics20/ics20.go index b184fd674..59ff4f6f4 100644 --- a/precompiles/ics20/ics20.go +++ b/precompiles/ics20/ics20.go @@ -89,6 +89,15 @@ func (p Precompile) RequiredGas(input []byte) uint64 { } func (p Precompile) Run(evm *vm.EVM, contract *vm.Contract, readonly bool) ([]byte, error) { + // Resolve the target method before RunNativeAction touches any state: the + // native-action preamble (cache context, multi-store snapshot, state DB + // commit) replays the caller's entire dirty set and is not paid for when the + // selector turns out to be unresolvable, because RequiredGas returns 0 for + // one. Resolving here keeps the revert identical while skipping that work. + if _, err := cmn.ResolveMethod(p.ABI, contract); err != nil { + return cmn.ReturnRevertError(evm, err) + } + return p.RunNativeAction(evm, contract, func(ctx sdk.Context) ([]byte, error) { return p.Execute(ctx, evm.StateDB, contract, readonly) }) diff --git a/precompiles/ics20/ics20_selector_test.go b/precompiles/ics20/ics20_selector_test.go new file mode 100644 index 000000000..0685671a4 --- /dev/null +++ b/precompiles/ics20/ics20_selector_test.go @@ -0,0 +1,92 @@ +package ics20_test + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/params" + "github.com/holiman/uint256" + "github.com/stretchr/testify/require" + + cmn "github.com/cosmos/evm/precompiles/common" + "github.com/cosmos/evm/precompiles/ics20" +) + +// runPrecompile drives ics20.Precompile.Run with a nil StateDB and returns the +// ABI-encoded revert reason. +// +// The early selector guard never looks at evm.StateDB, whereas the first thing +// RunNativeAction does is type-assert on it. A revert reason carrying +// cmn.ErrNotRunInEvm therefore proves the call reached the native-action +// preamble, and any other reason proves it was rejected before it - which is +// what lets these tests tell the two apart without standing up a full app. +func runPrecompile(t *testing.T, input []byte, value *uint256.Int) string { + t.Helper() + + evm := vm.NewEVM(vm.BlockContext{BlockNumber: big.NewInt(1)}, nil, params.TestChainConfig, vm.Config{}) + contract := vm.NewContract(common.Address{}, common.Address{}, value, 100_000, nil) + contract.Input = input + + bz, err := ics20.NewPrecompile(nil, nil, nil, nil).Run(evm, contract, false) + require.ErrorIs(t, err, vm.ErrExecutionReverted, "precompile errors are always surfaced as a revert") + + return string(bz) +} + +// TestRunResolvesSelectorBeforeNativeAction covers F-2026-18786: calldata that +// cannot be dispatched used to run the whole native-action preamble - cache +// context, multi-store snapshot and a state DB commit that replays the caller's +// entire dirty set - before SetupABI got around to failing on the selector, and +// RequiredGas returns 0 for an unresolvable selector so none of it was charged. +func TestRunResolvesSelectorBeforeNativeAction(t *testing.T) { + testCases := []struct { + name string + // input is the calldata handed to the precompile. + input []byte + // reachesNativeAction is true when the calldata is dispatchable and so + // must still be handed to RunNativeAction. + reachesNativeAction bool + // expReason, when set, must appear in the revert reason. + expReason string + }{ + { + name: "unknown selector is rejected before the native action", + input: []byte{0xde, 0xad, 0xbe, 0xef}, + expReason: "no method with id", + }, + { + name: "calldata shorter than a selector is rejected before the native action", + input: []byte{0x01, 0x02, 0x03}, + }, + { + name: "empty calldata is rejected before the native action", + input: nil, + }, + { + name: "a valid selector is still dispatched", + input: ics20.ABI.Methods[ics20.DenomsMethod].ID, + reachesNativeAction: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + reason := runPrecompile(t, tc.input, uint256.NewInt(0)) + + if tc.reachesNativeAction { + require.Contains(t, reason, cmn.ErrNotRunInEvm, + "a valid selector must still reach RunNativeAction") + return + } + + require.NotContains(t, reason, cmn.ErrNotRunInEvm, + "undispatchable calldata must not reach the native-action preamble (F-2026-18786)") + + if tc.expReason != "" { + require.Contains(t, reason, tc.expReason) + } + }) + } +} diff --git a/precompiles/slashing/slashing.go b/precompiles/slashing/slashing.go index bc99e984e..47d2ad15c 100644 --- a/precompiles/slashing/slashing.go +++ b/precompiles/slashing/slashing.go @@ -90,6 +90,15 @@ func (p Precompile) RequiredGas(input []byte) uint64 { } func (p Precompile) Run(evm *vm.EVM, contract *vm.Contract, readonly bool) ([]byte, error) { + // Resolve the target method before RunNativeAction touches any state: the + // native-action preamble (cache context, multi-store snapshot, state DB + // commit) replays the caller's entire dirty set and is not paid for when the + // selector turns out to be unresolvable, because RequiredGas returns 0 for + // one. Resolving here keeps the revert identical while skipping that work. + if _, err := cmn.ResolveMethod(p.ABI, contract); err != nil { + return cmn.ReturnRevertError(evm, err) + } + return p.RunNativeAction(evm, contract, func(ctx sdk.Context) ([]byte, error) { return p.Execute(ctx, evm.StateDB, contract, readonly) }) diff --git a/precompiles/slashing/slashing_selector_test.go b/precompiles/slashing/slashing_selector_test.go new file mode 100644 index 000000000..4bc530ca9 --- /dev/null +++ b/precompiles/slashing/slashing_selector_test.go @@ -0,0 +1,92 @@ +package slashing_test + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/params" + "github.com/holiman/uint256" + "github.com/stretchr/testify/require" + + cmn "github.com/cosmos/evm/precompiles/common" + "github.com/cosmos/evm/precompiles/slashing" +) + +// runPrecompile drives slashing.Precompile.Run with a nil StateDB and returns the +// ABI-encoded revert reason. +// +// The early selector guard never looks at evm.StateDB, whereas the first thing +// RunNativeAction does is type-assert on it. A revert reason carrying +// cmn.ErrNotRunInEvm therefore proves the call reached the native-action +// preamble, and any other reason proves it was rejected before it - which is +// what lets these tests tell the two apart without standing up a full app. +func runPrecompile(t *testing.T, input []byte, value *uint256.Int) string { + t.Helper() + + evm := vm.NewEVM(vm.BlockContext{BlockNumber: big.NewInt(1)}, nil, params.TestChainConfig, vm.Config{}) + contract := vm.NewContract(common.Address{}, common.Address{}, value, 100_000, nil) + contract.Input = input + + bz, err := slashing.NewPrecompile(nil, nil, nil, nil, nil).Run(evm, contract, false) + require.ErrorIs(t, err, vm.ErrExecutionReverted, "precompile errors are always surfaced as a revert") + + return string(bz) +} + +// TestRunResolvesSelectorBeforeNativeAction covers F-2026-18786: calldata that +// cannot be dispatched used to run the whole native-action preamble - cache +// context, multi-store snapshot and a state DB commit that replays the caller's +// entire dirty set - before SetupABI got around to failing on the selector, and +// RequiredGas returns 0 for an unresolvable selector so none of it was charged. +func TestRunResolvesSelectorBeforeNativeAction(t *testing.T) { + testCases := []struct { + name string + // input is the calldata handed to the precompile. + input []byte + // reachesNativeAction is true when the calldata is dispatchable and so + // must still be handed to RunNativeAction. + reachesNativeAction bool + // expReason, when set, must appear in the revert reason. + expReason string + }{ + { + name: "unknown selector is rejected before the native action", + input: []byte{0xde, 0xad, 0xbe, 0xef}, + expReason: "no method with id", + }, + { + name: "calldata shorter than a selector is rejected before the native action", + input: []byte{0x01, 0x02, 0x03}, + }, + { + name: "empty calldata is rejected before the native action", + input: nil, + }, + { + name: "a valid selector is still dispatched", + input: slashing.ABI.Methods[slashing.GetParamsMethod].ID, + reachesNativeAction: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + reason := runPrecompile(t, tc.input, uint256.NewInt(0)) + + if tc.reachesNativeAction { + require.Contains(t, reason, cmn.ErrNotRunInEvm, + "a valid selector must still reach RunNativeAction") + return + } + + require.NotContains(t, reason, cmn.ErrNotRunInEvm, + "undispatchable calldata must not reach the native-action preamble (F-2026-18786)") + + if tc.expReason != "" { + require.Contains(t, reason, tc.expReason) + } + }) + } +} diff --git a/precompiles/staking/staking.go b/precompiles/staking/staking.go index 9dd71a6a4..649954b0d 100644 --- a/precompiles/staking/staking.go +++ b/precompiles/staking/staking.go @@ -91,6 +91,15 @@ func (p Precompile) RequiredGas(input []byte) uint64 { } func (p Precompile) Run(evm *vm.EVM, contract *vm.Contract, readonly bool) ([]byte, error) { + // Resolve the target method before RunNativeAction touches any state: the + // native-action preamble (cache context, multi-store snapshot, state DB + // commit) replays the caller's entire dirty set and is not paid for when the + // selector turns out to be unresolvable, because RequiredGas returns 0 for + // one. Resolving here keeps the revert identical while skipping that work. + if _, err := cmn.ResolveMethod(p.ABI, contract); err != nil { + return cmn.ReturnRevertError(evm, err) + } + return p.RunNativeAction(evm, contract, func(ctx sdk.Context) ([]byte, error) { return p.Execute(ctx, evm.StateDB, contract, readonly) }) diff --git a/precompiles/staking/staking_selector_test.go b/precompiles/staking/staking_selector_test.go new file mode 100644 index 000000000..9f1f5496f --- /dev/null +++ b/precompiles/staking/staking_selector_test.go @@ -0,0 +1,92 @@ +package staking_test + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/params" + "github.com/holiman/uint256" + "github.com/stretchr/testify/require" + + cmn "github.com/cosmos/evm/precompiles/common" + "github.com/cosmos/evm/precompiles/staking" +) + +// runPrecompile drives staking.Precompile.Run with a nil StateDB and returns the +// ABI-encoded revert reason. +// +// The early selector guard never looks at evm.StateDB, whereas the first thing +// RunNativeAction does is type-assert on it. A revert reason carrying +// cmn.ErrNotRunInEvm therefore proves the call reached the native-action +// preamble, and any other reason proves it was rejected before it - which is +// what lets these tests tell the two apart without standing up a full app. +func runPrecompile(t *testing.T, input []byte, value *uint256.Int) string { + t.Helper() + + evm := vm.NewEVM(vm.BlockContext{BlockNumber: big.NewInt(1)}, nil, params.TestChainConfig, vm.Config{}) + contract := vm.NewContract(common.Address{}, common.Address{}, value, 100_000, nil) + contract.Input = input + + bz, err := staking.NewPrecompile(nil, nil, nil, nil, nil).Run(evm, contract, false) + require.ErrorIs(t, err, vm.ErrExecutionReverted, "precompile errors are always surfaced as a revert") + + return string(bz) +} + +// TestRunResolvesSelectorBeforeNativeAction covers F-2026-18786: calldata that +// cannot be dispatched used to run the whole native-action preamble - cache +// context, multi-store snapshot and a state DB commit that replays the caller's +// entire dirty set - before SetupABI got around to failing on the selector, and +// RequiredGas returns 0 for an unresolvable selector so none of it was charged. +func TestRunResolvesSelectorBeforeNativeAction(t *testing.T) { + testCases := []struct { + name string + // input is the calldata handed to the precompile. + input []byte + // reachesNativeAction is true when the calldata is dispatchable and so + // must still be handed to RunNativeAction. + reachesNativeAction bool + // expReason, when set, must appear in the revert reason. + expReason string + }{ + { + name: "unknown selector is rejected before the native action", + input: []byte{0xde, 0xad, 0xbe, 0xef}, + expReason: "no method with id", + }, + { + name: "calldata shorter than a selector is rejected before the native action", + input: []byte{0x01, 0x02, 0x03}, + }, + { + name: "empty calldata is rejected before the native action", + input: nil, + }, + { + name: "a valid selector is still dispatched", + input: staking.ABI.Methods[staking.DelegationMethod].ID, + reachesNativeAction: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + reason := runPrecompile(t, tc.input, uint256.NewInt(0)) + + if tc.reachesNativeAction { + require.Contains(t, reason, cmn.ErrNotRunInEvm, + "a valid selector must still reach RunNativeAction") + return + } + + require.NotContains(t, reason, cmn.ErrNotRunInEvm, + "undispatchable calldata must not reach the native-action preamble (F-2026-18786)") + + if tc.expReason != "" { + require.Contains(t, reason, tc.expReason) + } + }) + } +} diff --git a/precompiles/werc20/werc20.go b/precompiles/werc20/werc20.go index b9e67623e..616788fcf 100644 --- a/precompiles/werc20/werc20.go +++ b/precompiles/werc20/werc20.go @@ -95,6 +95,15 @@ func (p Precompile) RequiredGas(input []byte) uint64 { } func (p Precompile) Run(evm *vm.EVM, contract *vm.Contract, readonly bool) ([]byte, error) { + // Resolve the target method before RunNativeAction touches any state: the + // native-action preamble (cache context, multi-store snapshot, state DB + // commit) replays the caller's entire dirty set and is not paid for when the + // selector turns out to be unresolvable, because RequiredGas returns 0 for + // one. Resolving here keeps the revert identical while skipping that work. + if _, err := cmn.ResolveMethod(p.ABI, contract); err != nil { + return cmn.ReturnRevertError(evm, err) + } + return p.RunNativeAction(evm, contract, func(ctx sdk.Context) ([]byte, error) { return p.Execute(ctx, evm.StateDB, contract, readonly) }) diff --git a/precompiles/werc20/werc20_selector_test.go b/precompiles/werc20/werc20_selector_test.go new file mode 100644 index 000000000..51fa8a0f9 --- /dev/null +++ b/precompiles/werc20/werc20_selector_test.go @@ -0,0 +1,91 @@ +package werc20_test + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/params" + "github.com/holiman/uint256" + "github.com/stretchr/testify/require" + + cmn "github.com/cosmos/evm/precompiles/common" + "github.com/cosmos/evm/precompiles/werc20" + erc20types "github.com/cosmos/evm/x/erc20/types" +) + +// runPrecompile drives werc20.Precompile.Run with a nil StateDB and returns the +// ABI-encoded revert reason. +// +// The early selector guard never looks at evm.StateDB, whereas the first thing +// RunNativeAction does is type-assert on it. A revert reason carrying +// cmn.ErrNotRunInEvm therefore proves the call reached the native-action +// preamble, and any other reason proves it was rejected before it - which is +// what lets these tests tell the two apart without standing up a full app. +func runPrecompile(t *testing.T, input []byte, value *uint256.Int) string { + t.Helper() + + evm := vm.NewEVM(vm.BlockContext{BlockNumber: big.NewInt(1)}, nil, params.TestChainConfig, vm.Config{}) + contract := vm.NewContract(common.Address{}, common.Address{}, value, 100_000, nil) + contract.Input = input + + bz, err := werc20.NewPrecompile(erc20types.TokenPair{}, nil, nil, nil).Run(evm, contract, false) + require.ErrorIs(t, err, vm.ErrExecutionReverted, "precompile errors are always surfaced as a revert") + + return string(bz) +} + +// TestRunPreservesFallbackAndReceive is the regression guard for F-2026-18786. +// +// WERC20 is the one affected precompile whose ABI declares a `fallback` and a +// `receive`, and it leans on all three of SetupABI's special cases: empty +// calldata, calldata shorter than a selector, and an unknown selector all +// resolve to fallback/receive and behave like `deposit`. A naive +// "len(input) < 4 || MethodById fails -> revert" guard at the top of Run would +// have broken every one of them, so the guard resolves the method through +// cmn.ResolveMethod - the same helper SetupABI itself uses - instead. +func TestRunPreservesFallbackAndReceive(t *testing.T) { + testCases := []struct { + name string + input []byte + value *uint256.Int + }{ + { + name: "empty calldata with value resolves to receive", + input: nil, + value: uint256.NewInt(1), + }, + { + name: "empty calldata without value resolves to fallback", + input: nil, + value: uint256.NewInt(0), + }, + { + name: "calldata shorter than a selector resolves to fallback", + input: []byte{0x01, 0x02, 0x03}, + value: uint256.NewInt(1), + }, + { + name: "unknown selector resolves to fallback", + input: []byte("nonExistingMethod"), + value: uint256.NewInt(1), + }, + { + name: "a valid selector is still dispatched", + input: werc20.ABI.Methods[werc20.DepositMethod].ID, + value: uint256.NewInt(1), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + reason := runPrecompile(t, tc.input, tc.value) + + require.Contains(t, reason, cmn.ErrNotRunInEvm, + "WERC20 dispatches this calldata via fallback/receive, so it must still reach RunNativeAction") + require.NotContains(t, reason, "no method with id", + "the early selector guard must not reject calldata that fallback/receive would have handled") + }) + } +} From 0076392163cd39edd8d2e82f0d3aeb1bc17c7a91 Mon Sep 17 00:00:00 2001 From: Nilesh Gupta Date: Wed, 26 Aug 2026 19:55:50 +0530 Subject: [PATCH 07/15] fix: honour the gasCap argument in CallEVMWithData (#46) The parameter was accepted and ignored, so callers sandboxing EVM work got a 25M gas message regardless. Also runs the timeout callback on cachedCtx, as the ack path already does. --- CHANGELOG.md | 6 ++ .../integration/x/vm/test_call_evm_gascap.go | 85 +++++++++++++++++++ x/ibc/callbacks/keeper/keeper.go | 8 +- x/vm/keeper/call_evm.go | 26 +++++- 4 files changed, 119 insertions(+), 6 deletions(-) create mode 100644 tests/integration/x/vm/test_call_evm_gascap.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 6407d42a9..842bb48e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,12 @@ ### BUG FIXES +- Honour the `gasCap` argument in `CallEVMWithData`. The parameter was accepted and then ignored - + `GasLimit` was hardcoded to `config.DefaultGasCap` - so callers passing a cap to sandbox EVM work + (the IBC callback keeper passes its remaining Cosmos gas) got a message that could burn up to 25M + gas regardless. `DefaultGasCap` remains the ceiling, so a caller can only narrow the limit. This + matches `DerivedEVMCallWithData` and the shape upstream cosmos/evm settled on. Also routes the + timeout callback's EVM call through `cachedCtx`, as the acknowledgement path already does. - Resolve the ABI method at the top of every stateful precompile's `Run`, before `RunNativeAction` is entered. The native-action preamble takes a cache context, snapshots the multi-store and commits the state DB cache - replaying the caller's entire dirty account and storage set - and diff --git a/tests/integration/x/vm/test_call_evm_gascap.go b/tests/integration/x/vm/test_call_evm_gascap.go new file mode 100644 index 000000000..16e9c9358 --- /dev/null +++ b/tests/integration/x/vm/test_call_evm_gascap.go @@ -0,0 +1,85 @@ +package vm + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + + "github.com/cosmos/evm/contracts" + "github.com/cosmos/evm/server/config" + testconstants "github.com/cosmos/evm/testutil/constants" + utiltx "github.com/cosmos/evm/testutil/tx" + "github.com/cosmos/evm/x/erc20/types" +) + +// TestCallEVMWithDataHonoursGasCap is the guard for F-2026-18818. +// +// CallEVMWithData accepted a gasCap and then hardcoded GasLimit to +// config.DefaultGasCap, so the parameter did nothing. Callers passing one - the +// IBC callback keeper passes its remaining Cosmos gas, intending to sandbox the +// EVM work - got an EVM message that could burn up to 25M gas regardless. +// +// The assertion that catches this is the middle case: a cap below what the call +// costs has to actually stop the call. Asserting only that a large cap succeeds +// would pass just as happily against the ignored parameter. +func (s *KeeperTestSuite) TestCallEVMWithDataHonoursGasCap() { + erc20 := contracts.ERC20MinterBurnerDecimalsContract.ABI + contractAddr := common.HexToAddress(testconstants.WEVMOSContractMainnet) + + data, err := erc20.Pack("balanceOf", utiltx.GenerateAddress()) + s.Require().NoError(err) + + callWithCap := func(gasCap *big.Int) (uint64, error) { + s.SetupTest() + res, err := s.Network.App.GetEVMKeeper().CallEVMWithData( + s.Network.GetContext(), types.ModuleAddress, &contractAddr, data, false, gasCap, + ) + if res == nil { + return 0, err + } + return res.GasUsed, err + } + + // Baseline: no cap. Establishes what the call actually costs, so the + // too-small case below is derived rather than guessed. + baseline, err := callWithCap(nil) + s.Require().NoError(err, "an uncapped call must still work") + s.Require().Positive(baseline) + + // A cap below the call's own cost must fail it. This is the case that fails + // when gasCap is ignored. + _, err = callWithCap(new(big.Int).SetUint64(baseline - 1)) + s.Require().Error(err, "a gasCap below the call's cost must stop the call") + + // A modest cap that comfortably covers the call succeeds, while staying two + // orders of magnitude below DefaultGasCap - so success here is the supplied + // cap being honoured, not the default ceiling doing the work. + // + // Note GasUsed is not itself a viable limit. It is the net, post-refund + // figure (~24k for this call), whereas the execution needs materially more + // than that to be *given* up front - a cap of 2x GasUsed still reverts. Do + // not "tighten" this to baseline or a small multiple of it. + room := new(big.Int).SetUint64(baseline * 10) + used, err := callWithCap(room) + s.Require().NoError(err, "a gasCap with headroom over the call's cost must succeed") + s.Require().Less(room.Uint64(), config.DefaultGasCap, + "the headroom cap must stay below DefaultGasCap or this proves nothing") + s.Require().Positive(used) + + // DefaultGasCap remains the ceiling: a caller may narrow the limit, never + // widen it. + huge := new(big.Int).Mul(new(big.Int).SetUint64(config.DefaultGasCap), big.NewInt(10)) + used, err = callWithCap(huge) + s.Require().NoError(err) + s.Require().LessOrEqual(used, config.DefaultGasCap, + "a gasCap above DefaultGasCap must not raise the EVM gas limit") + + // A non-positive or absurdly wide cap falls back to the default ceiling + // rather than truncating to something meaningless. + _, err = callWithCap(big.NewInt(0)) + s.Require().NoError(err, "a zero gasCap must fall back to DefaultGasCap, not to a zero limit") + + overUint64 := new(big.Int).Lsh(big.NewInt(1), 70) + _, err = callWithCap(overUint64) + s.Require().NoError(err, "a gasCap wider than uint64 must fall back to DefaultGasCap") +} diff --git a/x/ibc/callbacks/keeper/keeper.go b/x/ibc/callbacks/keeper/keeper.go index d12603e26..c6d63760f 100644 --- a/x/ibc/callbacks/keeper/keeper.go +++ b/x/ibc/callbacks/keeper/keeper.go @@ -421,14 +421,18 @@ func (k ContractKeeper) IBCOnTimeoutPacketCallback( return err } - res, err := k.evmKeeper.CallEVM(ctx, *abi, sender, contractAddr, true, math.NewIntFromUint64(cachedCtx.GasMeter().GasRemaining()).BigInt(), "onPacketTimeout", + // NOTE: use the cached ctx for the EVM calls, as the acknowledgement path + // does. Running on the parent ctx committed the callback's EVM state + // regardless of the outcome and made writeFn() below a no-op, while the gas + // cap was still being read off cachedCtx (F-2026-18818). + res, err := k.evmKeeper.CallEVM(cachedCtx, *abi, sender, contractAddr, true, math.NewIntFromUint64(cachedCtx.GasMeter().GasRemaining()).BigInt(), "onPacketTimeout", packet.GetSourceChannel(), packet.GetSourcePort(), packet.GetSequence(), packet.GetData()) if err != nil { return errorsmod.Wrapf(types.ErrCallbackFailed, "EVM returned error: %s", err.Error()) } // Consume the actual gas used on the original callback context. - ctx.GasMeter().ConsumeGas(res.GasUsed, "callback onPacketAcknowledgement") + ctx.GasMeter().ConsumeGas(res.GasUsed, "callback onPacketTimeout") if ctx.GasMeter().IsOutOfGas() { return errorsmod.Wrapf(types.ErrCallbackFailed, "out of gas") } diff --git a/x/vm/keeper/call_evm.go b/x/vm/keeper/call_evm.go index de9507c6b..2e000fac6 100644 --- a/x/vm/keeper/call_evm.go +++ b/x/vm/keeper/call_evm.go @@ -59,12 +59,30 @@ func (k Keeper) CallEVMWithData( return nil, err } + // Honour the caller's gasCap. Callers that pass one (the IBC callback keeper + // passes the remaining Cosmos gas) are trying to sandbox the EVM work; before + // F-2026-18818 this parameter was accepted and silently ignored, so the EVM + // always ran to DefaultGasCap and could burn far more gas than the caller had + // budgeted. DefaultGasCap stays the ceiling, so a caller can only ever narrow + // the limit, never widen it. Mirrors DerivedEVMCallWithData, and matches the + // shape upstream cosmos/evm settled on. + gasLimit := config.DefaultGasCap + if gasCap != nil && gasCap.Sign() > 0 { + // A gasCap wider than uint64 cannot be a meaningful limit; fall back to + // the default ceiling rather than truncating it. + if gasCap.BitLen() <= 64 { + if provided := gasCap.Uint64(); provided < gasLimit { + gasLimit = provided + } + } + } + msg := core.Message{ From: from, To: contract, Nonce: nonce, Value: big.NewInt(0), - GasLimit: config.DefaultGasCap, + GasLimit: gasLimit, GasPrice: big.NewInt(0), GasTipCap: big.NewInt(0), GasFeeCap: big.NewInt(0), @@ -85,9 +103,9 @@ func (k Keeper) CallEVMWithData( // A failed execution still burned real EVM work, so charge it to the parent // Cosmos gas meter exactly like a successful one. Skipping this made a // revert (or a deliberate out-of-gas) a free way to consume block compute. - // msg.GasLimit above is the hardcoded config.DefaultGasCap, so res.GasUsed - // — which equals the gas limit on out-of-gas — is bounded by that cap and - // cannot be inflated by the caller. + // msg.GasLimit above is config.DefaultGasCap, which a caller-supplied gasCap + // can only narrow, so res.GasUsed — which equals the gas limit on + // out-of-gas — is bounded by that cap and cannot be inflated by the caller. ctx.GasMeter().ConsumeGas(res.GasUsed, "apply evm message (failed)") return res, errorsmod.Wrap(types.ErrVMExecution, res.VmError) } From 2dd5e31ec1c593850c5cc9d1cdc63ac89a7142bc Mon Sep 17 00:00:00 2001 From: Nilesh Gupta Date: Wed, 26 Aug 2026 19:57:12 +0530 Subject: [PATCH 08/15] fix: reject x/erc20 conversions whose token transfer emits an unexpected Approval event (#44) Wire the orphaned validateApprovalEventDoesNotExist guard into both native-ERC20 convert directions. --- CHANGELOG.md | 7 + tests/integration/x/erc20/test_msg_server.go | 153 ++++++++++++++++++- x/erc20/keeper/msg_server.go | 10 ++ x/erc20/keeper/util.go | 4 + x/erc20/keeper/util_test.go | 11 ++ 5 files changed, 183 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 842bb48e6..8104fb4ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,13 @@ the executor with its signature never checked, letting a victim-signed transaction be replayed as the victim. `VerifySender` is now required before `ApplyTransaction`, so `From` is always the ECDSA-recovered signer regardless of how the message arrived. +- Reject a `x/erc20` conversion whose token `transfer` emits an unexpected `Approval` event. Both + native-ERC20 convert directions escrow the token and mint (or burn) the paired coins, checking + only that the transfer succeeded and that the balance moved by the right amount. A registered + token that also grants a third party an allowance over the recipient satisfies both checks: the + escrow only drains later, when that allowance is spent, leaving the minted coins unbacked. The + `validateApprovalEventDoesNotExist` guard, already documented by both convert doc-comments but + never called, now runs on both paths. - Charge the parent Cosmos gas meter for failed EVM calls in `CallEVMWithData` and `DerivedEVMCallWithData`. Both functions returned on `res.Failed()` before reaching `ctx.GasMeter().ConsumeGas(res.GasUsed)`, so a reverting — or deliberately out-of-gas — diff --git a/tests/integration/x/erc20/test_msg_server.go b/tests/integration/x/erc20/test_msg_server.go index 06ed180d8..854445a79 100644 --- a/tests/integration/x/erc20/test_msg_server.go +++ b/tests/integration/x/erc20/test_msg_server.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/mock" "go.uber.org/mock/gomock" + "github.com/cosmos/evm/contracts" "github.com/cosmos/evm/testutil/integration/base/factory" "github.com/cosmos/evm/testutil/integration/evm/utils" "github.com/cosmos/evm/x/erc20/keeper" @@ -102,13 +103,17 @@ func (s *KeeperTestSuite) TestConvertERC20NativeERC20() { false, }, { - "pass - delayed malicious contract", + // The contract grants a third party an allowance over the escrow + // module on every `transfer`, which would leave the minted coins + // unbacked once that allowance is spent. The unexpected `Approval` + // event in the transfer logs must abort the conversion. + "fail - delayed malicious contract", 10, 10, func(common.Address) {}, func() {}, contractMaliciousDelayed, - true, + false, false, }, { @@ -386,6 +391,150 @@ func (s *KeeperTestSuite) TestConvertERC20NativeERC20() { s.mintFeeCollector = false } +// TestConvertERC20MaliciousApprovalKeepsEscrowInvariant asserts the escrow-and-mint invariant +// for the ERC20 -> Coin direction when the registered token hides an allowance grant inside its +// `transfer`. The module's balance check alone is satisfied by such a token - the escrow only +// drains later, when the third party spends the allowance - so the conversion has to be rejected +// on the unexpected `Approval` event. Asserting the error is not enough: the point of the fix is +// that no coins get minted against an escrow that can be emptied afterwards. +func (s *KeeperTestSuite) TestConvertERC20MaliciousApprovalKeepsEscrowInvariant() { + s.mintFeeCollector = true + defer func() { + s.mintFeeCollector = false + }() + s.SetupTest() + + contractAddr, err := s.setupRegisterERC20Pair(contractMaliciousDelayed) + s.Require().NoError(err) + s.Require().NotEqual(common.Address{}, contractAddr) + + var ( + erc20ABI = contracts.ERC20MinterBurnerDecimalsContract.ABI + sender = s.keyring.GetAccAddr(0) + senderHex = s.keyring.GetAddr(0) + amount = math.NewInt(10) + denom = types.CreateDenom(contractAddr.String()) + ) + + _, err = s.MintERC20Token(contractAddr, senderHex, amount.BigInt()) + s.Require().NoError(err) + + ctx := s.network.GetContext() + erc20Keeper := s.network.App.GetErc20Keeper() + bankKeeper := s.network.App.GetBankKeeper() + + escrowBefore := erc20Keeper.BalanceOf(ctx, erc20ABI, contractAddr, types.ModuleAddress) + s.Require().NotNil(escrowBefore) + senderTokensBefore := erc20Keeper.BalanceOf(ctx, erc20ABI, contractAddr, senderHex) + s.Require().NotNil(senderTokensBefore) + supplyBefore := bankKeeper.GetSupply(ctx, denom) + coinsBefore := bankKeeper.GetBalance(ctx, sender, denom) + s.Require().True(supplyBefore.IsZero(), "no coins should exist for the pair yet") + + convertMsg := types.NewMsgConvertERC20(amount, sender, contractAddr, senderHex) + + // Direct keeper call on a throwaway branch, to pin the rejection reason. + cacheCtx, _ := ctx.CacheContext() + _, err = erc20Keeper.ConvertERC20(cacheCtx, convertMsg) + s.Require().Error(err) + s.Require().ErrorContains(err, "unexpected Approval event") + + // Same message through a real tx, so the failed message's writes are rolled back + // exactly as they would be on chain. The gas limit is set explicitly because the + // factory would otherwise simulate the tx, and the simulation fails for the same reason. + gasLimit := uint64(1_000_000) + res, err := s.factory.CommitCosmosTx(s.keyring.GetPrivKey(0), factory.CosmosTxArgs{ + Gas: &gasLimit, + Msgs: []sdk.Msg{convertMsg}, + }) + s.Require().NoError(err) + s.Require().NotEqual(uint32(0), res.Code, "expected the conversion tx to fail, got log: %s", res.Log) + s.Require().Contains(res.Log, "unexpected Approval event") + + // Invariant: nothing escrowed, nothing minted. + ctx = s.network.GetContext() + + escrowAfter := erc20Keeper.BalanceOf(ctx, erc20ABI, contractAddr, types.ModuleAddress) + s.Require().NotNil(escrowAfter) + s.Require().Zero(escrowBefore.Cmp(escrowAfter), "escrow moved: before %s, after %s", escrowBefore, escrowAfter) + + senderTokensAfter := erc20Keeper.BalanceOf(ctx, erc20ABI, contractAddr, senderHex) + s.Require().NotNil(senderTokensAfter) + s.Require().Zero(senderTokensBefore.Cmp(senderTokensAfter), "sender tokens moved: before %s, after %s", senderTokensBefore, senderTokensAfter) + + supplyAfter := bankKeeper.GetSupply(ctx, denom) + s.Require().True(supplyAfter.IsZero(), "unbacked coins were minted: %s", supplyAfter) + s.Require().Equal(supplyBefore, supplyAfter) + s.Require().Equal(coinsBefore, bankKeeper.GetBalance(ctx, sender, denom)) +} + +// TestConvertCoinMaliciousApprovalKeepsEscrowInvariant covers the Coin -> ERC20 direction of the +// same guard. Here the hidden allowance is granted over the receiver rather than the module, so +// the unescrow must be rejected before any token leaves escrow and before the coins are burned. +// +// The whole exercise runs on a throwaway branch of the context: the malicious pair can never +// reach this state through committed txs, because the ERC20 -> Coin leg that would normally mint +// the coins is itself rejected by the same guard. +func (s *KeeperTestSuite) TestConvertCoinMaliciousApprovalKeepsEscrowInvariant() { + s.mintFeeCollector = true + defer func() { + s.mintFeeCollector = false + }() + s.SetupTest() + + contractAddr, err := s.setupRegisterERC20Pair(contractMaliciousDelayed) + s.Require().NoError(err) + s.Require().NotEqual(common.Address{}, contractAddr) + + var ( + erc20ABI = contracts.ERC20MinterBurnerDecimalsContract.ABI + sender = s.keyring.GetAccAddr(0) + receiverHex = s.keyring.GetAddr(1) + amount = math.NewInt(10) + denom = types.CreateDenom(contractAddr.String()) + ) + + // Fund the escrow directly: `mint` does not route through the malicious `transfer`. + _, err = s.MintERC20Token(contractAddr, types.ModuleAddress, amount.BigInt()) + s.Require().NoError(err) + + erc20Keeper := s.network.App.GetErc20Keeper() + bankKeeper := s.network.App.GetBankKeeper() + + ctx, _ := s.network.GetContext().CacheContext() + + // Give the sender the matching coins, as a successful ERC20 -> Coin leg would have. + coins := sdk.Coins{sdk.Coin{Denom: denom, Amount: amount}} + s.Require().NoError(bankKeeper.MintCoins(ctx, types.ModuleName, coins)) + s.Require().NoError(bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, sender, coins)) + + escrowBefore := erc20Keeper.BalanceOf(ctx, erc20ABI, contractAddr, types.ModuleAddress) + s.Require().NotNil(escrowBefore) + s.Require().Zero(escrowBefore.Cmp(amount.BigInt()), "escrow should hold the minted tokens") + receiverTokensBefore := erc20Keeper.BalanceOf(ctx, erc20ABI, contractAddr, receiverHex) + s.Require().NotNil(receiverTokensBefore) + supplyBefore := bankKeeper.GetSupply(ctx, denom) + + // baseapp runs every message on its own branch and drops it when the message errors; + // mirror that here so the assertions below see what the chain would have kept. + msgCtx, _ := ctx.CacheContext() + convertMsg := types.NewMsgConvertCoin(coins[0], receiverHex, sender) + _, err = erc20Keeper.ConvertCoin(msgCtx, convertMsg) + s.Require().Error(err) + s.Require().ErrorContains(err, "unexpected Approval event") + + // Invariant: escrow untouched, receiver got nothing, coins not burned. + escrowAfter := erc20Keeper.BalanceOf(ctx, erc20ABI, contractAddr, types.ModuleAddress) + s.Require().NotNil(escrowAfter) + s.Require().Zero(escrowBefore.Cmp(escrowAfter), "escrow moved: before %s, after %s", escrowBefore, escrowAfter) + + receiverTokensAfter := erc20Keeper.BalanceOf(ctx, erc20ABI, contractAddr, receiverHex) + s.Require().NotNil(receiverTokensAfter) + s.Require().Zero(receiverTokensBefore.Cmp(receiverTokensAfter), "receiver tokens moved: before %s, after %s", receiverTokensBefore, receiverTokensAfter) + + s.Require().Equal(supplyBefore, bankKeeper.GetSupply(ctx, denom), "coins were burned without releasing tokens") +} + func (s *KeeperTestSuite) TestConvertNativeERC20ToEVMERC20() { var ( contractAddr common.Address diff --git a/x/erc20/keeper/msg_server.go b/x/erc20/keeper/msg_server.go index 65f04aba4..ee5eb36fa 100644 --- a/x/erc20/keeper/msg_server.go +++ b/x/erc20/keeper/msg_server.go @@ -110,6 +110,11 @@ func (k Keeper) convertERC20IntoCoinsForNativeToken( } } + // Check for unexpected `Approval` event in logs + if err := validateApprovalEventDoesNotExist(res.Logs); err != nil { + return nil, err + } + // Check expected escrow balance after transfer execution // NOTE: coin fields already validated in the ValidateBasic() of the message coins := sdk.Coins{sdk.Coin{Denom: pair.Denom, Amount: msg.Amount}} @@ -320,6 +325,11 @@ func (k Keeper) convertCoinNativeERC20( } } + // Check for unexpected `Approval` event in logs + if err := validateApprovalEventDoesNotExist(res.Logs); err != nil { + return err + } + // Check expected Receiver balance after transfer execution balanceTokenAfter := k.BalanceOf(ctx, erc20, contract, receiver) if balanceTokenAfter == nil { diff --git a/x/erc20/keeper/util.go b/x/erc20/keeper/util.go index 97e2fe81e..b25a5268b 100644 --- a/x/erc20/keeper/util.go +++ b/x/erc20/keeper/util.go @@ -13,6 +13,10 @@ import ( // an unexpected `Approval` event func validateApprovalEventDoesNotExist(logs []*types.Log) error { for _, log := range logs { + // anonymous events (LOG0) carry no topics + if len(log.Topics) == 0 { + continue + } if log.Topics[0] == logApprovalSigHash.Hex() { return errors.Wrapf( types2.ErrUnexpectedEvent, "unexpected Approval event", diff --git a/x/erc20/keeper/util_test.go b/x/erc20/keeper/util_test.go index d5f59ffc2..8262f7d73 100644 --- a/x/erc20/keeper/util_test.go +++ b/x/erc20/keeper/util_test.go @@ -33,6 +33,17 @@ func TestValidateApprovalEventDoesNotExist(t *testing.T) { }, expectError: false, }, + { + name: "anonymous log without topics", + res: &types.MsgEthereumTxResponse{ + Logs: []*types.Log{ + { + Topics: []string{}, + }, + }, + }, + expectError: false, + }, { name: "has approval event", res: &types.MsgEthereumTxResponse{ From a7263454517581054541455cb0a526203aaef403 Mon Sep 17 00:00:00 2001 From: Nilesh Gupta Date: Wed, 26 Aug 2026 20:03:47 +0530 Subject: [PATCH 09/15] fix: F-2026-18182 | [Dual Defense] Unbounded UEA Payload Gas Can Stall Synchronous EVM Execution (#49) * fix: charge parent gas meter for failed EVM calls (F-2026-18824) CallEVMWithData and DerivedEVMCallWithData returned on res.Failed() before reaching ctx.GasMeter().ConsumeGas(res.GasUsed), so a reverting or deliberately out-of-gas internal call was free at the Cosmos meter. DerivedEVMCallWithData also clamps a caller-supplied gasLimit to config.DefaultGasCap: on an out-of-gas halt res.GasUsed equals the cap, so without the clamp the caller would choose how much gas the enclosing Cosmos tx is forced to consume and could panic it with OutOfGas. * fix: reject a non-uint64 payload gas limit instead of truncating it (F-2026-18182) --- CHANGELOG.md | 6 + tests/integration/x/vm/test_derived_call.go | 200 ++++++++++++++++++++ x/vm/keeper/call_evm.go | 15 ++ 3 files changed, 221 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8104fb4ae..754d0442b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,12 @@ equals the gas cap, so without the clamp the caller would pick how much gas the enclosing transaction is forced to consume and could panic it with `OutOfGas`. `CallEVMWithData` needs no clamp — its message gas limit is already the hardcoded `config.DefaultGasCap`. +- Reject a `gasLimit` that does not fit in a `uint64` in `DerivedEVMCallWithData` instead of + truncating it. `big.Int.Uint64` is undefined for values that do not fit and in practice returns + the low 64 bits, and node-side validation bounds the universal-payload gas limit to `uint256` — + so a payload declaring `2^64 + 5` was silently handed **5** gas and failed with a misleading + out-of-gas error rather than a clear rejection. The call now returns `ErrInvalidGasLimit` naming + the problem; `math.MaxUint64` still fits and is clamped to `config.DefaultGasCap` as before. - [\#690](https://github.com/cosmos/evm/pull/690) Fix Ledger hardware wallet support for coin type 60. - [\#769](https://github.com/cosmos/evm/pull/769) Fix erc20 ibc middleware to not to validate sender address format. - [\#790](https://github.com/cosmos/evm/pull/790) fix panic in historical query due to missing EvmCoinInfo. diff --git a/tests/integration/x/vm/test_derived_call.go b/tests/integration/x/vm/test_derived_call.go index 5cacb679b..7580a54c4 100644 --- a/tests/integration/x/vm/test_derived_call.go +++ b/tests/integration/x/vm/test_derived_call.go @@ -1,6 +1,7 @@ package vm import ( + "math" "math/big" "strconv" @@ -219,6 +220,11 @@ var gasBurnerRevertCode = []byte{0x60, 0x00, 0x62, 0x10, 0x00, 0x00, 0x52, 0x60, // for STOP: the same ~2.2M of memory expansion, but a successful execution. var gasBurnerSuccessCode = []byte{0x60, 0x00, 0x62, 0x10, 0x00, 0x00, 0x52, 0x00} +// stopCode is a single STOP opcode: the cheapest possible successful execution, +// costing exactly the 21,000 intrinsic gas of the call itself. It gives the gas +// estimator something it can predict exactly. +var stopCode = []byte{0x00} + // invalidOpcodeCode is a single INVALID opcode. An exceptional halt burns every // unit of gas handed to the frame, so res.GasUsed comes back equal to the gas // cap — the same shape as running out of gas, without waiting for a loop to @@ -377,3 +383,197 @@ func (s *KeeperTestSuite) TestDerivedEVMCallWithDataSuccessChargesGasUsed() { s.Require().LessOrEqual(charged, res.GasUsed+1_000_000, "res.GasUsed must be charged exactly once on the success path") } + +// --------------------------------------------------------------------------- +// F-2026-18182: a gas limit that does not fit in uint64 must be rejected, not +// silently truncated to its low 64 bits. +// --------------------------------------------------------------------------- + +// TestDerivedEVMCallWithDataRejectsNonUint64GasLimit is the regression test for +// F-2026-18182. big.Int.Uint64 is undefined for values that do not fit and in +// practice returns the low 64 bits, so before the IsUint64 guard a caller-supplied +// gasLimit at or above 2^64 was silently reinterpreted as a much smaller number. +// Node-side validation bounds UniversalPayload.GasLimit to uint256, so every value +// exercised here genuinely reaches this call. +// +// The third case is the one that makes the bug visible rather than merely wrong: +// 2^64 + 5_000_000 truncates to exactly 5_000_000 gas, which is enough for the +// burner to run to completion — so without the guard the call would report a clean +// success against a budget the caller never asked for. The first two truncate to 0 +// and 5, which fail intrinsic-gas checks and so surface as a confusing out-of-gas +// error instead of a clear rejection. Both shapes are wrong; asserting on the error +// text plus the untouched parent meter distinguishes the fix from either of them. +func (s *KeeperTestSuite) TestDerivedEVMCallWithDataRejectsNonUint64GasLimit() { + twoPow64 := new(big.Int).Lsh(big.NewInt(1), 64) + + testCases := []struct { + name string + gasLimit *big.Int + truncatesTo string + }{ + { + name: "2^64", + gasLimit: new(big.Int).Set(twoPow64), + truncatesTo: "0", + }, + { + name: "2^64 + 5", + gasLimit: new(big.Int).Add(twoPow64, big.NewInt(5)), + truncatesTo: "5", + }, + { + name: "2^64 + 5_000_000 (truncation would execute successfully)", + gasLimit: new(big.Int).Add(twoPow64, big.NewInt(5_000_000)), + truncatesTo: "5000000", + }, + { + name: "2^256 - 1 (the largest value node-side validation admits)", + gasLimit: new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 256), big.NewInt(1)), + truncatesTo: "18446744073709551615", + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + s.SetupTest() + + from := s.Keyring.GetAddr(0) + ctx := s.Network.GetContext().WithGasMeter(storetypes.NewGasMeter(parentGasMeterLimit)) + keeper := s.Network.App.GetEVMKeeper() + + // The burner needs ~2.2M gas: reachable under the truncated budget of + // the 5_000_000 case, unreachable under a correct rejection. + burner := s.deployRawCode(ctx, gasBurnerSuccessCode) + gasBefore := ctx.GasMeter().GasConsumed() + + res, err := keeper.DerivedEVMCallWithData( + ctx, from, &burner, nil, + true, // commit + false, // gasless + false, // isModuleSender + big.NewInt(0), tc.gasLimit, nil, + ) + + // Assert the *shape* of the outcome before the error text: under a + // truncation regression the 5_000_000 case returns (res, nil) and every + // error-flavoured assertion would be vacuous, so the no-execution checks + // have to come first to be the ones that actually fail. + charged := ctx.GasMeter().GasConsumed() - gasBefore + s.Require().Less(charged, uint64(100_000), + "a rejected gas limit must not run the EVM; truncating to %s would have burned ~2.2M", + tc.truncatesTo) + s.Require().Nil(res, + "a rejected gas limit must not produce an execution result") + + s.Require().Error(err, "a gas limit above uint64 must be rejected outright") + s.Require().ErrorContains(err, "does not fit in uint64", + "the rejection must name the problem rather than surface as out-of-gas") + s.Require().ErrorIs(err, evmtypes.ErrInvalidGasLimit) + }) + } +} + +// TestDerivedEVMCallWithDataAcceptsMaxUint64GasLimit pins the boundary: MaxUint64 +// exactly does fit in a uint64, so it must be accepted by the guard and then +// clamped to config.DefaultGasCap by the F-2026-18824 clamp — not rejected. The +// INVALID opcode burns the whole frame, so res.GasUsed reports the effective cap +// and shows the clamp took hold. +func (s *KeeperTestSuite) TestDerivedEVMCallWithDataAcceptsMaxUint64GasLimit() { + s.SetupTest() + + from := s.Keyring.GetAddr(0) + ctx := s.Network.GetContext().WithGasMeter(storetypes.NewGasMeter(parentGasMeterLimit)) + keeper := s.Network.App.GetEVMKeeper() + + halter := s.deployRawCode(ctx, invalidOpcodeCode) + gasBefore := ctx.GasMeter().GasConsumed() + + maxUint64 := new(big.Int).SetUint64(math.MaxUint64) + + var res *evmtypes.MsgEthereumTxResponse + var err error + s.Require().NotPanics(func() { + res, err = keeper.DerivedEVMCallWithData( + ctx, from, &halter, nil, + true, false, false, + big.NewInt(0), maxUint64, nil, + ) + }, "MaxUint64 fits in a uint64 and must not panic the parent gas meter once clamped") + + s.Require().Error(err, "the halter halts exceptionally, so the call still surfaces an error") + s.Require().NotErrorIs(err, evmtypes.ErrInvalidGasLimit, + "MaxUint64 fits in uint64 and must not be rejected by the IsUint64 guard") + s.Require().NotNil(res) + s.Require().True(res.Failed()) + s.Require().Equal(config.DefaultGasCap, res.GasUsed, + "MaxUint64 must be clamped to DefaultGasCap, not accepted as-is") + + s.Require().LessOrEqual(ctx.GasMeter().GasConsumed()-gasBefore, config.DefaultGasCap+1_000_000, + "the parent meter must never be charged beyond the clamped cap") +} + +// TestDerivedEVMCallWithDataNormalGasLimitUnaffected pins ordinary traffic against +// the new guard. 610,477 is the gasUsed observed on donut for a real UEA payload +// execution, ~40x below the 25M DefaultGasCap; it fits in a uint64, so the guard +// must be invisible to it. +func (s *KeeperTestSuite) TestDerivedEVMCallWithDataNormalGasLimitUnaffected() { + s.SetupTest() + + erc20ABI := contracts.ERC20MinterBurnerDecimalsContract.ABI + wevmos := common.HexToAddress(testconstants.WEVMOSContractMainnet) + from := s.Keyring.GetAddr(0) + ctx := s.Network.GetContext().WithGasMeter(storetypes.NewGasMeter(parentGasMeterLimit)) + keeper := s.Network.App.GetEVMKeeper() + + data, err := erc20ABI.Pack("balanceOf", from) + s.Require().NoError(err) + + res, err := keeper.DerivedEVMCallWithData( + ctx, from, &wevmos, data, + true, false, false, + big.NewInt(0), big.NewInt(610_477), nil, + ) + s.Require().NoError(err, "a real-world gas limit must be unaffected by the uint64 guard") + s.Require().False(res.Failed()) + s.Require().Greater(res.GasUsed, uint64(0)) + s.Require().Less(res.GasUsed, uint64(610_477), + "the call must run inside the declared budget, not against a truncated one") +} + +// TestDerivedEVMCallWithDataNilGasLimitUsesEstimation pins the nil path: the guard +// lives inside the `gasLimit != nil` branch, so a nil limit must still fall through +// to EstimateGasInternal untouched — and must certainly not be dereferenced. +func (s *KeeperTestSuite) TestDerivedEVMCallWithDataNilGasLimitUsesEstimation() { + s.SetupTest() + + from := s.Keyring.GetAddr(0) + // Deliberately the plain context: gas estimation binary-searches the EVM and + // this test is about the nil branch, not about metering. + ctx := s.Network.GetContext() + keeper := s.Network.App.GetEVMKeeper() + + // A STOP-only contract is the one target whose cost the estimator can predict + // exactly. The estimator is documented to underpredict on richer targets — a + // pre-existing property of this path that the guard neither causes nor fixes. + target := s.deployRawCode(ctx, stopCode) + + var res *evmtypes.MsgEthereumTxResponse + var err error + s.Require().NotPanics(func() { + res, err = keeper.DerivedEVMCallWithData( + ctx, from, &target, nil, + true, // commit — the estimation branch only runs when committing + false, // gasless + false, // isModuleSender + big.NewInt(0), nil, nil, + ) + }, "a nil gasLimit must never reach the IsUint64 guard") + + s.Require().NoError(err) + s.Require().NotErrorIs(err, evmtypes.ErrInvalidGasLimit, + "the guard lives inside the gasLimit != nil branch and must not fire on nil") + s.Require().NotNil(res) + s.Require().False(res.Failed()) + s.Require().Greater(res.GasUsed, uint64(0), + "the estimation path must still produce a workable budget") +} diff --git a/x/vm/keeper/call_evm.go b/x/vm/keeper/call_evm.go index 2e000fac6..20f50d34b 100644 --- a/x/vm/keeper/call_evm.go +++ b/x/vm/keeper/call_evm.go @@ -2,6 +2,7 @@ package keeper import ( "encoding/json" + "math" "math/big" "strconv" @@ -215,6 +216,20 @@ func (k Keeper) DerivedEVMCallWithData( gasCap = gasRes.Gas } if gasLimit != nil { + // Reject a limit that does not fit in uint64 instead of truncating it. + // big.Int.Uint64 is documented as undefined for values that do not fit and + // in practice returns the low 64 bits, so a payload declaring 2^64+5 would + // silently be handed 5 gas and fail with a misleading out-of-gas error. + // Node-side validation bounds UniversalPayload.GasLimit to uint256, so + // values at or above 2^64 do reach this call. IsUint64 also rejects a + // negative limit, for which Uint64 is equally undefined. + if !gasLimit.IsUint64() { + return nil, errorsmod.Wrapf( + types.ErrInvalidGasLimit, + "gas limit %s does not fit in uint64 (max %d)", + gasLimit, uint64(math.MaxUint64), + ) + } // Clamp the caller-supplied limit to DefaultGasCap. The failure path below // charges res.GasUsed to the parent Cosmos gas meter, and on out-of-gas // res.GasUsed equals this cap — so an unclamped caller value would let the From b678fbc564b56fbb94ae5e0324f04f2911eb1583 Mon Sep 17 00:00:00 2001 From: Nilesh Gupta Date: Wed, 26 Aug 2026 21:22:01 +0530 Subject: [PATCH 10/15] chore: bring cosmos/evm v0.6.0 onto audit-fixes (fixes F-2026-18785) (#47) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * v0.6.0 * chore: changelog (#1025) * Changelog * fix line length * fix: solhint line length and build-v05 legacy binary ref * test: raise TestChainUpgrade block-height wait for 5s block time * ci: move jobs off dead Depot runners to GitHub-hosted Seven jobs never run — they queue until GitHub cancels them at 24h. Bumps test/lint timeouts 15m->30m for the smaller GitHub runners. * fix(rpc): report base fee as the gas price of derived txs * refactor(statedb): use OverrideEvents instead of the reflect/unsafe shim The shim existed because cosmos-sdk v0.50.x had no in-place events setter; the fork now pins v0.53.6, where OverrideEvents is exactly em.events = events. * style(statedb): let prevEvents infer its type (staticcheck ST1023) --------- Co-authored-by: Vlad Co-authored-by: Vlad J Co-authored-by: Arya Lanjewar <102943033+AryaLanjewar3005@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/jsonrpc-compatibility.yml | 2 +- .github/workflows/lint.yml | 4 +- .github/workflows/test.yml | 4 +- CHANGELOG.md | 30 + Makefile | 55 +- ante/cosmos/eip712.go | 4 +- ante/evm/fee_checker.go | 2 +- contracts/contract_creation_tester.go | 10 + contracts/erc20_with_native_transfers.go | 10 + .../ics20_sequential_precompile_calls.go | 10 + contracts/ics20_transfer_tester.go | 10 + contracts/sequential_operations_tester.go | 10 + .../solidity/ContractCreationTester.json | 282 +++++ contracts/solidity/ContractCreationTester.sol | 237 +++++ .../solidity/ERC20WithNativeTransfers.json | 813 +++++++++++++++ .../solidity/ERC20WithNativeTransfers.sol | 150 +++ contracts/solidity/ICS20TransferTester.json | 217 ++++ contracts/solidity/ICS20TransferTester.sol | 120 +++ contracts/solidity/SequentialICS20Sender.json | 168 +++ contracts/solidity/SequentialICS20Sender.sol | 82 ++ .../solidity/SequentialOperationsTester.json | 274 +++++ .../solidity/SequentialOperationsTester.sol | 183 ++++ contracts/solidity/eips/testdata/Counter.sol | 15 + .../solidity/eips/testdata/CounterFactory.sol | 25 + docs/migrations/PUSHCHAIN_v0.5.1_to_v0.6.0.md | 283 +++++ ....5.0_UNRELEASED.md => v0.4.0_to_v0.5.0.md} | 0 docs/migrations/v0.5.x_to_v0.6.0.md | 491 +++++++++ ethereum/eip712/encoding.go | 4 +- ethereum/eip712/encoding_legacy.go | 2 +- evmd/app.go | 13 +- evmd/go.mod | 15 +- evmd/go.sum | 32 +- evmd/tests/ibc/autoflush_test.go | 967 ++++++++++++++++++ evmd/tests/ibc/helper.go | 82 +- evmd/tests/ibc/ibc_middleware_test.go | 153 ++- evmd/tests/ibc/ics20_erc20_conversion_test.go | 500 +++++++++ .../ibc/ics20_precompile_transfer_test.go | 19 + .../ics20_recursive_precompile_calls_test.go | 476 ++++++++- .../ics20_sequential_precompile_calls_test.go | 199 ++++ .../ibc/v2_ics20_precompile_transfer_test.go | 19 + .../integration/balance_handler/helper.go | 10 +- evmd/tests/testdata/debug/debug.go | 12 +- evmd/tests/testdata/debug/interface.go | 5 +- evmd/upgrades.go | 42 +- go.mod | 26 +- go.sum | 32 +- interfaces.go | 2 +- mempool/blockchain_test.go | 3 +- precompiles/common/balance_handler.go | 11 +- precompiles/common/interfaces.go | 6 + precompiles/common/precompile.go | 5 +- precompiles/ics20/ics20.go | 3 + precompiles/ics20/ics20_selector_test.go | 2 +- precompiles/ics20/tx.go | 97 +- precompiles/types/defaults.go | 4 +- precompiles/types/static_precompiles.go | 4 +- rpc/backend/comet_to_eth.go | 11 +- rpc/backend/derived_gas_price_test.go | 108 ++ rpc/types/types_test.go | 2 +- rpc/types/utils.go | 61 +- rpc/types/utils_test.go | 71 ++ .../precompiles/erc20/test_query.go | 2 +- .../precompiles/ics20/test_setup.go | 2 + tests/integration/x/erc20/test_convert.go | 390 +++++++ tests/integration/x/erc20/test_evm.go | 82 +- .../integration/x/erc20/test_ibc_callback.go | 12 +- tests/integration/x/erc20/test_msg_server.go | 427 ++------ tests/integration/x/erc20/test_proposals.go | 2 +- tests/integration/x/ibc/test_keeper.go | 2 - tests/integration/x/ibc/test_msg_server.go | 517 ---------- .../x/vm/state_transition_benchmark.go | 13 +- tests/integration/x/vm/test_call_evm.go | 161 ++- .../integration/x/vm/test_call_evm_gascap.go | 6 +- .../x/vm/test_commit_idempotency.go | 235 +++++ .../integration/x/vm/test_state_transition.go | 141 ++- tests/systemtests/Makefile | 2 +- tests/systemtests/go.mod | 6 +- tests/systemtests/go.sum | 12 +- tests/systemtests/upgrade_test.go | 10 +- testutil/tx/eip712.go | 5 +- x/erc20/keeper/convert.go | 177 ++++ x/erc20/keeper/evm.go | 53 +- x/erc20/keeper/ibc_callbacks.go | 4 +- x/erc20/keeper/keeper.go | 2 +- x/erc20/keeper/msg_server.go | 195 +--- x/erc20/module.go | 2 +- x/erc20/types/interfaces.go | 14 +- x/erc20/types/mocks/EVMKeeper.go | 141 ++- x/erc20/types/msg.go | 15 - x/erc20/types/msg_test.go | 8 +- x/feemarket/module.go | 2 +- x/feemarket/types/msg.go | 5 - x/ibc/callbacks/keeper/keeper.go | 15 +- x/ibc/callbacks/types/expected_keepers.go | 17 +- x/ibc/transfer/ibc_module.go | 22 - x/ibc/transfer/keeper/keeper.go | 50 - x/ibc/transfer/keeper/msg_server.go | 117 --- x/ibc/transfer/module.go | 69 -- x/ibc/transfer/types/channels.go | 13 - x/ibc/transfer/types/interfaces.go | 31 - x/ibc/transfer/v2/ibc_module.go | 22 - x/precisebank/module.go | 2 +- x/vm/keeper/call_evm.go | 66 +- x/vm/keeper/grpc_query.go | 12 +- x/vm/keeper/state_transition.go | 45 +- x/vm/keeper/statedb.go | 9 + x/vm/statedb/balance_events_test.go | 113 ++ x/vm/statedb/journal.go | 17 +- x/vm/statedb/statedb.go | 80 +- x/vm/statedb/statedb_test.go | 40 +- x/vm/types/denom_config_testing.go | 24 +- x/vm/types/errors.go | 4 + x/vm/types/msg.go | 5 - 114 files changed, 7972 insertions(+), 1937 deletions(-) create mode 100644 contracts/contract_creation_tester.go create mode 100644 contracts/erc20_with_native_transfers.go create mode 100644 contracts/ics20_sequential_precompile_calls.go create mode 100644 contracts/ics20_transfer_tester.go create mode 100644 contracts/sequential_operations_tester.go create mode 100644 contracts/solidity/ContractCreationTester.json create mode 100644 contracts/solidity/ContractCreationTester.sol create mode 100644 contracts/solidity/ERC20WithNativeTransfers.json create mode 100644 contracts/solidity/ERC20WithNativeTransfers.sol create mode 100644 contracts/solidity/ICS20TransferTester.json create mode 100644 contracts/solidity/ICS20TransferTester.sol create mode 100644 contracts/solidity/SequentialICS20Sender.json create mode 100644 contracts/solidity/SequentialICS20Sender.sol create mode 100644 contracts/solidity/SequentialOperationsTester.json create mode 100644 contracts/solidity/SequentialOperationsTester.sol create mode 100644 contracts/solidity/eips/testdata/Counter.sol create mode 100644 contracts/solidity/eips/testdata/CounterFactory.sol create mode 100644 docs/migrations/PUSHCHAIN_v0.5.1_to_v0.6.0.md rename docs/migrations/{v0.4.0_to_v0.5.0_UNRELEASED.md => v0.4.0_to_v0.5.0.md} (100%) create mode 100644 docs/migrations/v0.5.x_to_v0.6.0.md create mode 100644 evmd/tests/ibc/autoflush_test.go create mode 100644 evmd/tests/ibc/ics20_erc20_conversion_test.go create mode 100644 evmd/tests/ibc/ics20_sequential_precompile_calls_test.go create mode 100644 rpc/backend/derived_gas_price_test.go create mode 100644 tests/integration/x/erc20/test_convert.go delete mode 100644 tests/integration/x/ibc/test_msg_server.go create mode 100644 tests/integration/x/vm/test_commit_idempotency.go create mode 100644 x/erc20/keeper/convert.go delete mode 100644 x/ibc/transfer/ibc_module.go delete mode 100644 x/ibc/transfer/keeper/keeper.go delete mode 100644 x/ibc/transfer/keeper/msg_server.go delete mode 100644 x/ibc/transfer/module.go delete mode 100644 x/ibc/transfer/types/channels.go delete mode 100644 x/ibc/transfer/types/interfaces.go delete mode 100644 x/ibc/transfer/v2/ibc_module.go create mode 100644 x/vm/statedb/balance_events_test.go diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index fdf7e8e2c..6b7d5f523 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -18,7 +18,7 @@ permissions: read-all jobs: analyze: name: Analyze - runs-on: depot-ubuntu-24.04-8 + runs-on: ubuntu-latest permissions: actions: read contents: read diff --git a/.github/workflows/jsonrpc-compatibility.yml b/.github/workflows/jsonrpc-compatibility.yml index 3d2b63239..832e0a84b 100644 --- a/.github/workflows/jsonrpc-compatibility.yml +++ b/.github/workflows/jsonrpc-compatibility.yml @@ -28,7 +28,7 @@ jobs: if: ${{ !startsWith(github.ref, 'refs/tags/') && github.ref != 'refs/heads/main' }} jsonrpc-compatibility-test: - runs-on: depot-ubuntu-22.04-8 + runs-on: ubuntu-latest timeout-minutes: 45 steps: - uses: actions/setup-go@v5 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index f53336477..54e903b43 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -16,8 +16,8 @@ permissions: read-all jobs: golangci: name: Run golangci-lint - runs-on: depot-ubuntu-24.04-8 - timeout-minutes: 15 + runs-on: ubuntu-latest + timeout-minutes: 30 steps: - uses: actions/setup-go@v5 with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5b7d7e006..b9b55dc4f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,7 @@ jobs: if: "!startsWith(github.ref, 'refs/tags/') && github.ref != 'refs/heads/main'" test-unit-cover: - runs-on: depot-ubuntu-24.04-16 + runs-on: ubuntu-latest steps: - uses: actions/setup-go@v5 with: @@ -76,7 +76,7 @@ jobs: if: env.GIT_DIFF test-fuzz: - runs-on: depot-ubuntu-24.04-4 + runs-on: ubuntu-latest steps: - uses: actions/setup-go@v5 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index 754d0442b..433b72113 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ # CHANGELOG +## v0.6.0 + +Follow the [migration document](docs/migrations/v0.5.x_to_v0.6.0.md) for upgrade instructions. + +### BREAKING CHANGES +- Removed IBC Transfer wrapper. Users are now required to use the precompile to transfer ERC20 tokens. +- Added StateDB as a parameter to internal EVM calls. + +### DEPENDENCIES + +### IMPROVEMENTS + +### FEATURES + +### BUG FIXES + +- Re-apply the `validateApprovalEventDoesNotExist` guard (F-2026-18822) on the ERC20 -> coin + direction. v0.6.0 lifts that body out of `x/erc20/keeper/msg_server.go` into the new shared + `x/erc20/keeper/convert.go` (`ConvertERC20IntoCoinsForNativeToken`, also reached from the ICS20 + precompile) and drops the call on the way, while still promising it in the doc comment. The guard + is restored in the position it held before the move, and the `delayed malicious contract` case in + `TestConvertERC20IntoCoinsForNativeToken` expects a rejection again. +- Report the block base fee (instead of `0`) as the `gasPrice` of derived EVM transactions in + `eth_getTransactionByHash` / `eth_getBlockByNumber`, and as their receipt `effectiveGasPrice`. + Derived txs carry zero fee caps, so consumers that model burn as `base_fee * gas_used` — such as + Blockscout's block-reward formula — read blocks whose only content is derived txs as burning more + than they collected, and render a negative block reward. + ## v0.5.1 ### DEPENDENCIES @@ -100,6 +128,8 @@ - [\#730](https://github.com/cosmos/evm/pull/730) Fix panic if evm mempool not used. - [\#733](https://github.com/cosmos/evm/pull/733) Avoid rejecting tx with unsupported extension option for ExtensionOptionDynamicFeeTx. - [\#736](https://github.com/cosmos/evm/pull/736) Add InitEvmCoinInfo upgrade to avoid panic when denom is not registered. +- Add `stateDB` and `callFromPrecompile` parameters to internal EVM messages. +- Fixed an issue with events missing from final transaction result. ### IMPROVEMENTS diff --git a/Makefile b/Makefile index fc6aef2ad..d59ba1a75 100644 --- a/Makefile +++ b/Makefile @@ -132,12 +132,12 @@ PACKAGES_UNIT := $(shell go list ./... | grep -v '/tests/e2e$$' | grep -v '/simu PACKAGES_EVMD := $(shell cd evmd && go list ./... | grep -v '/simulation') COVERPKG_EVM := $(shell go list ./... | grep -v '/tests/e2e$$' | grep -v '/simulation' | paste -sd, -) COVERPKG_ALL := $(COVERPKG_EVM) -COMMON_COVER_ARGS := -timeout=15m -covermode=atomic +COMMON_COVER_ARGS := -timeout=30m -covermode=atomic TEST_PACKAGES := ./... TEST_TARGETS := test-unit test-evmd test-unit-cover test-race -test-unit: ARGS=-timeout=15m +test-unit: ARGS=-timeout=30m test-unit: TEST_PACKAGES=$(PACKAGES_UNIT) test-unit: run-tests @@ -145,11 +145,11 @@ test-race: ARGS=-race test-race: TEST_PACKAGES=$(PACKAGES_UNIT) test-race: run-tests -test-evmd: ARGS=-timeout=15m +test-evmd: ARGS=-timeout=30m test-evmd: @cd evmd && go test -race -tags=test -mod=readonly $(ARGS) $(EXTRA_ARGS) $(PACKAGES_EVMD) -test-unit-cover: ARGS=-timeout=15m -coverprofile=coverage.txt -covermode=atomic +test-unit-cover: ARGS=-timeout=30m -coverprofile=coverage.txt -covermode=atomic test-unit-cover: TEST_PACKAGES=$(PACKAGES_UNIT) test-unit-cover: run-tests @echo "🔍 Running evm (root) coverage..." @@ -167,9 +167,9 @@ test: test-unit test-all: @echo "🔍 Running evm module tests..." - @go test -race -tags=test -mod=readonly -timeout=15m $(PACKAGES_NOSIMULATION) + @go test -race -tags=test -mod=readonly -timeout=30m $(PACKAGES_NOSIMULATION) @echo "🔍 Running evmd module tests..." - @cd evmd && go test -race -tags=test -mod=readonly -timeout=15m $(PACKAGES_EVMD) + @cd evmd && go test -race -tags=test -mod=readonly -timeout=30m $(PACKAGES_EVMD) run-tests: ifneq (,$(shell which tparse 2>/dev/null)) @@ -216,7 +216,7 @@ lint: lint-go lint-python lint-contracts lint-go: @echo "--> Running linter" @go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(golangci_version) - @$(golangci_lint_cmd) run --timeout=15m + @$(golangci_lint_cmd) run --timeout=30m lint-python: find . -name "*.py" -type f -not -path "*/node_modules/*" | xargs pylint @@ -227,7 +227,7 @@ lint-contracts: lint-fix: @go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(golangci_version) - @$(golangci_lint_cmd) run --timeout=15m --fix + @$(golangci_lint_cmd) run --timeout=30m --fix lint-fix-contracts: solhint --fix contracts/**/*.sol @@ -378,33 +378,30 @@ test-rpc-compat-stop: .PHONY: localnet-start localnet-stop localnet-build-env localnet-build-nodes test-rpc-compat test-rpc-compat-stop -test-system: build-v04 build +test-system: build-v05 build mkdir -p ./tests/systemtests/binaries/ cp $(BUILDDIR)/evmd ./tests/systemtests/binaries/ cd tests/systemtests/Counter && forge build $(MAKE) -C tests/systemtests test -# V04_REF is the fork's v0.4.0 release state (last commit before the v0.5.0 -# upgrade work began). This fork has no upstream-style v0.4.x git tag, so the -# legacy binary for the v0.4.0-to-v0.5.0 upgrade test is built from this commit. -V04_REF ?= b5053b7e -V04_WORKTREE ?= $(BUILDDIR)/v04-src -build-v04: - mkdir -p ./tests/systemtests/binaries/v0.4 +# V05_REF is the fork's v0.5.x release state (the last commit before the v0.6.0 +# upgrade work began) — the "from" version for the v0.5.0-to-v0.6.0 upgrade +# system test. A commit hash is used (not the upstream `v0.5.1` tag) because that +# tag lives in cosmos/evm, not in this fork, so it is unavailable in CI even with +# fetch-tags. Mirrors V04_REF. +V05_REF ?= 96231e7a +V05_WORKTREE ?= $(BUILDDIR)/v05-src +build-v05: + mkdir -p ./tests/systemtests/binaries/v0.5 # Build the legacy binary in a throwaway worktree so the main checkout is - # never disturbed (the old recipe ran `git checkout $(V04_REF)` on the - # working tree, which breaks as soon as the build dirties go.mod). - rm -rf $(V04_WORKTREE) - git worktree add --force --detach $(V04_WORKTREE) $(V04_REF) - # The v0.4.0 ref predates later fixes and will not build as-is: - # - evmd/cmd/evmd/config/config.go has a duplicate map key (compile error), - # fixed via the patch below; - # - evmd/go.mod is out of sync with the root module graph (root requires a - # newer ibc-go than evmd pins), reconciled with GOFLAGS=-mod=mod. - cd $(V04_WORKTREE) && git apply $(CURDIR)/tests/systemtests/patches/v04-build-fixes.patch - cd $(V04_WORKTREE)/evmd && CGO_ENABLED="1" GOFLAGS=-mod=mod \ - go build -o $(CURDIR)/tests/systemtests/binaries/v0.4/evmd ./cmd/evmd - git worktree remove --force $(V04_WORKTREE) + # never disturbed (a `git checkout $(V05_REF)` on the working tree breaks as + # soon as the build dirties go.mod). Mirrors the isolated v0.4 legacy build + # introduced in the audit/CI fixes. + rm -rf $(V05_WORKTREE) + git worktree add --force --detach $(V05_WORKTREE) $(V05_REF) + cd $(V05_WORKTREE)/evmd && CGO_ENABLED="1" GOFLAGS=-mod=mod \ + go build -o $(CURDIR)/tests/systemtests/binaries/v0.5/evmd ./cmd/evmd + git worktree remove --force $(V05_WORKTREE) mocks: @echo "--> generating mocks" diff --git a/ante/cosmos/eip712.go b/ante/cosmos/eip712.go index e800cee0f..bdea0e7fa 100644 --- a/ante/cosmos/eip712.go +++ b/ante/cosmos/eip712.go @@ -25,7 +25,7 @@ import ( authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" ) -var evmCodec codec.ProtoCodecMarshaler +var evmCodec codec.Codec func init() { registry := codectypes.NewInterfaceRegistry() @@ -177,7 +177,7 @@ func VerifySignature( return errorsmod.Wrap(errortypes.ErrNoSignatures, "tx doesn't contain any msgs to verify signature") } - txBytes := legacytx.StdSignBytes( + txBytes := legacytx.StdSignBytes( //nolint:staticcheck // checking legacy type signerData.ChainID, signerData.AccountNumber, signerData.Sequence, diff --git a/ante/evm/fee_checker.go b/ante/evm/fee_checker.go index a31c1f013..a65d38551 100644 --- a/ante/evm/fee_checker.go +++ b/ante/evm/fee_checker.go @@ -93,7 +93,7 @@ func FeeChecker( } feeCoins := feeTx.GetFee() - feeAmtDec := sdkmath.LegacyNewDecFromInt(feeCoins.AmountOfNoDenomValidation(denom)) + feeAmtDec := sdkmath.LegacyNewDecFromInt(feeCoins.AmountOfNoDenomValidation(denom)) //nolint:staticcheck // checking legacy type feeCap := feeAmtDec.QuoInt(gas) if feeCap.LT(baseFee) { diff --git a/contracts/contract_creation_tester.go b/contracts/contract_creation_tester.go new file mode 100644 index 000000000..8732ea4a8 --- /dev/null +++ b/contracts/contract_creation_tester.go @@ -0,0 +1,10 @@ +package contracts + +import ( + contractutils "github.com/cosmos/evm/contracts/utils" + evmtypes "github.com/cosmos/evm/x/vm/types" +) + +func LoadContractCreationTester() (evmtypes.CompiledContract, error) { + return contractutils.LoadContractFromJSONFile("solidity/ContractCreationTester.json") +} diff --git a/contracts/erc20_with_native_transfers.go b/contracts/erc20_with_native_transfers.go new file mode 100644 index 000000000..9509af1e9 --- /dev/null +++ b/contracts/erc20_with_native_transfers.go @@ -0,0 +1,10 @@ +package contracts + +import ( + contractutils "github.com/cosmos/evm/contracts/utils" + evmtypes "github.com/cosmos/evm/x/vm/types" +) + +func LoadERC20WithNativeTransfers() (evmtypes.CompiledContract, error) { + return contractutils.LoadContractFromJSONFile("solidity/ERC20WithNativeTransfers.json") +} diff --git a/contracts/ics20_sequential_precompile_calls.go b/contracts/ics20_sequential_precompile_calls.go new file mode 100644 index 000000000..e25291bcf --- /dev/null +++ b/contracts/ics20_sequential_precompile_calls.go @@ -0,0 +1,10 @@ +package contracts + +import ( + contractutils "github.com/cosmos/evm/contracts/utils" + evmtypes "github.com/cosmos/evm/x/vm/types" +) + +func LoadSequentialICS20Sender() (evmtypes.CompiledContract, error) { + return contractutils.LoadContractFromJSONFile("solidity/SequentialICS20Sender.json") +} diff --git a/contracts/ics20_transfer_tester.go b/contracts/ics20_transfer_tester.go new file mode 100644 index 000000000..b89020089 --- /dev/null +++ b/contracts/ics20_transfer_tester.go @@ -0,0 +1,10 @@ +package contracts + +import ( + contractutils "github.com/cosmos/evm/contracts/utils" + evmtypes "github.com/cosmos/evm/x/vm/types" +) + +func LoadICS20TransferTester() (evmtypes.CompiledContract, error) { + return contractutils.LoadContractFromJSONFile("solidity/ICS20TransferTester.json") +} diff --git a/contracts/sequential_operations_tester.go b/contracts/sequential_operations_tester.go new file mode 100644 index 000000000..f3659e309 --- /dev/null +++ b/contracts/sequential_operations_tester.go @@ -0,0 +1,10 @@ +package contracts + +import ( + contractutils "github.com/cosmos/evm/contracts/utils" + evmtypes "github.com/cosmos/evm/x/vm/types" +) + +func LoadSequentialOperationsTester() (evmtypes.CompiledContract, error) { + return contractutils.LoadContractFromJSONFile("solidity/SequentialOperationsTester.json") +} diff --git a/contracts/solidity/ContractCreationTester.json b/contracts/solidity/ContractCreationTester.json new file mode 100644 index 000000000..3b12708cc --- /dev/null +++ b/contracts/solidity/ContractCreationTester.json @@ -0,0 +1,282 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "ContractCreationTester", + "sourceName": "solidity/ContractCreationTester.sol", + "abi": [ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "contractAddr", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "ContractCreated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "string", + "name": "operation", + "type": "string" + }, + { + "indexed": false, + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "name": "OperationCompleted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "ValueSent", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "creationValue", + "type": "uint256" + } + ], + "name": "createAndRevert", + "outputs": [ + { + "internalType": "contract SimpleReceiver", + "name": "", + "type": "address" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "createdContracts", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "getCreatedContract", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getCreatedContractsCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "validatorAddr", + "type": "string" + }, + { + "internalType": "uint256", + "name": "delegateAmount1", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "creationValue", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "delegateAmount2", + "type": "uint256" + } + ], + "name": "scenario4_delegateCreateDelegate", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "validatorAddr", + "type": "string" + }, + { + "internalType": "uint256", + "name": "delegateAmount1", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "creationValue", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "delegateAmount2", + "type": "uint256" + } + ], + "name": "scenario5_delegateCreateRevertDelegate", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "creationValue1", + "type": "uint256" + }, + { + "internalType": "string", + "name": "validatorAddr", + "type": "string" + }, + { + "internalType": "uint256", + "name": "delegateAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "creationValue2", + "type": "uint256" + } + ], + "name": "scenario6_createRevertDelegateCreateRevert", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "creationValue", + "type": "uint256" + }, + { + "internalType": "string", + "name": "validatorAddr", + "type": "string" + }, + { + "internalType": "uint256", + "name": "delegateAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "sendAmount", + "type": "uint256" + } + ], + "name": "scenario7_createDelegateRevertSend", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "revertCreationValue", + "type": "uint256" + }, + { + "internalType": "string", + "name": "validatorAddr", + "type": "string" + }, + { + "internalType": "uint256", + "name": "delegateAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "successCreationValue", + "type": "uint256" + } + ], + "name": "scenario8_createRevertDelegateCreateSuccess", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } + ], + "bytecode": "0x608080604052346100165761104a908161001c8239f35b600080fdfe60406080815260048036101561001f575b5050361561001d57600080fd5b005b600091823560e01c80630881139714610acd5780637a0aaa5b146108cd578381637d5b95391461079f578163aabf1086146105ba57508063b203934e1461059d578063c242737f1461051f578063c53e5ae314610470578063e71b671a146102765763edab9299146100915750610010565b829161009c36610c00565b8596919651946353266bbb60e01b9081875264e8d4a5100096879581806100cc60209b8c94048b30888501610c51565b038161080098895af1801561026c5761024f575b50875198600080516020610ff58339815191528a806100ff839d610cad565b0390a188516101f3808201908282106001600160401b0383111761023c578a9897969593898f969394818f94610182978391610de283398a815203019083f091826101fc5750505060808160068f9351918083528201526563726561746560d01b6060820152868b820152a15b8a51988997889687958652049130908501610c51565b03925af180156101f2576101c4575b5050516040808252600990820152683232b632b3b0ba329960b91b60608201526001602082015280608081015b0390a180f35b816101e392903d106101eb575b6101db8183610b28565b810190610c39565b503880610191565b503d6101d1565b83513d87823e3d90fd5b6001600160a01b0390921692600080516020610fd5833981519152929061022285610cd8565b51908152a28b8b518061023481610d15565b0390a161016c565b634e487b7160e01b8e526041855260248efd5b61026590883d8a116101eb576101db8183610b28565b50386100e0565b89513d8d823e3d90fd5b5061028036610bc6565b92919064e8d4a5100085519163c53e5ae360e01b83528488840152602094838680826024829886305af18c9281610441575b506103f257505050600080516020610ff58339815191528751806102d581610d85565b0390a15b6102f8875194859384936353266bbb60e01b85520490308b8501610c51565b0381896108005af180156103e8576103cb575b50825193600080516020610ff5833981519152858061032a8398610db7565b0390a18351906101f3808301918383106001600160401b038411176103b8575083918391610de2833988815203019083f080156101f2576001600160a01b031691600080516020610fd5833981519152919061038584610cd8565b8451908152a25160408082526007908201526631b932b0ba329960c91b60608201526001602082015280608081016101be565b634e487b7160e01b895260419052602488fd5b6103e190823d84116101eb576101db8183610b28565b503861030b565b84513d88823e3d90fd5b6001600160a01b0390911691600080516020610fd5833981519152919061041884610cd8565b8a51908152a2600080516020610ff583398151915287518061043981610d5c565b0390a16102d9565b610462919350823d8411610469575b61045a8183610b28565b810190610d3d565b91386102b2565b503d610450565b5082602036600319011261051c5781516101f3808201908282106001600160401b03831117610509576020918391610de283398481520301908435f0156104fe57815162461bcd60e51b8152602081850152602160248201527f496e74656e74696f6e616c20726576657274206166746572206372656174696f6044820152603760f91b6064820152608490fd5b9051903d90823e3d90fd5b634e487b7160e01b845260418652602484fd5b80fd5b5091903461059957602036600319011261059957803591548210156105605760208361054a84610afb565b905491519160018060a01b039160031b1c168152f35b606490602084519162461bcd60e51b83528201526013602482015272496e646578206f7574206f6620626f756e647360681b6044820152fd5b5080fd5b838234610599578160031936011261059957602091549051908152f35b9190506105c636610bc6565b90949291845164e8d4a5100063c53e5ae360e01b97888352858a84015260209583878082602482988d305af18a9281610780575b5061073157505050600080516020610ff583398151915288518061061d81610d85565b0390a15b610640885194859384936353266bbb60e01b85520490308d8501610c51565b0381876108005af180156107275761070a575b5080845196600080516020610ff58339815191528880610673839b610db7565b0390a18551968752860152818560248186305af1809584966106eb575b506106c557506080935060108351938085528401526f18dc99585d194c97dc995d995c9d195960821b6060840152820152a180f35b92936001600160a01b031692600080516020610fd5833981519152925061038584610cd8565b610703919650833d85116104695761045a8183610b28565b9438610690565b61072090833d85116101eb576101db8183610b28565b5038610653565b85513d86823e3d90fd5b6001600160a01b0390911691600080516020610fd5833981519152919061075784610cd8565b8b51908152a2600080516020610ff583398151915288518061077881610d5c565b0390a1610621565b610798919350823d84116104695761045a8183610b28565b91386105fa565b9290506107ab36610c00565b8596919651946353266bbb60e01b9081875264e8d4a5100096879581806107db60209b8c94048b30888501610c51565b038161080098895af1801561026c576108b0575b50875198600080516020610ff58339815191528a8061080e839d610cad565b0390a188516101f3808201908282106001600160401b0383111761023c57828e96959493928c92610de2833987815203019082f080156108a657899796959493926101829290916001600160a01b031690600080516020610fd5833981519152908a908e61087b85610cd8565b51908152a28b8b518061088d81610d15565b0390a18a51988997889687958652049130908501610c51565b8a513d86823e3d90fd5b6108c690883d8a116101eb576101db8183610b28565b50386107ef565b506108d736610bc6565b9094918451936101f3808601908682106001600160401b03831117610aba57908691610de28339898152602096879103019082f08015610ab0576001600160a01b03169291908390600080516020610fd583398151915290879061093a84610cd8565b8951908152a28464e8d4a51000875198600080516020610ff58339815191528a80610965839d610d15565b0390a1610987895194859384936353266bbb60e01b85520490308a8501610c51565b03818b6108005af19081610a93575b50610a7e5785608086518781526008888201526764656c656761746560c01b60608201528987820152a15b8680808084865af13d15610a79573d6109d981610b5f565b906109e688519283610b28565b815288863d92013e5b15610a42579183917ffb17d0033e42e6a76759d2c7c2795a304dfbba80679160ae60dc91aad4966e52869560019560809851908152a2835193808552840152631cd95b9960e21b6060840152820152a180f35b845162461bcd60e51b8152808401859052601160248201527015985b1d59481cd95b990819985a5b1959607a1b6044820152606490fd5b6109ef565b85855180610a8b81610db7565b0390a16109c1565b610aa990863d88116101eb576101db8183610b28565b5038610996565b86513d8a823e3d90fd5b634e487b7160e01b8a526041865260248afd5b509034610af7576020366003190112610af7573591805483101561051c575061054a602092610afb565b8280fd5b906000918254811015610b145782805260208320019190565b634e487b7160e01b83526032600452602483fd5b90601f801991011681019081106001600160401b03821117610b4957604052565b634e487b7160e01b600052604160045260246000fd5b6001600160401b038111610b4957601f01601f191660200190565b81601f82011215610bc157803590610b9182610b5f565b92610b9f6040519485610b28565b82845260208383010111610bc157816000926020809301838601378301015290565b600080fd5b906080600319830112610bc15760043591602435906001600160401b038211610bc157610bf591600401610b7a565b906044359060643590565b6080600319820112610bc157600435906001600160401b038211610bc157610c2a91600401610b7a565b90602435906044359060643590565b90816020910312610bc157518015158103610bc15790565b9392919060018060a01b03168452602060608186015281519182606087015260005b838110610c995750505060808160008260409488010152601f8019910116850101930152565b818101830151878201608001528201610c73565b9060408252600960408301526864656c65676174653160b81b60608301526001602060808401930152565b600054600160401b811015610b4957806001610cf79201600055610afb565b819291549060031b9160018060a01b03809116831b921b1916179055565b9060408252600660408301526563726561746560d01b60608301526001602060808401930152565b90816020910312610bc157516001600160a01b0381168103610bc15790565b906040825260076040830152666372656174653160c81b60608301526001602060808401930152565b9060408252601060408301526f18dc99585d194c57dc995d995c9d195960821b60608301526000602060808401930152565b9060408252600860408301526764656c656761746560c01b6060830152600160206080840193015256fe6080601f6101f338819003918201601f19168301916001600160401b038311848410176100e0578084926020946040528339810103126100db575180151581036100db57610096573360018060a01b03196001541617600155346000556040513481527fb263f5c1dda4b5b44a8d5658a105c64b6ec61c79463b79a1d0052a834d00fdc460203392a260405160fc90816100f78239f35b60405162461bcd60e51b815260206004820152601e60248201527f496e74656e74696f6e616c20636f6e7374727563746f722072657665727400006044820152606490fd5b600080fd5b634e487b7160e01b600052604160045260246000fdfe60808060405260043610156040575b503615601957600080fd5b600054348101809111602a57600055005b634e487b7160e01b600052601160045260246000fd5b600090813560e01c90816302d05d3f14609f575080632096525514608457633fa4f24503600e57346081578060031936011260815760209054604051908152f35b80fd5b50346081578060031936011260815760209054604051908152f35b90503460c2578160031936011260c2576001546001600160a01b03168152602090f35b5080fdfea2646970667358221220d21dae735f8bd27e93e963f6caaa38336d387a65ef5055b60b5b67a5ea9bc79064736f6c634300081400331dc05c1d6a563dddb6c22082af72b54ec2f0207ceb55db5d13cdabc208f303a99f4d25774676d497fe3d8c1e43709b68b186fad01b05798c1410a178ff4ed7d0a2646970667358221220d240419af0d6f7c5730c6adddac161e3c233245911fa32bb94dbc84c12c08f3f64736f6c63430008140033", + "deployedBytecode": "0x60406080815260048036101561001f575b5050361561001d57600080fd5b005b600091823560e01c80630881139714610acd5780637a0aaa5b146108cd578381637d5b95391461079f578163aabf1086146105ba57508063b203934e1461059d578063c242737f1461051f578063c53e5ae314610470578063e71b671a146102765763edab9299146100915750610010565b829161009c36610c00565b8596919651946353266bbb60e01b9081875264e8d4a5100096879581806100cc60209b8c94048b30888501610c51565b038161080098895af1801561026c5761024f575b50875198600080516020610ff58339815191528a806100ff839d610cad565b0390a188516101f3808201908282106001600160401b0383111761023c578a9897969593898f969394818f94610182978391610de283398a815203019083f091826101fc5750505060808160068f9351918083528201526563726561746560d01b6060820152868b820152a15b8a51988997889687958652049130908501610c51565b03925af180156101f2576101c4575b5050516040808252600990820152683232b632b3b0ba329960b91b60608201526001602082015280608081015b0390a180f35b816101e392903d106101eb575b6101db8183610b28565b810190610c39565b503880610191565b503d6101d1565b83513d87823e3d90fd5b6001600160a01b0390921692600080516020610fd5833981519152929061022285610cd8565b51908152a28b8b518061023481610d15565b0390a161016c565b634e487b7160e01b8e526041855260248efd5b61026590883d8a116101eb576101db8183610b28565b50386100e0565b89513d8d823e3d90fd5b5061028036610bc6565b92919064e8d4a5100085519163c53e5ae360e01b83528488840152602094838680826024829886305af18c9281610441575b506103f257505050600080516020610ff58339815191528751806102d581610d85565b0390a15b6102f8875194859384936353266bbb60e01b85520490308b8501610c51565b0381896108005af180156103e8576103cb575b50825193600080516020610ff5833981519152858061032a8398610db7565b0390a18351906101f3808301918383106001600160401b038411176103b8575083918391610de2833988815203019083f080156101f2576001600160a01b031691600080516020610fd5833981519152919061038584610cd8565b8451908152a25160408082526007908201526631b932b0ba329960c91b60608201526001602082015280608081016101be565b634e487b7160e01b895260419052602488fd5b6103e190823d84116101eb576101db8183610b28565b503861030b565b84513d88823e3d90fd5b6001600160a01b0390911691600080516020610fd5833981519152919061041884610cd8565b8a51908152a2600080516020610ff583398151915287518061043981610d5c565b0390a16102d9565b610462919350823d8411610469575b61045a8183610b28565b810190610d3d565b91386102b2565b503d610450565b5082602036600319011261051c5781516101f3808201908282106001600160401b03831117610509576020918391610de283398481520301908435f0156104fe57815162461bcd60e51b8152602081850152602160248201527f496e74656e74696f6e616c20726576657274206166746572206372656174696f6044820152603760f91b6064820152608490fd5b9051903d90823e3d90fd5b634e487b7160e01b845260418652602484fd5b80fd5b5091903461059957602036600319011261059957803591548210156105605760208361054a84610afb565b905491519160018060a01b039160031b1c168152f35b606490602084519162461bcd60e51b83528201526013602482015272496e646578206f7574206f6620626f756e647360681b6044820152fd5b5080fd5b838234610599578160031936011261059957602091549051908152f35b9190506105c636610bc6565b90949291845164e8d4a5100063c53e5ae360e01b97888352858a84015260209583878082602482988d305af18a9281610780575b5061073157505050600080516020610ff583398151915288518061061d81610d85565b0390a15b610640885194859384936353266bbb60e01b85520490308d8501610c51565b0381876108005af180156107275761070a575b5080845196600080516020610ff58339815191528880610673839b610db7565b0390a18551968752860152818560248186305af1809584966106eb575b506106c557506080935060108351938085528401526f18dc99585d194c97dc995d995c9d195960821b6060840152820152a180f35b92936001600160a01b031692600080516020610fd5833981519152925061038584610cd8565b610703919650833d85116104695761045a8183610b28565b9438610690565b61072090833d85116101eb576101db8183610b28565b5038610653565b85513d86823e3d90fd5b6001600160a01b0390911691600080516020610fd5833981519152919061075784610cd8565b8b51908152a2600080516020610ff583398151915288518061077881610d5c565b0390a1610621565b610798919350823d84116104695761045a8183610b28565b91386105fa565b9290506107ab36610c00565b8596919651946353266bbb60e01b9081875264e8d4a5100096879581806107db60209b8c94048b30888501610c51565b038161080098895af1801561026c576108b0575b50875198600080516020610ff58339815191528a8061080e839d610cad565b0390a188516101f3808201908282106001600160401b0383111761023c57828e96959493928c92610de2833987815203019082f080156108a657899796959493926101829290916001600160a01b031690600080516020610fd5833981519152908a908e61087b85610cd8565b51908152a28b8b518061088d81610d15565b0390a18a51988997889687958652049130908501610c51565b8a513d86823e3d90fd5b6108c690883d8a116101eb576101db8183610b28565b50386107ef565b506108d736610bc6565b9094918451936101f3808601908682106001600160401b03831117610aba57908691610de28339898152602096879103019082f08015610ab0576001600160a01b03169291908390600080516020610fd583398151915290879061093a84610cd8565b8951908152a28464e8d4a51000875198600080516020610ff58339815191528a80610965839d610d15565b0390a1610987895194859384936353266bbb60e01b85520490308a8501610c51565b03818b6108005af19081610a93575b50610a7e5785608086518781526008888201526764656c656761746560c01b60608201528987820152a15b8680808084865af13d15610a79573d6109d981610b5f565b906109e688519283610b28565b815288863d92013e5b15610a42579183917ffb17d0033e42e6a76759d2c7c2795a304dfbba80679160ae60dc91aad4966e52869560019560809851908152a2835193808552840152631cd95b9960e21b6060840152820152a180f35b845162461bcd60e51b8152808401859052601160248201527015985b1d59481cd95b990819985a5b1959607a1b6044820152606490fd5b6109ef565b85855180610a8b81610db7565b0390a16109c1565b610aa990863d88116101eb576101db8183610b28565b5038610996565b86513d8a823e3d90fd5b634e487b7160e01b8a526041865260248afd5b509034610af7576020366003190112610af7573591805483101561051c575061054a602092610afb565b8280fd5b906000918254811015610b145782805260208320019190565b634e487b7160e01b83526032600452602483fd5b90601f801991011681019081106001600160401b03821117610b4957604052565b634e487b7160e01b600052604160045260246000fd5b6001600160401b038111610b4957601f01601f191660200190565b81601f82011215610bc157803590610b9182610b5f565b92610b9f6040519485610b28565b82845260208383010111610bc157816000926020809301838601378301015290565b600080fd5b906080600319830112610bc15760043591602435906001600160401b038211610bc157610bf591600401610b7a565b906044359060643590565b6080600319820112610bc157600435906001600160401b038211610bc157610c2a91600401610b7a565b90602435906044359060643590565b90816020910312610bc157518015158103610bc15790565b9392919060018060a01b03168452602060608186015281519182606087015260005b838110610c995750505060808160008260409488010152601f8019910116850101930152565b818101830151878201608001528201610c73565b9060408252600960408301526864656c65676174653160b81b60608301526001602060808401930152565b600054600160401b811015610b4957806001610cf79201600055610afb565b819291549060031b9160018060a01b03809116831b921b1916179055565b9060408252600660408301526563726561746560d01b60608301526001602060808401930152565b90816020910312610bc157516001600160a01b0381168103610bc15790565b906040825260076040830152666372656174653160c81b60608301526001602060808401930152565b9060408252601060408301526f18dc99585d194c57dc995d995c9d195960821b60608301526000602060808401930152565b9060408252600860408301526764656c656761746560c01b6060830152600160206080840193015256fe6080601f6101f338819003918201601f19168301916001600160401b038311848410176100e0578084926020946040528339810103126100db575180151581036100db57610096573360018060a01b03196001541617600155346000556040513481527fb263f5c1dda4b5b44a8d5658a105c64b6ec61c79463b79a1d0052a834d00fdc460203392a260405160fc90816100f78239f35b60405162461bcd60e51b815260206004820152601e60248201527f496e74656e74696f6e616c20636f6e7374727563746f722072657665727400006044820152606490fd5b600080fd5b634e487b7160e01b600052604160045260246000fdfe60808060405260043610156040575b503615601957600080fd5b600054348101809111602a57600055005b634e487b7160e01b600052601160045260246000fd5b600090813560e01c90816302d05d3f14609f575080632096525514608457633fa4f24503600e57346081578060031936011260815760209054604051908152f35b80fd5b50346081578060031936011260815760209054604051908152f35b90503460c2578160031936011260c2576001546001600160a01b03168152602090f35b5080fdfea2646970667358221220d21dae735f8bd27e93e963f6caaa38336d387a65ef5055b60b5b67a5ea9bc79064736f6c634300081400331dc05c1d6a563dddb6c22082af72b54ec2f0207ceb55db5d13cdabc208f303a99f4d25774676d497fe3d8c1e43709b68b186fad01b05798c1410a178ff4ed7d0a2646970667358221220d240419af0d6f7c5730c6adddac161e3c233245911fa32bb94dbc84c12c08f3f64736f6c63430008140033", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/contracts/solidity/ContractCreationTester.sol b/contracts/solidity/ContractCreationTester.sol new file mode 100644 index 000000000..9887bf69b --- /dev/null +++ b/contracts/solidity/ContractCreationTester.sol @@ -0,0 +1,237 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./precompiles/staking/StakingI.sol"; +import "./precompiles/common/Types.sol"; + +/** + * @dev Simple contract that can be created and receive value + */ +contract SimpleReceiver { + uint256 public value; + address public creator; + + event ReceiverCreated(address indexed creator, uint256 initialValue); + + constructor(bool shouldRevert) payable { + if (shouldRevert) { + revert("Intentional constructor revert"); + } + creator = msg.sender; + value = msg.value; + emit ReceiverCreated(msg.sender, msg.value); + } + + receive() external payable { + value += msg.value; + } + + function getValue() external view returns (uint256) { + return value; + } +} + +/** + * @dev Contract to test contract creation with precompile calls + * Tests scenarios 4-7 with various combinations of CREATE and precompile operations + */ +contract ContractCreationTester { + event OperationCompleted(string operation, bool success); + event ContractCreated(address indexed contractAddr, uint256 value); + event ValueSent(address indexed recipient, uint256 amount); + + address[] public createdContracts; + + /// @dev Helper function that creates a contract and then reverts + /// @dev Used for testing reverted contract creation scenarios + function createAndRevert(uint256 creationValue) public payable returns (SimpleReceiver) { + SimpleReceiver newContract = new SimpleReceiver{value: creationValue}(false); + revert("Intentional revert after creation"); + } + + /// @dev Scenario 4: Precompile -> Create Contract -> Precompile + function scenario4_delegateCreateDelegate( + string memory validatorAddr, + uint256 delegateAmount1, + uint256 creationValue, + uint256 delegateAmount2 + ) external payable { + // 1. Precompile call (delegate) - convert from wei to base denom + uint256 delegateAmount1BaseDenom = delegateAmount1 / 1e12; + STAKING_CONTRACT.delegate( + address(this), + validatorAddr, + delegateAmount1BaseDenom + ); + emit OperationCompleted("delegate1", true); + + // 2. Create another contract with value (shouldRevert = false) + SimpleReceiver newContract = new SimpleReceiver{value: creationValue}(false); + createdContracts.push(address(newContract)); + emit ContractCreated(address(newContract), creationValue); + emit OperationCompleted("create", true); + + // 3. Precompile call (delegate again) - convert from wei to base denom + uint256 delegateAmount2BaseDenom = delegateAmount2 / 1e12; + STAKING_CONTRACT.delegate( + address(this), + validatorAddr, + delegateAmount2BaseDenom + ); + emit OperationCompleted("delegate2", true); + } + + /// @dev Scenario 5: Precompile -> Create Contract (reverted & caught) -> Precompile + function scenario5_delegateCreateRevertDelegate( + string memory validatorAddr, + uint256 delegateAmount1, + uint256 creationValue, + uint256 delegateAmount2 + ) external payable { + // 1. Precompile call (delegate) - convert from wei to base denom + uint256 delegateAmount1BaseDenom = delegateAmount1 / 1e12; + STAKING_CONTRACT.delegate( + address(this), + validatorAddr, + delegateAmount1BaseDenom + ); + emit OperationCompleted("delegate1", true); + + // 2. Try to create contract (will revert if insufficient value, catch it) + try new SimpleReceiver{value: creationValue}(false) returns (SimpleReceiver newContract) { + createdContracts.push(address(newContract)); + emit ContractCreated(address(newContract), creationValue); + emit OperationCompleted("create", true); + } catch { + emit OperationCompleted("create", false); + } + + // 3. Precompile call (delegate again) - convert from wei to base denom + uint256 delegateAmount2BaseDenom = delegateAmount2 / 1e12; + STAKING_CONTRACT.delegate( + address(this), + validatorAddr, + delegateAmount2BaseDenom + ); + emit OperationCompleted("delegate2", true); + } + + /// @dev Scenario 6: Create+Revert (caught) -> Precompile -> Create+Revert (caught) + /// @dev Creates fail because the helper function reverts after creation, testing auto-flush with reverted creations + function scenario6_createRevertDelegateCreateRevert( + uint256 creationValue1, + string memory validatorAddr, + uint256 delegateAmount, + uint256 creationValue2 + ) external payable { + // 1. Try to create contract (will revert after creation, catch it) + try this.createAndRevert(creationValue1) returns (SimpleReceiver newContract1) { + // This won't execute because createAndRevert reverts + createdContracts.push(address(newContract1)); + emit ContractCreated(address(newContract1), creationValue1); + emit OperationCompleted("create1", true); + } catch { + emit OperationCompleted("create1_reverted", false); + } + + // 2. Precompile call - convert from wei to base denom + uint256 delegateAmountBaseDenom = delegateAmount / 1e12; + STAKING_CONTRACT.delegate( + address(this), + validatorAddr, + delegateAmountBaseDenom + ); + emit OperationCompleted("delegate", true); + + // 3. Try to create contract again (will revert after creation, catch it) + try this.createAndRevert(creationValue2) returns (SimpleReceiver newContract2) { + // This won't execute because createAndRevert reverts + createdContracts.push(address(newContract2)); + emit ContractCreated(address(newContract2), creationValue2); + emit OperationCompleted("create2", true); + } catch { + emit OperationCompleted("create2_reverted", false); + } + } + + /// @dev Scenario 7: Create+Send -> Precompile (reverted & caught) -> Send more + function scenario7_createDelegateRevertSend( + uint256 creationValue, + string memory validatorAddr, + uint256 delegateAmount, + uint256 sendAmount + ) external payable { + // 1. Create contract and send it value (shouldRevert = false) + SimpleReceiver newContract = new SimpleReceiver{value: creationValue}(false); + createdContracts.push(address(newContract)); + emit ContractCreated(address(newContract), creationValue); + emit OperationCompleted("create", true); + + // 2. Precompile call (delegate) - convert from wei to base denom, reverted and caught + uint256 delegateAmountBaseDenom = delegateAmount / 1e12; + try STAKING_CONTRACT.delegate( + address(this), + validatorAddr, + delegateAmountBaseDenom + ) { + emit OperationCompleted("delegate", true); + } catch { + emit OperationCompleted("delegate", false); + } + + // 3. Send more value to the created contract + (bool success, ) = address(newContract).call{value: sendAmount}(""); + require(success, "Value send failed"); + emit ValueSent(address(newContract), sendAmount); + emit OperationCompleted("send", true); + } + + /// @dev Scenario 8: Create+Revert (caught) -> Delegate -> Create+Success + /// @dev Tests that reverted creation doesn't prevent successful creation after delegation + function scenario8_createRevertDelegateCreateSuccess( + uint256 revertCreationValue, + string memory validatorAddr, + uint256 delegateAmount, + uint256 successCreationValue + ) external payable { + // 1. Try to create contract (will revert after creation, catch it) + try this.createAndRevert{value: revertCreationValue}(revertCreationValue) returns ( + SimpleReceiver newContract1 + ) { + // This won't execute because createAndRevert reverts + createdContracts.push(address(newContract1)); + emit ContractCreated(address(newContract1), revertCreationValue); + emit OperationCompleted("create1", true); + } catch { + emit OperationCompleted("create1_reverted", false); + } + + // 2. Precompile call - convert from wei to base denom + uint256 delegateAmountBaseDenom = delegateAmount / 1e12; + STAKING_CONTRACT.delegate( + address(this), + validatorAddr, + delegateAmountBaseDenom + ); + emit OperationCompleted("delegate", true); + + // 3. Create contract successfully (shouldRevert = false) + SimpleReceiver newContract2 = new SimpleReceiver{value: successCreationValue}(false); + createdContracts.push(address(newContract2)); + emit ContractCreated(address(newContract2), successCreationValue); + emit OperationCompleted("create2", true); + } + + /// @dev Get count of created contracts + function getCreatedContractsCount() external view returns (uint256) { + return createdContracts.length; + } + + /// @dev Get created contract at index + function getCreatedContract(uint256 index) external view returns (address) { + require(index < createdContracts.length, "Index out of bounds"); + return createdContracts[index]; + } + + receive() external payable {} +} diff --git a/contracts/solidity/ERC20WithNativeTransfers.json b/contracts/solidity/ERC20WithNativeTransfers.json new file mode 100644 index 000000000..6d6ab695a --- /dev/null +++ b/contracts/solidity/ERC20WithNativeTransfers.json @@ -0,0 +1,813 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "ERC20WithNativeTransfers", + "sourceName": "solidity/ERC20WithNativeTransfers.sol", + "abi": [ + { + "inputs": [ + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "symbol", + "type": "string" + }, + { + "internalType": "uint8", + "name": "decimals_", + "type": "uint8" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "BeforeTransferHookTriggered", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "available", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "needed", + "type": "uint256" + } + ], + "name": "ContractBalanceCheck", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "DelegateCompleted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "step", + "type": "uint256" + } + ], + "name": "NativeTransferCompleted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "previousAdminRole", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "newAdminRole", + "type": "bytes32" + } + ], + "name": "RoleAdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "RoleGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "RoleRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [], + "name": "DEFAULT_ADMIN_ROLE", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "MINTER_ROLE", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "burn", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "burnFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_recipient1", + "type": "address" + }, + { + "internalType": "address", + "name": "_recipient2", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_transferAmount", + "type": "uint256" + }, + { + "internalType": "string", + "name": "_validatorAddr", + "type": "string" + }, + { + "internalType": "uint256", + "name": "_delegateAmount", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "_enableHook", + "type": "bool" + } + ], + "name": "configureHook", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "subtractedValue", + "type": "uint256" + } + ], + "name": "decreaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "delegateAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "enableHook", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + } + ], + "name": "getRoleAdmin", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "getRoleMember", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + } + ], + "name": "getRoleMemberCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "grantRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "hasRole", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "addedValue", + "type": "uint256" + } + ], + "name": "increaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "mint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "recipient1", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "recipient2", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "renounceRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "revokeRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "transferAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "validatorAddr", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } + ], + "bytecode": "0x604060808152346200043857620027cc803803806200001e816200043d565b9283398101606082820312620004385781516001600160401b0391908281116200043857816200005091850162000463565b9160209182850151828111620004385786916200006f91870162000463565b9401519360ff8516809503620004385783518281116200034f576005918254916001968784811c941680156200042d575b878510146200032e578190601f94858111620003d9575b508790858311600114620003715760009262000365575b5050600019600383901b1c191690871b1783555b80519384116200034f576006548681811c9116801562000344575b868210146200032e578493838211620002d4575b5050849183116001146200026a576000926200025e575b5050600019600383901b1c191690831b176006555b60008052600081528360002033600052815260ff8460002054161562000221575b60008052818152620001743385600020620004d5565b507f9f2df0fed2c77648de5860a4cc508cd0818c85b8b8a1ab4ceeef8d981c8956a680600052600082528460002033600052825260ff85600020541615620001e4575b60005252620001ca3383600020620004d5565b5060ff196007541617600755516122499081620005638239f35b806000526000825284600020336000528252846000208360ff19825416179055333382600080516020620027ac833981519152600080a4620001b7565b600080526000815283600020336000528152836000208260ff1982541617905533336000600080516020620027ac8339815191528180a46200015e565b01519050388062000128565b90859350601f198316916006600052856000209260005b87828210620002bd5750508411620002a3575b505050811b016006556200013d565b015160001960f88460031b161c1916905538808062000294565b838501518655899790950194938401930162000281565b90919293506006600052856000209084808701821c83019388881062000324575b9187968a93969594929601901c01915b82811062000314575062000111565b6000815586955088910162000305565b93508293620002f5565b634e487b7160e01b600052602260045260246000fd5b90607f1690620000fd565b634e487b7160e01b600052604160045260246000fd5b015190503880620000ce565b90899350601f1983169187600052896000209260005b8b828210620003c25750508411620003a8575b505050811b018355620000e2565b015160001960f88460031b161c191690553880806200039a565b8385015186558d9790950194938401930162000387565b909150856000528760002085808501881c8201928a861062000423575b918b918695949301891c01915b82811062000413575050620000b7565b600081558594508b910162000403565b92508192620003f6565b93607f1693620000a0565b600080fd5b6040519190601f01601f191682016001600160401b038111838210176200034f57604052565b919080601f84011215620004385782516001600160401b0381116200034f5760209062000499601f8201601f191683016200043d565b92818452828287010111620004385760005b818110620004c157508260009394955001015290565b8581018301518482018401528201620004ab565b919060018301600090828252806020526040822054156000146200055c57845494680100000000000000008610156200054857600186018082558610156200053457836040949596828552602085200155549382526020522055600190565b634e487b7160e01b83526032600452602483fd5b634e487b7160e01b83526041600452602483fd5b5092505056fe6080604081815260049182361015610022575b505050361561002057600080fd5b005b600092833560e01c91826301ffc9a714610fe3575081630688b13514610fba57816306fdde0314610f14578163095ea7b314610eea57816318160ddd14610ecc57816323b872dd14610e8f578163248a9ca314610e655781632f2ff15d14610db1578163313ce56714610d8f57816336568abe14610cfd5781633950935114610cad57816340c10f191461083e57816342966c6814610820578163504d27fd1461080157816370a08231146107c957816379cc6790146107995781639010d07c1461075857816391d148541461071257816395d89b41146106435781639f35c7e714610609578163a217fddf146105ee578163a457c2d714610546578163a9059cbb14610515578163aa3744bd146104e8578163b64bdb34146104c9578163ba84e217146104a5578163bd3fddd714610277578163ca15c8731461024f578163d539139314610214578163d547741f146101d2575063dd62ed3e146101875780610012565b346101ce57806003193601126101ce57806020926101a361109f565b6101ab6110ba565b6001600160a01b0391821683526003865283832091168252845220549051908152f35b5080fd5b9190503461021057806003193601126102105761020d913561020860016101f76110ba565b9383875286602052862001546111c7565b6114ef565b80f35b8280fd5b5050346101ce57816003193601126101ce57602090517f9f2df0fed2c77648de5860a4cc508cd0818c85b8b8a1ab4ceeef8d981c8956a68152f35b9050346102105760203660031901126102105760209282913581526001845220549051908152f35b839150346101ce5760c03660031901126101ce5761029361109f565b61029b6110ba565b9060643567ffffffffffffffff928382116104a157366023830112156104a157818501359384116104a1576024923684868501011161049d5760a4359586151580970361049957878052602098888a52808920338a528a5260ff818a2054161561046157505060078054610100600160a81b031916600892831b610100600160a81b031617905580546001600160a01b0319166001600160a01b0392909216919091179055604435600955600a54610352906110d0565b601f811161041d575b508495601f84116001146103b15750948495839495936103a4575b5050508160011b916000199060031b1c191617600a555b608435600b5560ff8019600c5416911617600c5580f35b0101359050848080610376565b91601f19841696600a87528387209387905b898210610403575050846001969798106103e7575b50505050811b01600a5561038d565b60001960f88660031b161c1992010135169055848080806103d8565b8060018497868395968901013581550196019201906103c3565b600a8652868620601f850160051c810191888610610457575b601f0160051c01905b81811061044c575061035b565b86815560010161043f565b9091508190610436565b5162461bcd60e51b8152908101899052601481860152734d75737420686176652061646d696e20726f6c6560601b6044820152606490fd5b8780fd5b8680fd5b8580fd5b5050346101ce57816003193601126101ce5760209060ff600c541690519015158152f35b5050346101ce57816003193601126101ce57602090600b549051908152f35b5050346101ce57816003193601126101ce57600754905160089190911c6001600160a01b03168152602090f35b5050346101ce57806003193601126101ce5760209061053f61053561109f565b6024359033611599565b5160018152f35b905082346105eb57826003193601126105eb5761056161109f565b918360243592338152600360205281812060018060a01b038616825260205220549082821061059a5760208561053f8585038733611a14565b608490602086519162461bcd60e51b8352820152602560248201527f45524332303a2064656372656173656420616c6c6f77616e63652062656c6f77604482015264207a65726f60d81b6064820152fd5b80fd5b5050346101ce57816003193601126101ce5751908152602090f35b5050346101ce57816003193601126101ce57805161063f916106358261062e8161110a565b038361118f565b5191829182611073565b0390f35b5050346101ce57816003193601126101ce5780519082600654610665816110d0565b808552906001908181169081156106ea5750600114610691575b5050506106358261063f94038361118f565b60068352602095507ff652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d3f5b8284106106d7575050508261063f94610635928201019461067f565b80548685018801529286019281016106bb565b61063f97506106359450602092508693915060ff191682840152151560051b8201019461067f565b9050346102105781600319360112610210578160209360ff926107336110ba565b903582528186528282206001600160a01b039091168252855220549151911615158152f35b9050346102105781600319360112610210576020926107839135815260018452826024359120611f7a565b905491519160018060a01b039160031b1c168152f35b5050346101ce573660031901126105eb5761020d6107b561109f565b602435906107c4823383611b16565b611bae565b5050346101ce5760203660031901126101ce5760209181906001600160a01b036107f161109f565b1681526002845220549051908152f35b5050346101ce57816003193601126101ce576020906009549051908152f35b8390346101ce5760203660031901126101ce5761020d903533611bae565b90503461021057816003193601126102105761085861109f565b906024928335917f9f2df0fed2c77648de5860a4cc508cd0818c85b8b8a1ab4ceeef8d981c8956a68652602093868552828720338852855260ff838820541615610c6c576001600160a01b03908116958615610c2b5760ff600c541680610c24575b80610c1c575b610900575b505091859391836108e6836000805160206121948339815191529654611576565b90558585526002835280852082815401905551908152a380f35b60008051602061213483398151915260608593999897959694965189815288878201528585820152a16009548015801580610bff575b610a6e575b5050869750600b969495965480151580610a5b575b610961575b508193959294506108c5565b909193809693955084518080936353266bbb60e01b8252308783015260608683015261098f6064830161110a565b90604483015203818b6108005af1908115610a51578891610a1b575b50156109e75750918391600080516020612194833981519152936000805160206121f48339815191528896600b548451908152a1949338610955565b60119085606494519362461bcd60e51b85528401528201527011195b1959d85d1a5bdb8819985a5b1959607a1b6044820152fd5b90508581813d8311610a4a575b610a32818361118f565b810103126104995751801515810361049957386109ab565b503d610a28565b84513d8a823e3d90fd5b50610a67600a546110d0565b1515610950565b8160011b908282046002141715610bed5747600080516020612154833981519152858051838152848a820152a110610ba25787808080938c60075460081c165af1610ab76120f3565b5015610b61576060878080808c60075460081c16600954908851908152818b820152600189820152600080516020612174833981519152968791a18d600854165af1610b016120f3565b5015610b3157606088996008999798995416600954855191825287820152600285820152a138809796959761093b565b825162461bcd60e51b8152808701869052601d818401526000805160206121d48339815191526044820152606490fd5b601c859185606494519362461bcd60e51b85528401528201527f4669727374206e6174697665207472616e73666572206661696c6564000000006044820152fd5b825162461bcd60e51b81528087018690526032818401526000805160206121b483398151915260448201527172206e6174697665207472616e736665727360701b6064820152608490fd5b634e487b7160e01b8952601187528289fd5b5060075460081c8a16151580610936575089600854161515610936565b5060016108c0565b50876108ba565b601f915085606494519362461bcd60e51b85528401528201527f45524332303a206d696e7420746f20746865207a65726f2061646472657373006044820152fd5b50601d8585606494519362461bcd60e51b85528401528201527f4d7573742068617665206d696e74657220726f6c6520746f206d696e740000006044820152fd5b5050346101ce57806003193601126101ce5761053f602092610cf6610cd061109f565b338352600386528483206001600160a01b03821684528652918490205460243590611576565b9033611a14565b839150346101ce57826003193601126101ce57610d186110ba565b90336001600160a01b03831603610d34579061020d91356114ef565b608490602085519162461bcd60e51b8352820152602f60248201527f416363657373436f6e74726f6c3a2063616e206f6e6c792072656e6f756e636560448201526e103937b632b9903337b91039b2b63360891b6064820152fd5b5050346101ce57816003193601126101ce5760209060ff600754169051908152f35b91905034610210578060031936011261021057610e1a9135906001610dd46110ba565b92808652602090868252610dec8385892001546111c7565b80875286825283872094838060a01b031694858852825260ff848820541615610e1e575b8652528320611f92565b5080f35b8087528682528387208588528252838720805460ff1916841790553385827f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d8a80a4610e10565b90503461021057602036600319011261021057816020936001923581528085522001549051908152f35b5050346101ce5760603660031901126101ce5760209061053f610eb061109f565b610eb86110ba565b60443591610ec7833383611b16565b611599565b90503461021057826003193601126102105760209250549051908152f35b5050346101ce57806003193601126101ce5760209061053f610f0a61109f565b6024359033611a14565b5050346101ce57816003193601126101ce5780519082600554610f36816110d0565b808552906001908181169081156106ea5750600114610f61575050506106358261063f94038361118f565b60058352602095507f036b6384b5eca791c62761152d0c79bb0604c104a5fb6f4eb0703f3154bb3db05b828410610fa7575050508261063f94610635928201019461067f565b8054868501880152928601928101610f8b565b5050346101ce57816003193601126101ce5760085490516001600160a01b039091168152602090f35b849134610210576020366003190112610210573563ffffffff60e01b81168091036102105760209250635a05180f60e01b8114908115611025575b5015158152f35b637965db0b60e01b81149150811561103f575b508361101e565b6301ffc9a760e01b14905083611038565b60005b8381106110635750506000910152565b8181015183820152602001611053565b604091602082526110938151809281602086015260208686019101611050565b601f01601f1916010190565b600435906001600160a01b03821682036110b557565b600080fd5b602435906001600160a01b03821682036110b557565b90600182811c92168015611100575b60208310146110ea57565b634e487b7160e01b600052602260045260246000fd5b91607f16916110df565b600a546000929161111a826110d0565b908181526001928381169081600014611174575060011461113a57505050565b90929350600a6000526020928360002092846000945b8386106111605750505050010190565b805485870183015294019385908201611150565b91935050602093945060ff191683830152151560051b010190565b90601f8019910116810190811067ffffffffffffffff8211176111b157604052565b634e487b7160e01b600052604160045260246000fd5b6000818152602090808252604092838220338352835260ff8483205416156111ef5750505050565b835167ffffffffffffffff91903360608201848111838210176114db578752602a825285820192873685378251156114c757603084538251916001928310156114b3576078602185015360295b838111611449575061140757908751946080860190868210908211176113f3578852604285528685019560603688378551156113df576030875385518210156113df5790607860218701536041915b8183116113715750505061132f57938593611315936113066048946112dd76020b1b1b2b9b9a1b7b73a3937b61d1030b1b1b7bab73a1604d1b9961132b9b519a8b978801525180926037880190611050565b8401917001034b99036b4b9b9b4b733903937b6329607d1b603784015251809386840190611050565b0103602881018552018361118f565b5162461bcd60e51b815291829160048301611073565b0390fd5b60648587519062461bcd60e51b825280600483015260248201527f537472696e67733a20686578206c656e67746820696e73756666696369656e746044820152fd5b909192600f811660108110156113cb576f181899199a1a9b1b9c1cb0b131b232b360811b901a6113a18589611f53565b5360041c9280156113b75760001901919061128b565b634e487b7160e01b82526011600452602482fd5b634e487b7160e01b83526032600452602483fd5b634e487b7160e01b81526032600452602490fd5b634e487b7160e01b87526041600452602487fd5b60648789519062461bcd60e51b825280600483015260248201527f537472696e67733a20686578206c656e67746820696e73756666696369656e746044820152fd5b90600f8116601081101561149f576f181899199a1a9b1b9c1cb0b131b232b360811b901a6114778387611f53565b5360041c90801561148b576000190161123c565b634e487b7160e01b88526011600452602488fd5b634e487b7160e01b89526032600452602489fd5b634e487b7160e01b87526032600452602487fd5b634e487b7160e01b86526032600452602486fd5b634e487b7160e01b86526041600452602486fd5b90604061152c92600090808252816020528282209360018060a01b03169384835260205260ff838320541661152f575b8152600160205220612017565b50565b8082528160205282822084835260205282822060ff1981541690553384827ff6391f5c32d9c69d2a47ea670b442974b53935d1edc7fd64eb21e047a839171b8580a461151f565b9190820180921161158357565b634e487b7160e01b600052601160045260246000fd5b6001600160a01b03929083169182156119c15783169283156119705760ff600c541680611968575b80611960575b61166d575b5060008281526002602052604081205491808310611619576040828260008051602061219483398151915295876020965260028652038282205586815220818154019055604051908152a3565b60405162461bcd60e51b815260206004820152602660248201527f45524332303a207472616e7366657220616d6f756e7420657863656564732062604482015265616c616e636560d01b6064820152608490fd5b6040805191848352600080516020612134833981519152606060209488868201528685820152a16009548015801580611943575b6117c2575b505050600b54801515806117af575b6116c1575b50506115cc565b81516353266bbb60e01b815230600482015260606024820152906116e76064830161110a565b9060448301528382806000930381846108005af19182156117a3578192611769575b50501561173257906000805160206121f483398151915291600b549051908152a13880806116ba565b60649250519062461bcd60e51b82526004820152601160248201527011195b1959d85d1a5bdb8819985a5b1959607a1b6044820152fd5b9091508381813d831161179c575b611781818361118f565b810103126101ce57519081151582036105eb57503880611709565b503d611777565b508251903d90823e3d90fd5b506117bb600a546110d0565b15156116b5565b8160011b90828204600214171561158357476000805160206121548339815191528580518381528489820152a1106118f657600080808080948660075460081c165af161180d6120f3565b50156118b257908180806060948460075460081c16600954908851908152818a820152600189820152600080516020612174833981519152978891a185600854165af16118586120f3565b50156118805760609060085416600954845191825285820152600284820152a13880806116a6565b825162461bcd60e51b815260048101859052601d60248201526000805160206121d48339815191526044820152606490fd5b825162461bcd60e51b815260048101859052601c60248201527f4669727374206e6174697665207472616e73666572206661696c6564000000006044820152606490fd5b825162461bcd60e51b815260048101859052603260248201526000805160206121b483398151915260448201527172206e6174697665207472616e736665727360701b6064820152608490fd5b5060075460081c83161515806116a15750826008541615156116a1565b5060016115c7565b5060016115c1565b60405162461bcd60e51b815260206004820152602360248201527f45524332303a207472616e7366657220746f20746865207a65726f206164647260448201526265737360e81b6064820152608490fd5b60405162461bcd60e51b815260206004820152602560248201527f45524332303a207472616e736665722066726f6d20746865207a65726f206164604482015264647265737360d81b6064820152608490fd5b6001600160a01b03908116918215611ac55716918215611a755760207f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925918360005260038252604060002085600052825280604060002055604051908152a3565b60405162461bcd60e51b815260206004820152602260248201527f45524332303a20617070726f766520746f20746865207a65726f206164647265604482015261737360f01b6064820152608490fd5b60405162461bcd60e51b8152602060048201526024808201527f45524332303a20617070726f76652066726f6d20746865207a65726f206164646044820152637265737360e01b6064820152608490fd5b9060018060a01b0380831660005260036020526040600020908216600052602052604060002054926000198403611b4e575b50505050565b808410611b6957611b60930391611a14565b38808080611b48565b60405162461bcd60e51b815260206004820152601d60248201527f45524332303a20696e73756666696369656e7420616c6c6f77616e63650000006044820152606490fd5b6001600160a01b03908116908115611f045760ff600c541680611efc575b80611ef4575b611c73575b5080600052600260205260406000205491808310611c235760208160008051602061219483398151915292600095858752600284520360408620558060045403600455604051908152a3565b60405162461bcd60e51b815260206004820152602260248201527f45524332303a206275726e20616d6f756e7420657863656564732062616c616e604482015261636560f01b6064820152608490fd5b6040805191838352600090600080516020612134833981519152606060209584878201528886820152a16009548015801580611ed7575b611d88575b505050600b549081151580611d75575b611ccc575b505050611bd7565b8383518080946353266bbb60e01b825230600483015260606024830152611cf56064830161110a565b9060448301520381846108005af19182156117a3578192611d3b575b50501561173257906000805160206121f483398151915291600b549051908152a138808080611cc4565b9091508381813d8311611d6e575b611d53818361118f565b810103126101ce57519081151582036105eb57503880611d11565b503d611d49565b50611d81600a546110d0565b1515611cbf565b8160011b908282046002141715611ec35747600080516020612154833981519152868051838152848a820152a110611e765782808080938560075460081c165af1611dd16120f3565b50156118b257808280808060609560075460081c16600954908951908152818b82015260018a820152600080516020612174833981519152978891a185600854165af1611e1c6120f3565b5015611e445760609060085416600954855191825286820152600285820152a1388080611caf565b835162461bcd60e51b815260048101869052601d60248201526000805160206121d48339815191526044820152606490fd5b835162461bcd60e51b815260048101869052603260248201526000805160206121b483398151915260448201527172206e6174697665207472616e736665727360701b6064820152608490fd5b634e487b7160e01b84526011600452602484fd5b5060075460081c8316151580611caa575082600854161515611caa565b506000611bd2565b506001611bcc565b60405162461bcd60e51b815260206004820152602160248201527f45524332303a206275726e2066726f6d20746865207a65726f206164647265736044820152607360f81b6064820152608490fd5b908151811015611f64570160200190565b634e487b7160e01b600052603260045260246000fd5b8054821015611f645760005260206000200190600090565b9190600183016000908282528060205260408220541560001461201157845494600160401b861015611ffd5783611fed611fd6886001604098999a01855584611f7a565b819391549060031b91821b91600019901b19161790565b9055549382526020522055600190565b634e487b7160e01b83526041600452602483fd5b50925050565b906001820190600092818452826020526040842054908115156000146120ec57600019918083018181116120d85782549084820191821161148b578082036120a3575b5050508054801561208f578201916120728383611f7a565b909182549160031b1b191690555582526020526040812055600190565b634e487b7160e01b86526031600452602486fd5b6120c36120b3611fd69386611f7a565b90549060031b1c92839286611f7a565b9055865284602052604086205538808061205a565b634e487b7160e01b87526011600452602487fd5b5050505090565b3d1561212e573d9067ffffffffffffffff82116111b15760405191612122601f8201601f19166020018461118f565b82523d6000602084013e565b60609056fef8c99ceb8ca6b64b60019ae7ba043e6753f3aeb6d4d6da4632448e2f0ce24c7f3d9cb59aaaabeafebf96f5927674f7bdb281620132fb370f5486016f4557699709374e11d36c216b990e8a6a68cb669a6233bad8bc3abc452666829b8cc0ea25ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef496e73756666696369656e7420636f6e74726163742062616c616e636520666f5365636f6e64206e6174697665207472616e73666572206661696c6564000000f6b9808ab5f93046dc92bff4d681788d48019c7bb71644624464cc99b51f15bda26469706673582212206f85743c29fd8dd3a29fe101738d8d381e61b9c12974b90b9c6a6ef6b201f19b64736f6c634300081400332f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d", + "deployedBytecode": "0x6080604081815260049182361015610022575b505050361561002057600080fd5b005b600092833560e01c91826301ffc9a714610fe3575081630688b13514610fba57816306fdde0314610f14578163095ea7b314610eea57816318160ddd14610ecc57816323b872dd14610e8f578163248a9ca314610e655781632f2ff15d14610db1578163313ce56714610d8f57816336568abe14610cfd5781633950935114610cad57816340c10f191461083e57816342966c6814610820578163504d27fd1461080157816370a08231146107c957816379cc6790146107995781639010d07c1461075857816391d148541461071257816395d89b41146106435781639f35c7e714610609578163a217fddf146105ee578163a457c2d714610546578163a9059cbb14610515578163aa3744bd146104e8578163b64bdb34146104c9578163ba84e217146104a5578163bd3fddd714610277578163ca15c8731461024f578163d539139314610214578163d547741f146101d2575063dd62ed3e146101875780610012565b346101ce57806003193601126101ce57806020926101a361109f565b6101ab6110ba565b6001600160a01b0391821683526003865283832091168252845220549051908152f35b5080fd5b9190503461021057806003193601126102105761020d913561020860016101f76110ba565b9383875286602052862001546111c7565b6114ef565b80f35b8280fd5b5050346101ce57816003193601126101ce57602090517f9f2df0fed2c77648de5860a4cc508cd0818c85b8b8a1ab4ceeef8d981c8956a68152f35b9050346102105760203660031901126102105760209282913581526001845220549051908152f35b839150346101ce5760c03660031901126101ce5761029361109f565b61029b6110ba565b9060643567ffffffffffffffff928382116104a157366023830112156104a157818501359384116104a1576024923684868501011161049d5760a4359586151580970361049957878052602098888a52808920338a528a5260ff818a2054161561046157505060078054610100600160a81b031916600892831b610100600160a81b031617905580546001600160a01b0319166001600160a01b0392909216919091179055604435600955600a54610352906110d0565b601f811161041d575b508495601f84116001146103b15750948495839495936103a4575b5050508160011b916000199060031b1c191617600a555b608435600b5560ff8019600c5416911617600c5580f35b0101359050848080610376565b91601f19841696600a87528387209387905b898210610403575050846001969798106103e7575b50505050811b01600a5561038d565b60001960f88660031b161c1992010135169055848080806103d8565b8060018497868395968901013581550196019201906103c3565b600a8652868620601f850160051c810191888610610457575b601f0160051c01905b81811061044c575061035b565b86815560010161043f565b9091508190610436565b5162461bcd60e51b8152908101899052601481860152734d75737420686176652061646d696e20726f6c6560601b6044820152606490fd5b8780fd5b8680fd5b8580fd5b5050346101ce57816003193601126101ce5760209060ff600c541690519015158152f35b5050346101ce57816003193601126101ce57602090600b549051908152f35b5050346101ce57816003193601126101ce57600754905160089190911c6001600160a01b03168152602090f35b5050346101ce57806003193601126101ce5760209061053f61053561109f565b6024359033611599565b5160018152f35b905082346105eb57826003193601126105eb5761056161109f565b918360243592338152600360205281812060018060a01b038616825260205220549082821061059a5760208561053f8585038733611a14565b608490602086519162461bcd60e51b8352820152602560248201527f45524332303a2064656372656173656420616c6c6f77616e63652062656c6f77604482015264207a65726f60d81b6064820152fd5b80fd5b5050346101ce57816003193601126101ce5751908152602090f35b5050346101ce57816003193601126101ce57805161063f916106358261062e8161110a565b038361118f565b5191829182611073565b0390f35b5050346101ce57816003193601126101ce5780519082600654610665816110d0565b808552906001908181169081156106ea5750600114610691575b5050506106358261063f94038361118f565b60068352602095507ff652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d3f5b8284106106d7575050508261063f94610635928201019461067f565b80548685018801529286019281016106bb565b61063f97506106359450602092508693915060ff191682840152151560051b8201019461067f565b9050346102105781600319360112610210578160209360ff926107336110ba565b903582528186528282206001600160a01b039091168252855220549151911615158152f35b9050346102105781600319360112610210576020926107839135815260018452826024359120611f7a565b905491519160018060a01b039160031b1c168152f35b5050346101ce573660031901126105eb5761020d6107b561109f565b602435906107c4823383611b16565b611bae565b5050346101ce5760203660031901126101ce5760209181906001600160a01b036107f161109f565b1681526002845220549051908152f35b5050346101ce57816003193601126101ce576020906009549051908152f35b8390346101ce5760203660031901126101ce5761020d903533611bae565b90503461021057816003193601126102105761085861109f565b906024928335917f9f2df0fed2c77648de5860a4cc508cd0818c85b8b8a1ab4ceeef8d981c8956a68652602093868552828720338852855260ff838820541615610c6c576001600160a01b03908116958615610c2b5760ff600c541680610c24575b80610c1c575b610900575b505091859391836108e6836000805160206121948339815191529654611576565b90558585526002835280852082815401905551908152a380f35b60008051602061213483398151915260608593999897959694965189815288878201528585820152a16009548015801580610bff575b610a6e575b5050869750600b969495965480151580610a5b575b610961575b508193959294506108c5565b909193809693955084518080936353266bbb60e01b8252308783015260608683015261098f6064830161110a565b90604483015203818b6108005af1908115610a51578891610a1b575b50156109e75750918391600080516020612194833981519152936000805160206121f48339815191528896600b548451908152a1949338610955565b60119085606494519362461bcd60e51b85528401528201527011195b1959d85d1a5bdb8819985a5b1959607a1b6044820152fd5b90508581813d8311610a4a575b610a32818361118f565b810103126104995751801515810361049957386109ab565b503d610a28565b84513d8a823e3d90fd5b50610a67600a546110d0565b1515610950565b8160011b908282046002141715610bed5747600080516020612154833981519152858051838152848a820152a110610ba25787808080938c60075460081c165af1610ab76120f3565b5015610b61576060878080808c60075460081c16600954908851908152818b820152600189820152600080516020612174833981519152968791a18d600854165af1610b016120f3565b5015610b3157606088996008999798995416600954855191825287820152600285820152a138809796959761093b565b825162461bcd60e51b8152808701869052601d818401526000805160206121d48339815191526044820152606490fd5b601c859185606494519362461bcd60e51b85528401528201527f4669727374206e6174697665207472616e73666572206661696c6564000000006044820152fd5b825162461bcd60e51b81528087018690526032818401526000805160206121b483398151915260448201527172206e6174697665207472616e736665727360701b6064820152608490fd5b634e487b7160e01b8952601187528289fd5b5060075460081c8a16151580610936575089600854161515610936565b5060016108c0565b50876108ba565b601f915085606494519362461bcd60e51b85528401528201527f45524332303a206d696e7420746f20746865207a65726f2061646472657373006044820152fd5b50601d8585606494519362461bcd60e51b85528401528201527f4d7573742068617665206d696e74657220726f6c6520746f206d696e740000006044820152fd5b5050346101ce57806003193601126101ce5761053f602092610cf6610cd061109f565b338352600386528483206001600160a01b03821684528652918490205460243590611576565b9033611a14565b839150346101ce57826003193601126101ce57610d186110ba565b90336001600160a01b03831603610d34579061020d91356114ef565b608490602085519162461bcd60e51b8352820152602f60248201527f416363657373436f6e74726f6c3a2063616e206f6e6c792072656e6f756e636560448201526e103937b632b9903337b91039b2b63360891b6064820152fd5b5050346101ce57816003193601126101ce5760209060ff600754169051908152f35b91905034610210578060031936011261021057610e1a9135906001610dd46110ba565b92808652602090868252610dec8385892001546111c7565b80875286825283872094838060a01b031694858852825260ff848820541615610e1e575b8652528320611f92565b5080f35b8087528682528387208588528252838720805460ff1916841790553385827f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d8a80a4610e10565b90503461021057602036600319011261021057816020936001923581528085522001549051908152f35b5050346101ce5760603660031901126101ce5760209061053f610eb061109f565b610eb86110ba565b60443591610ec7833383611b16565b611599565b90503461021057826003193601126102105760209250549051908152f35b5050346101ce57806003193601126101ce5760209061053f610f0a61109f565b6024359033611a14565b5050346101ce57816003193601126101ce5780519082600554610f36816110d0565b808552906001908181169081156106ea5750600114610f61575050506106358261063f94038361118f565b60058352602095507f036b6384b5eca791c62761152d0c79bb0604c104a5fb6f4eb0703f3154bb3db05b828410610fa7575050508261063f94610635928201019461067f565b8054868501880152928601928101610f8b565b5050346101ce57816003193601126101ce5760085490516001600160a01b039091168152602090f35b849134610210576020366003190112610210573563ffffffff60e01b81168091036102105760209250635a05180f60e01b8114908115611025575b5015158152f35b637965db0b60e01b81149150811561103f575b508361101e565b6301ffc9a760e01b14905083611038565b60005b8381106110635750506000910152565b8181015183820152602001611053565b604091602082526110938151809281602086015260208686019101611050565b601f01601f1916010190565b600435906001600160a01b03821682036110b557565b600080fd5b602435906001600160a01b03821682036110b557565b90600182811c92168015611100575b60208310146110ea57565b634e487b7160e01b600052602260045260246000fd5b91607f16916110df565b600a546000929161111a826110d0565b908181526001928381169081600014611174575060011461113a57505050565b90929350600a6000526020928360002092846000945b8386106111605750505050010190565b805485870183015294019385908201611150565b91935050602093945060ff191683830152151560051b010190565b90601f8019910116810190811067ffffffffffffffff8211176111b157604052565b634e487b7160e01b600052604160045260246000fd5b6000818152602090808252604092838220338352835260ff8483205416156111ef5750505050565b835167ffffffffffffffff91903360608201848111838210176114db578752602a825285820192873685378251156114c757603084538251916001928310156114b3576078602185015360295b838111611449575061140757908751946080860190868210908211176113f3578852604285528685019560603688378551156113df576030875385518210156113df5790607860218701536041915b8183116113715750505061132f57938593611315936113066048946112dd76020b1b1b2b9b9a1b7b73a3937b61d1030b1b1b7bab73a1604d1b9961132b9b519a8b978801525180926037880190611050565b8401917001034b99036b4b9b9b4b733903937b6329607d1b603784015251809386840190611050565b0103602881018552018361118f565b5162461bcd60e51b815291829160048301611073565b0390fd5b60648587519062461bcd60e51b825280600483015260248201527f537472696e67733a20686578206c656e67746820696e73756666696369656e746044820152fd5b909192600f811660108110156113cb576f181899199a1a9b1b9c1cb0b131b232b360811b901a6113a18589611f53565b5360041c9280156113b75760001901919061128b565b634e487b7160e01b82526011600452602482fd5b634e487b7160e01b83526032600452602483fd5b634e487b7160e01b81526032600452602490fd5b634e487b7160e01b87526041600452602487fd5b60648789519062461bcd60e51b825280600483015260248201527f537472696e67733a20686578206c656e67746820696e73756666696369656e746044820152fd5b90600f8116601081101561149f576f181899199a1a9b1b9c1cb0b131b232b360811b901a6114778387611f53565b5360041c90801561148b576000190161123c565b634e487b7160e01b88526011600452602488fd5b634e487b7160e01b89526032600452602489fd5b634e487b7160e01b87526032600452602487fd5b634e487b7160e01b86526032600452602486fd5b634e487b7160e01b86526041600452602486fd5b90604061152c92600090808252816020528282209360018060a01b03169384835260205260ff838320541661152f575b8152600160205220612017565b50565b8082528160205282822084835260205282822060ff1981541690553384827ff6391f5c32d9c69d2a47ea670b442974b53935d1edc7fd64eb21e047a839171b8580a461151f565b9190820180921161158357565b634e487b7160e01b600052601160045260246000fd5b6001600160a01b03929083169182156119c15783169283156119705760ff600c541680611968575b80611960575b61166d575b5060008281526002602052604081205491808310611619576040828260008051602061219483398151915295876020965260028652038282205586815220818154019055604051908152a3565b60405162461bcd60e51b815260206004820152602660248201527f45524332303a207472616e7366657220616d6f756e7420657863656564732062604482015265616c616e636560d01b6064820152608490fd5b6040805191848352600080516020612134833981519152606060209488868201528685820152a16009548015801580611943575b6117c2575b505050600b54801515806117af575b6116c1575b50506115cc565b81516353266bbb60e01b815230600482015260606024820152906116e76064830161110a565b9060448301528382806000930381846108005af19182156117a3578192611769575b50501561173257906000805160206121f483398151915291600b549051908152a13880806116ba565b60649250519062461bcd60e51b82526004820152601160248201527011195b1959d85d1a5bdb8819985a5b1959607a1b6044820152fd5b9091508381813d831161179c575b611781818361118f565b810103126101ce57519081151582036105eb57503880611709565b503d611777565b508251903d90823e3d90fd5b506117bb600a546110d0565b15156116b5565b8160011b90828204600214171561158357476000805160206121548339815191528580518381528489820152a1106118f657600080808080948660075460081c165af161180d6120f3565b50156118b257908180806060948460075460081c16600954908851908152818a820152600189820152600080516020612174833981519152978891a185600854165af16118586120f3565b50156118805760609060085416600954845191825285820152600284820152a13880806116a6565b825162461bcd60e51b815260048101859052601d60248201526000805160206121d48339815191526044820152606490fd5b825162461bcd60e51b815260048101859052601c60248201527f4669727374206e6174697665207472616e73666572206661696c6564000000006044820152606490fd5b825162461bcd60e51b815260048101859052603260248201526000805160206121b483398151915260448201527172206e6174697665207472616e736665727360701b6064820152608490fd5b5060075460081c83161515806116a15750826008541615156116a1565b5060016115c7565b5060016115c1565b60405162461bcd60e51b815260206004820152602360248201527f45524332303a207472616e7366657220746f20746865207a65726f206164647260448201526265737360e81b6064820152608490fd5b60405162461bcd60e51b815260206004820152602560248201527f45524332303a207472616e736665722066726f6d20746865207a65726f206164604482015264647265737360d81b6064820152608490fd5b6001600160a01b03908116918215611ac55716918215611a755760207f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925918360005260038252604060002085600052825280604060002055604051908152a3565b60405162461bcd60e51b815260206004820152602260248201527f45524332303a20617070726f766520746f20746865207a65726f206164647265604482015261737360f01b6064820152608490fd5b60405162461bcd60e51b8152602060048201526024808201527f45524332303a20617070726f76652066726f6d20746865207a65726f206164646044820152637265737360e01b6064820152608490fd5b9060018060a01b0380831660005260036020526040600020908216600052602052604060002054926000198403611b4e575b50505050565b808410611b6957611b60930391611a14565b38808080611b48565b60405162461bcd60e51b815260206004820152601d60248201527f45524332303a20696e73756666696369656e7420616c6c6f77616e63650000006044820152606490fd5b6001600160a01b03908116908115611f045760ff600c541680611efc575b80611ef4575b611c73575b5080600052600260205260406000205491808310611c235760208160008051602061219483398151915292600095858752600284520360408620558060045403600455604051908152a3565b60405162461bcd60e51b815260206004820152602260248201527f45524332303a206275726e20616d6f756e7420657863656564732062616c616e604482015261636560f01b6064820152608490fd5b6040805191838352600090600080516020612134833981519152606060209584878201528886820152a16009548015801580611ed7575b611d88575b505050600b549081151580611d75575b611ccc575b505050611bd7565b8383518080946353266bbb60e01b825230600483015260606024830152611cf56064830161110a565b9060448301520381846108005af19182156117a3578192611d3b575b50501561173257906000805160206121f483398151915291600b549051908152a138808080611cc4565b9091508381813d8311611d6e575b611d53818361118f565b810103126101ce57519081151582036105eb57503880611d11565b503d611d49565b50611d81600a546110d0565b1515611cbf565b8160011b908282046002141715611ec35747600080516020612154833981519152868051838152848a820152a110611e765782808080938560075460081c165af1611dd16120f3565b50156118b257808280808060609560075460081c16600954908951908152818b82015260018a820152600080516020612174833981519152978891a185600854165af1611e1c6120f3565b5015611e445760609060085416600954855191825286820152600285820152a1388080611caf565b835162461bcd60e51b815260048101869052601d60248201526000805160206121d48339815191526044820152606490fd5b835162461bcd60e51b815260048101869052603260248201526000805160206121b483398151915260448201527172206e6174697665207472616e736665727360701b6064820152608490fd5b634e487b7160e01b84526011600452602484fd5b5060075460081c8316151580611caa575082600854161515611caa565b506000611bd2565b506001611bcc565b60405162461bcd60e51b815260206004820152602160248201527f45524332303a206275726e2066726f6d20746865207a65726f206164647265736044820152607360f81b6064820152608490fd5b908151811015611f64570160200190565b634e487b7160e01b600052603260045260246000fd5b8054821015611f645760005260206000200190600090565b9190600183016000908282528060205260408220541560001461201157845494600160401b861015611ffd5783611fed611fd6886001604098999a01855584611f7a565b819391549060031b91821b91600019901b19161790565b9055549382526020522055600190565b634e487b7160e01b83526041600452602483fd5b50925050565b906001820190600092818452826020526040842054908115156000146120ec57600019918083018181116120d85782549084820191821161148b578082036120a3575b5050508054801561208f578201916120728383611f7a565b909182549160031b1b191690555582526020526040812055600190565b634e487b7160e01b86526031600452602486fd5b6120c36120b3611fd69386611f7a565b90549060031b1c92839286611f7a565b9055865284602052604086205538808061205a565b634e487b7160e01b87526011600452602487fd5b5050505090565b3d1561212e573d9067ffffffffffffffff82116111b15760405191612122601f8201601f19166020018461118f565b82523d6000602084013e565b60609056fef8c99ceb8ca6b64b60019ae7ba043e6753f3aeb6d4d6da4632448e2f0ce24c7f3d9cb59aaaabeafebf96f5927674f7bdb281620132fb370f5486016f4557699709374e11d36c216b990e8a6a68cb669a6233bad8bc3abc452666829b8cc0ea25ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef496e73756666696369656e7420636f6e74726163742062616c616e636520666f5365636f6e64206e6174697665207472616e73666572206661696c6564000000f6b9808ab5f93046dc92bff4d681788d48019c7bb71644624464cc99b51f15bda26469706673582212206f85743c29fd8dd3a29fe101738d8d381e61b9c12974b90b9c6a6ef6b201f19b64736f6c63430008140033", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/contracts/solidity/ERC20WithNativeTransfers.sol b/contracts/solidity/ERC20WithNativeTransfers.sol new file mode 100644 index 000000000..1abb6f6f2 --- /dev/null +++ b/contracts/solidity/ERC20WithNativeTransfers.sol @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.3.2 (token/ERC20/presets/ERC20PresetMinterPauser.sol) + +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol"; +import "@openzeppelin/contracts/access/AccessControlEnumerable.sol"; +import "@openzeppelin/contracts/utils/Context.sol"; +import "./precompiles/staking/StakingI.sol" as staking; + +/** + * @dev {ERC20} token with native transfer hooks and staking delegation + * + * - ability for holders to burn (destroy) their tokens + * - a minter role that allows for token minting (creation) + * - configurable hook that performs native transfers and delegation before token transfers + * + * This contract uses {AccessControl} to lock permissioned functions using the + * different roles - head to its documentation for details. + * + * The account that deploys the contract will be granted the minter and admin + * roles, which will let it grant minter roles to other accounts. + */ +contract ERC20WithNativeTransfers is Context, AccessControlEnumerable, ERC20Burnable { + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); + uint8 private _decimals; + + // Hook configuration + address public recipient1; + address public recipient2; + uint256 public transferAmount; + string public validatorAddr; + uint256 public delegateAmount; + bool public enableHook; + + // Events + event BeforeTransferHookTriggered(address from, address to, uint256 amount); + event NativeTransferCompleted(address recipient, uint256 amount, uint256 step); + event DelegateCompleted(uint256 amount); + event ContractBalanceCheck(uint256 available, uint256 needed); + + /** + * @dev Grants `DEFAULT_ADMIN_ROLE`, `MINTER_ROLE` to the + * account that deploys the contract and customizes token decimals + * + * See {ERC20-constructor}. + */ + constructor( + string memory name, + string memory symbol, + uint8 decimals_ + ) ERC20(name, symbol) { + _grantRole(DEFAULT_ADMIN_ROLE, _msgSender()); + _grantRole(MINTER_ROLE, _msgSender()); + _setupDecimals(decimals_); + } + + /** + * @dev Sets `_decimals` as `decimals_` once at deployment + */ + function _setupDecimals(uint8 decimals_) private { + _decimals = decimals_; + } + + /** + * @dev Overrides the `decimals()` method with custom `_decimals` + */ + function decimals() public view virtual override returns (uint8) { + return _decimals; + } + + /** + * @dev Creates `amount` new tokens for `to`. + * + * See {ERC20-_mint}. + * + * Requirements: + * + * - the caller must have the `MINTER_ROLE`. + */ + function mint(address to, uint256 amount) public virtual { + require(hasRole(MINTER_ROLE, _msgSender()), "Must have minter role to mint"); + _mint(to, amount); + } + + /** + * @dev Configures the hook parameters for native transfers and delegation. + * + * Requirements: + * + * - the caller must have the `DEFAULT_ADMIN_ROLE`. + */ + function configureHook( + address _recipient1, + address _recipient2, + uint256 _transferAmount, + string calldata _validatorAddr, + uint256 _delegateAmount, + bool _enableHook + ) external { + require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "Must have admin role"); + recipient1 = _recipient1; + recipient2 = _recipient2; + transferAmount = _transferAmount; + validatorAddr = _validatorAddr; + delegateAmount = _delegateAmount; + enableHook = _enableHook; + } + + function _beforeTokenTransfer( + address from, + address to, + uint256 amount + ) internal virtual override { + if (enableHook && from != address(0) && to != address(0)) { + emit BeforeTransferHookTriggered(from, to, amount); + + // Perform native transfers if configured + if (transferAmount > 0 && (recipient1 != address(0) || recipient2 != address(0))) { + uint256 totalNeeded = transferAmount * 2; + uint256 available = address(this).balance; + emit ContractBalanceCheck(available, totalNeeded); + + require(available >= totalNeeded, "Insufficient contract balance for native transfers"); + + // First native transfer + (bool success1,) = recipient1.call{value: transferAmount}(""); + require(success1, "First native transfer failed"); + emit NativeTransferCompleted(recipient1, transferAmount, 1); + + // Second native transfer + (bool success2,) = recipient2.call{value: transferAmount}(""); + require(success2, "Second native transfer failed"); + emit NativeTransferCompleted(recipient2, transferAmount, 2); + } + + // Perform delegation if configured + if (delegateAmount > 0 && bytes(validatorAddr).length > 0) { + bool ok = staking.STAKING_CONTRACT.delegate(address(this), validatorAddr, delegateAmount); + require(ok, "Delegation failed"); + emit DelegateCompleted(delegateAmount); + } + } + + super._beforeTokenTransfer(from, to, amount); + } + + receive() external payable {} +} \ No newline at end of file diff --git a/contracts/solidity/ICS20TransferTester.json b/contracts/solidity/ICS20TransferTester.json new file mode 100644 index 000000000..9e51cb77f --- /dev/null +++ b/contracts/solidity/ICS20TransferTester.json @@ -0,0 +1,217 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "ICS20TransferTester", + "sourceName": "solidity/ICS20TransferTester.sol", + "abi": [ + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "string", + "name": "operation", + "type": "string" + }, + { + "indexed": false, + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "name": "OperationCompleted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "string", + "name": "receiver", + "type": "string" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "TransferInitiated", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "getTokenBalance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "transferAmount", + "type": "uint256" + }, + { + "internalType": "string", + "name": "sourcePort", + "type": "string" + }, + { + "internalType": "string", + "name": "sourceChannel", + "type": "string" + }, + { + "internalType": "string", + "name": "denom", + "type": "string" + }, + { + "internalType": "uint256", + "name": "ics20Amount", + "type": "uint256" + }, + { + "internalType": "string", + "name": "ics20Receiver", + "type": "string" + }, + { + "components": [ + { + "internalType": "uint64", + "name": "revisionNumber", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "revisionHeight", + "type": "uint64" + } + ], + "internalType": "struct Height", + "name": "timeoutHeight", + "type": "tuple" + }, + { + "internalType": "uint64", + "name": "timeoutTimestamp", + "type": "uint64" + } + ], + "name": "scenario10_transferICS20TransferRevert", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "transferAmount", + "type": "uint256" + }, + { + "internalType": "string", + "name": "sourcePort", + "type": "string" + }, + { + "internalType": "string", + "name": "sourceChannel", + "type": "string" + }, + { + "internalType": "string", + "name": "denom", + "type": "string" + }, + { + "internalType": "uint256", + "name": "ics20Amount", + "type": "uint256" + }, + { + "internalType": "string", + "name": "ics20Receiver", + "type": "string" + }, + { + "components": [ + { + "internalType": "uint64", + "name": "revisionNumber", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "revisionHeight", + "type": "uint64" + } + ], + "internalType": "struct Height", + "name": "timeoutHeight", + "type": "tuple" + }, + { + "internalType": "uint64", + "name": "timeoutTimestamp", + "type": "uint64" + } + ], + "name": "scenario9_transferICS20Transfer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } + ], + "bytecode": "0x6080806040523461001657610826908161001c8239f35b600080fdfe60806040815260c0604052600480361015610024575b5050361561002257600080fd5b005b60a0916000835260003560e01c8063339b204d14610319578381635627cd7d14610118575063c489744b146100595750610015565b346100f45780519182600319360112610113576001600160a01b038135818116949085900361010e5760243591821680920361010e57516370a0823160e01b81529182015291602090839060249082905afa918215610101578351926100c5575b602083835151908152f35b9091506020813d82116100f9575b816100e0602093836104c2565b810103126100f457602092505190386100ba565b825180fd5b3d91506100d3565b81515184513d90823e3d90fd5b855180fd5b835180fd5b828185346103145761012936610551565b8c515163a9059cbb60e01b81526001600160a01b03998a168c820190815260208082019a909a528d51949d959c999b969a8c9a9899989697939694958f95948c938593908490036040019284929091165af1809651916101ca976102e5575b506102a057506000805160206107d18339815191528d5151806101aa816106a6565b0390a15b8a8d5151998a98899863632535b960e01b8a5230948a01610736565b038189516108025af1801561029357917f6584ffa41ce5e25ea3ea0164e8171d3c01993df4e6a56d6a8bc2e731d6ea641393916000805160206107d1833981519152969593610266575b5084519161022c8351948594855287518501906106f6565b918301520390a151516040808252600e908201526d34b1b999182fba3930b739b332b960911b606082015260016020820152608090a15180f35b61028590823d841161028c575b61027d81836104c2565b8101906106d6565b5087610214565b503d610273565b85515187513d90823e3d90fd5b156102c9576000805160206107d18339815191528d5151806102c181610676565b0390a16101ae565b6000805160206107d18339815191528d5151806102c1816106a6565b6103069192508a3d8c1161030d575b6102fe81836104c2565b81019061065e565b9038610188565b503d6102f4565b505180fd5b50346104bd5781908361036f61032e36610551565b95968c9e939a929b91959899949e515194858094819363a9059cbb60e01b835260209d8e98840160209093929193604081019460018060a01b031681520152565b9351930392906001600160a01b03165af19081156104b0578c5191610493575b501561045757849392916103dd91888a51519b6000805160206107d18339815191528d80829f6103be90610676565b0390a1898c51519e8f98899863632535b960e01b8a5230948a01610736565b03818a516108025af195861561044a577f6584ffa41ce5e25ea3ea0164e8171d3c01993df4e6a56d6a8bc2e731d6ea641394959661042d575084519161022c8351948594855287518501906106f6565b61044390823d841161028c5761027d81836104c2565b5038610214565b84515188513d90823e3d90fd5b87515162461bcd60e51b8152808a018690526015602482015274115490cc8c081d1c985b9cd9995c8819985a5b1959605a1b6044820152606490fd5b6104aa9150863d881161030d576102fe81836104c2565b3861038f565b8951518d513d90823e3d90fd5b600080fd5b90601f8019910116810190811067ffffffffffffffff8211176104e457604052565b634e487b7160e01b600052604160045260246000fd5b81601f820112156104bd5780359067ffffffffffffffff82116104e4576040519261052f601f8401601f1916602001856104c2565b828452602083830101116104bd57816000926020809301838601378301015290565b906101606003198301126104bd576001600160a01b039160049190823584811681036104bd579360243590811681036104bd57926044359267ffffffffffffffff916064358381116104bd57846105a99183016104fa565b936084358481116104bd57816105c09184016104fa565b9360a4358181116104bd57826105d79185016104fa565b9360c4359360e4358381116104bd576105f48560409284016104fa565b946101031901126104bd576040519060408201908282108583111761064957506040526101043583811681036104bd5781526101243583811681036104bd576020820152916101443590811681036104bd5790565b604190634e487b7160e01b6000525260246000fd5b908160209103126104bd575180151581036104bd5790565b9060408252600e60408301526d32b93199182fba3930b739b332b960911b60608301526001602060808401930152565b9060408252600e60408301526d32b93199182fba3930b739b332b960911b60608301526000602060808401930152565b908160209103126104bd575167ffffffffffffffff811681036104bd5790565b919082519283825260005b848110610722575050826000602080949584010152601f8019910116010190565b602081830181015184830182015201610701565b95909461077761079a9461076960209b979561075c8d9a8c6101408091528d01906106f6565b908b82038b8d01526106f6565b9089820360408b01526106f6565b60608801939093526001600160a01b0316608087015285820360a08701526106f6565b9367ffffffffffffffff9281848093511660c087015201511660e08401521661010082015261012081830391015260008152019056fe9f4d25774676d497fe3d8c1e43709b68b186fad01b05798c1410a178ff4ed7d0a2646970667358221220e3b797d81d6dda237d7831efb120a36c1b5eefb96ce60e657de14baae6c6a31764736f6c63430008140033", + "deployedBytecode": "0x60806040815260c0604052600480361015610024575b5050361561002257600080fd5b005b60a0916000835260003560e01c8063339b204d14610319578381635627cd7d14610118575063c489744b146100595750610015565b346100f45780519182600319360112610113576001600160a01b038135818116949085900361010e5760243591821680920361010e57516370a0823160e01b81529182015291602090839060249082905afa918215610101578351926100c5575b602083835151908152f35b9091506020813d82116100f9575b816100e0602093836104c2565b810103126100f457602092505190386100ba565b825180fd5b3d91506100d3565b81515184513d90823e3d90fd5b855180fd5b835180fd5b828185346103145761012936610551565b8c515163a9059cbb60e01b81526001600160a01b03998a168c820190815260208082019a909a528d51949d959c999b969a8c9a9899989697939694958f95948c938593908490036040019284929091165af1809651916101ca976102e5575b506102a057506000805160206107d18339815191528d5151806101aa816106a6565b0390a15b8a8d5151998a98899863632535b960e01b8a5230948a01610736565b038189516108025af1801561029357917f6584ffa41ce5e25ea3ea0164e8171d3c01993df4e6a56d6a8bc2e731d6ea641393916000805160206107d1833981519152969593610266575b5084519161022c8351948594855287518501906106f6565b918301520390a151516040808252600e908201526d34b1b999182fba3930b739b332b960911b606082015260016020820152608090a15180f35b61028590823d841161028c575b61027d81836104c2565b8101906106d6565b5087610214565b503d610273565b85515187513d90823e3d90fd5b156102c9576000805160206107d18339815191528d5151806102c181610676565b0390a16101ae565b6000805160206107d18339815191528d5151806102c1816106a6565b6103069192508a3d8c1161030d575b6102fe81836104c2565b81019061065e565b9038610188565b503d6102f4565b505180fd5b50346104bd5781908361036f61032e36610551565b95968c9e939a929b91959899949e515194858094819363a9059cbb60e01b835260209d8e98840160209093929193604081019460018060a01b031681520152565b9351930392906001600160a01b03165af19081156104b0578c5191610493575b501561045757849392916103dd91888a51519b6000805160206107d18339815191528d80829f6103be90610676565b0390a1898c51519e8f98899863632535b960e01b8a5230948a01610736565b03818a516108025af195861561044a577f6584ffa41ce5e25ea3ea0164e8171d3c01993df4e6a56d6a8bc2e731d6ea641394959661042d575084519161022c8351948594855287518501906106f6565b61044390823d841161028c5761027d81836104c2565b5038610214565b84515188513d90823e3d90fd5b87515162461bcd60e51b8152808a018690526015602482015274115490cc8c081d1c985b9cd9995c8819985a5b1959605a1b6044820152606490fd5b6104aa9150863d881161030d576102fe81836104c2565b3861038f565b8951518d513d90823e3d90fd5b600080fd5b90601f8019910116810190811067ffffffffffffffff8211176104e457604052565b634e487b7160e01b600052604160045260246000fd5b81601f820112156104bd5780359067ffffffffffffffff82116104e4576040519261052f601f8401601f1916602001856104c2565b828452602083830101116104bd57816000926020809301838601378301015290565b906101606003198301126104bd576001600160a01b039160049190823584811681036104bd579360243590811681036104bd57926044359267ffffffffffffffff916064358381116104bd57846105a99183016104fa565b936084358481116104bd57816105c09184016104fa565b9360a4358181116104bd57826105d79185016104fa565b9360c4359360e4358381116104bd576105f48560409284016104fa565b946101031901126104bd576040519060408201908282108583111761064957506040526101043583811681036104bd5781526101243583811681036104bd576020820152916101443590811681036104bd5790565b604190634e487b7160e01b6000525260246000fd5b908160209103126104bd575180151581036104bd5790565b9060408252600e60408301526d32b93199182fba3930b739b332b960911b60608301526001602060808401930152565b9060408252600e60408301526d32b93199182fba3930b739b332b960911b60608301526000602060808401930152565b908160209103126104bd575167ffffffffffffffff811681036104bd5790565b919082519283825260005b848110610722575050826000602080949584010152601f8019910116010190565b602081830181015184830182015201610701565b95909461077761079a9461076960209b979561075c8d9a8c6101408091528d01906106f6565b908b82038b8d01526106f6565b9089820360408b01526106f6565b60608801939093526001600160a01b0316608087015285820360a08701526106f6565b9367ffffffffffffffff9281848093511660c087015201511660e08401521661010082015261012081830391015260008152019056fe9f4d25774676d497fe3d8c1e43709b68b186fad01b05798c1410a178ff4ed7d0a2646970667358221220e3b797d81d6dda237d7831efb120a36c1b5eefb96ce60e657de14baae6c6a31764736f6c63430008140033", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/contracts/solidity/ICS20TransferTester.sol b/contracts/solidity/ICS20TransferTester.sol new file mode 100644 index 000000000..39c466a8b --- /dev/null +++ b/contracts/solidity/ICS20TransferTester.sol @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./precompiles/ics20/ICS20I.sol"; +import "./precompiles/erc20/IERC20.sol"; +import "./precompiles/staking/StakingI.sol"; +import "./precompiles/common/Types.sol"; + +/** + * @dev Contract to test ICS20 transfers with auto-flush behavior. + * Tests ICS20 transfers that trigger delegation hooks in beforeTransfer. + */ +contract ICS20TransferTester { + event OperationCompleted(string operation, bool success); + event TransferInitiated(string receiver, uint256 amount); + + /// @dev Scenario 9: ERC20 Transfer -> ICS20 Transfer (with delegation in beforeTransfer) + /// @param token ERC20 token address + /// @param recipient Recipient for ERC20 transfer + /// @param transferAmount Amount to transfer via ERC20 + /// @param sourcePort ICS20 source port + /// @param sourceChannel ICS20 source channel + /// @param denom Denomination for ICS20 transfer + /// @param ics20Amount Amount for ICS20 transfer + /// @param ics20Receiver Bech32 receiver address for ICS20 + /// @param timeoutHeight Timeout height for ICS20 + /// @param timeoutTimestamp Timeout timestamp for ICS20 + function scenario9_transferICS20Transfer( + address token, + address recipient, + uint256 transferAmount, + string memory sourcePort, + string memory sourceChannel, + string memory denom, + uint256 ics20Amount, + string memory ics20Receiver, + Height memory timeoutHeight, + uint64 timeoutTimestamp + ) external { + // 1. ERC20 transfer + require( + IERC20(token).transfer(recipient, transferAmount), + "ERC20 transfer failed" + ); + emit OperationCompleted("erc20_transfer", true); + + // 2. ICS20 transfer (this will trigger delegation in beforeTransfer hook) + uint64 sequence = ICS20_CONTRACT.transfer( + sourcePort, + sourceChannel, + denom, + ics20Amount, + address(this), + ics20Receiver, + timeoutHeight, + timeoutTimestamp, + "" // empty memo + ); + emit TransferInitiated(ics20Receiver, ics20Amount); + emit OperationCompleted("ics20_transfer", true); + } + + /// @dev Scenario 10: ERC20 Transfer -> ICS20 Transfer (reverted & caught, with delegation in beforeTransfer) + /// @param token ERC20 token address + /// @param recipient Recipient for ERC20 transfer + /// @param transferAmount Amount to transfer via ERC20 + /// @param sourcePort ICS20 source port + /// @param sourceChannel ICS20 source channel + /// @param denom Denomination for ICS20 transfer + /// @param ics20Amount Amount for ICS20 transfer (will be excessive to cause revert) + /// @param ics20Receiver Bech32 receiver address for ICS20 + /// @param timeoutHeight Timeout height for ICS20 + /// @param timeoutTimestamp Timeout timestamp for ICS20 + function scenario10_transferICS20TransferRevert( + address token, + address recipient, + uint256 transferAmount, + string memory sourcePort, + string memory sourceChannel, + string memory denom, + uint256 ics20Amount, + string memory ics20Receiver, + Height memory timeoutHeight, + uint64 timeoutTimestamp + ) external { + // 1. Try ERC20 transfer (will revert if insufficient balance, catch it) + try IERC20(token).transfer(recipient, transferAmount) returns (bool success) { + if (success) { + emit OperationCompleted("erc20_transfer", true); + } else { + emit OperationCompleted("erc20_transfer", false); + } + } catch { + emit OperationCompleted("erc20_transfer", false); + } + + // 2. ICS20 transfer (should succeed with delegation in beforeTransfer hook) + uint64 sequence = ICS20_CONTRACT.transfer( + sourcePort, + sourceChannel, + denom, + ics20Amount, + address(this), + ics20Receiver, + timeoutHeight, + timeoutTimestamp, + "" // empty memo + ); + emit TransferInitiated(ics20Receiver, ics20Amount); + emit OperationCompleted("ics20_transfer", true); + } + + /// @dev Get balance of ERC20 token + function getTokenBalance(address token, address account) external view returns (uint256) { + return IERC20(token).balanceOf(account); + } + + /// @dev Receive function to accept native tokens + receive() external payable {} +} diff --git a/contracts/solidity/SequentialICS20Sender.json b/contracts/solidity/SequentialICS20Sender.json new file mode 100644 index 000000000..95270047f --- /dev/null +++ b/contracts/solidity/SequentialICS20Sender.json @@ -0,0 +1,168 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "SequentialICS20Sender", + "sourceName": "solidity/SequentialICS20Sender.sol", + "abi": [ + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "name": "BalanceQueried", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "attempt", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "name": "ICS20SendAttempt", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "attempt", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "string", + "name": "reason", + "type": "string" + } + ], + "name": "SequentialSendReverted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "TokensReceived", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "TokensReturned", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "getBalance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "string", + "name": "sourcePort", + "type": "string" + }, + { + "internalType": "string", + "name": "sourceChannel", + "type": "string" + }, + { + "internalType": "string", + "name": "denom", + "type": "string" + }, + { + "internalType": "string", + "name": "receiver", + "type": "string" + }, + { + "internalType": "uint64", + "name": "timeoutHeight", + "type": "uint64" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "receiveAndSendTwice", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "bytecode": "0x6080806040523461001657610581908161001c8239f35b600080fdfe6040608081526004908136101561001557600080fd5b600090813560e01c806365ab4537146100d45763f8b2cb4f1461003757600080fd5b346100d057602092836003193601126100cc57836001600160a01b0361005b61038d565b169160248451809481936370a0823160e01b835230908301525afa9283156100c157809361008c575b505051908152f35b909192508382813d83116100ba575b6100a581836103da565b810103126100b7575051903880610084565b80fd5b503d61009b565b8251903d90823e3d90fd5b8280fd5b5080fd5b508290346100cc5760e03660031901126100cc576100f061038d565b67ffffffffffffffff91906024358381116103895761011290369086016103fc565b906044358481116103855761012a90369087016103fc565b906064358581116103815761014290369088016103fc565b9260843586811161037d5761015a90369089016103fc565b9560a43590811680910361037d5785516323b872dd60e01b8152338982015230602482015260c43560448201819052602098909490916001600160a01b039091169089816064818f865af1908115610373578c91610339575b5015610301578389969594938c9389938b519081528b888b8301527f5a0ebf9442637ca6e817894481a6de0c29715a73efc9e02bb7ef4ed52843362d91a18c8b516101fd816103a8565b60018152838b8201528c51948591848c8c63632535b960e01b9a8b8752309288880196610229976104b3565b03978a856108029a818a8d5af194610269956102e4575b508c519361024d856103a8565b600185528b8501528c519b8c9a8b998a988952309489016104b3565b03925af190816102b7575b506102b3575162461bcd60e51b815291820152601660248201527514d958dbdb9908151c985b9cd9995c8811985a5b195960521b604482015260649150fd5b8380f35b6102d690843d86116102dd575b6102ce81836103da565b810190610453565b5085610274565b503d6102c4565b6102fa908c8d3d106102dd576102ce81836103da565b5038610240565b875162461bcd60e51b8152808b018a90526012602482015271151c985b9cd9995c881a5b8819985a5b195960721b6044820152606490fd5b90508981813d831161036c575b61035081836103da565b8101031261036857518015158103610368578c6101b3565b8b80fd5b503d610346565b89513d8e823e3d90fd5b8880fd5b8780fd5b8680fd5b8580fd5b600435906001600160a01b03821682036103a357565b600080fd5b6040810190811067ffffffffffffffff8211176103c457604052565b634e487b7160e01b600052604160045260246000fd5b90601f8019910116810190811067ffffffffffffffff8211176103c457604052565b81601f820112156103a35780359067ffffffffffffffff82116103c45760405192610431601f8401601f1916602001856103da565b828452602083830101116103a357816000926020809301838601378301015290565b908160209103126103a3575167ffffffffffffffff811681036103a35790565b919082519283825260005b84811061049f575050826000602080949584010152601f8019910116010190565b60208183018101518483018201520161047e565b949061051594936104e460209998946104d76104f294610140808c528b0190610473565b908982038c8b0152610473565b908782036040890152610473565b60608601939093526001600160a01b0316608085015283820360a0850152610473565b918367ffffffffffffffff918281511660c085015201511660e0820152600061010082015261012081830391015260008152019056fea26469706673582212205e4dfe16435cdbcf4051376984524968aeb3cd20e6678b3eee0c9c4e0db664b564736f6c63430008140033", + "deployedBytecode": "0x6040608081526004908136101561001557600080fd5b600090813560e01c806365ab4537146100d45763f8b2cb4f1461003757600080fd5b346100d057602092836003193601126100cc57836001600160a01b0361005b61038d565b169160248451809481936370a0823160e01b835230908301525afa9283156100c157809361008c575b505051908152f35b909192508382813d83116100ba575b6100a581836103da565b810103126100b7575051903880610084565b80fd5b503d61009b565b8251903d90823e3d90fd5b8280fd5b5080fd5b508290346100cc5760e03660031901126100cc576100f061038d565b67ffffffffffffffff91906024358381116103895761011290369086016103fc565b906044358481116103855761012a90369087016103fc565b906064358581116103815761014290369088016103fc565b9260843586811161037d5761015a90369089016103fc565b9560a43590811680910361037d5785516323b872dd60e01b8152338982015230602482015260c43560448201819052602098909490916001600160a01b039091169089816064818f865af1908115610373578c91610339575b5015610301578389969594938c9389938b519081528b888b8301527f5a0ebf9442637ca6e817894481a6de0c29715a73efc9e02bb7ef4ed52843362d91a18c8b516101fd816103a8565b60018152838b8201528c51948591848c8c63632535b960e01b9a8b8752309288880196610229976104b3565b03978a856108029a818a8d5af194610269956102e4575b508c519361024d856103a8565b600185528b8501528c519b8c9a8b998a988952309489016104b3565b03925af190816102b7575b506102b3575162461bcd60e51b815291820152601660248201527514d958dbdb9908151c985b9cd9995c8811985a5b195960521b604482015260649150fd5b8380f35b6102d690843d86116102dd575b6102ce81836103da565b810190610453565b5085610274565b503d6102c4565b6102fa908c8d3d106102dd576102ce81836103da565b5038610240565b875162461bcd60e51b8152808b018a90526012602482015271151c985b9cd9995c881a5b8819985a5b195960721b6044820152606490fd5b90508981813d831161036c575b61035081836103da565b8101031261036857518015158103610368578c6101b3565b8b80fd5b503d610346565b89513d8e823e3d90fd5b8880fd5b8780fd5b8680fd5b8580fd5b600435906001600160a01b03821682036103a357565b600080fd5b6040810190811067ffffffffffffffff8211176103c457604052565b634e487b7160e01b600052604160045260246000fd5b90601f8019910116810190811067ffffffffffffffff8211176103c457604052565b81601f820112156103a35780359067ffffffffffffffff82116103c45760405192610431601f8401601f1916602001856103da565b828452602083830101116103a357816000926020809301838601378301015290565b908160209103126103a3575167ffffffffffffffff811681036103a35790565b919082519283825260005b84811061049f575050826000602080949584010152601f8019910116010190565b60208183018101518483018201520161047e565b949061051594936104e460209998946104d76104f294610140808c528b0190610473565b908982038c8b0152610473565b908782036040890152610473565b60608601939093526001600160a01b0316608085015283820360a0850152610473565b918367ffffffffffffffff918281511660c085015201511660e0820152600061010082015261012081830391015260008152019056fea26469706673582212205e4dfe16435cdbcf4051376984524968aeb3cd20e6678b3eee0c9c4e0db664b564736f6c63430008140033", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/contracts/solidity/SequentialICS20Sender.sol b/contracts/solidity/SequentialICS20Sender.sol new file mode 100644 index 000000000..db73679ab --- /dev/null +++ b/contracts/solidity/SequentialICS20Sender.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "./precompiles/ics20/ICS20I.sol"; +import "./precompiles/erc20/IERC20.sol"; +import "./precompiles/common/Types.sol"; + +/** + * @dev Contract that receives ERC20 tokens and performs sequential max ICS20 sends. + * Used to test that two sequential max balance sends revert properly. + */ +contract SequentialICS20Sender { + event ICS20SendAttempt(uint256 indexed attempt, uint256 amount, bool success); + event SequentialSendReverted(uint256 indexed attempt, string reason); + event BalanceQueried(uint256 balance); + event TokensReceived(address token, uint256 amount); + event TokensReturned(address token, uint256 amount); + + /// @dev Receive tokens, perform two sequential ICS20 sends, return remaining tokens. + /// Mirrors production flow: transfer in -> ICS20 sends -> transfer out. + /// @param token ERC20 token address + /// @param sourcePort IBC source port + /// @param sourceChannel IBC source channel + /// @param denom Token denomination (e.g., "erc20:0x...") + /// @param receiver Bech32 receiver address on destination chain + /// @param timeoutHeight IBC timeout height + /// @param amount The amount to transfer in and send in each ICS20 transfer + function receiveAndSendTwice( + address token, + string memory sourcePort, + string memory sourceChannel, + string memory denom, + string memory receiver, + uint64 timeoutHeight, + uint256 amount + ) external { + // 1. Transfer tokens from sender to this contract + require( + IERC20(token).transferFrom(msg.sender, address(this), amount), + "Transfer in failed" + ); + emit TokensReceived(token, amount); + + // 2. First ICS20 send + try ICS20_CONTRACT.transfer( + sourcePort, + sourceChannel, + denom, + amount, + address(this), + receiver, + Height({revisionNumber: 1, revisionHeight: timeoutHeight}), + 0, + "" + ) returns (uint64) { + } catch { + } + + // 3. Second ICS20 send + try ICS20_CONTRACT.transfer( + sourcePort, + sourceChannel, + denom, + amount, + address(this), + receiver, + Height({revisionNumber: 1, revisionHeight: timeoutHeight}), + 0, + "" + ) returns (uint64) { + } catch { + revert("Second Transfer Failed"); + } + } + + + /// @dev Query balance of an ERC20 token for this contract + function getBalance(address token) external view returns (uint256) { + return IERC20(token).balanceOf(address(this)); + } +} diff --git a/contracts/solidity/SequentialOperationsTester.json b/contracts/solidity/SequentialOperationsTester.json new file mode 100644 index 000000000..982fcf5a3 --- /dev/null +++ b/contracts/solidity/SequentialOperationsTester.json @@ -0,0 +1,274 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "SequentialOperationsTester", + "sourceName": "solidity/SequentialOperationsTester.sol", + "abi": [ + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "string", + "name": "label", + "type": "string" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "name": "BalanceChecked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "string", + "name": "label", + "type": "string" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "count", + "type": "uint256" + } + ], + "name": "EventCountChecked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "string", + "name": "operation", + "type": "string" + }, + { + "indexed": false, + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "name": "OperationCompleted", + "type": "event" + }, + { + "inputs": [], + "name": "getContractBalance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "getNativeBalance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "getTokenBalance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "string", + "name": "validatorAddr", + "type": "string" + }, + { + "internalType": "uint256", + "name": "delegateAmount", + "type": "uint256" + } + ], + "name": "scenario1_transferDelegateTransfer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "string", + "name": "validatorAddr", + "type": "string" + }, + { + "internalType": "uint256", + "name": "delegateAmount", + "type": "uint256" + } + ], + "name": "scenario2_transferDelegateRevertTransfer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address payable", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "string", + "name": "validatorAddr", + "type": "string" + }, + { + "internalType": "uint256", + "name": "delegateAmount", + "type": "uint256" + } + ], + "name": "scenario3_nativeTransferDelegateNativeTransfer", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address payable", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "string", + "name": "validatorAddr", + "type": "string" + }, + { + "internalType": "uint256", + "name": "delegateAmount", + "type": "uint256" + } + ], + "name": "scenario4_nativeTransferDelegateRevertNativeTransfer", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address payable", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "testNativeTransfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } + ], + "bytecode": "0x6080806040523461001657610ae2908161001c8239f35b600080fdfe60406080815260049081361015610020575b5050361561001e57600080fd5b005b600091823560e01c908163456503c8146105835781635b3d97d5146104b55781635c3f86c9146103905781636e348ad6146101af5781636f9fb98a14610194578163c489744b146100e457508063e1f756c3146100a95763efd8be620361001157346100a55760203660031901126100a55760209061009d6106bc565b319051908152f35b5080fd5b50806003193601126100a5578180806020946100c36106bc565b602435906001600160a01b03165af1906100db610992565b50519015158152f35b9190503461017a578060031936011261017a576100ff6106bc565b916024356001600160a01b038181169182900361019057602092602491855196879485936370a0823160e01b8552840152165afa91821561018657839261014b575b6020838351908152f35b9091506020813d821161017e575b81610166602093836106d7565b8101031261017a5760209250519038610141565b8280fd5b3d9150610159565b81513d85823e3d90fd5b8580fd5b5050346100a557816003193601126100a55751478152602090f35b9190503461017a576101c036610772565b855163a9059cbb60e01b8082526001600160a01b038681168a84019081526020808201889052959a94999895979590911693928b9291889082908190604001038186895af1908115610386579061021e918491610369575b5061082e565b8664e8d4a5100089519a600080516020610a8d8339815191528c80610243839f610872565b0390a16102658b519d8e9384936353266bbb60e01b855204903089850161089d565b0381856108005af1998a1561035f578798999a979697610342575b508989518061028e816108f9565b0390a16102bb8951978896879586948552840160209093929193604081019460018060a01b031681520152565b03925af191821561033857906102da9291869261030b575b505061094d565b516040808252600990820152683a3930b739b332b91960b91b60608201526001602082015280608081015b0390a180f35b61032a9250803d10610331575b61032281836106d7565b810190610816565b38806102d3565b503d610318565b83513d87823e3d90fd5b61035890873d89116103315761032281836106d7565b5038610280565b88513d84823e3d90fd5b6103809150893d8b116103315761032281836106d7565b38610218565b89513d85823e3d90fd5b91905061044c8380808064e8d4a5100060206103ab366107cc565b91946001600160a01b0390931693909290916103d6878080808a8a5af16103d0610992565b506109c2565b6104178c519d600080516020610a8d8339815191529e8f90806103f881610a0e565b0390a18d516353266bbb60e01b8152958694859404913090850161089d565b0381876108005af19081610497575b50610482578888518061043881610923565b0390a15b5af1610446610992565b50610a40565b5160408082526010908201526f3730ba34bb32afba3930b739b332b91960811b6060820152600160208201528060808101610305565b8888518061048f816108f9565b0390a161043c565b6104ae9060203d81116103315761032281836106d7565b5038610426565b9190508264e8d4a5100060206104ca366107cc565b91946001600160a01b0390931693909290916104ef878080808a8a5af16103d0610992565b61052f885199600080516020610a8d8339815191528b80610510839e610a0e565b0390a189516353266bbb60e01b8152958694859404913090850161089d565b0381876108005af18015610579579380938193829361044c9761055b575b5088885180610438816108f9565b6105729060203d81116103315761032281836106d7565b503861054d565b85513d86823e3d90fd5b9190503461017a5761059436610772565b855163a9059cbb60e01b8082526001600160a01b038681168a84019081526020808201889052939a9499989397959390911693928b929188908c908190604001038186895af19a8b156103865788999a9b6105f891859a999a91610369575061082e565b8664e8d4a510008b519c600080516020610a8d8339815191528e819f61061d81610872565b0390a161063f8d5194859384936353266bbb60e01b85520490308a850161089d565b0381866108005af1908161069f575b5061068a578989518061028e81610923565b6102bb8951978896879586948552840160209093929193604081019460018060a01b031681520152565b89895180610697816108f9565b0390a1610660565b6106b590883d8a116103315761032281836106d7565b503861064e565b600435906001600160a01b03821682036106d257565b600080fd5b90601f8019910116810190811067ffffffffffffffff8211176106f957604052565b634e487b7160e01b600052604160045260246000fd5b67ffffffffffffffff81116106f957601f01601f191660200190565b81601f820112156106d2578035906107428261070f565b9261075060405194856106d7565b828452602083830101116106d257816000926020809301838601378301015290565b9060a06003198301126106d2576001600160a01b039160043583811681036106d2579260243590811681036106d25791604435916064359067ffffffffffffffff82116106d2576107c59160040161072b565b9060843590565b60806003198201126106d2576004356001600160a01b03811681036106d25791602435916044359067ffffffffffffffff82116106d25761080f9160040161072b565b9060643590565b908160209103126106d2575180151581036106d25790565b1561083557565b60405162461bcd60e51b8152602060048201526015602482015274119a5c9cdd081d1c985b9cd9995c8819985a5b1959605a1b6044820152606490fd5b906040825260096040830152687472616e736665723160b81b60608301526001602060808401930152565b9392919060018060a01b03168452602060608186015281519182606087015260005b8381106108e55750505060808160008260409488010152601f8019910116850101930152565b8181018301518782016080015282016108bf565b9060408252600860408301526764656c656761746560c01b60608301526001602060808401930152565b9060408252600860408301526764656c656761746560c01b60608301526000602060808401930152565b1561095457565b60405162461bcd60e51b815260206004820152601660248201527514d958dbdb99081d1c985b9cd9995c8819985a5b195960521b6044820152606490fd5b3d156109bd573d906109a38261070f565b916109b160405193846106d7565b82523d6000602084013e565b606090565b156109c957565b60405162461bcd60e51b815260206004820152601c60248201527f4669727374206e6174697665207472616e73666572206661696c6564000000006044820152606490fd5b9060408252601060408301526f6e61746976655f7472616e736665723160801b60608301526001602060808401930152565b15610a4757565b60405162461bcd60e51b815260206004820152601d60248201527f5365636f6e64206e6174697665207472616e73666572206661696c65640000006044820152606490fdfe9f4d25774676d497fe3d8c1e43709b68b186fad01b05798c1410a178ff4ed7d0a264697066735822122041f6e19681792c3b477f80995457b249f902bd5716b911371fa6047817a468f064736f6c63430008140033", + "deployedBytecode": "0x60406080815260049081361015610020575b5050361561001e57600080fd5b005b600091823560e01c908163456503c8146105835781635b3d97d5146104b55781635c3f86c9146103905781636e348ad6146101af5781636f9fb98a14610194578163c489744b146100e457508063e1f756c3146100a95763efd8be620361001157346100a55760203660031901126100a55760209061009d6106bc565b319051908152f35b5080fd5b50806003193601126100a5578180806020946100c36106bc565b602435906001600160a01b03165af1906100db610992565b50519015158152f35b9190503461017a578060031936011261017a576100ff6106bc565b916024356001600160a01b038181169182900361019057602092602491855196879485936370a0823160e01b8552840152165afa91821561018657839261014b575b6020838351908152f35b9091506020813d821161017e575b81610166602093836106d7565b8101031261017a5760209250519038610141565b8280fd5b3d9150610159565b81513d85823e3d90fd5b8580fd5b5050346100a557816003193601126100a55751478152602090f35b9190503461017a576101c036610772565b855163a9059cbb60e01b8082526001600160a01b038681168a84019081526020808201889052959a94999895979590911693928b9291889082908190604001038186895af1908115610386579061021e918491610369575b5061082e565b8664e8d4a5100089519a600080516020610a8d8339815191528c80610243839f610872565b0390a16102658b519d8e9384936353266bbb60e01b855204903089850161089d565b0381856108005af1998a1561035f578798999a979697610342575b508989518061028e816108f9565b0390a16102bb8951978896879586948552840160209093929193604081019460018060a01b031681520152565b03925af191821561033857906102da9291869261030b575b505061094d565b516040808252600990820152683a3930b739b332b91960b91b60608201526001602082015280608081015b0390a180f35b61032a9250803d10610331575b61032281836106d7565b810190610816565b38806102d3565b503d610318565b83513d87823e3d90fd5b61035890873d89116103315761032281836106d7565b5038610280565b88513d84823e3d90fd5b6103809150893d8b116103315761032281836106d7565b38610218565b89513d85823e3d90fd5b91905061044c8380808064e8d4a5100060206103ab366107cc565b91946001600160a01b0390931693909290916103d6878080808a8a5af16103d0610992565b506109c2565b6104178c519d600080516020610a8d8339815191529e8f90806103f881610a0e565b0390a18d516353266bbb60e01b8152958694859404913090850161089d565b0381876108005af19081610497575b50610482578888518061043881610923565b0390a15b5af1610446610992565b50610a40565b5160408082526010908201526f3730ba34bb32afba3930b739b332b91960811b6060820152600160208201528060808101610305565b8888518061048f816108f9565b0390a161043c565b6104ae9060203d81116103315761032281836106d7565b5038610426565b9190508264e8d4a5100060206104ca366107cc565b91946001600160a01b0390931693909290916104ef878080808a8a5af16103d0610992565b61052f885199600080516020610a8d8339815191528b80610510839e610a0e565b0390a189516353266bbb60e01b8152958694859404913090850161089d565b0381876108005af18015610579579380938193829361044c9761055b575b5088885180610438816108f9565b6105729060203d81116103315761032281836106d7565b503861054d565b85513d86823e3d90fd5b9190503461017a5761059436610772565b855163a9059cbb60e01b8082526001600160a01b038681168a84019081526020808201889052939a9499989397959390911693928b929188908c908190604001038186895af19a8b156103865788999a9b6105f891859a999a91610369575061082e565b8664e8d4a510008b519c600080516020610a8d8339815191528e819f61061d81610872565b0390a161063f8d5194859384936353266bbb60e01b85520490308a850161089d565b0381866108005af1908161069f575b5061068a578989518061028e81610923565b6102bb8951978896879586948552840160209093929193604081019460018060a01b031681520152565b89895180610697816108f9565b0390a1610660565b6106b590883d8a116103315761032281836106d7565b503861064e565b600435906001600160a01b03821682036106d257565b600080fd5b90601f8019910116810190811067ffffffffffffffff8211176106f957604052565b634e487b7160e01b600052604160045260246000fd5b67ffffffffffffffff81116106f957601f01601f191660200190565b81601f820112156106d2578035906107428261070f565b9261075060405194856106d7565b828452602083830101116106d257816000926020809301838601378301015290565b9060a06003198301126106d2576001600160a01b039160043583811681036106d2579260243590811681036106d25791604435916064359067ffffffffffffffff82116106d2576107c59160040161072b565b9060843590565b60806003198201126106d2576004356001600160a01b03811681036106d25791602435916044359067ffffffffffffffff82116106d25761080f9160040161072b565b9060643590565b908160209103126106d2575180151581036106d25790565b1561083557565b60405162461bcd60e51b8152602060048201526015602482015274119a5c9cdd081d1c985b9cd9995c8819985a5b1959605a1b6044820152606490fd5b906040825260096040830152687472616e736665723160b81b60608301526001602060808401930152565b9392919060018060a01b03168452602060608186015281519182606087015260005b8381106108e55750505060808160008260409488010152601f8019910116850101930152565b8181018301518782016080015282016108bf565b9060408252600860408301526764656c656761746560c01b60608301526001602060808401930152565b9060408252600860408301526764656c656761746560c01b60608301526000602060808401930152565b1561095457565b60405162461bcd60e51b815260206004820152601660248201527514d958dbdb99081d1c985b9cd9995c8819985a5b195960521b6044820152606490fd5b3d156109bd573d906109a38261070f565b916109b160405193846106d7565b82523d6000602084013e565b606090565b156109c957565b60405162461bcd60e51b815260206004820152601c60248201527f4669727374206e6174697665207472616e73666572206661696c6564000000006044820152606490fd5b9060408252601060408301526f6e61746976655f7472616e736665723160801b60608301526001602060808401930152565b15610a4757565b60405162461bcd60e51b815260206004820152601d60248201527f5365636f6e64206e6174697665207472616e73666572206661696c65640000006044820152606490fdfe9f4d25774676d497fe3d8c1e43709b68b186fad01b05798c1410a178ff4ed7d0a264697066735822122041f6e19681792c3b477f80995457b249f902bd5716b911371fa6047817a468f064736f6c63430008140033", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/contracts/solidity/SequentialOperationsTester.sol b/contracts/solidity/SequentialOperationsTester.sol new file mode 100644 index 000000000..b5f5d10d3 --- /dev/null +++ b/contracts/solidity/SequentialOperationsTester.sol @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./precompiles/staking/StakingI.sol"; +import "./precompiles/erc20/IERC20.sol"; +import "./precompiles/bech32/Bech32I.sol"; +import "./precompiles/common/Types.sol"; + +/** + * @dev Contract to test sequential operations with auto-flush behavior. + * Tests combinations of ERC20 transfers, staking operations, and state changes. + */ +contract SequentialOperationsTester { + event OperationCompleted(string operation, bool success); + event BalanceChecked(string label, uint256 balance); + event EventCountChecked(string label, uint256 count); + + /// @dev Scenario 1: Transfer ERC20 -> Delegate -> Transfer ERC20 + /// @param token ERC20 token address + /// @param recipient Recipient for ERC20 transfers + /// @param amount Amount to transfer + /// @param validatorAddr Validator address (EVM format) + /// @param delegateAmount Amount to delegate + function scenario1_transferDelegateTransfer( + address token, + address recipient, + uint256 amount, + string memory validatorAddr, + uint256 delegateAmount + ) external { + // 1. Transfer ERC20 + require( + IERC20(token).transfer(recipient, amount), + "First transfer failed" + ); + emit OperationCompleted("transfer1", true); + + // 2. Staking delegate - convert from wei (18 decimals) to base denom (6 decimals) + uint256 delegateAmountBaseDenom = delegateAmount / 1e12; + STAKING_CONTRACT.delegate( + address(this), + validatorAddr, + delegateAmountBaseDenom + ); + emit OperationCompleted("delegate", true); + + // 3. Transfer ERC20 again + require( + IERC20(token).transfer(recipient, amount), + "Second transfer failed" + ); + emit OperationCompleted("transfer2", true); + } + + /// @dev Scenario 2: Transfer ERC20 -> Delegate (reverted & caught) -> Transfer ERC20 + /// @param token ERC20 token address + /// @param recipient Recipient for ERC20 transfers + /// @param amount Amount to transfer + /// @param validatorAddr Validator address (EVM format) + /// @param delegateAmount Amount to delegate (will revert) + function scenario2_transferDelegateRevertTransfer( + address token, + address recipient, + uint256 amount, + string memory validatorAddr, + uint256 delegateAmount + ) external { + // 1. Transfer ERC20 + require( + IERC20(token).transfer(recipient, amount), + "First transfer failed" + ); + emit OperationCompleted("transfer1", true); + + // 2. Try to delegate (will revert, catch it) + uint256 delegateAmountBaseDenom = delegateAmount / 1e12; + try STAKING_CONTRACT.delegate( + address(this), + validatorAddr, + delegateAmountBaseDenom + ) { + emit OperationCompleted("delegate", true); + } catch { + emit OperationCompleted("delegate", false); + } + + // 3. Transfer ERC20 again + require( + IERC20(token).transfer(recipient, amount), + "Second transfer failed" + ); + emit OperationCompleted("transfer2", true); + } + + /// @dev Scenario 3: Native transfer -> Delegate -> Native transfer + /// @param recipient Recipient for native transfers + /// @param amount Amount of native tokens to transfer (in wei) + /// @param validatorAddr Validator address + /// @param delegateAmount Amount to delegate (in wei, will be converted to base denom) + function scenario3_nativeTransferDelegateNativeTransfer( + address payable recipient, + uint256 amount, + string memory validatorAddr, + uint256 delegateAmount + ) external payable { + // 1. Transfer native tokens + (bool success1, ) = recipient.call{value: amount}(""); + require(success1, "First native transfer failed"); + emit OperationCompleted("native_transfer1", true); + + // 2. Staking delegate - convert from wei (18 decimals) to base denom (6 decimals) + uint256 delegateAmountBaseDenom = delegateAmount / 1e12; + STAKING_CONTRACT.delegate( + address(this), + validatorAddr, + delegateAmountBaseDenom + ); + emit OperationCompleted("delegate", true); + + // 3. Transfer native tokens again + (bool success2, ) = recipient.call{value: amount}(""); + require(success2, "Second native transfer failed"); + emit OperationCompleted("native_transfer2", true); + } + + /// @dev Scenario 4: Native transfer -> Delegate (reverted & caught) -> Native transfer + /// @param recipient Recipient for native transfers + /// @param amount Amount of native tokens to transfer (in wei) + /// @param validatorAddr Validator address + /// @param delegateAmount Amount to delegate (in wei, will be converted to base denom, will revert) + function scenario4_nativeTransferDelegateRevertNativeTransfer( + address payable recipient, + uint256 amount, + string memory validatorAddr, + uint256 delegateAmount + ) external payable { + // 1. Transfer native tokens + (bool success1, ) = recipient.call{value: amount}(""); + require(success1, "First native transfer failed"); + emit OperationCompleted("native_transfer1", true); + + // 2. Try to delegate - convert from wei (18 decimals) to base denom (6 decimals) + uint256 delegateAmountBaseDenom = delegateAmount / 1e12; + try STAKING_CONTRACT.delegate( + address(this), + validatorAddr, + delegateAmountBaseDenom + ) { + emit OperationCompleted("delegate", true); + } catch { + emit OperationCompleted("delegate", false); + } + + // 3. Transfer native tokens again + (bool success2, ) = recipient.call{value: amount}(""); + require(success2, "Second native transfer failed"); + emit OperationCompleted("native_transfer2", true); + } + + /// @dev Get balance of ERC20 token + function getTokenBalance(address token, address account) external view returns (uint256) { + return IERC20(token).balanceOf(account); + } + + /// @dev Get native balance + function getNativeBalance(address account) external view returns (uint256) { + return account.balance; + } + + /// @dev Test function to check contract balance + function getContractBalance() external view returns (uint256) { + return address(this).balance; + } + + /// @dev Simple native transfer test + function testNativeTransfer(address payable recipient, uint256 amount) external payable returns (bool) { + (bool success, ) = recipient.call{value: amount}(""); + return success; + } + + /// @dev Receive function to accept native tokens + receive() external payable {} +} diff --git a/contracts/solidity/eips/testdata/Counter.sol b/contracts/solidity/eips/testdata/Counter.sol new file mode 100644 index 000000000..30ba0869f --- /dev/null +++ b/contracts/solidity/eips/testdata/Counter.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: LGPL-3.0-only + +pragma solidity >=0.7.0 <0.9.0; + +contract Counter { + uint256 public counter = 1; + + function increment() external { + counter++; + } + + function decrement() external { + counter--; + } +} diff --git a/contracts/solidity/eips/testdata/CounterFactory.sol b/contracts/solidity/eips/testdata/CounterFactory.sol new file mode 100644 index 000000000..7b64412d2 --- /dev/null +++ b/contracts/solidity/eips/testdata/CounterFactory.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: LGPL-3.0-only + +pragma solidity >=0.7.0 <0.9.0; + +import "./Counter.sol"; + +contract Counterfactory { + Counter public counterInstance; + + constructor() { + counterInstance = new Counter(); + } + + function incrementCounter() public { + counterInstance.increment(); + } + + function decrementCounter() public { + counterInstance.decrement(); + } + + function getCounterValue() public view returns (uint256) { + return counterInstance.counter(); + } +} diff --git a/docs/migrations/PUSHCHAIN_v0.5.1_to_v0.6.0.md b/docs/migrations/PUSHCHAIN_v0.5.1_to_v0.6.0.md new file mode 100644 index 000000000..4ba05a2bb --- /dev/null +++ b/docs/migrations/PUSHCHAIN_v0.5.1_to_v0.6.0.md @@ -0,0 +1,283 @@ +# push-chain-evm — cosmos/evm v0.5.1 → v0.6.0 Upgrade + +This document describes how `push-chain-evm` (a heavily customized fork of +[`cosmos/evm`](https://github.com/cosmos/evm)) was upgraded from the **v0.5.1** +baseline to **v0.6.0**, how every upstream breaking change was absorbed, and — +most importantly — **how the push-chain–specific functionality was preserved**. + +> **Upstream reference:** the official cosmos/evm migration guide, +> *Migration v0.5 → v0.6*: +> +> +> Note: that page tracks the *latest* SDK/ibc-go line and refers to **ibc-go v11**. +> cosmos/evm **v0.6.0 itself** still pins **ibc-go v10**, so this fork stays on +> `github.com/cosmos/ibc-go/v10`. push-chain also pins **cosmos-sdk v0.50.x** +> (see [§7](#7-cosmos-sdk-v050-compatibility-the-overrideevents-shim)). + +--- + +## 1. Merge strategy & fork relationship + +`push-chain-evm`'s module path is `github.com/cosmos/evm`; it is a direct fork. +The relevant commit graph: + +``` +v0.5.1 (e2f6e47c) ──┬── 208 push-chain commits ──▶ HEAD (96231e7a) [the fork] + └── 2 commits ──────────────▶ v0.6.0 (6876c46d) [upstream] +``` + +* `merge-base(HEAD, v0.6.0) == v0.5.1`, so the upgrade is a clean three-way merge: + the upstream **v0.5.1 → v0.6.0 diff** is applied on top of the fork. +* `cosmos/evm` was added as the `upstream` remote and v0.6.0 fetched, then: + +```bash +git remote add upstream https://github.com/cosmos/evm.git +git fetch upstream --tags +git merge v0.6.0 # left UNCOMMITTED on branch evm-upgrade-0.6.0 +``` + +The v0.5.1→v0.6.0 delta touches ~101 files. Git auto-merged all but **3**; the +auto-merges were then verified semantically (see [§6](#6-auto-merged-files--semantic-verification)). + +--- + +## 2. What v0.6.0 changes (the breaking surface) + +| Area | Change | +|---|---| +| EVM execution API | `CallEVM`, `CallEVMWithData`, `ApplyMessage`, `ApplyMessageWithConfig` now take an explicit `stateDB *statedb.StateDB` (2nd param) and a `callFromPrecompile bool` (after `commit`). The caller now **owns** the StateDB; passing `nil` returns the new `ErrNilStateDB`. | +| StateDB | `CommitWithCacheCtx` → **`FlushToCacheCtx`**; `AddPrecompileFn`/`RevertMultiStore` drop their `events` argument; new `processedEventsCount` + `MarkEventProcessed`/`IsEventProcessed`; precompile-call event revert now uses `EventManager().OverrideEvents(...)`. | +| Custom IBC transfer | The bundled **`x/ibc/transfer`** module is **deleted**. ERC-20 ↔ IBC conversion moves to `x/erc20/keeper/convert.go` + the `erc20` IBC middleware + the ICS20 precompile. | +| ICS20 precompile | `ics20.NewPrecompile(...)` gains an `erc20Keeper cmn.ERC20Keeper` parameter. | +| `cmn.ERC20Keeper` | Gains `IsERC20Enabled`, `GetTokenPairID`, `ConvertERC20IntoCoinsForNativeToken(...)`. | +| Errors | New `types.ErrNilStateDB` (`"stateDB cannot be nil"`). | +| Dependencies | cosmos-sdk → v0.53.6, cometbft → v0.38.21, ledger-cosmos-go → v1.0.0, viper → v1.21.0, + `go.yaml.in/yaml/v3`; the `retract` block expands. | + +--- + +## 3. Conflict resolutions (3 files) + +### 3.1 `x/vm/keeper/call_evm.go` — the central conflict + +This file contains both push-chain's **derived-transaction** engine *and* the +upstream `CallEVM`/`CallEVMWithData` whose signatures changed. Resolution: + +* **Function signatures** were taken from v0.6.0 (`stateDB` + `callFromPrecompile`). +* **`CallEVMWithData`** was resolved to a **unified** body that honors *both* + contracts: + * It passes the caller-supplied `stateDB` straight through, so the v0.6.0 + nil-StateDB contract (`ErrNilStateDB`) is preserved — this is required by + upstream tests (`TestCallEVM`/`TestCallEVMWithData/…nil statedb`). + * It keeps push-chain's audit fix + (`a05ad85a — "use cache context in CallEVMWithData to prevent gas meter + overflow on revert"`) by **not** calling + `ResetGasMeterAndConsumeGas(ctx, ctx.GasMeter().Limit())` on a revert + (which can overflow the parent gas meter). In v0.6.0 the EVM already rolls + back the reverted call frame via its own snapshot, so the previous + cache-context wrap is no longer needed: + + ```go + res, err := k.ApplyMessage(ctx, stateDB, msg, nil, commit, callFromPrecompile, true) + if err != nil { + return nil, err + } + if res.Failed() { + // push-chain audit fix: do NOT ResetGasMeterAndConsumeGas here. + return res, errorsmod.Wrap(types.ErrVMExecution, res.VmError) + } + ctx.GasMeter().ConsumeGas(res.GasUsed, "apply evm message") + return res, nil + ``` + +* **`DerivedEVMCallWithData`** (push-chain) was adapted to the new + `ApplyMessageWithConfig` signature by creating its StateDB on the cache + context (`callFromPrecompile=false`, derived txs are not precompile calls): + + ```go + stateDB := statedb.New(tmpCtx, &k, txConfig) + res, err := k.ApplyMessageWithConfig(tmpCtx, stateDB, msg, nil, commit, false, cfg, txConfig, true, nil) + ``` + + All derived-tx semantics — the cache-context commit, ABCI event emission + (`ethereum_tx`/`tx_log`/`message`), the shared monotonic eth-tx index, the + gasless gas reporting, and the reverted-execution bloom/event guards — are + unchanged. + +### 3.2 `x/ibc/transfer/module.go` — modify/delete + +v0.6.0 deletes the entire custom transfer module; push-chain had only added a +`ConsensusVersion() = 5` override to it. The deletion was **accepted** +(`git rm -r x/ibc/transfer`) because v0.6.0 rewires every consumer +(`evmd/app.go`, `interfaces.go`, `precompiles/types/*`, `x/erc20/keeper/keeper.go`) +to the standard ibc-go transfer module + erc20 middleware. (The downstream +consequence of the `ConsensusVersion 5 → 6` change is documented in the +push-chain upgrade doc.) + +### 3.3 `Makefile` — systemtests legacy binary + +Base v0.5.1 built a `v0.4` legacy binary; v0.6.0 changed `test-system` to build a +`v0.5` binary via `git checkout v0.5.1`. push-chain had previously replaced that +fragile `git checkout` (which dirties `go.mod`) with an **isolated git worktree** +build (audit/CI fix `db3fbd13`). The resolution keeps push-chain's robust +worktree approach but retargets it to **v0.5.1** (`build-v05`), matching the +merged `tests/systemtests/upgrade_test.go` which now tests the +`v0.5.0-to-v0.6.0` upgrade and expects `binaries/v0.5/evmd`. + +--- + +## 4. push-chain customizations preserved (verified) + +| Feature | Where | Status | +|---|---|---| +| **Derived transactions** (`DerivedEVMCall` / `DerivedEVMCallWithData`) | `x/vm/keeper/call_evm.go` | Preserved; adapted to new `ApplyMessageWithConfig` signature. | +| **Derived-tx tracing** (`isUnsigned`, `unsignedTxAsMessage`, `from` param on `traceTx`/`TraceBlock`) | `x/vm/keeper/grpc_query.go` | Preserved (auto-merged with v0.6.0's per-call StateDB creation). | +| **baseFeeBurn** — `RefundGas(ctx, msg, leftoverGas, gasUsed, baseFee, denom)` burns `baseFee × gasUsed` | `x/vm/keeper/gas.go` + call in `state_transition.go` | Preserved. | +| **KV indexer derived-tx support** | `indexer/kv_indexer.go` | Preserved (untouched by v0.6.0). | +| **Derived-tx JSON-RPC** (receipts, logs, gas, tx_info) | `rpc/backend/*`, `rpc/types/*`, `rpc/namespaces/ethereum/eth/api.go` | Preserved (untouched by v0.6.0). | +| **CallEVMWithData gas-overflow audit fix** | `x/vm/keeper/call_evm.go` | Preserved (re-expressed for v0.6.0 — see §3.1). | +| **Audit fixes** F-2026-17736 / 17738 / 17740 / 17745 / 17752 / 17754 / 17775 | derived-tx + KV-indexer + RPC code | Preserved; regression tests pass. | +| **Param migration** (`v2/migrate_params.go`, `migrator.go`) | `x/vm/migrations/...` | Preserved. | +| **statedb nil-EventManager guard** | `x/vm/statedb/statedb.go` `New()` | Preserved (merged ahead of v0.6.0's `processedEventsCount` init, which would otherwise dereference a nil EventManager). | + +--- + +## 5. push-chain compatibility patches added during the upgrade + +### 5.1 `(*Keeper).NewStateDB(ctx)` — `x/vm/keeper/statedb.go` + +A small convenience that returns `statedb.New(ctx, k, statedb.NewEmptyTxConfig())`. +It lets external modules that consume the EVM keeper **through an interface** +(e.g. push-chain's `x/uexecutor`) construct the now-mandatory non-nil StateDB +without exposing all 12 `statedb.Keeper` methods on their interface. + +### 5.2 `overrideEventManagerEvents` shim — `x/vm/statedb/journal.go` + +See [§7](#7-cosmos-sdk-v050-compatibility-the-overrideevents-shim). + +### 5.3 Test coin-config nil-guard — `x/vm/types/denom_config_testing.go` + +The `-tags=test` coin-denom getters now fall back to a non-nil zero value when +the global `testingEvmCoinInfo` is momentarily nil. This mirrors the production +`getEvmCoinInfo()` nil-guard (CHANGELOG #816) and fixes a test-harness race +(see [§8](#8-testing--results)). + +### 5.4 `TestRefundGas/invalid GasPrice` fix — `tests/integration/x/vm/test_state_transition.go` + +A pre-existing push-chain test bug: it passed `leftoverGas = 0`, so +`remaining = leftoverGas × gasPrice = 0` and the negative-price error path was +never hit. Changed to `leftoverGas = params.TxGas` (with `expGasRefund = 0`) so +the case actually exercises the error. (Verified failing identically on +pre-merge HEAD.) + +--- + +## 6. Auto-merged files — semantic verification + +These were auto-merged by git and then verified to contain **both** sides: + +* `x/vm/keeper/grpc_query.go` — v0.6.0 adds `statedb.New(...)` before every + `ApplyMessageWithConfig` call (note the value vs. pointer receiver: + `statedb.New(ctx, &k, …)` in `EthCall`/`EstimateGasInternal`/predecessor loop, + `statedb.New(ctx, k, …)` in `traceTxWithMsg`); push-chain's derived-tx tracing + helpers and the `from common.Address` parameter are intact. +* `x/vm/keeper/state_transition.go` — v0.6.0's StateDB creation in + `ApplyTransaction` + the new `ApplyMessage(WithConfig)` signatures, alongside + push-chain's `RefundGas(..., res.GasUsed, cfg.BaseFee, ...)` baseFeeBurn call. +* `x/vm/statedb/statedb.go` — push-chain's nil-EventManager guard + + v0.6.0's `processedEventsCount`/`FlushToCacheCtx`. +* `x/vm/types/errors.go` — push-chain's raw-bytes `RevertError` + + v0.6.0's `ErrNilStateDB`. +* `go.mod` / `go.sum` / `evmd/go.mod` / `evmd/go.sum` — v0.6.0's dependency bumps + + push-chain's goja/regexp2/sourcemap (JS tracer) indirect deps; reconciled + with `go mod tidy`. + +The v0.6.0-only files that push-chain never modified were taken wholesale and +already use the new APIs: `x/erc20/keeper/{evm,msg_server,convert,ibc_callbacks}.go`, +`x/ibc/callbacks/keeper/keeper.go`, `precompiles/common/{precompile,balance_handler,interfaces}.go`, +`precompiles/ics20/*`, `evmd/app.go`, `interfaces.go`, `precompiles/types/*`. + +--- + +## 7. cosmos-sdk v0.50 compatibility: the `OverrideEvents` shim + +v0.6.0's precompile-call event revert calls +`s.cacheCtx.EventManager().OverrideEvents(pc.prevEvents)` in +`x/vm/statedb/journal.go`. **`EventManager.OverrideEvents` only exists in +cosmos-sdk v0.53**, but push-chain pins **cosmos-sdk v0.50.x** for the whole +application (the rest of v0.6.0 is v0.50-compatible). + +Bumping the parent's SDK constellation was rejected as out of scope — it would +cascade (`cosmossdk.io/collections v0.4 → v1.3`, `api v0.7.5 → v0.9.2`, +`x/tx v0.13 → v0.14`, …). Instead, a behavior-identical in-place shim was added: + +```go +// x/vm/statedb/journal.go +func overrideEventManagerEvents(em sdk.EventManagerI, events sdk.Events) { + f := reflect.ValueOf(em).Elem().FieldByName("events") + reflect.NewAt(f.Type(), unsafe.Pointer(f.UnsafeAddr())).Elem().Set(reflect.ValueOf(events)) +} +``` + +**Why in-place (and not a fresh-EventManager swap):** precompiles obtain the +cache context via `stateDB.GetCacheContext()`, which shares the *same* +`EventManager` pointer as `stateDB.cacheCtx`. `OverrideEvents` mutates that +manager in place, so all holders observe the revert. A swap +(`cacheCtx.WithEventManager(new)`) only updates `stateDB.cacheCtx` and diverges +from outstanding copies — this was confirmed to break +`x/vm/statedb/balance_events_test.go` and nested-precompile reverts. The shim +reproduces `OverrideEvents` exactly and works on both v0.50 and v0.53. + +> If push-chain ever moves to cosmos-sdk v0.53, revert this shim to the upstream +> `s.cacheCtx.EventManager().OverrideEvents(pc.prevEvents)` call. + +--- + +## 8. Testing & results + +Standalone builds and tests use the cosmos/evm `test` build tag: + +```bash +go build ./... # root module — clean +cd evmd && go build ./... # evmd example app — clean +go test -tags=test ./... # tests +``` + +**Passing** (push-chain-critical): + +* `indexer` (derived KV-indexer), `rpc/backend` (derived logs/gas/tracing/tx_info), + `x/vm/statedb` (incl. the precompile event-revert path through the shim). +* `evmd` `TestKeeperTestSuite` — all `TestDerivedEVMCall*`, `TestRefundGas` + (baseFeeBurn), `TestApplyMessage*` incl. the nil-StateDB cases, + `TestCallEVM`/`TestCallEVMWithData` incl. the nil-StateDB cases. +* `evmd` ERC20 keeper + ERC20 **precompile** integration (the + `callFromPrecompile=true` path), IBC callbacks, mempool, and the full + precompile suite. + +**Pre-existing failures (NOT introduced by this upgrade)** — each verified to +fail *identically* on pre-merge HEAD (`96231e7a`): + +* `evmd/.../precompiles/bank` `TestBankPrecompileIntegrationTestSuite` + (totalSupply / supplyOf). The queries are sent as **gas-paying transactions**; + push-chain's **baseFeeBurn** burns base fees and lowers total supply below the + upstream test's hard-coded expectation. (Fixing it would require teaching that + upstream test about baseFeeBurn.) +* `evmd/.../integration` `TestBackend/TestGetTransactionReceipt/success_-_tx_not_found` + — a pre-existing backend mock expectation. + +A mempool-test regression (`evmd/.../integration/mempool`) **was** introduced and +**fixed**: a leaked `legacypool` reorg goroutine of a torn-down test network read +the global `testingEvmCoinInfo` during a between-test reset nil-window (exposed by +v0.6.0 timing). Resolved by the test-coin-config nil-guard (§5.3); the suite now +passes deterministically. + +--- + +## 9. State of the repository + +* The merge is **staged but not committed** on branch `evm-upgrade-0.6.0` + (`git status` shows "All conflicts fixed but you are still merging"). Run + `git commit` to conclude it. +* The `upstream` remote (`github.com/cosmos/evm`) is retained. +* No state/store migration is required by the evm modules themselves: none of + `x/vm`, `x/erc20`, `x/feemarket`, `x/precisebank` bumped its `ConsensusVersion` + (all remain at their prior values); no store keys were added or removed. diff --git a/docs/migrations/v0.4.0_to_v0.5.0_UNRELEASED.md b/docs/migrations/v0.4.0_to_v0.5.0.md similarity index 100% rename from docs/migrations/v0.4.0_to_v0.5.0_UNRELEASED.md rename to docs/migrations/v0.4.0_to_v0.5.0.md diff --git a/docs/migrations/v0.5.x_to_v0.6.0.md b/docs/migrations/v0.5.x_to_v0.6.0.md new file mode 100644 index 000000000..a82bc13ad --- /dev/null +++ b/docs/migrations/v0.5.x_to_v0.6.0.md @@ -0,0 +1,491 @@ +# Cosmos EVM v0.5.x → v0.6.0 Migration + +## 0) Prep + +- Create a branch: `git switch -c upgrade/evm-v0.6`. +- Ensure a clean build + tests green pre-upgrade. +- Snapshot your current params/genesis for comparison later. + +--- + +## 1) Dependency bumps (go.mod) + +- Bump `github.com/cosmos/evm` to v0.6.0 and run: + +```bash +go mod tidy +``` + +--- + +## 2) App Wiring Changes + +### IBC Transfer Module + +v0.6.0 removes the custom IBC transfer keeper override and now uses the official IBC-Go transfer keeper directly. This means that **ERC20 conversions via Cosmos IBC transfer transactions are not possible**. These are now only handled in the ICS20 precompile, and any ERC20 transfer must be initiated through there. + +**Changes required in `app.go`:** + +1. **Update imports** - Replace custom transfer imports with official IBC-Go imports: + +```diff +- import ( +- "github.com/cosmos/evm/x/ibc/transfer" +- transferkeeper "github.com/cosmos/evm/x/ibc/transfer/keeper" +- transferv2 "github.com/cosmos/evm/x/ibc/transfer/v2" +- ibctransfer "github.com/cosmos/ibc-go/v10/modules/apps/transfer" +- ibctransfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" +- ) ++ import ( ++ transfer "github.com/cosmos/ibc-go/v10/modules/apps/transfer" ++ transferkeeper "github.com/cosmos/ibc-go/v10/modules/apps/transfer/keeper" ++ ibctransfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" ++ transferv2 "github.com/cosmos/ibc-go/v10/modules/apps/transfer/v2" ++ ) +``` + +2. **Update TransferKeeper initialization** - Remove ERC20 keeper parameter: + +```diff + app.TransferKeeper = transferkeeper.NewKeeper( + appCodec, + runtime.NewKVStoreService(keys[ibctransfertypes.StoreKey]), ++ nil, // ICS4Wrapper param + app.IBCKeeper.ChannelKeeper, + app.IBCKeeper.ChannelKeeper, + app.MsgServiceRouter(), + app.AccountKeeper, + app.BankKeeper, +- app.Erc20Keeper, // Remove: no longer passed to transfer keeper + authAddr, + ) +``` + +3. **Update module registration** - Use official transfer module: + +```diff + app.BasicModuleManager = module.NewBasicManager( + // ... other modules +- ibctransfertypes.ModuleName: transfer.AppModuleBasic{AppModuleBasic: &ibctransfer.AppModuleBasic{}}, ++ ibctransfertypes.ModuleName: transfer.AppModuleBasic{}, + ) +``` + +4. **Update ICS20 precompile wiring** - Pass ERC20 keeper to ICS20 precompile: + +```diff + precompiletypes.DefaultStaticPrecompiles( + *app.StakingKeeper, + app.DistrKeeper, + app.PreciseBankKeeper, + &app.Erc20Keeper, + &app.TransferKeeper, + app.IBCKeeper.ChannelKeeper, + app.GovKeeper, + app.SlashingKeeper, + appCodec, + ) +``` + +The ICS20 precompile now takes the ERC20 keeper as a parameter (instead of the transfer keeper receiving it). This allows the precompile to handle ERC20 conversions directly. + +--- + +## 3) Breaking API Changes + +### StateDB Requirements + +v0.6.0 introduces significant changes to event tracking and state management. All EVM execution functions now require an explicit `stateDB` parameter and a `callFromPrecompile` flag to properly handle event management and state transitions. + +**NOTE:** The only function calls affected are `CallEVM`, `CallEVMWithData`, `ApplyMessage`, and `ApplyMessageWithConfig`. These are typically used in common precompiles and logic that calls back into the EVM from the SDK. If your project does not use these functions, then no steps need to be taken for the upgrade. + +#### Advanced Changes + +The following functions have updated signatures: + +##### `CallEVM` + +**Before (v0.5.x):** +```go +func (k Keeper) CallEVM( + ctx sdk.Context, + abi abi.ABI, + from, contract common.Address, + commit bool, + gasCap *big.Int, + method string, + args ...interface{}, +) (*types.MsgEthereumTxResponse, error) +``` + +**After (v0.6.0):** +```go +func (k Keeper) CallEVM( + ctx sdk.Context, + stateDB *statedb.StateDB, + abi abi.ABI, + from, contract common.Address, + commit bool, + callFromPrecompile bool, + gasCap *big.Int, + method string, + args ...interface{}, +) (*types.MsgEthereumTxResponse, error) +``` + +##### `CallEVMWithData` + +**Before (v0.5.x):** +```go +func (k Keeper) CallEVMWithData( + ctx sdk.Context, + from common.Address, + contract *common.Address, + data []byte, + commit bool, + gasCap *big.Int, +) (*types.MsgEthereumTxResponse, error) +``` + +**After (v0.6.0):** +```go +func (k Keeper) CallEVMWithData( + ctx sdk.Context, + stateDB *statedb.StateDB, + from common.Address, + contract *common.Address, + data []byte, + commit bool, + callFromPrecompile bool, + gasCap *big.Int, +) (*types.MsgEthereumTxResponse, error) +``` + +##### `ApplyMessage` + +**Before (v0.5.x):** +```go +func (k *Keeper) ApplyMessage( + ctx sdk.Context, + msg core.Message, + tracer *tracing.Hooks, + commit bool, + internal bool, +) (*types.MsgEthereumTxResponse, error) +``` + +**After (v0.6.0):** +```go +func (k *Keeper) ApplyMessage( + ctx sdk.Context, + stateDB *statedb.StateDB, + msg core.Message, + tracer *tracing.Hooks, + commit bool, + callFromPrecompile bool, + internal bool, +) (*types.MsgEthereumTxResponse, error) +``` + +##### `ApplyMessageWithConfig` + +**Before (v0.5.x):** +```go +func (k *Keeper) ApplyMessageWithConfig( + ctx sdk.Context, + msg core.Message, + tracer *tracing.Hooks, + commit bool, + cfg *statedb.EVMConfig, + txConfig statedb.TxConfig, + internal bool, + overrides *rpctypes.StateOverride, +) (*types.MsgEthereumTxResponse, error) +``` + +**After (v0.6.0):** +```go +func (k *Keeper) ApplyMessageWithConfig( + ctx sdk.Context, + stateDB *statedb.StateDB, + msg core.Message, + tracer *tracing.Hooks, + commit bool, + callFromPrecompile bool, + cfg *statedb.EVMConfig, + txConfig statedb.TxConfig, + internal bool, + overrides *rpctypes.StateOverride, +) (*types.MsgEthereumTxResponse, error) +``` + +### Migration Steps + +#### For Non-Precompile Contexts + +If you're calling EVM functions from **outside** a precompile (e.g., from a module keeper, message server, or query handler): + +1. Create a new `stateDB` before calling EVM functions +2. Pass `false` for the `callFromPrecompile` parameter + +**Example:** + +```go +import ( + "github.com/cosmos/evm/x/vm/statedb" +) + +// Before (v0.5.x) +res, err := k.evmKeeper.CallEVM( + ctx, + abi, + from, + contract, + false, // commit + nil, // gasCap + "balanceOf", + account, +) + +// After (v0.6.0) +stateDB := statedb.New(ctx, k.evmKeeper, statedb.NewEmptyTxConfig()) +res, err := k.evmKeeper.CallEVM( + ctx, + stateDB, + abi, + from, + contract, + false, // commit + false, // callFromPrecompile + nil, // gasCap + "balanceOf", + account, +) +``` + +#### For Precompile Contexts + +If you're calling EVM functions from **within** a precompile: + +1. **Reuse the existing `stateDB`** from your precompile context (do not create a new one) +2. Pass `true` for the `callFromPrecompile` parameter +3. The existing `stateDB` is typically available as a parameter in your precompile function + +**Example:** + +```go +// In your precompile's Run() method, you'll have access to stateDB +func (p *MyPrecompile) Run( + evm *vm.EVM, + contract *vm.Contract, + readOnly bool, +) ([]byte, error) { + stateDB := evm.StateDB.(*statedb.StateDB) + + // Use the existing stateDB and set callFromPrecompile=true + res, err := p.evmKeeper.CallEVM( + ctx, + stateDB, // reuse existing stateDB + abi, + from, + contract, + true, // commit (will flush to cache context) + true, // callFromPrecompile + nil, // gasCap + "transfer", + recipient, + amount, + ) +} +``` + +### Important Notes + +- **Never pass `nil` for `stateDB`**: This will return `ErrNilStateDB` error +- **Commit behavior in precompiles**: When `commit=true` and `callFromPrecompile=true`, the state changes are flushed to the cache context rather than fully committed. This prevents collapsing the cache stack in nested call scenarios. + +### EVMKeeper Interface Changes + +If you implement or mock the `EVMKeeper` interface, update your implementation: + +```go +type EVMKeeper interface { + // Updated signatures + ApplyMessage( + ctx sdk.Context, + stateDB *statedb.StateDB, + msg core.Message, + tracer *tracing.Hooks, + commit, callFromPrecompile, internal bool, + ) (*evmtypes.MsgEthereumTxResponse, error) + + CallEVM( + ctx sdk.Context, + stateDB *statedb.StateDB, + abi abi.ABI, + from, contract common.Address, + commit, callFromPrecompile bool, + gasCap *big.Int, + method string, + args ...interface{}, + ) (*evmtypes.MsgEthereumTxResponse, error) + + CallEVMWithData( + ctx sdk.Context, + stateDB *statedb.StateDB, + from common.Address, + contract *common.Address, + data []byte, + commit bool, + callFromPrecompile bool, + gasCap *big.Int, + ) (*evmtypes.MsgEthereumTxResponse, error) + + // ... other methods +} +``` + +--- + +## 4) ERC20 Keeper Interface Changes + +The `ERC20Keeper` interface has new methods: + +```go +type ERC20Keeper interface { + // ... existing methods + + // New methods in v0.6.0 + IsERC20Enabled(ctx sdk.Context) bool + GetTokenPairID(ctx sdk.Context, token string) []byte + ConvertERC20IntoCoinsForNativeToken( + ctx sdk.Context, + stateDB *statedb.StateDB, + contract ethcommon.Address, + amount math.Int, + receiver sdk.AccAddress, + sender ethcommon.Address, + commit bool, + callFromPrecompile bool, + ) (*erc20types.MsgConvertERC20Response, error) +} +``` + +If you implement this interface, add these methods to your implementation. + +--- + +## 5) Error Handling + +A new error type has been added: + +```go +var ErrNilStateDB = errorsmod.Register(ModuleName, codeErrNilStateDB, "stateDB cannot be nil") +``` + +This error is returned when `nil` is passed as the `stateDB` parameter to EVM functions. + +--- + +## 6) Build & Tests + +```bash +go build ./... +go test ./... +``` + +### Testing Checklist + +After migration, verify: +- [ ] All EVM calls pass a valid `stateDB` +- [ ] Non-precompile calls use `callFromPrecompile=false` +- [ ] Precompile calls reuse existing `stateDB` and use `callFromPrecompile=true` +- [ ] Event emission works correctly in both success and revert scenarios +- [ ] State changes are properly committed or reverted + +--- + +## Common Migration Examples + +### Example 1: Module Keeper Query + +```go +// Before (v0.5.x) +func (k Keeper) QueryBalance(ctx sdk.Context, addr common.Address) (*big.Int, error) { + res, err := k.evmKeeper.CallEVM( + ctx, erc20ABI, moduleAddr, contract, false, nil, "balanceOf", addr, + ) + // ... +} + +// After (v0.6.0) +func (k Keeper) QueryBalance(ctx sdk.Context, addr common.Address) (*big.Int, error) { + stateDB := statedb.New(ctx, k.evmKeeper, statedb.NewEmptyTxConfig()) + res, err := k.evmKeeper.CallEVM( + ctx, stateDB, erc20ABI, moduleAddr, contract, false, false, nil, "balanceOf", addr, + ) + // ... +} +``` + +### Example 2: Message Server Transaction + +```go +// Before (v0.5.x) +func (ms msgServer) ConvertCoin( + goCtx context.Context, + msg *types.MsgConvertCoin + ) (*types.MsgConvertCoinResponse, error) { + ctx := sdk.UnwrapSDKContext(goCtx) + // ... + res, err := ms.evmKeeper.CallEVMWithData( + ctx, moduleAddr, &contract, data, true, nil, + ) + // ... +} + +// After (v0.6.0) +func (ms msgServer) ConvertCoin( + goCtx context.Context, + msg *types.MsgConvertCoin + ) (*types.MsgConvertCoinResponse, error) { + ctx := sdk.UnwrapSDKContext(goCtx) + // ... + stateDB := statedb.New(ctx, ms.evmKeeper, statedb.NewEmptyTxConfig()) + res, err := ms.evmKeeper.CallEVMWithData( + ctx, stateDB, moduleAddr, &contract, data, true, false, nil, + ) + // ... +} +``` + +### Example 3: Precompile Internal Call + +```go +// Before (v0.5.x) +func (p *StakingPrecompile) delegate( + ctx sdk.Context, + evm *vm.EVM, + // ... +) ([]byte, error) { + // ... delegate logic ... + res, err := p.evmKeeper.CallEVM( + ctx, delegationABI, from, contract, true, nil, "afterDelegate", + ) + // ... +} + +// After (v0.6.0) +func (p *StakingPrecompile) delegate( + ctx sdk.Context, + evm *vm.EVM, + stateDB *statedb.StateDB, // typically passed from Run() + // ... +) ([]byte, error) { + // ... delegate logic ... + res, err := p.evmKeeper.CallEVM( + ctx, stateDB, delegationABI, from, contract, true, true, nil, "afterDelegate", + ) + // ... +} +``` \ No newline at end of file diff --git a/ethereum/eip712/encoding.go b/ethereum/eip712/encoding.go index 5292fa4e4..c4cfa9899 100644 --- a/ethereum/eip712/encoding.go +++ b/ethereum/eip712/encoding.go @@ -14,7 +14,7 @@ import ( ) var ( - protoCodec codec.ProtoCodecMarshaler + protoCodec codec.Codec aminoCodec *codec.LegacyAmino eip155ChainID uint64 ) @@ -166,7 +166,7 @@ func decodeProtobufSignDoc(signDocBytes []byte) (apitypes.TypedData, error) { } // WrapTxToTypedData expects the payload as an Amino Sign Doc - signBytes := legacytx.StdSignBytes( + signBytes := legacytx.StdSignBytes( //nolint:staticcheck // check against deprecated type signDoc.ChainId, signDoc.AccountNumber, signerInfo.Sequence, diff --git a/ethereum/eip712/encoding_legacy.go b/ethereum/eip712/encoding_legacy.go index d0efecf50..760bf4813 100644 --- a/ethereum/eip712/encoding_legacy.go +++ b/ethereum/eip712/encoding_legacy.go @@ -175,7 +175,7 @@ func legacyDecodeProtobufSignDoc(signDocBytes []byte, eip155ChainID uint64) (api } // WrapTxToTypedData expects the payload as an Amino Sign Doc - signBytes := legacytx.StdSignBytes( + signBytes := legacytx.StdSignBytes( //nolint:staticcheck // checking legacy type signDoc.ChainId, signDoc.AccountNumber, signerInfo.Sequence, diff --git a/evmd/app.go b/evmd/app.go index f07b0a471..43dfe3e75 100644 --- a/evmd/app.go +++ b/evmd/app.go @@ -37,9 +37,6 @@ import ( feemarkettypes "github.com/cosmos/evm/x/feemarket/types" ibccallbackskeeper "github.com/cosmos/evm/x/ibc/callbacks/keeper" - "github.com/cosmos/evm/x/ibc/transfer" - transferkeeper "github.com/cosmos/evm/x/ibc/transfer/keeper" - transferv2 "github.com/cosmos/evm/x/ibc/transfer/v2" "github.com/cosmos/evm/x/precisebank" precisebankkeeper "github.com/cosmos/evm/x/precisebank/keeper" precisebanktypes "github.com/cosmos/evm/x/precisebank/types" @@ -48,8 +45,10 @@ import ( evmtypes "github.com/cosmos/evm/x/vm/types" "github.com/cosmos/gogoproto/proto" ibccallbacks "github.com/cosmos/ibc-go/v10/modules/apps/callbacks" - ibctransfer "github.com/cosmos/ibc-go/v10/modules/apps/transfer" + transfer "github.com/cosmos/ibc-go/v10/modules/apps/transfer" + transferkeeper "github.com/cosmos/ibc-go/v10/modules/apps/transfer/keeper" ibctransfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" + transferv2 "github.com/cosmos/ibc-go/v10/modules/apps/transfer/v2" ibc "github.com/cosmos/ibc-go/v10/modules/core" porttypes "github.com/cosmos/ibc-go/v10/modules/core/05-port/types" ibcapi "github.com/cosmos/ibc-go/v10/modules/core/api" @@ -405,7 +404,7 @@ func NewExampleApp( app.GovKeeper = *govKeeper.SetHooks( govtypes.NewMultiGovHooks( - // register the governance hooks + // register the governance hooks ), ) @@ -483,12 +482,12 @@ func NewExampleApp( app.TransferKeeper = transferkeeper.NewKeeper( appCodec, runtime.NewKVStoreService(keys[ibctransfertypes.StoreKey]), + nil, app.IBCKeeper.ChannelKeeper, app.IBCKeeper.ChannelKeeper, app.MsgServiceRouter(), app.AccountKeeper, app.BankKeeper, - app.Erc20Keeper, // Add ERC20 Keeper for ERC20 transfers authAddr, ) app.TransferKeeper.SetAddressCodec(evmaddress.NewEvmCodec(sdk.GetConfig().GetBech32AccountAddrPrefix())) @@ -585,7 +584,7 @@ func NewExampleApp( genutiltypes.ModuleName: genutil.NewAppModuleBasic(genutiltypes.DefaultMessageValidator), stakingtypes.ModuleName: staking.AppModuleBasic{}, govtypes.ModuleName: gov.NewAppModuleBasic(nil), - ibctransfertypes.ModuleName: transfer.AppModuleBasic{AppModuleBasic: &ibctransfer.AppModuleBasic{}}, + ibctransfertypes.ModuleName: transfer.AppModuleBasic{}, }, ) app.BasicModuleManager.RegisterLegacyAminoCodec(legacyAmino) diff --git a/evmd/go.mod b/evmd/go.mod index 85af9c70d..4f2878ef5 100644 --- a/evmd/go.mod +++ b/evmd/go.mod @@ -14,9 +14,9 @@ require ( cosmossdk.io/x/evidence v0.2.0 cosmossdk.io/x/feegrant v0.2.0 cosmossdk.io/x/upgrade v0.2.0 - github.com/cometbft/cometbft v0.38.19 + github.com/cometbft/cometbft v0.38.21 github.com/cosmos/cosmos-db v1.1.3 - github.com/cosmos/cosmos-sdk v0.53.5-0.20251030204916-768cb210885c + github.com/cosmos/cosmos-sdk v0.53.6 github.com/cosmos/evm v0.2.0 github.com/cosmos/gogoproto v1.7.2 github.com/cosmos/ibc-go/v10 v10.3.1-0.20250909102629-ed3b125c7b6f @@ -26,7 +26,7 @@ require ( github.com/spf13/cast v1.10.0 github.com/spf13/cobra v1.10.1 github.com/spf13/pflag v1.0.10 - github.com/spf13/viper v1.20.1 + github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 golang.org/x/sync v0.16.0 google.golang.org/grpc v1.75.0 @@ -87,7 +87,7 @@ require ( github.com/cosmos/gogogateway v1.2.0 // indirect github.com/cosmos/iavl v1.2.2 // indirect github.com/cosmos/ics23/go v0.11.0 // indirect - github.com/cosmos/ledger-cosmos-go v0.16.0 // indirect + github.com/cosmos/ledger-cosmos-go v1.0.0 // indirect github.com/crate-crypto/go-eth-kzg v1.3.0 // indirect github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect github.com/creachadair/atomicfile v0.3.7 // indirect @@ -207,11 +207,11 @@ require ( github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rs/cors v1.11.1 // indirect github.com/rs/zerolog v1.34.0 // indirect - github.com/sagikazarmark/locafero v0.9.0 // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/sasha-s/go-deadlock v0.3.5 // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect - github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.14.0 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect @@ -249,6 +249,7 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/arch v0.17.0 // indirect golang.org/x/crypto v0.41.0 // indirect golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect diff --git a/evmd/go.sum b/evmd/go.sum index 4ca463434..2253ef656 100644 --- a/evmd/go.sum +++ b/evmd/go.sum @@ -834,8 +834,8 @@ github.com/cockroachdb/redact v1.1.6/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZ github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= -github.com/cometbft/cometbft v0.38.19 h1:vNdtCkvhuwUlrcLPAyigV7lQpmmo+tAq8CsB8gZjEYw= -github.com/cometbft/cometbft v0.38.19/go.mod h1:UCu8dlHqvkAsmAFmWDRWNZJPlu6ya2fTWZlDrWsivwo= +github.com/cometbft/cometbft v0.38.21 h1:qcIJSH9LiwU5s6ZgKR5eRbsLNucbubfraDs5bzgjtOI= +github.com/cometbft/cometbft v0.38.21/go.mod h1:UCu8dlHqvkAsmAFmWDRWNZJPlu6ya2fTWZlDrWsivwo= github.com/cometbft/cometbft-db v0.14.1 h1:SxoamPghqICBAIcGpleHbmoPqy+crij/++eZz3DlerQ= github.com/cometbft/cometbft-db v0.14.1/go.mod h1:KHP1YghilyGV/xjD5DP3+2hyigWx0WTp9X+0Gnx0RxQ= github.com/consensys/gnark-crypto v0.18.0 h1:vIye/FqI50VeAr0B3dx+YjeIvmc3LWz4yEfbWBpTUf0= @@ -852,8 +852,8 @@ github.com/cosmos/cosmos-db v1.1.3 h1:7QNT77+vkefostcKkhrzDK9uoIEryzFrU9eoMeaQOP github.com/cosmos/cosmos-db v1.1.3/go.mod h1:kN+wGsnwUJZYn8Sy5Q2O0vCYA99MJllkKASbs6Unb9U= github.com/cosmos/cosmos-proto v1.0.0-beta.5 h1:eNcayDLpip+zVLRLYafhzLvQlSmyab+RC5W7ZfmxJLA= github.com/cosmos/cosmos-proto v1.0.0-beta.5/go.mod h1:hQGLpiIUloJBMdQMMWb/4wRApmI9hjHH05nefC0Ojec= -github.com/cosmos/cosmos-sdk v0.53.5-0.20251030204916-768cb210885c h1:HMVLvm0q3ahGvsyExkSCBcmvcdItMpTxAh4jllL4rJ4= -github.com/cosmos/cosmos-sdk v0.53.5-0.20251030204916-768cb210885c/go.mod h1:nifazrMGFjpmOuaVIZBQ8akQc160imzySYFEA8A7tus= +github.com/cosmos/cosmos-sdk v0.53.6 h1:aJeInld7rbsHtH1qLHu2aZJF9t40mGlqp3ylBLDT0HI= +github.com/cosmos/cosmos-sdk v0.53.6/go.mod h1:N6YuprhAabInbT3YGumGDKONbvPX5dNro7RjHvkQoKE= github.com/cosmos/go-bip39 v1.0.0 h1:pcomnQdrdH22njcAatO0yWojsUnCO3y2tNoV1cb6hHY= github.com/cosmos/go-bip39 v1.0.0/go.mod h1:RNJv0H/pOIVgxw6KS7QeX2a0Uo0aKUlfhZ4xuwvCdJw= github.com/cosmos/go-ethereum v1.16.2-cosmos-1 h1:QIaIS6HIdPSBdTvpFhxswhMLUJgcr4irbd2o9ZKldAI= @@ -871,8 +871,8 @@ github.com/cosmos/ics23/go v0.11.0 h1:jk5skjT0TqX5e5QJbEnwXIS2yI2vnmLOgpQPeM5Rtn github.com/cosmos/ics23/go v0.11.0/go.mod h1:A8OjxPE67hHST4Icw94hOxxFEJMBG031xIGF/JHNIY0= github.com/cosmos/keyring v1.2.0 h1:8C1lBP9xhImmIabyXW4c3vFjjLiBdGCmfLUfeZlV1Yo= github.com/cosmos/keyring v1.2.0/go.mod h1:fc+wB5KTk9wQ9sDx0kFXB3A0MaeGHM9AwRStKOQ5vOA= -github.com/cosmos/ledger-cosmos-go v0.16.0 h1:YKlWPG9NnGZIEUb2bEfZ6zhON1CHlNTg0QKRRGcNEd0= -github.com/cosmos/ledger-cosmos-go v0.16.0/go.mod h1:WrM2xEa8koYoH2DgeIuZXNarF7FGuZl3mrIOnp3Dp0o= +github.com/cosmos/ledger-cosmos-go v1.0.0 h1:jNKW89nPf0vR0EkjHG8Zz16h6p3zqwYEOxlHArwgYtw= +github.com/cosmos/ledger-cosmos-go v1.0.0/go.mod h1:mGaw2wDOf+Z6SfRJsMGxU9DIrBa4du0MAiPlpPhLAOE= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= @@ -1607,8 +1607,8 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k= -github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk= +github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/sasha-s/go-deadlock v0.3.5 h1:tNCOEEDG6tBqrNDOX35j/7hL5FcFViG6awUGROb2NsU= github.com/sasha-s/go-deadlock v0.3.5/go.mod h1:bugP6EGbdGYObIlx7pUZtWqlvo8k9H6vCBBsiChJQ5U= @@ -1626,14 +1626,14 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1 github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= -github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= -github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= -github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= @@ -1644,8 +1644,8 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= -github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= @@ -1795,8 +1795,8 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= -go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE= -go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU= diff --git a/evmd/tests/ibc/autoflush_test.go b/evmd/tests/ibc/autoflush_test.go new file mode 100644 index 000000000..f0abbe057 --- /dev/null +++ b/evmd/tests/ibc/autoflush_test.go @@ -0,0 +1,967 @@ +package ibc + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/suite" + + "github.com/cosmos/evm/contracts" + "github.com/cosmos/evm/evmd" + "github.com/cosmos/evm/evmd/tests/integration" + "github.com/cosmos/evm/testutil" + evmibctesting "github.com/cosmos/evm/testutil/ibc" + testutiltypes "github.com/cosmos/evm/testutil/types" + erc20types "github.com/cosmos/evm/x/erc20/types" + evmtypes "github.com/cosmos/evm/x/vm/types" + + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// Test constants +const ( + AutoFlushInitialTokenAmount int64 = 1_000_000_000_000_000_000 // 1 token with 18 decimals + AutoFlushDelegationAmount int64 = 100_000_000_000_000_000 // 0.1 token for delegation + AutoFlushTransferAmount int64 = 50_000_000_000_000_000 // 0.05 token for transfers + AutoFlushNativeAmount int64 = 1_000_000_000_000_000_000 // 1 native token (larger to avoid fractional issues) + AutoFlushSenderIndex = 1 +) + +// Test suite for auto-flush behavior with various operation combinations +type AutoFlushTestSuite struct { + suite.Suite + + coordinator *evmibctesting.Coordinator + chain *evmibctesting.TestChain +} + +func (suite *AutoFlushTestSuite) SetupTest() { + suite.coordinator = evmibctesting.NewCoordinator(suite.T(), 1, 0, integration.SetupEvmd) + suite.chain = suite.coordinator.GetChain(evmibctesting.GetEvmChainID(1)) +} + +func TestAutoFlushTestSuite(t *testing.T) { + suite.Run(t, new(AutoFlushTestSuite)) +} + +// Helper: Deploy and setup ERC20 token +func (suite *AutoFlushTestSuite) deployAndRegisterERC20(name, symbol string) (common.Address, evmtypes.CompiledContract) { + evmApp := suite.chain.App.(*evmd.EVMD) + + // Deploy ERC20 + erc20ContractData := contracts.ERC20MinterBurnerDecimalsContract + deploymentData := testutiltypes.ContractDeploymentData{ + Contract: erc20ContractData, + ConstructorArgs: []interface{}{name, symbol, uint8(18)}, + } + + erc20Addr, err := DeployContract(suite.T(), suite.chain, deploymentData) + suite.chain.NextBlock() + suite.Require().NoError(err) + + // Register ERC20 + _, err = evmApp.Erc20Keeper.RegisterERC20(suite.chain.GetContext(), &erc20types.MsgRegisterERC20{ + Signer: evmApp.AccountKeeper.GetModuleAddress("gov").String(), + Erc20Addresses: []string{erc20Addr.Hex()}, + }) + suite.Require().NoError(err) + suite.chain.NextBlock() + + return erc20Addr, erc20ContractData +} + +// Helper: Get validator address +func (suite *AutoFlushTestSuite) getValidatorAddress() string { + evmApp := suite.chain.App.(*evmd.EVMD) + ctx := suite.chain.GetContext() + + vals, err := evmApp.StakingKeeper.GetAllValidators(ctx) + suite.Require().NoError(err) + suite.Require().Greater(len(vals), 0, "no validators found") + + return vals[0].OperatorAddress +} + +// Helper: Mint tokens to address +func (suite *AutoFlushTestSuite) mintTokens(tokenAddr common.Address, erc20Data evmtypes.CompiledContract, recipient common.Address, amount *big.Int) { + evmApp := suite.chain.App.(*evmd.EVMD) + deployerAddr := common.BytesToAddress(suite.chain.SenderPrivKey.PubKey().Address().Bytes()) + + stateDB := testutil.NewStateDB(suite.chain.GetContext(), evmApp.EVMKeeper) + _, err := evmApp.GetEVMKeeper().CallEVM( + suite.chain.GetContext(), + stateDB, + erc20Data.ABI, + deployerAddr, + tokenAddr, + true, + false, + nil, + "mint", + recipient, + amount, + ) + suite.Require().NoError(err) + suite.chain.NextBlock() +} + +// Helper: Fund contract with native tokens +func (suite *AutoFlushTestSuite) fundContractNative(contractAddr sdk.AccAddress, amount sdkmath.Int) { + evmApp := suite.chain.App.(*evmd.EVMD) + bondDenom, err := evmApp.StakingKeeper.BondDenom(suite.chain.GetContext()) + suite.Require().NoError(err) + + coins := sdk.NewCoins(sdk.NewCoin(bondDenom, amount)) + err = evmApp.GetBankKeeper().MintCoins(suite.chain.GetContext(), "mint", coins) + suite.Require().NoError(err) + + err = evmApp.GetBankKeeper().SendCoinsFromModuleToAccount( + suite.chain.GetContext(), + "mint", + contractAddr, + coins, + ) + suite.Require().NoError(err) + suite.chain.NextBlock() +} + +// Helper: Count events and print them +func (suite *AutoFlushTestSuite) countEvents(ctx sdk.Context) int { + return len(ctx.EventManager().Events()) +} + + +// Helper: Check balances and events +func (suite *AutoFlushTestSuite) verifyState( + label string, + expectedEventCount int, + expectedBalances map[common.Address]map[common.Address]*big.Int, // tokenAddr -> holderAddr -> balance + expectedNativeBalances map[string]sdkmath.Int, // bech32 address -> balance + expectedAccountsCreated int, +) { + evmApp := suite.chain.App.(*evmd.EVMD) + ctx := suite.chain.GetContext() + + // Check event count + actualEventCount := suite.countEvents(ctx) + suite.Require().Equal(expectedEventCount, actualEventCount, + "%s: event count mismatch - expected %d, got %d", label, expectedEventCount, actualEventCount) + + // Check ERC20 balances + for tokenAddr, holders := range expectedBalances { + tokenPairID := evmApp.Erc20Keeper.GetTokenPairID(ctx, "erc20:"+tokenAddr.Hex()) + tokenPair, found := evmApp.Erc20Keeper.GetTokenPair(ctx, tokenPairID) + suite.Require().True(found, "%s: token pair not found for %s", label, tokenAddr.Hex()) + + erc20Data := contracts.ERC20MinterBurnerDecimalsContract + for holderAddr, expectedBal := range holders { + actualBal := evmApp.GetErc20Keeper().BalanceOf(ctx, erc20Data.ABI, tokenPair.GetERC20Contract(), holderAddr) + suite.Require().Equal(expectedBal.String(), actualBal.String(), + "%s: ERC20 balance mismatch for %s holding %s", label, holderAddr.Hex(), tokenAddr.Hex()) + } + } + + // Check native balances + for addrStr, expectedBal := range expectedNativeBalances { + bondDenom, err := evmApp.StakingKeeper.BondDenom(ctx) + suite.Require().NoError(err) + + addr, err := sdk.AccAddressFromBech32(addrStr) + suite.Require().NoError(err) + + actualBal := evmApp.GetBankKeeper().GetBalance(ctx, addr, bondDenom) + suite.Require().Equal(expectedBal.String(), actualBal.Amount.String(), + "%s: native balance mismatch for %s", label, addrStr) + } + + // TODO: Check account creation count if needed + _ = expectedAccountsCreated +} + +// Scenario 1: Transfer ERC20 -> Delegate -> Transfer ERC20 +func (suite *AutoFlushTestSuite) TestScenario1_TransferDelegateTransfer() { + suite.SetupTest() + + evmApp := suite.chain.App.(*evmd.EVMD) + ctx := suite.chain.GetContext() + + // Deploy test contract + contractData, err := contracts.LoadSequentialOperationsTester() + suite.Require().NoError(err) + + deploymentData := testutiltypes.ContractDeploymentData{ + Contract: contractData, + ConstructorArgs: []interface{}{}, + } + + contractAddr, err := DeployContract(suite.T(), suite.chain, deploymentData) + suite.chain.NextBlock() + suite.Require().NoError(err) + + contractAddrSDK := sdk.AccAddress(contractAddr.Bytes()) + + // Deploy and register ERC20 + tokenAddr, erc20Data := suite.deployAndRegisterERC20("TestToken", "TT") + + // Mint tokens to contract + suite.mintTokens(tokenAddr, erc20Data, contractAddr, big.NewInt(AutoFlushInitialTokenAmount)) + + // Fund contract with native tokens for delegation + suite.fundContractNative(contractAddrSDK, sdkmath.NewInt(AutoFlushDelegationAmount*2)) + + // Get recipient and validator + senderAccount := suite.chain.SenderAccounts[AutoFlushSenderIndex] + recipientAddr := common.BytesToAddress(senderAccount.SenderAccount.GetAddress().Bytes()) + validatorAddr := suite.getValidatorAddress() + + // Get balances before + ctx = suite.chain.GetContext() + contractBalBefore := evmApp.GetErc20Keeper().BalanceOf(ctx, erc20Data.ABI, tokenAddr, contractAddr) + recipientBalBefore := evmApp.GetErc20Keeper().BalanceOf(ctx, erc20Data.ABI, tokenAddr, recipientAddr) + + // Pack the call data + callData, err := contractData.ABI.Pack( + "scenario1_transferDelegateTransfer", + tokenAddr, + recipientAddr, + big.NewInt(AutoFlushTransferAmount), + validatorAddr, + big.NewInt(AutoFlushDelegationAmount), + ) + suite.Require().NoError(err) + + // Execute via SendEvmTx to get proper event tracking + res, _, _, err := suite.chain.SendEvmTx( + senderAccount, + AutoFlushSenderIndex, + contractAddr, + big.NewInt(0), + callData, + 0, + ) + suite.Require().NoError(err) + + // Get balances after + ctx = suite.chain.GetContext() + contractBalAfter := evmApp.GetErc20Keeper().BalanceOf(ctx, erc20Data.ABI, tokenAddr, contractAddr) + recipientBalAfter := evmApp.GetErc20Keeper().BalanceOf(ctx, erc20Data.ABI, tokenAddr, recipientAddr) + + expectedDelta := new(big.Int).Mul(big.NewInt(AutoFlushTransferAmount), big.NewInt(2)) + actualDelta := new(big.Int).Sub(contractBalBefore, contractBalAfter) + + // Verify balances + suite.Require().Equal(expectedDelta.String(), actualDelta.String(), "transfer amount mismatch") + suite.Require().Equal( + new(big.Int).Mul(big.NewInt(AutoFlushTransferAmount), big.NewInt(2)).String(), + new(big.Int).Sub(recipientBalAfter, recipientBalBefore).String(), + "recipient should receive 2x transfer amount", + ) + + // Verify delegation occurred + delegations, err := evmApp.StakingKeeper.GetAllDelegatorDelegations(ctx, contractAddrSDK) + suite.Require().NoError(err) + suite.Require().Equal(len(delegations), 1, "should have at 1 delegation") + bondedTokens, err := evmApp.StakingKeeper.GetDelegatorBonded(suite.chain.GetContext(), contractAddrSDK) + suite.Require().NoError(err) + suite.Require().Equal(AutoFlushDelegationAmount/1_000_000_000_000, bondedTokens.Int64()) + + suite.Require().Equal(DelegationEventCount+EVMEventCount, len(res.Events)) // 8 events, no gas token transfer +} + +// Scenario 2: Transfer ERC20 -> Delegate (reverted & caught) -> Transfer ERC20 +func (suite *AutoFlushTestSuite) TestScenario2_TransferDelegateRevertTransfer() { + suite.SetupTest() + + evmApp := suite.chain.App.(*evmd.EVMD) + ctx := suite.chain.GetContext() + + // Deploy test contract + contractData, err := contracts.LoadSequentialOperationsTester() + suite.Require().NoError(err) + + deploymentData := testutiltypes.ContractDeploymentData{ + Contract: contractData, + ConstructorArgs: []interface{}{}, + } + + contractAddr, err := DeployContract(suite.T(), suite.chain, deploymentData) + suite.chain.NextBlock() + suite.Require().NoError(err) + + // Deploy and register ERC20 + tokenAddr, erc20Data := suite.deployAndRegisterERC20("TestToken2", "TT2") + + // Mint tokens to contract + suite.mintTokens(tokenAddr, erc20Data, contractAddr, big.NewInt(AutoFlushInitialTokenAmount)) + + // Get recipient and validator + senderAccount := suite.chain.SenderAccounts[AutoFlushSenderIndex] + recipientAddr := common.BytesToAddress(senderAccount.SenderAccount.GetAddress().Bytes()) + validatorAddr := suite.getValidatorAddress() + + // Get balances before + ctx = suite.chain.GetContext() + contractBalBefore := evmApp.GetErc20Keeper().BalanceOf(ctx, erc20Data.ABI, tokenAddr, contractAddr) + recipientBalBefore := evmApp.GetErc20Keeper().BalanceOf(ctx, erc20Data.ABI, tokenAddr, recipientAddr) + + // Pack the call data with excessive delegation amount (will revert and be caught) + excessiveAmount := new(big.Int).Mul(big.NewInt(AutoFlushDelegationAmount), big.NewInt(1000)) + callData, err := contractData.ABI.Pack( + "scenario2_transferDelegateRevertTransfer", + tokenAddr, + recipientAddr, + big.NewInt(AutoFlushTransferAmount), + validatorAddr, + excessiveAmount, // Will fail - insufficient balance + ) + suite.Require().NoError(err) + + // Execute via SendEvmTx to get proper event tracking + res, _, _, err := suite.chain.SendEvmTx( + senderAccount, + AutoFlushSenderIndex, + contractAddr, + big.NewInt(0), + callData, + 0, + ) + suite.Require().NoError(err) + + // Get balances after + ctx = suite.chain.GetContext() + contractBalAfter := evmApp.GetErc20Keeper().BalanceOf(ctx, erc20Data.ABI, tokenAddr, contractAddr) + recipientBalAfter := evmApp.GetErc20Keeper().BalanceOf(ctx, erc20Data.ABI, tokenAddr, recipientAddr) + + expectedDelta := new(big.Int).Mul(big.NewInt(AutoFlushTransferAmount), big.NewInt(2)) + actualDelta := new(big.Int).Sub(contractBalBefore, contractBalAfter) + + // Verify balances - both transfers should succeed even though delegate failed + suite.Require().Equal(expectedDelta.String(), actualDelta.String(), "transfer amount mismatch") + suite.Require().Equal( + new(big.Int).Mul(big.NewInt(AutoFlushTransferAmount), big.NewInt(2)).String(), + new(big.Int).Sub(recipientBalAfter, recipientBalBefore).String(), + "recipient should receive 2x transfer amount", + ) + + // Verify NO delegation occurred (it was reverted) + contractAddrSDK := sdk.AccAddress(contractAddr.Bytes()) + delegations, err := evmApp.StakingKeeper.GetAllDelegatorDelegations(ctx, contractAddrSDK) + suite.Require().NoError(err) + suite.Require().Equal(0, len(delegations), "should have no delegations since it reverted") + bondedTokens, err := evmApp.StakingKeeper.GetDelegatorBonded(suite.chain.GetContext(), contractAddrSDK) + suite.Require().NoError(err) + suite.Require().Equal(int64(0), bondedTokens.Int64()) + + suite.Require().Equal(EVMEventCount, len(res.Events)) +} + +// Scenario 3: Native transfer -> Delegate -> Native transfer +func (suite *AutoFlushTestSuite) TestScenario3_NativeTransferDelegateNativeTransfer() { + suite.SetupTest() + + evmApp := suite.chain.App.(*evmd.EVMD) + ctx := suite.chain.GetContext() + + // Deploy test contract + contractData, err := contracts.LoadSequentialOperationsTester() + suite.Require().NoError(err) + + deploymentData := testutiltypes.ContractDeploymentData{ + Contract: contractData, + ConstructorArgs: []interface{}{}, + } + + contractAddr, err := DeployContract(suite.T(), suite.chain, deploymentData) + suite.chain.NextBlock() + suite.Require().NoError(err) + + contractAddrSDK := sdk.AccAddress(contractAddr.Bytes()) + senderAccount := suite.chain.SenderAccounts[AutoFlushSenderIndex] + + // Fund contract via bank module for all operations (both native transfers and delegation) + // Convert from wei (18 decimals) to aatom (6 decimals) by dividing by 1e12 + totalAmountWei := AutoFlushNativeAmount*2 + AutoFlushDelegationAmount + totalAmountAatom := sdkmath.NewInt(int64(totalAmountWei / 1_000_000_000_000)) + suite.fundContractNative(contractAddrSDK, totalAmountAatom) + + // Get recipient and validator + recipientAddr := common.BytesToAddress(senderAccount.SenderAccount.GetAddress().Bytes()) + recipientAddrSDK := senderAccount.SenderAccount.GetAddress() + validatorAddr := suite.getValidatorAddress() + + // Get balances before + ctx = suite.chain.GetContext() + bondDenom, err := evmApp.StakingKeeper.BondDenom(ctx) + suite.Require().NoError(err) + + contractBalBefore := evmApp.GetBankKeeper().GetBalance(ctx, contractAddrSDK, bondDenom) + recipientBalBefore := evmApp.GetBankKeeper().GetBalance(ctx, recipientAddrSDK, bondDenom) + + // Pack the call data + callData, err := contractData.ABI.Pack( + "scenario3_nativeTransferDelegateNativeTransfer", + recipientAddr, + big.NewInt(AutoFlushNativeAmount), + validatorAddr, + big.NewInt(AutoFlushDelegationAmount), + ) + suite.Require().NoError(err) + + // Execute via SendEvmTx to get proper event tracking + res, _, _, err := suite.chain.SendEvmTx( + senderAccount, + AutoFlushSenderIndex, + contractAddr, + big.NewInt(0), + callData, + 0, + ) + suite.Require().NoError(err) + + // Get balances after + ctx = suite.chain.GetContext() + contractBalAfter := evmApp.GetBankKeeper().GetBalance(ctx, contractAddrSDK, bondDenom) + recipientBalAfter := evmApp.GetBankKeeper().GetBalance(ctx, recipientAddrSDK, bondDenom) + + // Bank balances are in Cosmos units (6 decimals), EVM amounts are in wei (18 decimals) + // Conversion: 1e18 wei = 1e6 aatom (divide by 1e12) + conversionFactor := sdkmath.NewInt(1_000_000_000_000) // 1e12 + + // Verify balances - contract should lose 2x native transfer + delegation (in bank units) + expectedContractDeltaWei := sdkmath.NewInt(AutoFlushNativeAmount*2 + AutoFlushDelegationAmount) + expectedContractDelta := expectedContractDeltaWei.Quo(conversionFactor) + actualContractDelta := contractBalBefore.Amount.Sub(contractBalAfter.Amount) + + // Verify recipient received 2x native amount (in bank units) + expectedRecipientDeltaWei := sdkmath.NewInt(AutoFlushNativeAmount * 2) + expectedRecipientDelta := expectedRecipientDeltaWei.Quo(conversionFactor) + actualRecipientDelta := recipientBalAfter.Amount.Sub(recipientBalBefore.Amount) + + suite.Require().Equal(expectedContractDelta.String(), actualContractDelta.String(), "contract balance delta mismatch") + suite.Require().Equal(expectedRecipientDelta.String(), actualRecipientDelta.String(), "recipient should receive 2x native amount") + + // Verify delegation occurred + delegations, err := evmApp.StakingKeeper.GetAllDelegatorDelegations(ctx, contractAddrSDK) + suite.Require().NoError(err) + suite.Require().Equal(len(delegations), 1, "should have 1 delegation") + bondedTokens, err := evmApp.StakingKeeper.GetDelegatorBonded(suite.chain.GetContext(), contractAddrSDK) + suite.Require().NoError(err) + suite.Require().Equal(AutoFlushDelegationAmount/conversionFactor.Int64(), bondedTokens.Int64()) + + suite.Require().Equal(PreciseBankMintEventCount+PreciseBankBurnEventCount+DelegationEventCount+PreciseBankMintEventCount+PreciseBankBurnEventCount+EVMEventCount, len(res.Events)) +} + +// Scenario 4: Native transfer -> Delegate (reverted & caught) -> Native transfer +func (suite *AutoFlushTestSuite) TestScenario4_NativeTransferDelegateRevertNativeTransfer() { + suite.SetupTest() + + evmApp := suite.chain.App.(*evmd.EVMD) + ctx := suite.chain.GetContext() + + // Deploy test contract + contractData, err := contracts.LoadSequentialOperationsTester() + suite.Require().NoError(err) + + deploymentData := testutiltypes.ContractDeploymentData{ + Contract: contractData, + ConstructorArgs: []interface{}{}, + } + + contractAddr, err := DeployContract(suite.T(), suite.chain, deploymentData) + suite.chain.NextBlock() + suite.Require().NoError(err) + + contractAddrSDK := sdk.AccAddress(contractAddr.Bytes()) + + // Fund contract with native tokens for transfers via direct EVM transfer + senderAccount := suite.chain.SenderAccounts[AutoFlushSenderIndex] + nativeTransferValue := big.NewInt(AutoFlushNativeAmount * 2) // 2x transfers + _, _, _, err = suite.chain.SendEvmTx( + senderAccount, + AutoFlushSenderIndex, + contractAddr, + nativeTransferValue, + nil, // no data, just value transfer + 0, + ) + suite.Require().NoError(err) + suite.chain.NextBlock() + + // Get recipient and validator + recipientAddr := common.BytesToAddress(senderAccount.SenderAccount.GetAddress().Bytes()) + recipientAddrSDK := senderAccount.SenderAccount.GetAddress() + validatorAddr := suite.getValidatorAddress() + + // Get balances before + bondDenom, err := evmApp.StakingKeeper.BondDenom(ctx) + suite.Require().NoError(err) + + contractBalBefore := evmApp.GetBankKeeper().GetBalance(ctx, contractAddrSDK, bondDenom) + recipientBalBefore := evmApp.GetBankKeeper().GetBalance(ctx, recipientAddrSDK, bondDenom) + + // Pack the call data with excessive delegation amount (will revert and be caught) + excessiveAmount := new(big.Int).Mul(big.NewInt(AutoFlushDelegationAmount), big.NewInt(1000)) + callData, err := contractData.ABI.Pack( + "scenario4_nativeTransferDelegateRevertNativeTransfer", + recipientAddr, + big.NewInt(AutoFlushNativeAmount), + validatorAddr, + excessiveAmount, // Will fail - insufficient balance + ) + suite.Require().NoError(err) + + // Execute via SendEvmTx to get proper event tracking + res, _, _, err := suite.chain.SendEvmTx( + senderAccount, + AutoFlushSenderIndex, + contractAddr, + big.NewInt(0), + callData, + 0, + ) + suite.Require().NoError(err) + + // Get balances after + ctx = suite.chain.GetContext() + contractBalAfter := evmApp.GetBankKeeper().GetBalance(ctx, contractAddrSDK, bondDenom) + recipientBalAfter := evmApp.GetBankKeeper().GetBalance(ctx, recipientAddrSDK, bondDenom) + + // Bank balances are in Cosmos units (6 decimals), EVM amounts are in wei (18 decimals) + // Conversion: 1e18 wei = 1e6 aatom (divide by 1e12) + conversionFactor := sdkmath.NewInt(1_000_000_000_000) // 1e12 + + // Verify balances - contract should lose only 2x native transfer (delegation reverted, in bank units) + expectedContractDeltaWei := sdkmath.NewInt(AutoFlushNativeAmount * 2) + expectedContractDelta := expectedContractDeltaWei.Quo(conversionFactor) + actualContractDelta := contractBalBefore.Amount.Sub(contractBalAfter.Amount) + + // Verify recipient received 2x native amount (in bank units) + expectedRecipientDeltaWei := sdkmath.NewInt(AutoFlushNativeAmount * 2) + expectedRecipientDelta := expectedRecipientDeltaWei.Quo(conversionFactor) + actualRecipientDelta := recipientBalAfter.Amount.Sub(recipientBalBefore.Amount) + + suite.Require().Equal(expectedContractDelta.String(), actualContractDelta.String(), "contract balance delta mismatch") + suite.Require().Equal(expectedRecipientDelta.String(), actualRecipientDelta.String(), "recipient should receive 2x native amount") + + // Verify NO delegation occurred (it was reverted) + delegations, err := evmApp.StakingKeeper.GetAllDelegatorDelegations(ctx, contractAddrSDK) + suite.Require().NoError(err) + suite.Require().Equal(0, len(delegations), "should have no delegations since it reverted") + bondedTokens, err := evmApp.StakingKeeper.GetDelegatorBonded(suite.chain.GetContext(), contractAddrSDK) + suite.Require().NoError(err) + suite.Require().Equal(int64(0), bondedTokens.Int64()) + + suite.Require().Equal(PreciseBankBurnEventCount+PreciseBankBurnEventCount+EVMEventCount, len(res.Events)) // no delegation in middle, so we bundle the two events transfer events into one +} + +// Scenario 5: Delegate -> Create Contract -> Delegate +func (suite *AutoFlushTestSuite) TestScenario5_DelegateCreateDelegate() { + suite.SetupTest() + + evmApp := suite.chain.App.(*evmd.EVMD) + ctx := suite.chain.GetContext() + + // Deploy test contract + contractData, err := contracts.LoadContractCreationTester() + suite.Require().NoError(err) + + deploymentData := testutiltypes.ContractDeploymentData{ + Contract: contractData, + ConstructorArgs: []interface{}{}, + } + + contractAddr, err := DeployContract(suite.T(), suite.chain, deploymentData) + suite.chain.NextBlock() + suite.Require().NoError(err) + + contractAddrSDK := sdk.AccAddress(contractAddr.Bytes()) + + // Fund contract for delegations via bank module + delegationAmountAatom := sdkmath.NewInt(int64(AutoFlushDelegationAmount * 2)) + suite.fundContractNative(contractAddrSDK, delegationAmountAatom) + + // Get validator + senderAccount := suite.chain.SenderAccounts[AutoFlushSenderIndex] + validatorAddr := suite.getValidatorAddress() + + // Value to send when creating the contract + creationValue := big.NewInt(AutoFlushNativeAmount) + + // Pack the call data + callData, err := contractData.ABI.Pack( + "scenario4_delegateCreateDelegate", + validatorAddr, + big.NewInt(AutoFlushDelegationAmount), + big.NewInt(1).Quo(creationValue, big.NewInt(10)), + big.NewInt(AutoFlushDelegationAmount), + ) + suite.Require().NoError(err) + + // Execute via SendEvmTx with value for contract creation + res, _, _, err := suite.chain.SendEvmTx( + senderAccount, + AutoFlushSenderIndex, + contractAddr, + creationValue, // Send value for the new contract creation + callData, + 0, + ) + suite.Require().NoError(err) + + // Verify 1 delegations occurred + delegations, err := evmApp.StakingKeeper.GetAllDelegatorDelegations(ctx, contractAddrSDK) + suite.Require().NoError(err) + suite.Require().Equal(1, len(delegations), "should have 1 delegation") + bondedTokens, err := evmApp.StakingKeeper.GetDelegatorBonded(suite.chain.GetContext(), contractAddrSDK) + suite.Require().NoError(err) + suite.Require().Equal(AutoFlushDelegationAmount*2/1_000_000_000_000, bondedTokens.Int64()) + + suite.Require().Equal(PreciseBankMintEventCount+PreciseBankBurnEventCount+DelegationEventCount+PreciseBankMintEventCount+PreciseBankBurnEventCount+DelegationEventCount+WithdrawalNoTokensEventCount+EVMEventCount, len(res.Events)) +} + +// Scenario 6: Delegate -> Create Contract (reverted & caught) -> Delegate +func (suite *AutoFlushTestSuite) TestScenario6_DelegateCreateRevertDelegate() { + suite.SetupTest() + + evmApp := suite.chain.App.(*evmd.EVMD) + ctx := suite.chain.GetContext() + + // Deploy test contract + contractData, err := contracts.LoadContractCreationTester() + suite.Require().NoError(err) + + deploymentData := testutiltypes.ContractDeploymentData{ + Contract: contractData, + ConstructorArgs: []interface{}{}, + } + + contractAddr, err := DeployContract(suite.T(), suite.chain, deploymentData) + suite.chain.NextBlock() + suite.Require().NoError(err) + + contractAddrSDK := sdk.AccAddress(contractAddr.Bytes()) + + // Fund contract for delegations via bank module + delegationAmountAatom := sdkmath.NewInt(int64(AutoFlushDelegationAmount * 2)) + suite.fundContractNative(contractAddrSDK, delegationAmountAatom.QuoRaw(1_000_000_000_000)) + + // Get validator + senderAccount := suite.chain.SenderAccounts[AutoFlushSenderIndex] + validatorAddr := suite.getValidatorAddress() + + // Excessive value that will cause contract creation to fail + excessiveValue := new(big.Int).Mul(big.NewInt(AutoFlushNativeAmount), big.NewInt(1000)) + + // Pack the call data + callData, err := contractData.ABI.Pack( + "scenario5_delegateCreateRevertDelegate", + validatorAddr, + big.NewInt(AutoFlushDelegationAmount), + excessiveValue, // Will fail - insufficient value + big.NewInt(AutoFlushDelegationAmount), + ) + suite.Require().NoError(err) + + // Execute via SendEvmTx + res, _, _, err := suite.chain.SendEvmTx( + senderAccount, + AutoFlushSenderIndex, + contractAddr, + big.NewInt(0), + callData, + 0, + ) + suite.Require().NoError(err) + + // Verify 1 delegation occurred + delegations, err := evmApp.StakingKeeper.GetAllDelegatorDelegations(ctx, contractAddrSDK) + suite.Require().NoError(err) + suite.Require().Equal(1, len(delegations), "should have 1 delegation") + bondedTokens, err := evmApp.StakingKeeper.GetDelegatorBonded(suite.chain.GetContext(), contractAddrSDK) + suite.Require().NoError(err) + suite.Require().Equal(AutoFlushDelegationAmount*2/1_000_000_000_000, bondedTokens.Int64()) + + suite.Require().Equal(DelegationEventCount+DelegationEventCount+WithdrawalNoTokensEventCount+EVMEventCount, len(res.Events)) +} + +// Scenario 7: Create+Revert (caught) -> Delegate -> Create+Revert (caught) +func (suite *AutoFlushTestSuite) TestScenario7_CreateRevertDelegateCreateRevert() { + suite.SetupTest() + + evmApp := suite.chain.App.(*evmd.EVMD) + ctx := suite.chain.GetContext() + + // Deploy test contract + contractData, err := contracts.LoadContractCreationTester() + suite.Require().NoError(err) + + deploymentData := testutiltypes.ContractDeploymentData{ + Contract: contractData, + ConstructorArgs: []interface{}{}, + } + + contractAddr, err := DeployContract(suite.T(), suite.chain, deploymentData) + suite.chain.NextBlock() + suite.Require().NoError(err) + + contractAddrSDK := sdk.AccAddress(contractAddr.Bytes()) + senderAccount := suite.chain.SenderAccounts[AutoFlushSenderIndex] + + // Fund contract for delegation via bank module + delegationAmountAatom := sdkmath.NewInt(int64(AutoFlushDelegationAmount)) + suite.fundContractNative(contractAddrSDK, delegationAmountAatom) + + // Fund contract with some EVM balance for contract creations (even though they revert) + // This is needed because the contract needs balance to create sub-contracts + fundingAmount := big.NewInt(AutoFlushNativeAmount) + _, _, _, err = suite.chain.SendEvmTx( + senderAccount, + AutoFlushSenderIndex, + contractAddr, + fundingAmount, + nil, + 0, + ) + suite.Require().NoError(err) + suite.chain.NextBlock() + + // Get validator + validatorAddr := suite.getValidatorAddress() + + // Values for contract creations (use 0 to avoid balance issues with reverts) + creationValue1 := big.NewInt(0) + creationValue2 := big.NewInt(0) + + // Pack the call data + callData, err := contractData.ABI.Pack( + "scenario6_createRevertDelegateCreateRevert", + creationValue1, + validatorAddr, + big.NewInt(AutoFlushDelegationAmount), + creationValue2, + ) + suite.Require().NoError(err) + + // Execute via SendEvmTx (no value needed since contract creations use 0 and revert) + res, _, _, err := suite.chain.SendEvmTx( + senderAccount, + AutoFlushSenderIndex, + contractAddr, + big.NewInt(0), + callData, + 0, + ) + suite.Require().NoError(err) + + // Verify 1 delegation occurred + delegations, err := evmApp.StakingKeeper.GetAllDelegatorDelegations(ctx, contractAddrSDK) + suite.Require().NoError(err) + suite.Require().Equal(1, len(delegations), "should have 1 delegation") + bondedTokens, err := evmApp.StakingKeeper.GetDelegatorBonded(suite.chain.GetContext(), contractAddrSDK) + suite.Require().NoError(err) + suite.Require().Equal(AutoFlushDelegationAmount/1_000_000_000_000, bondedTokens.Int64()) + + suite.Require().Equal(DelegationEventCount+EVMEventCount, len(res.Events)) +} + +// Scenario 8: Create+Send -> Delegate (reverted & caught) -> Send more +func (suite *AutoFlushTestSuite) TestScenario8_CreateDelegateRevertSend() { + suite.SetupTest() + + evmApp := suite.chain.App.(*evmd.EVMD) + ctx := suite.chain.GetContext() + + // Deploy test contract + contractData, err := contracts.LoadContractCreationTester() + suite.Require().NoError(err) + + deploymentData := testutiltypes.ContractDeploymentData{ + Contract: contractData, + ConstructorArgs: []interface{}{}, + } + + contractAddr, err := DeployContract(suite.T(), suite.chain, deploymentData) + suite.chain.NextBlock() + suite.Require().NoError(err) + + // Get validator + senderAccount := suite.chain.SenderAccounts[AutoFlushSenderIndex] + validatorAddr := suite.getValidatorAddress() + + // Values for operations + creationValue := big.NewInt(AutoFlushNativeAmount) + sendAmount := big.NewInt(AutoFlushNativeAmount / 2) + excessiveDelegateAmount := new(big.Int).Mul(big.NewInt(AutoFlushDelegationAmount), big.NewInt(1000)) + + // Pack the call data + callData, err := contractData.ABI.Pack( + "scenario7_createDelegateRevertSend", + creationValue, + validatorAddr, + excessiveDelegateAmount, // Will fail - insufficient balance + sendAmount, + ) + suite.Require().NoError(err) + + // Execute via SendEvmTx with value for contract creation and sends + totalValue := new(big.Int).Add(creationValue, sendAmount) + res, _, _, err := suite.chain.SendEvmTx( + senderAccount, + AutoFlushSenderIndex, + contractAddr, + totalValue, + callData, + 0, + ) + suite.Require().NoError(err) + + // Verify NO delegation occurred (it was reverted) + contractAddrSDK := sdk.AccAddress(contractAddr.Bytes()) + delegations, err := evmApp.StakingKeeper.GetAllDelegatorDelegations(ctx, contractAddrSDK) + suite.Require().NoError(err) + suite.Require().Equal(0, len(delegations), "should have no delegations since it reverted") + + suite.Require().Equal(PreciseBankMintEventCount+PreciseBankBurnEventCount+EVMEventCount, len(res.Events)) +} + +// Scenario 9: Create+Revert (caught) -> Delegate -> Create+Success +func (suite *AutoFlushTestSuite) TestScenario9_CreateRevertDelegateCreateSuccess() { + suite.SetupTest() + + evmApp := suite.chain.App.(*evmd.EVMD) + + // Deploy test contract + contractData, err := contracts.LoadContractCreationTester() + suite.Require().NoError(err) + + deploymentData := testutiltypes.ContractDeploymentData{ + Contract: contractData, + ConstructorArgs: []interface{}{}, + } + + contractAddr, err := DeployContract(suite.T(), suite.chain, deploymentData) + suite.chain.NextBlock() + suite.Require().NoError(err) + + contractAddrSDK := sdk.AccAddress(contractAddr.Bytes()) + senderAccount := suite.chain.SenderAccounts[AutoFlushSenderIndex] + + // Fund contract for delegation via bank module + delegationAmountAatom := sdkmath.NewInt(int64(AutoFlushDelegationAmount / 1_000_000_000_000)) + suite.fundContractNative(contractAddrSDK, delegationAmountAatom) + + // Fund contract with EVM balance for contract creations + // Need enough for the successful creation (reverted one returns the value) + fundingAmount := big.NewInt(AutoFlushNativeAmount * 2) // Extra buffer + _, _, _, err = suite.chain.SendEvmTx( + senderAccount, + AutoFlushSenderIndex, + contractAddr, + fundingAmount, + nil, + 0, + ) + suite.Require().NoError(err) + suite.chain.NextBlock() + + // Get validator + validatorAddr := suite.getValidatorAddress() + + // Values for operations + revertCreationValue := big.NewInt(AutoFlushNativeAmount / 2) + successCreationValue := big.NewInt(AutoFlushNativeAmount / 2) + + // Get balances before + ctx := suite.chain.GetContext() + bondDenom, err := evmApp.StakingKeeper.BondDenom(ctx) + suite.Require().NoError(err) + contractBalBefore := evmApp.GetBankKeeper().GetBalance(ctx, contractAddrSDK, bondDenom) + + // Pack the call data + callData, err := contractData.ABI.Pack( + "scenario8_createRevertDelegateCreateSuccess", + revertCreationValue, + validatorAddr, + big.NewInt(AutoFlushDelegationAmount), + successCreationValue, + ) + suite.Require().NoError(err) + + // Execute via SendEvmTx + res, _, _, err := suite.chain.SendEvmTx( + senderAccount, + AutoFlushSenderIndex, + contractAddr, + big.NewInt(0), + callData, + 0, + ) + suite.Require().NoError(err) + + // Verify final state + ctx = suite.chain.GetContext() + + // Check delegation occurred + delegations, err := evmApp.StakingKeeper.GetAllDelegatorDelegations(ctx, contractAddrSDK) + suite.Require().NoError(err) + suite.Require().Equal(1, len(delegations), "should have 1 delegation") + bondedTokens, err := evmApp.StakingKeeper.GetDelegatorBonded(suite.chain.GetContext(), contractAddrSDK) + suite.Require().NoError(err) + suite.Require().Equal(AutoFlushDelegationAmount/1_000_000_000_000, bondedTokens.Int64()) + + // Check created contracts count + stateDB := testutil.NewStateDB(ctx, evmApp.EVMKeeper) + + countResult, err := evmApp.GetEVMKeeper().CallEVM( + ctx, + stateDB, + contractData.ABI, + contractAddr, + contractAddr, + true, + false, + nil, + "getCreatedContractsCount", + ) + suite.Require().NoError(err) + + var createdCount *big.Int + err = contractData.ABI.UnpackIntoInterface(&createdCount, "getCreatedContractsCount", countResult.Ret) + suite.Require().NoError(err) + suite.Require().Equal(int64(1), createdCount.Int64(), "should have 1 created contract (reverted one doesn't count)") + + // Get the created contract address + addrResult, err := evmApp.GetEVMKeeper().CallEVM( + ctx, + stateDB, + contractData.ABI, + contractAddr, + contractAddr, + true, + false, + nil, + "getCreatedContract", + big.NewInt(0), + ) + suite.Require().NoError(err) + + var createdContractAddr common.Address + err = contractData.ABI.UnpackIntoInterface(&createdContractAddr, "getCreatedContract", addrResult.Ret) + suite.Require().NoError(err) + + // Check the created contract's balance + createdContractBalance := stateDB.GetBalance(createdContractAddr) + suite.Require().Equal(successCreationValue.String(), createdContractBalance.String(), + "created contract should have the creation value") + + // Check main contract's bank balance decreased by delegation + created contract value + contractBalAfter := evmApp.GetBankKeeper().GetBalance(ctx, contractAddrSDK, bondDenom) + // Delta = delegation amount + successful creation value (reverted creation returns the value) + expectedDelta := sdkmath.NewInt((AutoFlushDelegationAmount + successCreationValue.Int64()) / 1_000_000_000_000) + actualDelta := contractBalBefore.Amount.Sub(contractBalAfter.Amount) + suite.Require().Equal(expectedDelta.String(), actualDelta.String(), "bank balance should decrease by delegation + creation value") + + suite.Require().Equal(DelegationEventCount+PreciseBankMintEventCount+PreciseBankBurnEventCount+EVMEventCount, len(res.Events)) +} diff --git a/evmd/tests/ibc/helper.go b/evmd/tests/ibc/helper.go index b7df90579..f9232f2a3 100644 --- a/evmd/tests/ibc/helper.go +++ b/evmd/tests/ibc/helper.go @@ -2,9 +2,11 @@ package ibc import ( "errors" + "fmt" "math/big" "testing" + abci "github.com/cometbft/cometbft/abci/types" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" @@ -14,15 +16,28 @@ import ( evmibctesting "github.com/cosmos/evm/testutil/ibc" testutiltypes "github.com/cosmos/evm/testutil/types" erc20types "github.com/cosmos/evm/x/erc20/types" + "github.com/cosmos/evm/x/vm/statedb" + "github.com/cosmos/evm/x/vm/types" ibctesting "github.com/cosmos/ibc-go/v10/testing" errorsmod "cosmossdk.io/errors" + 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" govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" ) +// Event count constants for test assertions +const ( + PreciseBankMintEventCount = 9 // Native transfer with mint operation + PreciseBankBurnEventCount = 9 // Native transfer with burn operation + DelegationEventCount = 3 // Staking delegation events + EVMEventCount = 5 // EVM transaction wrapper events + WithdrawalNoTokensEventCount = 1 // Withdrawal with no tokens + ICS20WithConversionEventCount = 15 // ERC20 conversion (7) + IBC packet (8) +) + // NativeErc20Info holds details about a deployed ERC20 token. type NativeErc20Info struct { Denom string @@ -40,7 +55,8 @@ func SetupNativeErc20(t *testing.T, chain *evmibctesting.TestChain, senderAcc ev evmApp := chain.App.(evm.EvmApp) // Deploy new ERC20 contract with default metadata - contractAddr, err := evmApp.GetErc20Keeper().DeployERC20Contract(evmCtx, banktypes.Metadata{ + stateDB := statedb.New(chain.GetContext(), chain.App.(evm.EvmApp).GetEVMKeeper(), statedb.NewEmptyTxConfig()) + contractAddr, err := DeployERC20Contract(evmCtx, stateDB, evmApp.GetAccountKeeper(), evmApp.GetEVMKeeper(), banktypes.Metadata{ DenomUnits: []*banktypes.DenomUnit{ {Denom: "example", Exponent: 18}, }, @@ -67,12 +83,15 @@ func SetupNativeErc20(t *testing.T, chain *evmibctesting.TestChain, senderAcc ev sendAmt := ibctesting.DefaultCoinAmount senderAddr := senderAcc.SenderAccount.GetAddress() + stateDB = statedb.New(evmCtx, evmApp.GetEVMKeeper(), statedb.NewEmptyTxConfig()) _, err = evmApp.GetEVMKeeper().CallEVM( evmCtx, + stateDB, contractAbi, erc20types.ModuleAddress, contractAddr, true, + false, nil, "mint", common.BytesToAddress(senderAddr), @@ -116,10 +135,69 @@ func DeployContract(t *testing.T, chain *evmibctesting.TestChain, deploymentData data := deploymentData.Contract.Bin data = append(data, ctorArgs...) - _, err = chain.App.(evm.EvmApp).GetEVMKeeper().CallEVMWithData(chain.GetContext(), from, nil, data, true, nil) + stateDB := statedb.New(chain.GetContext(), chain.App.(evm.EvmApp).GetEVMKeeper(), statedb.NewEmptyTxConfig()) + + _, err = chain.App.(evm.EvmApp).GetEVMKeeper().CallEVMWithData(chain.GetContext(), stateDB, from, nil, data, true, false, nil) if err != nil { return common.Address{}, errorsmod.Wrapf(err, "failed to deploy contract") } return crypto.CreateAddress(from, account.Nonce), nil } + +// DeployERC20Contract creates and deploys an ERC20 contract on the EVM with the +// erc20 module account as owner. +func DeployERC20Contract( + ctx sdk.Context, + stateDB *statedb.StateDB, + accountKeeper erc20types.AccountKeeper, + evmKeeper erc20types.EVMKeeper, + coinMetadata banktypes.Metadata, +) (common.Address, error) { + decimals := uint8(0) + if len(coinMetadata.DenomUnits) > 0 { + decimalsIdx := len(coinMetadata.DenomUnits) - 1 + decimals = uint8(coinMetadata.DenomUnits[decimalsIdx].Exponent) //#nosec G115 // exponent will not exceed uint8 + } + ctorArgs, err := contracts.ERC20MinterBurnerDecimalsContract.ABI.Pack( + "", + coinMetadata.Name, + coinMetadata.Symbol, + decimals, + ) + if err != nil { + return common.Address{}, errorsmod.Wrapf(types.ErrABIPack, "coin metadata is invalid %s: %s", coinMetadata.Name, err.Error()) + } + + data := make([]byte, len(contracts.ERC20MinterBurnerDecimalsContract.Bin)+len(ctorArgs)) + copy(data[:len(contracts.ERC20MinterBurnerDecimalsContract.Bin)], contracts.ERC20MinterBurnerDecimalsContract.Bin) + copy(data[len(contracts.ERC20MinterBurnerDecimalsContract.Bin):], ctorArgs) + + nonce, err := accountKeeper.GetSequence(ctx, erc20types.ModuleAddress.Bytes()) + if err != nil { + return common.Address{}, err + } + + contractAddr := crypto.CreateAddress(erc20types.ModuleAddress, nonce) + _, err = evmKeeper.CallEVMWithData(ctx, stateDB, erc20types.ModuleAddress, nil, data, true, false, nil) + if err != nil { + return common.Address{}, errorsmod.Wrapf(err, "failed to deploy contract for %s", coinMetadata.Name) + } + + return contractAddr, nil +} + +// PrintEvents prints all events with their attributes for debugging +func PrintEvents(label string, events []abci.Event) { + fmt.Printf("\n========== Events for %s ==========\n", label) + fmt.Printf("Total Event Count: %d\n\n", len(events)) + + for i, event := range events { + fmt.Printf("[%d] Type: %s\n", i, event.Type) + for _, attr := range event.Attributes { + fmt.Printf(" %s: %s\n", attr.Key, attr.Value) + } + fmt.Println() + } + fmt.Printf("========================================\n\n") +} diff --git a/evmd/tests/ibc/ibc_middleware_test.go b/evmd/tests/ibc/ibc_middleware_test.go index b930641dd..7a607544c 100644 --- a/evmd/tests/ibc/ibc_middleware_test.go +++ b/evmd/tests/ibc/ibc_middleware_test.go @@ -15,6 +15,7 @@ import ( "github.com/cosmos/evm/evmd" "github.com/cosmos/evm/evmd/tests/integration" "github.com/cosmos/evm/ibc" + ics20precompile "github.com/cosmos/evm/precompiles/ics20" "github.com/cosmos/evm/testutil" evmibctesting "github.com/cosmos/evm/testutil/ibc" testutiltypes "github.com/cosmos/evm/testutil/types" @@ -23,6 +24,7 @@ import ( "github.com/cosmos/evm/x/erc20/types" ibctestutil "github.com/cosmos/evm/x/ibc/callbacks/testutil" callbacktypes "github.com/cosmos/evm/x/ibc/callbacks/types" + "github.com/cosmos/evm/x/vm/statedb" evmtypes "github.com/cosmos/evm/x/vm/types" ibctransfer "github.com/cosmos/ibc-go/v10/modules/apps/transfer" transfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" @@ -45,7 +47,8 @@ type MiddlewareTestSuite struct { evmChainA *evmibctesting.TestChain chainB *evmibctesting.TestChain - path *evmibctesting.Path + path *evmibctesting.Path + evmChainAPrecompile *ics20precompile.Precompile } // SetupTest initializes the coordinator and test chains before each test. @@ -65,6 +68,80 @@ func (suite *MiddlewareTestSuite) SetupTest() { // ensure the channel is found to verify proper setup _, found := suite.evmChainA.App.GetIBCKeeper().ChannelKeeper.GetChannel(suite.evmChainA.GetContext(), suite.path.EndpointA.ChannelConfig.PortID, suite.path.EndpointA.ChannelID) suite.Require().True(found) + + // Setup ICS20 precompile for evmChainA + evmAppA := suite.evmChainA.App.(*evmd.EVMD) + suite.evmChainAPrecompile = ics20precompile.NewPrecompile( + evmAppA.BankKeeper, + *evmAppA.StakingKeeper, + evmAppA.TransferKeeper, + evmAppA.IBCKeeper.ChannelKeeper, + evmAppA.Erc20Keeper, + ) +} + +// transferViaPrecompile sends an IBC transfer using the ICS20 precompile. +// This is required for native ERC20 tokens. +func (suite *MiddlewareTestSuite) transferViaPrecompile( + ctx sdk.Context, + sourcePort, sourceChannel string, + token sdk.Coin, + sender, receiver string, + timeoutHeight clienttypes.Height, + timeoutTimestamp uint64, + memo string, +) (uint64, error) { + evmApp := suite.evmChainA.App.(*evmd.EVMD) + + // Convert sender to address type + senderAddr := sdk.MustAccAddressFromBech32(sender) + senderEthAddr := common.BytesToAddress(senderAddr) + + // Create timeoutHeight struct matching the ABI + type Height struct { + RevisionNumber uint64 + RevisionHeight uint64 + } + timeoutHeightStruct := Height{ + RevisionNumber: timeoutHeight.RevisionNumber, + RevisionHeight: timeoutHeight.RevisionHeight, + } + + // Call the precompile through the EVM with correct parameter order and types + // ABI order: sourcePort, sourceChannel, denom, amount, sender (address), receiver, timeoutHeight (struct), timeoutTimestamp, memo + stateDB := statedb.New(suite.evmChainA.GetContext(), evmApp.GetEVMKeeper(), statedb.NewEmptyTxConfig()) + res, err := evmApp.EVMKeeper.CallEVM( + ctx, + stateDB, + ics20precompile.ABI, + senderEthAddr, + common.HexToAddress(ics20precompile.PrecompileAddress), + true, + false, + nil, + "transfer", + sourcePort, + sourceChannel, + token.Denom, + token.Amount.BigInt(), + senderEthAddr, + receiver, + timeoutHeightStruct, + timeoutTimestamp, + memo, + ) + if err != nil { + return 0, err + } + + // Unpack the sequence from the result + var sequence uint64 + err = ics20precompile.ABI.UnpackIntoInterface(&sequence, "transfer", res.Ret) + if err != nil { + return 0, err + } + + return sequence, nil } func TestMiddlewareTestSuite(t *testing.T) { @@ -617,17 +694,22 @@ func (suite *MiddlewareTestSuite) TestOnRecvPacketNativeErc20() { senderEthAddr := nativeErc20.Account sender := sdk.AccAddress(senderEthAddr.Bytes()) - // Transfer half the initial balance out + // Transfer half the initial balance out using the precompile // Sender transfers 50 out (escrowed) - msg := transfertypes.NewMsgTransfer( - path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, + _, err := suite.transferViaPrecompile( + evmCtx, + path.EndpointA.ChannelConfig.PortID, + path.EndpointA.ChannelID, sdk.NewCoin(nativeErc20.Denom, sendAmt), - sender.String(), chainBAccount.String(), - timeoutHeight, 0, "", + sender.String(), + chainBAccount.String(), + timeoutHeight, + 0, + "", ) - - _, err := suite.evmChainA.SendMsgs(msg) suite.Require().NoError(err) // message committed + suite.evmChainA.NextBlock() + evmCtx = suite.evmChainA.GetContext() // Balance after transfer should be initial balance - sendAmt balAfterTransfer := evmApp.Erc20Keeper.BalanceOf(evmCtx, nativeErc20.ContractAbi, nativeErc20.ContractAddr, senderEthAddr) @@ -1211,19 +1293,21 @@ func (suite *MiddlewareTestSuite) TestOnAcknowledgementPacketWithCallback() { ack, receiver, ) + stateDB := statedb.New(suite.evmChainA.GetContext(), evmApp.GetEVMKeeper(), statedb.NewEmptyTxConfig()) // Validate results if tc.expError == "" { suite.Require().NoError(err, "Expected success but got error") - // Verify callback execution by checking counter increment if strings.Contains(tc.memo(), "src_callback") { counterRes, err := evmApp.EVMKeeper.CallEVM( ctxA, + stateDB, contractData.ABI, common.BytesToAddress(suite.evmChainA.SenderAccount.GetAddress()), contractAddr, false, + false, big.NewInt(100000), "getCounter", ) @@ -1250,10 +1334,12 @@ func (suite *MiddlewareTestSuite) TestOnAcknowledgementPacketWithCallback() { counterRes, err := evmApp.EVMKeeper.CallEVM( ctxA, + stateDB, contractData.ABI, common.BytesToAddress(suite.evmChainA.SenderAccount.GetAddress()), contractAddr, false, + false, big.NewInt(100000), "getCounter", ) @@ -1483,13 +1569,6 @@ func (suite *MiddlewareTestSuite) TestOnAcknowledgementPacketNativeErc20() { sender := sdk.AccAddress(senderEthAddr.Bytes()) receiver := suite.chainB.SenderAccount.GetAddress() - // Send the native erc20 token from evmChainA to chainB. - msg := transfertypes.NewMsgTransfer( - path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, - sdk.NewCoin(nativeErc20.Denom, sendAmt), sender.String(), receiver.String(), - timeoutHeight, 0, "", - ) - escrowAddr := transfertypes.GetEscrowAddress(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID) // checkEscrow is a check function to ensure the native erc20 token is escrowed. checkEscrow := func() { @@ -1512,8 +1591,21 @@ func (suite *MiddlewareTestSuite) TestOnAcknowledgementPacketNativeErc20() { suite.Require().Equal(nativeErc20.InitialBal.String(), erc20BalAfterIbcTransfer.String()) } - _, err := suite.evmChainA.SendMsgs(msg) + // Send the native erc20 token from evmChainA to chainB using the precompile. + _, err := suite.transferViaPrecompile( + evmCtx, + path.EndpointA.ChannelConfig.PortID, + path.EndpointA.ChannelID, + sdk.NewCoin(nativeErc20.Denom, sendAmt), + sender.String(), + receiver.String(), + timeoutHeight, + 0, + "", + ) suite.Require().NoError(err) // message committed + suite.evmChainA.NextBlock() + evmCtx = suite.evmChainA.GetContext() checkEscrow() transferStack, ok := suite.evmChainA.App.GetIBCKeeper().PortKeeper.Route(transfertypes.ModuleName) @@ -2012,6 +2104,7 @@ func (suite *MiddlewareTestSuite) TestOnTimeoutPacketWithCallback() { receiver, ) balAfterTimeout := evmApp.BankKeeper.GetBalance(ctxA, sender, bondDenom) + stateDB := statedb.New(suite.evmChainA.GetContext(), evmApp.GetEVMKeeper(), statedb.NewEmptyTxConfig()) // Validate results if tc.expError == "" { @@ -2023,10 +2116,12 @@ func (suite *MiddlewareTestSuite) TestOnTimeoutPacketWithCallback() { if strings.Contains(tc.memo(), "src_callback") { counterRes, err := evmApp.EVMKeeper.CallEVM( ctxA, + stateDB, contractData.ABI, common.BytesToAddress(suite.evmChainA.SenderAccount.GetAddress()), contractAddr, false, + false, big.NewInt(100000), "getCounter", ) @@ -2055,10 +2150,12 @@ func (suite *MiddlewareTestSuite) TestOnTimeoutPacketWithCallback() { if strings.Contains(tc.memo(), "src_callback") && strings.Contains(tc.expError, "ABCI code") { counterRes, err := evmApp.EVMKeeper.CallEVM( ctxA, + stateDB, contractData.ABI, common.BytesToAddress(suite.evmChainA.SenderAccount.GetAddress()), contractAddr, false, + false, big.NewInt(100000), "getCounter", ) @@ -2130,12 +2227,6 @@ func (suite *MiddlewareTestSuite) TestOnTimeoutPacketNativeErc20() { sender := sdk.AccAddress(senderEthAddr.Bytes()) receiver := suite.chainB.SenderAccount.GetAddress() - msg := transfertypes.NewMsgTransfer( - path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, - sdk.NewCoin(nativeErc20.Denom, sendAmt), sender.String(), receiver.String(), - timeoutHeight, 0, "", - ) - escrowAddr := transfertypes.GetEscrowAddress(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID) // checkEscrow is a check function to ensure the native erc20 token is escrowed. checkEscrow := func() { @@ -2157,8 +2248,22 @@ func (suite *MiddlewareTestSuite) TestOnTimeoutPacketNativeErc20() { erc20BalAfterIbcTransfer := evmApp.Erc20Keeper.BalanceOf(evmCtx, nativeErc20.ContractAbi, nativeErc20.ContractAddr, senderEthAddr) suite.Require().Equal(nativeErc20.InitialBal.String(), erc20BalAfterIbcTransfer.String()) } - _, err := suite.evmChainA.SendMsgs(msg) + + // Send the native erc20 token from evmChainA to chainB using the precompile. + _, err := suite.transferViaPrecompile( + evmCtx, + path.EndpointA.ChannelConfig.PortID, + path.EndpointA.ChannelID, + sdk.NewCoin(nativeErc20.Denom, sendAmt), + sender.String(), + receiver.String(), + timeoutHeight, + 0, + "", + ) suite.Require().NoError(err) // message committed + suite.evmChainA.NextBlock() + evmCtx = suite.evmChainA.GetContext() checkEscrow() transferStack, ok := suite.evmChainA.App.GetIBCKeeper().PortKeeper.Route(transfertypes.ModuleName) diff --git a/evmd/tests/ibc/ics20_erc20_conversion_test.go b/evmd/tests/ibc/ics20_erc20_conversion_test.go new file mode 100644 index 000000000..886f088d1 --- /dev/null +++ b/evmd/tests/ibc/ics20_erc20_conversion_test.go @@ -0,0 +1,500 @@ +package ibc + +import ( + "fmt" + "math/big" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/suite" + + "github.com/cosmos/evm/evmd" + "github.com/cosmos/evm/evmd/tests/integration" + "github.com/cosmos/evm/precompiles/ics20" + evmibctesting "github.com/cosmos/evm/testutil/ibc" + erc20types "github.com/cosmos/evm/x/erc20/types" + transfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" + clienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types" + + sdkmath "cosmossdk.io/math" + + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" +) + +type ICS20ERC20ConversionTestSuite struct { + suite.Suite + + coordinator *evmibctesting.Coordinator + + chainA *evmibctesting.TestChain + chainAPrecompile *ics20.Precompile + chainB *evmibctesting.TestChain + chainBPrecompile *ics20.Precompile +} + +func (suite *ICS20ERC20ConversionTestSuite) SetupTest() { + suite.coordinator = evmibctesting.NewCoordinator(suite.T(), 2, 0, integration.SetupEvmd) + suite.chainA = suite.coordinator.GetChain(evmibctesting.GetEvmChainID(1)) + suite.chainB = suite.coordinator.GetChain(evmibctesting.GetEvmChainID(2)) + + evmAppA := suite.chainA.App.(*evmd.EVMD) + suite.chainAPrecompile = ics20.NewPrecompile( + evmAppA.BankKeeper, + *evmAppA.StakingKeeper, + evmAppA.TransferKeeper, + evmAppA.IBCKeeper.ChannelKeeper, + evmAppA.Erc20Keeper, + ) + evmAppB := suite.chainB.App.(*evmd.EVMD) + suite.chainBPrecompile = ics20.NewPrecompile( + evmAppB.BankKeeper, + *evmAppB.StakingKeeper, + evmAppB.TransferKeeper, + evmAppB.IBCKeeper.ChannelKeeper, + evmAppB.Erc20Keeper, + ) +} + +func TestICS20ERC20ConversionTestSuite(t *testing.T) { + suite.Run(t, new(ICS20ERC20ConversionTestSuite)) +} + +// TestTransferWithERC20Conversion tests IBC transfers with ERC20 token conversion +func (suite *ICS20ERC20ConversionTestSuite) TestTransferWithERC20Conversion() { + var ( + denom string + amount sdkmath.Int + sender common.Address + nativeErc20 *NativeErc20Info + path *evmibctesting.Path + ) + + receiver := suite.chainB.SenderAccount.GetAddress().String() + timeoutHeight := clienttypes.NewHeight(1, 110) + + testCases := []struct { + name string + malleate func() + expPass bool + }{ + { + "pass - no token pair", + func() { + evmAppA := suite.chainA.App.(*evmd.EVMD) + var err error + denom, err = evmAppA.StakingKeeper.BondDenom(suite.chainA.GetContext()) + suite.Require().NoError(err) + amount = sdkmath.NewInt(10) + sender = common.BytesToAddress(suite.chainA.SenderAccount.GetAddress().Bytes()) + }, + true, + }, + { + "no-op - disabled erc20 by params - sufficient sdk.Coins balance", + func() { + // Deploy and mint ERC20 + nativeErc20 = SetupNativeErc20(suite.T(), suite.chainA, suite.chainA.SenderAccounts[0]) + denom = nativeErc20.Denom + amount = sdkmath.NewInt(nativeErc20.InitialBal.Int64()) + + // Convert ERC20 to coins + evmAppA := suite.chainA.App.(*evmd.EVMD) + _, err := evmAppA.Erc20Keeper.ConvertERC20(suite.chainA.GetContext(), &erc20types.MsgConvertERC20{ + ContractAddress: nativeErc20.ContractAddr.Hex(), + Amount: amount, + Receiver: suite.chainA.SenderAccount.GetAddress().String(), + Sender: nativeErc20.Account.Hex(), + }) + suite.Require().NoError(err) + suite.chainA.NextBlock() + + // Disable ERC20 + params := evmAppA.Erc20Keeper.GetParams(suite.chainA.GetContext()) + params.EnableErc20 = false + err = evmAppA.Erc20Keeper.SetParams(suite.chainA.GetContext(), params) + suite.Require().NoError(err) + suite.chainA.NextBlock() + + sender = nativeErc20.Account + }, + true, + }, + { + "error - disabled erc20 by params - insufficient sdk.Coins balance", + func() { + // Deploy and mint ERC20 but don't convert + nativeErc20 = SetupNativeErc20(suite.T(), suite.chainA, suite.chainA.SenderAccounts[0]) + denom = nativeErc20.Denom + amount = sdkmath.NewInt(nativeErc20.InitialBal.Int64()) + + evmAppA := suite.chainA.App.(*evmd.EVMD) + ctx := suite.chainA.GetContext() + + // No conversion to IBC coin, so the balance is insufficient + suite.Require().EqualValues( + evmAppA.BankKeeper.GetBalance(ctx, suite.chainA.SenderAccount.GetAddress(), denom).Amount, + sdkmath.ZeroInt(), + "Bank balance should be zero since we didn't convert", + ) + + // Disable ERC20 without converting + params := evmAppA.Erc20Keeper.GetParams(ctx) + params.EnableErc20 = false + err := evmAppA.Erc20Keeper.SetParams(ctx, params) + suite.Require().NoError(err) + suite.chainA.NextBlock() + + sender = nativeErc20.Account + }, + false, + }, + { + "error - pair not registered", + func() { + denom = "unregistered" + amount = sdkmath.NewInt(10) + sender = common.BytesToAddress(suite.chainA.SenderAccount.GetAddress().Bytes()) + }, + false, + }, + { + "no-op - pair is disabled", + func() { + // Deploy and mint ERC20 + nativeErc20 = SetupNativeErc20(suite.T(), suite.chainA, suite.chainA.SenderAccounts[0]) + denom = nativeErc20.Denom + amount = sdkmath.NewInt(nativeErc20.InitialBal.Int64()) + + // Convert to coins first + evmAppA := suite.chainA.App.(*evmd.EVMD) + _, err := evmAppA.Erc20Keeper.ConvertERC20(suite.chainA.GetContext(), &erc20types.MsgConvertERC20{ + ContractAddress: nativeErc20.ContractAddr.Hex(), + Amount: amount, + Receiver: suite.chainA.SenderAccount.GetAddress().String(), + Sender: nativeErc20.Account.Hex(), + }) + suite.Require().NoError(err) + suite.chainA.NextBlock() + + // Disable the token pair + govAddr := authtypes.NewModuleAddress(govtypes.ModuleName).String() + _, err = evmAppA.Erc20Keeper.ToggleConversion(suite.chainA.GetContext(), &erc20types.MsgToggleConversion{ + Token: denom, + Authority: govAddr, + }) + suite.Require().NoError(err) + suite.chainA.NextBlock() + + sender = nativeErc20.Account + }, + true, + }, + { + "pass - has enough balance in erc20 - need to convert", + func() { + // Deploy and mint ERC20 but don't convert - transfer should auto-convert + nativeErc20 = SetupNativeErc20(suite.T(), suite.chainA, suite.chainA.SenderAccounts[0]) + denom = nativeErc20.Denom + amount = sdkmath.NewInt(nativeErc20.InitialBal.Int64()) + + // Verify denom format is correct + suite.Require().Equal( + erc20types.CreateDenom(nativeErc20.ContractAddr.String()), + denom, + "Denom should match the ERC20 contract address format", + ) + + sender = nativeErc20.Account + }, + true, + }, + { + "pass - has enough balance in coins", + func() { + // Deploy and mint ERC20 + nativeErc20 = SetupNativeErc20(suite.T(), suite.chainA, suite.chainA.SenderAccounts[0]) + denom = nativeErc20.Denom + amount = sdkmath.NewInt(nativeErc20.InitialBal.Int64()) + + // Convert to coins + evmAppA := suite.chainA.App.(*evmd.EVMD) + _, err := evmAppA.Erc20Keeper.ConvertERC20(suite.chainA.GetContext(), &erc20types.MsgConvertERC20{ + ContractAddress: nativeErc20.ContractAddr.Hex(), + Amount: amount, + Receiver: suite.chainA.SenderAccount.GetAddress().String(), + Sender: nativeErc20.Account.Hex(), + }) + suite.Require().NoError(err) + suite.chainA.NextBlock() + + sender = nativeErc20.Account + }, + true, + }, + { + "error - fail conversion - no balance in erc20", + func() { + // Deploy and register ERC20 but don't mint + nativeErc20 = SetupNativeErc20(suite.T(), suite.chainA, suite.chainA.SenderAccounts[0]) + denom = nativeErc20.Denom + // Request more than minted + amount = sdkmath.NewInt(nativeErc20.InitialBal.Int64() * 2) + sender = nativeErc20.Account + }, + false, + }, + { + "pass - verify correct prefix trimming for ERC20 native tokens", + func() { + // Deploy and mint ERC20 + nativeErc20 = SetupNativeErc20(suite.T(), suite.chainA, suite.chainA.SenderAccounts[0]) + denom = nativeErc20.Denom + amount = sdkmath.NewInt(nativeErc20.InitialBal.Int64()) + + evmAppA := suite.chainA.App.(*evmd.EVMD) + ctx := suite.chainA.GetContext() + + // Create a denom with erc20: prefix + erc20Denom := erc20types.CreateDenom(nativeErc20.ContractAddr.String()) + suite.Require().Equal(erc20types.Erc20NativeCoinDenomPrefix+nativeErc20.ContractAddr.String(), erc20Denom) + + // Verify that GetTokenPairID works correctly with the contract address (hex string) + pairIDFromAddress := evmAppA.Erc20Keeper.GetTokenPairID(ctx, nativeErc20.ContractAddr.String()) + suite.Require().NotEmpty(pairIDFromAddress) + + // Verify that GetTokenPairID works correctly with the full denom + pairIDFromDenom := evmAppA.Erc20Keeper.GetTokenPairID(ctx, erc20Denom) + suite.Require().NotEmpty(pairIDFromDenom) + + // Both should return the same pair ID + suite.Require().Equal(pairIDFromAddress, pairIDFromDenom) + + sender = nativeErc20.Account + }, + true, + }, + { + "no-op - fail transfer", + func() { + evmAppA := suite.chainA.App.(*evmd.EVMD) + ctx := suite.chainA.GetContext() + senderAcc := suite.chainA.SenderAccount.GetAddress() + + // Create a fake IBC voucher denom (IBC-transferred token) + denom = "ibc/DF63978F803A2E27CA5CC9B7631654CCF0BBC788B3B7F0A10200508E37C70992" + + // Register it as an ERC20 extension + _, err := evmAppA.Erc20Keeper.RegisterERC20Extension(ctx, denom) + suite.Require().NoError(err) + suite.chainA.NextBlock() + + // Verify the pair exists + pairID := evmAppA.Erc20Keeper.GetTokenPairID(ctx, denom) + suite.Require().NotEmpty(pairID, "Token pair should be registered") + + pair, found := evmAppA.Erc20Keeper.GetTokenPair(ctx, pairID) + suite.Require().True(found) + suite.Require().Equal(pair.Denom, denom) + + // Try to transfer without having any balance (should fail) + amount = sdkmath.NewInt(10) + sender = common.BytesToAddress(senderAcc) + }, + false, + }, + } + + for _, tc := range testCases { + suite.Run(fmt.Sprintf("Case %s", tc.name), func() { + suite.SetupTest() + + // Setup IBC path + path = evmibctesting.NewTransferPath(suite.chainA, suite.chainB) + path.Setup() + + // Run test-specific setup + tc.malleate() + + // Call precompile transfer + data, err := suite.chainAPrecompile.ABI.Pack( + "transfer", + transfertypes.PortID, + path.EndpointA.ChannelID, + denom, + amount.BigInt(), + sender, + receiver, + timeoutHeight, + uint64(0), + "", + ) + suite.Require().NoError(err) + + res, _, _, err := suite.chainA.SendEvmTx( + suite.chainA.SenderAccounts[0], + 0, + suite.chainAPrecompile.Address(), + big.NewInt(0), + data, + 0, + ) + + if tc.expPass { + suite.Require().NoError(err) + suite.Require().Equal(uint32(0), res.Code, res.Log) + } else { + suite.Require().Error(err) + } + }) + } +} + +// TestPrefixTrimming specifically tests that the erc20: prefix is correctly handled +func (suite *ICS20ERC20ConversionTestSuite) TestPrefixTrimming() { + var ( + denom string + amount sdkmath.Int + sender common.Address + nativeErc20 *NativeErc20Info + path *evmibctesting.Path + ) + + receiver := suite.chainB.SenderAccount.GetAddress().String() + timeoutHeight := clienttypes.NewHeight(1, 110) + + testCases := []struct { + name string + malleate func() + expPass bool + }{ + { + "pass - correct prefix trimming erc20:", + func() { + nativeErc20 = SetupNativeErc20(suite.T(), suite.chainA, suite.chainA.SenderAccounts[0]) + denom = nativeErc20.Denom + amount = sdkmath.NewInt(nativeErc20.InitialBal.Int64()) + + // Verify the denom has the correct prefix + suite.Require().Contains(denom, erc20types.Erc20NativeCoinDenomPrefix) + + evmAppA := suite.chainA.App.(*evmd.EVMD) + ctx := suite.chainA.GetContext() + + // TEST: Verify that the prefix trimming works correctly + // The Transfer method should trim "erc20:" prefix to get the hex address + expectedTrimmed := strings.TrimPrefix(denom, erc20types.Erc20NativeCoinDenomPrefix) + suite.Require().Equal(nativeErc20.ContractAddr.String(), expectedTrimmed, + "Correct: Trimming 'erc20:' yields the contract address") + + // TEST: Verify that incorrect prefix trimming would fail + // If we incorrectly trim "erc20/" instead of "erc20:", we'd get the wrong string + incorrectTrimmed := strings.TrimPrefix(denom, erc20types.ModuleName+"/") + suite.Require().NotEqual(nativeErc20.ContractAddr.String(), incorrectTrimmed, + "Bug: Trimming 'erc20/' does not yield the contract address") + suite.Require().Equal(denom, incorrectTrimmed, + "Since 'erc20/' is not in the string, TrimPrefix returns it unchanged") + + // Verify that GetTokenPairID works correctly with the contract address (hex string) + pairIDFromAddress := evmAppA.Erc20Keeper.GetTokenPairID(ctx, nativeErc20.ContractAddr.String()) + suite.Require().NotEmpty(pairIDFromAddress) + + // Verify that GetTokenPairID works correctly with the full denom + pairIDFromDenom := evmAppA.Erc20Keeper.GetTokenPairID(ctx, denom) + suite.Require().NotEmpty(pairIDFromDenom) + + // Both should return the same pair ID + suite.Require().Equal(pairIDFromAddress, pairIDFromDenom) + + sender = nativeErc20.Account + }, + true, + }, + { + "pass - demonstrate bug impact", + func() { + nativeErc20 = SetupNativeErc20(suite.T(), suite.chainA, suite.chainA.SenderAccounts[0]) + denom = nativeErc20.Denom + amount = sdkmath.NewInt(nativeErc20.InitialBal.Int64()) + + evmAppA := suite.chainA.App.(*evmd.EVMD) + ctx := suite.chainA.GetContext() + + // Demonstrate the bug's impact: incorrect vs correct prefix trimming + // The denom format is "erc20:0x1234..." where "erc20:" is the prefix + + // CORRECT trimming: trim "erc20:" to get the hex address + correctTrimmed := strings.TrimPrefix(denom, erc20types.Erc20NativeCoinDenomPrefix) + suite.Require().Equal(nativeErc20.ContractAddr.String(), correctTrimmed, + "Trimming 'erc20:' should yield the contract address") + + // INCORRECT trimming: trim "erc20/" instead (the bug) + // This doesn't match the actual prefix, so TrimPrefix returns the string unchanged + incorrectTrimmed := strings.TrimPrefix(denom, erc20types.ModuleName+"/") + suite.Require().Equal(denom, incorrectTrimmed, + "Trimming 'erc20/' should not change the string since prefix is 'erc20:'") + suite.Require().NotEqual(nativeErc20.ContractAddr.String(), incorrectTrimmed, + "Incorrect trimming does not yield the contract address") + + // Demonstrate why the bug wasn't caught earlier: + // Both lookups work due to dual mapping in the keeper + // The keeper maps both "0x1234..." and "erc20:0x1234..." to the same pair + pairIDFromCorrect := evmAppA.Erc20Keeper.GetTokenPairID(ctx, correctTrimmed) // "0x1234..." + pairIDFromIncorrect := evmAppA.Erc20Keeper.GetTokenPairID(ctx, incorrectTrimmed) // "erc20:0x1234..." + + suite.Require().NotEmpty(pairIDFromCorrect) + suite.Require().NotEmpty(pairIDFromIncorrect) + suite.Require().Equal(pairIDFromCorrect, pairIDFromIncorrect, + "Both lookups succeed due to dual mapping, masking the prefix bug") + + sender = nativeErc20.Account + }, + true, + }, + } + + for _, tc := range testCases { + suite.Run(fmt.Sprintf("Case %s", tc.name), func() { + suite.SetupTest() + + // Setup IBC path + path = evmibctesting.NewTransferPath(suite.chainA, suite.chainB) + path.Setup() + + // Run test-specific setup + tc.malleate() + + // Call precompile transfer + data, err := suite.chainAPrecompile.ABI.Pack( + "transfer", + transfertypes.PortID, + path.EndpointA.ChannelID, + denom, + amount.BigInt(), + sender, + receiver, + timeoutHeight, + uint64(0), + "", + ) + suite.Require().NoError(err) + + res, _, _, err := suite.chainA.SendEvmTx( + suite.chainA.SenderAccounts[0], + 0, + suite.chainAPrecompile.Address(), + big.NewInt(0), + data, + 0, + ) + + if tc.expPass { + suite.Require().NoError(err) + suite.Require().Equal(uint32(0), res.Code, res.Log) + } else { + suite.Require().Error(err) + } + }) + } +} diff --git a/evmd/tests/ibc/ics20_precompile_transfer_test.go b/evmd/tests/ibc/ics20_precompile_transfer_test.go index fcb1df32c..c7a70c6c8 100644 --- a/evmd/tests/ibc/ics20_precompile_transfer_test.go +++ b/evmd/tests/ibc/ics20_precompile_transfer_test.go @@ -13,12 +13,14 @@ import ( "github.com/ethereum/go-ethereum/core/vm" "github.com/stretchr/testify/suite" + "github.com/cosmos/evm" "github.com/cosmos/evm/evmd" "github.com/cosmos/evm/evmd/tests/integration" "github.com/cosmos/evm/precompiles/ics20" chainutil "github.com/cosmos/evm/testutil" evmibctesting "github.com/cosmos/evm/testutil/ibc" evmante "github.com/cosmos/evm/x/vm/ante" + "github.com/cosmos/evm/x/vm/statedb" transfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" clienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types" @@ -51,6 +53,7 @@ func (suite *ICS20TransferTestSuite) SetupTest() { *evmAppA.StakingKeeper, evmAppA.TransferKeeper, evmAppA.IBCKeeper.ChannelKeeper, + evmAppA.Erc20Keeper, ) evmAppB := suite.chainB.App.(*evmd.EVMD) suite.chainBPrecompile = ics20.NewPrecompile( @@ -58,6 +61,7 @@ func (suite *ICS20TransferTestSuite) SetupTest() { *evmAppB.StakingKeeper, evmAppB.TransferKeeper, evmAppB.IBCKeeper.ChannelKeeper, + evmAppB.Erc20Keeper, ) } @@ -229,12 +233,15 @@ func (suite *ICS20TransferTestSuite) TestHandleMsgTransfer() { // denoms query method chainBAddr := common.BytesToAddress(suite.chainB.SenderAccount.GetAddress().Bytes()) ctxB := evmante.BuildEvmExecutionCtx(suite.chainB.GetContext()) + stateDB := statedb.New(ctxB, suite.chainB.App.(evm.EvmApp).GetEVMKeeper(), statedb.NewEmptyTxConfig()) evmRes, err := evmAppB.EVMKeeper.CallEVM( ctxB, + stateDB, suite.chainBPrecompile.ABI, chainBAddr, suite.chainBPrecompile.Address(), false, + false, nil, ics20.DenomsMethod, query.PageRequest{ @@ -254,10 +261,12 @@ func (suite *ICS20TransferTestSuite) TestHandleMsgTransfer() { // denom query method with result evmRes, err = evmAppB.EVMKeeper.CallEVM( ctxB, + stateDB, suite.chainBPrecompile.ABI, chainBAddr, suite.chainBPrecompile.Address(), false, + false, nil, ics20.DenomMethod, chainBDenom.Hash().String(), @@ -271,10 +280,12 @@ func (suite *ICS20TransferTestSuite) TestHandleMsgTransfer() { // denom query method not exists case evmRes, err = evmAppB.EVMKeeper.CallEVM( ctxB, + stateDB, suite.chainBPrecompile.ABI, chainBAddr, suite.chainBPrecompile.Address(), false, + false, nil, ics20.DenomMethod, "0000000000000000000000000000000000000000000000000000000000000000", @@ -288,10 +299,12 @@ func (suite *ICS20TransferTestSuite) TestHandleMsgTransfer() { // denom query method invalid error case evmRes, err = evmAppB.EVMKeeper.CallEVM( ctxB, + stateDB, suite.chainBPrecompile.ABI, chainBAddr, suite.chainBPrecompile.Address(), false, + false, nil, ics20.DenomMethod, "INVALID-DENOM-HASH", @@ -305,10 +318,12 @@ func (suite *ICS20TransferTestSuite) TestHandleMsgTransfer() { // denomHash query method evmRes, err = evmAppB.EVMKeeper.CallEVM( ctxB, + stateDB, suite.chainBPrecompile.ABI, chainBAddr, suite.chainBPrecompile.Address(), false, + false, nil, ics20.DenomHashMethod, chainBDenom.Path(), @@ -322,10 +337,12 @@ func (suite *ICS20TransferTestSuite) TestHandleMsgTransfer() { // denomHash query method not exists case evmRes, err = evmAppB.EVMKeeper.CallEVM( ctxB, + stateDB, suite.chainBPrecompile.ABI, chainBAddr, suite.chainBPrecompile.Address(), false, + false, nil, ics20.DenomHashMethod, "transfer/channel-0/erc20:not-exists-case", @@ -338,10 +355,12 @@ func (suite *ICS20TransferTestSuite) TestHandleMsgTransfer() { // denomHash query method invalid error case evmRes, err = evmAppB.EVMKeeper.CallEVM( ctxB, + stateDB, suite.chainBPrecompile.ABI, chainBAddr, suite.chainBPrecompile.Address(), false, + false, nil, ics20.DenomHashMethod, "", diff --git a/evmd/tests/ibc/ics20_recursive_precompile_calls_test.go b/evmd/tests/ibc/ics20_recursive_precompile_calls_test.go index 7af6ebeea..cc86d2231 100644 --- a/evmd/tests/ibc/ics20_recursive_precompile_calls_test.go +++ b/evmd/tests/ibc/ics20_recursive_precompile_calls_test.go @@ -6,34 +6,32 @@ package ibc import ( - "fmt" "math/big" "testing" - distributionkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper" - distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" - minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" - stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" - "github.com/cosmos/evm/utils" - - "github.com/cosmos/evm/contracts" - testutiltypes "github.com/cosmos/evm/testutil/types" - erc20types "github.com/cosmos/evm/x/erc20/types" - evmtypes "github.com/cosmos/evm/x/vm/types" - "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/suite" + "github.com/cosmos/evm/contracts" "github.com/cosmos/evm/evmd" "github.com/cosmos/evm/evmd/tests/integration" "github.com/cosmos/evm/precompiles/ics20" evmibctesting "github.com/cosmos/evm/testutil/ibc" + testutiltypes "github.com/cosmos/evm/testutil/types" + "github.com/cosmos/evm/utils" + erc20types "github.com/cosmos/evm/x/erc20/types" + "github.com/cosmos/evm/x/vm/statedb" + evmtypes "github.com/cosmos/evm/x/vm/types" transfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" clienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types" sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" + distributionkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper" + distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" ) // Test constants @@ -71,6 +69,7 @@ type stakingRewards struct { RewardAmt sdkmath.Int } + func (suite *ICS20RecursivePrecompileCallsTestSuite) prepareStakingRewards(ctx sdk.Context, stkRs ...stakingRewards) (sdk.Context, error) { for _, r := range stkRs { // set distribution module account balance which pays out the rewards @@ -153,12 +152,15 @@ func (suite *ICS20RecursivePrecompileCallsTestSuite) setupContractForTesting( suite.Require().NoError(err, "sending native tokens to contract should succeed") // Mint ERC20 tokens + stateDB := statedb.New(suite.chainA.GetContext(), evmAppA.GetEVMKeeper(), statedb.NewEmptyTxConfig()) _, err = evmAppA.GetEVMKeeper().CallEVM( suite.chainA.GetContext(), + stateDB, contractData.ABI, deployerAddr, contractAddr, true, + false, nil, "mint", senderEVMAddr, @@ -171,12 +173,15 @@ func (suite *ICS20RecursivePrecompileCallsTestSuite) setupContractForTesting( vals, err := evmAppA.StakingKeeper.GetAllValidators(suite.chainA.GetContext()) suite.Require().NoError(err) + stateDB = statedb.New(suite.chainA.GetContext(), evmAppA.GetEVMKeeper(), statedb.NewEmptyTxConfig()) _, err = evmAppA.GetEVMKeeper().CallEVM( ctxA, + stateDB, contractData.ABI, deployerAddr, contractAddr, true, + false, nil, "delegate", vals[0].OperatorAddress, @@ -220,12 +225,13 @@ func (suite *ICS20RecursivePrecompileCallsTestSuite) SetupTest() { *evmAppA.StakingKeeper, evmAppA.TransferKeeper, evmAppA.IBCKeeper.ChannelKeeper, + evmAppA.Erc20Keeper, ) bondDenom, err := evmAppA.StakingKeeper.BondDenom(suite.chainA.GetContext()) suite.Require().NoError(err) evmAppA.Erc20Keeper.GetTokenPair(suite.chainA.GetContext(), evmAppA.Erc20Keeper.GetTokenPairID(suite.chainA.GetContext(), bondDenom)) - //evmAppA.Erc20Keeper.SetNativePrecompile(suite.chainA.GetContext(), werc20.Address()) + // evmAppA.Erc20Keeper.SetNativePrecompile(suite.chainA.GetContext(), werc20.Address()) avail := evmAppA.Erc20Keeper.IsNativePrecompileAvailable(suite.chainA.GetContext(), common.HexToAddress("0xD4949664cD82660AaE99bEdc034a0deA8A0bd517")) suite.Require().True(avail) @@ -236,6 +242,7 @@ func (suite *ICS20RecursivePrecompileCallsTestSuite) SetupTest() { *evmAppB.StakingKeeper, evmAppB.TransferKeeper, evmAppB.IBCKeeper.ChannelKeeper, + evmAppB.Erc20Keeper, ) } @@ -343,7 +350,7 @@ func (suite *ICS20RecursivePrecompileCallsTestSuite) TestHandleMsgTransfer() { suite.Require().NoError(err) contractBondDenomBalance := evmAppA.BankKeeper.GetBalance(suite.chainA.GetContext(), nativeErc20.ContractAddr.Bytes(), bondDenom) - suite.Require().Equal(contractBondDenomBalance.Amount, sdkmath.NewInt(50)) + suite.Require().Equal(sdkmath.NewInt(50), contractBondDenomBalance.Amount) // Check distribution rewards after transfer afterRewards, err := querier.DelegationRewards(suite.chainA.GetContext(), &distrtypes.QueryDelegationRewardsRequest{ @@ -429,7 +436,6 @@ func (suite *ICS20RecursivePrecompileCallsTestSuite) TestHandleMsgTransfer() { suite.Require().NoError(err) eventAmount := len(res.Events) - fmt.Println(res.Events) tc.postCheck(querier, vals[0].OperatorAddress, eventAmount) @@ -452,7 +458,7 @@ func (suite *ICS20RecursivePrecompileCallsTestSuite) TestHandleMsgTransfer() { relayerBalance := GetBalance(relayerAddr) // relay send - pathAToB.EndpointA.Chain.SenderAccount = evmAppA.AccountKeeper.GetAccount(suite.chainA.GetContext(), relayerAddr) //update account in the path as the sequence recorded in that object is out of date + pathAToB.EndpointA.Chain.SenderAccount = evmAppA.AccountKeeper.GetAccount(suite.chainA.GetContext(), relayerAddr) // update account in the path as the sequence recorded in that object is out of date err = pathAToB.RelayPacket(packet) suite.Require().NoError(err) // relay committed @@ -491,6 +497,444 @@ func (suite *ICS20RecursivePrecompileCallsTestSuite) TestHandleMsgTransfer() { } } +// TestContractICS20TransferWithDelegationHook tests a contract calling ICS20 transfer +// on an ERC20 token that has a delegation in the beforeTransfer hook +// Contract -> ICS20 Start -> ERC20 -> Delegate -> ICS20 End +func (suite *ICS20RecursivePrecompileCallsTestSuite) TestContractICS20TransferWithDelegationHook() { + suite.SetupTest() // reset + + pathAToB := evmibctesting.NewTransferPath(suite.chainA, suite.chainB) + pathAToB.Setup() + + senderAccount := suite.chainA.SenderAccounts[SenderIndex] + + // Deploy ICS20TransferTester contract + testerData, err := contracts.LoadICS20TransferTester() + suite.Require().NoError(err) + + testerDeploymentData := testutiltypes.ContractDeploymentData{ + Contract: testerData, + ConstructorArgs: []interface{}{}, + } + + testerAddr, err := DeployContract(suite.T(), suite.chainA, testerDeploymentData) + suite.chainA.NextBlock() + suite.Require().NoError(err) + + // Deploy regular ERC20 token (no hooks) for dummy transfer + regularTokenData := contracts.ERC20MinterBurnerDecimalsContract + + regularTokenDeploymentData := testutiltypes.ContractDeploymentData{ + Contract: regularTokenData, + ConstructorArgs: []interface{}{"DummyToken", "DT", uint8(18)}, + } + + regularTokenAddr, err := DeployContract(suite.T(), suite.chainA, regularTokenDeploymentData) + suite.chainA.NextBlock() + suite.Require().NoError(err) + + // Deploy ERC20WithNativeTransfers contract + hookTokenData, err := contracts.LoadERC20WithNativeTransfers() + suite.Require().NoError(err) + + hookTokenDeploymentData := testutiltypes.ContractDeploymentData{ + Contract: hookTokenData, + ConstructorArgs: []interface{}{"HookToken", "HT", uint8(18)}, + } + + hookTokenAddr, err := DeployContract(suite.T(), suite.chainA, hookTokenDeploymentData) + suite.chainA.NextBlock() + suite.Require().NoError(err) + + evmAppA := suite.chainA.App.(*evmd.EVMD) + ctxA := suite.chainA.GetContext() + + // Register both ERC20 contracts + _, err = evmAppA.Erc20Keeper.RegisterERC20(ctxA, &erc20types.MsgRegisterERC20{ + Signer: evmAppA.AccountKeeper.GetModuleAddress("gov").String(), + Erc20Addresses: []string{hookTokenAddr.Hex()}, + }) + suite.Require().NoError(err, "registering hook token should succeed") + suite.chainA.NextBlock() + + // Mint tokens to tester contract using CallEVM (setup function) + hookTokenAmount := big.NewInt(InitialTokenAmount) + mintData, err := hookTokenData.ABI.Pack("mint", testerAddr, hookTokenAmount) + suite.Require().NoError(err) + + stateDB := statedb.New(ctxA, evmAppA.GetEVMKeeper(), statedb.NewEmptyTxConfig()) + deployer := common.BytesToAddress(suite.chainA.SenderPrivKey.PubKey().Address().Bytes()) + _, err = evmAppA.GetEVMKeeper().CallEVMWithData(ctxA, stateDB, deployer, &hookTokenAddr, mintData, true, false, nil) + suite.Require().NoError(err) + suite.chainA.NextBlock() + + // Mint regular tokens to tester for the dummy transfer + regularTokenAmount := big.NewInt(1000) + ctxA = suite.chainA.GetContext() + stateDB = statedb.New(ctxA, evmAppA.GetEVMKeeper(), statedb.NewEmptyTxConfig()) + _, err = evmAppA.GetEVMKeeper().CallEVM( + ctxA, + stateDB, + regularTokenData.ABI, + deployer, + regularTokenAddr, + true, + false, + nil, + "mint", + testerAddr, + regularTokenAmount, + ) + suite.Require().NoError(err, "mint regular tokens to tester should succeed") + suite.chainA.NextBlock() + + // Configure hook to perform delegation + bondDenom, err := evmAppA.StakingKeeper.BondDenom(ctxA) + suite.Require().NoError(err) + + vals, err := evmAppA.StakingKeeper.GetAllValidators(ctxA) + suite.Require().NoError(err) + validatorAddr := vals[0].OperatorAddress + + // Fund hook token contract with native tokens for delegation using EVM transaction + hookTokenAddrSDK := sdk.AccAddress(hookTokenAddr.Bytes()) + delegationAmountSDK := sdkmath.NewInt(DelegationAmount / 1_000_000_000_000) // Convert from wei to base denom + // Fund exact amount: delegation + 2 native transfers (1 aatom each, no rounding with 1e12 wei) + fundAmountAatom := delegationAmountSDK.AddRaw(2) // +2 for two 1 aatom native transfers + + // Convert aatom to wei for EVM transaction: aatom * 1e12 = wei + fundAmountWei := new(big.Int).Mul(fundAmountAatom.BigInt(), big.NewInt(1_000_000_000_000)) + + // Send EVM transaction with value to fund the contract (updates both bank module and StateDB) + _, _, _, err = suite.chainA.SendEvmTx( + senderAccount, + 0, // senderAccIdx + hookTokenAddr, // to (not pointer) + fundAmountWei, + []byte{}, // empty calldata + 0, // gasLimit (0 = auto) + ) + suite.Require().NoError(err) + suite.chainA.NextBlock() + + // Configure hook parameters + recipient1 := common.BytesToAddress(senderAccount.SenderAccount.GetAddress().Bytes()) + recipient2 := recipient1 + transferAmount := big.NewInt(1_000_000_000_000) // 1e12 wei = exactly 1 aatom (no rounding) + delegateAmount := big.NewInt(DelegationAmount / 1_000_000_000_000) // Already in base denom + + configData, err := hookTokenData.ABI.Pack( + "configureHook", + recipient1, + recipient2, + transferAmount, + validatorAddr, + delegateAmount, + true, // enable hook + ) + suite.Require().NoError(err) + + // Use CallEVM for configuration (setup function) + ctxA = suite.chainA.GetContext() + stateDB = statedb.New(ctxA, evmAppA.GetEVMKeeper(), statedb.NewEmptyTxConfig()) + _, err = evmAppA.GetEVMKeeper().CallEVMWithData(ctxA, stateDB, deployer, &hookTokenAddr, configData, true, false, nil) + suite.Require().NoError(err) + suite.chainA.NextBlock() + + // Setup transfer parameters + timeoutHeight := clienttypes.NewHeight(1, TimeoutHeight) + hookTokenDenom := "erc20:" + hookTokenAddr.Hex() + transferTokenAmount := sdkmath.NewIntFromBigInt(big.NewInt(InitialTokenAmount / 2)) + + // Get balances before + ctxA = suite.chainA.GetContext() + testerHookBalBefore := evmAppA.Erc20Keeper.BalanceOf(ctxA, hookTokenData.ABI, hookTokenAddr, testerAddr) + hookTokenNativeBalBefore := evmAppA.GetBankKeeper().GetBalance(ctxA, hookTokenAddrSDK, bondDenom) + recipientAddrSDK := senderAccount.SenderAccount.GetAddress() + recipientNativeBalBefore := evmAppA.GetBankKeeper().GetBalance(ctxA, recipientAddrSDK, bondDenom) + + // Call scenario9_transferICS20Transfer from tester contract + callData, err := testerData.ABI.Pack( + "scenario9_transferICS20Transfer", + regularTokenAddr, // token (regular token without hooks) + recipient1, // recipient for dummy transfer + big.NewInt(100), // transferAmount (dummy transfer to avoid triggering hookToken hook) + pathAToB.EndpointA.ChannelConfig.PortID, // sourcePort + pathAToB.EndpointA.ChannelID, // sourceChannel + hookTokenDenom, // denom + transferTokenAmount.BigInt(), // ics20Amount + suite.chainB.SenderAccount.GetAddress().String(), // ics20Receiver + timeoutHeight, // timeoutHeight + uint64(0), // timeoutTimestamp + ) + suite.Require().NoError(err) + + // Execute transaction + res, _, _, err := suite.chainA.SendEvmTx(senderAccount, SenderIndex, testerAddr, big.NewInt(0), callData, 0) + suite.Require().NoError(err) + + // Get balances after + ctxA = suite.chainA.GetContext() + testerHookBalAfter := evmAppA.Erc20Keeper.BalanceOf(ctxA, hookTokenData.ABI, hookTokenAddr, testerAddr) + hookTokenNativeBalAfter := evmAppA.GetBankKeeper().GetBalance(ctxA, hookTokenAddrSDK, bondDenom) + recipientNativeBalAfter := evmAppA.GetBankKeeper().GetBalance(ctxA, recipientAddrSDK, bondDenom) + + // Verify ERC20 balance changes + expectedERC20Delta := transferTokenAmount.BigInt() + actualERC20Delta := new(big.Int).Sub(testerHookBalBefore, testerHookBalAfter) + suite.Require().Equal(expectedERC20Delta.String(), actualERC20Delta.String(), "hook token should be transferred via ICS20") + + // Verify native balance changes + // Hook contract should lose: delegation (1e6 aatom) + 2 native transfers (1 aatom each) = 1_000_002 aatom + conversionFactor := int64(1_000_000_000_000) // 1e12 wei to aatom conversion + expectedDelegationAatom := DelegationAmount / conversionFactor + expectedTransferAatom := int64(2) // 2 transfers of 1e12 wei each = 2 aatom total + expectedHookNativeDelta := sdkmath.NewInt(expectedDelegationAatom + expectedTransferAatom) + actualHookNativeDelta := hookTokenNativeBalBefore.Amount.Sub(hookTokenNativeBalAfter.Amount) + suite.Require().Equal(expectedHookNativeDelta.String(), actualHookNativeDelta.String(), + "hook token contract should lose delegation + 2 native transfers") + + // Recipient should receive 2 native transfers (2 aatom) + expectedRecipientDelta := sdkmath.NewInt(expectedTransferAatom) + actualRecipientDelta := recipientNativeBalAfter.Amount.Sub(recipientNativeBalBefore.Amount) + suite.Require().Equal(expectedRecipientDelta.String(), actualRecipientDelta.String(), + "recipient should receive 2 native transfers") + + // Verify delegation occurred + delegations, err := evmAppA.StakingKeeper.GetAllDelegatorDelegations(ctxA, hookTokenAddrSDK) + suite.Require().NoError(err) + suite.Require().Equal(1, len(delegations), "should have 1 delegation from beforeTransfer hook") + + // Verify total bonded amount + bondedTokens, err := evmAppA.StakingKeeper.GetDelegatorBonded(ctxA, hookTokenAddrSDK) + suite.Require().NoError(err) + expectedBondedAmount := DelegationAmount / 1_000_000_000_000 // Convert wei to base denom + suite.Require().Equal(int64(expectedBondedAmount), bondedTokens.Int64(), + "bonded tokens should equal delegation amount") + + // Verify event count + suite.Require().Equal(PreciseBankMintEventCount+PreciseBankBurnEventCount+DelegationEventCount+ICS20WithConversionEventCount+EVMEventCount, len(res.Events), "should have 41 events") +} + +// TestContractICS20TransferRevertWithDelegationHook tests a contract with reverted ERC20 transfer +// followed by ICS20 transfer on an ERC20 token that has a delegation in the beforeTransfer hook +func (suite *ICS20RecursivePrecompileCallsTestSuite) TestContractICS20TransferRevertWithDelegationHook() { + suite.SetupTest() // reset + + pathAToB := evmibctesting.NewTransferPath(suite.chainA, suite.chainB) + pathAToB.Setup() + + senderAccount := suite.chainA.SenderAccounts[SenderIndex] + + // Deploy ICS20TransferTester contract + testerData, err := contracts.LoadICS20TransferTester() + suite.Require().NoError(err) + + testerDeploymentData := testutiltypes.ContractDeploymentData{ + Contract: testerData, + ConstructorArgs: []interface{}{}, + } + + testerAddr, err := DeployContract(suite.T(), suite.chainA, testerDeploymentData) + suite.chainA.NextBlock() + suite.Require().NoError(err) + + // Deploy regular ERC20 token for the first transfer (that will revert) + regularTokenData := contracts.ERC20MinterBurnerDecimalsContract + + regularTokenDeploymentData := testutiltypes.ContractDeploymentData{ + Contract: regularTokenData, + ConstructorArgs: []interface{}{"RegularToken", "RT", uint8(18)}, + } + + regularTokenAddr, err := DeployContract(suite.T(), suite.chainA, regularTokenDeploymentData) + suite.chainA.NextBlock() + suite.Require().NoError(err) + + // Deploy ERC20WithNativeTransfers contract + hookTokenData, err := contracts.LoadERC20WithNativeTransfers() + suite.Require().NoError(err) + + hookTokenDeploymentData := testutiltypes.ContractDeploymentData{ + Contract: hookTokenData, + ConstructorArgs: []interface{}{"HookToken", "HT", uint8(18)}, + } + + hookTokenAddr, err := DeployContract(suite.T(), suite.chainA, hookTokenDeploymentData) + suite.chainA.NextBlock() + suite.Require().NoError(err) + + evmAppA := suite.chainA.App.(*evmd.EVMD) + ctxA := suite.chainA.GetContext() + + // Get deployer address for CallEVM + deployer := common.BytesToAddress(suite.chainA.SenderPrivKey.PubKey().Address().Bytes()) + + // Register both ERC20 contracts + _, err = evmAppA.Erc20Keeper.RegisterERC20(ctxA, &erc20types.MsgRegisterERC20{ + Signer: evmAppA.AccountKeeper.GetModuleAddress("gov").String(), + Erc20Addresses: []string{regularTokenAddr.Hex(), hookTokenAddr.Hex()}, + }) + suite.Require().NoError(err, "registering tokens should succeed") + suite.chainA.NextBlock() + + // Mint only a small amount of regular tokens to tester using CallEVM (setup function) + regularTokenAmount := big.NewInt(1000) + mintRegularData, err := regularTokenData.ABI.Pack("mint", testerAddr, regularTokenAmount) + suite.Require().NoError(err) + + stateDB := statedb.New(ctxA, evmAppA.GetEVMKeeper(), statedb.NewEmptyTxConfig()) + _, err = evmAppA.GetEVMKeeper().CallEVMWithData(ctxA, stateDB, deployer, ®ularTokenAddr, mintRegularData, true, false, nil) + suite.Require().NoError(err) + suite.chainA.NextBlock() + + // Mint tokens to tester contract (hook token) using CallEVM (setup function) + hookTokenAmount := big.NewInt(InitialTokenAmount) + mintHookData, err := hookTokenData.ABI.Pack("mint", testerAddr, hookTokenAmount) + suite.Require().NoError(err) + + ctxA = suite.chainA.GetContext() + stateDB = statedb.New(ctxA, evmAppA.GetEVMKeeper(), statedb.NewEmptyTxConfig()) + _, err = evmAppA.GetEVMKeeper().CallEVMWithData(ctxA, stateDB, deployer, &hookTokenAddr, mintHookData, true, false, nil) + suite.Require().NoError(err) + suite.chainA.NextBlock() + + vals, err := evmAppA.StakingKeeper.GetAllValidators(ctxA) + suite.Require().NoError(err) + validatorAddr := vals[0].OperatorAddress + + // Fund hook token contract with native tokens for delegation using EVM transaction + hookTokenAddrSDK := sdk.AccAddress(hookTokenAddr.Bytes()) + delegationAmountSDK := sdkmath.NewInt(DelegationAmount / 1_000_000_000_000) // Convert from wei to base denom + // Fund exact amount: delegation + 2 native transfers (1 aatom each, no rounding with 1e12 wei) + fundAmountAatom := delegationAmountSDK.AddRaw(2) // +2 for two 1 aatom native transfers + + // Convert aatom to wei for EVM transaction: aatom * 1e12 = wei + fundAmountWei := new(big.Int).Mul(fundAmountAatom.BigInt(), big.NewInt(1_000_000_000_000)) + + // Send EVM transaction with value to fund the contract (updates both bank module and StateDB) + _, _, _, err = suite.chainA.SendEvmTx( + senderAccount, + 0, // senderAccIdx + hookTokenAddr, // to (not pointer) + fundAmountWei, + []byte{}, // empty calldata + 0, // gasLimit (0 = auto) + ) + suite.Require().NoError(err) + suite.chainA.NextBlock() + + // Configure hook parameters + recipient1 := common.BytesToAddress(senderAccount.SenderAccount.GetAddress().Bytes()) + recipient2 := recipient1 + transferAmount := big.NewInt(1_000_000_000_000) // 1e12 wei = exactly 1 aatom (no rounding) + delegateAmount := big.NewInt(DelegationAmount / 1_000_000_000_000) // Already in base denom + + configData, err := hookTokenData.ABI.Pack( + "configureHook", + recipient1, + recipient2, + transferAmount, + validatorAddr, + delegateAmount, + true, // enable hook + ) + suite.Require().NoError(err) + + // Use CallEVM for configuration (setup function) + ctxA = suite.chainA.GetContext() + stateDB = statedb.New(ctxA, evmAppA.GetEVMKeeper(), statedb.NewEmptyTxConfig()) + _, err = evmAppA.GetEVMKeeper().CallEVMWithData(ctxA, stateDB, deployer, &hookTokenAddr, configData, true, false, nil) + suite.Require().NoError(err) + suite.chainA.NextBlock() + + // Setup transfer parameters + timeoutHeight := clienttypes.NewHeight(1, TimeoutHeight) + hookTokenDenom := "erc20:" + hookTokenAddr.Hex() + transferTokenAmount := sdkmath.NewIntFromBigInt(big.NewInt(InitialTokenAmount / 2)) + + // Get balances before + ctxA = suite.chainA.GetContext() + bondDenom, err := evmAppA.StakingKeeper.BondDenom(ctxA) + suite.Require().NoError(err) + regularTokenBalBefore := evmAppA.Erc20Keeper.BalanceOf(ctxA, regularTokenData.ABI, regularTokenAddr, testerAddr) + testerHookBalBefore := evmAppA.Erc20Keeper.BalanceOf(ctxA, hookTokenData.ABI, hookTokenAddr, testerAddr) + hookTokenNativeBalBefore := evmAppA.GetBankKeeper().GetBalance(ctxA, hookTokenAddrSDK, bondDenom) + recipientAddrSDK := senderAccount.SenderAccount.GetAddress() + recipientNativeBalBefore := evmAppA.GetBankKeeper().GetBalance(ctxA, recipientAddrSDK, bondDenom) + + // Call scenario10_transferICS20TransferRevert from tester contract + // First transfer will revert due to excessive amount + excessiveAmount := big.NewInt(1000000) // More than the minted amount + callData, err := testerData.ABI.Pack( + "scenario10_transferICS20TransferRevert", + regularTokenAddr, // token (will revert) + recipient1, // recipient + excessiveAmount, // excessive transferAmount (will revert) + pathAToB.EndpointA.ChannelConfig.PortID, // sourcePort + pathAToB.EndpointA.ChannelID, // sourceChannel + hookTokenDenom, // denom + transferTokenAmount.BigInt(), // ics20Amount + suite.chainB.SenderAccount.GetAddress().String(), // ics20Receiver + timeoutHeight, // timeoutHeight + uint64(0), // timeoutTimestamp + ) + suite.Require().NoError(err) + + // Execute contract call + res, _, _, err := suite.chainA.SendEvmTx(senderAccount, SenderIndex, testerAddr, big.NewInt(0), callData, 0) + suite.Require().NoError(err) + + // Get balances after + ctxA = suite.chainA.GetContext() + regularTokenBalAfter := evmAppA.Erc20Keeper.BalanceOf(ctxA, regularTokenData.ABI, regularTokenAddr, testerAddr) + testerHookBalAfter := evmAppA.Erc20Keeper.BalanceOf(ctxA, hookTokenData.ABI, hookTokenAddr, testerAddr) + hookTokenNativeBalAfter := evmAppA.GetBankKeeper().GetBalance(ctxA, hookTokenAddrSDK, bondDenom) + recipientNativeBalAfter := evmAppA.GetBankKeeper().GetBalance(ctxA, recipientAddrSDK, bondDenom) + + // Verify regular token balance unchanged (transfer reverted) + suite.Require().Equal(regularTokenBalBefore.String(), regularTokenBalAfter.String(), + "regular token balance should be unchanged since transfer reverted") + + // Verify hook token balance changed (ICS20 transfer succeeded) + expectedERC20Delta := transferTokenAmount.BigInt() + actualERC20Delta := new(big.Int).Sub(testerHookBalBefore, testerHookBalAfter) + suite.Require().Equal(expectedERC20Delta.String(), actualERC20Delta.String(), + "hook token should be transferred via ICS20") + + // Verify native balance changes + // Hook contract should lose: delegation (1e6 aatom) + 2 native transfers (1 aatom each) = 1_000_002 aatom + conversionFactor := int64(1_000_000_000_000) // 1e12 wei to aatom conversion + expectedDelegationAatom := DelegationAmount / conversionFactor + expectedTransferAatom := int64(2) // 2 transfers of 1e12 wei each = 2 aatom total + expectedHookNativeDelta := sdkmath.NewInt(expectedDelegationAatom + expectedTransferAatom) + actualHookNativeDelta := hookTokenNativeBalBefore.Amount.Sub(hookTokenNativeBalAfter.Amount) + suite.Require().Equal(expectedHookNativeDelta.String(), actualHookNativeDelta.String(), + "hook token contract should lose delegation + 2 native transfers") + + // Recipient should receive 2 native transfers (2 aatom) + expectedRecipientDelta := sdkmath.NewInt(expectedTransferAatom) + actualRecipientDelta := recipientNativeBalAfter.Amount.Sub(recipientNativeBalBefore.Amount) + suite.Require().Equal(expectedRecipientDelta.String(), actualRecipientDelta.String(), + "recipient should receive 2 native transfers") + + // Verify delegation occurred (from beforeTransfer hook) + delegations, err := evmAppA.StakingKeeper.GetAllDelegatorDelegations(ctxA, hookTokenAddrSDK) + suite.Require().NoError(err) + suite.Require().Equal(1, len(delegations), "should have 1 delegation from beforeTransfer hook") + + // Verify total bonded amount + bondedTokens, err := evmAppA.StakingKeeper.GetDelegatorBonded(ctxA, hookTokenAddrSDK) + suite.Require().NoError(err) + expectedBondedAmount := DelegationAmount / 1_000_000_000_000 // Convert wei to base denom + suite.Require().Equal(int64(expectedBondedAmount), bondedTokens.Int64(), + "bonded tokens should equal delegation amount") + + // Verify event count + suite.Require().Equal(PreciseBankMintEventCount+PreciseBankBurnEventCount+DelegationEventCount+ICS20WithConversionEventCount+EVMEventCount, len(res.Events), "should have 41 events") + +} + func TestICS20RecursivePrecompileCallsTestSuite(t *testing.T) { suite.Run(t, new(ICS20RecursivePrecompileCallsTestSuite)) } diff --git a/evmd/tests/ibc/ics20_sequential_precompile_calls_test.go b/evmd/tests/ibc/ics20_sequential_precompile_calls_test.go new file mode 100644 index 000000000..105026f43 --- /dev/null +++ b/evmd/tests/ibc/ics20_sequential_precompile_calls_test.go @@ -0,0 +1,199 @@ +package ibc + +import ( + "fmt" + "github.com/cosmos/evm/testutil" + "math/big" + "testing" + + "github.com/cosmos/evm/contracts" + testutiltypes "github.com/cosmos/evm/testutil/types" + erc20types "github.com/cosmos/evm/x/erc20/types" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/suite" + + "github.com/cosmos/evm/evmd" + "github.com/cosmos/evm/evmd/tests/integration" + "github.com/cosmos/evm/precompiles/ics20" + evmibctesting "github.com/cosmos/evm/testutil/ibc" +) + +// Test constants for sequential ICS20 sends +const ( + SeqICS20InitialTokenAmount = 1_000_000_000_000_000_000 // 1 token with 18 decimals + SeqICS20SenderIndex = 1 + SeqICS20TimeoutHeight = 110 +) + +// Test suite for ICS20 sequential max sends +type ICS20SequentialPrecompileCallsTestSuite struct { + suite.Suite + + coordinator *evmibctesting.Coordinator + + chainA *evmibctesting.TestChain + chainAPrecompile *ics20.Precompile + chainB *evmibctesting.TestChain + chainBPrecompile *ics20.Precompile +} + +func (suite *ICS20SequentialPrecompileCallsTestSuite) SetupTest() { + suite.coordinator = evmibctesting.NewCoordinator(suite.T(), 2, 0, integration.SetupEvmd) + suite.chainA = suite.coordinator.GetChain(evmibctesting.GetEvmChainID(1)) + suite.chainB = suite.coordinator.GetChain(evmibctesting.GetEvmChainID(2)) + + evmAppA := suite.chainA.App.(*evmd.EVMD) + suite.chainAPrecompile = ics20.NewPrecompile( + evmAppA.BankKeeper, + *evmAppA.StakingKeeper, + evmAppA.TransferKeeper, + evmAppA.IBCKeeper.ChannelKeeper, + evmAppA.Erc20Keeper, + ) + bondDenom, err := evmAppA.StakingKeeper.BondDenom(suite.chainA.GetContext()) + suite.Require().NoError(err) + + evmAppA.Erc20Keeper.GetTokenPair(suite.chainA.GetContext(), evmAppA.Erc20Keeper.GetTokenPairID(suite.chainA.GetContext(), bondDenom)) + + avail := evmAppA.Erc20Keeper.IsNativePrecompileAvailable(suite.chainA.GetContext(), common.HexToAddress("0xD4949664cD82660AaE99bEdc034a0deA8A0bd517")) + suite.Require().True(avail) + + evmAppB := suite.chainB.App.(*evmd.EVMD) + suite.chainBPrecompile = ics20.NewPrecompile( + evmAppB.BankKeeper, + *evmAppB.StakingKeeper, + evmAppB.TransferKeeper, + evmAppB.IBCKeeper.ChannelKeeper, + evmAppB.Erc20Keeper, + ) +} + +// TestReceiveAndSendTwice tests the production flow: transfer tokens in -> ICS20 sends -> transfer out +// This mirrors the actual transaction pattern seen on-chain. +func (suite *ICS20SequentialPrecompileCallsTestSuite) TestReceiveAndSendTwice() { + suite.SetupTest() + + pathAToB := evmibctesting.NewTransferPath(suite.chainA, suite.chainB) + pathAToB.Setup() + + evmAppA := suite.chainA.App.(*evmd.EVMD) + // Deployer has MINTER_ROLE - use it for minting + deployerAddr := common.BytesToAddress(suite.chainA.SenderPrivKey.PubKey().Address().Bytes()) + // Use a different account for the actual test tx + senderAccount := suite.chainA.SenderAccounts[SeqICS20SenderIndex] + senderAddr := common.BytesToAddress(senderAccount.SenderAccount.GetAddress().Bytes()) + + // 1. Deploy ERC20 contract + erc20ContractData := contracts.ERC20MinterBurnerDecimalsContract + erc20DeploymentData := testutiltypes.ContractDeploymentData{ + Contract: erc20ContractData, + ConstructorArgs: []interface{}{"TestToken", "TT", uint8(18)}, + } + erc20Addr, err := DeployContract(suite.T(), suite.chainA, erc20DeploymentData) + suite.chainA.NextBlock() + suite.Require().NoError(err) + fmt.Printf("ERC20 contract deployed at: %s\n", erc20Addr.Hex()) + + // 2. Register the ERC20 + _, err = evmAppA.Erc20Keeper.RegisterERC20(suite.chainA.GetContext(), &erc20types.MsgRegisterERC20{ + Signer: evmAppA.AccountKeeper.GetModuleAddress("gov").String(), + Erc20Addresses: []string{erc20Addr.Hex()}, + }) + suite.Require().NoError(err) + suite.chainA.NextBlock() + + // 3. Deploy the Sender contract + senderContractData, err := contracts.LoadSequentialICS20Sender() + suite.Require().NoError(err) + senderDeploymentData := testutiltypes.ContractDeploymentData{ + Contract: senderContractData, + ConstructorArgs: []interface{}{}, + } + contractAddr, err := DeployContract(suite.T(), suite.chainA, senderDeploymentData) + suite.chainA.NextBlock() + suite.Require().NoError(err) + fmt.Printf("Sender contract deployed at: %s\n", contractAddr.Hex()) + + // 4. Mint ERC20 tokens to the test sender using deployer (who has MINTER_ROLE) + mintStateDB := testutil.NewStateDB(suite.chainA.GetContext(), evmAppA.EVMKeeper) + _, err = evmAppA.GetEVMKeeper().CallEVM(suite.chainA.GetContext(), mintStateDB, erc20ContractData.ABI, deployerAddr, erc20Addr, true, false, nil, "mint", senderAddr, big.NewInt(SeqICS20InitialTokenAmount)) + suite.Require().NoError(err) + suite.chainA.NextBlock() + + // Verify minted balance + senderBal := evmAppA.GetErc20Keeper().BalanceOf(suite.chainA.GetContext(), erc20ContractData.ABI, erc20Addr, senderAddr) + suite.Require().Equal(big.NewInt(SeqICS20InitialTokenAmount), senderBal) + fmt.Printf("Sender balance before: %s\n", senderBal.String()) + + // 5. Approve the contract to spend tokens (called by sender) + approveStateDB := testutil.NewStateDB(suite.chainA.GetContext(), evmAppA.EVMKeeper) + _, err = evmAppA.GetEVMKeeper().CallEVM(suite.chainA.GetContext(), approveStateDB, erc20ContractData.ABI, senderAddr, erc20Addr, true, false, nil, "approve", contractAddr, big.NewInt(SeqICS20InitialTokenAmount*2)) + suite.Require().NoError(err) + suite.chainA.NextBlock() + + // Get balances before + senderBalBefore := evmAppA.GetErc20Keeper().BalanceOf(suite.chainA.GetContext(), erc20ContractData.ABI, erc20Addr, senderAddr) + contractBalBefore := evmAppA.GetErc20Keeper().BalanceOf(suite.chainA.GetContext(), erc20ContractData.ABI, erc20Addr, contractAddr) + fmt.Printf("Before tx - Sender: %s, Contract: %s\n", senderBalBefore.String(), contractBalBefore.String()) + + // 6. Call receiveAndSendTwice - this should: + // - Transfer tokens from sender to contract + // - Attempt first ICS20 send (should succeed) + // - Attempt second ICS20 send (should fail - no balance) + // - Revert and return tokens + denom := "erc20:" + erc20Addr.Hex() + data, err := senderContractData.ABI.Pack( + "receiveAndSendTwice", + erc20Addr, + pathAToB.EndpointA.ChannelConfig.PortID, + pathAToB.EndpointA.ChannelID, + denom, + suite.chainB.SenderAccount.GetAddress().String(), + uint64(SeqICS20TimeoutHeight), + big.NewInt(SeqICS20InitialTokenAmount), + ) + suite.Require().NoError(err) + + res, _, _, err := suite.chainA.SendEvmTx(senderAccount, SeqICS20SenderIndex, contractAddr, big.NewInt(0), data, 0) + + // Log events + if res != nil { + fmt.Printf("Total events: %d\n", len(res.Events)) + for i, event := range res.Events { + fmt.Printf("Event %d: type=%s\n", i, event.Type) + for _, attr := range event.Attributes { + fmt.Printf(" %s: %s\n", attr.Key, attr.Value) + } + } + } + + // Check for revert + hasRevertEvent := false + if res != nil { + for _, event := range res.Events { + if event.Type == "ethereum_tx" { + for _, attr := range event.Attributes { + if attr.Key == "ethereumTxFailed" { + hasRevertEvent = true + fmt.Printf("Found revert: %s\n", attr.Value) + } + } + } + } + } + + // Get balances after + senderBalAfter := evmAppA.GetErc20Keeper().BalanceOf(suite.chainA.GetContext(), erc20ContractData.ABI, erc20Addr, senderAddr) + contractBalAfter := evmAppA.GetErc20Keeper().BalanceOf(suite.chainA.GetContext(), erc20ContractData.ABI, erc20Addr, contractAddr) + fmt.Printf("After tx - Sender: %s, Contract: %s\n", senderBalAfter.String(), contractBalAfter.String()) + + // The second ICS20 send should have failed, causing a revert + // Sender balance should be unchanged (tokens returned on revert) + suite.Require().True(hasRevertEvent || err != nil, "expected transaction to revert on second ICS20 send") + suite.Require().Equal(senderBalBefore.String(), senderBalAfter.String(), "sender balance should be unchanged after revert") + suite.Require().Equal(contractBalBefore.String(), contractBalAfter.String(), "contract balance should be unchanged after revert") +} + +func TestICS20SequentialPrecompileCallsTestSuite(t *testing.T) { + suite.Run(t, new(ICS20SequentialPrecompileCallsTestSuite)) +} diff --git a/evmd/tests/ibc/v2_ics20_precompile_transfer_test.go b/evmd/tests/ibc/v2_ics20_precompile_transfer_test.go index 27c7c4565..01421ddb7 100644 --- a/evmd/tests/ibc/v2_ics20_precompile_transfer_test.go +++ b/evmd/tests/ibc/v2_ics20_precompile_transfer_test.go @@ -14,12 +14,14 @@ import ( "github.com/ethereum/go-ethereum/core/vm" "github.com/stretchr/testify/suite" + "github.com/cosmos/evm" "github.com/cosmos/evm/evmd" "github.com/cosmos/evm/evmd/tests/integration" "github.com/cosmos/evm/precompiles/ics20" chainutil "github.com/cosmos/evm/testutil" evmibctesting "github.com/cosmos/evm/testutil/ibc" evmante "github.com/cosmos/evm/x/vm/ante" + "github.com/cosmos/evm/x/vm/statedb" transfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" clienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types" @@ -52,6 +54,7 @@ func (suite *ICS20TransferV2TestSuite) SetupTest() { *evmAppA.StakingKeeper, evmAppA.TransferKeeper, evmAppA.IBCKeeper.ChannelKeeper, + evmAppA.Erc20Keeper, ) evmAppB := suite.chainB.App.(*evmd.EVMD) suite.chainBPrecompile = ics20.NewPrecompile( @@ -59,6 +62,7 @@ func (suite *ICS20TransferV2TestSuite) SetupTest() { *evmAppB.StakingKeeper, evmAppB.TransferKeeper, evmAppB.IBCKeeper.ChannelKeeper, + evmAppB.Erc20Keeper, ) } @@ -235,12 +239,15 @@ func (suite *ICS20TransferV2TestSuite) TestHandleMsgTransfer() { // denoms query method chainBAddr := common.BytesToAddress(suite.chainB.SenderAccount.GetAddress().Bytes()) ctxB := evmante.BuildEvmExecutionCtx(suite.chainB.GetContext()) + stateDB := statedb.New(ctxB, suite.chainB.App.(evm.EvmApp).GetEVMKeeper(), statedb.NewEmptyTxConfig()) evmRes, err := evmAppB.EVMKeeper.CallEVM( ctxB, + stateDB, suite.chainBPrecompile.ABI, chainBAddr, suite.chainBPrecompile.Address(), false, + false, nil, ics20.DenomsMethod, query.PageRequest{ @@ -260,10 +267,12 @@ func (suite *ICS20TransferV2TestSuite) TestHandleMsgTransfer() { // denom query method evmRes, err = evmAppB.EVMKeeper.CallEVM( ctxB, + stateDB, suite.chainBPrecompile.ABI, chainBAddr, suite.chainBPrecompile.Address(), false, + false, nil, ics20.DenomMethod, chainBDenom.Hash().String(), @@ -277,10 +286,12 @@ func (suite *ICS20TransferV2TestSuite) TestHandleMsgTransfer() { // denom query method not exists case evmRes, err = evmAppB.EVMKeeper.CallEVM( ctxB, + stateDB, suite.chainBPrecompile.ABI, chainBAddr, suite.chainBPrecompile.Address(), false, + false, nil, ics20.DenomMethod, "0000000000000000000000000000000000000000000000000000000000000000", @@ -294,10 +305,12 @@ func (suite *ICS20TransferV2TestSuite) TestHandleMsgTransfer() { // denom query method invalid error case evmRes, err = evmAppB.EVMKeeper.CallEVM( ctxB, + stateDB, suite.chainBPrecompile.ABI, chainBAddr, suite.chainBPrecompile.Address(), false, + false, nil, ics20.DenomMethod, "INVALID-DENOM-HASH", @@ -310,10 +323,12 @@ func (suite *ICS20TransferV2TestSuite) TestHandleMsgTransfer() { // denomHash query method evmRes, err = evmAppB.EVMKeeper.CallEVM( ctxB, + stateDB, suite.chainBPrecompile.ABI, chainBAddr, suite.chainBPrecompile.Address(), false, + false, nil, ics20.DenomHashMethod, chainBDenom.Path(), @@ -327,10 +342,12 @@ func (suite *ICS20TransferV2TestSuite) TestHandleMsgTransfer() { // denomHash query method not exists case evmRes, err = evmAppB.EVMKeeper.CallEVM( ctxB, + stateDB, suite.chainBPrecompile.ABI, chainBAddr, suite.chainBPrecompile.Address(), false, + false, nil, ics20.DenomHashMethod, "transfer/channel-0/erc20:not-exists-case", @@ -343,10 +360,12 @@ func (suite *ICS20TransferV2TestSuite) TestHandleMsgTransfer() { // denomHash query method invalid error case evmRes, err = evmAppB.EVMKeeper.CallEVM( ctxB, + stateDB, suite.chainBPrecompile.ABI, chainBAddr, suite.chainBPrecompile.Address(), false, + false, nil, ics20.DenomHashMethod, "", diff --git a/evmd/tests/integration/balance_handler/helper.go b/evmd/tests/integration/balance_handler/helper.go index 85c6a0a6c..3d9c8e75f 100644 --- a/evmd/tests/integration/balance_handler/helper.go +++ b/evmd/tests/integration/balance_handler/helper.go @@ -7,11 +7,12 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" - errorsmod "cosmossdk.io/errors" - "github.com/cosmos/evm" - testutiltypes "github.com/cosmos/evm/testutil/types" evmibctesting "github.com/cosmos/evm/testutil/ibc" + testutiltypes "github.com/cosmos/evm/testutil/types" + "github.com/cosmos/evm/x/vm/statedb" + + errorsmod "cosmossdk.io/errors" ) // DeployContract deploys a contract to the test chain @@ -32,8 +33,9 @@ func DeployContract(t *testing.T, chain *evmibctesting.TestChain, deploymentData data := deploymentData.Contract.Bin data = append(data, ctorArgs...) + stateDB := statedb.New(chain.GetContext(), chain.App.(evm.EvmApp).GetEVMKeeper(), statedb.NewEmptyTxConfig()) - _, err = chain.App.(evm.EvmApp).GetEVMKeeper().CallEVMWithData(chain.GetContext(), from, nil, data, true, nil) + _, err = chain.App.(evm.EvmApp).GetEVMKeeper().CallEVMWithData(chain.GetContext(), stateDB, from, nil, data, true, false, nil) if err != nil { return common.Address{}, errorsmod.Wrapf(err, "failed to deploy contract") } diff --git a/evmd/tests/testdata/debug/debug.go b/evmd/tests/testdata/debug/debug.go index 0cc02bfac..f7a476f16 100644 --- a/evmd/tests/testdata/debug/debug.go +++ b/evmd/tests/testdata/debug/debug.go @@ -9,9 +9,12 @@ import ( "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/core/vm" + cmn "github.com/cosmos/evm/precompiles/common" + "github.com/cosmos/evm/x/vm/statedb" + storetypes "cosmossdk.io/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" - cmn "github.com/cosmos/evm/precompiles/common" ) // Precompile defines a debugging precompile for use in testing. @@ -66,7 +69,12 @@ func (p Precompile) Call0(ctx sdk.Context, stateDB vm.StateDB, contract *vm.Cont caller := contract.Caller() fmt.Printf("Execute debug precompile %s, %p\n", caller.String(), p.BalanceHandlerFactory) - rsp, err := p.evmKeeper.CallEVMWithData(ctx, p.Address(), &caller, data, true, nil) + stateDBExp := stateDB.(*statedb.StateDB) + // Note: when called from within a precompile context, we do not set + // commit to true. Doing so will collapse the cache stack and subsequent + // reversions will panic. + rsp, err := p.evmKeeper.CallEVMWithData(ctx, stateDBExp, p.Address(), &caller, data, true, true, nil) + fmt.Println("callback response:", rsp.Ret, err) if err != nil { return nil, err diff --git a/evmd/tests/testdata/debug/interface.go b/evmd/tests/testdata/debug/interface.go index 2fc70e75a..70fb9e56f 100644 --- a/evmd/tests/testdata/debug/interface.go +++ b/evmd/tests/testdata/debug/interface.go @@ -2,6 +2,7 @@ package debug import ( sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/evm/x/vm/statedb" evmtypes "github.com/cosmos/evm/x/vm/types" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" @@ -9,13 +10,15 @@ import ( ) type EVMKeeper interface { - CallEVM(ctx sdk.Context, abi abi.ABI, from, contract common.Address, commit bool, gasCap *big.Int, method string, args ...interface{}) (*evmtypes.MsgEthereumTxResponse, error) + CallEVM(ctx sdk.Context, stateDB *statedb.StateDB, abi abi.ABI, from, contract common.Address, commit bool, callFromPrecompile bool, gasCap *big.Int, method string, args ...interface{}) (*evmtypes.MsgEthereumTxResponse, error) CallEVMWithData( ctx sdk.Context, + stateDB *statedb.StateDB, from common.Address, contract *common.Address, data []byte, commit bool, + callFromPrecompile bool, gasCap *big.Int, ) (*evmtypes.MsgEthereumTxResponse, error) } diff --git a/evmd/upgrades.go b/evmd/upgrades.go index 272428f6b..69f279e9f 100644 --- a/evmd/upgrades.go +++ b/evmd/upgrades.go @@ -3,9 +3,6 @@ package evmd import ( "context" - banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" - "github.com/cosmos/evm/x/vm/types" - sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" @@ -19,7 +16,7 @@ import ( // NOTE: This upgrade defines a reference implementation of what an upgrade // could look like when an application is migrating from EVMD version // v0.4.0 to v0.5.x -const UpgradeName = "v0.4.0-to-v0.5.0" +const UpgradeName = "v0.5.0-to-v0.6.0" func (app EVMD) RegisterUpgradeHandlers() { app.UpgradeKeeper.SetUpgradeHandler( @@ -27,43 +24,6 @@ func (app EVMD) RegisterUpgradeHandlers() { func(ctx context.Context, _ upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) { sdkCtx := sdk.UnwrapSDKContext(ctx) sdkCtx.Logger().Debug("this is a debug level message to test that verbose logging mode has properly been enabled during a chain upgrade") - - app.BankKeeper.SetDenomMetaData(ctx, banktypes.Metadata{ - Description: "Example description", - DenomUnits: []*banktypes.DenomUnit{ - { - Denom: "atest", - Exponent: 0, - Aliases: nil, - }, - { - Denom: "test", - Exponent: 18, - Aliases: nil, - }, - }, - Base: "atest", - Display: "test", - Name: "Test Token", - Symbol: "TEST", - URI: "example_uri", - URIHash: "example_uri_hash", - }) - - // (Required for NON-18 denom chains *only) - // Update EVM params to add Extended denom options - // Ensure that this corresponds to the EVM denom - // (tyically the bond denom) - evmParams := app.EVMKeeper.GetParams(sdkCtx) - evmParams.ExtendedDenomOptions = &types.ExtendedDenomOptions{ExtendedDenom: "atest"} - err := app.EVMKeeper.SetParams(sdkCtx, evmParams) - if err != nil { - return nil, err - } - // Initialize EvmCoinInfo in the module store - if err := app.EVMKeeper.InitEvmCoinInfo(sdkCtx); err != nil { - return nil, err - } return app.ModuleManager.RunMigrations(ctx, app.Configurator(), fromVM) }, ) diff --git a/go.mod b/go.mod index b825157a5..f7596d225 100644 --- a/go.mod +++ b/go.mod @@ -17,14 +17,14 @@ require ( cosmossdk.io/x/upgrade v0.2.0 github.com/btcsuite/btcd v0.24.2 github.com/btcsuite/btcd/btcutil v1.1.6 - github.com/cometbft/cometbft v0.38.19 + github.com/cometbft/cometbft v0.38.21 github.com/cosmos/cosmos-db v1.1.3 github.com/cosmos/cosmos-proto v1.0.0-beta.5 - github.com/cosmos/cosmos-sdk v0.53.5-0.20251030204916-768cb210885c + github.com/cosmos/cosmos-sdk v0.53.6 github.com/cosmos/go-bip39 v1.0.0 github.com/cosmos/gogoproto v1.7.2 github.com/cosmos/ibc-go/v10 v10.3.1-0.20250909102629-ed3b125c7b6f - github.com/cosmos/ledger-cosmos-go v0.16.0 + github.com/cosmos/ledger-cosmos-go v1.0.0 github.com/creachadair/tomledit v0.0.28 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/ethereum/go-ethereum v1.15.11 @@ -43,7 +43,7 @@ require ( github.com/rs/cors v1.11.1 github.com/spf13/cast v1.10.0 github.com/spf13/cobra v1.10.1 - github.com/spf13/viper v1.20.1 + github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 github.com/tidwall/gjson v1.18.0 github.com/tidwall/sjson v1.2.5 @@ -216,11 +216,11 @@ require ( github.com/rivo/uniseg v0.2.0 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rs/zerolog v1.34.0 // indirect - github.com/sagikazarmark/locafero v0.9.0 // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/sasha-s/go-deadlock v0.3.5 // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect - github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.14.0 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect github.com/stretchr/objx v0.5.2 // indirect @@ -254,6 +254,7 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/arch v0.17.0 // indirect golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect golang.org/x/oauth2 v0.30.0 // indirect @@ -284,4 +285,13 @@ replace ( github.com/syndtr/goleveldb => github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 ) -retract v0.4.0 +retract ( + v0.5.1 + v0.5.0 + v0.4.2 + v0.4.1 + v0.4.0 + v0.3.2 + v0.3.1 + v0.3.0 +) diff --git a/go.sum b/go.sum index 4ca463434..2253ef656 100644 --- a/go.sum +++ b/go.sum @@ -834,8 +834,8 @@ github.com/cockroachdb/redact v1.1.6/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZ github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= -github.com/cometbft/cometbft v0.38.19 h1:vNdtCkvhuwUlrcLPAyigV7lQpmmo+tAq8CsB8gZjEYw= -github.com/cometbft/cometbft v0.38.19/go.mod h1:UCu8dlHqvkAsmAFmWDRWNZJPlu6ya2fTWZlDrWsivwo= +github.com/cometbft/cometbft v0.38.21 h1:qcIJSH9LiwU5s6ZgKR5eRbsLNucbubfraDs5bzgjtOI= +github.com/cometbft/cometbft v0.38.21/go.mod h1:UCu8dlHqvkAsmAFmWDRWNZJPlu6ya2fTWZlDrWsivwo= github.com/cometbft/cometbft-db v0.14.1 h1:SxoamPghqICBAIcGpleHbmoPqy+crij/++eZz3DlerQ= github.com/cometbft/cometbft-db v0.14.1/go.mod h1:KHP1YghilyGV/xjD5DP3+2hyigWx0WTp9X+0Gnx0RxQ= github.com/consensys/gnark-crypto v0.18.0 h1:vIye/FqI50VeAr0B3dx+YjeIvmc3LWz4yEfbWBpTUf0= @@ -852,8 +852,8 @@ github.com/cosmos/cosmos-db v1.1.3 h1:7QNT77+vkefostcKkhrzDK9uoIEryzFrU9eoMeaQOP github.com/cosmos/cosmos-db v1.1.3/go.mod h1:kN+wGsnwUJZYn8Sy5Q2O0vCYA99MJllkKASbs6Unb9U= github.com/cosmos/cosmos-proto v1.0.0-beta.5 h1:eNcayDLpip+zVLRLYafhzLvQlSmyab+RC5W7ZfmxJLA= github.com/cosmos/cosmos-proto v1.0.0-beta.5/go.mod h1:hQGLpiIUloJBMdQMMWb/4wRApmI9hjHH05nefC0Ojec= -github.com/cosmos/cosmos-sdk v0.53.5-0.20251030204916-768cb210885c h1:HMVLvm0q3ahGvsyExkSCBcmvcdItMpTxAh4jllL4rJ4= -github.com/cosmos/cosmos-sdk v0.53.5-0.20251030204916-768cb210885c/go.mod h1:nifazrMGFjpmOuaVIZBQ8akQc160imzySYFEA8A7tus= +github.com/cosmos/cosmos-sdk v0.53.6 h1:aJeInld7rbsHtH1qLHu2aZJF9t40mGlqp3ylBLDT0HI= +github.com/cosmos/cosmos-sdk v0.53.6/go.mod h1:N6YuprhAabInbT3YGumGDKONbvPX5dNro7RjHvkQoKE= github.com/cosmos/go-bip39 v1.0.0 h1:pcomnQdrdH22njcAatO0yWojsUnCO3y2tNoV1cb6hHY= github.com/cosmos/go-bip39 v1.0.0/go.mod h1:RNJv0H/pOIVgxw6KS7QeX2a0Uo0aKUlfhZ4xuwvCdJw= github.com/cosmos/go-ethereum v1.16.2-cosmos-1 h1:QIaIS6HIdPSBdTvpFhxswhMLUJgcr4irbd2o9ZKldAI= @@ -871,8 +871,8 @@ github.com/cosmos/ics23/go v0.11.0 h1:jk5skjT0TqX5e5QJbEnwXIS2yI2vnmLOgpQPeM5Rtn github.com/cosmos/ics23/go v0.11.0/go.mod h1:A8OjxPE67hHST4Icw94hOxxFEJMBG031xIGF/JHNIY0= github.com/cosmos/keyring v1.2.0 h1:8C1lBP9xhImmIabyXW4c3vFjjLiBdGCmfLUfeZlV1Yo= github.com/cosmos/keyring v1.2.0/go.mod h1:fc+wB5KTk9wQ9sDx0kFXB3A0MaeGHM9AwRStKOQ5vOA= -github.com/cosmos/ledger-cosmos-go v0.16.0 h1:YKlWPG9NnGZIEUb2bEfZ6zhON1CHlNTg0QKRRGcNEd0= -github.com/cosmos/ledger-cosmos-go v0.16.0/go.mod h1:WrM2xEa8koYoH2DgeIuZXNarF7FGuZl3mrIOnp3Dp0o= +github.com/cosmos/ledger-cosmos-go v1.0.0 h1:jNKW89nPf0vR0EkjHG8Zz16h6p3zqwYEOxlHArwgYtw= +github.com/cosmos/ledger-cosmos-go v1.0.0/go.mod h1:mGaw2wDOf+Z6SfRJsMGxU9DIrBa4du0MAiPlpPhLAOE= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= @@ -1607,8 +1607,8 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k= -github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk= +github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/sasha-s/go-deadlock v0.3.5 h1:tNCOEEDG6tBqrNDOX35j/7hL5FcFViG6awUGROb2NsU= github.com/sasha-s/go-deadlock v0.3.5/go.mod h1:bugP6EGbdGYObIlx7pUZtWqlvo8k9H6vCBBsiChJQ5U= @@ -1626,14 +1626,14 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1 github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= -github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= -github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= -github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= @@ -1644,8 +1644,8 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= -github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= @@ -1795,8 +1795,8 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= -go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE= -go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU= diff --git a/interfaces.go b/interfaces.go index ecebf7e16..98429f004 100644 --- a/interfaces.go +++ b/interfaces.go @@ -6,9 +6,9 @@ import ( erc20keeper "github.com/cosmos/evm/x/erc20/keeper" feemarketkeeper "github.com/cosmos/evm/x/feemarket/keeper" "github.com/cosmos/evm/x/ibc/callbacks/keeper" - transferkeeper "github.com/cosmos/evm/x/ibc/transfer/keeper" precisebankkeeper "github.com/cosmos/evm/x/precisebank/keeper" evmkeeper "github.com/cosmos/evm/x/vm/keeper" + transferkeeper "github.com/cosmos/ibc-go/v10/modules/apps/transfer/keeper" ibctesting "github.com/cosmos/ibc-go/v10/testing" storetypes "cosmossdk.io/store/types" diff --git a/mempool/blockchain_test.go b/mempool/blockchain_test.go index 6e076867c..1aff786f4 100644 --- a/mempool/blockchain_test.go +++ b/mempool/blockchain_test.go @@ -29,7 +29,8 @@ func createMockContext() sdk.Context { return sdk.Context{}. WithBlockTime(time.Now()). WithBlockHeader(cmtproto.Header{AppHash: []byte("00000000000000000000000000000000")}). - WithBlockHeight(1) + WithBlockHeight(1). + WithEventManager(sdk.NewEventManager()) } // TestBlockchainRaceCondition tests concurrent access to NotifyNewBlock and StateAt diff --git a/precompiles/common/balance_handler.go b/precompiles/common/balance_handler.go index 92f182763..54aa248fb 100644 --- a/precompiles/common/balance_handler.go +++ b/precompiles/common/balance_handler.go @@ -54,7 +54,6 @@ func (bh *BalanceHandler) BeforeBalanceChange(ctx sdk.Context) { // 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. @@ -68,7 +67,14 @@ func (bh *BalanceHandler) BeforeBalanceChange(ctx sdk.Context) { func (bh *BalanceHandler) AfterBalanceChange(ctx sdk.Context, stateDB *statedb.StateDB) error { events := ctx.EventManager().Events() - for _, event := range events[bh.prevEventsLen:] { + for i, event := range events[bh.prevEventsLen:] { + eventIdx := bh.prevEventsLen + i + + // Skip events already processed by flushing before the precompile was called. + if stateDB.IsEventProcessed(eventIdx) { + continue + } + switch event.Type { case banktypes.EventTypeCoinSpent: spenderAddr, err := ParseAddress(event, banktypes.AttributeKeySpender) @@ -131,6 +137,7 @@ func (bh *BalanceHandler) AfterBalanceChange(ctx sdk.Context, stateDB *statedb.S } default: + // Non-balance events are already marked as processed above continue } } diff --git a/precompiles/common/interfaces.go b/precompiles/common/interfaces.go index e69e11d39..fb6ebdb1b 100644 --- a/precompiles/common/interfaces.go +++ b/precompiles/common/interfaces.go @@ -6,10 +6,13 @@ import ( ethcommon "github.com/ethereum/go-ethereum/common" erc20types "github.com/cosmos/evm/x/erc20/types" + "github.com/cosmos/evm/x/vm/statedb" ibctypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" connectiontypes "github.com/cosmos/ibc-go/v10/modules/core/03-connection/types" channeltypes "github.com/cosmos/ibc-go/v10/modules/core/04-channel/types" + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types" @@ -62,4 +65,7 @@ type ERC20Keeper interface { GetCoinAddress(ctx sdk.Context, denom string) (ethcommon.Address, error) GetERC20Map(ctx sdk.Context, erc20 ethcommon.Address) []byte GetTokenPair(ctx sdk.Context, id []byte) (erc20types.TokenPair, bool) + IsERC20Enabled(ctx sdk.Context) bool + GetTokenPairID(ctx sdk.Context, token string) []byte + ConvertERC20IntoCoinsForNativeToken(ctx sdk.Context, stateDB *statedb.StateDB, contract ethcommon.Address, amount math.Int, receiver sdk.AccAddress, sender ethcommon.Address, commit bool, callFromPrecompile bool) (*erc20types.MsgConvertERC20Response, error) } diff --git a/precompiles/common/precompile.go b/precompiles/common/precompile.go index 81a0d061f..8d4280d6b 100644 --- a/precompiles/common/precompile.go +++ b/precompiles/common/precompile.go @@ -69,17 +69,16 @@ func (p Precompile) runNativeAction(evm *vm.EVM, contract *vm.Contract, action N // take a snapshot of the current state before any changes // to be able to revert the changes snapshot := stateDB.MultiStoreSnapshot() - events := ctx.EventManager().Events() // add precompileCall entry on the stateDB journal // this allows to revert the changes within an evm tx - if err := stateDB.AddPrecompileFn(snapshot, events); err != nil { + if err := stateDB.AddPrecompileFn(snapshot); err != nil { return nil, err } // commit the current changes in the cache ctx // to get the updated state for the precompile call - if err := stateDB.CommitWithCacheCtx(); err != nil { + if err := stateDB.FlushToCacheCtx(); err != nil { return nil, err } diff --git a/precompiles/ics20/ics20.go b/precompiles/ics20/ics20.go index 59ff4f6f4..bd2702608 100644 --- a/precompiles/ics20/ics20.go +++ b/precompiles/ics20/ics20.go @@ -45,6 +45,7 @@ type Precompile struct { stakingKeeper cmn.StakingKeeper transferKeeper cmn.TransferKeeper channelKeeper cmn.ChannelKeeper + erc20Keeper cmn.ERC20Keeper } // NewPrecompile creates a new ICS-20 Precompile instance as a @@ -54,6 +55,7 @@ func NewPrecompile( stakingKeeper cmn.StakingKeeper, transferKeeper cmn.TransferKeeper, channelKeeper cmn.ChannelKeeper, + erc20Keeper cmn.ERC20Keeper, ) *Precompile { return &Precompile{ Precompile: cmn.Precompile{ @@ -67,6 +69,7 @@ func NewPrecompile( transferKeeper: transferKeeper, channelKeeper: channelKeeper, stakingKeeper: stakingKeeper, + erc20Keeper: erc20Keeper, } } diff --git a/precompiles/ics20/ics20_selector_test.go b/precompiles/ics20/ics20_selector_test.go index 0685671a4..0fa854fe6 100644 --- a/precompiles/ics20/ics20_selector_test.go +++ b/precompiles/ics20/ics20_selector_test.go @@ -29,7 +29,7 @@ func runPrecompile(t *testing.T, input []byte, value *uint256.Int) string { contract := vm.NewContract(common.Address{}, common.Address{}, value, 100_000, nil) contract.Input = input - bz, err := ics20.NewPrecompile(nil, nil, nil, nil).Run(evm, contract, false) + bz, err := ics20.NewPrecompile(nil, nil, nil, nil, nil).Run(evm, contract, false) require.ErrorIs(t, err, vm.ErrExecutionReverted, "precompile errors are always surfaced as a revert") return string(bz) diff --git a/precompiles/ics20/tx.go b/precompiles/ics20/tx.go index 6cea580c0..26007e5bd 100644 --- a/precompiles/ics20/tx.go +++ b/precompiles/ics20/tx.go @@ -2,18 +2,25 @@ package ics20 import ( "fmt" + "strings" "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/vm" + "github.com/hashicorp/go-metrics" cmn "github.com/cosmos/evm/precompiles/common" + erc20types "github.com/cosmos/evm/x/erc20/types" + "github.com/cosmos/evm/x/vm/statedb" transfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" connectiontypes "github.com/cosmos/ibc-go/v10/modules/core/03-connection/types" channeltypes "github.com/cosmos/ibc-go/v10/modules/core/04-channel/types" host "github.com/cosmos/ibc-go/v10/modules/core/24-host" errorsmod "cosmossdk.io/errors" + storetypes "cosmossdk.io/store/types" + "github.com/cosmos/cosmos-sdk/telemetry" sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -87,6 +94,93 @@ func (p *Precompile) validateV1TransferChannel(ctx sdk.Context, msg *transfertyp return nil } +// transferWithStateDB handles IBC transfers with ERC20 token conversion support. +// If user doesn't have enough balance of coin, it will attempt to convert +// ERC20 tokens to the coin denomination, and continue with a regular transfer. +func (p *Precompile) transferWithStateDB(ctx sdk.Context, stateDB *statedb.StateDB, msg *transfertypes.MsgTransfer) (*transfertypes.MsgTransferResponse, error) { + // Temporarily save the KV and transient KV gas config. To avoid extra costs for relayers + // these two gas config are replaced with empty one and should be restored before exiting this function. + kvGasCfg := ctx.KVGasConfig() + transientKVGasCfg := ctx.TransientKVGasConfig() + ctx = ctx. + WithKVGasConfig(storetypes.GasConfig{}). + WithTransientKVGasConfig(storetypes.GasConfig{}) + + defer func() { + // Return the KV gas config to initial values + ctx = ctx. + WithKVGasConfig(kvGasCfg). + WithTransientKVGasConfig(transientKVGasCfg) + }() + + // use native denom or contract address + denom := strings.TrimPrefix(msg.Token.Denom, erc20types.Erc20NativeCoinDenomPrefix) + + pairID := p.erc20Keeper.GetTokenPairID(ctx, denom) + if len(pairID) == 0 { + // no-op: token is not registered so we can proceed with regular transfer + return p.transferKeeper.Transfer(ctx, msg) + } + + pair, _ := p.erc20Keeper.GetTokenPair(ctx, pairID) + if !pair.Enabled { + // no-op: pair is not enabled so we can proceed with regular transfer + return p.transferKeeper.Transfer(ctx, msg) + } + + sender := sdk.MustAccAddressFromBech32(msg.Sender) + + if !p.erc20Keeper.IsERC20Enabled(ctx) { + // no-op: continue with regular transfer + return p.transferKeeper.Transfer(ctx, msg) + } + + // update the msg denom to the token pair denom + msg.Token.Denom = pair.Denom + + if !pair.IsNativeERC20() { + return p.transferKeeper.Transfer(ctx, msg) + } + // if the user has enough balance of the Cosmos representation, then we don't need to Convert + balance := p.bankKeeper.SpendableCoin(ctx, sender, pair.Denom) + if balance.Amount.GTE(msg.Token.Amount) { + + defer func() { + telemetry.IncrCounterWithLabels( + []string{"erc20", "ibc", "transfer", "total"}, + 1, + []metrics.Label{ + telemetry.NewLabel("denom", pair.Denom), + }, + ) + }() + + return p.transferKeeper.Transfer(ctx, msg) + } + + // Only convert if the pair is a native ERC20 + // only convert the remaining difference + difference := msg.Token.Amount.Sub(balance.Amount) + + // Convert the ERC20 tokens to Cosmos IBC Coin + erc20Sender := common.BytesToAddress(sender.Bytes()) + if _, err := p.erc20Keeper.ConvertERC20IntoCoinsForNativeToken(ctx, stateDB, pair.GetERC20Contract(), difference, sender, erc20Sender, true, true); err != nil { + return nil, err + } + + defer func() { + telemetry.IncrCounterWithLabels( + []string{"erc20", "ibc", "transfer", "total"}, + 1, + []metrics.Label{ + telemetry.NewLabel("denom", pair.Denom), + }, + ) + }() + + return p.transferKeeper.Transfer(ctx, msg) +} + // Transfer implements the ICS20 transfer transactions. func (p *Precompile) Transfer( ctx sdk.Context, @@ -119,7 +213,8 @@ func (p *Precompile) Transfer( return nil, fmt.Errorf(cmn.ErrRequesterIsNotMsgSender, msgSender.String(), sender.String()) } - res, err := p.transferKeeper.Transfer(ctx, msg) + stateDBExp := stateDB.(*statedb.StateDB) + res, err := p.transferWithStateDB(ctx, stateDBExp, msg) if err != nil { return nil, err } diff --git a/precompiles/types/defaults.go b/precompiles/types/defaults.go index 65b855e37..45581a06a 100644 --- a/precompiles/types/defaults.go +++ b/precompiles/types/defaults.go @@ -7,7 +7,7 @@ import ( evmaddress "github.com/cosmos/evm/encoding/address" cmn "github.com/cosmos/evm/precompiles/common" erc20Keeper "github.com/cosmos/evm/x/erc20/keeper" - transferkeeper "github.com/cosmos/evm/x/ibc/transfer/keeper" + transferkeeper "github.com/cosmos/ibc-go/v10/modules/apps/transfer/keeper" channelkeeper "github.com/cosmos/ibc-go/v10/modules/core/04-channel/keeper" "cosmossdk.io/core/address" @@ -80,7 +80,7 @@ func DefaultStaticPrecompiles( WithBech32Precompile(). WithStakingPrecompile(stakingKeeper, bankKeeper, opts...). WithDistributionPrecompile(distributionKeeper, stakingKeeper, bankKeeper, opts...). - WithICS20Precompile(bankKeeper, stakingKeeper, transferKeeper, channelKeeper). + WithICS20Precompile(bankKeeper, stakingKeeper, transferKeeper, channelKeeper, erc20Keeper). WithBankPrecompile(bankKeeper, erc20Keeper). WithGovPrecompile(govKeeper, bankKeeper, codec, opts...). WithSlashingPrecompile(slashingKeeper, bankKeeper, opts...) diff --git a/precompiles/types/static_precompiles.go b/precompiles/types/static_precompiles.go index ca49445f9..90fa95a2e 100644 --- a/precompiles/types/static_precompiles.go +++ b/precompiles/types/static_precompiles.go @@ -17,7 +17,7 @@ import ( slashingprecompile "github.com/cosmos/evm/precompiles/slashing" stakingprecompile "github.com/cosmos/evm/precompiles/staking" erc20Keeper "github.com/cosmos/evm/x/erc20/keeper" - transferkeeper "github.com/cosmos/evm/x/ibc/transfer/keeper" + transferkeeper "github.com/cosmos/ibc-go/v10/modules/apps/transfer/keeper" channelkeeper "github.com/cosmos/ibc-go/v10/modules/core/04-channel/keeper" "github.com/cosmos/cosmos-sdk/codec" @@ -104,12 +104,14 @@ func (s StaticPrecompiles) WithICS20Precompile( stakingKeeper stakingkeeper.Keeper, transferKeeper *transferkeeper.Keeper, channelKeeper *channelkeeper.Keeper, + erc20Keeper *erc20Keeper.Keeper, ) StaticPrecompiles { ibcTransferPrecompile := ics20precompile.NewPrecompile( bankKeeper, stakingKeeper, transferKeeper, channelKeeper, + erc20Keeper, ) s[ibcTransferPrecompile.Address()] = ibcTransferPrecompile diff --git a/rpc/backend/comet_to_eth.go b/rpc/backend/comet_to_eth.go index 750dc99bb..3c2bc1fd9 100644 --- a/rpc/backend/comet_to_eth.go +++ b/rpc/backend/comet_to_eth.go @@ -424,9 +424,16 @@ func (b *Backend) ReceiptsFromCometBlock( cumulatedGasUsed += txResult.GasUsed var effectiveGasPrice *big.Int - if baseFee != nil { + switch { + case additional != nil: + // Derived tx: reconstructed with zero fee caps, so the generic EIP-1559 + // formula would report 0 here and make fee-accounting consumers read the + // block as burning more than it collected. Report the base fee, matching + // the `gasPrice` served for the same tx by eth_getTransactionByHash. + effectiveGasPrice = rpctypes.DerivedTxGasPrice(ethMsg.Raw.Transaction, baseFee) + case baseFee != nil: effectiveGasPrice = rpctypes.EffectiveGasPrice(ethMsg.Raw.Transaction, baseFee) - } else { + default: effectiveGasPrice = ethMsg.Raw.GasFeeCap() } diff --git a/rpc/backend/derived_gas_price_test.go b/rpc/backend/derived_gas_price_test.go new file mode 100644 index 000000000..3607f12c8 --- /dev/null +++ b/rpc/backend/derived_gas_price_test.go @@ -0,0 +1,108 @@ +package backend + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + abcitypes "github.com/cometbft/cometbft/abci/types" + tmrpctypes "github.com/cometbft/cometbft/rpc/core/types" + tmtypes "github.com/cometbft/cometbft/types" + + "github.com/cosmos/evm/rpc/backend/mocks" + rpctypes "github.com/cosmos/evm/rpc/types" + servertypes "github.com/cosmos/evm/server/types" + evmtypes "github.com/cosmos/evm/x/vm/types" + + sdkmath "cosmossdk.io/math" +) + +// TestDerivedTxReceiptEffectiveGasPrice is a regression test for the negative block +// rewards reported by Blockscout for blocks whose only content is derived txs. +// +// Derived txs are reconstructed from events with zero fee caps, so the generic EIP-1559 +// effective-price formula resolves them to 0. A receipt reporting gasUsed > 0 at +// effectiveGasPrice == 0 makes any consumer that models burn as base_fee * gas_used read +// the block as burning more than it collected. The receipt must report the base fee, and +// must agree with the `gasPrice` served by eth_getTransactionByHash for the same tx. +func TestDerivedTxReceiptEffectiveGasPrice(t *testing.T) { + const ( + height = int64(100) + gasUsed = uint64(98_212) + gasLimit = uint64(50_000_000) + ) + baseFee := big.NewInt(1_000_000_000) // 1 gwei, as on donut + + backend := setupMockBackend(t) + mockEVMQueryClient := backend.QueryClient.QueryClient.(*mocks.EVMQueryClient) + mockEVMQueryClient.On("BaseFee", mock.Anything, mock.Anything). + Return(&evmtypes.QueryBaseFeeResponse{BaseFee: ptrInt(sdkmath.NewIntFromBigInt(baseFee))}, nil) + + limit := gasLimit + additional := &rpctypes.TxResultAdditionalFields{ + Hash: common.BigToHash(big.NewInt(0xdeadbeef)), + Type: evmtypes.DerivedTxType, + Recipient: common.HexToAddress("0x7e5ac993907bc433046316948fa23b0c9c702664"), + Sender: common.HexToAddress("0x5826874ddef35d5f802634e212fbab949cb34f6a"), + Value: big.NewInt(0), + GasUsed: gasUsed, + GasLimit: &limit, + Nonce: 1, + } + ethMsg := backend.parseDerivedTxFromAdditionalFields(additional) + require.NotNil(t, ethMsg) + + backend.Indexer = &MockIndexer{ + txResults: map[common.Hash]*servertypes.TxResult{ + additional.Hash: { + Height: height, + TxIndex: 0, + EthTxIndex: 0, + MsgIndex: 0, + GasUsed: gasUsed, + }, + }, + } + + resBlock := &tmrpctypes.ResultBlock{ + BlockID: tmtypes.BlockID{Hash: common.BigToHash(big.NewInt(0xb10c)).Bytes()}, + Block: &tmtypes.Block{Header: tmtypes.Header{Height: height}}, + } + blockRes := &tmrpctypes.ResultBlockResults{ + Height: height, + TxsResults: []*abcitypes.ExecTxResult{{Code: 0}}, + } + + receipts, err := backend.ReceiptsFromCometBlock( + resBlock, + blockRes, + []*evmtypes.MsgEthereumTx{ethMsg}, + []*rpctypes.TxResultAdditionalFields{additional}, + ) + require.NoError(t, err) + require.Len(t, receipts, 1) + + require.Equal(t, baseFee, receipts[0].EffectiveGasPrice, + "derived tx receipt must report the base fee, not 0") + require.Equal(t, gasUsed, receipts[0].GasUsed) + + // The tx object served for the same derived tx must agree, so that consumers reading + // either field compute the same (zero) net fee. + rpcTx, err := rpctypes.NewRPCTransactionFromIncompleteMsg( + ethMsg, + common.BytesToHash(resBlock.BlockID.Hash), + uint64(height), + 0, + baseFee, + backend.EvmChainID, + additional.Hash, + ) + require.NoError(t, err) + require.Equal(t, receipts[0].EffectiveGasPrice, rpcTx.GasPrice.ToInt(), + "eth_getTransactionByHash gasPrice must match the receipt effectiveGasPrice") +} + +func ptrInt(i sdkmath.Int) *sdkmath.Int { return &i } diff --git a/rpc/types/types_test.go b/rpc/types/types_test.go index 33360236e..c2aca1692 100644 --- a/rpc/types/types_test.go +++ b/rpc/types/types_test.go @@ -27,7 +27,7 @@ func (p *precompileContract) Run(evm *vm.EVM, contract *vm.Contract, readonly bo func TestApply(t *testing.T) { emptyTxConfig := statedb.NewEmptyTxConfig() - db := statedb.New(sdk.Context{}, mocks.NewEVMKeeper(), emptyTxConfig) + db := statedb.New(sdk.Context{}.WithEventManager(sdk.NewEventManager()), mocks.NewEVMKeeper(), emptyTxConfig) precompiles := map[common.Address]vm.PrecompiledContract{ common.BytesToAddress([]byte{0x1}): &precompileContract{}, common.BytesToAddress([]byte{0x2}): &precompileContract{}, diff --git a/rpc/types/utils.go b/rpc/types/utils.go index 9a27cb5b4..a81db719f 100644 --- a/rpc/types/utils.go +++ b/rpc/types/utils.go @@ -304,28 +304,63 @@ func NewRPCTransactionFromIncompleteMsg( from := msg.GetSender() v, r, s := tx.RawSignatureValues() result := &RPCTransaction{ - Type: hexutil.Uint64(tx.Type()), - From: from, - Gas: hexutil.Uint64(tx.Gas()), - GasPrice: (*hexutil.Big)(tx.GasPrice()), - Hash: txHash, - Input: hexutil.Bytes(tx.Data()), - Nonce: hexutil.Uint64(tx.Nonce()), - To: tx.To(), - Value: (*hexutil.Big)(tx.Value()), - V: (*hexutil.Big)(v), - R: (*hexutil.Big)(r), - S: (*hexutil.Big)(s), - ChainID: (*hexutil.Big)(chainID), + Type: hexutil.Uint64(tx.Type()), + From: from, + Gas: hexutil.Uint64(tx.Gas()), + Hash: txHash, + Input: hexutil.Bytes(tx.Data()), + Nonce: hexutil.Uint64(tx.Nonce()), + To: tx.To(), + Value: (*hexutil.Big)(tx.Value()), + V: (*hexutil.Big)(v), + R: (*hexutil.Big)(r), + S: (*hexutil.Big)(s), + ChainID: (*hexutil.Big)(chainID), } if blockHash != (common.Hash{}) { result.BlockHash = &blockHash result.BlockNumber = (*hexutil.Big)(new(big.Int).SetUint64(blockNumber)) result.TransactionIndex = (*hexutil.Uint64)(&index) + result.GasPrice = (*hexutil.Big)(DerivedTxGasPrice(tx, baseFee)) + } else { + // Not mined: there is no block base fee to report against, so fall back to + // the reconstructed tx's own price. + result.GasPrice = (*hexutil.Big)(tx.GasPrice()) } return result, nil } +// DerivedTxGasPrice returns the gas price to report over JSON-RPC for a derived +// (protocol-internal, non-user-signed) EVM transaction mined in a block with the +// given base fee. +// +// Derived txs are constructed with zero fee caps — they are not signed by a user and +// no fee is charged for them — so the generic EIP-1559 formulas resolve their price to +// 0. Reporting 0 breaks consumers that model every transaction as burning +// base_fee * gas_used. Blockscout computes a block's reward as +// +// Σ(gas_used * gas_price) − base_fee_per_gas * Σ(gas_used) +// +// which goes negative for blocks whose only content is derived txs, even though such a +// block moves no value at all: nothing is paid to the proposer and nothing is burnt. +// +// Reporting exactly the base fee — the effective price of a transaction that adds no +// priority tip — makes that arithmetic net to zero, which matches the chain's actual +// economics. Both the transaction object's `gasPrice` and the receipt's +// `effectiveGasPrice` must use this so the two agree. +// +// When the base fee is unavailable (pruned node), the reconstructed tx's own price is +// returned rather than inventing one. +func DerivedTxGasPrice(tx *ethtypes.Transaction, baseFee *big.Int) *big.Int { + if baseFee == nil { + if tx == nil { + return big.NewInt(0) + } + return tx.GasPrice() + } + return new(big.Int).Set(baseFee) +} + // effectiveGasPrice computes the transaction gas fee, based on the given basefee value. // // price = min(gasTipCap + baseFee, gasFeeCap) diff --git a/rpc/types/utils_test.go b/rpc/types/utils_test.go index badeddd71..2e5430de5 100644 --- a/rpc/types/utils_test.go +++ b/rpc/types/utils_test.go @@ -47,3 +47,74 @@ func TestNewRPCTransactionFromIncompleteMsgGas(t *testing.T) { require.Equal(t, txHash, rpcTx.Hash, "hash must be the supplied derived tx hash") require.Equal(t, sender, rpcTx.From) } + +// TestNewRPCTransactionFromIncompleteMsgGasPrice is a regression test for the negative +// block rewards reported by Blockscout for blocks whose only content is derived txs. +// +// Derived txs are reconstructed with zero fee caps, so reporting their raw price yields +// gasPrice == 0 while the receipt still reports gasUsed > 0. Blockscout derives a block's +// reward as Σ(gas_used * gas_price) − base_fee_per_gas * Σ(gas_used), which goes negative +// for such blocks even though they move no value. A mined derived tx must therefore report +// exactly the block base fee — the price of a tx that adds no priority tip — so that +// arithmetic nets to zero. +func TestNewRPCTransactionFromIncompleteMsgGasPrice(t *testing.T) { + to := common.HexToAddress("0x000000000000000000000000000000000000dEaD") + txHash := common.BigToHash(big.NewInt(1)) + blockHash := common.BigToHash(big.NewInt(2)) + baseFee := big.NewInt(1_000_000_000) // 1 gwei + + // Derived txs are reconstructed as EIP-1559 txs with zero fee caps. + newMsg := func() *evmtypes.MsgEthereumTx { + inner := ethtypes.NewTx(ðtypes.DynamicFeeTx{ + ChainID: big.NewInt(1), + Nonce: 0, + GasFeeCap: big.NewInt(0), + GasTipCap: big.NewInt(0), + Gas: 60000, + To: &to, + Value: big.NewInt(0), + }) + msg := &evmtypes.MsgEthereumTx{} + msg.FromEthereumTx(inner) + msg.From = common.BytesToAddress([]byte("sender")).Bytes() + return msg + } + + t.Run("mined tx reports the block base fee", func(t *testing.T) { + rpcTx, err := NewRPCTransactionFromIncompleteMsg( + newMsg(), blockHash, 7, 0, baseFee, big.NewInt(1), txHash, + ) + require.NoError(t, err) + require.NotNil(t, rpcTx.GasPrice) + require.Equal(t, baseFee, rpcTx.GasPrice.ToInt(), + "gasPrice must be the base fee so tx fees and burnt fees cancel out") + }) + + t.Run("mined tx with unknown base fee falls back to the tx price", func(t *testing.T) { + rpcTx, err := NewRPCTransactionFromIncompleteMsg( + newMsg(), blockHash, 7, 0, nil, big.NewInt(1), txHash, + ) + require.NoError(t, err) + require.NotNil(t, rpcTx.GasPrice) + require.Equal(t, big.NewInt(0), rpcTx.GasPrice.ToInt()) + }) + + t.Run("unmined tx falls back to the tx price", func(t *testing.T) { + rpcTx, err := NewRPCTransactionFromIncompleteMsg( + newMsg(), common.Hash{}, 0, 0, baseFee, big.NewInt(1), txHash, + ) + require.NoError(t, err) + require.NotNil(t, rpcTx.GasPrice) + require.Equal(t, big.NewInt(0), rpcTx.GasPrice.ToInt()) + }) + + t.Run("supplied base fee is not aliased", func(t *testing.T) { + bf := big.NewInt(1_000_000_000) + rpcTx, err := NewRPCTransactionFromIncompleteMsg( + newMsg(), blockHash, 7, 0, bf, big.NewInt(1), txHash, + ) + require.NoError(t, err) + rpcTx.GasPrice.ToInt().SetInt64(42) + require.Equal(t, big.NewInt(1_000_000_000), bf, "caller's baseFee must not be mutated") + }) +} diff --git a/tests/integration/precompiles/erc20/test_query.go b/tests/integration/precompiles/erc20/test_query.go index f462bc2c1..580a7f7c4 100644 --- a/tests/integration/precompiles/erc20/test_query.go +++ b/tests/integration/precompiles/erc20/test_query.go @@ -9,7 +9,7 @@ import ( "github.com/cosmos/evm/precompiles/erc20" "github.com/cosmos/evm/testutil" - transferkeeper "github.com/cosmos/evm/x/ibc/transfer/keeper" + transferkeeper "github.com/cosmos/ibc-go/v10/modules/apps/transfer/keeper" "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" sdkmath "cosmossdk.io/math" diff --git a/tests/integration/precompiles/ics20/test_setup.go b/tests/integration/precompiles/ics20/test_setup.go index cd90158c5..3476668db 100644 --- a/tests/integration/precompiles/ics20/test_setup.go +++ b/tests/integration/precompiles/ics20/test_setup.go @@ -48,6 +48,7 @@ func (s *PrecompileTestSuite) SetupTest() { *evmAppA.GetStakingKeeper(), evmAppA.GetTransferKeeper(), evmAppA.GetIBCKeeper().ChannelKeeper, + evmAppA.GetErc20Keeper(), ) s.chainABondDenom, _ = evmAppA.GetStakingKeeper().BondDenom(s.chainA.GetContext()) evmAppB := s.chainB.App.(evm.EvmApp) @@ -56,6 +57,7 @@ func (s *PrecompileTestSuite) SetupTest() { *evmAppB.GetStakingKeeper(), evmAppB.GetTransferKeeper(), evmAppB.GetIBCKeeper().ChannelKeeper, + evmAppB.GetErc20Keeper(), ) s.chainBBondDenom, _ = evmAppB.GetStakingKeeper().BondDenom(s.chainB.GetContext()) } diff --git a/tests/integration/x/erc20/test_convert.go b/tests/integration/x/erc20/test_convert.go new file mode 100644 index 000000000..c510c4cbe --- /dev/null +++ b/tests/integration/x/erc20/test_convert.go @@ -0,0 +1,390 @@ +package erc20 + +import ( + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/holiman/uint256" + "github.com/stretchr/testify/mock" + "go.uber.org/mock/gomock" + + "github.com/cosmos/evm/testutil/integration/evm/utils" + "github.com/cosmos/evm/x/erc20/keeper" + "github.com/cosmos/evm/x/erc20/types" + erc20mocks "github.com/cosmos/evm/x/erc20/types/mocks" + "github.com/cosmos/evm/x/vm/statedb" + evmtypes "github.com/cosmos/evm/x/vm/types" + + "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" +) + +// TestConvertERC20IntoCoinsForNativeToken tests the core conversion logic +func (s *KeeperTestSuite) TestConvertERC20IntoCoinsForNativeToken() { + var ( + contractAddr common.Address + coinName string + ) + testCases := []struct { + name string + mint int64 + transfer int64 + malleate func(common.Address) + extra func() + contractType int + expPass bool + selfdestructed bool + }{ + { + "ok - sufficient funds", + 100, + 10, + func(common.Address) {}, + func() {}, + contractMinterBurner, + true, + false, + }, + { + "ok - equal funds", + 10, + 10, + func(common.Address) {}, + func() {}, + contractMinterBurner, + true, + false, + }, + { + "fail - insufficient funds - callEVM", + 0, + 10, + func(common.Address) {}, + func() {}, + contractMinterBurner, + false, + false, + }, + { + "fail - minting disabled", + 100, + 10, + func(contractAddr common.Address) { + params := types.DefaultParams() + params.EnableErc20 = false + err := utils.UpdateERC20Params( + utils.UpdateParamsInput{ + Tf: s.factory, + Network: s.network, + Pk: s.keyring.GetPrivKey(0), + Params: params, + }, + ) + s.Require().NoError(err) + }, + func() {}, + contractMinterBurner, + false, + false, + }, + { + "fail - direct balance manipulation contract", + 100, + 10, + func(common.Address) {}, + func() {}, + contractDirectBalanceManipulation, + false, + false, + }, + { + // The contract grants a third party an allowance over the escrow + // module on every `transfer`, which would leave the minted coins + // unbacked once that allowance is spent. The unexpected `Approval` + // event in the transfer logs must abort the conversion + // (F-2026-18822). Upstream v0.6.0 still expects this to pass because + // it moved this body into x/erc20/keeper/convert.go without the + // validateApprovalEventDoesNotExist guard. + "fail - delayed malicious contract", + 10, + 10, + func(common.Address) {}, + func() {}, + contractMaliciousDelayed, + false, + false, + }, + { + "fail - negative transfer amount", + 10, + -10, + func(common.Address) {}, + func() {}, + contractMinterBurner, + false, + false, + }, + { + "fail - force evm fail", + 100, + 10, + func(common.Address) {}, + func() { + mockEVMKeeper := &erc20mocks.EVMKeeper{} + transferKeeper := s.network.App.GetTransferKeeper() + erc20Keeper := keeper.NewKeeper( + s.network.App.GetKey("erc20"), s.network.App.AppCodec(), + authtypes.NewModuleAddress(govtypes.ModuleName), s.network.App.GetAccountKeeper(), + s.network.App.GetBankKeeper(), mockEVMKeeper, s.network.App.GetStakingKeeper(), + &transferKeeper, + ) + s.network.App.SetErc20Keeper(erc20Keeper) + + existingAcc := &statedb.Account{Nonce: uint64(1), Balance: uint256.NewInt(1)} + balance := make([]uint8, 32) + mockEVMKeeper.On("EstimateGasInternal", mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.EstimateGasResponse{Gas: uint64(200)}, nil) + mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, + mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Once() + mockEVMKeeper.On("CallEVMWithData", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, + mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("forced ApplyMessage error")) + mockEVMKeeper.On("GetAccountWithoutBalance", mock.Anything, mock.Anything).Return(existingAcc, nil) + mockEVMKeeper.On("IsContract", mock.Anything, mock.Anything).Return(true) + }, + contractMinterBurner, + false, + false, + }, + { + "fail - force get balance fail", + 100, + 10, + func(common.Address) {}, + func() { + mockEVMKeeper := &erc20mocks.EVMKeeper{} + transferKeeper := s.network.App.GetTransferKeeper() + erc20Keeper := keeper.NewKeeper( + s.network.App.GetKey("erc20"), s.network.App.AppCodec(), + authtypes.NewModuleAddress(govtypes.ModuleName), s.network.App.GetAccountKeeper(), + s.network.App.GetBankKeeper(), mockEVMKeeper, s.network.App.GetStakingKeeper(), + &transferKeeper, + ) + s.network.App.SetErc20Keeper(erc20Keeper) + + existingAcc := &statedb.Account{Nonce: uint64(1), Balance: uint256.NewInt(1)} + balance := make([]uint8, 32) + balance[31] = uint8(1) + mockEVMKeeper.On("EstimateGasInternal", mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.EstimateGasResponse{Gas: uint64(200)}, nil) + mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Twice() + mockEVMKeeper.On("CallEVMWithData", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("forced balance error")) + mockEVMKeeper.On("GetAccountWithoutBalance", mock.Anything, mock.Anything).Return(existingAcc, nil) + mockEVMKeeper.On("IsContract", mock.Anything, mock.Anything).Return(true) + }, + contractMinterBurner, + false, + false, + }, + { + "fail - force transfer unpack fail", + 100, + 10, + func(common.Address) {}, + func() { + mockEVMKeeper := &erc20mocks.EVMKeeper{} + transferKeeper := s.network.App.GetTransferKeeper() + erc20Keeper := keeper.NewKeeper( + s.network.App.GetKey("erc20"), s.network.App.AppCodec(), + authtypes.NewModuleAddress(govtypes.ModuleName), s.network.App.GetAccountKeeper(), + s.network.App.GetBankKeeper(), mockEVMKeeper, s.network.App.GetStakingKeeper(), + &transferKeeper, + ) + s.network.App.SetErc20Keeper(erc20Keeper) + + existingAcc := &statedb.Account{Nonce: uint64(1), Balance: uint256.NewInt(1)} + balance := make([]uint8, 32) + mockEVMKeeper.On("EstimateGasInternal", mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.EstimateGasResponse{Gas: uint64(200)}, nil) + mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, + mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Once() + mockEVMKeeper.On("CallEVMWithData", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{}, nil) + mockEVMKeeper.On("GetAccountWithoutBalance", mock.Anything, mock.Anything).Return(existingAcc, nil) + mockEVMKeeper.On("IsContract", mock.Anything, mock.Anything).Return(true) + }, + contractMinterBurner, + false, + false, + }, + { + "fail - force invalid transfer fail", + 100, + 10, + func(common.Address) {}, + func() { + mockEVMKeeper := &erc20mocks.EVMKeeper{} + transferKeeper := s.network.App.GetTransferKeeper() + erc20Keeper := keeper.NewKeeper( + s.network.App.GetKey("erc20"), s.network.App.AppCodec(), + authtypes.NewModuleAddress(govtypes.ModuleName), s.network.App.GetAccountKeeper(), + s.network.App.GetBankKeeper(), mockEVMKeeper, s.network.App.GetStakingKeeper(), + &transferKeeper, + ) + s.network.App.SetErc20Keeper(erc20Keeper) + + existingAcc := &statedb.Account{Nonce: uint64(1), Balance: uint256.NewInt(1)} + balance := make([]uint8, 32) + mockEVMKeeper.On("EstimateGasInternal", mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.EstimateGasResponse{Gas: uint64(200)}, nil) + mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, + mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Once() + mockEVMKeeper.On("CallEVMWithData", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, + mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil) + mockEVMKeeper.On("GetAccountWithoutBalance", mock.Anything, mock.Anything).Return(existingAcc, nil) + mockEVMKeeper.On("IsContract", mock.Anything, mock.Anything).Return(true) + }, + contractMinterBurner, + false, + false, + }, + { + "fail - force mint fail", + 100, + 10, + func(common.Address) {}, + func() { + ctrl := gomock.NewController(s.T()) + mockBankKeeper := erc20mocks.NewMockBankKeeper(ctrl) + transferKeeper := s.network.App.GetTransferKeeper() + erc20Keeper := keeper.NewKeeper( + s.network.App.GetKey("erc20"), s.network.App.AppCodec(), + authtypes.NewModuleAddress(govtypes.ModuleName), s.network.App.GetAccountKeeper(), + mockBankKeeper, s.network.App.GetEVMKeeper(), s.network.App.GetStakingKeeper(), + &transferKeeper, + ) + s.network.App.SetErc20Keeper(erc20Keeper) + + mockBankKeeper.EXPECT().MintCoins(gomock.Any(), gomock.Any(), gomock.Any()).Return(fmt.Errorf("failed to mint")).AnyTimes() + mockBankKeeper.EXPECT().SendCoinsFromModuleToAccount(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(fmt.Errorf("failed to unescrow")).AnyTimes() + mockBankKeeper.EXPECT().BlockedAddr(gomock.Any()).Return(false).AnyTimes() + mockBankKeeper.EXPECT().GetBalance(gomock.Any(), gomock.Any(), gomock.Any()).Return(sdk.Coin{Denom: "coin", Amount: math.OneInt()}).AnyTimes() + mockBankKeeper.EXPECT().IsSendEnabledCoin(gomock.Any(), gomock.Any()).Return(true).AnyTimes() + }, + contractMinterBurner, + false, + false, + }, + { + "fail - force send minted fail", + 100, + 10, + func(common.Address) {}, + func() { + ctrl := gomock.NewController(s.T()) + mockBankKeeper := erc20mocks.NewMockBankKeeper(ctrl) + transferKeeper := s.network.App.GetTransferKeeper() + erc20Keeper := keeper.NewKeeper( + s.network.App.GetKey("erc20"), s.network.App.AppCodec(), + authtypes.NewModuleAddress(govtypes.ModuleName), s.network.App.GetAccountKeeper(), + mockBankKeeper, s.network.App.GetEVMKeeper(), s.network.App.GetStakingKeeper(), + &transferKeeper, + ) + s.network.App.SetErc20Keeper(erc20Keeper) + + mockBankKeeper.EXPECT().MintCoins(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + mockBankKeeper.EXPECT().SendCoinsFromModuleToAccount(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(fmt.Errorf("failed to unescrow")) + mockBankKeeper.EXPECT().BlockedAddr(gomock.Any()).Return(false) + mockBankKeeper.EXPECT().GetBalance(gomock.Any(), gomock.Any(), gomock.Any()).Return(sdk.Coin{Denom: "coin", Amount: math.OneInt()}) + mockBankKeeper.EXPECT().IsSendEnabledCoin(gomock.Any(), gomock.Any()).Return(true).AnyTimes() + }, + contractMinterBurner, + false, + false, + }, + { + "fail - force bank balance fail", + 100, + 10, + func(common.Address) {}, + func() { + ctrl := gomock.NewController(s.T()) + mockBankKeeper := erc20mocks.NewMockBankKeeper(ctrl) + transferKeeper := s.network.App.GetTransferKeeper() + erc20Keeper := keeper.NewKeeper( + s.network.App.GetKey("erc20"), s.network.App.AppCodec(), + authtypes.NewModuleAddress(govtypes.ModuleName), s.network.App.GetAccountKeeper(), + mockBankKeeper, s.network.App.GetEVMKeeper(), s.network.App.GetStakingKeeper(), + &transferKeeper, + ) + s.network.App.SetErc20Keeper(erc20Keeper) + + mockBankKeeper.EXPECT().MintCoins(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + mockBankKeeper.EXPECT().SendCoinsFromModuleToAccount(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + mockBankKeeper.EXPECT().BlockedAddr(gomock.Any()).Return(false) + mockBankKeeper.EXPECT().GetBalance(gomock.Any(), gomock.Any(), gomock.Any()).Return(sdk.Coin{Denom: coinName, Amount: math.OneInt()}).AnyTimes() + mockBankKeeper.EXPECT().IsSendEnabledCoin(gomock.Any(), gomock.Any()).Return(true).AnyTimes() + }, + contractMinterBurner, + false, + false, + }, + } + for _, tc := range testCases { + s.Run(fmt.Sprintf("Case %s", tc.name), func() { + var err error + s.mintFeeCollector = true + defer func() { + s.mintFeeCollector = false + }() + + s.SetupTest() + + contractAddr, err = s.setupRegisterERC20Pair(tc.contractType) + s.Require().NoError(err) + + tc.malleate(contractAddr) + s.Require().NotNil(contractAddr) + + coinName = types.CreateDenom(contractAddr.String()) + sender := s.keyring.GetAccAddr(0) + senderHex := s.keyring.GetAddr(0) + + _, err = s.MintERC20Token(contractAddr, senderHex, big.NewInt(tc.mint)) + s.Require().NoError(err) + + tc.extra() + + // Get context AFTER state modifications so changes are visible + ctx := s.network.GetContext() + stateDB := statedb.New(ctx, s.network.App.GetEVMKeeper(), statedb.NewEmptyTxConfig()) + + if tc.expPass { + // Call the conversion function directly + _, err = s.network.App.GetErc20Keeper().ConvertERC20IntoCoinsForNativeToken(ctx, stateDB, contractAddr, math.NewInt(tc.transfer), sender, senderHex, true, false) + s.Require().NoError(err, tc.name) + + cosmosBalance := s.network.App.GetBankKeeper().GetBalance(ctx, sender, coinName) + + acc := s.network.App.GetEVMKeeper().GetAccountWithoutBalance(ctx, contractAddr) + if tc.selfdestructed { + s.Require().Nil(acc, "expected contract to be destroyed") + } else { + s.Require().NotNil(acc) + } + + isContract := s.network.App.GetEVMKeeper().IsContract(ctx, contractAddr) + if tc.selfdestructed || !isContract { + id := s.network.App.GetErc20Keeper().GetTokenPairID(ctx, contractAddr.String()) + _, found := s.network.App.GetErc20Keeper().GetTokenPair(ctx, id) + s.Require().False(found) + } else { + s.Require().Equal(cosmosBalance.Amount, math.NewInt(tc.transfer)) + } + } else { + // Call the conversion function directly + _, err = s.network.App.GetErc20Keeper().ConvertERC20IntoCoinsForNativeToken(ctx, stateDB, contractAddr, math.NewInt(tc.transfer), sender, senderHex, true, false) + s.Require().Error(err, tc.name) + } + }) + } + s.mintFeeCollector = false +} diff --git a/tests/integration/x/erc20/test_evm.go b/tests/integration/x/erc20/test_evm.go index 4e1977414..832d14244 100644 --- a/tests/integration/x/erc20/test_evm.go +++ b/tests/integration/x/erc20/test_evm.go @@ -84,8 +84,8 @@ func (s *KeeperTestSuite) TestBalanceOf() { { "Failed to call Evm", func() { - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, - mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("forced ApplyMessage error")) + mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, + mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("forced ApplyMessage error")) }, int64(0), false, @@ -93,8 +93,8 @@ func (s *KeeperTestSuite) TestBalanceOf() { { "Incorrect res", func() { - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, - mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: []uint8{0, 0}}, nil).Once() + mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, + mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: []uint8{0, 0}}, nil).Once() }, int64(0), false, @@ -104,8 +104,8 @@ func (s *KeeperTestSuite) TestBalanceOf() { func() { balance := make([]uint8, 32) balance[31] = uint8(10) - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, - mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Once() + mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, + mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Once() }, int64(10), true, @@ -147,16 +147,16 @@ func (s *KeeperTestSuite) TestQueryERC20ForceFail() { { "Failed to call Evm", func() { - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, - mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("forced ApplyMessage error")) + mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, + mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("forced ApplyMessage error")) }, false, }, { "Incorrect res", func() { - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, - mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: []uint8{0, 0}}, nil).Once() + mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, + mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: []uint8{0, 0}}, nil).Once() }, false, }, @@ -165,8 +165,8 @@ func (s *KeeperTestSuite) TestQueryERC20ForceFail() { func() { ret := []uint8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 67, 111, 105, 110, 32, 84, 111, 107, 101, 110, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: ret}, nil).Once() - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, - mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{VmError: "Error"}, nil).Once() + mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, + mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{VmError: "Error"}, nil).Once() }, false, }, @@ -174,10 +174,10 @@ func (s *KeeperTestSuite) TestQueryERC20ForceFail() { "incorrect symbol res", func() { ret := []uint8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 67, 111, 105, 110, 32, 84, 111, 107, 101, 110, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, - mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: ret}, nil).Once() - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, - mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: []uint8{0, 0}}, nil).Once() + mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, + mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: ret}, nil).Once() + mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, + mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: []uint8{0, 0}}, nil).Once() }, false, }, @@ -186,11 +186,11 @@ func (s *KeeperTestSuite) TestQueryERC20ForceFail() { func() { ret := []uint8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 67, 111, 105, 110, 32, 84, 111, 107, 101, 110, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} retSymbol := []uint8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 67, 84, 75, 78, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, + mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: ret}, nil).Once() - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, + mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: retSymbol}, nil).Once() - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, + mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{VmError: "Error"}, nil).Once() }, false, @@ -200,12 +200,12 @@ func (s *KeeperTestSuite) TestQueryERC20ForceFail() { func() { ret := []uint8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 67, 111, 105, 110, 32, 84, 111, 107, 101, 110, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} retSymbol := []uint8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 67, 84, 75, 78, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, - mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: ret}, nil).Once() - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, - mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: retSymbol}, nil).Once() - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, - mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: []uint8{0, 0}}, nil).Once() + mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, + mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: ret}, nil).Once() + mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, + mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: retSymbol}, nil).Once() + mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, + mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: []uint8{0, 0}}, nil).Once() }, false, }, @@ -286,11 +286,11 @@ func (s *KeeperTestSuite) TestQueryERC20Bytes32Fallback() { symbolData := createStringData("MKR") decimalsData := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18} - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "name"). + mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "name"). Return(&evmtypes.MsgEthereumTxResponse{Ret: nameData}, nil).Once() - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "symbol"). + mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "symbol"). Return(&evmtypes.MsgEthereumTxResponse{Ret: symbolData}, nil).Once() - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "decimals"). + mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "decimals"). Return(&evmtypes.MsgEthereumTxResponse{Ret: decimalsData}, nil).Once() }, types.ERC20Data{Name: "Maker", Symbol: "MKR", Decimals: 18}, @@ -304,11 +304,11 @@ func (s *KeeperTestSuite) TestQueryERC20Bytes32Fallback() { decimalsData := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18} // First call tries string unpacking (will fail), then tries bytes32 (will succeed) - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "name"). + mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "name"). Return(&evmtypes.MsgEthereumTxResponse{Ret: nameData}, nil).Once() - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "symbol"). + mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "symbol"). Return(&evmtypes.MsgEthereumTxResponse{Ret: symbolData}, nil).Once() - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "decimals"). + mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "decimals"). Return(&evmtypes.MsgEthereumTxResponse{Ret: decimalsData}, nil).Once() }, types.ERC20Data{Name: "Maker", Symbol: "MKR", Decimals: 18}, @@ -321,11 +321,11 @@ func (s *KeeperTestSuite) TestQueryERC20Bytes32Fallback() { symbolData := createBytes32Data("MKR") decimalsData := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18} - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "name"). + mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "name"). Return(&evmtypes.MsgEthereumTxResponse{Ret: nameData}, nil).Once() - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "symbol"). + mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "symbol"). Return(&evmtypes.MsgEthereumTxResponse{Ret: symbolData}, nil).Once() - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "decimals"). + mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "decimals"). Return(&evmtypes.MsgEthereumTxResponse{Ret: decimalsData}, nil).Once() }, types.ERC20Data{Name: "Maker", Symbol: "MKR", Decimals: 18}, @@ -345,11 +345,11 @@ func (s *KeeperTestSuite) TestQueryERC20Bytes32Fallback() { decimalsData := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18} - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "name"). + mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "name"). Return(&evmtypes.MsgEthereumTxResponse{Ret: nameData}, nil).Once() - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "symbol"). + mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "symbol"). Return(&evmtypes.MsgEthereumTxResponse{Ret: symbolData}, nil).Once() - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "decimals"). + mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "decimals"). Return(&evmtypes.MsgEthereumTxResponse{Ret: decimalsData}, nil).Once() }, types.ERC20Data{Name: "Maker", Symbol: "MKR", Decimals: 18}, @@ -358,7 +358,7 @@ func (s *KeeperTestSuite) TestQueryERC20Bytes32Fallback() { { "EVM call fails for name", func() { - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "name"). + mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "name"). Return(nil, fmt.Errorf("EVM call failed")).Once() }, types.ERC20Data{}, @@ -369,7 +369,7 @@ func (s *KeeperTestSuite) TestQueryERC20Bytes32Fallback() { func() { invalidData := []byte{0xFF, 0xFF} // Invalid data that will fail both unpacking methods - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "name"). + mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "name"). Return(&evmtypes.MsgEthereumTxResponse{Ret: invalidData}, nil).Once() }, types.ERC20Data{}, @@ -380,9 +380,9 @@ func (s *KeeperTestSuite) TestQueryERC20Bytes32Fallback() { func() { nameData := createStringData("Maker") - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "name"). + mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "name"). Return(&evmtypes.MsgEthereumTxResponse{Ret: nameData}, nil).Once() - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "symbol"). + mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "symbol"). Return(nil, fmt.Errorf("EVM call failed")).Once() }, types.ERC20Data{}, diff --git a/tests/integration/x/erc20/test_ibc_callback.go b/tests/integration/x/erc20/test_ibc_callback.go index 7af9916a2..056d8f6ee 100644 --- a/tests/integration/x/erc20/test_ibc_callback.go +++ b/tests/integration/x/erc20/test_ibc_callback.go @@ -426,8 +426,8 @@ func (s *KeeperTestSuite) TestConvertCoinToERC20FromPacket() { ), ) s.Require().NoError(err) - - _, err = s.network.App.GetEVMKeeper().CallEVM(ctx, contracts.ERC20MinterBurnerDecimalsContract.ABI, s.keyring.GetAddr(0), contractAddr, true, nil, "mint", types.ModuleAddress, big.NewInt(10)) + stateDB := statedb.New(ctx, s.network.App.GetEVMKeeper(), statedb.NewEmptyTxConfig()) + _, err = s.network.App.GetEVMKeeper().CallEVM(ctx, stateDB, contracts.ERC20MinterBurnerDecimalsContract.ABI, s.keyring.GetAddr(0), contractAddr, true, false, nil, "mint", types.ModuleAddress, big.NewInt(10)) s.Require().NoError(err) return transfertypes.NewFungibleTokenPacketData(pair.Denom, "10", senderAddr, "", "") @@ -563,7 +563,8 @@ func (s *KeeperTestSuite) TestOnAcknowledgementPacket() { ) s.Require().NoError(err) - _, err = s.network.App.GetEVMKeeper().CallEVM(ctx, contracts.ERC20MinterBurnerDecimalsContract.ABI, s.keyring.GetAddr(0), contractAddr, true, nil, "mint", types.ModuleAddress, big.NewInt(100)) + stateDB := statedb.New(ctx, s.network.App.GetEVMKeeper(), statedb.NewEmptyTxConfig()) + _, err = s.network.App.GetEVMKeeper().CallEVM(ctx, stateDB, contracts.ERC20MinterBurnerDecimalsContract.ABI, s.keyring.GetAddr(0), contractAddr, true, false, nil, "mint", types.ModuleAddress, big.NewInt(100)) s.Require().NoError(err) ack = channeltypes.NewErrorAcknowledgement(errors.New("error")) @@ -669,7 +670,8 @@ func (s *KeeperTestSuite) TestOnTimeoutPacket() { pair, _ = s.network.App.GetErc20Keeper().GetTokenPair(ctx, id) s.Require().NotNil(pair) - _, err = s.network.App.GetEVMKeeper().CallEVM(ctx, contracts.ERC20MinterBurnerDecimalsContract.ABI, s.keyring.GetAddr(0), contractAddr, true, nil, "mint", types.ModuleAddress, big.NewInt(100)) + stateDB := statedb.New(ctx, s.network.App.GetEVMKeeper(), statedb.NewEmptyTxConfig()) + _, err = s.network.App.GetEVMKeeper().CallEVM(ctx, stateDB, contracts.ERC20MinterBurnerDecimalsContract.ABI, s.keyring.GetAddr(0), contractAddr, true, false, nil, "mint", types.ModuleAddress, big.NewInt(100)) s.Require().NoError(err) // Fund module account with ATOM, ERC20 coins and IBC vouchers @@ -848,7 +850,7 @@ func (s *KeeperTestSuite) TestConvertCoinNativeERC20RollsBackEscrowOnEVMFailure( s.Require().Equal(math.NewInt(100), senderBefore.Amount) err := s.network.App.GetErc20Keeper().ConvertCoinNativeERC20( - ctx, pair, math.NewInt(10), common.BytesToAddress(sender), sender, + ctx, pair, math.NewInt(10), common.BytesToAddress(sender), sender, false, ) s.Require().Error(err, "a paused token must fail the unescrow transfer") diff --git a/tests/integration/x/erc20/test_msg_server.go b/tests/integration/x/erc20/test_msg_server.go index 854445a79..d09f2bd9b 100644 --- a/tests/integration/x/erc20/test_msg_server.go +++ b/tests/integration/x/erc20/test_msg_server.go @@ -11,7 +11,6 @@ import ( "github.com/cosmos/evm/contracts" "github.com/cosmos/evm/testutil/integration/base/factory" - "github.com/cosmos/evm/testutil/integration/evm/utils" "github.com/cosmos/evm/x/erc20/keeper" "github.com/cosmos/evm/x/erc20/types" erc20mocks "github.com/cosmos/evm/x/erc20/types/mocks" @@ -25,370 +24,121 @@ import ( govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" ) -func (s *KeeperTestSuite) TestConvertERC20NativeERC20() { - var ( - contractAddr common.Address - coinName string - ) +// TestConvertERC20 tests the ConvertERC20 msg server method, +// focusing on message validation and address parsing +func (s *KeeperTestSuite) TestConvertERC20() { testCases := []struct { - name string - mint int64 - transfer int64 - malleate func(common.Address) - extra func() - contractType int - expPass bool - selfdestructed bool + name string + setup func() *types.MsgConvertERC20 + expPass bool }{ { - "ok - sufficient funds", - 100, - 10, - func(common.Address) {}, - func() {}, - contractMinterBurner, - true, - false, - }, - { - "ok - equal funds", - 10, - 10, - func(common.Address) {}, - func() {}, - contractMinterBurner, - true, - false, - }, - { - "fail - insufficient funds - callEVM", - 0, - 10, - func(common.Address) {}, - func() {}, - contractMinterBurner, - false, - false, - }, - { - "fail - minting disabled", - 100, - 10, - func(common.Address) { - params := types.DefaultParams() - params.EnableErc20 = false - err := utils.UpdateERC20Params( - utils.UpdateParamsInput{ - Tf: s.factory, - Network: s.network, - Pk: s.keyring.GetPrivKey(0), - Params: params, - }, - ) + "pass - valid message with proper addresses", + func() *types.MsgConvertERC20 { + contractAddr, err := s.setupRegisterERC20Pair(contractMinterBurner) s.Require().NoError(err) - }, - func() {}, - contractMinterBurner, - false, - false, - }, - { - "fail - direct balance manipulation contract", - 100, - 10, - func(common.Address) {}, - func() {}, - contractDirectBalanceManipulation, - false, - false, - }, - { - // The contract grants a third party an allowance over the escrow - // module on every `transfer`, which would leave the minted coins - // unbacked once that allowance is spent. The unexpected `Approval` - // event in the transfer logs must abort the conversion. - "fail - delayed malicious contract", - 10, - 10, - func(common.Address) {}, - func() {}, - contractMaliciousDelayed, - false, - false, - }, - { - "fail - negative transfer contract", - 10, - -10, - func(common.Address) {}, - func() {}, - contractMinterBurner, - false, - false, - }, - { - "fail - force evm fail", - 100, - 10, - func(common.Address) {}, - func() { - mockEVMKeeper := &erc20mocks.EVMKeeper{} - transferKeeper := s.network.App.GetTransferKeeper() - erc20Keeper := keeper.NewKeeper( - s.network.App.GetKey("erc20"), s.network.App.AppCodec(), - authtypes.NewModuleAddress(govtypes.ModuleName), s.network.App.GetAccountKeeper(), - s.network.App.GetBankKeeper(), mockEVMKeeper, s.network.App.GetStakingKeeper(), - &transferKeeper, - ) - s.network.App.SetErc20Keeper(erc20Keeper) - existingAcc := &statedb.Account{Nonce: uint64(1), Balance: uint256.NewInt(1)} - balance := make([]uint8, 32) - mockEVMKeeper.On("EstimateGasInternal", mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.EstimateGasResponse{Gas: uint64(200)}, nil) - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, - mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Once() - mockEVMKeeper.On("CallEVMWithData", mock.Anything, mock.Anything, mock.Anything, mock.Anything, - mock.Anything, mock.Anything).Return(nil, fmt.Errorf("forced ApplyMessage error")) - mockEVMKeeper.On("GetAccountWithoutBalance", mock.Anything, mock.Anything).Return(existingAcc, nil) - mockEVMKeeper.On("IsContract", mock.Anything, mock.Anything).Return(true) - }, - contractMinterBurner, - false, - false, - }, - { - "fail - force get balance fail", - 100, - 10, - func(common.Address) {}, - func() { - mockEVMKeeper := &erc20mocks.EVMKeeper{} - transferKeeper := s.network.App.GetTransferKeeper() - erc20Keeper := keeper.NewKeeper( - s.network.App.GetKey("erc20"), s.network.App.AppCodec(), - authtypes.NewModuleAddress(govtypes.ModuleName), s.network.App.GetAccountKeeper(), - s.network.App.GetBankKeeper(), mockEVMKeeper, s.network.App.GetStakingKeeper(), - &transferKeeper, - ) - s.network.App.SetErc20Keeper(erc20Keeper) + sender := s.keyring.GetAccAddr(0) + senderHex := s.keyring.GetAddr(0) - existingAcc := &statedb.Account{Nonce: uint64(1), Balance: uint256.NewInt(1)} - balance := make([]uint8, 32) - balance[31] = uint8(1) - mockEVMKeeper.On("EstimateGasInternal", mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.EstimateGasResponse{Gas: uint64(200)}, nil) - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Twice() - mockEVMKeeper.On("CallEVMWithData", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("forced balance error")) - mockEVMKeeper.On("GetAccountWithoutBalance", mock.Anything, mock.Anything).Return(existingAcc, nil) - mockEVMKeeper.On("IsContract", mock.Anything, mock.Anything).Return(true) + _, err = s.MintERC20Token(contractAddr, senderHex, big.NewInt(100)) + s.Require().NoError(err) + + return types.NewMsgConvertERC20( + math.NewInt(10), + sender, + contractAddr, + senderHex, + ) }, - contractMinterBurner, - false, - false, + true, }, { - "fail - force transfer unpack fail", - 100, - 10, - func(common.Address) {}, - func() { - mockEVMKeeper := &erc20mocks.EVMKeeper{} - transferKeeper := s.network.App.GetTransferKeeper() - erc20Keeper := keeper.NewKeeper( - s.network.App.GetKey("erc20"), s.network.App.AppCodec(), - authtypes.NewModuleAddress(govtypes.ModuleName), s.network.App.GetAccountKeeper(), - s.network.App.GetBankKeeper(), mockEVMKeeper, s.network.App.GetStakingKeeper(), - &transferKeeper, - ) - s.network.App.SetErc20Keeper(erc20Keeper) + "fail - invalid receiver bech32 address format", + func() *types.MsgConvertERC20 { + contractAddr, err := s.setupRegisterERC20Pair(contractMinterBurner) + s.Require().NoError(err) - existingAcc := &statedb.Account{Nonce: uint64(1), Balance: uint256.NewInt(1)} - balance := make([]uint8, 32) - mockEVMKeeper.On("EstimateGasInternal", mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.EstimateGasResponse{Gas: uint64(200)}, nil) - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, - mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Once() - mockEVMKeeper.On("CallEVMWithData", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{}, nil) - mockEVMKeeper.On("GetAccountWithoutBalance", mock.Anything, mock.Anything).Return(existingAcc, nil) - mockEVMKeeper.On("IsContract", mock.Anything, mock.Anything).Return(true) - }, - contractMinterBurner, - false, - false, - }, + sender := s.keyring.GetAccAddr(0) + senderHex := s.keyring.GetAddr(0) - { - "fail - force invalid transfer fail", - 100, - 10, - func(common.Address) {}, - func() { - mockEVMKeeper := &erc20mocks.EVMKeeper{} - transferKeeper := s.network.App.GetTransferKeeper() - erc20Keeper := keeper.NewKeeper( - s.network.App.GetKey("erc20"), s.network.App.AppCodec(), - authtypes.NewModuleAddress(govtypes.ModuleName), s.network.App.GetAccountKeeper(), - s.network.App.GetBankKeeper(), mockEVMKeeper, s.network.App.GetStakingKeeper(), - &transferKeeper, - ) - s.network.App.SetErc20Keeper(erc20Keeper) + _, err = s.MintERC20Token(contractAddr, senderHex, big.NewInt(100)) + s.Require().NoError(err) - existingAcc := &statedb.Account{Nonce: uint64(1), Balance: uint256.NewInt(1)} - balance := make([]uint8, 32) - mockEVMKeeper.On("EstimateGasInternal", mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.EstimateGasResponse{Gas: uint64(200)}, nil) - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, - mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Once() - mockEVMKeeper.On("CallEVMWithData", mock.Anything, mock.Anything, mock.Anything, mock.Anything, - mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil) - mockEVMKeeper.On("GetAccountWithoutBalance", mock.Anything, mock.Anything).Return(existingAcc, nil) - mockEVMKeeper.On("IsContract", mock.Anything, mock.Anything).Return(true) - }, - contractMinterBurner, - false, - false, - }, - { - "fail - force mint fail", - 100, - 10, - func(common.Address) {}, - func() { - ctrl := gomock.NewController(s.T()) - mockBankKeeper := erc20mocks.NewMockBankKeeper(ctrl) - transferKeeper := s.network.App.GetTransferKeeper() - erc20Keeper := keeper.NewKeeper( - s.network.App.GetKey("erc20"), s.network.App.AppCodec(), - authtypes.NewModuleAddress(govtypes.ModuleName), s.network.App.GetAccountKeeper(), - mockBankKeeper, s.network.App.GetEVMKeeper(), s.network.App.GetStakingKeeper(), - &transferKeeper, + msg := types.NewMsgConvertERC20( + math.NewInt(10), + sender, + contractAddr, + senderHex, ) - s.network.App.SetErc20Keeper(erc20Keeper) - - mockBankKeeper.EXPECT().MintCoins(gomock.Any(), gomock.Any(), gomock.Any()).Return(fmt.Errorf("failed to mint")).AnyTimes() - mockBankKeeper.EXPECT().SendCoinsFromModuleToAccount(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(fmt.Errorf("failed to unescrow")).AnyTimes() - mockBankKeeper.EXPECT().BlockedAddr(gomock.Any()).Return(false).AnyTimes() - mockBankKeeper.EXPECT().GetBalance(gomock.Any(), gomock.Any(), gomock.Any()).Return(sdk.Coin{Denom: "coin", Amount: math.OneInt()}).AnyTimes() - mockBankKeeper.EXPECT().IsSendEnabledCoin(gomock.Any(), gomock.Any()).Return(true).AnyTimes() + // Create invalid bech32 address with valid length but invalid format + // Using wrong prefix or invalid checksum + msg.Receiver = "cosmos100000000000000000000000000000000" + return msg }, - contractMinterBurner, - false, false, }, { - "fail - force send minted fail", - 100, - 10, - func(common.Address) {}, - func() { - ctrl := gomock.NewController(s.T()) - mockBankKeeper := erc20mocks.NewMockBankKeeper(ctrl) - transferKeeper := s.network.App.GetTransferKeeper() - erc20Keeper := keeper.NewKeeper( - s.network.App.GetKey("erc20"), s.network.App.AppCodec(), - authtypes.NewModuleAddress(govtypes.ModuleName), s.network.App.GetAccountKeeper(), - mockBankKeeper, s.network.App.GetEVMKeeper(), s.network.App.GetStakingKeeper(), - &transferKeeper, - ) - s.network.App.SetErc20Keeper(erc20Keeper) + "fail - invalid sender hex address format", + func() *types.MsgConvertERC20 { + contractAddr, err := s.setupRegisterERC20Pair(contractMinterBurner) + s.Require().NoError(err) - mockBankKeeper.EXPECT().MintCoins(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) - mockBankKeeper.EXPECT().SendCoinsFromModuleToAccount(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(fmt.Errorf("failed to unescrow")) - mockBankKeeper.EXPECT().BlockedAddr(gomock.Any()).Return(false) - mockBankKeeper.EXPECT().GetBalance(gomock.Any(), gomock.Any(), gomock.Any()).Return(sdk.Coin{Denom: "coin", Amount: math.OneInt()}) - mockBankKeeper.EXPECT().IsSendEnabledCoin(gomock.Any(), gomock.Any()).Return(true).AnyTimes() + sender := s.keyring.GetAccAddr(0) + senderHex := s.keyring.GetAddr(0) + + _, err = s.MintERC20Token(contractAddr, senderHex, big.NewInt(100)) + s.Require().NoError(err) + + msg := types.NewMsgConvertERC20( + math.NewInt(10), + sender, + contractAddr, + senderHex, + ) + // Create invalid hex address - not a valid hex string + msg.Sender = "0xZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ" + return msg }, - contractMinterBurner, - false, false, }, { - "fail - force bank balance fail", - 100, - 10, - func(common.Address) {}, - func() { - ctrl := gomock.NewController(s.T()) - mockBankKeeper := erc20mocks.NewMockBankKeeper(ctrl) - transferKeeper := s.network.App.GetTransferKeeper() - erc20Keeper := keeper.NewKeeper( - s.network.App.GetKey("erc20"), s.network.App.AppCodec(), - authtypes.NewModuleAddress(govtypes.ModuleName), s.network.App.GetAccountKeeper(), - mockBankKeeper, s.network.App.GetEVMKeeper(), s.network.App.GetStakingKeeper(), - &transferKeeper, + "fail - invalid contract hex address format", + func() *types.MsgConvertERC20 { + sender := s.keyring.GetAccAddr(0) + senderHex := s.keyring.GetAddr(0) + + msg := types.NewMsgConvertERC20( + math.NewInt(10), + sender, + common.HexToAddress("0x0"), + senderHex, ) - s.network.App.SetErc20Keeper(erc20Keeper) - - mockBankKeeper.EXPECT().MintCoins(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) - mockBankKeeper.EXPECT().SendCoinsFromModuleToAccount(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) - mockBankKeeper.EXPECT().BlockedAddr(gomock.Any()).Return(false) - mockBankKeeper.EXPECT().GetBalance(gomock.Any(), gomock.Any(), gomock.Any()).Return(sdk.Coin{Denom: coinName, Amount: math.OneInt()}).AnyTimes() - mockBankKeeper.EXPECT().IsSendEnabledCoin(gomock.Any(), gomock.Any()).Return(true).AnyTimes() + // Create invalid hex address - not a valid hex string + msg.ContractAddress = "0xGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG" + return msg }, - contractMinterBurner, - false, false, }, } + for _, tc := range testCases { - s.Run(fmt.Sprintf("Case %s", tc.name), func() { - var err error + s.Run(tc.name, func() { s.mintFeeCollector = true - defer func() { - s.mintFeeCollector = false - }() + defer func() { s.mintFeeCollector = false }() s.SetupTest() - - contractAddr, err = s.setupRegisterERC20Pair(tc.contractType) - s.Require().NoError(err) - - tc.malleate(contractAddr) - s.Require().NotNil(contractAddr) - - coinName = types.CreateDenom(contractAddr.String()) - sender := s.keyring.GetAccAddr(0) - - _, err = s.MintERC20Token(contractAddr, s.keyring.GetAddr(0), big.NewInt(tc.mint)) - s.Require().NoError(err) - // update context with latest committed changes - - tc.extra() - - convertERC20Msg := types.NewMsgConvertERC20( - math.NewInt(tc.transfer), - sender, - contractAddr, - s.keyring.GetAddr(0), - ) - - ctx := s.network.GetContext() + msg := tc.setup() if tc.expPass { - _, err = s.factory.CommitCosmosTx(s.keyring.GetPrivKey(0), factory.CosmosTxArgs{Msgs: []sdk.Msg{convertERC20Msg}}) + _, err := s.network.App.GetErc20Keeper().ConvertERC20(s.network.GetContext(), msg) s.Require().NoError(err, tc.name) - - cosmosBalance := s.network.App.GetBankKeeper().GetBalance(ctx, sender, coinName) - - acc := s.network.App.GetEVMKeeper().GetAccountWithoutBalance(ctx, contractAddr) - if tc.selfdestructed { - s.Require().Nil(acc, "expected contract to be destroyed") - } else { - s.Require().NotNil(acc) - } - - isContract := s.network.App.GetEVMKeeper().IsContract(s.network.GetContext(), contractAddr) - if tc.selfdestructed || !isContract { - id := s.network.App.GetErc20Keeper().GetTokenPairID(ctx, contractAddr.String()) - _, found := s.network.App.GetErc20Keeper().GetTokenPair(ctx, id) - s.Require().False(found) - } else { - s.Require().Equal(cosmosBalance.Amount, math.NewInt(tc.transfer)) - } } else { - _, err = s.network.App.GetErc20Keeper().ConvertERC20(ctx, convertERC20Msg) + _, err := s.network.App.GetErc20Keeper().ConvertERC20(s.network.GetContext(), msg) s.Require().Error(err, tc.name) } }) } - s.mintFeeCollector = false } // TestConvertERC20MaliciousApprovalKeepsEscrowInvariant asserts the escrow-and-mint invariant @@ -599,10 +349,10 @@ func (s *KeeperTestSuite) TestConvertNativeERC20ToEVMERC20() { existingAcc := &statedb.Account{Nonce: uint64(1), Balance: uint256.NewInt(1)} balance := make([]uint8, 32) mockEVMKeeper.On("EstimateGasInternal", mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.EstimateGasResponse{Gas: uint64(200)}, nil) - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, - mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, fmt.Errorf("forced ApplyMessage error")).Once() - mockEVMKeeper.On("CallEVMWithData", mock.Anything, mock.Anything, mock.Anything, mock.Anything, - mock.Anything, mock.Anything).Return(nil, fmt.Errorf("forced ApplyMessage error")) + mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, + mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, fmt.Errorf("forced ApplyMessage error")).Once() + mockEVMKeeper.On("CallEVMWithData", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, + mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("forced ApplyMessage error")) mockEVMKeeper.On("GetAccountWithoutBalance", mock.Anything, mock.Anything).Return(existingAcc, nil) mockEVMKeeper.On("IsContract", mock.Anything, mock.Anything).Return(true) }, @@ -630,8 +380,11 @@ func (s *KeeperTestSuite) TestConvertNativeERC20ToEVMERC20() { balance := make([]uint8, 32) balance[31] = uint8(1) mockEVMKeeper.On("EstimateGasInternal", mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.EstimateGasResponse{Gas: uint64(200)}, nil) - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Times(3) - mockEVMKeeper.On("CallEVMWithData", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("forced balance error")) + mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, + mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Times(3) + mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, + mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Maybe() + mockEVMKeeper.On("CallEVMWithData", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("forced balance error")) mockEVMKeeper.On("GetAccountWithoutBalance", mock.Anything, mock.Anything).Return(existingAcc, nil) mockEVMKeeper.On("IsContract", mock.Anything, mock.Anything).Return(true) }, @@ -658,9 +411,9 @@ func (s *KeeperTestSuite) TestConvertNativeERC20ToEVMERC20() { existingAcc := &statedb.Account{Nonce: uint64(1), Balance: uint256.NewInt(1)} balance := make([]uint8, 32) mockEVMKeeper.On("EstimateGasInternal", mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.EstimateGasResponse{Gas: uint64(200)}, nil) - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, - mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Twice() - mockEVMKeeper.On("CallEVMWithData", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{}, nil) + mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, + mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Twice() + mockEVMKeeper.On("CallEVMWithData", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{}, nil) mockEVMKeeper.On("GetAccountWithoutBalance", mock.Anything, mock.Anything).Return(existingAcc, nil) mockEVMKeeper.On("IsContract", mock.Anything, mock.Anything).Return(true) }, @@ -688,9 +441,9 @@ func (s *KeeperTestSuite) TestConvertNativeERC20ToEVMERC20() { existingAcc := &statedb.Account{Nonce: uint64(1), Balance: uint256.NewInt(1)} balance := make([]uint8, 32) mockEVMKeeper.On("EstimateGasInternal", mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.EstimateGasResponse{Gas: uint64(200)}, nil) - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, + mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Twice() - mockEVMKeeper.On("CallEVMWithData", mock.Anything, mock.Anything, mock.Anything, mock.Anything, + mockEVMKeeper.On("CallEVMWithData", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil) mockEVMKeeper.On("GetAccountWithoutBalance", mock.Anything, mock.Anything).Return(existingAcc, nil) mockEVMKeeper.On("IsContract", mock.Anything, mock.Anything).Return(true) diff --git a/tests/integration/x/erc20/test_proposals.go b/tests/integration/x/erc20/test_proposals.go index 997bd9d8e..86fbc9e58 100644 --- a/tests/integration/x/erc20/test_proposals.go +++ b/tests/integration/x/erc20/test_proposals.go @@ -168,7 +168,7 @@ func (s *KeeperTestSuite) TestRegisterERC20() { s.network.App.SetErc20Keeper(erc20Keeper) mockEVMKeeper.On("EstimateGasInternal", mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.EstimateGasResponse{Gas: uint64(200)}, nil) - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("forced CallEVM error")) + mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("forced CallEVM error")) mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("forced ApplyMessage error")) }, s.keyring.GetAccAddr(0).String(), diff --git a/tests/integration/x/ibc/test_keeper.go b/tests/integration/x/ibc/test_keeper.go index 71a21f685..6c36a9ad0 100644 --- a/tests/integration/x/ibc/test_keeper.go +++ b/tests/integration/x/ibc/test_keeper.go @@ -42,8 +42,6 @@ type KeeperTestSuite struct { otherDenom string } -var timeoutHeight = clienttypes.NewHeight(1000, 1000) - func NewKeeperTestSuite(create network.CreateEvmApp, options ...network.ConfigOption) *KeeperTestSuite { return &KeeperTestSuite{ create: create, diff --git a/tests/integration/x/ibc/test_msg_server.go b/tests/integration/x/ibc/test_msg_server.go deleted file mode 100644 index 56a9599ce..000000000 --- a/tests/integration/x/ibc/test_msg_server.go +++ /dev/null @@ -1,517 +0,0 @@ -package ibc - -import ( - "fmt" - "strings" - - "github.com/stretchr/testify/mock" - - "github.com/cosmos/evm/testutil/integration/evm/utils" - testutils "github.com/cosmos/evm/testutil/integration/evm/utils" - "github.com/cosmos/evm/testutil/keyring" - erc20types "github.com/cosmos/evm/x/erc20/types" - transferkeeper "github.com/cosmos/evm/x/ibc/transfer/keeper" - evmtypes "github.com/cosmos/evm/x/vm/types" - "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" - channeltypes "github.com/cosmos/ibc-go/v10/modules/core/04-channel/types" - - "cosmossdk.io/math" - - "github.com/cosmos/cosmos-sdk/runtime" - 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" - govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" -) - -func (suite *KeeperTestSuite) TestTransfer() { - var ( - ctx sdk.Context - sender keyring.Key - ) - mockChannelKeeper := &MockChannelKeeper{} - mockICS4Wrapper := &MockICS4Wrapper{} - mockChannelKeeper.On("GetNextSequenceSend", mock.Anything, mock.Anything, mock.Anything).Return(1, true) - mockChannelKeeper.On("GetChannel", mock.Anything, mock.Anything, mock.Anything).Return(channeltypes.Channel{Counterparty: channeltypes.NewCounterparty("transfer", "channel-1")}, true) - mockICS4Wrapper.On("SendPacket", mock.Anything, mock.Anything, mock.Anything).Return(nil) - authAddr := authtypes.NewModuleAddress(govtypes.ModuleName).String() - receiver := sdk.AccAddress([]byte("receiver")) - chan0 := "channel-0" - - testCases := []struct { - name string - malleate func() *types.MsgTransfer - expPass bool - }{ - { - "pass - no token pair", - func() *types.MsgTransfer { - transferMsg := types.NewMsgTransfer(types.PortID, chan0, sdk.NewCoin(evmtypes.GetEVMCoinDenom(), math.NewInt(10)), sender.AccAddr.String(), receiver.String(), timeoutHeight, 0, "") - return transferMsg - }, - true, - }, - { - "error - invalid sender", - func() *types.MsgTransfer { - addr := "" - contractAddr, err := suite.DeployContract("coin", "token", uint8(6)) - suite.Require().NoError(err) - - transferMsg := types.NewMsgTransfer(types.PortID, chan0, sdk.NewCoin(erc20types.CreateDenom(contractAddr.String()), math.NewInt(10)), addr, receiver.String(), timeoutHeight, 0, "") - return transferMsg - }, - false, - }, - { - "no-op - disabled erc20 by params - sufficient sdk.Coins balance", - func() *types.MsgTransfer { - contractAddr, err := suite.DeployContract("coin", "token", uint8(6)) - suite.Require().NoError(err) - - pair, err := utils.RegisterERC20(suite.factory, suite.network, utils.ERC20RegistrationData{ - Addresses: []string{contractAddr.Hex()}, - ProposerPriv: sender.Priv, - }) - suite.Require().NoError(err) - suite.Require().True(len(pair) == 1) - - amt := math.NewInt(10) - _, err = suite.MintERC20Token(contractAddr, sender.Addr, amt.BigInt()) - suite.Require().NoError(err) - - // convert all ERC20 to IBC coin - err = suite.ConvertERC20(sender, contractAddr, amt) - suite.Require().NoError(err) - - params := suite.network.App.GetErc20Keeper().GetParams(ctx) - params.EnableErc20 = false - - err = utils.UpdateERC20Params(utils.UpdateParamsInput{ - Tf: suite.factory, - Network: suite.network, - Pk: sender.Priv, - Params: params, - }) - suite.Require().NoError(err) - - coin := sdk.NewCoin(pair[0].Denom, amt) - transferMsg := types.NewMsgTransfer(types.PortID, chan0, coin, sender.AccAddr.String(), receiver.String(), timeoutHeight, 0, "") - - return transferMsg - }, - true, - }, - { - "error - disabled erc20 by params - insufficient sdk.Coins balance", - func() *types.MsgTransfer { - contractAddr, err := suite.DeployContract("coin", "token", uint8(6)) - suite.Require().NoError(err) - - pair, err := utils.RegisterERC20(suite.factory, suite.network, utils.ERC20RegistrationData{ - Addresses: []string{contractAddr.Hex()}, - ProposerPriv: sender.Priv, - }) - suite.Require().NoError(err) - suite.Require().True(len(pair) == 1) - - amt := math.NewInt(10) - _, err = suite.MintERC20Token(contractAddr, sender.Addr, amt.BigInt()) - suite.Require().NoError(err) - - // No conversion to IBC coin, so the balance is insufficient - suite.Require().EqualValues(suite.network.App.GetBankKeeper().GetBalance( - ctx, sender.AccAddr, pair[0].Denom).Amount, math.ZeroInt()) - - params := suite.network.App.GetErc20Keeper().GetParams(ctx) - params.EnableErc20 = false - err = utils.UpdateERC20Params(utils.UpdateParamsInput{ - Tf: suite.factory, - Network: suite.network, - Pk: sender.Priv, - Params: params, - }) - suite.Require().NoError(err) - - coin := sdk.NewCoin(pair[0].Denom, amt) - transferMsg := types.NewMsgTransfer(types.PortID, chan0, coin, sender.AccAddr.String(), receiver.String(), timeoutHeight, 0, "") - - return transferMsg - }, - false, - }, - { - "no-op - pair not registered", - func() *types.MsgTransfer { - coin := sdk.NewCoin(suite.otherDenom, math.NewInt(10)) - transferMsg := types.NewMsgTransfer(types.PortID, chan0, coin, sender.AccAddr.String(), receiver.String(), timeoutHeight, 0, "") - return transferMsg - }, - true, - }, - { - "no-op - pair is disabled", - func() *types.MsgTransfer { - contractAddr, err := suite.DeployContract("coin", "token", uint8(6)) - suite.Require().NoError(err) - - pair, err := utils.RegisterERC20(suite.factory, suite.network, utils.ERC20RegistrationData{ - Addresses: []string{contractAddr.Hex()}, - ProposerPriv: sender.Priv, - }) - suite.Require().NoError(err) - suite.Require().True(len(pair) == 1) - - amt := math.NewInt(10) - _, err = suite.MintERC20Token(contractAddr, sender.Addr, amt.BigInt()) - suite.Require().NoError(err) - - // convert all erc20 to coins to perform regular transfer without conversion - err = suite.ConvertERC20(sender, contractAddr, amt) - suite.Require().NoError(err) - - // disable token conversion - err = utils.ToggleTokenConversion(suite.factory, suite.network, sender.Priv, pair[0].Denom) - suite.Require().NoError(err) - - coin := sdk.NewCoin(pair[0].Denom, math.NewInt(10)) - transferMsg := types.NewMsgTransfer(types.PortID, chan0, coin, sender.AccAddr.String(), receiver.String(), timeoutHeight, 0, "") - - return transferMsg - }, - true, - }, - { - "pass - has enough balance in erc20 - need to convert", - func() *types.MsgTransfer { - contractAddr, err := suite.DeployContract("coin", "token", uint8(6)) - suite.Require().NoError(err) - - res, err := utils.RegisterERC20(suite.factory, suite.network, utils.ERC20RegistrationData{ - Addresses: []string{contractAddr.Hex()}, - ProposerPriv: sender.Priv, - }) - suite.Require().NoError(err) - suite.Require().True(len(res) == 1) - pair := res[0] - suite.Require().Equal(erc20types.CreateDenom(pair.Erc20Address), pair.Denom) - - amt := math.NewInt(10) - _, err = suite.MintERC20Token(contractAddr, sender.Addr, amt.BigInt()) - suite.Require().NoError(err) - - transferMsg := types.NewMsgTransfer(types.PortID, chan0, sdk.NewCoin(pair.Denom, amt), sender.AccAddr.String(), receiver.String(), timeoutHeight, 0, "") - - return transferMsg - }, - true, - }, - { - "pass - has enough balance in coins", - func() *types.MsgTransfer { - contractAddr, err := suite.DeployContract("coin", "token", uint8(6)) - suite.Require().NoError(err) - - pair, err := utils.RegisterERC20(suite.factory, suite.network, utils.ERC20RegistrationData{ - Addresses: []string{contractAddr.Hex()}, - ProposerPriv: sender.Priv, - }) - suite.Require().NoError(err) - suite.Require().True(len(pair) == 1) - - // mint some erc20 tokens - amt := math.NewInt(10) - _, err = suite.MintERC20Token(contractAddr, suite.keyring.GetAddr(0), amt.BigInt()) - suite.Require().NoError(err) - - // convert all to IBC coins - err = suite.ConvertERC20(sender, contractAddr, amt) - suite.Require().NoError(err) - - transferMsg := types.NewMsgTransfer(types.PortID, chan0, sdk.NewCoin(pair[0].Denom, amt), sender.AccAddr.String(), receiver.String(), timeoutHeight, 0, "") - - return transferMsg - }, - true, - }, - { - "error - fail conversion - no balance in erc20", - func() *types.MsgTransfer { - contractAddr, err := suite.DeployContract("coin", "token", uint8(6)) - suite.Require().NoError(err) - - pair, err := utils.RegisterERC20(suite.factory, suite.network, utils.ERC20RegistrationData{ - Addresses: []string{contractAddr.Hex()}, - ProposerPriv: sender.Priv, - }) - suite.Require().NoError(err) - suite.Require().True(len(pair) == 1) - - transferMsg := types.NewMsgTransfer(types.PortID, chan0, sdk.NewCoin(pair[0].Denom, math.NewInt(10)), sender.AccAddr.String(), receiver.String(), timeoutHeight, 0, "") - return transferMsg - }, - false, - }, - { - "pass - verify correct prefix trimming for ERC20 native tokens", - func() *types.MsgTransfer { - contractAddr, err := suite.DeployContract("coin", "token", uint8(6)) - suite.Require().NoError(err) - - pair, err := testutils.RegisterERC20(suite.factory, suite.network, testutils.ERC20RegistrationData{ - Addresses: []string{contractAddr.Hex()}, - ProposerPriv: sender.Priv, - }) - suite.Require().NoError(err) - suite.Require().True(len(pair) == 1) - - // Mint ERC20 tokens - amt := math.NewInt(10) - _, err = suite.MintERC20Token(contractAddr, sender.Addr, amt.BigInt()) - suite.Require().NoError(err) - - // Create a denom with erc20: prefix - erc20Denom := erc20types.CreateDenom(contractAddr.String()) - suite.Require().Equal(erc20types.Erc20NativeCoinDenomPrefix+contractAddr.String(), erc20Denom) - - // Verify that GetTokenPairID works correctly with the contract address (hex string) - pairIDFromAddress := suite.network.App.GetErc20Keeper().GetTokenPairID(ctx, contractAddr.String()) - suite.Require().NotEmpty(pairIDFromAddress) - - // Verify that GetTokenPairID works correctly with the full denom - pairIDFromDenom := suite.network.App.GetErc20Keeper().GetTokenPairID(ctx, erc20Denom) - suite.Require().NotEmpty(pairIDFromDenom) - - // Both should return the same pair ID - suite.Require().Equal(pairIDFromAddress, pairIDFromDenom) - - transferMsg := types.NewMsgTransfer(types.PortID, chan0, sdk.NewCoin(erc20Denom, amt), sender.AccAddr.String(), receiver.String(), timeoutHeight, 0, "") - - return transferMsg - }, - true, - }, - - // STRV2 - // native coin - perform normal ibc transfer - { - "no-op - fail transfer", - func() *types.MsgTransfer { - senderAcc := suite.keyring.GetAccAddr(0) - - denom := "ibc/DF63978F803A2E27CA5CC9B7631654CCF0BBC788B3B7F0A10200508E37C70992" - coinMetadata := banktypes.Metadata{ - Name: "Generic IBC name", - Symbol: "IBC", - Description: "Generic IBC token description", - DenomUnits: []*banktypes.DenomUnit{ - { - Denom: denom, - Exponent: 0, - Aliases: []string{denom}, - }, - { - Denom: denom, - Exponent: 18, - }, - }, - Display: denom, - Base: denom, - } - - coin := sdk.NewCoin(denom, math.NewInt(10)) - - pair, err := suite.network.App.GetErc20Keeper().RegisterERC20Extension(suite.network.GetContext(), coinMetadata.Base) - suite.Require().Equal(pair.Denom, denom) - suite.Require().NoError(err) - - transferMsg := types.NewMsgTransfer(types.PortID, chan0, coin, senderAcc.String(), receiver.String(), timeoutHeight, 0, "") - - return transferMsg - }, - false, - }, - } - for _, tc := range testCases { - suite.Run(fmt.Sprintf("Case %s", tc.name), func() { - suite.SetupTest() - sender = suite.keyring.GetKey(0) - ctx = suite.network.GetContext() - - suite.network.App.SetTransferKeeper(transferkeeper.NewKeeper( - suite.network.App.AppCodec(), - runtime.NewKVStoreService(suite.network.App.GetKey(types.StoreKey)), - &MockICS4Wrapper{}, // ICS4 Wrapper - mockChannelKeeper, - suite.network.App.MsgServiceRouter(), - suite.network.App.GetAccountKeeper(), - suite.network.App.GetBankKeeper(), - suite.network.App.GetErc20Keeper(), // Add ERC20 Keeper for ERC20 transfers - authAddr, - )) - msg := tc.malleate() - - // get updated context with the latest changes - ctx = suite.network.GetContext() - - _, err := suite.network.App.GetTransferKeeper().Transfer(ctx, msg) - if tc.expPass { - suite.Require().NoError(err) - } else { - suite.Require().Error(err) - } - }) - } -} - -// TestPrefixTrimming tests that the Transfer method correctly trims the erc20: prefix -// This test specifically catches the bug where "erc20/" was being trimmed instead of "erc20:" -func (suite *KeeperTestSuite) TestPrefixTrimming() { - var ( - ctx sdk.Context - sender keyring.Key - ) - mockChannelKeeper := &MockChannelKeeper{} - mockICS4Wrapper := &MockICS4Wrapper{} - mockChannelKeeper.On("GetNextSequenceSend", mock.Anything, mock.Anything, mock.Anything).Return(1, true) - mockChannelKeeper.On("GetChannel", mock.Anything, mock.Anything, mock.Anything).Return(channeltypes.Channel{Counterparty: channeltypes.NewCounterparty("transfer", "channel-1")}, true) - mockICS4Wrapper.On("SendPacket", mock.Anything, mock.Anything, mock.Anything).Return(nil) - authAddr := authtypes.NewModuleAddress(govtypes.ModuleName).String() - receiver := sdk.AccAddress([]byte("receiver")) - chan0 := "channel-0" - - testCases := []struct { - name string - malleate func() *types.MsgTransfer - expPass bool - description string - }{ - { - name: "pass - correct prefix trimming erc20:", - malleate: func() *types.MsgTransfer { - contractAddr, err := suite.DeployContract("coin", "token", uint8(6)) - suite.Require().NoError(err) - - pair, err := testutils.RegisterERC20(suite.factory, suite.network, testutils.ERC20RegistrationData{ - Addresses: []string{contractAddr.Hex()}, - ProposerPriv: sender.Priv, - }) - suite.Require().NoError(err) - suite.Require().True(len(pair) == 1) - - // Mint ERC20 tokens - amt := math.NewInt(10) - _, err = suite.MintERC20Token(contractAddr, sender.Addr, amt.BigInt()) - suite.Require().NoError(err) - - // Create a denom with erc20: prefix - erc20Denom := erc20types.CreateDenom(contractAddr.String()) - suite.Require().Equal(erc20types.Erc20NativeCoinDenomPrefix+contractAddr.String(), erc20Denom) - - // TEST: Verify that the prefix trimming works correctly - // The Transfer method should trim "erc20:" prefix to get the hex address - expectedTrimmed := strings.TrimPrefix(erc20Denom, erc20types.Erc20NativeCoinDenomPrefix) - suite.Require().Equal(contractAddr.String(), expectedTrimmed) - - // Verify that GetTokenPairID works correctly with the contract address (hex string) - pairIDFromAddress := suite.network.App.GetErc20Keeper().GetTokenPairID(ctx, contractAddr.String()) - suite.Require().NotEmpty(pairIDFromAddress) - - // Verify that GetTokenPairID works correctly with the full denom - pairIDFromDenom := suite.network.App.GetErc20Keeper().GetTokenPairID(ctx, erc20Denom) - suite.Require().NotEmpty(pairIDFromDenom) - - // Both should return the same pair ID - suite.Require().Equal(pairIDFromAddress, pairIDFromDenom) - - // TEST: Verify that incorrect prefix trimming would fail - // If we incorrectly trim "erc20/" instead of "erc20:", we'd get the wrong string - incorrectTrimmed := strings.TrimPrefix(erc20Denom, erc20types.ModuleName+"/") - suite.Require().NotEqual(contractAddr.String(), incorrectTrimmed) - suite.Require().Equal(erc20Denom, incorrectTrimmed) // Since "erc20/" is not in the string, it returns unchanged - - transferMsg := types.NewMsgTransfer(types.PortID, chan0, sdk.NewCoin(erc20Denom, amt), sender.AccAddr.String(), receiver.String(), timeoutHeight, 0, "") - - return transferMsg - }, - expPass: true, - description: "Test that verifies correct prefix trimming for ERC20 native tokens", - }, - { - name: "pass - demonstrate bug impact", - malleate: func() *types.MsgTransfer { - contractAddr, err := suite.DeployContract("coin2", "token2", uint8(6)) - suite.Require().NoError(err) - - pair, err := testutils.RegisterERC20(suite.factory, suite.network, testutils.ERC20RegistrationData{ - Addresses: []string{contractAddr.Hex()}, - ProposerPriv: sender.Priv, - }) - suite.Require().NoError(err) - suite.Require().True(len(pair) == 1) - - // Mint ERC20 tokens - amt := math.NewInt(10) - _, err = suite.MintERC20Token(contractAddr, sender.Addr, amt.BigInt()) - suite.Require().NoError(err) - - // Create a denom with erc20: prefix - erc20Denom := erc20types.CreateDenom(contractAddr.String()) - - // TEST: Demonstrate the bug's impact - // With correct prefix trimming ("erc20:"), we get the hex address - correctTrimmed := strings.TrimPrefix(erc20Denom, erc20types.Erc20NativeCoinDenomPrefix) - suite.Require().Equal(contractAddr.String(), correctTrimmed) - - // With incorrect prefix trimming ("erc20/"), we get the full denom (no change) - incorrectTrimmed := strings.TrimPrefix(erc20Denom, erc20types.ModuleName+"/") - suite.Require().Equal(erc20Denom, incorrectTrimmed) - - // Both lookups should work due to dual mapping, but use different code paths - pairIDFromCorrect := suite.network.App.GetErc20Keeper().GetTokenPairID(ctx, correctTrimmed) - pairIDFromIncorrect := suite.network.App.GetErc20Keeper().GetTokenPairID(ctx, incorrectTrimmed) - - suite.Require().NotEmpty(pairIDFromCorrect) - suite.Require().NotEmpty(pairIDFromIncorrect) - suite.Require().Equal(pairIDFromCorrect, pairIDFromIncorrect) - - transferMsg := types.NewMsgTransfer(types.PortID, chan0, sdk.NewCoin(erc20Denom, amt), sender.AccAddr.String(), receiver.String(), timeoutHeight, 0, "") - - return transferMsg - }, - expPass: true, - description: "Test that demonstrates why the bug wasn't caught - both lookups work", - }, - } - - for _, tc := range testCases { - suite.Run(fmt.Sprintf("Case %s", tc.name), func() { - suite.SetupTest() - sender = suite.keyring.GetKey(0) - ctx = suite.network.GetContext() - - suite.network.App.SetTransferKeeper(transferkeeper.NewKeeper( - suite.network.App.AppCodec(), - runtime.NewKVStoreService(suite.network.App.GetKey(types.StoreKey)), - &MockICS4Wrapper{}, // ICS4 Wrapper - mockChannelKeeper, - suite.network.App.MsgServiceRouter(), - suite.network.App.GetAccountKeeper(), - suite.network.App.GetBankKeeper(), - suite.network.App.GetErc20Keeper(), // Add ERC20 Keeper for ERC20 transfers - authAddr, - )) - msg := tc.malleate() - - // get updated context with the latest changes - ctx = suite.network.GetContext() - - _, err := suite.network.App.GetTransferKeeper().Transfer(ctx, msg) - if tc.expPass { - suite.Require().NoError(err) - } else { - suite.Require().Error(err) - } - }) - } -} diff --git a/tests/integration/x/vm/state_transition_benchmark.go b/tests/integration/x/vm/state_transition_benchmark.go index e3e0a2c93..566306484 100644 --- a/tests/integration/x/vm/state_transition_benchmark.go +++ b/tests/integration/x/vm/state_transition_benchmark.go @@ -12,6 +12,7 @@ import ( "github.com/stretchr/testify/require" utiltx "github.com/cosmos/evm/testutil/tx" + "github.com/cosmos/evm/x/vm/statedb" evmtypes "github.com/cosmos/evm/x/vm/types" "github.com/cosmos/cosmos-sdk/crypto/keyring" @@ -287,8 +288,10 @@ func BenchmarkApplyMessage(b *testing.B) { ) require.NoError(b, err) + stateDB := statedb.New(suite.Network.GetContext(), suite.Network.App.GetEVMKeeper(), statedb.NewEmptyTxConfig()) + b.StartTimer() - resp, err := suite.Network.App.GetEVMKeeper().ApplyMessage(suite.Network.GetContext(), *m, nil, true, false) + resp, err := suite.Network.App.GetEVMKeeper().ApplyMessage(suite.Network.GetContext(), stateDB, *m, nil, true, false, false) b.StopTimer() require.NoError(b, err) @@ -321,8 +324,10 @@ func BenchmarkApplyMessageWithLegacyTx(b *testing.B) { ) require.NoError(b, err) + stateDB := statedb.New(suite.Network.GetContext(), suite.Network.App.GetEVMKeeper(), statedb.NewEmptyTxConfig()) + b.StartTimer() - resp, err := suite.Network.App.GetEVMKeeper().ApplyMessage(suite.Network.GetContext(), *m, nil, true, false) + resp, err := suite.Network.App.GetEVMKeeper().ApplyMessage(suite.Network.GetContext(), stateDB, *m, nil, true, false, false) b.StopTimer() require.NoError(b, err) @@ -355,8 +360,10 @@ func BenchmarkApplyMessageWithDynamicFeeTx(b *testing.B) { ) require.NoError(b, err) + stateDB := statedb.New(suite.Network.GetContext(), suite.Network.App.GetEVMKeeper(), statedb.NewEmptyTxConfig()) + b.StartTimer() - resp, err := suite.Network.App.GetEVMKeeper().ApplyMessage(suite.Network.GetContext(), *m, nil, true, false) + resp, err := suite.Network.App.GetEVMKeeper().ApplyMessage(suite.Network.GetContext(), stateDB, *m, nil, true, false, false) b.StopTimer() require.NoError(b, err) diff --git a/tests/integration/x/vm/test_call_evm.go b/tests/integration/x/vm/test_call_evm.go index 787ef3dd3..1ebd84183 100644 --- a/tests/integration/x/vm/test_call_evm.go +++ b/tests/integration/x/vm/test_call_evm.go @@ -9,6 +9,7 @@ import ( testconstants "github.com/cosmos/evm/testutil/constants" utiltx "github.com/cosmos/evm/testutil/tx" "github.com/cosmos/evm/x/erc20/types" + "github.com/cosmos/evm/x/vm/statedb" evmtypes "github.com/cosmos/evm/x/vm/types" storetypes "cosmossdk.io/store/types" @@ -17,19 +18,32 @@ import ( func (s *KeeperTestSuite) TestCallEVM() { wcosmosEVMContract := common.HexToAddress(testconstants.WEVMOSContractMainnet) testCases := []struct { - name string - method string - expPass bool + name string + method string + stateDB *statedb.StateDB + expPass bool + expError string }{ { "unknown method", "", + nil, false, + "", }, { "pass", "balanceOf", + nil, true, + "", + }, + { + "fail with nil statedb", + "balanceOf", + nil, + false, + "stateDB cannot be nil", }, } for _, tc := range testCases { @@ -37,12 +51,23 @@ func (s *KeeperTestSuite) TestCallEVM() { erc20 := contracts.ERC20MinterBurnerDecimalsContract.ABI account := utiltx.GenerateAddress() - res, err := s.Network.App.GetEVMKeeper().CallEVM(s.Network.GetContext(), erc20, types.ModuleAddress, wcosmosEVMContract, false, nil, tc.method, account) + + var stateDB *statedb.StateDB + if tc.stateDB == nil && tc.name != "fail with nil statedb" { + stateDB = statedb.New(s.Network.GetContext(), s.Network.App.GetEVMKeeper(), statedb.NewEmptyTxConfig()) + } else { + stateDB = tc.stateDB + } + + res, err := s.Network.App.GetEVMKeeper().CallEVM(s.Network.GetContext(), stateDB, erc20, types.ModuleAddress, wcosmosEVMContract, false, false, nil, tc.method, account) if tc.expPass { s.Require().IsTypef(&evmtypes.MsgEthereumTxResponse{}, res, tc.name) s.Require().NoError(err) } else { s.Require().Error(err) + if tc.expError != "" { + s.Require().Contains(err.Error(), tc.expError) + } } } } @@ -55,64 +80,88 @@ func (s *KeeperTestSuite) TestCallEVMWithData() { from common.Address malleate func() []byte deploy bool + useNilDB bool expPass bool + expError string }{ { - "pass with unknown method", - types.ModuleAddress, - func() []byte { + name: "pass with unknown method", + from: types.ModuleAddress, + malleate: func() []byte { account := utiltx.GenerateAddress() data, _ := erc20.Pack("", account) return data }, - false, - true, + deploy: false, + useNilDB: false, + expPass: true, + expError: "", }, { - "pass", - types.ModuleAddress, - func() []byte { + name: "pass", + from: types.ModuleAddress, + malleate: func() []byte { account := utiltx.GenerateAddress() data, _ := erc20.Pack("balanceOf", account) return data }, - false, - true, + deploy: false, + useNilDB: false, + expPass: true, + expError: "", }, { - "pass with empty data", - types.ModuleAddress, - func() []byte { + name: "pass with empty data", + from: types.ModuleAddress, + malleate: func() []byte { return []byte{} }, - false, - true, + deploy: false, + useNilDB: false, + expPass: true, + expError: "", }, - { - "fail empty sender", - common.Address{}, - func() []byte { + name: "fail empty sender", + from: common.Address{}, + malleate: func() []byte { return []byte{} }, - false, - false, + deploy: false, + useNilDB: false, + expPass: false, + expError: "", }, { - "deploy", - types.ModuleAddress, - func() []byte { + name: "fail with nil statedb", + from: types.ModuleAddress, + malleate: func() []byte { + account := utiltx.GenerateAddress() + data, _ := erc20.Pack("balanceOf", account) + return data + }, + deploy: false, + useNilDB: true, + expPass: false, + expError: "stateDB cannot be nil", + }, + { + name: "deploy", + from: types.ModuleAddress, + malleate: func() []byte { ctorArgs, _ := contracts.ERC20MinterBurnerDecimalsContract.ABI.Pack("", "test", "test", uint8(18)) data := append(contracts.ERC20MinterBurnerDecimalsContract.Bin, ctorArgs...) //nolint:gocritic return data }, - true, - true, + deploy: true, + useNilDB: false, + expPass: true, + expError: "", }, { - "fail deploy", - types.ModuleAddress, - func() []byte { + name: "fail deploy", + from: types.ModuleAddress, + malleate: func() []byte { params := s.Network.App.GetEVMKeeper().GetParams(s.Network.GetContext()) params.AccessControl.Create = evmtypes.AccessControlType{ AccessType: evmtypes.AccessTypeRestricted, @@ -122,8 +171,23 @@ func (s *KeeperTestSuite) TestCallEVMWithData() { data := append(contracts.ERC20MinterBurnerDecimalsContract.Bin, ctorArgs...) //nolint:gocritic return data }, - true, - false, + deploy: true, + useNilDB: false, + expPass: false, + expError: "", + }, + { + name: "fail deploy with nil statedb", + from: types.ModuleAddress, + malleate: func() []byte { + ctorArgs, _ := contracts.ERC20MinterBurnerDecimalsContract.ABI.Pack("", "test", "test", uint8(18)) + data := append(contracts.ERC20MinterBurnerDecimalsContract.Bin, ctorArgs...) //nolint:gocritic + return data + }, + deploy: true, + useNilDB: true, + expPass: false, + expError: "stateDB cannot be nil", }, } @@ -135,10 +199,15 @@ func (s *KeeperTestSuite) TestCallEVMWithData() { var res *evmtypes.MsgEthereumTxResponse var err error + var stateDB *statedb.StateDB + if !tc.useNilDB { + stateDB = statedb.New(s.Network.GetContext(), s.Network.App.GetEVMKeeper(), statedb.NewEmptyTxConfig()) + } + if tc.deploy { - res, err = s.Network.App.GetEVMKeeper().CallEVMWithData(s.Network.GetContext(), tc.from, nil, data, true, nil) + res, err = s.Network.App.GetEVMKeeper().CallEVMWithData(s.Network.GetContext(), stateDB, tc.from, nil, data, true, false, nil) } else { - res, err = s.Network.App.GetEVMKeeper().CallEVMWithData(s.Network.GetContext(), tc.from, &wcosmosEVMContract, data, false, nil) + res, err = s.Network.App.GetEVMKeeper().CallEVMWithData(s.Network.GetContext(), stateDB, tc.from, &wcosmosEVMContract, data, false, false, nil) } if tc.expPass { @@ -146,6 +215,9 @@ func (s *KeeperTestSuite) TestCallEVMWithData() { s.Require().NoError(err) } else { s.Require().Error(err) + if tc.expError != "" { + s.Require().Contains(err.Error(), tc.expError) + } } }) } @@ -155,8 +227,9 @@ func (s *KeeperTestSuite) TestCallEVMWithData() { // F-2026-18824. Like DerivedEVMCallWithData, this function returned on // res.Failed() before reaching ctx.GasMeter().ConsumeGas, so a reverting call // performed real EVM work for free at the Cosmos meter. Unlike the derived path -// it needs no clamp: msg.GasLimit here is the hardcoded config.DefaultGasCap, so -// res.GasUsed is already bounded and the caller cannot inflate it. +// it needs no clamp: msg.GasLimit here is config.DefaultGasCap, which a +// caller-supplied gasCap can only narrow (F-2026-18818), so res.GasUsed is +// already bounded and the caller cannot inflate it. func (s *KeeperTestSuite) TestCallEVMWithDataFailedCallChargesParentGas() { s.SetupTest() @@ -165,9 +238,12 @@ func (s *KeeperTestSuite) TestCallEVMWithDataFailedCallChargesParentGas() { keeper := s.Network.App.GetEVMKeeper() burner := s.deployRawCode(ctx, gasBurnerRevertCode) + // v0.6.0: the caller supplies the StateDB. Build it after deployRawCode so it + // sees the burner's code. + stateDB := statedb.New(ctx, keeper, statedb.NewEmptyTxConfig()) gasBefore := ctx.GasMeter().GasConsumed() - res, err := keeper.CallEVMWithData(ctx, from, &burner, nil, true, nil) + res, err := keeper.CallEVMWithData(ctx, stateDB, from, &burner, nil, true, false, nil) s.Require().Error(err, "the burner reverts, so the call must surface an error") s.Require().NotNil(res) s.Require().True(res.Failed()) @@ -190,9 +266,12 @@ func (s *KeeperTestSuite) TestCallEVMWithDataSuccessChargesGasUsed() { keeper := s.Network.App.GetEVMKeeper() burner := s.deployRawCode(ctx, gasBurnerSuccessCode) + // v0.6.0: the caller supplies the StateDB. Build it after deployRawCode so it + // sees the burner's code. + stateDB := statedb.New(ctx, keeper, statedb.NewEmptyTxConfig()) gasBefore := ctx.GasMeter().GasConsumed() - res, err := keeper.CallEVMWithData(ctx, from, &burner, nil, true, nil) + res, err := keeper.CallEVMWithData(ctx, stateDB, from, &burner, nil, true, false, nil) s.Require().NoError(err) s.Require().False(res.Failed()) s.Require().Greater(res.GasUsed, uint64(2_000_000)) diff --git a/tests/integration/x/vm/test_call_evm_gascap.go b/tests/integration/x/vm/test_call_evm_gascap.go index 16e9c9358..2141e8359 100644 --- a/tests/integration/x/vm/test_call_evm_gascap.go +++ b/tests/integration/x/vm/test_call_evm_gascap.go @@ -10,6 +10,7 @@ import ( testconstants "github.com/cosmos/evm/testutil/constants" utiltx "github.com/cosmos/evm/testutil/tx" "github.com/cosmos/evm/x/erc20/types" + "github.com/cosmos/evm/x/vm/statedb" ) // TestCallEVMWithDataHonoursGasCap is the guard for F-2026-18818. @@ -31,8 +32,11 @@ func (s *KeeperTestSuite) TestCallEVMWithDataHonoursGasCap() { callWithCap := func(gasCap *big.Int) (uint64, error) { s.SetupTest() + ctx := s.Network.GetContext() + // v0.6.0: the caller supplies the StateDB. + stateDB := statedb.New(ctx, s.Network.App.GetEVMKeeper(), statedb.NewEmptyTxConfig()) res, err := s.Network.App.GetEVMKeeper().CallEVMWithData( - s.Network.GetContext(), types.ModuleAddress, &contractAddr, data, false, gasCap, + ctx, stateDB, types.ModuleAddress, &contractAddr, data, false, false, gasCap, ) if res == nil { return 0, err diff --git a/tests/integration/x/vm/test_commit_idempotency.go b/tests/integration/x/vm/test_commit_idempotency.go new file mode 100644 index 000000000..42cf358b1 --- /dev/null +++ b/tests/integration/x/vm/test_commit_idempotency.go @@ -0,0 +1,235 @@ +package vm + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/holiman/uint256" + + vmkeeper "github.com/cosmos/evm/x/vm/keeper" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// TestCommitIdempotency verifies that calling FlushToCacheCtx multiple times +// without any state changes produces identical results across all branches in commitWithCtx: +// - DeleteAccount (self-destructed) +// - SetCode/DeleteCode (code operations) +// - SetAccount (account updates) +// - SetState/DeleteState (storage operations) +func (s *KeeperTestSuite) TestCommitIdempotency() { + s.SetupTest() + evmKeeper := s.Network.App.GetEVMKeeper() + + // Setup test accounts + addr1 := common.BytesToAddress([]byte("addr1")) + addr2 := common.BytesToAddress([]byte("addr2")) + addr3 := common.BytesToAddress([]byte("addr3")) + addr4 := common.BytesToAddress([]byte("addr4")) + + // Test data + code := []byte{0x60, 0x80, 0x60, 0x40, 0x52} // sample bytecode + emptyCodeHash := crypto.Keccak256Hash(nil) + storageKey1 := common.HexToHash("0x1") + storageKey2 := common.HexToHash("0x2") + + // Setup state to cover all branches + db := s.StateDB() + cacheCtx, err := db.GetCacheContext() + s.Require().NoError(err) + + // addr1: Account with code and storage (tests SetCode + SetState) + db.CreateAccount(addr1) + code1 := []byte{0x60, 0x42} + db.SetCode(addr1, code1) + db.SetState(addr1, storageKey1, common.HexToHash("0x456")) + db.AddBalance(addr1, uint256ToInt(big.NewInt(1000)), 0) + + // addr2: Account with empty/deleted code (tests DeleteCode) + db.CreateAccount(addr2) + db.SetCode(addr2, nil) + db.AddBalance(addr2, uint256ToInt(big.NewInt(2000)), 0) + + // addr3: Self-destructed account (tests DeleteAccount) + db.CreateAccount(addr3) + db.SetCode(addr3, code) + db.SelfDestruct(addr3) + + // addr4: Account with storage deleted (tests DeleteState) + db.CreateAccount(addr4) + db.SetState(addr4, storageKey1, common.Hash{}) // deleted storage + db.SetState(addr4, storageKey2, common.HexToHash("0x789")) + db.AddBalance(addr4, uint256ToInt(big.NewInt(4000)), 0) + + // Commit once to persist state + err = db.FlushToCacheCtx() + s.Require().NoError(err) + + // Capture state after first commit + snapshot1 := captureState(cacheCtx, evmKeeper, []common.Address{addr1, addr2, addr3, addr4}) + + err = db.FlushToCacheCtx() + s.Require().NoError(err) + + snapshot2 := captureState(cacheCtx, evmKeeper, []common.Address{addr1, addr2, addr3, addr4}) + + err = db.FlushToCacheCtx() + s.Require().NoError(err) + + snapshot3 := captureState(cacheCtx, evmKeeper, []common.Address{addr1, addr2, addr3, addr4}) + + // All snapshots should be identical + s.Require().Equal(snapshot1, snapshot2, "First and second commit should produce identical state") + s.Require().Equal(snapshot2, snapshot3, "Second and third commit should produce identical state") + + // Verify specific invariants + s.Require().Equal(code1, evmKeeper.GetCode(cacheCtx, crypto.Keccak256Hash(code1))) + s.Require().Empty(evmKeeper.GetCode(cacheCtx, emptyCodeHash)) + s.Require().Nil(evmKeeper.GetAccount(cacheCtx, addr3)) + s.Require().Equal(common.Hash{}, evmKeeper.GetState(cacheCtx, addr4, storageKey1)) +} + +// TestCommitIdempotencyWithStorage tests idempotency of storage operations +func (s *KeeperTestSuite) TestCommitIdempotencyWithStorage() { + s.SetupTest() + evmKeeper := s.Network.App.GetEVMKeeper() + + addr := common.BytesToAddress([]byte("testaddr")) + storageKey := common.HexToHash("0x1") + targetValue := common.HexToHash("0x222") + + // Setup: Create account and set storage + db := s.StateDB() + cacheCtx, err := db.GetCacheContext() + s.Require().NoError(err) + db.CreateAccount(addr) + db.SetState(addr, storageKey, targetValue) + err = db.FlushToCacheCtx() + s.Require().NoError(err) + + snapshot1 := evmKeeper.GetState(cacheCtx, addr, storageKey) + + // Multiple commits without changes should be idempotent + err = db.FlushToCacheCtx() + s.Require().NoError(err) + snapshot2 := evmKeeper.GetState(cacheCtx, addr, storageKey) + + err = db.FlushToCacheCtx() + s.Require().NoError(err) + snapshot3 := evmKeeper.GetState(cacheCtx, addr, storageKey) + + s.Require().Equal(snapshot1, snapshot2) + s.Require().Equal(snapshot2, snapshot3) + s.Require().Equal(targetValue, snapshot3) +} + +// TestCommitIdempotencyWithCodeDeletion tests idempotency of code deletion +func (s *KeeperTestSuite) TestCommitIdempotencyWithCodeDeletion() { + s.SetupTest() + evmKeeper := s.Network.App.GetEVMKeeper() + + addr := common.BytesToAddress([]byte("testaddr")) + + // Setup: Create account and delete code + db := s.StateDB() + cacheCtx, err := db.GetCacheContext() + s.Require().NoError(err) + db.CreateAccount(addr) + db.SetCode(addr, nil) + err = db.FlushToCacheCtx() + s.Require().NoError(err) + + codeHash1 := evmKeeper.GetCodeHash(cacheCtx, addr) + + // Multiple commits without changes should be idempotent + err = db.FlushToCacheCtx() + s.Require().NoError(err) + codeHash2 := evmKeeper.GetCodeHash(cacheCtx, addr) + + err = db.FlushToCacheCtx() + s.Require().NoError(err) + codeHash3 := evmKeeper.GetCodeHash(cacheCtx, addr) + + s.Require().Equal(codeHash1, codeHash2) + s.Require().Equal(codeHash2, codeHash3) +} + +// TestCommitIdempotencyWithSelfDestruct tests idempotency of account deletion +func (s *KeeperTestSuite) TestCommitIdempotencyWithSelfDestruct() { + s.SetupTest() + evmKeeper := s.Network.App.GetEVMKeeper() + + addr := common.BytesToAddress([]byte("testaddr")) + + // Setup: Create account and self-destruct + db := s.StateDB() + cacheCtx, err := db.GetCacheContext() + s.Require().NoError(err) + db.CreateAccount(addr) + db.SelfDestruct(addr) + err = db.FlushToCacheCtx() + s.Require().NoError(err) + + account1 := evmKeeper.GetAccount(cacheCtx, addr) + + // Multiple commits without changes should be idempotent + err = db.FlushToCacheCtx() + s.Require().NoError(err) + account2 := evmKeeper.GetAccount(cacheCtx, addr) + + err = db.FlushToCacheCtx() + s.Require().NoError(err) + account3 := evmKeeper.GetAccount(cacheCtx, addr) + + s.Require().Nil(account1) + s.Require().Nil(account2) + s.Require().Nil(account3) +} + +// accountState captures relevant account state for comparison +type accountState struct { + Exists bool + Balance *big.Int + Nonce uint64 + CodeHash common.Hash + StorageState map[common.Hash]common.Hash +} + +// stateSnapshot captures the state of multiple accounts +type stateSnapshot map[common.Address]accountState + +// captureState reads and captures the current state of given addresses +func captureState(ctx sdk.Context, evmKeeper *vmkeeper.Keeper, addrs []common.Address) stateSnapshot { + snapshot := make(stateSnapshot) + + for _, addr := range addrs { + account := evmKeeper.GetAccount(ctx, addr) + if account == nil { + snapshot[addr] = accountState{Exists: false} + continue + } + + storage := make(map[common.Hash]common.Hash) + // Capture all storage keys for this address + evmKeeper.ForEachStorage(ctx, addr, func(key, value common.Hash) bool { + storage[key] = value + return true + }) + + snapshot[addr] = accountState{ + Exists: true, + Balance: account.Balance.ToBig(), + Nonce: account.Nonce, + CodeHash: common.BytesToHash(account.CodeHash), + StorageState: storage, + } + } + + return snapshot +} + +func uint256ToInt(i *big.Int) *uint256.Int { + u, _ := uint256.FromBig(i) + return u +} diff --git a/tests/integration/x/vm/test_state_transition.go b/tests/integration/x/vm/test_state_transition.go index 608b80b9b..a1e53b053 100644 --- a/tests/integration/x/vm/test_state_transition.go +++ b/tests/integration/x/vm/test_state_transition.go @@ -26,6 +26,7 @@ import ( utiltx "github.com/cosmos/evm/testutil/tx" feemarkettypes "github.com/cosmos/evm/x/feemarket/types" "github.com/cosmos/evm/x/vm/keeper" + "github.com/cosmos/evm/x/vm/statedb" "github.com/cosmos/evm/x/vm/types" sdkmath "cosmossdk.io/math" @@ -451,11 +452,16 @@ func (s *KeeperTestSuite) TestRefundGas() { expGasRefund: params.TxGas / params.RefundQuotient, }, { + // A negative gas price must make RefundGas error. RefundGas computes + // remaining = leftoverGas * gasPrice, so leftoverGas must be > 0 for the + // negative price to surface (with leftoverGas 0 the product is 0 and no + // error is raised). leftoverGas == TxGas == transactionGas, so gasUsed is + // 0 and the expected gas refund is 0. name: "invalid GasPrice in message", - leftoverGas: 0, + leftoverGas: params.TxGas, refundQuotient: params.RefundQuotient, noError: false, - expGasRefund: params.TxGas / params.RefundQuotient, + expGasRefund: 0, gasPrice: big.NewInt(-100), }, } @@ -804,31 +810,68 @@ func (s *KeeperTestSuite) TestApplyMessage() { defer func() { s.EnableFeemarket = false }() s.SetupTest() - // Generate a transfer tx message - sender := s.Keyring.GetKey(0) - recipient := s.Keyring.GetAddr(1) - transferArgs := types.EvmTxArgs{ - To: &recipient, - Amount: big.NewInt(100), + testCases := []struct { + name string + useNilDB bool + expPass bool + expError string + }{ + { + name: "success", + useNilDB: false, + expPass: true, + expError: "", + }, + { + name: "fail with nil statedb", + useNilDB: true, + expPass: false, + expError: "stateDB cannot be nil", + }, } - coreMsg, err := s.Factory.GenerateGethCoreMsg( - sender.Priv, - transferArgs, - ) - s.Require().NoError(err) - tracer := s.Network.App.GetEVMKeeper().Tracer( - s.Network.GetContext(), - *coreMsg, - types.GetEthChainConfig(), - ) - res, err := s.Network.App.GetEVMKeeper().ApplyMessage(s.Network.GetContext(), *coreMsg, tracer, true, false) - s.Require().NoError(err) - s.Require().False(res.Failed()) + for _, tc := range testCases { + s.Run(fmt.Sprintf("Case %s", tc.name), func() { + // Generate a transfer tx message + sender := s.Keyring.GetKey(0) + recipient := s.Keyring.GetAddr(1) + transferArgs := types.EvmTxArgs{ + To: &recipient, + Amount: big.NewInt(100), + } + coreMsg, err := s.Factory.GenerateGethCoreMsg( + sender.Priv, + transferArgs, + ) + s.Require().NoError(err) - // Compare gas to a transfer tx gas - expectedGasUsed := params.TxGas - s.Require().Equal(expectedGasUsed, res.GasUsed) + tracer := s.Network.App.GetEVMKeeper().Tracer( + s.Network.GetContext(), + *coreMsg, + types.GetEthChainConfig(), + ) + + var stateDB *statedb.StateDB + if !tc.useNilDB { + stateDB = statedb.New(s.Network.GetContext(), s.Network.App.GetEVMKeeper(), statedb.NewEmptyTxConfig()) + } + + res, err := s.Network.App.GetEVMKeeper().ApplyMessage(s.Network.GetContext(), stateDB, *coreMsg, tracer, true, false, false) + + if tc.expPass { + s.Require().NoError(err) + s.Require().False(res.Failed()) + // Compare gas to a transfer tx gas + expectedGasUsed := params.TxGas + s.Require().Equal(expectedGasUsed, res.GasUsed) + } else { + s.Require().Error(err) + if tc.expError != "" { + s.Require().Contains(err.Error(), tc.expError) + } + } + }) + } } func (s *KeeperTestSuite) TestApplyMessageWithConfig() { @@ -853,6 +896,7 @@ func (s *KeeperTestSuite) TestApplyMessageWithConfig() { getEVMParams func() types.Params getFeeMarketParams func() feemarkettypes.Params overrides *rpctypes.StateOverride + useNilDB bool expErr bool expVMErr bool expectedGasUsed uint64 @@ -873,6 +917,7 @@ func (s *KeeperTestSuite) TestApplyMessageWithConfig() { getEVMParams: types.DefaultParams, getFeeMarketParams: feemarkettypes.DefaultParams, overrides: nil, + useNilDB: false, expErr: false, expVMErr: false, expectedGasUsed: params.TxGas, @@ -1095,11 +1140,33 @@ func (s *KeeperTestSuite) TestApplyMessageWithConfig() { return params }, overrides: nil, + useNilDB: false, expErr: true, expVMErr: false, expectedGasUsed: 0, postCheck: nil, }, + { + name: "fail with nil statedb", + getMessage: func() core.Message { + sender := s.Keyring.GetKey(0) + recipient := s.Keyring.GetAddr(1) + msg, err := s.Factory.GenerateGethCoreMsg(sender.Priv, types.EvmTxArgs{ + To: &recipient, + Amount: big.NewInt(100), + }) + s.Require().NoError(err) + return *msg + }, + getEVMParams: types.DefaultParams, + getFeeMarketParams: feemarkettypes.DefaultParams, + overrides: nil, + useNilDB: true, + expErr: true, + expVMErr: false, + expectedGasUsed: 0, + postCheck: nil, + }, } for _, tc := range testCases { @@ -1129,16 +1196,12 @@ func (s *KeeperTestSuite) TestApplyMessageWithConfig() { ) s.Require().NoError(err) - res, err := s.Network.App.GetEVMKeeper().ApplyMessageWithConfig( - s.Network.GetContext(), - msg, - nil, - true, - config, - txConfig, - false, - tc.overrides, - ) + var stateDB *statedb.StateDB + if !tc.useNilDB { + stateDB = statedb.New(s.Network.GetContext(), s.Network.App.GetEVMKeeper(), statedb.NewEmptyTxConfig()) + } + + res, err := s.Network.App.GetEVMKeeper().ApplyMessageWithConfig(s.Network.GetContext(), stateDB, msg, nil, true, false, config, txConfig, false, tc.overrides) if tc.expErr { s.Require().Error(err) @@ -1226,13 +1289,9 @@ func (s *KeeperTestSuite) TestApplyMessageWithNegativeAmount() { ctx := s.Network.GetContext() balance0Before := s.Network.App.GetBankKeeper().GetBalance(ctx, s.Keyring.GetAccAddr(0), "aatom") balance1Before := s.Network.App.GetBankKeeper().GetBalance(ctx, s.Keyring.GetAccAddr(1), "aatom") - res, err := s.Network.App.GetEVMKeeper().ApplyMessage( - s.Network.GetContext(), - *coreMsg, - tracer, - true, - false, - ) + stateDB := statedb.New(s.Network.GetContext(), s.Network.App.GetEVMKeeper(), statedb.NewEmptyTxConfig()) + + res, err := s.Network.App.GetEVMKeeper().ApplyMessage(s.Network.GetContext(), stateDB, *coreMsg, tracer, true, false, false) s.Require().Nil(res) s.Require().Error(err) diff --git a/tests/systemtests/Makefile b/tests/systemtests/Makefile index 05b3d2e49..b0aa5b041 100644 --- a/tests/systemtests/Makefile +++ b/tests/systemtests/Makefile @@ -9,4 +9,4 @@ test: go test -failfast -timeout=30m -p=1 -mod=readonly -tags='system_test' -v ./... -run TestExceptions --wait-time=$(WAIT_TIME) --block-time=8s --binary evmd --chain-id local-4221 go test -failfast -timeout=30m -p=1 -mod=readonly -tags='system_test' -v ./... -run TestEIP7702 --wait-time=$(WAIT_TIME) --block-time=5s --binary evmd --chain-id local-4221 go test -failfast -timeout=30m -p=1 -mod=readonly -tags='system_test' -v ./... -run TestEIP712 --wait-time=$(WAIT_TIME) --block-time=5s --binary evmd --chain-id local-4221 - go test -failfast -timeout=30m -p=1 -mod=readonly -tags='system_test' -v ./... -run 'TestUpgrade|TestEth' --wait-time=$(WAIT_TIME) --block-time=5s --binary evmd --chain-id local-4221 + go test -failfast -timeout=30m -p=1 -mod=readonly -tags='system_test' -v ./... -run 'TestChainUpgrade|TestEth' --wait-time=$(WAIT_TIME) --block-time=5s --binary evmd --chain-id local-4221 diff --git a/tests/systemtests/go.mod b/tests/systemtests/go.mod index 34c36d65a..5ab649fb8 100644 --- a/tests/systemtests/go.mod +++ b/tests/systemtests/go.mod @@ -5,8 +5,8 @@ go 1.24.4 require ( cosmossdk.io/math v1.5.3 cosmossdk.io/systemtests v1.4.0 - github.com/cometbft/cometbft v0.38.19 - github.com/cosmos/cosmos-sdk v0.53.5-0.20251030204916-768cb210885c + github.com/cometbft/cometbft v0.38.21 + github.com/cosmos/cosmos-sdk v0.53.6 github.com/cosmos/evm v0.5.0-rc.0 github.com/ethereum/go-ethereum v1.15.11 github.com/holiman/uint256 v1.3.2 @@ -61,7 +61,7 @@ require ( github.com/cosmos/iavl v1.2.6 // indirect github.com/cosmos/ibc-go/v10 v10.3.1-0.20250909102629-ed3b125c7b6f // indirect github.com/cosmos/ics23/go v0.11.0 // indirect - github.com/cosmos/ledger-cosmos-go v0.16.0 // indirect + github.com/cosmos/ledger-cosmos-go v1.0.0 // indirect github.com/crate-crypto/go-eth-kzg v1.3.0 // indirect github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect github.com/creachadair/tomledit v0.0.29 // indirect diff --git a/tests/systemtests/go.sum b/tests/systemtests/go.sum index f53fd3b2c..568e7f412 100644 --- a/tests/systemtests/go.sum +++ b/tests/systemtests/go.sum @@ -178,8 +178,8 @@ github.com/cockroachdb/redact v1.1.6/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZ github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb h1:3bCgBvB8PbJVMX1ouCcSIxvsqKPYM7gs72o0zC76n9g= github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= -github.com/cometbft/cometbft v0.38.19 h1:vNdtCkvhuwUlrcLPAyigV7lQpmmo+tAq8CsB8gZjEYw= -github.com/cometbft/cometbft v0.38.19/go.mod h1:UCu8dlHqvkAsmAFmWDRWNZJPlu6ya2fTWZlDrWsivwo= +github.com/cometbft/cometbft v0.38.21 h1:qcIJSH9LiwU5s6ZgKR5eRbsLNucbubfraDs5bzgjtOI= +github.com/cometbft/cometbft v0.38.21/go.mod h1:UCu8dlHqvkAsmAFmWDRWNZJPlu6ya2fTWZlDrWsivwo= github.com/cometbft/cometbft-db v1.0.4 h1:cezb8yx/ZWcF124wqUtAFjAuDksS1y1yXedvtprUFxs= github.com/cometbft/cometbft-db v1.0.4/go.mod h1:M+BtHAGU2XLrpUxo3Nn1nOCcnVCiLM9yx5OuT0u5SCA= github.com/consensys/gnark-crypto v0.18.0 h1:vIye/FqI50VeAr0B3dx+YjeIvmc3LWz4yEfbWBpTUf0= @@ -196,8 +196,8 @@ github.com/cosmos/cosmos-db v1.1.3 h1:7QNT77+vkefostcKkhrzDK9uoIEryzFrU9eoMeaQOP github.com/cosmos/cosmos-db v1.1.3/go.mod h1:kN+wGsnwUJZYn8Sy5Q2O0vCYA99MJllkKASbs6Unb9U= github.com/cosmos/cosmos-proto v1.0.0-beta.5 h1:eNcayDLpip+zVLRLYafhzLvQlSmyab+RC5W7ZfmxJLA= github.com/cosmos/cosmos-proto v1.0.0-beta.5/go.mod h1:hQGLpiIUloJBMdQMMWb/4wRApmI9hjHH05nefC0Ojec= -github.com/cosmos/cosmos-sdk v0.53.5-0.20251030204916-768cb210885c h1:HMVLvm0q3ahGvsyExkSCBcmvcdItMpTxAh4jllL4rJ4= -github.com/cosmos/cosmos-sdk v0.53.5-0.20251030204916-768cb210885c/go.mod h1:nifazrMGFjpmOuaVIZBQ8akQc160imzySYFEA8A7tus= +github.com/cosmos/cosmos-sdk v0.53.6 h1:aJeInld7rbsHtH1qLHu2aZJF9t40mGlqp3ylBLDT0HI= +github.com/cosmos/cosmos-sdk v0.53.6/go.mod h1:N6YuprhAabInbT3YGumGDKONbvPX5dNro7RjHvkQoKE= github.com/cosmos/go-bip39 v1.0.0 h1:pcomnQdrdH22njcAatO0yWojsUnCO3y2tNoV1cb6hHY= github.com/cosmos/go-bip39 v1.0.0/go.mod h1:RNJv0H/pOIVgxw6KS7QeX2a0Uo0aKUlfhZ4xuwvCdJw= github.com/cosmos/go-ethereum v1.16.2-cosmos-1 h1:QIaIS6HIdPSBdTvpFhxswhMLUJgcr4irbd2o9ZKldAI= @@ -213,8 +213,8 @@ github.com/cosmos/ibc-go/v10 v10.3.1-0.20250909102629-ed3b125c7b6f h1:I5t5Tuewh6 github.com/cosmos/ibc-go/v10 v10.3.1-0.20250909102629-ed3b125c7b6f/go.mod h1:a74pAPUSJ7NewvmvELU74hUClJhwnmm5MGbEaiTw/kE= github.com/cosmos/ics23/go v0.11.0 h1:jk5skjT0TqX5e5QJbEnwXIS2yI2vnmLOgpQPeM5RtnU= github.com/cosmos/ics23/go v0.11.0/go.mod h1:A8OjxPE67hHST4Icw94hOxxFEJMBG031xIGF/JHNIY0= -github.com/cosmos/ledger-cosmos-go v0.16.0 h1:YKlWPG9NnGZIEUb2bEfZ6zhON1CHlNTg0QKRRGcNEd0= -github.com/cosmos/ledger-cosmos-go v0.16.0/go.mod h1:WrM2xEa8koYoH2DgeIuZXNarF7FGuZl3mrIOnp3Dp0o= +github.com/cosmos/ledger-cosmos-go v1.0.0 h1:jNKW89nPf0vR0EkjHG8Zz16h6p3zqwYEOxlHArwgYtw= +github.com/cosmos/ledger-cosmos-go v1.0.0/go.mod h1:mGaw2wDOf+Z6SfRJsMGxU9DIrBa4du0MAiPlpPhLAOE= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= diff --git a/tests/systemtests/upgrade_test.go b/tests/systemtests/upgrade_test.go index 9ae700e88..3cd6ac7c6 100644 --- a/tests/systemtests/upgrade_test.go +++ b/tests/systemtests/upgrade_test.go @@ -18,7 +18,7 @@ import ( const ( upgradeHeight int64 = 22 - upgradeName = "v0.4.0-to-v0.5.0" // must match UpgradeName in evmd/upgrades.go + upgradeName = "v0.5.0-to-v0.6.0" // must match UpgradeName in evmd/upgrades.go ) func TestChainUpgrade(t *testing.T) { @@ -31,7 +31,7 @@ func TestChainUpgrade(t *testing.T) { currentBranchBinary := systest.Sut.ExecBinary() currentInitializer := systest.Sut.TestnetInitializer() - legacyBinary := systest.WorkDir + "/binaries/v0.4/evmd" + legacyBinary := systest.WorkDir + "/binaries/v0.5/evmd" systest.Sut.SetExecBinary(legacyBinary) systest.Sut.SetTestnetInitializer(systest.InitializerWithBinary(legacyBinary, systest.Sut)) systest.Sut.SetupChain() @@ -76,7 +76,11 @@ func TestChainUpgrade(t *testing.T) { }(i) } - systest.Sut.AwaitBlockHeight(t, upgradeHeight-1, 60*time.Second) + // Allow enough time to reach the pre-upgrade height. The systemtests block + // time is --block-time=5s (timeout_commit 4.5s), so reaching block + // upgradeHeight-1 from genesis takes ~(upgradeHeight-1)*5s ≈ 100s — well over + // the original hardcoded 60s, which timed out on slower CI runners. + systest.Sut.AwaitBlockHeight(t, upgradeHeight-1, 180*time.Second) t.Logf("current_height: %d\n", systest.Sut.CurrentHeight()) raw = cli.CustomQuery("q", "gov", "proposal", proposalID) proposalStatus := gjson.Get(raw, "proposal.status").String() diff --git a/testutil/tx/eip712.go b/testutil/tx/eip712.go index 89b4a57ae..f13e89f5f 100644 --- a/testutil/tx/eip712.go +++ b/testutil/tx/eip712.go @@ -73,11 +73,10 @@ func PrepareEIP712CosmosTx( return nil, err } - // using nolint:all because the staticcheck nolint is not working as expected - fee := legacytx.NewStdFee(txArgs.Gas, txArgs.Fees) //nolint:all + fee := legacytx.NewStdFee(txArgs.Gas, txArgs.Fees) //nolint:staticcheck // check against deprecated type msgs := txArgs.Msgs - data := legacytx.StdSignBytes(ctx.ChainID(), accNumber, nonce, 0, fee, msgs, "") + data := legacytx.StdSignBytes(ctx.ChainID(), accNumber, nonce, 0, fee, msgs, "") //nolint:staticcheck // check against deprecated type typedDataArgs := typedDataArgs{ chainID: args.EVMChainID, diff --git a/x/erc20/keeper/convert.go b/x/erc20/keeper/convert.go new file mode 100644 index 000000000..e3e234ade --- /dev/null +++ b/x/erc20/keeper/convert.go @@ -0,0 +1,177 @@ +package keeper + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/hashicorp/go-metrics" + + "github.com/cosmos/evm/contracts" + "github.com/cosmos/evm/x/erc20/types" + "github.com/cosmos/evm/x/vm/statedb" + + "cosmossdk.io/errors" + "cosmossdk.io/math" + + "github.com/cosmos/cosmos-sdk/telemetry" + sdk "github.com/cosmos/cosmos-sdk/types" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" +) + +// ConvertERC20IntoCoinsForNativeToken handles the erc20 conversion for a native erc20 token pair. +// This function is used by both the msg server and precompiles (like ICS20). +// It performs the following operations: +// - validates the token pair and checks if conversion is enabled +// - removes the token pair if the contract is suicided +// - escrows tokens on module account +// - mints coins on bank module +// - sends minted coins to the receiver +// - checks if coin balance increased by amount +// - checks if token balance decreased by amount +// - checks for unexpected `Approval` event in logs +func (k Keeper) ConvertERC20IntoCoinsForNativeToken(ctx sdk.Context, stateDB *statedb.StateDB, contract common.Address, amount math.Int, receiver sdk.AccAddress, sender common.Address, commit bool, callFromPrecompile bool) (*types.MsgConvertERC20Response, error) { + // Validate and get token pair + pair, err := k.MintingEnabled(ctx, receiver, contract.Hex()) + if err != nil { + return nil, err + } + + // Check that this is a native ERC20 token + if !pair.IsNativeERC20() { + if pair.IsNativeCoin() { + return nil, types.ErrNativeConversionDisabled + } + return nil, types.ErrUndefinedOwner + } + + // Remove token pair if contract is suicided + acc := k.evmKeeper.GetAccountWithoutBalance(ctx, pair.GetERC20Contract()) + if acc == nil || !acc.HasCodeHash() { + k.DeleteTokenPair(ctx, pair) + k.Logger(ctx).Debug( + "deleting selfdestructed token pair from state", + "contract", pair.Erc20Address, + ) + // NOTE: return nil error to persist the changes from the deletion + return nil, nil + } + erc20 := contracts.ERC20MinterBurnerDecimalsContract.ABI + erc20Contract := pair.GetERC20Contract() + balanceCoin := k.bankKeeper.GetBalance(ctx, receiver, pair.Denom) + balanceToken := k.BalanceOf(ctx, erc20, erc20Contract, types.ModuleAddress) + if balanceToken == nil { + return nil, errors.Wrap(types.ErrEVMCall, "failed to retrieve balance") + } + + // Escrow tokens on module account + transferData, err := erc20.Pack("transfer", types.ModuleAddress, amount.BigInt()) + if err != nil { + return nil, err + } + + res, err := k.evmKeeper.CallEVMWithData(ctx, stateDB, sender, &erc20Contract, transferData, commit, callFromPrecompile, nil) + if err != nil { + return nil, err + } + + // Check evm call response + var unpackedRet types.ERC20BoolResponse + if len(res.Ret) == 0 { + // if the token does not return a value, check for the transfer event in logs + if err := validateTransferEventExists(res.Logs, erc20Contract); err != nil { + return nil, err + } + } else { + if err := erc20.UnpackIntoInterface(&unpackedRet, "transfer", res.Ret); err != nil { + return nil, err + } + if !unpackedRet.Value { + return nil, errors.Wrap(errortypes.ErrLogic, "failed to execute transfer") + } + } + + // Check for unexpected `Approval` event in logs (F-2026-18822). + // v0.6.0 lifted this body out of msg_server.convertERC20IntoCoinsForNativeToken + // into this shared helper and dropped the guard on the way, even though the + // doc comment above still promises it. Re-applied here, in the same position + // it held before the move, so both the msg server and the ICS20 precompile + // path are covered. + if err := validateApprovalEventDoesNotExist(res.Logs); err != nil { + return nil, err + } + + // Check expected escrow balance after transfer execution + coins := sdk.Coins{sdk.Coin{Denom: pair.Denom, Amount: amount}} + tokens := coins[0].Amount.BigInt() + balanceTokenAfter := k.BalanceOf(ctx, erc20, erc20Contract, types.ModuleAddress) + if balanceTokenAfter == nil { + return nil, errors.Wrap(types.ErrEVMCall, "failed to retrieve balance") + } + + expToken := big.NewInt(0).Add(balanceToken, tokens) + + if r := balanceTokenAfter.Cmp(expToken); r != 0 { + return nil, errors.Wrapf( + types.ErrBalanceInvariance, + "invalid token balance - expected: %v, actual: %v", + expToken, balanceTokenAfter, + ) + } + + // Mint coins + if err := k.bankKeeper.MintCoins(ctx, types.ModuleName, coins); err != nil { + return nil, err + } + + // Send minted coins to the receiver + if err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, receiver, coins); err != nil { + return nil, err + } + + // Check expected receiver balance after transfer + balanceCoinAfter := k.bankKeeper.GetBalance(ctx, receiver, pair.Denom) + expCoin := balanceCoin.Add(coins[0]) + + if ok := balanceCoinAfter.Equal(expCoin); !ok { + return nil, errors.Wrapf( + types.ErrBalanceInvariance, + "invalid coin balance - expected: %v, actual: %v", + expCoin, balanceCoinAfter, + ) + } + + defer func() { + telemetry.IncrCounterWithLabels( + []string{"tx", "msg", "convert", "erc20", "total"}, + 1, + []metrics.Label{ + telemetry.NewLabel("coin", pair.Denom), + }, + ) + + if amount.IsInt64() { + telemetry.IncrCounterWithLabels( + []string{"tx", "msg", "convert", "erc20", "amount", "total"}, + float32(amount.Int64()), + []metrics.Label{ + telemetry.NewLabel("denom", pair.Denom), + }, + ) + } + }() + + ctx.EventManager().EmitEvents( + sdk.Events{ + sdk.NewEvent( + types.EventTypeConvertERC20, + sdk.NewAttribute(sdk.AttributeKeySender, sender.Hex()), + sdk.NewAttribute(types.AttributeKeyReceiver, receiver.String()), + sdk.NewAttribute(sdk.AttributeKeyAmount, amount.String()), + sdk.NewAttribute(types.AttributeKeyCosmosCoin, pair.Denom), + sdk.NewAttribute(types.AttributeKeyERC20Token, contract.Hex()), + ), + }, + ) + + return &types.MsgConvertERC20Response{}, nil +} diff --git a/x/erc20/keeper/evm.go b/x/erc20/keeper/evm.go index 05fdafb3d..1e14d2754 100644 --- a/x/erc20/keeper/evm.go +++ b/x/erc20/keeper/evm.go @@ -10,11 +10,11 @@ import ( "github.com/cosmos/evm/contracts" "github.com/cosmos/evm/utils" "github.com/cosmos/evm/x/erc20/types" + "github.com/cosmos/evm/x/vm/statedb" errorsmod "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" - banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" ) var ( @@ -25,45 +25,6 @@ var ( logApprovalSigHash = crypto.Keccak256Hash(logApprovalSig) ) -// DeployERC20Contract creates and deploys an ERC20 contract on the EVM with the -// erc20 module account as owner. -func (k Keeper) DeployERC20Contract( - ctx sdk.Context, - coinMetadata banktypes.Metadata, -) (common.Address, error) { - decimals := uint8(0) - if len(coinMetadata.DenomUnits) > 0 { - decimalsIdx := len(coinMetadata.DenomUnits) - 1 - decimals = uint8(coinMetadata.DenomUnits[decimalsIdx].Exponent) //#nosec G115 // exponent will not exceed uint8 - } - ctorArgs, err := contracts.ERC20MinterBurnerDecimalsContract.ABI.Pack( - "", - coinMetadata.Name, - coinMetadata.Symbol, - decimals, - ) - if err != nil { - return common.Address{}, errorsmod.Wrapf(types.ErrABIPack, "coin metadata is invalid %s: %s", coinMetadata.Name, err.Error()) - } - - data := make([]byte, len(contracts.ERC20MinterBurnerDecimalsContract.Bin)+len(ctorArgs)) - copy(data[:len(contracts.ERC20MinterBurnerDecimalsContract.Bin)], contracts.ERC20MinterBurnerDecimalsContract.Bin) - copy(data[len(contracts.ERC20MinterBurnerDecimalsContract.Bin):], ctorArgs) - - nonce, err := k.accountKeeper.GetSequence(ctx, types.ModuleAddress.Bytes()) - if err != nil { - return common.Address{}, err - } - - contractAddr := crypto.CreateAddress(types.ModuleAddress, nonce) - _, err = k.evmKeeper.CallEVMWithData(ctx, types.ModuleAddress, nil, data, true, nil) - if err != nil { - return common.Address{}, errorsmod.Wrapf(err, "failed to deploy contract for %s", coinMetadata.Name) - } - - return contractAddr, nil -} - // QueryERC20 returns the data of a deployed ERC20 contract func (k Keeper) QueryERC20( ctx sdk.Context, @@ -84,7 +45,9 @@ func (k Keeper) QueryERC20( } // Decimals - standard uint8, no fallback needed - res, err := k.evmKeeper.CallEVM(ctx, erc20, types.ModuleAddress, contract, false, nil, "decimals") + stateDB := statedb.New(ctx, k.evmKeeper, statedb.NewEmptyTxConfig()) + // Okay to assume we're not calling from a precompile, as queries will just revert state changes. + res, err := k.evmKeeper.CallEVM(ctx, stateDB, erc20, types.ModuleAddress, contract, false, false, nil, "decimals") if err != nil { return types.ERC20Data{}, err } @@ -107,7 +70,9 @@ func (k Keeper) queryERC20String( method string, ) (string, error) { // 1) Call into the EVM - res, err := k.evmKeeper.CallEVM(ctx, erc20, types.ModuleAddress, contract, false, nil, method) + stateDB := statedb.New(ctx, k.evmKeeper, statedb.NewEmptyTxConfig()) + // Okay to assume we're not calling from a precompile, as queries will just revert state changes. + res, err := k.evmKeeper.CallEVM(ctx, stateDB, erc20, types.ModuleAddress, contract, false, false, nil, method) if err != nil { return "", err } @@ -140,7 +105,9 @@ func (k Keeper) BalanceOf( abi abi.ABI, contract, account common.Address, ) *big.Int { - res, err := k.evmKeeper.CallEVM(ctx, abi, types.ModuleAddress, contract, false, nil, "balanceOf", account) + stateDB := statedb.New(ctx, k.evmKeeper, statedb.NewEmptyTxConfig()) + // Okay to assume we're not calling from a precompile, as queries will just revert state changes. + res, err := k.evmKeeper.CallEVM(ctx, stateDB, abi, types.ModuleAddress, contract, false, false, nil, "balanceOf", account) if err != nil { return nil } diff --git a/x/erc20/keeper/ibc_callbacks.go b/x/erc20/keeper/ibc_callbacks.go index 6270ed5ae..af8429a2d 100644 --- a/x/erc20/keeper/ibc_callbacks.go +++ b/x/erc20/keeper/ibc_callbacks.go @@ -134,7 +134,7 @@ func (k Keeper) OnRecvPacket( return channeltypes.NewErrorAcknowledgement(err) } - if err := k.ConvertCoinNativeERC20(ctx, pair, coin.Amount, common.BytesToAddress(recipient.Bytes()), recipient); err != nil { + if err := k.ConvertCoinNativeERC20(ctx, pair, coin.Amount, common.BytesToAddress(recipient.Bytes()), recipient, false); err != nil { return channeltypes.NewErrorAcknowledgement(err) } @@ -239,7 +239,7 @@ func (k Keeper) ConvertCoinToERC20FromPacket(ctx sdk.Context, data transfertypes } // Convert from Coin to ERC20 - if err := k.ConvertCoinNativeERC20(ctx, pair, coin.Amount, common.BytesToAddress(sender), sender); err != nil { + if err := k.ConvertCoinNativeERC20(ctx, pair, coin.Amount, common.BytesToAddress(sender), sender, false); err != nil { // Swallowing the error is deliberate: the refund itself already // succeeded and must not be undone by a failure to re-wrap it into // the ERC20 representation. This is only safe because diff --git a/x/erc20/keeper/keeper.go b/x/erc20/keeper/keeper.go index 981a2de14..f800eba58 100644 --- a/x/erc20/keeper/keeper.go +++ b/x/erc20/keeper/keeper.go @@ -4,7 +4,7 @@ import ( "fmt" "github.com/cosmos/evm/x/erc20/types" - transferkeeper "github.com/cosmos/evm/x/ibc/transfer/keeper" + transferkeeper "github.com/cosmos/ibc-go/v10/modules/apps/transfer/keeper" "cosmossdk.io/core/address" "cosmossdk.io/log" diff --git a/x/erc20/keeper/msg_server.go b/x/erc20/keeper/msg_server.go index ee5eb36fa..8dc07170e 100644 --- a/x/erc20/keeper/msg_server.go +++ b/x/erc20/keeper/msg_server.go @@ -5,15 +5,14 @@ import ( "math/big" "github.com/ethereum/go-ethereum/common" - "github.com/hashicorp/go-metrics" "github.com/cosmos/evm/contracts" "github.com/cosmos/evm/x/erc20/types" + "github.com/cosmos/evm/x/vm/statedb" sdkerrors "cosmossdk.io/errors" "cosmossdk.io/math" - "github.com/cosmos/cosmos-sdk/telemetry" sdk "github.com/cosmos/cosmos-sdk/types" errortypes "github.com/cosmos/cosmos-sdk/types/errors" govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" @@ -29,167 +28,18 @@ func (k Keeper) ConvertERC20( ) (*types.MsgConvertERC20Response, error) { ctx := sdk.UnwrapSDKContext(goCtx) - // Error checked during msg validation - receiver := sdk.MustAccAddressFromBech32(msg.Receiver) - sender := common.HexToAddress(msg.Sender) - - pair, err := k.MintingEnabled(ctx, receiver, msg.ContractAddress) + // Parse message + receiver, err := sdk.AccAddressFromBech32(msg.Receiver) if err != nil { return nil, err } + sender := common.HexToAddress(msg.Sender) + contract := common.HexToAddress(msg.ContractAddress) - // Check ownership and execute conversion - if pair.IsNativeERC20() { - // Remove token pair if contract is suicided - acc := k.evmKeeper.GetAccountWithoutBalance(ctx, pair.GetERC20Contract()) - if acc == nil || !acc.HasCodeHash() { - k.DeleteTokenPair(ctx, pair) - k.Logger(ctx).Debug( - "deleting selfdestructed token pair from state", - "contract", pair.Erc20Address, - ) - // NOTE: return nil error to persist the changes from the deletion - return nil, nil - } - - return k.convertERC20IntoCoinsForNativeToken(ctx, pair, msg, receiver, sender) // case 2.1 - } else if pair.IsNativeCoin() { - return nil, types.ErrNativeConversionDisabled - } - - return nil, types.ErrUndefinedOwner -} - -// convertERC20IntoCoinsForNativeToken handles the erc20 conversion for a native erc20 token -// pair: -// - escrow tokens on module account -// - mint coins on bank module -// - send minted coins to the receiver -// - check if coin balance increased by amount -// - check if token balance decreased by amount -// - check for unexpected `Approval` event in logs -func (k Keeper) convertERC20IntoCoinsForNativeToken( - ctx sdk.Context, - pair types.TokenPair, - msg *types.MsgConvertERC20, - receiver sdk.AccAddress, - sender common.Address, -) (*types.MsgConvertERC20Response, error) { - erc20 := contracts.ERC20MinterBurnerDecimalsContract.ABI - contract := pair.GetERC20Contract() - balanceCoin := k.bankKeeper.GetBalance(ctx, receiver, pair.Denom) - balanceToken := k.BalanceOf(ctx, erc20, contract, types.ModuleAddress) - if balanceToken == nil { - return nil, sdkerrors.Wrap(types.ErrEVMCall, "failed to retrieve balance") - } - - // Escrow tokens on module account - transferData, err := erc20.Pack("transfer", types.ModuleAddress, msg.Amount.BigInt()) - if err != nil { - return nil, err - } - - res, err := k.evmKeeper.CallEVMWithData(ctx, sender, &contract, transferData, true, nil) - if err != nil { - return nil, err - } - - // Check evm call response - var unpackedRet types.ERC20BoolResponse - if len(res.Ret) == 0 { - // if the token does not return a value, check for the transfer event in logs - if err := validateTransferEventExists(res.Logs, contract); err != nil { - return nil, err - } - } else { - if err := erc20.UnpackIntoInterface(&unpackedRet, "transfer", res.Ret); err != nil { - return nil, err - } - if !unpackedRet.Value { - return nil, sdkerrors.Wrap(errortypes.ErrLogic, "failed to execute transfer") - } - } - - // Check for unexpected `Approval` event in logs - if err := validateApprovalEventDoesNotExist(res.Logs); err != nil { - return nil, err - } - - // Check expected escrow balance after transfer execution - // NOTE: coin fields already validated in the ValidateBasic() of the message - coins := sdk.Coins{sdk.Coin{Denom: pair.Denom, Amount: msg.Amount}} - tokens := coins[0].Amount.BigInt() - balanceTokenAfter := k.BalanceOf(ctx, erc20, contract, types.ModuleAddress) - if balanceTokenAfter == nil { - return nil, sdkerrors.Wrap(types.ErrEVMCall, "failed to retrieve balance") - } - - expToken := big.NewInt(0).Add(balanceToken, tokens) - - if r := balanceTokenAfter.Cmp(expToken); r != 0 { - return nil, sdkerrors.Wrapf( - types.ErrBalanceInvariance, - "invalid token balance - expected: %v, actual: %v", - expToken, balanceTokenAfter, - ) - } - - // Mint coins - if err := k.bankKeeper.MintCoins(ctx, types.ModuleName, coins); err != nil { - return nil, err - } - - // Send minted coins to the receiver - if err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, receiver, coins); err != nil { - return nil, err - } - - // Check expected receiver balance after transfer - balanceCoinAfter := k.bankKeeper.GetBalance(ctx, receiver, pair.Denom) - expCoin := balanceCoin.Add(coins[0]) - - if ok := balanceCoinAfter.Equal(expCoin); !ok { - return nil, sdkerrors.Wrapf( - types.ErrBalanceInvariance, - "invalid coin balance - expected: %v, actual: %v", - expCoin, balanceCoinAfter, - ) - } - - defer func() { - telemetry.IncrCounterWithLabels( - []string{"tx", "msg", "convert", "erc20", "total"}, - 1, - []metrics.Label{ - telemetry.NewLabel("coin", pair.Denom), - }, - ) - - if msg.Amount.IsInt64() { - telemetry.IncrCounterWithLabels( - []string{"tx", "msg", "convert", "erc20", "amount", "total"}, - float32(msg.Amount.Int64()), - []metrics.Label{ - telemetry.NewLabel("denom", pair.Denom), - }, - ) - } - }() + // Create stateDB for this transaction + stateDB := statedb.New(ctx, k.evmKeeper, statedb.NewEmptyTxConfig()) - ctx.EventManager().EmitEvents( - sdk.Events{ - sdk.NewEvent( - types.EventTypeConvertERC20, - sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender), - sdk.NewAttribute(types.AttributeKeyReceiver, msg.Receiver), - sdk.NewAttribute(sdk.AttributeKeyAmount, msg.Amount.String()), - sdk.NewAttribute(types.AttributeKeyCosmosCoin, pair.Denom), - sdk.NewAttribute(types.AttributeKeyERC20Token, msg.ContractAddress), - ), - }, - ) - - return &types.MsgConvertERC20Response{}, nil + return k.ConvertERC20IntoCoinsForNativeToken(ctx, stateDB, contract, msg.Amount, receiver, sender, true, false) } // ConvertCoin converts native Cosmos coins into ERC20 tokens for both @@ -224,7 +74,7 @@ func (k Keeper) ConvertCoin( return nil, nil } - return nil, k.ConvertCoinNativeERC20(ctx, pair, msg.Coin.Amount, receiver, sender) + return nil, k.ConvertCoinNativeERC20(ctx, pair, msg.Coin.Amount, receiver, sender, false) case pair.IsNativeCoin(): return nil, types.ErrNativeConversionDisabled } @@ -237,18 +87,27 @@ func (k Keeper) ConvertCoin( // the burn either all commit or none of them do. // // The atomicity is load-bearing, not cosmetic. The body escrows coins onto the -// module account before calling the EVM, and CallEVM caches only the EVM -// execution — so on a VM failure the discarded EVM cache used to leave the bank -// escrow applied, stranding the sender's coins on the erc20 module account with -// no user-facing way to reclaim them. The IBC refund path (see -// ConvertCoinToERC20FromPacket) swallows this error and commits the block, which -// made that stranding permanent (F-2026-18819). +// module account before calling the EVM, and the EVM call isolates only the EVM +// execution — so on a VM failure the bank escrow used to survive, stranding the +// sender's coins on the erc20 module account with no user-facing way to reclaim +// them. The IBC refund path (see ConvertCoinToERC20FromPacket) swallows this +// error and commits the block, which made that stranding permanent +// (F-2026-18819). +// +// As of v0.6.0 this branch is the ONLY rollback boundary around the escrow. +// CallEVMWithData no longer wraps ApplyMessage in its own ctx.CacheContext(): the +// caller supplies the StateDB and ApplyMessageWithConfig commits it whenever +// commit is true, reverted frames included (it relies on the EVM's own +// RevertToSnapshot having already unwound them). Do not remove the CacheContext +// below on the grounds that "the EVM call rolls itself back" — the bank escrow is +// a plain SDK store write and was never part of what the EVM rolls back. func (k Keeper) ConvertCoinNativeERC20( ctx sdk.Context, pair types.TokenPair, amount math.Int, receiver common.Address, sender sdk.AccAddress, + callFromPrecompile bool, ) error { // CacheContext branches the multistore and installs a fresh EventManager; // writeCache replays those events onto the parent and commits the branch. @@ -259,7 +118,7 @@ func (k Keeper) ConvertCoinNativeERC20( // should not be free. cacheCtx, writeCache := ctx.CacheContext() - if err := k.convertCoinNativeERC20(cacheCtx, pair, amount, receiver, sender); err != nil { + if err := k.convertCoinNativeERC20(cacheCtx, pair, amount, receiver, sender, callFromPrecompile); err != nil { // Branch discarded: the escrow, the EVM state and any events go with it. return err } @@ -284,6 +143,7 @@ func (k Keeper) convertCoinNativeERC20( amount math.Int, receiver common.Address, sender sdk.AccAddress, + callFromPrecompile bool, ) error { if !amount.IsPositive() { return sdkerrors.Wrap(types.ErrNegativeToken, "converted coin amount must be positive") @@ -304,7 +164,8 @@ func (k Keeper) convertCoinNativeERC20( } // Unescrow Tokens and send to receiver - res, err := k.evmKeeper.CallEVM(ctx, erc20, types.ModuleAddress, contract, true, nil, "transfer", receiver, amount.BigInt()) + stateDB := statedb.New(ctx, k.evmKeeper, statedb.NewEmptyTxConfig()) + res, err := k.evmKeeper.CallEVM(ctx, stateDB, erc20, types.ModuleAddress, contract, true, callFromPrecompile, nil, "transfer", receiver, amount.BigInt()) if err != nil { return err } diff --git a/x/erc20/module.go b/x/erc20/module.go index 332509f11..854a3326c 100644 --- a/x/erc20/module.go +++ b/x/erc20/module.go @@ -31,7 +31,7 @@ const consensusVersion = 1 // type check to ensure the interface is properly implemented var ( - _ module.AppModule = AppModule{} + _ module.AppModule = AppModule{} //nolint:staticcheck // check against deprecated type _ module.AppModuleBasic = AppModuleBasic{} _ module.AppModuleSimulation = AppModule{} diff --git a/x/erc20/types/interfaces.go b/x/erc20/types/interfaces.go index 327057333..a03dbe2f3 100644 --- a/x/erc20/types/interfaces.go +++ b/x/erc20/types/interfaces.go @@ -17,6 +17,7 @@ import ( "cosmossdk.io/core/address" "cosmossdk.io/log" + storetypes "cosmossdk.io/store/types" sdk "github.com/cosmos/cosmos-sdk/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" @@ -41,16 +42,23 @@ type EVMKeeper interface { GetParams(ctx sdk.Context) evmtypes.Params GetAccountWithoutBalance(ctx sdk.Context, addr common.Address) *statedb.Account EstimateGasInternal(c context.Context, req *evmtypes.EthCallRequest, fromType evmtypes.CallType) (*evmtypes.EstimateGasResponse, error) - ApplyMessage(ctx sdk.Context, msg core.Message, tracer *tracing.Hooks, commit, internal bool) (*evmtypes.MsgEthereumTxResponse, error) + ApplyMessage(ctx sdk.Context, stateDB *statedb.StateDB, msg core.Message, tracer *tracing.Hooks, commit, callFromPrecompile, internal bool) (*evmtypes.MsgEthereumTxResponse, error) DeleteAccount(ctx sdk.Context, addr common.Address) error IsAvailableStaticPrecompile(params *evmtypes.Params, address common.Address) bool - CallEVM(ctx sdk.Context, abi abi.ABI, from, contract common.Address, commit bool, gasCap *big.Int, method string, args ...interface{}) (*evmtypes.MsgEthereumTxResponse, error) - CallEVMWithData(ctx sdk.Context, from common.Address, contract *common.Address, data []byte, commit bool, gasCap *big.Int) (*evmtypes.MsgEthereumTxResponse, error) + CallEVM(ctx sdk.Context, stateDB *statedb.StateDB, abi abi.ABI, from, contract common.Address, commit, callFromPrecompile bool, gasCap *big.Int, method string, args ...interface{}) (*evmtypes.MsgEthereumTxResponse, error) + CallEVMWithData(ctx sdk.Context, stateDB *statedb.StateDB, from common.Address, contract *common.Address, data []byte, commit bool, callFromPrecompile bool, gasCap *big.Int) (*evmtypes.MsgEthereumTxResponse, error) GetCode(ctx sdk.Context, hash common.Hash) []byte SetCode(ctx sdk.Context, hash []byte, bytecode []byte) SetAccount(ctx sdk.Context, address common.Address, account statedb.Account) error GetAccount(ctx sdk.Context, address common.Address) *statedb.Account IsContract(ctx sdk.Context, address common.Address) bool + GetState(ctx sdk.Context, addr common.Address, key common.Hash) common.Hash + GetCodeHash(ctx sdk.Context, addr common.Address) common.Hash + ForEachStorage(ctx sdk.Context, addr common.Address, cb func(key, value common.Hash) bool) + DeleteState(ctx sdk.Context, addr common.Address, key common.Hash) + SetState(ctx sdk.Context, addr common.Address, key common.Hash, value []byte) + DeleteCode(ctx sdk.Context, codeHash []byte) + KVStoreKeys() map[string]*storetypes.KVStoreKey } type Erc20Keeper interface { diff --git a/x/erc20/types/mocks/EVMKeeper.go b/x/erc20/types/mocks/EVMKeeper.go index eacc172d6..71dba53cf 100644 --- a/x/erc20/types/mocks/EVMKeeper.go +++ b/x/erc20/types/mocks/EVMKeeper.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.53.0. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package mocks @@ -17,6 +17,8 @@ import ( statedb "github.com/cosmos/evm/x/vm/statedb" + storetypes "cosmossdk.io/store/types" + tracing "github.com/ethereum/go-ethereum/core/tracing" types "github.com/cosmos/cosmos-sdk/types" @@ -29,9 +31,9 @@ type EVMKeeper struct { mock.Mock } -// ApplyMessage provides a mock function with given fields: ctx, msg, tracer, commit -func (_m *EVMKeeper) ApplyMessage(ctx types.Context, msg core.Message, tracer *tracing.Hooks, commit bool, internal bool) (*vmtypes.MsgEthereumTxResponse, error) { - ret := _m.Called(ctx, msg, tracer, commit) +// ApplyMessage provides a mock function with given fields: ctx, stateDB, msg, tracer, commit, callFromPrecompile, internal +func (_m *EVMKeeper) ApplyMessage(ctx types.Context, stateDB *statedb.StateDB, msg core.Message, tracer *tracing.Hooks, commit bool, callFromPrecompile bool, internal bool) (*vmtypes.MsgEthereumTxResponse, error) { + ret := _m.Called(ctx, stateDB, msg, tracer, commit, callFromPrecompile, internal) if len(ret) == 0 { panic("no return value specified for ApplyMessage") @@ -39,19 +41,19 @@ func (_m *EVMKeeper) ApplyMessage(ctx types.Context, msg core.Message, tracer *t var r0 *vmtypes.MsgEthereumTxResponse var r1 error - if rf, ok := ret.Get(0).(func(types.Context, core.Message, *tracing.Hooks, bool) (*vmtypes.MsgEthereumTxResponse, error)); ok { - return rf(ctx, msg, tracer, commit) + if rf, ok := ret.Get(0).(func(types.Context, *statedb.StateDB, core.Message, *tracing.Hooks, bool, bool, bool) (*vmtypes.MsgEthereumTxResponse, error)); ok { + return rf(ctx, stateDB, msg, tracer, commit, callFromPrecompile, internal) } - if rf, ok := ret.Get(0).(func(types.Context, core.Message, *tracing.Hooks, bool) *vmtypes.MsgEthereumTxResponse); ok { - r0 = rf(ctx, msg, tracer, commit) + if rf, ok := ret.Get(0).(func(types.Context, *statedb.StateDB, core.Message, *tracing.Hooks, bool, bool, bool) *vmtypes.MsgEthereumTxResponse); ok { + r0 = rf(ctx, stateDB, msg, tracer, commit, callFromPrecompile, internal) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*vmtypes.MsgEthereumTxResponse) } } - if rf, ok := ret.Get(1).(func(types.Context, core.Message, *tracing.Hooks, bool) error); ok { - r1 = rf(ctx, msg, tracer, commit) + if rf, ok := ret.Get(1).(func(types.Context, *statedb.StateDB, core.Message, *tracing.Hooks, bool, bool, bool) error); ok { + r1 = rf(ctx, stateDB, msg, tracer, commit, callFromPrecompile, internal) } else { r1 = ret.Error(1) } @@ -59,10 +61,10 @@ func (_m *EVMKeeper) ApplyMessage(ctx types.Context, msg core.Message, tracer *t return r0, r1 } -// CallEVM provides a mock function with given fields: ctx, _a1, from, contract, commit, gasCap, method, args -func (_m *EVMKeeper) CallEVM(ctx types.Context, _a1 abi.ABI, from common.Address, contract common.Address, commit bool, gasCap *big.Int, method string, args ...interface{}) (*vmtypes.MsgEthereumTxResponse, error) { +// CallEVM provides a mock function with given fields: ctx, stateDB, _a2, from, contract, commit, callFromPrecompile, gasCap, method, args +func (_m *EVMKeeper) CallEVM(ctx types.Context, stateDB *statedb.StateDB, _a2 abi.ABI, from common.Address, contract common.Address, commit bool, callFromPrecompile bool, gasCap *big.Int, method string, args ...interface{}) (*vmtypes.MsgEthereumTxResponse, error) { var _ca []interface{} - _ca = append(_ca, ctx, _a1, from, contract, commit, gasCap, method) + _ca = append(_ca, ctx, stateDB, _a2, from, contract, commit, callFromPrecompile, gasCap, method) _ca = append(_ca, args...) ret := _m.Called(_ca...) @@ -72,19 +74,19 @@ func (_m *EVMKeeper) CallEVM(ctx types.Context, _a1 abi.ABI, from common.Address var r0 *vmtypes.MsgEthereumTxResponse var r1 error - if rf, ok := ret.Get(0).(func(types.Context, abi.ABI, common.Address, common.Address, bool, *big.Int, string, ...interface{}) (*vmtypes.MsgEthereumTxResponse, error)); ok { - return rf(ctx, _a1, from, contract, commit, gasCap, method, args...) + if rf, ok := ret.Get(0).(func(types.Context, *statedb.StateDB, abi.ABI, common.Address, common.Address, bool, bool, *big.Int, string, ...interface{}) (*vmtypes.MsgEthereumTxResponse, error)); ok { + return rf(ctx, stateDB, _a2, from, contract, commit, callFromPrecompile, gasCap, method, args...) } - if rf, ok := ret.Get(0).(func(types.Context, abi.ABI, common.Address, common.Address, bool, *big.Int, string, ...interface{}) *vmtypes.MsgEthereumTxResponse); ok { - r0 = rf(ctx, _a1, from, contract, commit, gasCap, method, args...) + if rf, ok := ret.Get(0).(func(types.Context, *statedb.StateDB, abi.ABI, common.Address, common.Address, bool, bool, *big.Int, string, ...interface{}) *vmtypes.MsgEthereumTxResponse); ok { + r0 = rf(ctx, stateDB, _a2, from, contract, commit, callFromPrecompile, gasCap, method, args...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*vmtypes.MsgEthereumTxResponse) } } - if rf, ok := ret.Get(1).(func(types.Context, abi.ABI, common.Address, common.Address, bool, *big.Int, string, ...interface{}) error); ok { - r1 = rf(ctx, _a1, from, contract, commit, gasCap, method, args...) + if rf, ok := ret.Get(1).(func(types.Context, *statedb.StateDB, abi.ABI, common.Address, common.Address, bool, bool, *big.Int, string, ...interface{}) error); ok { + r1 = rf(ctx, stateDB, _a2, from, contract, commit, callFromPrecompile, gasCap, method, args...) } else { r1 = ret.Error(1) } @@ -92,9 +94,9 @@ func (_m *EVMKeeper) CallEVM(ctx types.Context, _a1 abi.ABI, from common.Address return r0, r1 } -// CallEVMWithData provides a mock function with given fields: ctx, from, contract, data, commit, gasCap -func (_m *EVMKeeper) CallEVMWithData(ctx types.Context, from common.Address, contract *common.Address, data []byte, commit bool, gasCap *big.Int) (*vmtypes.MsgEthereumTxResponse, error) { - ret := _m.Called(ctx, from, contract, data, commit, gasCap) +// CallEVMWithData provides a mock function with given fields: ctx, stateDB, from, contract, data, commit, callFromPrecompile, gasCap +func (_m *EVMKeeper) CallEVMWithData(ctx types.Context, stateDB *statedb.StateDB, from common.Address, contract *common.Address, data []byte, commit bool, callFromPrecompile bool, gasCap *big.Int) (*vmtypes.MsgEthereumTxResponse, error) { + ret := _m.Called(ctx, stateDB, from, contract, data, commit, callFromPrecompile, gasCap) if len(ret) == 0 { panic("no return value specified for CallEVMWithData") @@ -102,19 +104,19 @@ func (_m *EVMKeeper) CallEVMWithData(ctx types.Context, from common.Address, con var r0 *vmtypes.MsgEthereumTxResponse var r1 error - if rf, ok := ret.Get(0).(func(types.Context, common.Address, *common.Address, []byte, bool, *big.Int) (*vmtypes.MsgEthereumTxResponse, error)); ok { - return rf(ctx, from, contract, data, commit, gasCap) + if rf, ok := ret.Get(0).(func(types.Context, *statedb.StateDB, common.Address, *common.Address, []byte, bool, bool, *big.Int) (*vmtypes.MsgEthereumTxResponse, error)); ok { + return rf(ctx, stateDB, from, contract, data, commit, callFromPrecompile, gasCap) } - if rf, ok := ret.Get(0).(func(types.Context, common.Address, *common.Address, []byte, bool, *big.Int) *vmtypes.MsgEthereumTxResponse); ok { - r0 = rf(ctx, from, contract, data, commit, gasCap) + if rf, ok := ret.Get(0).(func(types.Context, *statedb.StateDB, common.Address, *common.Address, []byte, bool, bool, *big.Int) *vmtypes.MsgEthereumTxResponse); ok { + r0 = rf(ctx, stateDB, from, contract, data, commit, callFromPrecompile, gasCap) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*vmtypes.MsgEthereumTxResponse) } } - if rf, ok := ret.Get(1).(func(types.Context, common.Address, *common.Address, []byte, bool, *big.Int) error); ok { - r1 = rf(ctx, from, contract, data, commit, gasCap) + if rf, ok := ret.Get(1).(func(types.Context, *statedb.StateDB, common.Address, *common.Address, []byte, bool, bool, *big.Int) error); ok { + r1 = rf(ctx, stateDB, from, contract, data, commit, callFromPrecompile, gasCap) } else { r1 = ret.Error(1) } @@ -140,6 +142,16 @@ func (_m *EVMKeeper) DeleteAccount(ctx types.Context, addr common.Address) error return r0 } +// DeleteCode provides a mock function with given fields: ctx, codeHash +func (_m *EVMKeeper) DeleteCode(ctx types.Context, codeHash []byte) { + _m.Called(ctx, codeHash) +} + +// DeleteState provides a mock function with given fields: ctx, addr, key +func (_m *EVMKeeper) DeleteState(ctx types.Context, addr common.Address, key common.Hash) { + _m.Called(ctx, addr, key) +} + // EstimateGasInternal provides a mock function with given fields: c, req, fromType func (_m *EVMKeeper) EstimateGasInternal(c context.Context, req *vmtypes.EthCallRequest, fromType vmtypes.CallType) (*vmtypes.EstimateGasResponse, error) { ret := _m.Called(c, req, fromType) @@ -170,6 +182,11 @@ func (_m *EVMKeeper) EstimateGasInternal(c context.Context, req *vmtypes.EthCall return r0, r1 } +// ForEachStorage provides a mock function with given fields: ctx, addr, cb +func (_m *EVMKeeper) ForEachStorage(ctx types.Context, addr common.Address, cb func(common.Hash, common.Hash) bool) { + _m.Called(ctx, addr, cb) +} + // GetAccount provides a mock function with given fields: ctx, address func (_m *EVMKeeper) GetAccount(ctx types.Context, address common.Address) *statedb.Account { ret := _m.Called(ctx, address) @@ -230,6 +247,26 @@ func (_m *EVMKeeper) GetCode(ctx types.Context, hash common.Hash) []byte { return r0 } +// GetCodeHash provides a mock function with given fields: ctx, addr +func (_m *EVMKeeper) GetCodeHash(ctx types.Context, addr common.Address) common.Hash { + ret := _m.Called(ctx, addr) + + if len(ret) == 0 { + panic("no return value specified for GetCodeHash") + } + + var r0 common.Hash + if rf, ok := ret.Get(0).(func(types.Context, common.Address) common.Hash); ok { + r0 = rf(ctx, addr) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(common.Hash) + } + } + + return r0 +} + // GetParams provides a mock function with given fields: ctx func (_m *EVMKeeper) GetParams(ctx types.Context) vmtypes.Params { ret := _m.Called(ctx) @@ -248,6 +285,26 @@ func (_m *EVMKeeper) GetParams(ctx types.Context) vmtypes.Params { return r0 } +// GetState provides a mock function with given fields: ctx, addr, key +func (_m *EVMKeeper) GetState(ctx types.Context, addr common.Address, key common.Hash) common.Hash { + ret := _m.Called(ctx, addr, key) + + if len(ret) == 0 { + panic("no return value specified for GetState") + } + + var r0 common.Hash + if rf, ok := ret.Get(0).(func(types.Context, common.Address, common.Hash) common.Hash); ok { + r0 = rf(ctx, addr, key) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(common.Hash) + } + } + + return r0 +} + // IsAvailableStaticPrecompile provides a mock function with given fields: params, address func (_m *EVMKeeper) IsAvailableStaticPrecompile(params *vmtypes.Params, address common.Address) bool { ret := _m.Called(params, address) @@ -284,6 +341,26 @@ func (_m *EVMKeeper) IsContract(ctx types.Context, address common.Address) bool return r0 } +// KVStoreKeys provides a mock function with no fields +func (_m *EVMKeeper) KVStoreKeys() map[string]*storetypes.KVStoreKey { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for KVStoreKeys") + } + + var r0 map[string]*storetypes.KVStoreKey + if rf, ok := ret.Get(0).(func() map[string]*storetypes.KVStoreKey); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]*storetypes.KVStoreKey) + } + } + + return r0 +} + // SetAccount provides a mock function with given fields: ctx, address, account func (_m *EVMKeeper) SetAccount(ctx types.Context, address common.Address, account statedb.Account) error { ret := _m.Called(ctx, address, account) @@ -307,13 +384,17 @@ func (_m *EVMKeeper) SetCode(ctx types.Context, hash []byte, bytecode []byte) { _m.Called(ctx, hash, bytecode) } +// SetState provides a mock function with given fields: ctx, addr, key, value +func (_m *EVMKeeper) SetState(ctx types.Context, addr common.Address, key common.Hash, value []byte) { + _m.Called(ctx, addr, key, value) +} + // NewEVMKeeper creates a new instance of EVMKeeper. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewEVMKeeper(t interface { mock.TestingT Cleanup(func()) -}, -) *EVMKeeper { +}) *EVMKeeper { mock := &EVMKeeper{} mock.Mock.Test(t) diff --git a/x/erc20/types/msg.go b/x/erc20/types/msg.go index 8c4213b13..a6649cad7 100644 --- a/x/erc20/types/msg.go +++ b/x/erc20/types/msg.go @@ -79,11 +79,6 @@ func (msg MsgConvertERC20) ValidateBasic() error { return nil } -// GetSignBytes encodes the message for signing -func (msg MsgConvertERC20) GetSignBytes() []byte { - return sdk.MustSortJSON(AminoCdc.MustMarshalJSON(&msg)) -} - // ValidateBasic does a sanity check of the provided data func (m *MsgUpdateParams) ValidateBasic() error { if _, err := sdk.AccAddressFromBech32(m.Authority); err != nil { @@ -92,11 +87,6 @@ func (m *MsgUpdateParams) ValidateBasic() error { return nil } -// GetSignBytes implements the LegacyMsg interface. -func (m MsgUpdateParams) GetSignBytes() []byte { - return sdk.MustSortJSON(AminoCdc.MustMarshalJSON(&m)) -} - // ValidateBasic does a sanity check of the provided data func (m *MsgRegisterERC20) ValidateBasic() error { _, err := sdk.AccAddressFromBech32(m.Signer) @@ -144,8 +134,3 @@ func (msg MsgConvertCoin) ValidateBasic() error { } return nil } - -// GetSignBytes encodes the message for signing -func (msg MsgConvertCoin) GetSignBytes() []byte { - return sdk.MustSortJSON(AminoCdc.MustMarshalJSON(&msg)) -} diff --git a/x/erc20/types/msg_test.go b/x/erc20/types/msg_test.go index 28b9c4ad9..b71a4ad3b 100644 --- a/x/erc20/types/msg_test.go +++ b/x/erc20/types/msg_test.go @@ -25,16 +25,14 @@ func TestMsgsTestSuite(t *testing.T) { } func (suite *MsgsTestSuite) TestMsgConvertERC20Getters() { - msgInvalid := types.MsgConvertERC20{} msg := types.NewMsgConvertERC20( math.NewInt(100), - sdk.AccAddress(utiltx.GenerateAddress().Bytes()), + utiltx.GenerateAddress().Bytes(), utiltx.GenerateAddress(), utiltx.GenerateAddress(), ) suite.Require().Equal(types.RouterKey, msg.Route()) suite.Require().Equal(types.TypeMsgConvertERC20, msg.Type()) - suite.Require().NotNil(msgInvalid.GetSignBytes()) } func (suite *MsgsTestSuite) TestMsgConvertERC20New() { @@ -132,18 +130,16 @@ func (suite *MsgsTestSuite) TestMsgConvertERC20() { } func (suite *MsgsTestSuite) TestMsgConvertCoinGetters() { - msgInvalid := types.MsgConvertCoin{} msg := types.NewMsgConvertCoin( sdk.NewCoin( "atest", math.NewInt(100), ), utiltx.GenerateAddress(), - sdk.AccAddress(utiltx.GenerateAddress().Bytes()), + utiltx.GenerateAddress().Bytes(), ) suite.Require().Equal(types.RouterKey, msg.Route()) suite.Require().Equal(types.TypeMsgConvertCoin, msg.Type()) - suite.Require().NotNil(msgInvalid.GetSignBytes()) } func (suite *MsgsTestSuite) TestNewMsgConvertCoin() { diff --git a/x/feemarket/module.go b/x/feemarket/module.go index 67557898d..9e78672cc 100644 --- a/x/feemarket/module.go +++ b/x/feemarket/module.go @@ -29,7 +29,7 @@ import ( const consensusVersion = 1 var ( - _ module.AppModule = AppModule{} + _ module.AppModule = AppModule{} //nolint:staticcheck // check against deprecated type _ module.AppModuleBasic = AppModuleBasic{} _ appmodule.HasEndBlocker = AppModule{} diff --git a/x/feemarket/types/msg.go b/x/feemarket/types/msg.go index 31b23e593..19a0cdd32 100644 --- a/x/feemarket/types/msg.go +++ b/x/feemarket/types/msg.go @@ -16,8 +16,3 @@ func (m *MsgUpdateParams) ValidateBasic() error { return m.Params.Validate() } - -// GetSignBytes implements the LegacyMsg interface. -func (m MsgUpdateParams) GetSignBytes() []byte { - return sdk.MustSortJSON(AminoCdc.MustMarshalJSON(&m)) -} diff --git a/x/ibc/callbacks/keeper/keeper.go b/x/ibc/callbacks/keeper/keeper.go index c6d63760f..c9fe36353 100644 --- a/x/ibc/callbacks/keeper/keeper.go +++ b/x/ibc/callbacks/keeper/keeper.go @@ -12,6 +12,7 @@ import ( erc20types "github.com/cosmos/evm/x/erc20/types" "github.com/cosmos/evm/x/ibc/callbacks/types" evmante "github.com/cosmos/evm/x/vm/ante" + "github.com/cosmos/evm/x/vm/statedb" evmtypes "github.com/cosmos/evm/x/vm/types" callbacktypes "github.com/cosmos/ibc-go/v10/modules/apps/callbacks/types" transfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" @@ -129,6 +130,7 @@ func (k ContractKeeper) IBCReceivePacketCallback( cachedCtx, writeFn := ctx.CacheContext() cachedCtx = evmante.BuildEvmExecutionCtx(cachedCtx). WithGasMeter(evmtypes.NewInfiniteGasMeterWithLimit(cbData.CommitGasLimit)) + stateDB := statedb.New(cachedCtx, k.evmKeeper, statedb.NewEmptyTxConfig()) // receiver := sdk.MustAccAddressFromBech32(data.Receiver) receiver, err := sdk.AccAddressFromBech32(data.Receiver) @@ -189,7 +191,7 @@ func (k ContractKeeper) IBCReceivePacketCallback( // Call the EVM with the remaining gas as the maximum gas limit. // Up to now, the remaining gas is equal to the callback gas limit set by the user. // NOTE: use the cached ctx for the EVM calls. - res, err := k.evmKeeper.CallEVM(cachedCtx, erc20.ABI, receiverHex, tokenPair.GetERC20Contract(), true, remainingGas, "approve", contractAddr, amountInt.BigInt()) + res, err := k.evmKeeper.CallEVM(cachedCtx, stateDB, erc20.ABI, receiverHex, tokenPair.GetERC20Contract(), true, false, remainingGas, "approve", contractAddr, amountInt.BigInt()) if err != nil { return errorsmod.Wrapf(types.ErrAllowanceFailed, "failed to set allowance: %v", err) } @@ -212,7 +214,7 @@ func (k ContractKeeper) IBCReceivePacketCallback( } // NOTE: use the cached ctx for the EVM calls. - res, err = k.evmKeeper.CallEVMWithData(cachedCtx, receiverHex, &contractAddr, cbData.Calldata, true, remainingGas) + res, err = k.evmKeeper.CallEVMWithData(cachedCtx, stateDB, receiverHex, &contractAddr, cbData.Calldata, true, false, remainingGas) if err != nil { return errorsmod.Wrapf(types.ErrEVMCallFailed, "EVM returned error: %s", err.Error()) } @@ -297,6 +299,7 @@ func (k ContractKeeper) IBCOnAcknowledgementPacketCallback( cachedCtx, writeFn := ctx.CacheContext() cachedCtx = evmante.BuildEvmExecutionCtx(cachedCtx). WithGasMeter(evmtypes.NewInfiniteGasMeterWithLimit(cbData.CommitGasLimit)) + stateDB := statedb.New(cachedCtx, k.evmKeeper, statedb.NewEmptyTxConfig()) if len(cbData.Calldata) != 0 { return errorsmod.Wrap(types.ErrInvalidCalldata, "acknowledgement callback data should not contain calldata") @@ -323,7 +326,7 @@ func (k ContractKeeper) IBCOnAcknowledgementPacketCallback( // Call the onPacketAcknowledgement function in the contract // NOTE: use the cached ctx for the EVM calls. - res, err := k.evmKeeper.CallEVM(cachedCtx, *abi, sender, contractAddr, true, math.NewIntFromUint64(cachedCtx.GasMeter().GasRemaining()).BigInt(), "onPacketAcknowledgement", + res, err := k.evmKeeper.CallEVM(cachedCtx, stateDB, *abi, sender, contractAddr, true, false, math.NewIntFromUint64(cachedCtx.GasMeter().GasRemaining()).BigInt(), "onPacketAcknowledgement", packet.GetSourceChannel(), packet.GetSourcePort(), packet.GetSequence(), packet.GetData(), acknowledgement) if err != nil { return errorsmod.Wrapf(types.ErrCallbackFailed, "EVM returned error: %s", err.Error()) @@ -397,6 +400,7 @@ func (k ContractKeeper) IBCOnTimeoutPacketCallback( cachedCtx, writeFn := ctx.CacheContext() cachedCtx = evmante.BuildEvmExecutionCtx(cachedCtx). WithGasMeter(evmtypes.NewInfiniteGasMeterWithLimit(cbData.CommitGasLimit)) + stateDB := statedb.New(cachedCtx, k.evmKeeper, statedb.NewEmptyTxConfig()) if len(cbData.Calldata) != 0 { return errorsmod.Wrap(types.ErrInvalidCalldata, "timeout callback data should not contain calldata") @@ -424,8 +428,9 @@ func (k ContractKeeper) IBCOnTimeoutPacketCallback( // NOTE: use the cached ctx for the EVM calls, as the acknowledgement path // does. Running on the parent ctx committed the callback's EVM state // regardless of the outcome and made writeFn() below a no-op, while the gas - // cap was still being read off cachedCtx (F-2026-18818). - res, err := k.evmKeeper.CallEVM(cachedCtx, *abi, sender, contractAddr, true, math.NewIntFromUint64(cachedCtx.GasMeter().GasRemaining()).BigInt(), "onPacketTimeout", + // cap was still being read off cachedCtx (F-2026-18818). The StateDB above is + // likewise branched off cachedCtx, so the two must agree. + res, err := k.evmKeeper.CallEVM(cachedCtx, stateDB, *abi, sender, contractAddr, true, false, math.NewIntFromUint64(cachedCtx.GasMeter().GasRemaining()).BigInt(), "onPacketTimeout", packet.GetSourceChannel(), packet.GetSourcePort(), packet.GetSequence(), packet.GetData()) if err != nil { return errorsmod.Wrapf(types.ErrCallbackFailed, "EVM returned error: %s", err.Error()) diff --git a/x/ibc/callbacks/types/expected_keepers.go b/x/ibc/callbacks/types/expected_keepers.go index bafdf9c3b..546a1c7ea 100644 --- a/x/ibc/callbacks/types/expected_keepers.go +++ b/x/ibc/callbacks/types/expected_keepers.go @@ -11,6 +11,8 @@ import ( "github.com/cosmos/evm/x/vm/statedb" evmtypes "github.com/cosmos/evm/x/vm/types" + storetypes "cosmossdk.io/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -22,11 +24,22 @@ type AccountKeeper interface { // EVMKeeper defines the expected EVM keeper interface used on erc20 type EVMKeeper interface { - CallEVM(ctx sdk.Context, abi abi.ABI, from, contract common.Address, commit bool, gasCap *big.Int, method string, args ...interface{}) (*evmtypes.MsgEthereumTxResponse, error) - CallEVMWithData(ctx sdk.Context, from common.Address, contract *common.Address, data []byte, commit bool, gasCap *big.Int) (*evmtypes.MsgEthereumTxResponse, error) + CallEVM(ctx sdk.Context, stateDB *statedb.StateDB, abi abi.ABI, from, contract common.Address, commit bool, callFromPrecompile bool, gasCap *big.Int, method string, args ...interface{}) (*evmtypes.MsgEthereumTxResponse, error) + CallEVMWithData(ctx sdk.Context, stateDB *statedb.StateDB, from common.Address, contract *common.Address, data []byte, commit bool, callFromPrecompile bool, gasCap *big.Int) (*evmtypes.MsgEthereumTxResponse, error) GetAccountOrEmpty(ctx sdk.Context, addr common.Address) statedb.Account GetAccount(ctx sdk.Context, addr common.Address) *statedb.Account IsContract(ctx sdk.Context, addr common.Address) bool + GetState(ctx sdk.Context, addr common.Address, key common.Hash) common.Hash + GetCode(ctx sdk.Context, codeHash common.Hash) []byte + GetCodeHash(ctx sdk.Context, addr common.Address) common.Hash + ForEachStorage(ctx sdk.Context, addr common.Address, cb func(key common.Hash, value common.Hash) bool) + SetAccount(ctx sdk.Context, addr common.Address, account statedb.Account) error + DeleteState(ctx sdk.Context, addr common.Address, key common.Hash) + SetState(ctx sdk.Context, addr common.Address, key common.Hash, value []byte) + DeleteCode(ctx sdk.Context, codeHash []byte) + SetCode(ctx sdk.Context, codeHash []byte, code []byte) + DeleteAccount(ctx sdk.Context, addr common.Address) error + KVStoreKeys() map[string]*storetypes.KVStoreKey } type ERC20Keeper interface { diff --git a/x/ibc/transfer/ibc_module.go b/x/ibc/transfer/ibc_module.go deleted file mode 100644 index 1d5a4a6c6..000000000 --- a/x/ibc/transfer/ibc_module.go +++ /dev/null @@ -1,22 +0,0 @@ -package transfer - -import ( - "github.com/cosmos/evm/x/ibc/transfer/keeper" - ibctransfer "github.com/cosmos/ibc-go/v10/modules/apps/transfer" - porttypes "github.com/cosmos/ibc-go/v10/modules/core/05-port/types" -) - -var _ porttypes.IBCModule = IBCModule{} - -// IBCModule implements the ICS26 interface for transfer given the transfer keeper. -type IBCModule struct { - *ibctransfer.IBCModule -} - -// NewIBCModule creates a new IBCModule given the keeper -func NewIBCModule(k keeper.Keeper) IBCModule { - transferModule := ibctransfer.NewIBCModule(*k.Keeper) - return IBCModule{ - IBCModule: &transferModule, - } -} diff --git a/x/ibc/transfer/keeper/keeper.go b/x/ibc/transfer/keeper/keeper.go deleted file mode 100644 index 48b150803..000000000 --- a/x/ibc/transfer/keeper/keeper.go +++ /dev/null @@ -1,50 +0,0 @@ -package keeper - -import ( - "github.com/cosmos/evm/x/ibc/transfer/types" - "github.com/cosmos/ibc-go/v10/modules/apps/transfer/keeper" - transfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" - porttypes "github.com/cosmos/ibc-go/v10/modules/core/05-port/types" - - corestore "cosmossdk.io/core/store" - - "github.com/cosmos/cosmos-sdk/codec" -) - -// Keeper defines the modified IBC transfer keeper that embeds the original one. -// It also contains the bank keeper and the erc20 keeper to support ERC20 tokens -// to be sent via IBC. -type Keeper struct { - *keeper.Keeper - bankKeeper types.BankKeeper - erc20Keeper types.ERC20Keeper - accountKeeper types.AccountKeeper -} - -// NewKeeper creates a new IBC transfer Keeper instance -func NewKeeper( - cdc codec.BinaryCodec, - storeService corestore.KVStoreService, - - ics4Wrapper porttypes.ICS4Wrapper, - channelKeeper transfertypes.ChannelKeeper, - msgRouter transfertypes.MessageRouter, - authKeeper types.AccountKeeper, - bankKeeper types.BankKeeper, - erc20Keeper types.ERC20Keeper, - authority string, -) Keeper { - // create the original IBC transfer keeper for embedding - transferKeeper := keeper.NewKeeper( - cdc, storeService, nil, - ics4Wrapper, channelKeeper, msgRouter, - authKeeper, bankKeeper, authority, - ) - - return Keeper{ - Keeper: &transferKeeper, - bankKeeper: bankKeeper, - erc20Keeper: erc20Keeper, - accountKeeper: authKeeper, - } -} diff --git a/x/ibc/transfer/keeper/msg_server.go b/x/ibc/transfer/keeper/msg_server.go deleted file mode 100644 index 337158397..000000000 --- a/x/ibc/transfer/keeper/msg_server.go +++ /dev/null @@ -1,117 +0,0 @@ -package keeper - -import ( - "context" - "strings" - - "github.com/ethereum/go-ethereum/common" - "github.com/hashicorp/go-metrics" - - erc20types "github.com/cosmos/evm/x/erc20/types" - "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" - - storetypes "cosmossdk.io/store/types" - - "github.com/cosmos/cosmos-sdk/telemetry" - sdk "github.com/cosmos/cosmos-sdk/types" -) - -var _ types.MsgServer = Keeper{} - -// Transfer defines a gRPC msg server method for the MsgTransfer message. -// This implementation overrides the default ICS20 transfer by converting -// the ERC20 tokens to their Cosmos representation if the token pair has been -// registered through governance. -// If user doesn't have enough balance of coin, it will attempt to convert -// ERC20 tokens to the coin denomination, and continue with a regular transfer. -func (k Keeper) Transfer(goCtx context.Context, msg *types.MsgTransfer) (*types.MsgTransferResponse, error) { - ctx := sdk.UnwrapSDKContext(goCtx) - - // Temporarily save the KV and transient KV gas config. To avoid extra costs for relayers - // these two gas config are replaced with empty one and should be restored before exiting this function. - kvGasCfg := ctx.KVGasConfig() - transientKVGasCfg := ctx.TransientKVGasConfig() - ctx = ctx. - WithKVGasConfig(storetypes.GasConfig{}). - WithTransientKVGasConfig(storetypes.GasConfig{}) - - defer func() { - // Return the KV gas config to initial values - ctx = ctx. - WithKVGasConfig(kvGasCfg). - WithTransientKVGasConfig(transientKVGasCfg) - }() - - // use native denom or contract address - denom := strings.TrimPrefix(msg.Token.Denom, erc20types.Erc20NativeCoinDenomPrefix) - - pairID := k.erc20Keeper.GetTokenPairID(ctx, denom) - if len(pairID) == 0 { - // no-op: token is not registered so we can proceed with regular transfer - return k.Keeper.Transfer(ctx, msg) - } - - pair, _ := k.erc20Keeper.GetTokenPair(ctx, pairID) - if !pair.Enabled { - // no-op: pair is not enabled so we can proceed with regular transfer - return k.Keeper.Transfer(ctx, msg) - } - - sender := sdk.MustAccAddressFromBech32(msg.Sender) - - if !k.erc20Keeper.IsERC20Enabled(ctx) { - // no-op: continue with regular transfer - return k.Keeper.Transfer(ctx, msg) - } - - // update the msg denom to the token pair denom - msg.Token.Denom = pair.Denom - - if !pair.IsNativeERC20() { - return k.Keeper.Transfer(ctx, msg) - } - // if the user has enough balance of the Cosmos representation, then we don't need to Convert - balance := k.bankKeeper.SpendableCoin(ctx, sender, pair.Denom) - if balance.Amount.GTE(msg.Token.Amount) { - - defer func() { - telemetry.IncrCounterWithLabels( - []string{"erc20", "ibc", "transfer", "total"}, - 1, - []metrics.Label{ - telemetry.NewLabel("denom", pair.Denom), - }, - ) - }() - - return k.Keeper.Transfer(ctx, msg) - } - - // Only convert if the pair is a native ERC20 - // only convert the remaining difference - difference := msg.Token.Amount.Sub(balance.Amount) - - msgConvertERC20 := erc20types.NewMsgConvertERC20( - difference, - sender, - pair.GetERC20Contract(), - common.BytesToAddress(sender.Bytes()), - ) - - // Use MsgConvertERC20 to convert the ERC20 to a Cosmos IBC Coin - if _, err := k.erc20Keeper.ConvertERC20(ctx, msgConvertERC20); err != nil { - return nil, err - } - - defer func() { - telemetry.IncrCounterWithLabels( - []string{"erc20", "ibc", "transfer", "total"}, - 1, - []metrics.Label{ - telemetry.NewLabel("denom", pair.Denom), - }, - ) - }() - - return k.Keeper.Transfer(ctx, msg) -} diff --git a/x/ibc/transfer/module.go b/x/ibc/transfer/module.go deleted file mode 100644 index 63ede1377..000000000 --- a/x/ibc/transfer/module.go +++ /dev/null @@ -1,69 +0,0 @@ -package transfer - -import ( - "fmt" - - "github.com/cosmos/evm/x/ibc/transfer/keeper" - ibctransfer "github.com/cosmos/ibc-go/v10/modules/apps/transfer" - ibctransferkeeper "github.com/cosmos/ibc-go/v10/modules/apps/transfer/keeper" - "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" - - "github.com/cosmos/cosmos-sdk/types/module" -) - -const consensusVersion = 5 - -var ( - _ module.AppModule = AppModule{} - _ module.AppModuleBasic = AppModuleBasic{} -) - -// AppModuleBasic embeds the IBC Transfer AppModuleBasic -type AppModuleBasic struct { - *ibctransfer.AppModuleBasic -} - -// AppModule represents the AppModule for this module -type AppModule struct { - *ibctransfer.AppModule - keeper keeper.Keeper -} - -// NewAppModule creates a new 20-transfer module -func NewAppModule(k keeper.Keeper) AppModule { - am := ibctransfer.NewAppModule(*k.Keeper) - return AppModule{ - AppModule: &am, - keeper: k, - } -} - -// ConsensusVersion returns the consensus state-breaking version for the module. -func (AppModuleBasic) ConsensusVersion() uint64 { - return consensusVersion -} - -// RegisterServices registers module services. -func (am AppModule) RegisterServices(cfg module.Configurator) { - // Override Transfer Msg Server - types.RegisterMsgServer(cfg.MsgServer(), am.keeper) - types.RegisterQueryServer(cfg.QueryServer(), am.keeper) - - m := ibctransferkeeper.NewMigrator(*am.keeper.Keeper) - - if err := cfg.RegisterMigration(types.ModuleName, 2, m.MigrateTotalEscrowForDenom); err != nil { - panic(fmt.Sprintf("failed to migrate transfer app from version 2 to 3: %v", err)) - } - - if err := cfg.RegisterMigration(types.ModuleName, 3, m.MigrateParams); err != nil { - panic(fmt.Errorf("failed to migrate transfer app version 3 to 4 (self-managed params migration): %v", err)) - } - - if err := cfg.RegisterMigration(types.ModuleName, 4, m.MigrateDenomMetadata); err != nil { - panic(fmt.Errorf("failed to migrate transfer app from version 4 to 5 (set denom metadata migration): %v", err)) - } - - if err := cfg.RegisterMigration(types.ModuleName, 5, m.MigrateDenomTraceToDenom); err != nil { - panic(fmt.Errorf("failed to migrate transfer app from version 5 to 6 (migrate DenomTrace to Denom): %v", err)) - } -} diff --git a/x/ibc/transfer/types/channels.go b/x/ibc/transfer/types/channels.go deleted file mode 100644 index 1316b848f..000000000 --- a/x/ibc/transfer/types/channels.go +++ /dev/null @@ -1,13 +0,0 @@ -package types - -// Osmosis channels -const ( - OsmosisTestnetChannelID = "channel-215" - OsmosisMainnetChannelID = "channel-0" -) - -// Stride channels -const ( - StrideTestnetChannelID = "channel-25" - StrideMainnetChannelID = "channel-25" -) diff --git a/x/ibc/transfer/types/interfaces.go b/x/ibc/transfer/types/interfaces.go deleted file mode 100644 index fa5d349f6..000000000 --- a/x/ibc/transfer/types/interfaces.go +++ /dev/null @@ -1,31 +0,0 @@ -package types - -import ( - "context" - - erc20types "github.com/cosmos/evm/x/erc20/types" - transfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" - - sdk "github.com/cosmos/cosmos-sdk/types" -) - -// AccountKeeper defines the expected interface needed to retrieve account info. -type AccountKeeper interface { - transfertypes.AccountKeeper - GetAccount(context.Context, sdk.AccAddress) sdk.AccountI -} - -// BankKeeper defines the expected interface needed to check balances and send coins. -type BankKeeper interface { - transfertypes.BankKeeper - GetBalance(ctx context.Context, addr sdk.AccAddress, denom string) sdk.Coin -} - -// ERC20Keeper defines the expected ERC20 keeper interface for supporting -// ERC20 token transfers via IBC. -type ERC20Keeper interface { - IsERC20Enabled(ctx sdk.Context) bool - GetTokenPairID(ctx sdk.Context, token string) []byte - GetTokenPair(ctx sdk.Context, id []byte) (erc20types.TokenPair, bool) - ConvertERC20(ctx context.Context, msg *erc20types.MsgConvertERC20) (*erc20types.MsgConvertERC20Response, error) -} diff --git a/x/ibc/transfer/v2/ibc_module.go b/x/ibc/transfer/v2/ibc_module.go deleted file mode 100644 index 06996dd90..000000000 --- a/x/ibc/transfer/v2/ibc_module.go +++ /dev/null @@ -1,22 +0,0 @@ -package v2 - -import ( - "github.com/cosmos/evm/x/ibc/transfer/keeper" - v2 "github.com/cosmos/ibc-go/v10/modules/apps/transfer/v2" - ibcapi "github.com/cosmos/ibc-go/v10/modules/core/api" -) - -var _ ibcapi.IBCModule = IBCModule{} - -// IBCModule implements the ICS26 interface for transfer given the transfer keeper. -type IBCModule struct { - *v2.IBCModule -} - -// NewIBCModule creates a new IBCModule given the keeper -func NewIBCModule(k keeper.Keeper) IBCModule { - transferModule := v2.NewIBCModule(*k.Keeper) - return IBCModule{ - IBCModule: transferModule, - } -} diff --git a/x/precisebank/module.go b/x/precisebank/module.go index 5da71ceb7..50da3777f 100644 --- a/x/precisebank/module.go +++ b/x/precisebank/module.go @@ -26,7 +26,7 @@ import ( const ConsensusVersion = 1 var ( - _ module.AppModule = AppModule{} + _ module.AppModule = AppModule{} //nolint:staticcheck // check against deprecated type _ module.AppModuleBasic = AppModuleBasic{} _ module.HasABCIGenesis = AppModule{} diff --git a/x/vm/keeper/call_evm.go b/x/vm/keeper/call_evm.go index 20f50d34b..49eac591d 100644 --- a/x/vm/keeper/call_evm.go +++ b/x/vm/keeper/call_evm.go @@ -13,6 +13,7 @@ import ( ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/cosmos/evm/server/config" + "github.com/cosmos/evm/x/vm/statedb" "github.com/cosmos/evm/x/vm/types" errorsmod "cosmossdk.io/errors" @@ -22,15 +23,9 @@ import ( ) // CallEVM performs a smart contract method call using given args. -func (k Keeper) CallEVM( - ctx sdk.Context, - abi abi.ABI, - from, contract common.Address, - commit bool, - gasCap *big.Int, - method string, - args ...interface{}, -) (*types.MsgEthereumTxResponse, error) { +// Note: if you call this from a precompile context, ensure that +// you use the existing stateDB. +func (k Keeper) CallEVM(ctx sdk.Context, stateDB *statedb.StateDB, abi abi.ABI, from, contract common.Address, commit, callFromPrecompile bool, gasCap *big.Int, method string, args ...interface{}) (*types.MsgEthereumTxResponse, error) { data, err := abi.Pack(method, args...) if err != nil { return nil, errorsmod.Wrap( @@ -39,7 +34,7 @@ func (k Keeper) CallEVM( ) } - resp, err := k.CallEVMWithData(ctx, from, &contract, data, commit, gasCap) + resp, err := k.CallEVMWithData(ctx, stateDB, from, &contract, data, commit, callFromPrecompile, gasCap) if err != nil { return resp, errorsmod.Wrapf(err, "contract call failed: method '%s', contract '%s'", method, contract) } @@ -47,14 +42,9 @@ func (k Keeper) CallEVM( } // CallEVMWithData performs a smart contract method call using contract data. -func (k Keeper) CallEVMWithData( - ctx sdk.Context, - from common.Address, - contract *common.Address, - data []byte, - commit bool, - gasCap *big.Int, -) (*types.MsgEthereumTxResponse, error) { +// Note: if you call this from a precompile context, ensure that +// you use the existing stateDB. +func (k Keeper) CallEVMWithData(ctx sdk.Context, stateDB *statedb.StateDB, from common.Address, contract *common.Address, data []byte, commit bool, callFromPrecompile bool, gasCap *big.Int) (*types.MsgEthereumTxResponse, error) { nonce, err := k.accountKeeper.GetSequence(ctx, from.Bytes()) if err != nil { return nil, err @@ -91,27 +81,39 @@ func (k Keeper) CallEVMWithData( AccessList: ethtypes.AccessList{}, } - // Use a cache context so that a reverting EVM call does not commit its state - // changes. The cache shares the parent gas meter, so gas is metered against - // the caller either way; only the store writes are discarded on failure. - tmpCtx, commitState := ctx.CacheContext() - res, err := k.ApplyMessage(tmpCtx, msg, nil, commit, true) + // v0.6.0: the StateDB is supplied by the caller and ApplyMessage rejects a nil + // StateDB (returns ErrNilStateDB). Pass it (and callFromPrecompile) straight + // through so that contract — and the precompile snapshot/flush chain — is + // preserved. + // + // This replaces the ctx.CacheContext() sandbox the pre-v0.6.0 code wrapped + // around ApplyMessage: state isolation on a revert is now the StateDB's + // journal/snapshot job, not a store branch. Gas accounting is unaffected — + // ApplyMessage never touches ctx.GasMeter(), so the explicit ConsumeGas calls + // below remain the only charge to the parent meter, exactly as before. + res, err := k.ApplyMessage(ctx, stateDB, msg, nil, commit, callFromPrecompile, true) if err != nil { return nil, err } if res.Failed() { // A failed execution still burned real EVM work, so charge it to the parent - // Cosmos gas meter exactly like a successful one. Skipping this made a - // revert (or a deliberate out-of-gas) a free way to consume block compute. - // msg.GasLimit above is config.DefaultGasCap, which a caller-supplied gasCap - // can only narrow, so res.GasUsed — which equals the gas limit on - // out-of-gas — is bounded by that cap and cannot be inflated by the caller. + // Cosmos gas meter exactly like a successful one (F-2026-18824). Skipping + // this made a revert (or a deliberate out-of-gas) a free way to consume + // block compute. msg.GasLimit above is config.DefaultGasCap, which a + // caller-supplied gasCap can only narrow, so res.GasUsed — which equals the + // gas limit on out-of-gas — is bounded by that cap and cannot be inflated by + // the caller. + // + // Note we charge exactly res.GasUsed and, unlike upstream, deliberately do + // NOT call k.ResetGasMeterAndConsumeGas(ctx, ctx.GasMeter().Limit()): + // consuming the full gas limit on a revert can overflow the parent gas + // meter. The EVM has already rolled back the reverted call frame via its own + // snapshot, so no extra state isolation is required. ctx.GasMeter().ConsumeGas(res.GasUsed, "apply evm message (failed)") return res, errorsmod.Wrap(types.ErrVMExecution, res.VmError) } - commitState() ctx.GasMeter().ConsumeGas(res.GasUsed, "apply evm message") return res, nil @@ -276,7 +278,11 @@ func (k Keeper) DerivedEVMCallWithData( // thus restricted to be used only inside `ApplyMessage`. tmpCtx, commitState := ctx.CacheContext() - res, err := k.ApplyMessageWithConfig(tmpCtx, msg, nil, commit, cfg, txConfig, true, nil) + // v0.6.0: the StateDB is created by the caller and passed in. Derived txs are + // not precompile calls, so callFromPrecompile is false and the StateDB lives + // on the cache context (committed only when both tx and hooks succeed). + stateDB := statedb.New(tmpCtx, &k, txConfig) + res, err := k.ApplyMessageWithConfig(tmpCtx, stateDB, msg, nil, commit, false, cfg, txConfig, true, nil) if err != nil { return nil, err } diff --git a/x/vm/keeper/grpc_query.go b/x/vm/keeper/grpc_query.go index 1bc1f959b..7af0e763e 100644 --- a/x/vm/keeper/grpc_query.go +++ b/x/vm/keeper/grpc_query.go @@ -265,7 +265,8 @@ func (k Keeper) EthCall(c context.Context, req *types.EthCallRequest) (*types.Ms txConfig := statedb.NewEmptyTxConfig() // pass false to not commit StateDB - res, err := k.ApplyMessageWithConfig(ctx, *msg, nil, false, cfg, txConfig, false, overrides) + stateDB := statedb.New(ctx, &k, txConfig) + res, err := k.ApplyMessageWithConfig(ctx, stateDB, *msg, nil, false, false, cfg, txConfig, false, overrides) if err != nil { return nil, status.Error(codes.Internal, err.Error()) } @@ -405,7 +406,8 @@ func (k Keeper) EstimateGasInternal(c context.Context, req *types.EthCallRequest tmpCtx = buildTraceCtx(tmpCtx, msg.GasLimit) } // pass false to not commit StateDB - rsp, err = k.ApplyMessageWithConfig(tmpCtx, *msg, nil, false, cfg, txConfig, false, nil) + stateDB := statedb.New(tmpCtx, &k, txConfig) + rsp, err = k.ApplyMessageWithConfig(tmpCtx, stateDB, *msg, nil, false, false, cfg, txConfig, false, nil) if err != nil { if errors.Is(err, core.ErrIntrinsicGas) || errors.Is(err, core.ErrFloorDataGas) { return true, nil, nil // Special case, raise gas limit @@ -566,7 +568,8 @@ func (k Keeper) TraceTx(c context.Context, req *types.QueryTraceTxRequest) (*typ ctx = buildTraceCtx(ctx, msg.GasLimit) // we ignore the error here. this endpoint, ideally, is called internally from the ETH backend, which will call this query // using all previous txs in the trace transaction's block. some of those _could_ be invalid transactions. - rsp, _ := k.ApplyMessageWithConfig(ctx, *msg, nil, true, cfg, txConfig, false, nil) + stateDB := statedb.New(ctx, &k, txConfig) + rsp, _ := k.ApplyMessageWithConfig(ctx, stateDB, *msg, nil, true, false, cfg, txConfig, false, nil) if rsp != nil { ctx.GasMeter().ConsumeGas(rsp.GasUsed, "evm predecessor tx") txConfig.LogIndex += uint(len(rsp.Logs)) @@ -853,7 +856,8 @@ func (k *Keeper) traceTxWithMsg( // Build EVM execution context ctx = buildTraceCtx(ctx, msg.GasLimit) - res, err := k.ApplyMessageWithConfig(ctx, *msg, tracer.Hooks, commitMessage, cfg, txConfig, false, nil) + stateDB := statedb.New(ctx, k, txConfig) + res, err := k.ApplyMessageWithConfig(ctx, stateDB, *msg, tracer.Hooks, commitMessage, false, cfg, txConfig, false, nil) if err != nil { return nil, 0, status.Error(codes.Internal, err.Error()) } diff --git a/x/vm/keeper/state_transition.go b/x/vm/keeper/state_transition.go index 4a69f4de7..62d314ab5 100644 --- a/x/vm/keeper/state_transition.go +++ b/x/vm/keeper/state_transition.go @@ -214,7 +214,8 @@ func (k *Keeper) ApplyTransaction(ctx sdk.Context, tx *ethtypes.Transaction) (*t tmpCtx, commitFn := ctx.CacheContext() // pass true to commit the StateDB - res, err := k.ApplyMessageWithConfig(tmpCtx, *msg, nil, true, cfg, txConfig, false, nil) + stateDB := statedb.New(tmpCtx, k, txConfig) + res, err := k.ApplyMessageWithConfig(tmpCtx, stateDB, *msg, nil, true, false, cfg, txConfig, false, nil) if err != nil { // when a transaction contains multiple msg, as long as one of the msg fails // all gas will be deducted. so is not msg.Gas() @@ -335,14 +336,16 @@ func (k *Keeper) ApplyTransaction(ctx sdk.Context, tx *ethtypes.Transaction) (*t } // ApplyMessage calls ApplyMessageWithConfig with an empty TxConfig. -func (k *Keeper) ApplyMessage(ctx sdk.Context, msg core.Message, tracer *tracing.Hooks, commit bool, internal bool) (*types.MsgEthereumTxResponse, error) { +// Note: if you call this from a precompile context, ensure that +// you use the existing stateDB. +func (k *Keeper) ApplyMessage(ctx sdk.Context, stateDB *statedb.StateDB, msg core.Message, tracer *tracing.Hooks, commit, callFromPrecompile, internal bool) (*types.MsgEthereumTxResponse, error) { cfg, err := k.EVMConfig(ctx, ctx.BlockHeader().ProposerAddress) if err != nil { return nil, errorsmod.Wrap(err, "failed to load evm config") } txConfig := statedb.NewEmptyTxConfig() - return k.ApplyMessageWithConfig(ctx, msg, tracer, commit, cfg, txConfig, internal, nil) + return k.ApplyMessageWithConfig(ctx, stateDB, msg, tracer, commit, callFromPrecompile, cfg, txConfig, internal, nil) } // ApplyMessageWithConfig computes the new state by applying the given message against the existing state. @@ -382,23 +385,15 @@ func (k *Keeper) ApplyMessage(ctx sdk.Context, msg core.Message, tracer *tracing // // # Commit parameter // -// If commit is true, the `StateDB` will be committed, otherwise discarded. -func (k *Keeper) ApplyMessageWithConfig( - ctx sdk.Context, - msg core.Message, - tracer *tracing.Hooks, - commit bool, - cfg *statedb.EVMConfig, - txConfig statedb.TxConfig, - internal bool, - overrides *rpctypes.StateOverride, -) (*types.MsgEthereumTxResponse, error) { +// If commit is true, the `StateDB` will be committed or flushed (if called from within a precompile), otherwise discarded. +func (k *Keeper) ApplyMessageWithConfig(ctx sdk.Context, stateDB *statedb.StateDB, msg core.Message, tracer *tracing.Hooks, commit bool, callFromPrecompile bool, cfg *statedb.EVMConfig, txConfig statedb.TxConfig, internal bool, overrides *rpctypes.StateOverride) (*types.MsgEthereumTxResponse, error) { var ( ret []byte // return bytes from evm execution vmErr error // vm errors do not effect consensus and are therefore not assigned to err ) - - stateDB := statedb.New(ctx, k, txConfig) + if stateDB == nil { + return nil, types.ErrNilStateDB + } ethCfg := types.GetEthChainConfig() evm := k.NewEVMWithOverridePrecompiles(ctx, msg, cfg, tracer, stateDB, overrides == nil) // Gas limit suffices for the floor data cost (EIP-7623) @@ -456,7 +451,10 @@ func (k *Keeper) ApplyMessageWithConfig( // access list preparation is moved from ante handler to here, because it's needed when `ApplyMessage` is called // under contexts where ante handlers are not run, for example `eth_call` and `eth_estimateGas`. - stateDB.Prepare(rules, msg.From, common.Address{}, msg.To, evm.ActivePrecompiles(), msg.AccessList) + // If we're in a nested precompile scenario, then we don't want to prepare the stateDB a second time. + if !callFromPrecompile { + stateDB.Prepare(rules, msg.From, common.Address{}, msg.To, evm.ActivePrecompiles(), msg.AccessList) + } convertedValue, err := utils.Uint256FromBigInt(msg.Value) if err != nil { @@ -523,11 +521,18 @@ func (k *Keeper) ApplyMessageWithConfig( // The dirty states in `StateDB` is either committed or discarded after return if commit { - if err := stateDB.Commit(); err != nil { - return nil, errorsmod.Wrap(err, "failed to commit stateDB") + // In a precompile context, we never want to commit, as that will collapse the cache stack. + // Instead, we want to flush to the cacheCtx. + if callFromPrecompile { + if err := stateDB.FlushToCacheCtx(); err != nil { + return nil, errorsmod.Wrap(err, "failed to flush stateDB to cacheCtx") + } + } else { + if err := stateDB.Commit(); err != nil { + return nil, errorsmod.Wrap(err, "failed to commit stateDB") + } } } - // calculate a minimum amount of gas to be charged to sender if GasLimit // is considerably higher than GasUsed to stay more aligned with CometBFT gas mechanics // for more info https://github.com/evmos/ethermint/issues/1085 diff --git a/x/vm/keeper/statedb.go b/x/vm/keeper/statedb.go index 828e0da60..7b6a66269 100644 --- a/x/vm/keeper/statedb.go +++ b/x/vm/keeper/statedb.go @@ -19,6 +19,15 @@ import ( var _ statedb.Keeper = &Keeper{} +// NewStateDB returns a fresh StateDB bound to this keeper and ctx, using an +// empty TxConfig. It is a convenience for callers that reach the EVM keeper +// through an interface (e.g. other Cosmos modules) and therefore cannot call +// statedb.New directly, but must supply a non-nil StateDB to CallEVM / +// ApplyMessage as required since cosmos/evm v0.6.0. +func (k *Keeper) NewStateDB(ctx sdk.Context) *statedb.StateDB { + return statedb.New(ctx, k, statedb.NewEmptyTxConfig()) +} + // ---------------------------------------------------------------------------- // StateDB Keeper implementation // ---------------------------------------------------------------------------- diff --git a/x/vm/statedb/balance_events_test.go b/x/vm/statedb/balance_events_test.go new file mode 100644 index 000000000..a50dbf202 --- /dev/null +++ b/x/vm/statedb/balance_events_test.go @@ -0,0 +1,113 @@ +package statedb + +import ( + "testing" + + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// mockSnapshotter is a simple mock implementation of the Snapshotter interface for testing. +type mockSnapshotter struct { + snapshots []int + current int +} + +func newMockSnapshotter() *mockSnapshotter { + return &mockSnapshotter{ + snapshots: []int{}, + current: 0, + } +} + +func (m *mockSnapshotter) Snapshot() int { + id := m.current + m.snapshots = append(m.snapshots, id) + m.current++ + return id +} + +func (m *mockSnapshotter) RevertToSnapshot(snapshot int) { + // Find the snapshot index + for i, s := range m.snapshots { + if s == snapshot { + m.snapshots = m.snapshots[:i+1] + m.current = snapshot + 1 + return + } + } +} + +// TestRevertToSnapshot_ProcessedEventsInvariant verifies the invariant: +// "After any revert, processedEventsCount <= current event count" +// This tests cacheCtx event manager behavior during EVM execution with precompile calls and reverts. +func TestRevertToSnapshot_ProcessedEventsInvariant(t *testing.T) { + // Test each revert scenario independently since reverting invalidates future snapshots + testCases := []struct { + name string + numPrecompiles int + revertToIndex int + expectedEvents int + }{ + {"revert to 5 precompile calls", 10, 5, 5}, + {"revert to 2 precompile calls", 10, 2, 2}, + {"revert to 0 precompile calls", 10, 0, 0}, + {"revert to 8 precompile calls", 10, 8, 8}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mockSnap := newMockSnapshotter() + stateDB := &StateDB{ + validRevisions: []revision{}, + nextRevisionID: 0, + journal: newJournal(), + snapshotter: mockSnap, + } + + ctx := sdk.Context{}.WithEventManager(sdk.NewEventManager()) + cacheCtx := sdk.Context{}.WithEventManager(sdk.NewEventManager()) + stateDB.ctx = ctx + stateDB.cacheCtx = cacheCtx + stateDB.writeCache = func() {} // Simulate cache exists (after first precompile call) + + // Snapshot with 0 events (before any precompile calls) + snapshots := []int{stateDB.Snapshot()} + + // Simulate precompile calls - each emits an event + for i := 0; i < tc.numPrecompiles; i++ { + // Create multi-store snapshot for precompile journal entry + multiStoreSnapshot := mockSnap.Snapshot() + + // Add precompile journal entry (captures events before precompile) + err := stateDB.AddPrecompileFn(multiStoreSnapshot) + require.NoError(t, err) + + // Emit event during "precompile execution" + cacheCtx.EventManager().EmitEvent(sdk.NewEvent("test", sdk.NewAttribute("count", string(rune(i))))) + + // Update processed events count (simulates FlushToCacheCtx) + stateDB.processedEventsCount = len(cacheCtx.EventManager().Events()) + + // Take snapshot after precompile + snap := stateDB.Snapshot() + snapshots = append(snapshots, snap) + } + + // Revert to the target snapshot + stateDB.RevertToSnapshot(snapshots[tc.revertToIndex]) + + currentEventCount := len(cacheCtx.EventManager().Events()) + require.Equal(t, tc.expectedEvents, currentEventCount, "event count mismatch after revert") + + // Verify invariant: processedEventsCount <= current event count + require.LessOrEqual(t, stateDB.processedEventsCount, currentEventCount, + "processedEventsCount %d exceeds current event count %d", + stateDB.processedEventsCount, currentEventCount) + + require.Equal(t, tc.expectedEvents, stateDB.processedEventsCount, + "processedEventsCount should match expected event count") + }) + } +} diff --git a/x/vm/statedb/journal.go b/x/vm/statedb/journal.go index 3ae0817dd..35bce1d90 100644 --- a/x/vm/statedb/journal.go +++ b/x/vm/statedb/journal.go @@ -144,8 +144,9 @@ type ( slot *common.Hash } precompileCallChange struct { - snapshot int - events sdk.Events + snapshot int + prevEvents sdk.Events + prevProcessedEventCount int } createContractChange struct { account *common.Address @@ -180,7 +181,17 @@ func (ch createContractChange) Dirtied() *common.Address { func (pc precompileCallChange) Revert(s *StateDB) { // rollback multi store from cache ctx to the previous // state stored in the snapshot - s.RevertMultiStore(pc.snapshot, pc.events) + s.RevertMultiStore(pc.snapshot) + + // Restore events to the state before this precompile call. OverrideEvents + // sets the manager's events in place (em.events = events), which is required + // here: precompiles hold the cacheCtx obtained from GetCacheContext() and + // share this EventManager pointer, so replacing the manager would diverge + // from those outstanding copies. + s.cacheCtx.EventManager().OverrideEvents(pc.prevEvents) + + // Restore processed events counter + s.processedEventsCount = pc.prevProcessedEventCount } func (pc precompileCallChange) Dirtied() *common.Address { diff --git a/x/vm/statedb/statedb.go b/x/vm/statedb/statedb.go index 56ee390a2..51972629f 100644 --- a/x/vm/statedb/statedb.go +++ b/x/vm/statedb/statedb.go @@ -34,7 +34,6 @@ import ( type revision struct { id int journalIndex int - events sdk.Events } var _ vm.StateDB = &StateDB{} @@ -80,6 +79,12 @@ type StateDB struct { // The count of calls to precompiles precompileCallsCounter uint8 + + // processedEventsCount tracks how many events have been + // processed by BalanceHandler. Events are processed sequentially starting + // from index 0. Event counter tracks events to avoid having them reprocessed. + // On revert, this counter is rewound to the snapshot's event count. + processedEventsCount int } func (s *StateDB) CreateContract(address common.Address) { @@ -150,13 +155,14 @@ func New(ctx sdk.Context, keeper Keeper, txConfig TxConfig) *StateDB { ctx = ctx.WithEventManager(sdk.NewEventManager()) } return &StateDB{ - keeper: keeper, - ctx: ctx, - stateObjects: make(map[common.Address]*stateObject), - journal: newJournal(), - accessList: newAccessList(), - transientStorage: newTransientStorage(), - txConfig: txConfig, + keeper: keeper, + ctx: ctx, + stateObjects: make(map[common.Address]*stateObject), + journal: newJournal(), + accessList: newAccessList(), + transientStorage: newTransientStorage(), + txConfig: txConfig, + processedEventsCount: len(ctx.EventManager().Events()), } } @@ -187,12 +193,8 @@ func (s *StateDB) MultiStoreSnapshot() int { return s.snapshotter.Snapshot() } -func (s *StateDB) RevertMultiStore(snapshot int, events sdk.Events) { +func (s *StateDB) RevertMultiStore(snapshot int) { s.snapshotter.RevertToSnapshot(snapshot) - s.writeCache = func() { - s.ctx.EventManager().EmitEvents(events) - s.cacheCtx.MultiStore().(storetypes.CacheMultiStore).Write() - } } // cache creates the stateDB cache context @@ -211,7 +213,8 @@ func (s *StateDB) cache() error { s.snapshotter = snapshotStore s.cacheCtx = s.cacheCtx.WithMultiStore(snapshotStore) s.writeCache = func() { - s.ctx.EventManager().EmitEvents(s.cacheCtx.EventManager().Events()) + eventsToEmit := s.cacheCtx.EventManager().Events() + s.ctx.EventManager().EmitEvents(eventsToEmit) s.cacheCtx.MultiStore().(storetypes.CacheMultiStore).Write() } @@ -436,10 +439,14 @@ func (s *StateDB) setStateObject(object *stateObject) { // AddPrecompileFn adds a precompileCall journal entry // with a snapshot of the multi-store and events previous // to the precompile call. -func (s *StateDB) AddPrecompileFn(snapshot int, events sdk.Events) error { +func (s *StateDB) AddPrecompileFn(snapshot int) error { + // Capture events before the precompile call + prevEvents := s.cacheCtx.EventManager().Events() + s.journal.append(precompileCallChange{ - snapshot: snapshot, - events: events, + snapshot: snapshot, + prevEvents: prevEvents, + prevProcessedEventCount: s.processedEventsCount, }) s.precompileCallsCounter++ if s.precompileCallsCounter > types.MaxPrecompileCalls { @@ -448,6 +455,23 @@ func (s *StateDB) AddPrecompileFn(snapshot int, events sdk.Events) error { return nil } +// MarkEventProcessed records that the event at the given index +// has been seen by BalanceHandler. Events must be marked sequentially. +func (s *StateDB) MarkEventProcessed(idx int) { + // Events must be processed sequentially - idx should equal current count + if idx != s.processedEventsCount { + panic(fmt.Sprintf("balance events must be processed sequentially: expected %d, got %d", + s.processedEventsCount, idx)) + } + s.processedEventsCount++ +} + +// IsEventProcessed reports whether the event at idx has already been +// seen by a previous AfterBalanceChange invocation. +func (s *StateDB) IsEventProcessed(idx int) bool { + return idx < s.processedEventsCount +} + // AddBalance adds amount to the account associated with addr. func (s *StateDB) AddBalance(addr common.Address, amount *uint256.Int, reason tracing.BalanceChangeReason) uint256.Int { stateObject := s.getOrNewStateObject(addr) @@ -667,7 +691,7 @@ func (s *StateDB) SlotInAccessList(addr common.Address, slot common.Hash) (addre func (s *StateDB) Snapshot() int { id := s.nextRevisionID s.nextRevisionID++ - s.validRevisions = append(s.validRevisions, revision{id, s.journal.length(), s.ctx.EventManager().Events()}) + s.validRevisions = append(s.validRevisions, revision{id, s.journal.length()}) return id } @@ -682,12 +706,8 @@ func (s *StateDB) RevertToSnapshot(revid int) { } snapshot := s.validRevisions[idx].journalIndex - // revert back to snapshotted events - eventManager := sdk.NewEventManager() - eventManager.EmitEvents(s.validRevisions[idx].events) - s.ctx = s.ctx.WithEventManager(eventManager) - // Replay the journal to undo changes and remove invalidated snapshots + // Event restoration is handled by precompileCallChange.Revert() s.journal.Revert(s, snapshot) s.validRevisions = s.validRevisions[:idx] } @@ -703,11 +723,19 @@ func (s *StateDB) Commit() error { return s.commitWithCtx(s.ctx) } -// CommitWithCacheCtx writes the dirty states to keeper using the cacheCtx. +// FlushToCacheCtx writes the dirty states to keeper using the cacheCtx. // This function is used before any precompile call to make sure the cacheCtx // is updated with the latest changes within the tx (StateDB's journal entries). -func (s *StateDB) CommitWithCacheCtx() error { - return s.commitWithCtx(s.cacheCtx) +func (s *StateDB) FlushToCacheCtx() error { + if err := s.commitWithCtx(s.cacheCtx); err != nil { + return err + } + + // Set counter to event count - all flushed events (from mint/burn during commit) are now accounted for. + // This prevents the balance handler from re-adding already flushed events. + s.processedEventsCount = len(s.cacheCtx.EventManager().Events()) + + return nil } // commitWithCtx writes the dirty states to keeper diff --git a/x/vm/statedb/statedb_test.go b/x/vm/statedb/statedb_test.go index 8cea4d214..4e21eb322 100644 --- a/x/vm/statedb/statedb_test.go +++ b/x/vm/statedb/statedb_test.go @@ -58,7 +58,7 @@ func (suite *StateDBTestSuite) TestAccount() { suite.Require().Empty(acct.Balance) suite.Require().False(acct.HasCodeHash()) - db = statedb.New(sdk.Context{}, keeper, emptyTxConfig) + db = statedb.New(sdk.Context{}.WithEventManager(sdk.NewEventManager()), keeper, emptyTxConfig) suite.Require().Equal(true, db.Exist(address)) suite.Require().Equal(true, db.Empty(address)) suite.Require().Equal(common.U2560, db.GetBalance(address)) @@ -81,7 +81,7 @@ func (suite *StateDBTestSuite) TestAccount() { suite.Require().NoError(db.Commit()) // SelfDestruct - db = statedb.New(sdk.Context{}, db.Keeper(), emptyTxConfig) + db = statedb.New(sdk.Context{}.WithEventManager(sdk.NewEventManager()), db.Keeper(), emptyTxConfig) suite.Require().False(db.HasSelfDestructed(address)) db.SelfDestruct(address) @@ -96,7 +96,7 @@ func (suite *StateDBTestSuite) TestAccount() { suite.Require().NoError(db.Commit()) // not accessible from StateDB anymore - db = statedb.New(sdk.Context{}, db.Keeper(), emptyTxConfig) + db = statedb.New(sdk.Context{}.WithEventManager(sdk.NewEventManager()), db.Keeper(), emptyTxConfig) suite.Require().False(db.Exist(address)) // and cleared in keeper too @@ -134,7 +134,7 @@ func (suite *StateDBTestSuite) TestAccount() { suite.Require().NoError(db.Commit()) // not accessible from StateDB anymore - db = statedb.New(sdk.Context{}, db.Keeper(), emptyTxConfig) + db = statedb.New(sdk.Context{}.WithEventManager(sdk.NewEventManager()), db.Keeper(), emptyTxConfig) suite.Require().False(db.Exist(address)) // and cleared in keeper too @@ -159,7 +159,7 @@ func (suite *StateDBTestSuite) TestAccount() { suite.Require().NoError(db.Commit()) // SelfDestruct - db = statedb.New(sdk.Context{}, db.Keeper(), emptyTxConfig) + db = statedb.New(sdk.Context{}.WithEventManager(sdk.NewEventManager()), db.Keeper(), emptyTxConfig) suite.Require().False(db.HasSelfDestructed(address)) _, _ = db.SelfDestruct6780(address) @@ -172,7 +172,7 @@ func (suite *StateDBTestSuite) TestAccount() { suite.Require().NoError(db.Commit()) // Same-tx maintains state - db = statedb.New(sdk.Context{}, db.Keeper(), emptyTxConfig) + db = statedb.New(sdk.Context{}.WithEventManager(sdk.NewEventManager()), db.Keeper(), emptyTxConfig) suite.Require().True(db.Exist(address)) suite.Require().False(db.HasSelfDestructed(address)) // but code and state are still accessible in dirty state @@ -193,7 +193,7 @@ func (suite *StateDBTestSuite) TestAccount() { suite.Run(tc.name, func() { ctx := sdk.Context{}.WithEventManager(sdk.NewEventManager()) keeper := mocks.NewEVMKeeper() - db := statedb.New(sdk.Context{}, keeper, emptyTxConfig) + db := statedb.New(sdk.Context{}.WithEventManager(sdk.NewEventManager()), keeper, emptyTxConfig) tc.malleate(ctx, db) }) } @@ -201,7 +201,7 @@ func (suite *StateDBTestSuite) TestAccount() { func (suite *StateDBTestSuite) TestAccountOverride() { keeper := mocks.NewEVMKeeper() - db := statedb.New(sdk.Context{}, keeper, emptyTxConfig) + db := statedb.New(sdk.Context{}.WithEventManager(sdk.NewEventManager()), keeper, emptyTxConfig) // test balance carry over when overwritten amount := uint256.NewInt(1) @@ -233,7 +233,7 @@ func (suite *StateDBTestSuite) TestDBError() { }}, } for _, tc := range testCases { - db := statedb.New(sdk.Context{}, mocks.NewEVMKeeper(), emptyTxConfig) + db := statedb.New(sdk.Context{}.WithEventManager(sdk.NewEventManager()), mocks.NewEVMKeeper(), emptyTxConfig) tc.malleate(db) suite.Require().Error(db.Commit()) } @@ -276,7 +276,7 @@ func (suite *StateDBTestSuite) TestBalance() { suite.Run(tc.name, func() { ctx := sdk.Context{}.WithEventManager(sdk.NewEventManager()) keeper := mocks.NewEVMKeeper() - db := statedb.New(sdk.Context{}, keeper, emptyTxConfig) + db := statedb.New(sdk.Context{}.WithEventManager(sdk.NewEventManager()), keeper, emptyTxConfig) tc.malleate(db) // check dirty state @@ -331,7 +331,7 @@ func (suite *StateDBTestSuite) TestState() { suite.Run(tc.name, func() { ctx := sdk.Context{}.WithEventManager(sdk.NewEventManager()) keeper := mocks.NewEVMKeeper() - db := statedb.New(sdk.Context{}, keeper, emptyTxConfig) + db := statedb.New(sdk.Context{}.WithEventManager(sdk.NewEventManager()), keeper, emptyTxConfig) tc.malleate(db) suite.Require().NoError(db.Commit()) @@ -341,7 +341,7 @@ func (suite *StateDBTestSuite) TestState() { } // check ForEachStorage - db = statedb.New(sdk.Context{}, keeper, emptyTxConfig) + db = statedb.New(sdk.Context{}.WithEventManager(sdk.NewEventManager()), keeper, emptyTxConfig) collected := CollectContractStorage(db) if len(tc.expStates) > 0 { suite.Require().Equal(tc.expStates, collected) @@ -374,7 +374,7 @@ func (suite *StateDBTestSuite) TestCode() { for _, tc := range testCases { suite.Run(tc.name, func() { keeper := mocks.NewEVMKeeper() - db := statedb.New(sdk.Context{}, keeper, emptyTxConfig) + db := statedb.New(sdk.Context{}.WithEventManager(sdk.NewEventManager()), keeper, emptyTxConfig) tc.malleate(db) // check dirty state @@ -385,7 +385,7 @@ func (suite *StateDBTestSuite) TestCode() { suite.Require().NoError(db.Commit()) // check again - db = statedb.New(sdk.Context{}, keeper, emptyTxConfig) + db = statedb.New(sdk.Context{}.WithEventManager(sdk.NewEventManager()), keeper, emptyTxConfig) suite.Require().Equal(tc.expCode, db.GetCode(address)) suite.Require().Equal(len(tc.expCode), db.GetCodeSize(address)) suite.Require().Equal(tc.expCodeHash, db.GetCodeHash(address)) @@ -495,7 +495,7 @@ func (suite *StateDBTestSuite) TestNestedSnapshot() { } func (suite *StateDBTestSuite) TestInvalidSnapshotId() { - db := statedb.New(sdk.Context{}, mocks.NewEVMKeeper(), emptyTxConfig) + db := statedb.New(sdk.Context{}.WithEventManager(sdk.NewEventManager()), mocks.NewEVMKeeper(), emptyTxConfig) suite.Require().Panics(func() { db.RevertToSnapshot(1) }) @@ -586,7 +586,7 @@ func (suite *StateDBTestSuite) TestAccessList() { } for _, tc := range testCases { - db := statedb.New(sdk.Context{}, mocks.NewEVMKeeper(), emptyTxConfig) + db := statedb.New(sdk.Context{}.WithEventManager(sdk.NewEventManager()), mocks.NewEVMKeeper(), emptyTxConfig) tc.malleate(db) } } @@ -598,7 +598,7 @@ func (suite *StateDBTestSuite) TestLog() { txHash, 1, 1, ) - db := statedb.New(sdk.Context{}, mocks.NewEVMKeeper(), txConfig) + db := statedb.New(sdk.Context{}.WithEventManager(sdk.NewEventManager()), mocks.NewEVMKeeper(), txConfig) data := []byte("hello world") db.AddLog(ðtypes.Log{ Address: address, @@ -648,7 +648,7 @@ func (suite *StateDBTestSuite) TestRefund() { }, 0, true}, } for _, tc := range testCases { - db := statedb.New(sdk.Context{}, mocks.NewEVMKeeper(), emptyTxConfig) + db := statedb.New(sdk.Context{}.WithEventManager(sdk.NewEventManager()), mocks.NewEVMKeeper(), emptyTxConfig) if !tc.expPanic { tc.malleate(db) suite.Require().Equal(tc.expRefund, db.GetRefund()) @@ -669,7 +669,7 @@ func (suite *StateDBTestSuite) TestIterateStorage() { value2 := common.BigToHash(big.NewInt(4)) keeper := mocks.NewEVMKeeper() - db := statedb.New(sdk.Context{}, keeper, emptyTxConfig) + db := statedb.New(sdk.Context{}.WithEventManager(sdk.NewEventManager()), keeper, emptyTxConfig) db.SetState(address, key1, value1) db.SetState(address, key2, value2) @@ -725,7 +725,7 @@ func (suite *StateDBTestSuite) TestSetStorage() { for _, tc := range testCases { suite.Run(tc.name, func() { keeper := mocks.NewEVMKeeper() - db := statedb.New(sdk.Context{}, keeper, emptyTxConfig) + db := statedb.New(sdk.Context{}.WithEventManager(sdk.NewEventManager()), keeper, emptyTxConfig) for k, v := range tc.prestate { db.SetState(contract, k, v) } diff --git a/x/vm/types/denom_config_testing.go b/x/vm/types/denom_config_testing.go index 9e731d7db..a22eecbb8 100644 --- a/x/vm/types/denom_config_testing.go +++ b/x/vm/types/denom_config_testing.go @@ -61,25 +61,41 @@ func setDisplayDenom(displayDenom string) error { return nil } +// getTestingEvmCoinInfo returns the configured testing coin info, falling back to +// a non-nil zero value when it is momentarily nil. +// +// NOTE (push-chain): the configurator resets the global to nil (resetEVMCoinInfo) +// before re-setting it between test cases, creating a brief window in which a +// leaked background goroutine — e.g. the legacypool reorg loop of a torn-down +// test network — can read it and panic. This mirrors the production +// getEvmCoinInfo() nil-guard in denom_config.go (added for the same class of +// "coin info accessed before initialization" issue). Test-only (build tag test). +func getTestingEvmCoinInfo() *EvmCoinInfo { + if testingEvmCoinInfo == nil { + return &EvmCoinInfo{} + } + return testingEvmCoinInfo +} + // GetEVMCoinDecimals returns the decimals used in the representation of the EVM // coin. func GetEVMCoinDecimals() Decimals { - return Decimals(testingEvmCoinInfo.Decimals) + return Decimals(getTestingEvmCoinInfo().Decimals) } // GetEVMCoinDenom returns the denom used for the EVM coin. func GetEVMCoinDenom() string { - return testingEvmCoinInfo.Denom + return getTestingEvmCoinInfo().Denom } // GetEVMCoinExtendedDenom returns the extended denom used for the EVM coin. func GetEVMCoinExtendedDenom() string { - return testingEvmCoinInfo.ExtendedDenom + return getTestingEvmCoinInfo().ExtendedDenom } // GetEVMCoinDisplayDenom returns the display denom used for the EVM coin. func GetEVMCoinDisplayDenom() string { - return testingEvmCoinInfo.DisplayDenom + return getTestingEvmCoinInfo().DisplayDenom } // setTestingEVMCoinInfo allows to define denom and decimals of the coin used in the EVM. diff --git a/x/vm/types/errors.go b/x/vm/types/errors.go index 823a16e4e..7aa590e2b 100644 --- a/x/vm/types/errors.go +++ b/x/vm/types/errors.go @@ -32,6 +32,7 @@ const ( codeErrABIPack codeErrABIUnpack codeErrInvalidPreinstall + codeErrNilStateDB ) var ( @@ -94,6 +95,9 @@ var ( // RevertSelector is selector of ErrExecutionReverted RevertSelector = crypto.Keccak256([]byte("Error(string)"))[:4] + + // ErrNilStateDB + ErrNilStateDB = errorsmod.Register(ModuleName, codeErrNilStateDB, "stateDB cannot be nil") ) // RevertReasonBytes converts a message to ABI-encoded revert bytes. diff --git a/x/vm/types/msg.go b/x/vm/types/msg.go index 19bda468f..f4d7ff6e6 100644 --- a/x/vm/types/msg.go +++ b/x/vm/types/msg.go @@ -354,8 +354,3 @@ func (m *MsgUpdateParams) ValidateBasic() error { return m.Params.Validate() } - -// GetSignBytes implements the LegacyMsg interface. -func (m MsgUpdateParams) GetSignBytes() []byte { - return sdk.MustSortJSON(AminoCdc.MustMarshalJSON(&m)) -} From 3e63a077effc8b0bbc75c6a4c319bec780162a2f Mon Sep 17 00:00:00 2001 From: Nilesh Gupta Date: Wed, 26 Aug 2026 21:22:25 +0530 Subject: [PATCH 11/15] fix: clamp the fee market's cumulative gas wanted instead of failing the block (F-2026-18144) (#50) EndBlock returned an error when the block's cumulative gasWanted (or gasUsed) did not fit in an int64. gasWanted is an unchecked uint64 sum of per-tx declarations, so under max_gas: -1 two txs declaring MaxInt64 land it in (MaxInt64, MaxUint64] with neither individually rejectable, and the error surfaces through FinalizeBlock after the block is decided. Clamp both to MaxInt64 and log instead. AddTransientGasWanted now saturates rather than wrapping. --- CHANGELOG.md | 9 +++ tests/integration/x/feemarket/test_abci.go | 71 ++++++++++++++++++++++ x/feemarket/keeper/abci.go | 18 ++++-- x/feemarket/keeper/keeper.go | 12 +++- 4 files changed, 102 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 433b72113..8e09c73f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,15 @@ Follow the [migration document](docs/migrations/v0.5.x_to_v0.6.0.md) for upgrade the executor with its signature never checked, letting a victim-signed transaction be replayed as the victim. `VerifySender` is now required before `ApplyTransaction`, so `From` is always the ECDSA-recovered signer regardless of how the message arrived. +- Clamp the fee market's cumulative gas wanted instead of failing the block. `EndBlock` converted + both the block's cumulative `gasWanted` and its `gasUsed` to `int64` and returned an error when + either did not fit. `gasWanted` is an unchecked uint64 sum of per-transaction declarations, and + under `max_gas: -1` nothing bounds a single declaration, so two transactions declaring `MaxInt64` + sum into `(MaxInt64, MaxUint64]` with neither being individually rejectable. An error out of + `EndBlock` surfaces through `FinalizeBlock` after the block has been decided, leaving CometBFT at + height H and the application at H-1. Both values are now clamped to `MaxInt64` and logged. + `AddTransientGasWanted` also saturates instead of wrapping, so the total can never silently roll + over to a small number. - Reject a `x/erc20` conversion whose token `transfer` emits an unexpected `Approval` event. Both native-ERC20 convert directions escrow the token and mint (or burn) the paired coins, checking only that the transfer succeeded and that the balance moved by the right amount. A registered diff --git a/tests/integration/x/feemarket/test_abci.go b/tests/integration/x/feemarket/test_abci.go index df7a0f5eb..1c15af3e3 100644 --- a/tests/integration/x/feemarket/test_abci.go +++ b/tests/integration/x/feemarket/test_abci.go @@ -1,6 +1,8 @@ package feemarket import ( + "math" + "github.com/cosmos/evm/testutil/integration/evm/network" storetypes "cosmossdk.io/store/types" @@ -59,3 +61,72 @@ func (s *KeeperTestSuite) TestEndBlock() { }) } } + +// TestEndBlockGasWantedOverflowIsClamped covers F-2026-18144. +// +// The cumulative gas wanted is a uint64 sum of per-transaction declarations. +// Under an unbounded block gas limit (max_gas: -1) nothing caps a single +// declaration, so two transactions declaring MaxInt64 sum into +// (MaxInt64, MaxUint64] - a perfectly valid uint64 that cannot be converted +// back to int64. EndBlock used to return an error for that, and an error out +// of EndBlock surfaces through FinalizeBlock after the block has been decided, +// stopping the chain. It must clamp instead. +func (s *KeeperTestSuite) TestEndBlockGasWantedOverflowIsClamped() { + testCases := []struct { + name string + gasWanted uint64 + expGasWanted uint64 + }{ + { + // 2 * MaxInt64, the two-transaction shape from the finding. + "two MaxInt64 declarations", + uint64(math.MaxInt64)*2 - 1, + // clamped to MaxInt64, then * MinGasMultiplier (0.5) + uint64(math.MaxInt64) / 2, + }, + { + "MaxUint64", + uint64(math.MaxUint64), + uint64(math.MaxInt64) / 2, + }, + { + // Exactly MaxInt64 is representable and must be left alone. + "MaxInt64 is not clamped", + uint64(math.MaxInt64), + uint64(math.MaxInt64) / 2, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + nw := network.NewUnitTestNetwork(s.create, s.options...) + ctx := nw.GetContext().WithBlockGasMeter(storetypes.NewInfiniteGasMeter()) + + nw.App.GetFeeMarketKeeper().SetTransientBlockGasWanted(ctx, tc.gasWanted) + + err := nw.App.GetFeeMarketKeeper().EndBlock(ctx) + + gasWanted := nw.App.GetFeeMarketKeeper().GetBlockGasWanted(ctx) + s.Equal(tc.expGasWanted, gasWanted, tc.name) + s.NoError(err, "EndBlock must not fail the block on an oversized cumulative gas wanted") + }) + } +} + +// TestAddTransientGasWantedSaturates: the accumulation itself must not wrap. +// A wrapped total would silently under-report the block's gas wanted to the +// base fee calculation. +func (s *KeeperTestSuite) TestAddTransientGasWantedSaturates() { + nw := network.NewUnitTestNetwork(s.create, s.options...) + ctx := nw.GetContext() + k := nw.App.GetFeeMarketKeeper() + + total, err := k.AddTransientGasWanted(ctx, math.MaxUint64) + s.Require().NoError(err) + s.Require().Equal(uint64(math.MaxUint64), total) + + total, err = k.AddTransientGasWanted(ctx, 1) + s.Require().NoError(err) + s.Equal(uint64(math.MaxUint64), total, "the cumulative gas wanted must saturate, not wrap to 0") + s.Equal(uint64(math.MaxUint64), k.GetTransientGasWanted(ctx)) +} diff --git a/x/feemarket/keeper/abci.go b/x/feemarket/keeper/abci.go index ec807fa77..01604966d 100644 --- a/x/feemarket/keeper/abci.go +++ b/x/feemarket/keeper/abci.go @@ -3,6 +3,7 @@ package keeper import ( "errors" "fmt" + gomath "math" "github.com/cosmos/evm/x/feemarket/types" @@ -57,16 +58,21 @@ func (k *Keeper) EndBlock(ctx sdk.Context) error { gasWanted := math.NewIntFromUint64(k.GetTransientGasWanted(ctx)) gasUsed := math.NewIntFromUint64(ctx.BlockGasMeter().GasConsumedToLimit()) + // Clamp rather than error. Both quantities are uint64 sums that can exceed + // MaxInt64 without any single transaction being individually rejectable, + // and this runs in EndBlock: an error here surfaces through FinalizeBlock + // after the block has already been decided, which stops the chain. A base + // fee computed from a clamped total is a far better outcome than a halt. + // MinGasMultiplier is validated to be <= 1, so MaxInt64 stays convertible + // to uint64 in the calculation below. if !gasWanted.IsInt64() { - err := fmt.Errorf("integer overflow by integer type conversion. Gas wanted > MaxInt64. Gas wanted: %s", gasWanted) - k.Logger(ctx).Error(err.Error()) - return err + k.Logger(ctx).Error("gas wanted exceeds MaxInt64, clamping", "gas_wanted", gasWanted.String()) + gasWanted = math.NewInt(gomath.MaxInt64) } if !gasUsed.IsInt64() { - err := fmt.Errorf("integer overflow by integer type conversion. Gas used > MaxInt64. Gas used: %s", gasUsed) - k.Logger(ctx).Error(err.Error()) - return err + k.Logger(ctx).Error("gas used exceeds MaxInt64, clamping", "gas_used", gasUsed.String()) + gasUsed = math.NewInt(gomath.MaxInt64) } // to prevent BaseFee manipulation we limit the gasWanted so that diff --git a/x/feemarket/keeper/keeper.go b/x/feemarket/keeper/keeper.go index b43b21c14..ee23bef54 100644 --- a/x/feemarket/keeper/keeper.go +++ b/x/feemarket/keeper/keeper.go @@ -1,6 +1,8 @@ package keeper import ( + gomath "math" + "github.com/cosmos/evm/x/feemarket/types" "cosmossdk.io/log" @@ -75,9 +77,15 @@ func (k Keeper) SetTransientBlockGasWanted(ctx sdk.Context, gasWanted uint64) { store.Set(types.KeyPrefixTransientBlockGasWanted, gasBz) } -// AddTransientGasWanted adds the cumulative gas wanted in the transient store +// AddTransientGasWanted adds the cumulative gas wanted in the transient store. +// The sum saturates instead of wrapping: a wrapped total would silently +// under-report the block's gas wanted to the base fee calculation. func (k Keeper) AddTransientGasWanted(ctx sdk.Context, gasWanted uint64) (uint64, error) { - result := k.GetTransientGasWanted(ctx) + gasWanted + current := k.GetTransientGasWanted(ctx) + result := current + gasWanted + if result < current { + result = gomath.MaxUint64 + } k.SetTransientBlockGasWanted(ctx, result) return result, nil } From 89c7e52be5414db7c7ea55189d8b3682c4f49e09 Mon Sep 17 00:00:00 2001 From: Nilesh Gupta Date: Wed, 26 Aug 2026 21:27:00 +0530 Subject: [PATCH 12/15] fix(statedb): restore the in-place event-override shim for cosmos-sdk v0.50.x v0.6.0 calls EventManager.OverrideEvents directly, which exists only on cosmos-sdk v0.53. push-chain-node pins cosmos-sdk to v0.50.10 via replace, so the direct call compiles in this repo but breaks the node build with 'OverrideEvents undefined'. Restores the reflect-based in-place setter and documents why it must stay until the node moves off v0.50.x. --- x/vm/statedb/journal.go | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/x/vm/statedb/journal.go b/x/vm/statedb/journal.go index 35bce1d90..6323f8cd5 100644 --- a/x/vm/statedb/journal.go +++ b/x/vm/statedb/journal.go @@ -18,7 +18,9 @@ package statedb import ( "bytes" + "reflect" "sort" + "unsafe" "github.com/ethereum/go-ethereum/common" "github.com/holiman/uint256" @@ -26,6 +28,26 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) +// overrideEventManagerEvents replaces, in place, the events held by em with the +// provided events. It mirrors cosmos-sdk v0.53's EventManager.OverrideEvents. +// +// NOTE (push-chain): upstream cosmos/evm v0.6.0 calls em.OverrideEvents(prevEvents) +// directly, but that method only exists on the cosmos-sdk v0.53 EventManager. +// push-chain-node pins cosmos-sdk to the v0.50.x line (the rest of v0.6.0 is +// compatible with it), which has no in-place events setter. The precompile-call +// revert path REQUIRES an in-place mutation — precompiles obtain the cacheCtx via +// GetCacheContext(), which shares the same EventManager pointer, so swapping in a +// fresh EventManager would diverge from those outstanding copies. We therefore set +// the unexported `events` field directly, which is exactly what OverrideEvents +// does on v0.53. Do NOT replace this with a direct OverrideEvents call until the +// node's cosmos-sdk replace directive moves off v0.50.x — it compiles here but +// breaks the node build. +func overrideEventManagerEvents(em sdk.EventManagerI, events sdk.Events) { + // The concrete type behind EventManagerI is *sdk.EventManager. + f := reflect.ValueOf(em).Elem().FieldByName("events") + reflect.NewAt(f.Type(), unsafe.Pointer(f.UnsafeAddr())).Elem().Set(reflect.ValueOf(events)) +} + // JournalEntry is a modification entry in the state change journal that can be // Reverted on demand. type JournalEntry interface { @@ -188,7 +210,7 @@ func (pc precompileCallChange) Revert(s *StateDB) { // here: precompiles hold the cacheCtx obtained from GetCacheContext() and // share this EventManager pointer, so replacing the manager would diverge // from those outstanding copies. - s.cacheCtx.EventManager().OverrideEvents(pc.prevEvents) + overrideEventManagerEvents(s.cacheCtx.EventManager(), pc.prevEvents) // Restore processed events counter s.processedEventsCount = pc.prevProcessedEventCount From 7ac130d5db3e23146e318985e4b75ace87192e3e Mon Sep 17 00:00:00 2001 From: Nilesh Gupta Date: Wed, 2 Sep 2026 11:50:48 +0530 Subject: [PATCH 13/15] fix(vm): propagate response on DerivedEVMCall VM failure (#52) Callers bill GasUsed from the receipt; returning nil made that a no-op. --- x/vm/keeper/call_evm.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/x/vm/keeper/call_evm.go b/x/vm/keeper/call_evm.go index 49eac591d..6c22f12aa 100644 --- a/x/vm/keeper/call_evm.go +++ b/x/vm/keeper/call_evm.go @@ -152,7 +152,11 @@ func (k Keeper) DerivedEVMCall( resp, err := k.DerivedEVMCallWithData(ctx, from, &contract, data, commit, gasless, isModuleSender, value, gasLimit, manualNonce) if err != nil { - return nil, errorsmod.Wrapf(err, "contract call failed: method '%s', contract '%s'", method, contract) + // Propagate resp alongside the error. On a VM failure (revert / out of gas) + // DerivedEVMCallWithData still returns a response carrying GasUsed; dropping + // it here leaves callers that charge for the gas a call actually burned with + // nothing to charge against. CallEVM above propagates for the same reason. + return resp, errorsmod.Wrapf(err, "contract call failed: method '%s', contract '%s'", method, contract) } return resp, nil } From de01393647d10b788e9a2bdd1ae9a0d3dc785762 Mon Sep 17 00:00:00 2001 From: Nilesh Gupta Date: Fri, 11 Sep 2026 10:15:45 +0530 Subject: [PATCH 14/15] fix: drop duplicate TestSubBalanceUnderflowPanics from the merge --- x/vm/statedb/statedb_test.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/x/vm/statedb/statedb_test.go b/x/vm/statedb/statedb_test.go index 7d102ff13..6f5dde745 100644 --- a/x/vm/statedb/statedb_test.go +++ b/x/vm/statedb/statedb_test.go @@ -298,16 +298,6 @@ func (suite *StateDBTestSuite) TestBalance() { } } -func (suite *StateDBTestSuite) TestSubBalanceUnderflowPanics() { - db := statedb.New(sdk.Context{}.WithEventManager(sdk.NewEventManager()), mocks.NewEVMKeeper(), emptyTxConfig) - db.AddBalance(address, uint256.NewInt(1), tracing.BalanceChangeUnspecified) - - expectedPanic := fmt.Sprintf("state balance underflow for %s: have=%s sub=%s", address.Hex(), "1", "2") - suite.Require().PanicsWithValue(expectedPanic, func() { - db.SubBalance(address, uint256.NewInt(2), tracing.BalanceChangeUnspecified) - }) -} - func (suite *StateDBTestSuite) TestState() { key1 := common.BigToHash(big.NewInt(1)) value1 := common.BigToHash(big.NewInt(1)) From e09e3cd0f74e6b517a5029501aa35f456fd4925d Mon Sep 17 00:00:00 2001 From: Nilesh Gupta Date: Fri, 11 Sep 2026 10:27:33 +0530 Subject: [PATCH 15/15] fix: import grouping in test_call_evm --- tests/integration/x/vm/test_call_evm.go | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/x/vm/test_call_evm.go b/tests/integration/x/vm/test_call_evm.go index 991c7f8cc..d4859a099 100644 --- a/tests/integration/x/vm/test_call_evm.go +++ b/tests/integration/x/vm/test_call_evm.go @@ -13,6 +13,7 @@ import ( evmtypes "github.com/cosmos/evm/x/vm/types" storetypes "cosmossdk.io/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" )