From c3e24223758a108d20a5b3da698dfd91341d23a5 Mon Sep 17 00:00:00 2001 From: AdriaCarrera Date: Thu, 13 Mar 2025 13:50:49 +0100 Subject: [PATCH] feat: erc20factory --- app/precompiles.go | 7 + local-node.sh | 1 + precompiles/erc20factory/ERC20FactoryI.sol | 31 ++ precompiles/erc20factory/abi.json | 74 +++++ precompiles/erc20factory/erc20factory.go | 139 +++++++++ precompiles/erc20factory/erc20factory_test.go | 71 +++++ precompiles/erc20factory/methods.go | 174 ++++++++++++ precompiles/erc20factory/methods_test.go | 264 ++++++++++++++++++ precompiles/erc20factory/setup_test.go | 43 +++ 9 files changed, 804 insertions(+) create mode 100644 precompiles/erc20factory/ERC20FactoryI.sol create mode 100644 precompiles/erc20factory/abi.json create mode 100644 precompiles/erc20factory/erc20factory.go create mode 100644 precompiles/erc20factory/erc20factory_test.go create mode 100644 precompiles/erc20factory/methods.go create mode 100644 precompiles/erc20factory/methods_test.go create mode 100644 precompiles/erc20factory/setup_test.go diff --git a/app/precompiles.go b/app/precompiles.go index c30fc4d..f9afdcc 100644 --- a/app/precompiles.go +++ b/app/precompiles.go @@ -2,6 +2,7 @@ package app import ( "fmt" + "github.com/xrplevm/node/v6/precompiles/erc20factory" govkeeper "github.com/cosmos/cosmos-sdk/x/gov/keeper" @@ -83,6 +84,11 @@ func NewAvailableStaticPrecompiles( panic(fmt.Errorf("failed to instantiate gov precompile: %w", err)) } + erc20factoryPrecompile, err := erc20factory.NewPrecompile(authzKeeper, erc20Keeper, bankKeeper) + if err != nil { + panic(fmt.Errorf("failed to instantiate erc20factory precompile: %w", err)) + } + // Stateless precompiles precompiles[bech32Precompile.Address()] = bech32Precompile precompiles[p256Precompile.Address()] = p256Precompile @@ -93,5 +99,6 @@ func NewAvailableStaticPrecompiles( precompiles[ibcTransferPrecompile.Address()] = ibcTransferPrecompile precompiles[bankPrecompile.Address()] = bankPrecompile precompiles[govPrecompile.Address()] = govPrecompile + precompiles[erc20factoryPrecompile.Address()] = erc20factoryPrecompile return precompiles } diff --git a/local-node.sh b/local-node.sh index 3144a7b..31048a7 100755 --- a/local-node.sh +++ b/local-node.sh @@ -40,6 +40,7 @@ jq '.consensus.params["block"]["max_gas"]="10500000"' "$GENESIS" >"$TMP_GENESIS" jq '.app_state["crisis"]["constant_fee"]["denom"]="token"' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" jq '.app_state["evm"]["params"]["evm_denom"]="token"' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" jq '.app_state["evm"]["params"]["allow_unprotected_txs"]=true' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" +jq '.app_state["evm"]["params"]["active_static_precompiles"]+=["0x0000000000000000000000000000000000000900"]' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" jq '.app_state["gov"]["params"]["min_deposit"][0]["denom"]="token"' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" jq '.app_state["gov"]["params"]["min_deposit"][0]["amount"]="1"' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" jq '.app_state["gov"]["params"]["voting_period"]="10s"' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" diff --git a/precompiles/erc20factory/ERC20FactoryI.sol b/precompiles/erc20factory/ERC20FactoryI.sol new file mode 100644 index 0000000..ca27c95 --- /dev/null +++ b/precompiles/erc20factory/ERC20FactoryI.sol @@ -0,0 +1,31 @@ +// 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. +ERC20FactoryI constant ERC20_FACTORY_CONTRACT = ERC20FactoryI(ERC20_FACTORY_PRECOMPILE_ADDRESS); + +interface ERC20FactoryI { + /// @dev Defines a method for creating an ERC20 token. + /// @param tokenPairType Token Pair type + /// @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( + uint8 tokenPairType, + bytes32 salt, + string memory name, + string memory symbol, + uint8 decimals + ) external returns (address tokenAddress); + + /// @dev Calculates the deterministic address for a new token. + /// @param tokenPairType Token Pair type + /// @param salt Salt used for deployment + /// @return tokenAddress The calculated ERC20 token address. + function calculateAddress(uint8 tokenPairType, bytes32 salt) external view returns (address tokenAddress); +} diff --git a/precompiles/erc20factory/abi.json b/precompiles/erc20factory/abi.json new file mode 100644 index 0000000..433badf --- /dev/null +++ b/precompiles/erc20factory/abi.json @@ -0,0 +1,74 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "ERC20FactoryI", + "sourceName": "solidity/precompiles/erc20factory/ERC20FactoryI.sol", + "abi": [ + { + "inputs": [ + { + "internalType": "uint8", + "name": "tokenPairType", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "salt", + "type": "bytes32" + } + ], + "name": "calculateAddress", + "outputs": [ + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "tokenPairType", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "salt", + "type": "bytes32" + }, + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "symbol", + "type": "string" + }, + { + "internalType": "uint8", + "name": "decimals", + "type": "uint8" + } + ], + "name": "create", + "outputs": [ + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "bytecode": "0x", + "deployedBytecode": "0x", + "linkReferences": {}, + "deployedLinkReferences": {} +} \ No newline at end of file diff --git a/precompiles/erc20factory/erc20factory.go b/precompiles/erc20factory/erc20factory.go new file mode 100644 index 0000000..7955adf --- /dev/null +++ b/precompiles/erc20factory/erc20factory.go @@ -0,0 +1,139 @@ +package erc20factory + +import ( + storetypes "cosmossdk.io/store/types" + "embed" + "fmt" + authzkeeper "github.com/cosmos/cosmos-sdk/x/authz/keeper" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + "github.com/ethereum/go-ethereum/common" + cmn "github.com/evmos/evmos/v20/precompiles/common" + erc20keeper "github.com/evmos/evmos/v20/x/erc20/keeper" + "github.com/evmos/evmos/v20/x/evm/core/vm" +) + +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.Keeper + bankKeeper bankkeeper.Keeper +} + +// NewPrecompile creates a new bech32 Precompile instance as a +// PrecompiledContract interface. +func NewPrecompile(authzKeeper authzkeeper.Keeper, erc20Keeper erc20keeper.Keeper, bankKeeper bankkeeper.Keeper) (*Precompile, error) { + newABI, err := cmn.LoadABI(f, "abi.json") + if err != nil { + return nil, err + } + + p := &Precompile{ + Precompile: cmn.Precompile{ + ABI: newABI, + AuthzKeeper: authzKeeper, + KvGasConfig: storetypes.KVGasConfig(), + TransientKVGasConfig: storetypes.TransientGasConfig(), + ApprovalExpiration: cmn.DefaultExpirationDuration, // should be configurable in the future. + }, + erc20Keeper: erc20Keeper, + bankKeeper: bankKeeper, + } + + // SetAddress defines the address of the distribution compile contract. + p.SetAddress(common.HexToAddress(Erc20FactoryAddress)) + 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, snapshot, method, initialGas, args, err := p.RunSetup(evm, contract, readOnly, p.IsTransaction) + if err != nil { + return nil, err + } + // 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)() + + switch method.Name { + // Bank queries + case CreateMethod: + bz, err = p.Create(ctx, method, contract.Caller(), args) + case CalculateAddressMethod: + bz, err = p.CalculateAddress(method, contract.Caller(), args) + default: + return nil, fmt.Errorf(cmn.ErrUnknownMethod, method.Name) + } + + if err != nil { + return nil, err + } + + cost := ctx.GasMeter().GasConsumed() - initialGas + + if !contract.UseGas(cost) { + return nil, vm.ErrOutOfGas + } + + if err := p.AddJournalEntries(stateDB, snapshot); 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(methodName string) bool { + switch methodName { + case CreateMethod: + return true + default: + return false + } +} diff --git a/precompiles/erc20factory/erc20factory_test.go b/precompiles/erc20factory/erc20factory_test.go new file mode 100644 index 0000000..9cbbdab --- /dev/null +++ b/precompiles/erc20factory/erc20factory_test.go @@ -0,0 +1,71 @@ +package erc20factory_test + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/xrplevm/node/v6/precompiles/erc20factory" +) + +func (s *PrecompileTestSuite) TestIsTransaction() { + s.SetupTest() + + // Constants + s.Require().Equal(s.precompile.Address().String(), erc20factory.Erc20FactoryAddress) + + // Queries + s.Require().False(s.precompile.IsTransaction(erc20factory.CalculateAddressMethod)) + + // Transactions + s.Require().True(s.precompile.IsTransaction(erc20factory.CreateMethod)) +} + +func (s *PrecompileTestSuite) TestRequiredGas() { + s.SetupTest() + salt := common.HexToHash("0x4f5b6f778b28c4d67a9c12345678901234567890123456789012345678901234") + + testcases := []struct { + name string + malleate func() []byte + expGas uint64 + }{ + { + name: erc20factory.CreateMethod, + malleate: func() []byte { + bz, err := s.precompile.ABI.Pack(erc20factory.CreateMethod, uint8(0), salt, "Test token", "TT", uint8(3)) + s.Require().NoError(err, "expected no error packing ABI") + return bz + }, + expGas: erc20factory.GasCreate, + }, + { + name: erc20factory.CalculateAddressMethod, + malleate: func() []byte { + bz, err := s.precompile.ABI.Pack(erc20factory.CalculateAddressMethod, uint8(0), salt) + s.Require().NoError(err, "expected no error packing ABI") + return bz + }, + expGas: erc20factory.GasCalculateAddress, + }, + { + name: "invalid method", + malleate: func() []byte { + return []byte("invalid method") + }, + expGas: 0, + }, + { + name: "input bytes too short", + malleate: func() []byte { + return []byte{0x00, 0x00, 0x00} + }, + expGas: 0, + }, + } + + for _, tc := range testcases { + s.Run(tc.name, func() { + input := tc.malleate() + + s.Require().Equal(tc.expGas, s.precompile.RequiredGas(input)) + }) + } +} diff --git a/precompiles/erc20factory/methods.go b/precompiles/erc20factory/methods.go new file mode 100644 index 0000000..7e1a6e7 --- /dev/null +++ b/precompiles/erc20factory/methods.go @@ -0,0 +1,174 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package erc20factory + +import ( + "cosmossdk.io/errors" + "encoding/binary" + "fmt" + sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + cmn "github.com/evmos/evmos/v20/precompiles/common" + erc20types "github.com/evmos/evmos/v20/x/erc20/types" +) + +const ( + // CreateMethod defines the ABI method name to create a new ERC20 Token Pair + CreateMethod = "create" + CalculateAddressMethod = "calculateAddress" +) + +// Create CreateERC20Precompile creates a new ERC20 TokenPair +func (p Precompile) Create( + ctx sdk.Context, + method *abi.Method, + caller common.Address, + args []interface{}, +) ([]byte, error) { + if len(args) != 5 { + return nil, fmt.Errorf(cmn.ErrInvalidNumberOfArgs, 5, len(args)) + } + + tokenType, ok := args[0].(uint8) + if !ok { + return nil, fmt.Errorf("invalid tokenType") + } + + salt, ok := args[1].([32]uint8) + if !ok { + return nil, fmt.Errorf("invalid salt") + } + + name, ok := args[2].(string) + if !ok || len(name) < 3 || len(name) > 128 { + return nil, fmt.Errorf("invalid name") + } + + symbol, ok := args[3].(string) + if !ok || len(symbol) < 3 || len(symbol) > 16 { + return nil, fmt.Errorf("invalid symbol") + } + + decimals, ok := args[4].(uint8) + if !ok { + return nil, fmt.Errorf("invalid decimals") + } + + address := crypto.CreateAddress2(caller, salt, calculateCodeHash(tokenType)) + + 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) + pair.TokenType = uint32(tokenType) + + p.erc20Keeper.SetToken(ctx, pair) + + err = p.erc20Keeper.EnableDynamicPrecompiles(ctx, pair.GetERC20Contract()) + if err != nil { + return nil, err + } + + return method.Outputs.Pack(address) +} + +// CalculateAddress calculates the address of a new ERC20 Token Pair +func (p Precompile) CalculateAddress( + method *abi.Method, + caller common.Address, + args []interface{}, +) ([]byte, error) { + if len(args) != 2 { + return nil, fmt.Errorf(cmn.ErrInvalidNumberOfArgs, 2, len(args)) + } + + tokenType, ok := args[0].(uint8) + if !ok { + return nil, fmt.Errorf("invalid tokenType") + } + + salt, ok := args[1].([32]uint8) + if !ok { + return nil, fmt.Errorf("invalid salt") + } + + address := crypto.CreateAddress2(caller, salt, calculateCodeHash(tokenType)) + + return method.Outputs.Pack(address) +} + +func calculateCodeHash(tokenType uint8) []byte { + tokenTypeBytes := make([]byte, 4) + binary.LittleEndian.PutUint32(tokenTypeBytes, uint32(tokenType)) + return tokenTypeBytes +} + +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/methods_test.go b/precompiles/erc20factory/methods_test.go new file mode 100644 index 0000000..8d6fba5 --- /dev/null +++ b/precompiles/erc20factory/methods_test.go @@ -0,0 +1,264 @@ +package erc20factory_test + +import ( + "fmt" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common" + erc20types "github.com/evmos/evmos/v20/x/erc20/types" + "github.com/xrplevm/node/v6/precompiles/erc20factory" +) + +func (s *PrecompileTestSuite) TestCreate() { + method := s.precompile.Methods[erc20factory.CreateMethod] + // fromAddr is the address of the keyring account used for testing. + fromAddr := s.keyring.GetKey(0).Addr + salt := [32]uint8(common.HexToHash("0x4f5b6f778b28c4d67a9c12345678901234567890123456789012345678901234").Bytes()) + testcases := []struct { + name string + malleate func() []interface{} + postCheck func(ctx sdk.Context, output []byte) + expErr bool + errContains string + }{ + { + "success - create token", + func() []interface{} { + + return []interface{}{uint8(0), salt, "AAA", "aaa", uint8(3)} + }, + func(ctx sdk.Context, 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") + + tokenId := s.network.App.Erc20Keeper.GetTokenPairID(ctx, address.String()) + tokenPair, found := s.network.App.Erc20Keeper.GetTokenPair(ctx, tokenId) + s.Require().True(found, "expected no error getting token pair") + s.Require().Equal(uint32(0), tokenPair.TokenType, "expected TokenType to match") + s.Require().Equal(fmt.Sprintf("erc20/%s", address.String()), tokenPair.Denom, "expected token Denom to match") + s.Require().Equal("", tokenPair.OwnerAddress, "expected OwnerAddress to match") + s.Require().Equal(true, tokenPair.Enabled, "expected Enabled to match") + s.Require().Equal(address.String(), tokenPair.Erc20Address, "expected Erc20Address to match") + s.Require().Equal(erc20types.OWNER_EXTERNAL, tokenPair.ContractOwner, "expected ContractOwner to match") + }, + false, + "", + }, + { + "fail - invalid tokenType", + func() []interface{} { + return []interface{}{"", salt, "AAA", "aaa", uint8(0)} + }, + func(_ sdk.Context, _ []byte) {}, + true, + "invalid tokenType", + }, + { + "fail - invalid salt", + func() []interface{} { + return []interface{}{uint8(0), "", "AAA", "aaa", uint8(0)} + }, + func(_ sdk.Context, _ []byte) {}, + true, + "invalid salt", + }, + { + "fail - invalid name (bad type)", + func() []interface{} { + return []interface{}{uint8(0), salt, 10, "aaa", uint8(0)} + }, + func(_ sdk.Context, _ []byte) {}, + true, + "invalid name", + }, + { + "fail - invalid name (too short)", + func() []interface{} { + return []interface{}{uint8(0), salt, "AA", "aaa", uint8(0)} + }, + func(_ sdk.Context, _ []byte) {}, + true, + "invalid name", + }, + { + "fail - invalid name (too long)", + func() []interface{} { + return []interface{}{ + uint8(0), + salt, + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "aaa", + uint8(0), + } + }, + func(_ sdk.Context, _ []byte) {}, + true, + "invalid name", + }, + { + "fail - invalid symbol (bad type)", + func() []interface{} { + return []interface{}{ + uint8(0), + salt, + "AAA", + 0, + uint8(0), + } + }, + func(_ sdk.Context, _ []byte) {}, + true, + "invalid symbol", + }, + { + "fail - invalid symbol (too short)", + func() []interface{} { + return []interface{}{ + uint8(0), + salt, + "AAA", + "AA", + uint8(0), + } + }, + func(_ sdk.Context, _ []byte) {}, + true, + "invalid symbol", + }, + { + "fail - invalid symbol (too long)", + func() []interface{} { + return []interface{}{ + uint8(0), + salt, + "AAA", + "AAAAAAAAAAAAAAAAA", + uint8(0), + } + }, + func(_ sdk.Context, _ []byte) {}, + true, + "invalid symbol", + }, + { + "fail - invalid decimals", + func() []interface{} { + return []interface{}{ + uint8(0), + salt, + "AAA", + "AAA", + uint32(0), + } + }, + func(_ sdk.Context, _ []byte) {}, + true, + "invalid decimals", + }, + } + + for _, tc := range testcases { + s.Run(tc.name, func() { + s.SetupTest() + ctx := s.network.GetContext() + output, err := s.precompile.Create(ctx, &method, fromAddr, tc.malleate()) + if tc.expErr { + s.Require().Error(err, "expected create to fail") + s.Require().Contains(err.Error(), tc.errContains, "expected create to fail with specific error") + } else { + s.Require().NoError(err, "expected create succeeded") + tc.postCheck(ctx, output) + } + }) + } +} + +func (s *PrecompileTestSuite) TestCalculateAddress() { + method := s.precompile.Methods[erc20factory.CalculateAddressMethod] + // fromAddr is the address of the keyring account used for testing. + fromAddr := common.HexToAddress("0xDc411BaFB148ebDA2B63EBD5f3D8669DD4383Af5") + salt := [32]uint8(common.HexToHash("0x4f5b6f778b28c4d67a9c12345678901234567890123456789012345678901234").Bytes()) + testcases := []struct { + name string + malleate func() []interface{} + postCheck func(output []byte) + expErr bool + errContains string + }{ + { + "success - calculate address", + func() []interface{} { + return []interface{}{uint8(0), salt} + }, + 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") + s.Require().Equal(address.String(), "0x188a919f3583f8e02183332E6c73E944E002C553", "expected address to match") + }, + false, + "", + }, + { + "fail - invalid tokenType", + func() []interface{} { + return []interface{}{"", salt} + }, + func(_ []byte) {}, + true, + "invalid tokenType", + }, + { + "fail - invalid salt", + func() []interface{} { + return []interface{}{uint8(0), ""} + }, + func(_ []byte) {}, + true, + "invalid salt", + }, + } + + for _, tc := range testcases { + s.Run(tc.name, func() { + s.SetupTest() + output, err := s.precompile.CalculateAddress(&method, fromAddr, tc.malleate()) + if tc.expErr { + s.Require().Error(err, "expected create to fail") + s.Require().Contains(err.Error(), tc.errContains, "expected create to fail with specific error") + } else { + s.Require().NoError(err, "expected create succeeded") + tc.postCheck(output) + } + }) + } +} + +func (s *PrecompileTestSuite) TestAddressCalculateMatch() { + calculateAddressMethod := s.precompile.Methods[erc20factory.CalculateAddressMethod] + createMethod := s.precompile.Methods[erc20factory.CreateMethod] + fromAddr := common.HexToAddress("0xDc411BaFB148ebDA2B63EBD5f3D8669DD4383Af5") + salt := [32]uint8(common.HexToHash("0x4f5b6f778b28c4d67a9c12345678901234567890123456789012345678901234").Bytes()) + s.Run("calculated address match created", func() { + s.SetupTest() + calculateAddressOutput, err := s.precompile.CalculateAddress(&calculateAddressMethod, fromAddr, []interface{}{uint8(0), salt}) + s.Require().NoError(err, "expected no error calculating address") + calculateAddressRes, err := calculateAddressMethod.Outputs.Unpack(calculateAddressOutput) + s.Require().NoError(err, "expected no error unpacking output") + calculatedAddress, ok := calculateAddressRes[0].(common.Address) + s.Require().True(ok, "expected address type") + + createOutput, err := s.precompile.Create(s.network.GetContext(), &createMethod, fromAddr, []interface{}{uint8(0), salt, "AAA", "aaa", uint8(0)}) + s.Require().NoError(err, "expected no error creating token") + createRes, err := createMethod.Outputs.Unpack(createOutput) + s.Require().NoError(err, "expected no error unpacking output") + createdAddress, ok := createRes[0].(common.Address) + s.Require().True(ok, "expected address type") + + s.Require().Equal(calculatedAddress.String(), createdAddress.String(), "expected calculated address to match created address") + }) +} diff --git a/precompiles/erc20factory/setup_test.go b/precompiles/erc20factory/setup_test.go new file mode 100644 index 0000000..2565979 --- /dev/null +++ b/precompiles/erc20factory/setup_test.go @@ -0,0 +1,43 @@ +package erc20factory_test + +import ( + "github.com/xrplevm/node/v6/precompiles/erc20factory" + "testing" + + testkeyring "github.com/evmos/evmos/v20/testutil/integration/evmos/keyring" + "github.com/evmos/evmos/v20/testutil/integration/evmos/network" + "github.com/stretchr/testify/suite" +) + +var s *PrecompileTestSuite + +// PrecompileTestSuite is the implementation of the TestSuite interface for ERC20 precompile +// unit tests. +type PrecompileTestSuite struct { + suite.Suite + + network *network.UnitTestNetwork + keyring testkeyring.Keyring + + precompile *erc20factory.Precompile +} + +func TestPrecompileTestSuite(t *testing.T) { + s = new(PrecompileTestSuite) + suite.Run(t, s) +} + +func (s *PrecompileTestSuite) SetupTest() { + keyring := testkeyring.New(2) + integrationNetwork := network.NewUnitTestNetwork( + network.WithPreFundedAccounts(keyring.GetAllAccAddrs()...), + ) + + s.keyring = keyring + s.network = integrationNetwork + + precompile, err := erc20factory.NewPrecompile(integrationNetwork.App.AuthzKeeper, integrationNetwork.App.Erc20Keeper, integrationNetwork.App.BankKeeper) + s.Require().NoError(err, "failed to create erc20factory precompile") + + s.precompile = precompile +}