From f961d0f73d85b0dcf4c50356506e4e3557fd5242 Mon Sep 17 00:00:00 2001 From: Vlad Date: Mon, 2 Mar 2026 09:49:46 -0500 Subject: [PATCH 1/8] v0.6.0 --- CHANGELOG.md | 2 + Makefile | 10 +- 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 | 235 +++++ .../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 + ....5.0_UNRELEASED.md => v0.4.0_to_v0.5.0.md} | 0 docs/migrations/v0.5.x_to_v0.6.0.md | 485 +++++++++ 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/tx.go | 97 +- precompiles/types/defaults.go | 4 +- precompiles/types/static_precompiles.go | 4 +- rpc/types/types_test.go | 2 +- .../precompiles/erc20/test_query.go | 2 +- .../precompiles/ics20/test_setup.go | 2 + tests/integration/x/erc20/test_convert.go | 383 +++++++ tests/integration/x/erc20/test_evm.go | 82 +- .../integration/x/erc20/test_ibc_callback.go | 10 +- tests/integration/x/erc20/test_msg_server.go | 423 ++------ 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 | 146 ++- .../x/vm/test_commit_idempotency.go | 235 +++++ .../integration/x/vm/test_state_transition.go | 132 ++- tests/systemtests/Makefile | 2 +- tests/systemtests/go.mod | 6 +- tests/systemtests/go.sum | 12 +- tests/systemtests/upgrade_test.go | 4 +- testutil/tx/eip712.go | 5 +- x/erc20/keeper/convert.go | 167 +++ 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 | 174 +--- 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 | 12 +- 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 | 62 -- 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 | 28 +- x/vm/keeper/grpc_query.go | 12 +- x/vm/keeper/state_transition.go | 45 +- x/vm/statedb/balance_events_test.go | 113 ++ x/vm/statedb/journal.go | 13 +- x/vm/statedb/statedb.go | 80 +- x/vm/statedb/statedb_test.go | 40 +- x/vm/types/errors.go | 4 + x/vm/types/msg.go | 5 - 101 files changed, 7265 insertions(+), 1850 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 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 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/CHANGELOG.md b/CHANGELOG.md index fc3ecb5fc..cfc616c3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,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 cda314b6e..0f85d784d 100644 --- a/Makefile +++ b/Makefile @@ -378,17 +378,17 @@ 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 -build-v04: - mkdir -p ./tests/systemtests/binaries/v0.4 - git checkout v0.4.1 +build-v05: + mkdir -p ./tests/systemtests/binaries/v0.5 + git checkout v0.5.1 make build - cp $(BUILDDIR)/evmd ./tests/systemtests/binaries/v0.4 + cp $(BUILDDIR)/evmd ./tests/systemtests/binaries/v0.5 git checkout - 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..e59900625 --- /dev/null +++ b/contracts/solidity/ContractCreationTester.sol @@ -0,0 +1,235 @@ +// 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/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..90dd21c20 --- /dev/null +++ b/docs/migrations/v0.5.x_to_v0.6.0.md @@ -0,0 +1,485 @@ +# 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 bcd83b1be..f12d11cd7 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 13d5f5d2d..d9a64a027 100644 --- a/evmd/go.sum +++ b/evmd/go.sum @@ -835,8 +835,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= @@ -853,8 +853,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= @@ -872,8 +872,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= @@ -1614,8 +1614,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= @@ -1633,14 +1633,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= @@ -1651,8 +1651,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= @@ -1802,8 +1802,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 ff50080b1..d453e0766 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 @@ -213,11 +213,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 @@ -251,6 +251,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 @@ -281,4 +282,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 35d99b611..5d5959beb 100644 --- a/go.sum +++ b/go.sum @@ -832,8 +832,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= @@ -850,8 +850,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= @@ -869,8 +869,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= @@ -1599,8 +1599,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= @@ -1618,14 +1618,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= @@ -1636,8 +1636,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= @@ -1787,8 +1787,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 1554fe1b3..242d525b3 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 b184fd674..df90904c4 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/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/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/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..a0d1599b6 --- /dev/null +++ b/tests/integration/x/erc20/test_convert.go @@ -0,0 +1,383 @@ +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, + }, + { + "pass - delayed malicious contract", + 10, + 10, + func(common.Address) {}, + func() {}, + contractMaliciousDelayed, + true, + 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 0086d4b78..957a7e629 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 diff --git a/tests/integration/x/erc20/test_msg_server.go b/tests/integration/x/erc20/test_msg_server.go index 06ed180d8..18f5d010c 100644 --- a/tests/integration/x/erc20/test_msg_server.go +++ b/tests/integration/x/erc20/test_msg_server.go @@ -10,7 +10,6 @@ import ( "go.uber.org/mock/gomock" "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" @@ -24,366 +23,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, - }, - { - "pass - delayed malicious contract", - 10, - 10, - func(common.Address) {}, - func() {}, - contractMaliciousDelayed, - true, - 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 } func (s *KeeperTestSuite) TestConvertNativeERC20ToEVMERC20() { @@ -450,10 +204,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) }, @@ -481,8 +235,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) }, @@ -509,9 +266,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) }, @@ -539,9 +296,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 013b200f2..ba24039a5 100644 --- a/tests/integration/x/vm/test_call_evm.go +++ b/tests/integration/x/vm/test_call_evm.go @@ -9,25 +9,39 @@ 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" ) 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 { @@ -35,12 +49,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) + } } } } @@ -53,64 +78,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, @@ -120,8 +169,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", }, } @@ -133,10 +197,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 { @@ -144,6 +213,9 @@ func (s *KeeperTestSuite) TestCallEVMWithData() { s.Require().NoError(err) } else { s.Require().Error(err) + if tc.expError != "" { + s.Require().Contains(err.Error(), tc.expError) + } } }) } 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 f44f5d5e6..1847c9026 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" @@ -800,31 +801,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) + + 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) - // Compare gas to a transfer tx gas - expectedGasUsed := params.TxGas - s.Require().Equal(expectedGasUsed, res.GasUsed) + 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() { @@ -849,6 +887,7 @@ func (s *KeeperTestSuite) TestApplyMessageWithConfig() { getEVMParams func() types.Params getFeeMarketParams func() feemarkettypes.Params overrides *rpctypes.StateOverride + useNilDB bool expErr bool expVMErr bool expectedGasUsed uint64 @@ -869,6 +908,7 @@ func (s *KeeperTestSuite) TestApplyMessageWithConfig() { getEVMParams: types.DefaultParams, getFeeMarketParams: feemarkettypes.DefaultParams, overrides: nil, + useNilDB: false, expErr: false, expVMErr: false, expectedGasUsed: params.TxGas, @@ -1091,11 +1131,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 { @@ -1125,16 +1187,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) @@ -1222,13 +1280,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..2aebdbaca 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() 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..aa06cf9e8 --- /dev/null +++ b/x/erc20/keeper/convert.go @@ -0,0 +1,167 @@ +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 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 4848dc10b..3b30a557b 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) } @@ -234,7 +234,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 { // 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/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 51c8621e4..79e029260 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,162 +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 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), - }, - ) - } - }() - - 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), - ), - }, - ) + // Create stateDB for this transaction + stateDB := statedb.New(ctx, k.evmKeeper, statedb.NewEmptyTxConfig()) - 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 @@ -219,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 } @@ -234,13 +89,7 @@ func (k Keeper) ConvertCoin( // - burn escrowed Coins // - check if token balance increased by amount // - check for unexpected `Approval` event in logs -func (k Keeper) ConvertCoinNativeERC20( - ctx sdk.Context, - pair types.TokenPair, - amount math.Int, - receiver common.Address, - sender sdk.AccAddress, -) error { +func (k Keeper) ConvertCoinNativeERC20(ctx sdk.Context, pair types.TokenPair, 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") } @@ -260,7 +109,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 d12603e26..eee3e5d3c 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") @@ -421,7 +425,7 @@ func (k ContractKeeper) IBCOnTimeoutPacketCallback( return err } - res, err := k.evmKeeper.CallEVM(ctx, *abi, sender, contractAddr, true, math.NewIntFromUint64(cachedCtx.GasMeter().GasRemaining()).BigInt(), "onPacketTimeout", + res, err := k.evmKeeper.CallEVM(ctx, 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 bda640f07..000000000 --- a/x/ibc/transfer/module.go +++ /dev/null @@ -1,62 +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" -) - -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, - } -} - -// 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 3f5b36c7a..2abe5bb7d 100644 --- a/x/vm/keeper/call_evm.go +++ b/x/vm/keeper/call_evm.go @@ -9,6 +9,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" @@ -17,15 +18,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( @@ -34,7 +29,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) } @@ -42,14 +37,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 @@ -68,7 +58,7 @@ func (k Keeper) CallEVMWithData( AccessList: ethtypes.AccessList{}, } - res, err := k.ApplyMessage(ctx, msg, nil, commit, true) + res, err := k.ApplyMessage(ctx, stateDB, msg, nil, commit, callFromPrecompile, true) if err != nil { return nil, err } diff --git a/x/vm/keeper/grpc_query.go b/x/vm/keeper/grpc_query.go index 0e69e5ce0..0032b4b78 100644 --- a/x/vm/keeper/grpc_query.go +++ b/x/vm/keeper/grpc_query.go @@ -263,7 +263,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()) } @@ -403,7 +404,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 @@ -555,7 +557,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)) @@ -833,7 +836,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 eb8975cf2..c9e7df65e 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() @@ -334,14 +335,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. @@ -381,23 +384,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) @@ -455,7 +450,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 { @@ -522,11 +520,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/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..a008ffe6b 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,13 @@ 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 + 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 98185b60e..d10d1cd39 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) { @@ -147,13 +152,14 @@ func (s *StateDB) Finalise(deleteEmptyObjects bool) { // New creates a new state from a given trie. func New(ctx sdk.Context, keeper Keeper, txConfig TxConfig) *StateDB { 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()), } } @@ -184,12 +190,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 @@ -208,7 +210,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() } @@ -433,10 +436,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 + var prevEvents sdk.Events = 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 { @@ -445,6 +452,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) @@ -664,7 +688,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 } @@ -679,12 +703,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] } @@ -700,11 +720,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 69bbddc2c..2e380cadc 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()) } @@ -267,7 +267,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 @@ -322,7 +322,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()) @@ -332,7 +332,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) @@ -365,7 +365,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 @@ -376,7 +376,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)) @@ -486,7 +486,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) }) @@ -577,7 +577,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) } } @@ -589,7 +589,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, @@ -639,7 +639,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()) @@ -660,7 +660,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) @@ -716,7 +716,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/errors.go b/x/vm/types/errors.go index aba448436..245b2d20d 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 6876c46d17c76a8861a94a888dd904962012ecce Mon Sep 17 00:00:00 2001 From: Vlad J Date: Mon, 2 Mar 2026 10:15:41 -0500 Subject: [PATCH 2/8] chore: changelog (#1025) * Changelog * fix line length --- CHANGELOG.md | 16 ++++++++++++++++ docs/migrations/v0.5.x_to_v0.6.0.md | 10 ++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cfc616c3a..56954aef9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # 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 + ## v0.5.1 ### DEPENDENCIES diff --git a/docs/migrations/v0.5.x_to_v0.6.0.md b/docs/migrations/v0.5.x_to_v0.6.0.md index 90dd21c20..a82bc13ad 100644 --- a/docs/migrations/v0.5.x_to_v0.6.0.md +++ b/docs/migrations/v0.5.x_to_v0.6.0.md @@ -432,7 +432,10 @@ func (k Keeper) QueryBalance(ctx sdk.Context, addr common.Address) (*big.Int, er ```go // Before (v0.5.x) -func (ms msgServer) ConvertCoin(goCtx context.Context, msg *types.MsgConvertCoin) (*types.MsgConvertCoinResponse, error) { +func (ms msgServer) ConvertCoin( + goCtx context.Context, + msg *types.MsgConvertCoin + ) (*types.MsgConvertCoinResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) // ... res, err := ms.evmKeeper.CallEVMWithData( @@ -442,7 +445,10 @@ func (ms msgServer) ConvertCoin(goCtx context.Context, msg *types.MsgConvertCoin } // After (v0.6.0) -func (ms msgServer) ConvertCoin(goCtx context.Context, msg *types.MsgConvertCoin) (*types.MsgConvertCoinResponse, error) { +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()) From c6be492910fa191be6485683a453797e834a8864 Mon Sep 17 00:00:00 2001 From: Arya Lanjewar <102943033+AryaLanjewar3005@users.noreply.github.com> Date: Sat, 27 Jun 2026 15:14:51 +0530 Subject: [PATCH 3/8] fix: solhint line length and build-v05 legacy binary ref --- Makefile | 15 +++++++++------ contracts/solidity/ContractCreationTester.sol | 4 +++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index 23c3b0698..54e7bf9f4 100644 --- a/Makefile +++ b/Makefile @@ -384,16 +384,19 @@ test-system: build-v05 build cd tests/systemtests/Counter && forge build $(MAKE) -C tests/systemtests test -# V05_REF is the upstream v0.5.1 release tag — the "from" version for the -# v0.5.0-to-v0.6.0 upgrade system test. -V05_REF ?= v0.5.1 +# 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 v0.5.1` on the working - # tree, which breaks as soon as the build dirties go.mod). Mirrors the - # isolated v0.4 legacy build introduced in the audit/CI fixes. + # 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 \ diff --git a/contracts/solidity/ContractCreationTester.sol b/contracts/solidity/ContractCreationTester.sol index e59900625..9887bf69b 100644 --- a/contracts/solidity/ContractCreationTester.sol +++ b/contracts/solidity/ContractCreationTester.sol @@ -195,7 +195,9 @@ contract ContractCreationTester { uint256 successCreationValue ) external payable { // 1. Try to create contract (will revert after creation, catch it) - try this.createAndRevert{value: revertCreationValue}(revertCreationValue) returns (SimpleReceiver newContract1) { + 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); From 6c22ba0b1e9e7a380176885a321d1338cacb54c0 Mon Sep 17 00:00:00 2001 From: Arya Lanjewar <102943033+AryaLanjewar3005@users.noreply.github.com> Date: Sat, 27 Jun 2026 16:28:01 +0530 Subject: [PATCH 4/8] test: raise TestChainUpgrade block-height wait for 5s block time --- tests/systemtests/upgrade_test.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/systemtests/upgrade_test.go b/tests/systemtests/upgrade_test.go index 2aebdbaca..3cd6ac7c6 100644 --- a/tests/systemtests/upgrade_test.go +++ b/tests/systemtests/upgrade_test.go @@ -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() From 8b535eeeaa1bbcd278d57940d9e87df4b951419e Mon Sep 17 00:00:00 2001 From: Nilesh Gupta Date: Mon, 3 Aug 2026 12:38:47 +0530 Subject: [PATCH 5/8] ci: move jobs off dead Depot runners to GitHub-hosted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seven jobs never run — they queue until GitHub cancels them at 24h. Bumps test/lint timeouts 15m->30m for the smaller GitHub runners. --- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/jsonrpc-compatibility.yml | 2 +- .github/workflows/lint.yml | 4 ++-- .github/workflows/test.yml | 4 ++-- Makefile | 16 ++++++++-------- 5 files changed, 14 insertions(+), 14 deletions(-) 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/Makefile b/Makefile index 54e7bf9f4..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 From d4143b06d9beed885b53ad31e009d573a18658ff Mon Sep 17 00:00:00 2001 From: Arya Lanjewar <102943033+AryaLanjewar3005@users.noreply.github.com> Date: Wed, 29 Jul 2026 12:27:46 +0530 Subject: [PATCH 6/8] fix(rpc): report base fee as the gas price of derived txs --- CHANGELOG.md | 6 ++ rpc/backend/comet_to_eth.go | 11 ++- rpc/backend/derived_gas_price_test.go | 108 ++++++++++++++++++++++++++ rpc/types/utils.go | 61 +++++++++++---- rpc/types/utils_test.go | 71 +++++++++++++++++ 5 files changed, 242 insertions(+), 15 deletions(-) create mode 100644 rpc/backend/derived_gas_price_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 56954aef9..65519cf7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,12 @@ Follow the [migration document](docs/migrations/v0.5.x_to_v0.6.0.md) for upgrade ### BUG FIXES +- 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 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/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") + }) +} From bc77624a1c346bb966e747671cc16f364ac515a6 Mon Sep 17 00:00:00 2001 From: Nilesh Gupta Date: Wed, 26 Aug 2026 15:34:03 +0530 Subject: [PATCH 7/8] 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. --- x/vm/statedb/journal.go | 30 ++++++------------------------ 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/x/vm/statedb/journal.go b/x/vm/statedb/journal.go index 7ac0106bb..35bce1d90 100644 --- a/x/vm/statedb/journal.go +++ b/x/vm/statedb/journal.go @@ -18,9 +18,7 @@ package statedb import ( "bytes" - "reflect" "sort" - "unsafe" "github.com/ethereum/go-ethereum/common" "github.com/holiman/uint256" @@ -28,24 +26,6 @@ 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 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. -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 { @@ -203,10 +183,12 @@ func (pc precompileCallChange) Revert(s *StateDB) { // state stored in the snapshot s.RevertMultiStore(pc.snapshot) - // Restore events to the state before this precompile call. - // Equivalent to s.cacheCtx.EventManager().OverrideEvents(pc.prevEvents); see - // overrideEventManagerEvents for why the shim is used. - overrideEventManagerEvents(s.cacheCtx.EventManager(), pc.prevEvents) + // 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 From 570aff4dc6941459b145a99bc4ce614cc75d63b1 Mon Sep 17 00:00:00 2001 From: Nilesh Gupta Date: Wed, 26 Aug 2026 20:56:36 +0530 Subject: [PATCH 8/8] style(statedb): let prevEvents infer its type (staticcheck ST1023) --- x/vm/statedb/statedb.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/vm/statedb/statedb.go b/x/vm/statedb/statedb.go index 316c09bb1..51972629f 100644 --- a/x/vm/statedb/statedb.go +++ b/x/vm/statedb/statedb.go @@ -441,7 +441,7 @@ func (s *StateDB) setStateObject(object *stateObject) { // to the precompile call. func (s *StateDB) AddPrecompileFn(snapshot int) error { // Capture events before the precompile call - var prevEvents sdk.Events = s.cacheCtx.EventManager().Events() + prevEvents := s.cacheCtx.EventManager().Events() s.journal.append(precompileCallChange{ snapshot: snapshot,