diff --git a/CHANGELOG.md b/CHANGELOG.md index 90165d117..2c5713f16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,8 +39,10 @@ ### FEATURES - [\#346](https://github.com/cosmos/evm/pull/346) Add eth_createAccessList method and implementation +- [\#405](https://github.com/cosmos/evm/pull/405) Add erc20 factory precompile. - [\#502](https://github.com/cosmos/evm/pull/502) Add block time in derived logs. -- [\#633](https://github.com/cosmos/evm/pull/633) go-ethereum metrics are now emitted on a separate server. default address: 127.0.0.1:8100. +- [\#588](https://github.com/cosmos/evm/pull/588) go-ethereum metrics are now available in Cosmos SDK's telemetry server at host:port/geth/metrics (default localhost:1317/geth/metrics). + ### STATE BREAKING diff --git a/evmd/tests/integration/precompiles/erc20factory/precompile_erc20_factory_test.go b/evmd/tests/integration/precompiles/erc20factory/precompile_erc20_factory_test.go new file mode 100644 index 000000000..26eb5d1c2 --- /dev/null +++ b/evmd/tests/integration/precompiles/erc20factory/precompile_erc20_factory_test.go @@ -0,0 +1,14 @@ +package erc20factory + +import ( + "testing" + + "github.com/cosmos/evm/evmd/tests/integration" + factory "github.com/cosmos/evm/tests/integration/precompiles/erc20factory" + "github.com/stretchr/testify/suite" +) + +func TestErc20FactoryPrecompileTestSuite(t *testing.T) { + s := factory.NewPrecompileTestSuite(integration.CreateEvmd) + suite.Run(t, s) +} diff --git a/go.mod b/go.mod index 2e66b7626..e40893caf 100644 --- a/go.mod +++ b/go.mod @@ -92,6 +92,7 @@ require ( github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/chigopher/pathlib v0.19.1 // indirect github.com/chzyer/readline v1.5.1 // indirect github.com/cloudwego/base64x v0.1.5 // indirect github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect @@ -170,10 +171,12 @@ require ( github.com/hdevalence/ed25519consensus v0.2.0 // indirect github.com/holiman/bloomfilter/v2 v2.0.3 // indirect github.com/huandu/skiplist v1.2.1 // indirect + github.com/huandu/xstrings v1.4.0 // indirect github.com/huin/goupnp v1.3.0 // indirect github.com/iancoleman/strcase v0.3.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackpal/go-nat-pmp v1.0.2 // indirect + github.com/jinzhu/copier v0.4.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jmhodges/levigo v1.0.0 // indirect github.com/klauspost/compress v1.18.0 // indirect @@ -232,6 +235,7 @@ require ( github.com/tklauser/numcpus v0.6.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ulikunitz/xz v0.5.11 // indirect + github.com/vektra/mockery/v2 v2.53.4 // indirect github.com/zeebo/errs v1.4.0 // indirect github.com/zondax/ledger-go v0.14.3 // indirect go.etcd.io/bbolt v1.4.0-alpha.1 // indirect @@ -250,6 +254,7 @@ require ( go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/arch v0.17.0 // indirect golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect + golang.org/x/mod v0.26.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sys v0.35.0 // indirect golang.org/x/term v0.34.0 // indirect diff --git a/go.sum b/go.sum index b3f7089a2..3d8373130 100644 --- a/go.sum +++ b/go.sum @@ -783,6 +783,8 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/chigopher/pathlib v0.19.1 h1:RoLlUJc0CqBGwq239cilyhxPNLXTK+HXoASGyGznx5A= +github.com/chigopher/pathlib v0.19.1/go.mod h1:tzC1dZLW8o33UQpWkNkhvPwL5n4yyFRFm/jL1YGWFvY= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= @@ -1279,6 +1281,8 @@ github.com/huandu/go-assert v1.1.5 h1:fjemmA7sSfYHJD7CUqs9qTwwfdNAx7/j2/ZlHXzNB3 github.com/huandu/go-assert v1.1.5/go.mod h1:yOLvuqZwmcHIC5rIzrBhT7D3Q9c3GFnd0JrPVhn/06U= github.com/huandu/skiplist v1.2.1 h1:dTi93MgjwErA/8idWTzIw4Y1kZsMWx35fmI2c8Rij7w= github.com/huandu/skiplist v1.2.1/go.mod h1:7v3iFjLcSAzO4fN5B8dvebvo/qsfumiLiDXMrPiHF9w= +github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= +github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= @@ -1305,6 +1309,8 @@ github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= +github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= +github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= @@ -1704,6 +1710,8 @@ github.com/urfave/cli v1.22.1 h1:+mkCCcOFKPnCmVYVcURKps1Xe+3zP90gSYGNfRkjoIY= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/vektra/mockery/v2 v2.53.4 h1:abBWJLUQppM7T/VsLasBwgl7XXQRWH6lC3bnbJpOCLk= +github.com/vektra/mockery/v2 v2.53.4/go.mod h1:hIFFb3CvzPdDJJiU7J4zLRblUMv7OuezWsHPmswriwo= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= @@ -1875,6 +1883,8 @@ golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= +golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= diff --git a/local_node.sh b/local_node.sh index c04c16382..fa0fff988 100755 --- a/local_node.sh +++ b/local_node.sh @@ -240,7 +240,7 @@ if [[ $overwrite == "y" || $overwrite == "Y" ]]; then jq '.app_state["bank"]["denom_metadata"]=[{"description":"The native staking token for evmd.","denom_units":[{"denom":"atest","exponent":0,"aliases":["attotest"]},{"denom":"test","exponent":18,"aliases":[]}],"base":"atest","display":"test","name":"Test Token","symbol":"TEST","uri":"","uri_hash":""}]' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" - jq '.app_state["evm"]["params"]["active_static_precompiles"]=["0x0000000000000000000000000000000000000100","0x0000000000000000000000000000000000000400","0x0000000000000000000000000000000000000800","0x0000000000000000000000000000000000000801","0x0000000000000000000000000000000000000802","0x0000000000000000000000000000000000000803","0x0000000000000000000000000000000000000804","0x0000000000000000000000000000000000000805", "0x0000000000000000000000000000000000000806", "0x0000000000000000000000000000000000000807"]' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" + jq '.app_state["evm"]["params"]["active_static_precompiles"]=["0x0000000000000000000000000000000000000100","0x0000000000000000000000000000000000000400","0x0000000000000000000000000000000000000800","0x0000000000000000000000000000000000000801","0x0000000000000000000000000000000000000802","0x0000000000000000000000000000000000000803","0x0000000000000000000000000000000000000804","0x0000000000000000000000000000000000000805", "0x0000000000000000000000000000000000000806", "0x0000000000000000000000000000000000000807", "0x0000000000000000000000000000000000000900"]' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" jq '.app_state["evm"]["params"]["evm_denom"]="atest"' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" diff --git a/mempool/txpool/legacypool/reset_testing.go b/mempool/txpool/legacypool/reset_testing.go index 16a11a2a5..b24088c3d 100644 --- a/mempool/txpool/legacypool/reset_testing.go +++ b/mempool/txpool/legacypool/reset_testing.go @@ -22,17 +22,17 @@ func (pool *LegacyPool) reset(oldHead, newHead *types.Header) { log.Debug("Skipping reorg on Cosmos chain (testing mode)", "oldHead", oldHead.Hash(), "newHead", newHead.Hash(), "newParent", newHead.ParentHash) reinject = nil // No transactions to reinject } - + // Initialize the internal state to the current head if newHead == nil { newHead = pool.chain.CurrentBlock() // Special case during testing } - + // Ensure BaseFee is set for EIP-1559 compatibility in tests if newHead.BaseFee == nil && pool.chainconfig.IsLondon(newHead.Number) { // Set a default base fee for testing newHead.BaseFee = big.NewInt(1000000000) // 1 gwei default } - + pool.resetInternalState(newHead, reinject) -} \ No newline at end of file +} diff --git a/precompiles/common/interfaces.go b/precompiles/common/interfaces.go index e69e11d39..56ef41eab 100644 --- a/precompiles/common/interfaces.go +++ b/precompiles/common/interfaces.go @@ -26,6 +26,8 @@ type BankKeeper interface { SendCoins(ctx context.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) error SpendableCoin(ctx context.Context, addr sdk.AccAddress, denom string) sdk.Coin BlockedAddr(addr sdk.AccAddress) bool + MintCoins(ctx context.Context, moduleName string, amt sdk.Coins) error + SendCoinsFromModuleToAccount(ctx context.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error } type TransferKeeper interface { diff --git a/precompiles/common/mocks/BankKeeper.go b/precompiles/common/mocks/BankKeeper.go index a3c72593c..2ad57e099 100644 --- a/precompiles/common/mocks/BankKeeper.go +++ b/precompiles/common/mocks/BankKeeper.go @@ -150,12 +150,49 @@ func (_m *BankKeeper) SpendableCoin(ctx context.Context, addr types.AccAddress, return r0 } +// MintCoins provides a mock function with given fields: ctx, moduleName, amt +func (_m *BankKeeper) MintCoins(ctx context.Context, moduleName string, amt types.Coins) error { + ret := _m.Called(ctx, moduleName, amt) + + if len(ret) == 0 { + panic("no return value specified for MintCoins") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, types.Coins) error); ok { + r0 = rf(ctx, moduleName, amt) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SendCoinsFromModuleToAccount provides a mock function with given fields: ctx, senderModule, recipientAddr, amt +func (_m *BankKeeper) SendCoinsFromModuleToAccount(ctx context.Context, senderModule string, recipientAddr types.AccAddress, amt types.Coins) error { + ret := _m.Called(ctx, senderModule, recipientAddr, amt) + + if len(ret) == 0 { + panic("no return value specified for SendCoinsFromModuleToAccount") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, types.AccAddress, types.Coins) error); ok { + r0 = rf(ctx, senderModule, recipientAddr, amt) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // NewBankKeeper creates a new instance of BankKeeper. 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 NewBankKeeper(t interface { mock.TestingT Cleanup(func()) -}) *BankKeeper { +}, +) *BankKeeper { mock := &BankKeeper{} mock.Mock.Test(t) diff --git a/precompiles/erc20factory/IERC20Factory.sol b/precompiles/erc20factory/IERC20Factory.sol new file mode 100644 index 000000000..55e840968 --- /dev/null +++ b/precompiles/erc20factory/IERC20Factory.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.8.17; + +/** + * @dev The ERC20 Factory contract's address. + */ +address constant ERC20_FACTORY_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000900; + +/** + * @dev The ERC20 Factory contract's instance. + */ +IERC20Factory constant ERC20_FACTORY_CONTRACT = IERC20Factory(ERC20_FACTORY_PRECOMPILE_ADDRESS); + +interface IERC20Factory { + /** + * @dev Emitted when a new ERC20 token is created. + * @param tokenAddress The address of the ERC20 token. + * @param salt The salt used for deployment. + * @param name The name of the token. + * @param symbol The symbol of the token. + * @param decimals The decimals of the token. + */ + event Create( + address indexed tokenAddress, + bytes32 salt, + string name, + string symbol, + uint8 decimals, + address minter, + uint256 premintedSupply + ); + + /** + * @dev Defines a method for creating an ERC20 token. + * @param salt Salt used for deployment + * @param name The name of the token. + * @param symbol The symbol of the token. + * @param decimals the decimals of the token. + * @return tokenAddress The ERC20 token address. + */ + function create( + bytes32 salt, + string memory name, + string memory symbol, + uint8 decimals, + address minter, + uint256 premintedSupply + ) external returns (address tokenAddress); + + /** + * @dev Calculates the deterministic address for a new token. + * @param salt Salt used for deployment + * @return tokenAddress The calculated ERC20 token address. + */ + function calculateAddress(bytes32 salt) external view returns (address tokenAddress); +} \ No newline at end of file diff --git a/precompiles/erc20factory/abi.json b/precompiles/erc20factory/abi.json new file mode 100644 index 000000000..4ff56cdd6 --- /dev/null +++ b/precompiles/erc20factory/abi.json @@ -0,0 +1,123 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "IERC20Factory", + "sourceName": "solidity/precompiles/erc20factory/IERC20Factory.sol", + "abi": [ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "salt", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "indexed": false, + "internalType": "string", + "name": "symbol", + "type": "string" + }, + { + "indexed": false, + "internalType": "uint8", + "name": "decimals", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "address", + "name": "minter", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "premintedSupply", + "type": "uint256" + } + ], + "name": "Create", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "salt", + "type": "bytes32" + } + ], + "name": "calculateAddress", + "outputs": [ + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "salt", + "type": "bytes32" + }, + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "symbol", + "type": "string" + }, + { + "internalType": "uint8", + "name": "decimals", + "type": "uint8" + }, + { + "internalType": "address", + "name": "minter", + "type": "address" + }, + { + "internalType": "uint256", + "name": "premintedSupply", + "type": "uint256" + } + ], + "name": "create", + "outputs": [ + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "bytecode": "0x", + "deployedBytecode": "0x", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/precompiles/erc20factory/erc20factory.go b/precompiles/erc20factory/erc20factory.go new file mode 100644 index 000000000..fb93dd4d3 --- /dev/null +++ b/precompiles/erc20factory/erc20factory.go @@ -0,0 +1,163 @@ +package erc20factory + +import ( + "embed" + "fmt" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/core/vm" + + cmn "github.com/cosmos/evm/precompiles/common" + + storetypes "cosmossdk.io/store/types" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +const ( + Erc20FactoryAddress = "0x0000000000000000000000000000000000000900" + // GasCreate defines the gas required to create a new ERC20 Token Pair calculated from a ERC20 deploy transaction + GasCreate = 3_000_000 + // GasCalculateAddress defines the gas required to calculate the address of a new ERC20 Token Pair + GasCalculateAddress = 3_000 +) + +var _ vm.PrecompiledContract = &Precompile{} + +// Embed abi json file to the executable binary. Needed when importing as dependency. +// +//go:embed abi.json +var f embed.FS + +// Precompile defines the precompiled contract for Bech32 encoding. +type Precompile struct { + cmn.Precompile + erc20Keeper ERC20Keeper + bankKeeper BankKeeper + evmKeeper EvmKeeper +} + +// NewPrecompile creates a new bech32 Precompile instance as a +// PrecompiledContract interface. +func NewPrecompile(erc20Keeper ERC20Keeper, bankKeeper cmn.BankKeeper, keeper EvmKeeper) (*Precompile, error) { + newABI, err := cmn.LoadABI(f, "abi.json") + if err != nil { + return nil, err + } + + p := &Precompile{ + Precompile: cmn.Precompile{ + ABI: newABI, + KvGasConfig: storetypes.KVGasConfig(), + TransientKVGasConfig: storetypes.TransientGasConfig(), + }, + erc20Keeper: erc20Keeper, + bankKeeper: bankKeeper, + evmKeeper: keeper, + } + + // SetAddress defines the address of the distribution compile contract. + p.SetAddress(common.HexToAddress(Erc20FactoryAddress)) + + p.SetBalanceHandler(bankKeeper) + return p, nil +} + +// Address defines the address of the bech32 precompiled contract. +func (Precompile) Address() common.Address { + return common.HexToAddress(Erc20FactoryAddress) +} + +// RequiredGas calculates the contract gas use. +func (p Precompile) RequiredGas(input []byte) uint64 { + // NOTE: This check avoid panicking when trying to decode the method ID + if len(input) < 4 { + return 0 + } + + methodID := input[:4] + method, err := p.MethodById(methodID) + if err != nil { + return 0 + } + + switch method.Name { + // ERC-20 transactions + case CreateMethod: + return GasCreate + case CalculateAddressMethod: + return GasCalculateAddress + default: + return 0 + } +} + +// Run executes the precompiled contract bech32 methods defined in the ABI. +func (p Precompile) Run(evm *vm.EVM, contract *vm.Contract, readOnly bool) (bz []byte, err error) { + ctx, stateDB, method, initialGas, args, err := p.RunSetup(evm, contract, readOnly, p.IsTransaction) + if err != nil { + return nil, err + } + + // Start the balance change handler before executing the precompile + p.GetBalanceHandler().BeforeBalanceChange(ctx) + + // This handles any out of gas errors that may occur during the execution of a precompile query. + // It avoids panics and returns the out of gas error so the EVM can continue gracefully. + defer cmn.HandleGasError(ctx, contract, initialGas, &err)() + + bz, err = p.HandleMethod(ctx, contract, stateDB, method, args) + if err != nil { + return nil, err + } + + cost := ctx.GasMeter().GasConsumed() - initialGas + + if !contract.UseGas(cost, nil, tracing.GasChangeCallPrecompiledContract) { + return nil, vm.ErrOutOfGas + } + + // Process the native balance changes after the method execution. + err = p.GetBalanceHandler().AfterBalanceChange(ctx, stateDB) + if err != nil { + return nil, err + } + + return bz, nil +} + +// IsTransaction checks if the given method name corresponds to a transaction or query. +// +// Available ERC20 Factory transactions are: +// - Create +func (Precompile) IsTransaction(method *abi.Method) bool { + switch method.Name { + case CreateMethod: + return true + default: + return false + } +} + +// HandleMethod handles the execution of each of the ERC-20 Factory methods. +func (p *Precompile) HandleMethod( + ctx sdk.Context, + contract *vm.Contract, + stateDB vm.StateDB, + method *abi.Method, + args []interface{}, +) (bz []byte, err error) { + switch method.Name { + // ERC-20 Factory transactions + case CreateMethod: + bz, err = p.Create(ctx, stateDB, method, contract.Caller(), args) + // ERC-20 Factory queries + case CalculateAddressMethod: + bz, err = p.CalculateAddress(method, contract.Caller(), args) + default: + return nil, fmt.Errorf(cmn.ErrUnknownMethod, method.Name) + } + return bz, err +} diff --git a/precompiles/erc20factory/events.go b/precompiles/erc20factory/events.go new file mode 100644 index 000000000..2f778b772 --- /dev/null +++ b/precompiles/erc20factory/events.go @@ -0,0 +1,56 @@ +package erc20factory + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + + cmn "github.com/cosmos/evm/precompiles/common" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +const ( + // EventTypeCreate is the event type for the Create event. + EventTypeCreate = "Create" +) + +// EmitCreateEvent emits the Create event. +func (p Precompile) EmitCreateEvent(ctx sdk.Context, stateDB vm.StateDB, tokenAddress common.Address, salt [32]uint8, name string, symbol string, decimals uint8, minter common.Address, premintedSupply *big.Int) error { + event := p.Events[EventTypeCreate] + topics := make([]common.Hash, 2) // Only 2 topics: event ID + tokenAddress + + topics[0] = event.ID + + var err error + topics[1], err = cmn.MakeTopic(tokenAddress) + if err != nil { + return err + } + + // Pack the non-indexed event parameters into the data field + arguments := abi.Arguments{ + event.Inputs[1], // salt + event.Inputs[2], // name + event.Inputs[3], // symbol + event.Inputs[4], // decimals + event.Inputs[5], // minter + event.Inputs[6], // premintedSupply + } + packed, err := arguments.Pack(salt, name, symbol, decimals, minter, premintedSupply) + if err != nil { + return err + } + + stateDB.AddLog(ðtypes.Log{ + Address: p.Address(), + Topics: topics, + Data: packed, + BlockNumber: uint64(ctx.BlockHeight()), //nolint:gosec // G115 // block height won't exceed uint64 + }) + + return nil +} diff --git a/precompiles/erc20factory/interfaces.go b/precompiles/erc20factory/interfaces.go new file mode 100644 index 000000000..0e80185f3 --- /dev/null +++ b/precompiles/erc20factory/interfaces.go @@ -0,0 +1,29 @@ +package erc20factory + +import ( + "context" + + "github.com/ethereum/go-ethereum/common" + + erc20types "github.com/cosmos/evm/x/erc20/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" +) + +type ERC20Keeper interface { + SetToken(ctx sdk.Context, token erc20types.TokenPair) error + EnableDynamicPrecompile(ctx sdk.Context, address common.Address) error + IsDenomRegistered(ctx sdk.Context, denom string) bool +} + +type BankKeeper interface { + GetDenomMetaData(ctx context.Context, denom string) (banktypes.Metadata, bool) + SetDenomMetaData(ctx context.Context, denomMetaData banktypes.Metadata) + MintCoins(ctx context.Context, moduleName string, amt sdk.Coins) error + SendCoinsFromModuleToAccount(ctx context.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error +} + +type EvmKeeper interface { + GetCodeHash(ctx sdk.Context, addr common.Address) common.Hash +} diff --git a/precompiles/erc20factory/query.go b/precompiles/erc20factory/query.go new file mode 100644 index 000000000..221841236 --- /dev/null +++ b/precompiles/erc20factory/query.go @@ -0,0 +1,29 @@ +package erc20factory + +import ( + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" +) + +const ( + // CalculateAddressMethod defines the ABI method name for the CalculateAddress + // query. + CalculateAddressMethod = "calculateAddress" +) + +// CalculateAddress calculates the address of a new ERC20 Token Pair +func (p Precompile) CalculateAddress( + method *abi.Method, + caller common.Address, + args []interface{}, +) ([]byte, error) { + salt, err := ParseCalculateAddressArgs(args) + if err != nil { + return nil, err + } + + address := crypto.CreateAddress2(caller, salt, []byte{}) + + return method.Outputs.Pack(address) +} diff --git a/precompiles/erc20factory/tx.go b/precompiles/erc20factory/tx.go new file mode 100644 index 000000000..d42386362 --- /dev/null +++ b/precompiles/erc20factory/tx.go @@ -0,0 +1,144 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package erc20factory + +import ( + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + + erc20types "github.com/cosmos/evm/x/erc20/types" + evmtypes "github.com/cosmos/evm/x/vm/types" + + "cosmossdk.io/errors" + "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" +) + +const ( + // CreateMethod defines the ABI method name to create a new ERC20 Token Pair + CreateMethod = "create" +) + +// Create CreateERC20Precompile creates a new ERC20 TokenPair +func (p Precompile) Create( + ctx sdk.Context, + stateDB vm.StateDB, + method *abi.Method, + caller common.Address, + args []interface{}, +) ([]byte, error) { + salt, name, symbol, decimals, minter, premintedSupply, err := ParseCreateArgs(args) + if err != nil { + return nil, err + } + + address := crypto.CreateAddress2(caller, salt, []byte{}) + + hash := p.evmKeeper.GetCodeHash(ctx, address) + if hash.Cmp(common.BytesToHash(evmtypes.EmptyCodeHash)) != 0 { + return nil, errors.Wrapf( + erc20types.ErrContractAlreadyExists, + "contract already exists at address %s", address.String(), + ) + } + + metadata, err := p.createCoinMetadata(ctx, address, name, symbol, decimals) + if err != nil { + return nil, errors.Wrap( + err, "failed to create wrapped coin denom metadata for ERC20", + ) + } + + if err := metadata.Validate(); err != nil { + return nil, errors.Wrapf( + err, "ERC20 token data is invalid for contract %s", address.String(), + ) + } + + p.bankKeeper.SetDenomMetaData(ctx, *metadata) + + pair := erc20types.NewTokenPair(address, metadata.Name, erc20types.OWNER_EXTERNAL) + + err = p.erc20Keeper.SetToken(ctx, pair) + if err != nil { + return nil, err + } + + err = p.erc20Keeper.EnableDynamicPrecompile(ctx, pair.GetERC20Contract()) + if err != nil { + return nil, err + } + + coins := sdk.NewCoins(sdk.NewCoin(metadata.Base, math.NewIntFromBigInt(premintedSupply))) + if err := p.bankKeeper.MintCoins(ctx, erc20types.ModuleName, coins); err != nil { + return nil, err + } + if err := p.bankKeeper.SendCoinsFromModuleToAccount(ctx, erc20types.ModuleName, sdk.AccAddress(minter.Bytes()), coins); err != nil { + return nil, err + } + + if err = p.EmitCreateEvent(ctx, stateDB, address, salt, name, symbol, decimals, minter, premintedSupply); err != nil { + return nil, err + } + + return method.Outputs.Pack(address) +} + +func (p Precompile) createCoinMetadata(ctx sdk.Context, address common.Address, name string, symbol string, decimals uint8) (*banktypes.Metadata, error) { + addressString := address.String() + denom := erc20types.CreateDenom(addressString) + + _, found := p.bankKeeper.GetDenomMetaData(ctx, denom) + if found { + return nil, errors.Wrap( + erc20types.ErrInternalTokenPair, "denom metadata already registered", + ) + } + + if p.erc20Keeper.IsDenomRegistered(ctx, denom) { + return nil, errors.Wrapf( + erc20types.ErrInternalTokenPair, "coin denomination already registered: %s", name, + ) + } + + // base denomination + base := erc20types.CreateDenom(addressString) + + // create a bank denom metadata based on the ERC20 token ABI details + // metadata name is should always be the contract since it's the key + // to the bank store + metadata := banktypes.Metadata{ + Description: erc20types.CreateDenomDescription(addressString), + Base: base, + // NOTE: Denom units MUST be increasing + DenomUnits: []*banktypes.DenomUnit{ + { + Denom: base, + Exponent: 0, + }, + }, + Name: base, + Symbol: symbol, + Display: base, + } + + // only append metadata if decimals > 0, otherwise validation fails + if decimals > 0 { + nameSanitized := erc20types.SanitizeERC20Name(name) + metadata.DenomUnits = append( + metadata.DenomUnits, + &banktypes.DenomUnit{ + Denom: nameSanitized, + Exponent: uint32(decimals), //#nosec G115 + }, + ) + metadata.Display = nameSanitized + } + + return &metadata, nil +} diff --git a/precompiles/erc20factory/types.go b/precompiles/erc20factory/types.go new file mode 100644 index 000000000..0feaba006 --- /dev/null +++ b/precompiles/erc20factory/types.go @@ -0,0 +1,91 @@ +package erc20factory + +import ( + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + + cmn "github.com/cosmos/evm/precompiles/common" +) + +// EventCreate defines the event data for the ERC20 Factory Create event. +type EventCreate struct { + TokenAddress common.Address + TokenPairType uint8 + Salt [32]uint8 + Name string + Symbol string + Decimals uint8 + Minter common.Address + PremintedSupply *big.Int +} + +// ParseCreateArgs parses the arguments from the create method and returns +// the token type, salt, name, symbol, decimals, minter, and preminted supply. +func ParseCreateArgs(args []interface{}) (salt [32]uint8, name string, symbol string, decimals uint8, minter common.Address, premintedSupply *big.Int, err error) { + if len(args) != 6 { + return [32]uint8{}, "", "", uint8(0), common.Address{}, nil, fmt.Errorf(cmn.ErrInvalidNumberOfArgs, 7, len(args)) + } + + salt, ok := args[0].([32]uint8) + if !ok { + return [32]uint8{}, "", "", uint8(0), common.Address{}, nil, fmt.Errorf("invalid salt") + } + + name, ok = args[1].(string) + if !ok || len(name) < 3 || len(name) > 128 { + return [32]uint8{}, "", "", uint8(0), common.Address{}, nil, fmt.Errorf("invalid name") + } + + symbol, ok = args[2].(string) + if !ok || len(symbol) < 1 || len(symbol) > 16 { + return [32]uint8{}, "", "", uint8(0), common.Address{}, nil, fmt.Errorf("invalid symbol") + } + + decimals, ok = args[3].(uint8) + if !ok { + return [32]uint8{}, "", "", uint8(0), common.Address{}, nil, fmt.Errorf("invalid decimals") + } + + minter, ok = args[4].(common.Address) + if !ok { + return [32]uint8{}, "", "", uint8(0), common.Address{}, nil, fmt.Errorf("invalid minter") + } + + // Validate that minter is not the zero address + if minter == (common.Address{}) { + return [32]uint8{}, "", "", uint8(0), common.Address{}, nil, fmt.Errorf("invalid minter: cannot be zero address") + } + + premintedSupply, ok = args[5].(*big.Int) + if !ok { + return [32]uint8{}, "", "", uint8(0), common.Address{}, nil, fmt.Errorf("invalid premintedSupply: expected *big.Int") + } + + if premintedSupply.Sign() < 0 { + return [32]uint8{}, "", "", uint8(0), common.Address{}, nil, fmt.Errorf("invalid premintedSupply: cannot be negative") + } + + maxUint256 := new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 256), big.NewInt(1)) + if premintedSupply.Cmp(maxUint256) > 0 { + return [32]uint8{}, "", "", uint8(0), common.Address{}, nil, fmt.Errorf("premintedSupply exceeds uint256 maximum") + } + + return salt, name, symbol, decimals, minter, premintedSupply, nil +} + +// ParseCalculateAddressArgs parses the arguments from the calculateAddress method and returns +// the token type and salt. +func ParseCalculateAddressArgs(args []interface{}) (salt [32]uint8, err error) { + if len(args) != 1 { + return [32]uint8{}, fmt.Errorf(cmn.ErrInvalidNumberOfArgs, 2, len(args)) + } + + salt, ok := args[0].([32]uint8) + if !ok { + return [32]uint8{}, fmt.Errorf("invalid salt") + } + + return salt, nil +} diff --git a/precompiles/types/defaults.go b/precompiles/types/defaults.go index b64570365..c154f3a97 100644 --- a/precompiles/types/defaults.go +++ b/precompiles/types/defaults.go @@ -11,6 +11,7 @@ import ( "github.com/cosmos/evm/precompiles/bech32" cmn "github.com/cosmos/evm/precompiles/common" distprecompile "github.com/cosmos/evm/precompiles/distribution" + "github.com/cosmos/evm/precompiles/erc20factory" govprecompile "github.com/cosmos/evm/precompiles/gov" ics20precompile "github.com/cosmos/evm/precompiles/ics20" "github.com/cosmos/evm/precompiles/p256" @@ -160,6 +161,11 @@ func DefaultStaticPrecompiles( panic(fmt.Errorf("failed to instantiate slashing precompile: %w", err)) } + erc20FactoryPrecompile, err := erc20factory.NewPrecompile(&erc20Keeper, bankKeeper, evmKeeper) + if err != nil { + panic(fmt.Errorf("failed to instantiate erc20 factory precompile: %w", err)) + } + // Stateless precompiles precompiles[bech32Precompile.Address()] = bech32Precompile precompiles[p256Precompile.Address()] = p256Precompile @@ -171,6 +177,7 @@ func DefaultStaticPrecompiles( precompiles[bankPrecompile.Address()] = bankPrecompile precompiles[govPrecompile.Address()] = govPrecompile precompiles[slashingPrecompile.Address()] = slashingPrecompile + precompiles[erc20FactoryPrecompile.Address()] = erc20FactoryPrecompile return precompiles } diff --git a/tests/integration/precompiles/erc20factory/test_erc20factory.go b/tests/integration/precompiles/erc20factory/test_erc20factory.go new file mode 100644 index 000000000..41ca6ce21 --- /dev/null +++ b/tests/integration/precompiles/erc20factory/test_erc20factory.go @@ -0,0 +1,67 @@ +package erc20factory + +import ( + "math/big" + + "github.com/cosmos/evm/precompiles/erc20factory" + utiltx "github.com/cosmos/evm/testutil/tx" +) + +const ( + tokenName = "Test" + tokenSymbol = "TEST" +) + +func (s *PrecompileTestSuite) TestIsTransaction() { + s.SetupTest() + + // Queries + method := s.precompile.Methods[erc20factory.CalculateAddressMethod] + s.Require().False(s.precompile.IsTransaction(&method)) + + // Transactions + method = s.precompile.Methods[erc20factory.CreateMethod] + s.Require().True(s.precompile.IsTransaction(&method)) +} + +func (s *PrecompileTestSuite) TestRequiredGas() { + s.SetupTest() + + mintAddr := utiltx.GenerateAddress() + decimals := uint8(18) + amount := big.NewInt(1000000) + name := tokenName + symbol := tokenSymbol + + testcases := []struct { + name string + malleate func() []byte + expGas uint64 + }{ + { + name: erc20factory.CalculateAddressMethod, + malleate: func() []byte { + bz, err := s.precompile.Pack(erc20factory.CalculateAddressMethod, [32]uint8{}) + s.Require().NoError(err, "expected no error packing ABI") + return bz + }, + expGas: erc20factory.GasCalculateAddress, + }, + { + name: erc20factory.CreateMethod, + malleate: func() []byte { + bz, err := s.precompile.Pack(erc20factory.CreateMethod, [32]uint8{}, name, symbol, decimals, mintAddr, amount) + s.Require().NoError(err, "expected no error packing ABI") + return bz + }, + expGas: erc20factory.GasCreate, + }, + } + + for _, tc := range testcases { + s.Run(tc.name, func() { + gas := s.precompile.RequiredGas(tc.malleate()) + s.Require().Equal(tc.expGas, gas) + }) + } +} diff --git a/tests/integration/precompiles/erc20factory/test_events.go b/tests/integration/precompiles/erc20factory/test_events.go new file mode 100644 index 000000000..1fd8cd857 --- /dev/null +++ b/tests/integration/precompiles/erc20factory/test_events.go @@ -0,0 +1,70 @@ +package erc20factory + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + + cmn "github.com/cosmos/evm/precompiles/common" + "github.com/cosmos/evm/precompiles/erc20factory" + utiltx "github.com/cosmos/evm/testutil/tx" +) + +func (s *PrecompileTestSuite) TestEmitCreateEvent() { + testcases := []struct { + testName string + tokenAddress common.Address + tokenType uint8 + salt [32]uint8 + name string + symbol string + decimals uint8 + minter common.Address + premintedSupply *big.Int + }{ + { + testName: "pass", + tokenAddress: utiltx.GenerateAddress(), + tokenType: 0, + salt: [32]uint8{0}, + name: "Test", + symbol: "TEST", + decimals: 18, + minter: utiltx.GenerateAddress(), + premintedSupply: big.NewInt(1000000), + }, + } + + for _, tc := range testcases { + s.Run(tc.testName, func() { + s.SetupTest() + stateDB := s.network.GetStateDB() + + err := s.precompile.EmitCreateEvent(s.network.GetContext(), stateDB, tc.tokenAddress, tc.salt, tc.name, tc.symbol, tc.decimals, tc.minter, tc.premintedSupply) + s.Require().NoError(err, "expected create event to be emitted successfully") + + log := stateDB.Logs()[0] + s.Require().Equal(log.Address, s.precompile.Address()) + + // Check event signature matches the one emitted + event := s.precompile.ABI.Events[erc20factory.EventTypeCreate] + s.Require().Equal(crypto.Keccak256Hash([]byte(event.Sig)), common.HexToHash(log.Topics[0].Hex())) + s.Require().Equal(log.BlockNumber, uint64(s.network.GetContext().BlockHeight())) //nolint:gosec // G115 + + // Check event parameters + var createEvent erc20factory.EventCreate + err = cmn.UnpackLog(s.precompile.ABI, &createEvent, erc20factory.EventTypeCreate, *log) + s.Require().NoError(err, "unable to unpack log into create event") + + s.Require().Equal(tc.tokenAddress, createEvent.TokenAddress, "expected different token address") + s.Require().Equal(tc.tokenType, createEvent.TokenPairType, "expected different token type") + s.Require().Equal(tc.salt, createEvent.Salt, "expected different salt") + s.Require().Equal(tc.name, createEvent.Name, "expected different name") + s.Require().Equal(tc.symbol, createEvent.Symbol, "expected different symbol") + s.Require().Equal(tc.decimals, createEvent.Decimals, "expected different decimals") + s.Require().Equal(tc.minter, createEvent.Minter, "expected different minter") + s.Require().Equal(tc.premintedSupply, createEvent.PremintedSupply, "expected different preminted supply") + }) + } +} diff --git a/tests/integration/precompiles/erc20factory/test_query.go b/tests/integration/precompiles/erc20factory/test_query.go new file mode 100644 index 000000000..fd17eb274 --- /dev/null +++ b/tests/integration/precompiles/erc20factory/test_query.go @@ -0,0 +1,65 @@ +package erc20factory + +import ( + "github.com/ethereum/go-ethereum/common" + + "github.com/cosmos/evm/precompiles/erc20factory" +) + +func (s *PrecompileTestSuite) TestCalculateAddress() { + defaultCaller := common.HexToAddress("0xDc411BaFB148ebDA2B63EBD5f3D8669DD4383Af5") + + testcases := []struct { + name string + caller common.Address + args []interface{} + expPass bool + errContains string + expAddress common.Address + }{ + { + name: "pass - correct arguments", + caller: defaultCaller, + args: []interface{}{ + [32]uint8(common.HexToHash("0x4f5b6f778b28c4d67a9c12345678901234567890123456789012345678901234").Bytes()), + }, + expPass: true, + expAddress: common.HexToAddress("0xc047E2F9302F4dE42115E40CEdb3FA0F1CfbD6b7"), + }, + { + name: "fail - invalid salt", + caller: defaultCaller, + args: []interface{}{ + "invalid salt", + }, + errContains: "invalid salt", + }, + { + name: "fail - invalid number of arguments", + caller: defaultCaller, + args: []interface{}{ + 1, 2, 3, + }, + errContains: "invalid number of arguments", + }, + } + + for _, tc := range testcases { + s.Run(tc.name, func() { + s.SetupTest() + + precompile := s.setupERC20FactoryPrecompile() + + method := precompile.Methods[erc20factory.CalculateAddressMethod] + + bz, err := precompile.CalculateAddress( + &method, + tc.caller, + tc.args, + ) + + // NOTE: all output and error checking happens in here + s.requireOut(bz, err, method, tc.expPass, tc.errContains, tc.expAddress) + }) + } +} diff --git a/tests/integration/precompiles/erc20factory/test_setup.go b/tests/integration/precompiles/erc20factory/test_setup.go new file mode 100644 index 000000000..e8f07c504 --- /dev/null +++ b/tests/integration/precompiles/erc20factory/test_setup.go @@ -0,0 +1,62 @@ +package erc20factory + +import ( + "github.com/stretchr/testify/suite" + + "github.com/cosmos/evm/precompiles/erc20factory" + "github.com/cosmos/evm/testutil/integration/evm/factory" + "github.com/cosmos/evm/testutil/integration/evm/grpc" + "github.com/cosmos/evm/testutil/integration/evm/network" + testkeyring "github.com/cosmos/evm/testutil/keyring" +) + +// PrecompileTestSuite is the implementation of the TestSuite interface for ERC20 Factory precompile +// unit tests. +type PrecompileTestSuite struct { + suite.Suite + + create network.CreateEvmApp + options []network.ConfigOption + bondDenom string + network *network.UnitTestNetwork + factory factory.TxFactory + grpcHandler grpc.Handler + keyring testkeyring.Keyring + + precompile *erc20factory.Precompile +} + +func NewPrecompileTestSuite(create network.CreateEvmApp, options ...network.ConfigOption) *PrecompileTestSuite { + return &PrecompileTestSuite{ + create: create, + options: options, + } +} + +func (s *PrecompileTestSuite) SetupTest() { + keyring := testkeyring.New(2) + options := []network.ConfigOption{ + network.WithPreFundedAccounts(keyring.GetAllAccAddrs()...), + } + options = append(options, s.options...) + integrationNetwork := network.NewUnitTestNetwork(s.create, options...) + grpcHandler := grpc.NewIntegrationHandler(integrationNetwork) + txFactory := factory.New(integrationNetwork, grpcHandler) + + ctx := integrationNetwork.GetContext() + sk := integrationNetwork.App.GetStakingKeeper() + bondDenom, err := sk.BondDenom(ctx) + s.Require().NoError(err) + s.Require().NotEmpty(bondDenom, "bond denom cannot be empty") + + s.bondDenom = bondDenom + s.factory = txFactory + s.grpcHandler = grpcHandler + s.keyring = keyring + s.network = integrationNetwork + + // Instantiate the precompile with an exemplary token denomination. + // + // NOTE: This has to be done AFTER assigning the suite fields. + s.precompile = s.setupERC20FactoryPrecompile() +} diff --git a/tests/integration/precompiles/erc20factory/test_tx.go b/tests/integration/precompiles/erc20factory/test_tx.go new file mode 100644 index 000000000..e83b83982 --- /dev/null +++ b/tests/integration/precompiles/erc20factory/test_tx.go @@ -0,0 +1,164 @@ +package erc20factory + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + + "github.com/cosmos/evm/precompiles/erc20factory" + erc20types "github.com/cosmos/evm/x/erc20/types" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func (s *PrecompileTestSuite) TestCreate() { + caller := common.HexToAddress("0x2c7882f69Cd115F470aAEde121f57F932936a56f") + mintAddr := common.HexToAddress("0x73657398D483143AF7db7899757e5E7037fB713d") + expectedAddress := common.HexToAddress("0xc5ecc46b3cf020351c2186afCD5C734EE15E4da2") + amount := big.NewInt(1000000) + decimals := uint8(18) + name := "Test" + symbol := "TEST" + + method := s.precompile.Methods[erc20factory.CreateMethod] + + testcases := []struct { + name string + args []interface{} + expPass bool + postExpPass func(output []byte) + errContains string + expAddress common.Address + }{ + { + name: "pass - correct arguments", + args: []interface{}{[32]uint8(common.HexToHash("0x4f5b6f778b28c4d67a9c12345678901234567890123456789012345678901234").Bytes()), name, symbol, decimals, mintAddr, amount}, + expPass: true, + postExpPass: func(output []byte) { + res, err := method.Outputs.Unpack(output) + s.Require().NoError(err, "expected no error unpacking output") + s.Require().Len(res, 1, "expected one output") + address, ok := res[0].(common.Address) + s.Require().True(ok, "expected address type") + + // Check the balance of the token for the mintAddr + balance := s.network.App.GetBankKeeper().GetBalance(s.network.GetContext(), sdk.AccAddress(mintAddr.Bytes()), erc20types.CreateDenom(address.String())) + s.Require().Equal(amount, balance.Amount.BigInt(), "expected balance to match preminted amount") + + s.Require().Equal(address.String(), expectedAddress, "expected address to match") + }, + expAddress: expectedAddress, + }, + { + name: "fail - blocked addresses cannot receive tokens", + args: []interface{}{ + [32]uint8(common.HexToHash("0x4f5b6f778b28c4d67a9c12345678901234567890123456789012345678901234").Bytes()), + name, + symbol, + decimals, + erc20types.ModuleAddress, + amount, + }, + }, + { + name: "fail - invalid salt", + args: []interface{}{ + "invalid salt", + name, + symbol, + decimals, + mintAddr, + amount, + }, + errContains: "invalid salt", + }, + { + name: "fail - invalid name", + args: []interface{}{ + [32]uint8{}, + "", + symbol, + decimals, + mintAddr, + amount, + }, + errContains: "invalid name", + }, + { + name: "fail - invalid symbol", + args: []interface{}{ + [32]uint8{}, + name, + "", + decimals, + mintAddr, + amount, + }, + errContains: "invalid symbol", + }, + { + name: "fail - invalid decimals", + args: []interface{}{ + [32]uint8{}, + name, + symbol, + "invalid decimals", + mintAddr, + amount, + }, + errContains: "invalid decimals", + }, + { + name: "fail - invalid minter", + args: []interface{}{ + [32]uint8{}, + name, + symbol, + decimals, + "invalid address", + amount, + }, + errContains: "invalid minter", + }, + { + name: "fail - invalid preminted supply", + args: []interface{}{ + [32]uint8{}, + name, + symbol, + decimals, + mintAddr, + "invalid amount", + }, + errContains: "invalid premintedSupply", + }, + { + name: "fail - invalid number of arguments", + args: []interface{}{ + 1, 2, 3, + }, + errContains: "invalid number of arguments", + }, + } + + for _, tc := range testcases { + s.Run(tc.name, func() { + s.SetupTest() + + precompile := s.setupERC20FactoryPrecompile() + + method := precompile.Methods[erc20factory.CreateMethod] + + bz, err := precompile.Create( + s.network.GetContext(), + s.network.GetStateDB(), + &method, + caller, + tc.args, + ) + + // NOTE: all output and error checking happens in here + s.requireOut(bz, err, method, tc.expPass, tc.errContains, tc.expAddress) + }) + } +} diff --git a/tests/integration/precompiles/erc20factory/test_types.go b/tests/integration/precompiles/erc20factory/test_types.go new file mode 100644 index 000000000..bc665c95f --- /dev/null +++ b/tests/integration/precompiles/erc20factory/test_types.go @@ -0,0 +1,193 @@ +package erc20factory + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + + "github.com/cosmos/evm/precompiles/erc20factory" + utiltx "github.com/cosmos/evm/testutil/tx" +) + +func (s *PrecompileTestSuite) TestParseCalculateAddressArgs() { + s.SetupTest() + + testcases := []struct { + name string + args []interface{} + expPass bool + errContains string + }{ + { + name: "pass - correct arguments", + args: []interface{}{ + [32]uint8{}, + }, + expPass: true, + }, + { + name: "fail - invalid salt", + args: []interface{}{ + uint8(0), + "invalid salt", + }, + }, + { + name: "fail - invalid number of arguments", + args: []interface{}{ + 1, 2, 3, + }, + errContains: "invalid number of arguments", + }, + } + + for _, tc := range testcases { + s.Run(tc.name, func() { + salt, err := erc20factory.ParseCalculateAddressArgs(tc.args) + if tc.expPass { + s.Require().NoError(err, "unexpected error parsing the calculate address arguments") + s.Require().Equal(salt, tc.args[0], "expected different salt") + } else { + s.Require().Error(err, "expected an error parsing the calculate address arguments") + s.Require().ErrorContains(err, tc.errContains, "expected different error message") + } + }) + } +} + +func (s *PrecompileTestSuite) TestParseCreateArgs() { + addr := utiltx.GenerateAddress() + decimals := uint8(18) + amount := big.NewInt(1000000) + name := "Test" + symbol := "TEST" + + s.SetupTest() + + testcases := []struct { + name string + args []interface{} + expPass bool + errContains string + }{ + { + name: "pass - correct arguments", + args: []interface{}{ + [32]uint8{}, + name, + symbol, + decimals, + addr, + amount, + }, + expPass: true, + }, + { + name: "fail - invalid salt", + args: []interface{}{ + "invalid salt", + name, + symbol, + decimals, + addr, + big.NewInt(1000000), + }, + }, + { + name: "fail - invalid name", + args: []interface{}{ + [32]uint8{}, + uint8(0), + symbol, + decimals, + addr, + big.NewInt(1000000), + }, + errContains: "invalid name", + }, + { + name: "fail - invalid symbol", + args: []interface{}{ + [32]uint8{}, + name, + "", + decimals, + addr, + big.NewInt(1000000), + }, + errContains: "invalid symbol", + }, + { + name: "fail - invalid decimals", + args: []interface{}{ + [32]uint8{}, + name, + symbol, + "invalid decimals", + addr, + big.NewInt(1000000), + }, + errContains: "invalid decimals", + }, + { + name: "fail - invalid minter", + args: []interface{}{ + [32]uint8{}, + name, + symbol, + decimals, + "invalid address", + big.NewInt(1000000), + }, + errContains: "invalid minter", + }, + { + name: "fail - zero address minter", + args: []interface{}{ + [32]uint8{}, + name, + symbol, + decimals, + common.Address{}, // Zero address + big.NewInt(1000000), + }, + errContains: "invalid minter: cannot be zero address", + }, + { + name: "fail - invalid preminted supply", + args: []interface{}{ + [32]uint8{}, + name, + symbol, + decimals, + addr, + big.NewInt(-1), + }, + errContains: "invalid premintedSupply: cannot be negative", + }, + { + name: "fail - invalid number of arguments", + args: []interface{}{ + 1, 2, 3, 4, 5, + }, + }, + } + + for _, tc := range testcases { + s.Run(tc.name, func() { + salt, name, symbol, decimals, minter, premintedSupply, err := erc20factory.ParseCreateArgs(tc.args) + if tc.expPass { + s.Require().NoError(err, "unexpected error parsing the create arguments") + s.Require().Equal(salt, tc.args[0], "expected different salt") + s.Require().Equal(name, tc.args[1], "expected different name") + s.Require().Equal(symbol, tc.args[2], "expected different symbol") + s.Require().Equal(decimals, tc.args[3], "expected different decimals") + s.Require().Equal(minter, tc.args[4], "expected different minter") + s.Require().Equal(premintedSupply, tc.args[5], "expected different preminted supply") + } else { + s.Require().Error(err, "expected an error parsing the create arguments") + s.Require().ErrorContains(err, tc.errContains, "expected different error message") + } + }) + } +} diff --git a/tests/integration/precompiles/erc20factory/test_utils.go b/tests/integration/precompiles/erc20factory/test_utils.go new file mode 100644 index 000000000..c85d95aa1 --- /dev/null +++ b/tests/integration/precompiles/erc20factory/test_utils.go @@ -0,0 +1,59 @@ +package erc20factory + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi" + + "github.com/cosmos/evm/precompiles/erc20factory" +) + +func (s *PrecompileTestSuite) setupERC20FactoryPrecompile() *erc20factory.Precompile { + precompile, err := erc20factory.NewPrecompile( + s.network.App.GetErc20Keeper(), + s.network.App.GetBankKeeper(), + s.network.App.GetEVMKeeper(), + ) + s.Require().NoError(err, "failed to create erc20factory precompile") + + return precompile +} + +// requireOut is a helper utility to reduce the amount of boilerplate code in the query tests. +// +// It requires the output bytes and error to match the expected values. Additionally, the method outputs +// are unpacked and the first value is compared to the expected value. +// +// NOTE: It's sufficient to only check the first value because all methods in the ERC20 precompile only +// return a single value. +func (s *PrecompileTestSuite) requireOut( + bz []byte, + err error, + method abi.Method, + expPass bool, + errContains string, + expValue interface{}, +) { + if expPass { + s.Require().NoError(err, "expected no error") + s.Require().NotEmpty(bz, "expected bytes not to be empty") + + // Unpack the name into a string + out, err := method.Outputs.Unpack(bz) + s.Require().NoError(err, "expected no error unpacking") + + // Check if expValue is a big.Int. Because of a difference in uninitialized/empty values for big.Ints, + // this comparison is often not working as expected, so we convert to Int64 here and compare those values. + bigExp, ok := expValue.(*big.Int) + if ok { + bigOut, ok := out[0].(*big.Int) + s.Require().True(ok, "expected output to be a big.Int") + s.Require().Equal(bigExp.Int64(), bigOut.Int64(), "expected different value") + } else { + s.Require().Equal(expValue, out[0], "expected different value") + } + } else { + s.Require().Error(err, "expected error") + s.Require().Contains(err.Error(), errContains, "expected different error") + } +} diff --git a/tests/jsonrpc/simulator/namespaces/debug.go b/tests/jsonrpc/simulator/namespaces/debug.go index a02cc0a85..88c9ac02e 100644 --- a/tests/jsonrpc/simulator/namespaces/debug.go +++ b/tests/jsonrpc/simulator/namespaces/debug.go @@ -999,7 +999,7 @@ func DebugStartGoTrace(rCtx *types.RPCContext) (*types.RpcResult, error) { // Call debug_startGoTrace with test parameters filename := "/tmp/go_trace_start.out" - + var result any err := rCtx.Evmd.RPCClient().Call(&result, string(MethodNameDebugStartGoTrace), filename) if err != nil { @@ -1024,7 +1024,7 @@ func DebugStartGoTrace(rCtx *types.RPCContext) (*types.RpcResult, error) { rCtx.AlreadyTestedRPCs = append(rCtx.AlreadyTestedRPCs, rpcResult) return rpcResult, nil } - + rpcResult := &types.RpcResult{ Method: MethodNameDebugStartGoTrace, Status: types.Ok, @@ -1065,7 +1065,7 @@ func DebugStopGoTrace(rCtx *types.RPCContext) (*types.RpcResult, error) { rCtx.AlreadyTestedRPCs = append(rCtx.AlreadyTestedRPCs, rpcResult) return rpcResult, nil } - + rpcResult := &types.RpcResult{ Method: MethodNameDebugStopGoTrace, Status: types.Ok, diff --git a/tests/jsonrpc/simulator/namespaces/eth.go b/tests/jsonrpc/simulator/namespaces/eth.go index 2d711eaf1..803b00510 100644 --- a/tests/jsonrpc/simulator/namespaces/eth.go +++ b/tests/jsonrpc/simulator/namespaces/eth.go @@ -1671,7 +1671,6 @@ func EthEstimateGas(rCtx *types.RPCContext) (*types.RpcResult, error) { func EthFeeHistory(rCtx *types.RPCContext) (*types.RpcResult, error) { var result interface{} err := rCtx.Evmd.RPCClient().Call(&result, string(MethodNameEthFeeHistory), "0x2", "latest", []float64{25.0, 50.0, 75.0}) - if err != nil { if err.Error() == "the method "+string(MethodNameEthFeeHistory)+" does not exist/is not available" || err.Error() == types.ErrorMethodNotFound { @@ -1927,7 +1926,6 @@ func EthGetHeaderByHash(rCtx *types.RPCContext) (*types.RpcResult, error) { var header any err = rCtx.Evmd.RPCClient().Call(&header, string(MethodNameEthGetHeaderByHash), receipt.BlockHash.Hex()) - if err != nil { if strings.Contains(err.Error(), "does not exist/is not available") || strings.Contains(err.Error(), "Method not found") { @@ -2005,7 +2003,6 @@ func EthGetHeaderByNumber(rCtx *types.RPCContext) (*types.RpcResult, error) { var header any err = rCtx.Evmd.RPCClient().Call(&header, string(MethodNameEthGetHeaderByNumber), blockNumberHex) - if err != nil { if strings.Contains(err.Error(), "does not exist/is not available") || strings.Contains(err.Error(), "Method not found") { @@ -2092,7 +2089,6 @@ func EthSimulateV1(rCtx *types.RPCContext) (*types.RpcResult, error) { var result any err := rCtx.Evmd.RPCClient().Call(&result, string(MethodNameEthSimulateV1), simulationReq) - if err != nil { if strings.Contains(err.Error(), "does not exist/is not available") || strings.Contains(err.Error(), "Method not found") || @@ -2159,7 +2155,6 @@ func EthPendingTransactions(rCtx *types.RPCContext) (*types.RpcResult, error) { var pendingTxs any err := rCtx.Evmd.RPCClient().Call(&pendingTxs, string(MethodNameEthPendingTransactions)) - if err != nil { if strings.Contains(err.Error(), "does not exist/is not available") || strings.Contains(err.Error(), "Method not found") || diff --git a/tests/jsonrpc/simulator/report/report.go b/tests/jsonrpc/simulator/report/report.go index d1f668bc7..20b664b2e 100644 --- a/tests/jsonrpc/simulator/report/report.go +++ b/tests/jsonrpc/simulator/report/report.go @@ -359,7 +359,6 @@ func PrintCategoryMatrix(summary *types.TestSummary) { catSummary.Total) } } - } func PrintSummary(summary *types.TestSummary) { diff --git a/tests/jsonrpc/simulator/types/context.go b/tests/jsonrpc/simulator/types/context.go index 79d0c2203..3a65245a2 100644 --- a/tests/jsonrpc/simulator/types/context.go +++ b/tests/jsonrpc/simulator/types/context.go @@ -77,7 +77,6 @@ type RPCContext struct { // Dual API testing fields EnableComparison bool // Enable dual API comparison ComparisonResults []*ComparisonResult // Store comparison results - } func NewRPCContext(conf *config.Config) (*RPCContext, error) { @@ -134,7 +133,6 @@ func (rCtx *RPCContext) AlreadyTested(rpc RpcName) *RpcResult { } } return nil - } // CompareRPCCall performs a dual API call and compares response structures diff --git a/tests/jsonrpc/simulator/utils/test_helpers.go b/tests/jsonrpc/simulator/utils/test_helpers.go index d584b116d..ea0bda5de 100644 --- a/tests/jsonrpc/simulator/utils/test_helpers.go +++ b/tests/jsonrpc/simulator/utils/test_helpers.go @@ -390,7 +390,6 @@ func Legacy(rCtx *types.RPCContext, methodName types.RpcName, category string, r // First test if the API is actually implemented var result interface{} err := rCtx.Evmd.RPCClient().Call(&result, string(methodName)) - if err != nil { // Check if it's a "method not found" error (API not implemented) if err.Error() == "the method "+string(methodName)+" does not exist/is not available" || diff --git a/tests/solidity/suites/precompiles/test/erc20factory.js b/tests/solidity/suites/precompiles/test/erc20factory.js new file mode 100644 index 000000000..eac7d3bb1 --- /dev/null +++ b/tests/solidity/suites/precompiles/test/erc20factory.js @@ -0,0 +1,60 @@ +const { expect } = require('chai') +const hre = require('hardhat') + +const abi = [ + "function create(bytes32 salt, string memory name, string memory symbol, uint8 decimals, address minter, uint256 premintedSupply) external returns (address)", + "function calculateAddress(bytes32 salt) external view returns (address)", + "event Create(address indexed tokenAddress, bytes32 salt, string name, string symbol, uint8 decimals, address minter, uint256 premintedSupply)" +] + +describe('ERC20Factory', function () { + + it('should calculate the correct address', async function () { + const salt = '0x4f5b6f778b28c4d67a9c12345678901234567890123456789012345678901234' + const erc20Factory = await hre.ethers.getContractAt('IERC20Factory', '0x0000000000000000000000000000000000000900') + console.log("erc20Factory contract loaded") + const expectedAddress = await erc20Factory.calculateAddress(salt) + console.log("erc20factory calculateAddress") + expect(expectedAddress).to.equal('0x8C9521848ee0d03BF47390F98c6dc968DA0b2915') + }) + + it('should create a new ERC20 token', async function () { + const salt = '0x4f5b6f778b28c4d67a9c12345678901234567890123456789012345678901234' + const name = 'Test' + const symbol = 'TEST' + const decimals = 18 + const premintedSupply = hre.ethers.parseEther("1000000") // 1M tokens + const [signer] = await hre.ethers.getSigners() + const minter = signer.address + + // Calculate the expected token address before deployment + const erc20Factory = await hre.ethers.getContractAt('IERC20Factory', '0x0000000000000000000000000000000000000900') + + const tokenAddress = await erc20Factory.calculateAddress(salt) + const tx = await erc20Factory.connect(signer).create(salt, name, symbol, decimals, minter, premintedSupply) + + // Get the token address from the transaction receipt + const receipt = await tx.wait() + expect(receipt.status).to.equal(1) // Check transaction was successful + + // Create a contract instance with the full ABI including events for event filtering + const erc20FactoryWithEvents = new hre.ethers.Contract('0x0000000000000000000000000000000000000900', abi, signer) + + // Get the Create event from the transaction receipt + const createEvents = await erc20FactoryWithEvents.queryFilter(erc20FactoryWithEvents.filters.Create(), receipt.blockNumber, receipt.blockNumber) + expect(createEvents.length).to.equal(1) + expect(createEvents[0].args.tokenAddress).to.equal(tokenAddress) + expect(createEvents[0].args.salt).to.equal(salt) + expect(createEvents[0].args.name).to.equal(name) + expect(createEvents[0].args.symbol).to.equal(symbol) + expect(createEvents[0].args.decimals).to.equal(decimals) + expect(createEvents[0].args.minter).to.equal(minter) + expect(createEvents[0].args.premintedSupply).to.equal(premintedSupply) + + // Get the token contract instance + const erc20Token = await hre.ethers.getContractAt('contracts/cosmos/erc20/IERC20.sol:IERC20', tokenAddress) + + // Verify token details through IERC20 queries + expect(await erc20Token.totalSupply()).to.equal(premintedSupply) + }) +}) \ No newline at end of file diff --git a/x/erc20/types/errors.go b/x/erc20/types/errors.go index 99ad293f2..7f50187f9 100644 --- a/x/erc20/types/errors.go +++ b/x/erc20/types/errors.go @@ -25,4 +25,5 @@ var ( ErrInvalidAllowance = errorsmod.Register(ModuleName, 18, "invalid allowance") ErrNegativeToken = errorsmod.Register(ModuleName, 19, "token amount is negative") ErrExpectedEvent = errorsmod.Register(ModuleName, 20, "expected event") + ErrContractAlreadyExists = errorsmod.Register(ModuleName, 21, "contract already exists") )