diff --git a/Makefile b/Makefile index c8381d9538..6f0267ba32 100644 --- a/Makefile +++ b/Makefile @@ -202,8 +202,13 @@ mocks: @bash ./scripts/mocks-generate.sh .PHONY: mocks +precompiles: + @echo "--> Generating bindings for precompiled contracts" + @bash ./scripts/bindings-stateful-precompiles.sh +.PHONY: precompiles + # generate also includes Go code formatting -generate: proto-gen openapi specs typescript docs-zetacored mocks fmt +generate: proto-gen openapi specs typescript docs-zetacored mocks precompiles fmt .PHONY: generate diff --git a/app/app.go b/app/app.go index 4c74dee89b..32d85214eb 100644 --- a/app/app.go +++ b/app/app.go @@ -92,6 +92,7 @@ import ( "github.com/zeta-chain/zetacore/app/ante" "github.com/zeta-chain/zetacore/docs/openapi" zetamempool "github.com/zeta-chain/zetacore/pkg/mempool" + "github.com/zeta-chain/zetacore/precompiles" srvflags "github.com/zeta-chain/zetacore/server/flags" authoritymodule "github.com/zeta-chain/zetacore/x/authority" authoritykeeper "github.com/zeta-chain/zetacore/x/authority/keeper" @@ -566,7 +567,7 @@ func New( &app.FeeMarketKeeper, tracer, evmSs, - []evmkeeper.CustomContractFn{}, + precompiles.StatefulContracts(&app.FungibleKeeper, appCodec, storetypes.TransientGasConfig()), app.ConsensusParamsKeeper, aggregateAllKeys(keys, tKeys, memKeys), ) @@ -1049,10 +1050,20 @@ func (app *App) SimulationManager() *module.SimulationManager { func (app *App) BlockedAddrs() map[string]bool { blockList := make(map[string]bool) + for k, v := range blockedReceivingModAcc { addr := authtypes.NewModuleAddress(k) blockList[addr.String()] = v } + + // Each enabled precompiled stateful contract should be added as a BlockedAddrs. + // That way it's marked as non payable by the bank keeper. + for addr, enabled := range precompiles.EnabledStatefulContracts { + if enabled { + blockList[addr.String()] = enabled + } + } + return blockList } diff --git a/changelog.md b/changelog.md index bef780d4a2..fcd82cffe7 100644 --- a/changelog.md +++ b/changelog.md @@ -8,6 +8,7 @@ * [2634](https://github.com/zeta-chain/node/pull/2634) - add support for EIP-1559 gas fees * [2597](https://github.com/zeta-chain/node/pull/2597) - Add generic rpc metrics to zetaclient * [2538](https://github.com/zeta-chain/node/pull/2538) - add background worker routines to shutdown zetaclientd when needed for tss migration +* [2633](https://github.com/zeta-chain/node/pull/2633) - support for stateful precompiled contracts. ### Refactor @@ -17,6 +18,10 @@ * [2654](https://github.com/zeta-chain/node/pull/2654) - add validation for authorization list in when validating genesis state for authorization module +### Tests + +* [2703](https://github.com/zeta-chain/node/pull/2703) - add e2e tests for stateful precompiled contracts + ## v19.0.0 ### Breaking Changes @@ -57,7 +62,6 @@ * [2524](https://github.com/zeta-chain/node/pull/2524) - add inscription envelop parsing * [2560](https://github.com/zeta-chain/node/pull/2560) - add support for Solana SOL token withdraw * [2533](https://github.com/zeta-chain/node/pull/2533) - parse memo from both OP_RETURN and inscription -* [2633](https://github.com/zeta-chain/node/pull/2633) - support for stateful precompiled contracts. ### Refactor diff --git a/cmd/zetae2e/config/clients.go b/cmd/zetae2e/config/clients.go index e70f2c566a..ac4686fc23 100644 --- a/cmd/zetae2e/config/clients.go +++ b/cmd/zetae2e/config/clients.go @@ -11,6 +11,7 @@ import ( "github.com/ethereum/go-ethereum/ethclient" "github.com/gagliardetto/solana-go/rpc" "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" "github.com/zeta-chain/zetacore/e2e/config" authoritytypes "github.com/zeta-chain/zetacore/x/authority/types" @@ -156,7 +157,7 @@ func getZetaClients(rpc string) ( zetaChainClients, error, ) { - grpcConn, err := grpc.Dial(rpc, grpc.WithInsecure()) + grpcConn, err := grpc.Dial(rpc, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { return zetaChainClients{}, err } diff --git a/cmd/zetae2e/config/localnet.yml b/cmd/zetae2e/config/localnet.yml index eb6ba8fbb9..220fb15255 100644 --- a/cmd/zetae2e/config/localnet.yml +++ b/cmd/zetae2e/config/localnet.yml @@ -41,6 +41,10 @@ additional_accounts: bech32_address: "zeta1pvtxa708yvdmszn687nne6nl8qn704daf420xz" evm_address: "0x0B166ef9e7231Bb80A7A3FA73CEA7F3827E7D5BD" private_key: "0bcc2fa28b526f90e1d54648d612db901e860bf68248555593f91ea801c6b482" + user_precompile: + bech32_address: "zeta1k4f0l2e9qqjccxnstwj0uaarxvn44lj990she9" + evm_address: "0xb552FFAb2500258C1A705Ba4Fe77A333275AFE45" + private_key: "bd6b74387f11b31d21e87c2ae7a23ec269aee08a355dad6c508a6fceb79d1f48" policy_accounts: emergency_policy_account: bech32_address: "zeta16m2cnrdwtgweq4njc6t470vl325gw4kp6s7tap" @@ -67,5 +71,4 @@ rpcs: solana: "http://solana:8899" zetacore_grpc: "zetacore0:9090" zetacore_rpc: "http://zetacore0:26657" - -# contracts will be populated on first run \ No newline at end of file +# contracts will be populated on first run diff --git a/cmd/zetae2e/local/local.go b/cmd/zetae2e/local/local.go index cb24096859..ae7af95374 100644 --- a/cmd/zetae2e/local/local.go +++ b/cmd/zetae2e/local/local.go @@ -228,6 +228,9 @@ func localE2ETest(cmd *cobra.Command, _ []string) { erc20AdvancedTests := []string{ e2etests.TestERC20DepositRestrictedName, } + precompiledContractTests := []string{ + e2etests.TestZetaPrecompilesPrototypeName, + } zetaTests := []string{ e2etests.TestZetaWithdrawName, e2etests.TestMessagePassingExternalChainsName, @@ -283,6 +286,7 @@ func localE2ETest(cmd *cobra.Command, _ []string) { ethereumTests = append(ethereumTests, ethereumAdvancedTests...) } + eg.Go(statefulPrecompilesTestRoutine(conf, deployerRunner, verbose, precompiledContractTests...)) eg.Go(erc20TestRoutine(conf, deployerRunner, verbose, erc20Tests...)) eg.Go(zetaTestRoutine(conf, deployerRunner, verbose, zetaTests...)) eg.Go(zevmMPTestRoutine(conf, deployerRunner, verbose, zevmMPTests...)) diff --git a/cmd/zetae2e/local/precompiles.go b/cmd/zetae2e/local/precompiles.go new file mode 100644 index 0000000000..55372431e6 --- /dev/null +++ b/cmd/zetae2e/local/precompiles.go @@ -0,0 +1,54 @@ +package local + +import ( + "fmt" + "time" + + "github.com/fatih/color" + + "github.com/zeta-chain/zetacore/e2e/config" + "github.com/zeta-chain/zetacore/e2e/e2etests" + "github.com/zeta-chain/zetacore/e2e/runner" +) + +// statefulPrecompilesTestRoutine runs steateful precompiles related e2e tests +func statefulPrecompilesTestRoutine( + conf config.Config, + deployerRunner *runner.E2ERunner, + verbose bool, + testNames ...string, +) func() error { + return func() (err error) { + account := conf.AdditionalAccounts.UserPrecompile + + precompileRunner, err := initTestRunner( + "precompiles", + conf, + deployerRunner, + account, + runner.NewLogger(verbose, color.FgRed, "precompiles"), + ) + if err != nil { + return err + } + + precompileRunner.Logger.Print("🏃 starting stateful precompiled contracts tests") + startTime := time.Now() + + testsToRun, err := precompileRunner.GetE2ETestsToRunByName( + e2etests.AllE2ETests, + testNames..., + ) + if err != nil { + return fmt.Errorf("precompiled contracts tests failed: %v", err) + } + + if err := precompileRunner.RunE2ETests(testsToRun); err != nil { + return fmt.Errorf("precompiled contracts tests failed: %v", err) + } + + precompileRunner.Logger.Print("🍾 precompiled contracts tests completed in %s", time.Since(startTime).String()) + + return err + } +} diff --git a/codecov.yml b/codecov.yml index 45df0a7f87..474cfe97ee 100644 --- a/codecov.yml +++ b/codecov.yml @@ -75,4 +75,9 @@ ignore: - "server" - "testutil" - "tool" - - "typescript/**/*" \ No newline at end of file + - "typescript/**/*" + - "precompiles/**/bindings.go" + - "precompiles/**/*.abi" + - "precompiles/**/*.json" + - "precompiles/**/*.sol" + - "precompiles/prototype/IPrototype.go" diff --git a/e2e/config/config.go b/e2e/config/config.go index 8e8006c042..2be878b444 100644 --- a/e2e/config/config.go +++ b/e2e/config/config.go @@ -68,6 +68,7 @@ type AdditionalAccounts struct { UserMisc Account `yaml:"user_misc"` UserAdmin Account `yaml:"user_admin"` UserMigration Account `yaml:"user_migration"` + UserPrecompile Account `yaml:"user_precompile"` } type PolicyAccounts struct { @@ -212,6 +213,7 @@ func (a AdditionalAccounts) AsSlice() []Account { a.UserMisc, a.UserAdmin, a.UserMigration, + a.UserPrecompile, } } @@ -304,6 +306,10 @@ func (c *Config) GenerateKeys() error { if err != nil { return err } + c.AdditionalAccounts.UserPrecompile, err = generateAccount() + if err != nil { + return err + } c.PolicyAccounts.EmergencyPolicyAccount, err = generateAccount() if err != nil { diff --git a/e2e/e2etests/e2etests.go b/e2e/e2etests/e2etests.go index 402eed367e..b2e78ed380 100644 --- a/e2e/e2etests/e2etests.go +++ b/e2e/e2etests/e2etests.go @@ -116,6 +116,11 @@ const ( Not used to test functionalities but do various interactions with the netwoks */ TestDeploy = "deploy" + + /* + Stateful precompiled contracts tests + */ + TestZetaPrecompilesPrototypeName = "precompile_contracts_prototype" ) // AllE2ETests is an ordered list of all e2e tests @@ -589,4 +594,13 @@ var AllE2ETests = []runner.E2ETest{ []runner.ArgDefinition{}, TestMigrateTSS, ), + /* + Stateful precompiled contracts tests + */ + runner.NewE2ETest( + TestZetaPrecompilesPrototypeName, + "test stateful precompiled contracts prototype", + []runner.ArgDefinition{}, + TestPrecompilesRegular, + ), } diff --git a/e2e/e2etests/test_precompiles_prototype.go b/e2e/e2etests/test_precompiles_prototype.go new file mode 100644 index 0000000000..a78a20385f --- /dev/null +++ b/e2e/e2etests/test_precompiles_prototype.go @@ -0,0 +1,36 @@ +package e2etests + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + + "github.com/zeta-chain/zetacore/e2e/runner" + "github.com/zeta-chain/zetacore/precompiles/prototype" +) + +func TestPrecompilesRegular(r *runner.E2ERunner, args []string) { + require.Len(r, args, 0, "No arguments expected") + + iPrototype, err := prototype.NewIPrototype(prototype.ContractAddress, r.ZEVMClient) + require.NoError(r, err, "Failed to create prototype contract caller") + + res, err := iPrototype.Bech32ify(nil, "zeta", common.HexToAddress("0xB9Dbc229Bf588A613C00BEE8e662727AB8121cfE")) + require.NoError(r, err, "Error calling Bech32ify") + require.Equal(r, "zeta1h8duy2dltz9xz0qqhm5wvcnj02upy887fyn43u", res, "Failed to validate Bech32ify result") + + addr, err := iPrototype.Bech32ToHexAddr(nil, "zeta1h8duy2dltz9xz0qqhm5wvcnj02upy887fyn43u") + require.NoError(r, err, "Error calling Bech32ToHexAddr") + require.Equal( + r, + "0xB9Dbc229Bf588A613C00BEE8e662727AB8121cfE", + addr.String(), + "Failed to validate Bech32ToHexAddr result", + ) + + chainID, err := r.EVMClient.ChainID(r.Ctx) + require.NoError(r, err, "Error retrieving ChainID") + + balance, err := iPrototype.GetGasStabilityPoolBalance(nil, chainID.Int64()) + require.NoError(r, err, "Error calling GetGasStabilityPoolBalance") + require.NotNil(r, balance, "GetGasStabilityPoolBalance returned balance is nil") +} diff --git a/precompiles/precompiles.go b/precompiles/precompiles.go new file mode 100644 index 0000000000..ad5eb55c50 --- /dev/null +++ b/precompiles/precompiles.go @@ -0,0 +1,43 @@ +package precompiles + +import ( + "github.com/cosmos/cosmos-sdk/codec" + storetypes "github.com/cosmos/cosmos-sdk/store/types" + sdktypes "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + ethparams "github.com/ethereum/go-ethereum/params" + evmkeeper "github.com/zeta-chain/ethermint/x/evm/keeper" + + "github.com/zeta-chain/zetacore/precompiles/prototype" + "github.com/zeta-chain/zetacore/x/fungible/keeper" +) + +// EnabledStatefulContracts contains the list of all enabled stateful precompiles. +// This is useful for listing and reading from other packages, such as BlockedAddrs() function. +// Setting to false a contract here will disable it, not being included in the blockchain. +var EnabledStatefulContracts = map[common.Address]bool{ + prototype.ContractAddress: true, +} + +// StatefulContracts returns all the registered precompiled contracts. +func StatefulContracts( + fungibleKeeper *keeper.Keeper, + cdc codec.Codec, + gasConfig storetypes.GasConfig, +) (precompiledContracts []evmkeeper.CustomContractFn) { + // Initialize at 0 the custom compiled contracts and the addresses. + precompiledContracts = make([]evmkeeper.CustomContractFn, 0) + + // Define the regular contract function. + if EnabledStatefulContracts[prototype.ContractAddress] { + prototype := func(_ sdktypes.Context, _ ethparams.Rules) vm.PrecompiledContract { + return prototype.NewIPrototypeContract(fungibleKeeper, cdc, gasConfig) + } + + // Append the regular contract to the precompiledContracts slice. + precompiledContracts = append(precompiledContracts, prototype) + } + + return precompiledContracts +} diff --git a/precompiles/precompiles_test.go b/precompiles/precompiles_test.go new file mode 100644 index 0000000000..06c8ccb0bb --- /dev/null +++ b/precompiles/precompiles_test.go @@ -0,0 +1,39 @@ +package precompiles + +import ( + "testing" + + storetypes "github.com/cosmos/cosmos-sdk/store/types" + ethparams "github.com/ethereum/go-ethereum/params" + "github.com/stretchr/testify/require" + ethermint "github.com/zeta-chain/ethermint/types" + "github.com/zeta-chain/zetacore/testutil/keeper" +) + +func Test_StatefulContracts(t *testing.T) { + k, ctx, _, _ := keeper.FungibleKeeper(t) + gasConfig := storetypes.TransientGasConfig() + + var encoding ethermint.EncodingConfig + appCodec := encoding.Codec + + var expectedContracts int + for _, enabled := range EnabledStatefulContracts { + if enabled { + expectedContracts++ + } + } + + // StatefulContracts() should return all the enabled contracts. + contracts := StatefulContracts(k, appCodec, gasConfig) + require.NotNil(t, contracts, "StatefulContracts() should not return a nil slice") + require.Len(t, contracts, expectedContracts, "StatefulContracts() should return all the enabled contracts") + + // Extract the contract function from the first contract. + customContractFn := contracts[0] + contract := customContractFn(ctx, ethparams.Rules{}) + + // Check the contract function returns a valid address. + contractAddr := contract.Address() + require.NotNil(t, contractAddr, "The called contract should have a valid address") +} diff --git a/precompiles/prototype/IPrototype.abi b/precompiles/prototype/IPrototype.abi new file mode 100644 index 0000000000..d510aa3ec5 --- /dev/null +++ b/precompiles/prototype/IPrototype.abi @@ -0,0 +1,64 @@ +[ + { + "inputs": [ + { + "internalType": "string", + "name": "bech32", + "type": "string" + } + ], + "name": "bech32ToHexAddr", + "outputs": [ + { + "internalType": "address", + "name": "addr", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "prefix", + "type": "string" + }, + { + "internalType": "address", + "name": "addr", + "type": "address" + } + ], + "name": "bech32ify", + "outputs": [ + { + "internalType": "string", + "name": "bech32", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "int64", + "name": "chainID", + "type": "int64" + } + ], + "name": "getGasStabilityPoolBalance", + "outputs": [ + { + "internalType": "uint256", + "name": "result", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/precompiles/prototype/IPrototype.go b/precompiles/prototype/IPrototype.go new file mode 100644 index 0000000000..ab97bd36fc --- /dev/null +++ b/precompiles/prototype/IPrototype.go @@ -0,0 +1,274 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package prototype + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// IPrototypeMetaData contains all meta data concerning the IPrototype contract. +var IPrototypeMetaData = &bind.MetaData{ + ABI: "[{\"inputs\":[{\"internalType\":\"string\",\"name\":\"bech32\",\"type\":\"string\"}],\"name\":\"bech32ToHexAddr\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"addr\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"string\",\"name\":\"prefix\",\"type\":\"string\"},{\"internalType\":\"address\",\"name\":\"addr\",\"type\":\"address\"}],\"name\":\"bech32ify\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"bech32\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"int64\",\"name\":\"chainID\",\"type\":\"int64\"}],\"name\":\"getGasStabilityPoolBalance\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"result\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]", +} + +// IPrototypeABI is the input ABI used to generate the binding from. +// Deprecated: Use IPrototypeMetaData.ABI instead. +var IPrototypeABI = IPrototypeMetaData.ABI + +// IPrototype is an auto generated Go binding around an Ethereum contract. +type IPrototype struct { + IPrototypeCaller // Read-only binding to the contract + IPrototypeTransactor // Write-only binding to the contract + IPrototypeFilterer // Log filterer for contract events +} + +// IPrototypeCaller is an auto generated read-only Go binding around an Ethereum contract. +type IPrototypeCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// IPrototypeTransactor is an auto generated write-only Go binding around an Ethereum contract. +type IPrototypeTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// IPrototypeFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type IPrototypeFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// IPrototypeSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type IPrototypeSession struct { + Contract *IPrototype // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// IPrototypeCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type IPrototypeCallerSession struct { + Contract *IPrototypeCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// IPrototypeTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type IPrototypeTransactorSession struct { + Contract *IPrototypeTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// IPrototypeRaw is an auto generated low-level Go binding around an Ethereum contract. +type IPrototypeRaw struct { + Contract *IPrototype // Generic contract binding to access the raw methods on +} + +// IPrototypeCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type IPrototypeCallerRaw struct { + Contract *IPrototypeCaller // Generic read-only contract binding to access the raw methods on +} + +// IPrototypeTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type IPrototypeTransactorRaw struct { + Contract *IPrototypeTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewIPrototype creates a new instance of IPrototype, bound to a specific deployed contract. +func NewIPrototype(address common.Address, backend bind.ContractBackend) (*IPrototype, error) { + contract, err := bindIPrototype(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &IPrototype{IPrototypeCaller: IPrototypeCaller{contract: contract}, IPrototypeTransactor: IPrototypeTransactor{contract: contract}, IPrototypeFilterer: IPrototypeFilterer{contract: contract}}, nil +} + +// NewIPrototypeCaller creates a new read-only instance of IPrototype, bound to a specific deployed contract. +func NewIPrototypeCaller(address common.Address, caller bind.ContractCaller) (*IPrototypeCaller, error) { + contract, err := bindIPrototype(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &IPrototypeCaller{contract: contract}, nil +} + +// NewIPrototypeTransactor creates a new write-only instance of IPrototype, bound to a specific deployed contract. +func NewIPrototypeTransactor(address common.Address, transactor bind.ContractTransactor) (*IPrototypeTransactor, error) { + contract, err := bindIPrototype(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &IPrototypeTransactor{contract: contract}, nil +} + +// NewIPrototypeFilterer creates a new log filterer instance of IPrototype, bound to a specific deployed contract. +func NewIPrototypeFilterer(address common.Address, filterer bind.ContractFilterer) (*IPrototypeFilterer, error) { + contract, err := bindIPrototype(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &IPrototypeFilterer{contract: contract}, nil +} + +// bindIPrototype binds a generic wrapper to an already deployed contract. +func bindIPrototype(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := IPrototypeMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_IPrototype *IPrototypeRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _IPrototype.Contract.IPrototypeCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_IPrototype *IPrototypeRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _IPrototype.Contract.IPrototypeTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_IPrototype *IPrototypeRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _IPrototype.Contract.IPrototypeTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_IPrototype *IPrototypeCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _IPrototype.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_IPrototype *IPrototypeTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _IPrototype.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_IPrototype *IPrototypeTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _IPrototype.Contract.contract.Transact(opts, method, params...) +} + +// Bech32ToHexAddr is a free data retrieval call binding the contract method 0xe4e2a4ec. +// +// Solidity: function bech32ToHexAddr(string bech32) view returns(address addr) +func (_IPrototype *IPrototypeCaller) Bech32ToHexAddr(opts *bind.CallOpts, bech32 string) (common.Address, error) { + var out []interface{} + err := _IPrototype.contract.Call(opts, &out, "bech32ToHexAddr", bech32) + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// Bech32ToHexAddr is a free data retrieval call binding the contract method 0xe4e2a4ec. +// +// Solidity: function bech32ToHexAddr(string bech32) view returns(address addr) +func (_IPrototype *IPrototypeSession) Bech32ToHexAddr(bech32 string) (common.Address, error) { + return _IPrototype.Contract.Bech32ToHexAddr(&_IPrototype.CallOpts, bech32) +} + +// Bech32ToHexAddr is a free data retrieval call binding the contract method 0xe4e2a4ec. +// +// Solidity: function bech32ToHexAddr(string bech32) view returns(address addr) +func (_IPrototype *IPrototypeCallerSession) Bech32ToHexAddr(bech32 string) (common.Address, error) { + return _IPrototype.Contract.Bech32ToHexAddr(&_IPrototype.CallOpts, bech32) +} + +// Bech32ify is a free data retrieval call binding the contract method 0x0615b74e. +// +// Solidity: function bech32ify(string prefix, address addr) view returns(string bech32) +func (_IPrototype *IPrototypeCaller) Bech32ify(opts *bind.CallOpts, prefix string, addr common.Address) (string, error) { + var out []interface{} + err := _IPrototype.contract.Call(opts, &out, "bech32ify", prefix, addr) + + if err != nil { + return *new(string), err + } + + out0 := *abi.ConvertType(out[0], new(string)).(*string) + + return out0, err + +} + +// Bech32ify is a free data retrieval call binding the contract method 0x0615b74e. +// +// Solidity: function bech32ify(string prefix, address addr) view returns(string bech32) +func (_IPrototype *IPrototypeSession) Bech32ify(prefix string, addr common.Address) (string, error) { + return _IPrototype.Contract.Bech32ify(&_IPrototype.CallOpts, prefix, addr) +} + +// Bech32ify is a free data retrieval call binding the contract method 0x0615b74e. +// +// Solidity: function bech32ify(string prefix, address addr) view returns(string bech32) +func (_IPrototype *IPrototypeCallerSession) Bech32ify(prefix string, addr common.Address) (string, error) { + return _IPrototype.Contract.Bech32ify(&_IPrototype.CallOpts, prefix, addr) +} + +// GetGasStabilityPoolBalance is a free data retrieval call binding the contract method 0x3ee8d1a4. +// +// Solidity: function getGasStabilityPoolBalance(int64 chainID) view returns(uint256 result) +func (_IPrototype *IPrototypeCaller) GetGasStabilityPoolBalance(opts *bind.CallOpts, chainID int64) (*big.Int, error) { + var out []interface{} + err := _IPrototype.contract.Call(opts, &out, "getGasStabilityPoolBalance", chainID) + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// GetGasStabilityPoolBalance is a free data retrieval call binding the contract method 0x3ee8d1a4. +// +// Solidity: function getGasStabilityPoolBalance(int64 chainID) view returns(uint256 result) +func (_IPrototype *IPrototypeSession) GetGasStabilityPoolBalance(chainID int64) (*big.Int, error) { + return _IPrototype.Contract.GetGasStabilityPoolBalance(&_IPrototype.CallOpts, chainID) +} + +// GetGasStabilityPoolBalance is a free data retrieval call binding the contract method 0x3ee8d1a4. +// +// Solidity: function getGasStabilityPoolBalance(int64 chainID) view returns(uint256 result) +func (_IPrototype *IPrototypeCallerSession) GetGasStabilityPoolBalance(chainID int64) (*big.Int, error) { + return _IPrototype.Contract.GetGasStabilityPoolBalance(&_IPrototype.CallOpts, chainID) +} diff --git a/precompiles/prototype/IPrototype.json b/precompiles/prototype/IPrototype.json new file mode 100644 index 0000000000..3b7c9ea8cb --- /dev/null +++ b/precompiles/prototype/IPrototype.json @@ -0,0 +1,66 @@ +{ + "abi": [ + { + "inputs": [ + { + "internalType": "string", + "name": "bech32", + "type": "string" + } + ], + "name": "bech32ToHexAddr", + "outputs": [ + { + "internalType": "address", + "name": "addr", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "prefix", + "type": "string" + }, + { + "internalType": "address", + "name": "addr", + "type": "address" + } + ], + "name": "bech32ify", + "outputs": [ + { + "internalType": "string", + "name": "bech32", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "int64", + "name": "chainID", + "type": "int64" + } + ], + "name": "getGasStabilityPoolBalance", + "outputs": [ + { + "internalType": "uint256", + "name": "result", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } + ] +} diff --git a/precompiles/prototype/IPrototype.sol b/precompiles/prototype/IPrototype.sol new file mode 100644 index 0000000000..eb8f34d770 --- /dev/null +++ b/precompiles/prototype/IPrototype.sol @@ -0,0 +1,34 @@ +pragma solidity ^0.8.26; + +/// @dev The IPrototype contract's address. +address constant IPROTOTYPE_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000065; // 101 + +/// @dev The IPrototype contract's instance. +IPrototype constant IPROTOTYPE_CONTRACT = IPrototype( + IPROTOTYPE_PRECOMPILE_ADDRESS +); + +interface IPrototype { + /// @dev converting a bech32 address to hexadecimal address. + /// @param bech32 The bech32 address. + /// @return addr The hexadecimal address. + function bech32ToHexAddr( + string memory bech32 + ) external view returns (address addr); + + /// @dev converting a hex address to bech32 address. + /// @param prefix of the bech32, e.g. zeta. + /// @param addr The hex address + /// @return bech32 The bech32 address. + function bech32ify( + string memory prefix, + address addr + ) external view returns (string memory bech32); + + /// @dev returns the balance of the gas stability pool + /// @param chainID to query gas. + /// @return result of the call. + function getGasStabilityPoolBalance( + int64 chainID + ) external view returns (uint256 result); +} diff --git a/precompiles/prototype/bindings.go b/precompiles/prototype/bindings.go new file mode 100644 index 0000000000..e4a31a5e56 --- /dev/null +++ b/precompiles/prototype/bindings.go @@ -0,0 +1,7 @@ +//go:generate sh -c "solc IPrototype.sol --combined-json abi | jq '.contracts.\"IPrototype.sol:IPrototype\"' > IPrototype.json" +//go:generate sh -c "cat IPrototype.json | jq .abi > IPrototype.abi" +//go:generate sh -c "abigen --abi IPrototype.abi --pkg prototype --type IPrototype --out IPrototype.go" + +package prototype + +var _ Contract diff --git a/precompiles/prototype/prototype.go b/precompiles/prototype/prototype.go new file mode 100644 index 0000000000..930095f32f --- /dev/null +++ b/precompiles/prototype/prototype.go @@ -0,0 +1,258 @@ +package prototype + +import ( + "fmt" + "strings" + + "github.com/cosmos/cosmos-sdk/codec" + storetypes "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + + ptypes "github.com/zeta-chain/zetacore/precompiles/types" + fungiblekeeper "github.com/zeta-chain/zetacore/x/fungible/keeper" +) + +const ( + Bech32ToHexAddrMethodName = "bech32ToHexAddr" + Bech32ifyMethodName = "bech32ify" + GetGasStabilityPoolBalanceName = "getGasStabilityPoolBalance" +) + +var ( + ABI abi.ABI + ContractAddress = common.HexToAddress("0x0000000000000000000000000000000000000065") + GasRequiredByMethod = map[[4]byte]uint64{} +) + +func init() { + initABI() +} + +func initABI() { + if err := ABI.UnmarshalJSON([]byte(IPrototypeMetaData.ABI)); err != nil { + panic(err) + } + + GasRequiredByMethod = map[[4]byte]uint64{} + for methodName := range ABI.Methods { + var methodID [4]byte + copy(methodID[:], ABI.Methods[methodName].ID[:4]) + switch methodName { + case Bech32ToHexAddrMethodName: + GasRequiredByMethod[methodID] = 10000 + case Bech32ifyMethodName: + GasRequiredByMethod[methodID] = 10000 + case GetGasStabilityPoolBalanceName: + GasRequiredByMethod[methodID] = 10000 + default: + GasRequiredByMethod[methodID] = 0 + } + } +} + +type Contract struct { + ptypes.BaseContract + + fungibleKeeper fungiblekeeper.Keeper + cdc codec.Codec + kvGasConfig storetypes.GasConfig +} + +func NewIPrototypeContract( + fungibleKeeper *fungiblekeeper.Keeper, + cdc codec.Codec, + kvGasConfig storetypes.GasConfig, +) *Contract { + return &Contract{ + BaseContract: ptypes.NewBaseContract(ContractAddress), + fungibleKeeper: *fungibleKeeper, + cdc: cdc, + kvGasConfig: kvGasConfig, + } +} + +// Address() is required to implement the PrecompiledContract interface. +func (c *Contract) Address() common.Address { + return ContractAddress +} + +// Abi() is required to implement the PrecompiledContract interface. +func (c *Contract) Abi() abi.ABI { + return ABI +} + +// RequiredGas is required to implement the PrecompiledContract interface. +// The gas has to be calculated deterministically based on the input. +func (c *Contract) RequiredGas(input []byte) uint64 { + // base cost to prevent large input size + baseCost := uint64(len(input)) * c.kvGasConfig.WriteCostPerByte + + // get methodID (first 4 bytes) + var methodID [4]byte + copy(methodID[:], input[:4]) + + if requiredGas, ok := GasRequiredByMethod[methodID]; ok { + return requiredGas + baseCost + } + + // Can not happen, but return 0 if the method is not found. + return 0 +} + +// Bech32ToHexAddr converts a bech32 address to a hex address. +func (c *Contract) Bech32ToHexAddr(method *abi.Method, args []interface{}) ([]byte, error) { + if len(args) != 1 { + return nil, &ptypes.ErrInvalidNumberOfArgs{ + Got: len(args), + Expect: 1, + } + } + + bech32String, ok := args[0].(string) + if !ok { + return nil, fmt.Errorf("invalid argument, wanted a string, got: %T", args[0]) + } + + bech32String = strings.TrimSpace(bech32String) + if bech32String == "" { + return nil, fmt.Errorf("invalid bech32 address: %s", bech32String) + } + + // 1 is always the separator between the bech32 prefix and the bech32 data part. + // https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#bech32 + bech32Prefix, bech32Data, found := strings.Cut(bech32String, "1") + if !found || bech32Data == "" || bech32Prefix == "" || bech32Prefix == bech32String { + return nil, fmt.Errorf("invalid bech32 address: %s", bech32String) + } + + addressBz, err := sdk.GetFromBech32(bech32String, bech32Prefix) + if err != nil { + return nil, err + } + + if err := sdk.VerifyAddressFormat(addressBz); err != nil { + return nil, err + } + + return method.Outputs.Pack(common.BytesToAddress(addressBz)) +} + +// Bech32ify converts a hex address to a bech32 address. +func (c *Contract) Bech32ify(method *abi.Method, args []interface{}) ([]byte, error) { + if len(args) != 2 { + return nil, &ptypes.ErrInvalidNumberOfArgs{ + Got: len(args), + Expect: 2, + } + } + + cfg := sdk.GetConfig() + prefix, ok := args[0].(string) + if !ok { + return nil, fmt.Errorf("invalid bech32 human readable prefix (HRP): %v", args[0]) + } + + if strings.TrimSpace(prefix) == "" { + return nil, fmt.Errorf( + "invalid bech32 human readable prefix (HRP). Please provide a either an account, validator or consensus address prefix (eg: %s, %s, %s)", + cfg.GetBech32AccountAddrPrefix(), + cfg.GetBech32ValidatorAddrPrefix(), + cfg.GetBech32ConsensusAddrPrefix(), + ) + } + + address, ok := args[1].(common.Address) + if !ok { + return nil, fmt.Errorf("invalid hex address") + } + + // NOTE: safety check, should not happen given that the address is 20 bytes. + if err := sdk.VerifyAddressFormat(address.Bytes()); err != nil { + return nil, err + } + + bech32Str, err := sdk.Bech32ifyAddressBytes(prefix, address.Bytes()) + if err != nil { + return nil, err + } + + addressBz, err := sdk.GetFromBech32(bech32Str, prefix) + if err != nil { + return nil, err + } + + if err := sdk.VerifyAddressFormat(addressBz); err != nil { + return nil, err + } + + return method.Outputs.Pack(bech32Str) +} + +func (c *Contract) GetGasStabilityPoolBalance( + ctx sdk.Context, + method *abi.Method, + args []interface{}, +) ([]byte, error) { + if len(args) != 1 { + return nil, &(ptypes.ErrInvalidNumberOfArgs{ + Got: len(args), + Expect: 1, + }) + } + + // Unwrap arguments. The chainID is the first and unique argument. + chainID, ok := args[0].(int64) + if !ok { + return nil, ptypes.ErrInvalidArgument{ + Got: chainID, + } + } + + balance, err := c.fungibleKeeper.GetGasStabilityPoolBalance(ctx, chainID) + if err != nil { + return nil, fmt.Errorf("error calling fungible keeper: %s", err.Error()) + } + + return method.Outputs.Pack(balance) +} + +// Run is the entrypoint of the precompiled contract, it switches over the input method, +// and execute them accordingly. +func (c *Contract) Run(evm *vm.EVM, contract *vm.Contract, _ bool) ([]byte, error) { + method, err := ABI.MethodById(contract.Input[:4]) + if err != nil { + return nil, err + } + + args, err := method.Inputs.Unpack(contract.Input[4:]) + if err != nil { + return nil, err + } + + stateDB := evm.StateDB.(ptypes.ExtStateDB) + + switch method.Name { + case GetGasStabilityPoolBalanceName: + var res []byte + execErr := stateDB.ExecuteNativeAction(contract.Address(), nil, func(ctx sdk.Context) error { + res, err = c.GetGasStabilityPoolBalance(ctx, method, args) + return err + }) + if execErr != nil { + return nil, err + } + return res, nil + + case Bech32ToHexAddrMethodName: + return c.Bech32ToHexAddr(method, args) + case Bech32ifyMethodName: + return c.Bech32ify(method, args) + default: + return nil, ptypes.ErrInvalidMethod{ + Method: method.Name, + } + } +} diff --git a/precompiles/prototype/prototype_test.go b/precompiles/prototype/prototype_test.go new file mode 100644 index 0000000000..3c0d88b642 --- /dev/null +++ b/precompiles/prototype/prototype_test.go @@ -0,0 +1,339 @@ +package prototype + +import ( + "encoding/json" + "testing" + + storetypes "github.com/cosmos/cosmos-sdk/store/types" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + ethermint "github.com/zeta-chain/ethermint/types" + "github.com/zeta-chain/zetacore/precompiles/types" + "github.com/zeta-chain/zetacore/testutil/keeper" +) + +func Test_IPrototypeContract(t *testing.T) { + /* + Configuration + */ + + var encoding ethermint.EncodingConfig + appCodec := encoding.Codec + k, _, _, _ := keeper.FungibleKeeper(t) + gasConfig := storetypes.TransientGasConfig() + + /* + Contract and ABI tests + */ + + // Create a new IPrototypeContract instance and get Address and Abi. + contract := NewIPrototypeContract(k, appCodec, gasConfig) + require.NotNil(t, contract, "NewIPrototypeContract() should not return a nil contract") + + address := contract.Address() + require.Equal(t, ContractAddress, address, "contract address should match the precompiled address") + + abi := contract.Abi() + require.NotNil(t, abi, "contract ABI should not be nil") + + // Check all methods are present in the ABI. + bech32ToHex := abi.Methods[Bech32ToHexAddrMethodName] + require.NotNil(t, bech32ToHex, "bech32ToHexAddr method should be present in the ABI") + + bech32ify := abi.Methods[Bech32ifyMethodName] + require.NotNil(t, bech32ify, "bech32ify method should be present in the ABI") + + getGasStabilityPoolBalance := abi.Methods[GetGasStabilityPoolBalanceName] + require.NotNil(t, getGasStabilityPoolBalance, "getGasStabilityPoolBalance method should be present in the ABI") + + /* + Gas tests + */ + + // Check all methods use the correct gas amount. + var method [4]byte + + gasBech32ToHex := contract.RequiredGas(bech32ToHex.ID) + copy(method[:], bech32ToHex.ID[:4]) + baseCost := uint64(len(method)) * gasConfig.WriteCostPerByte + require.Equal( + t, + GasRequiredByMethod[method]+baseCost, + gasBech32ToHex, + "bech32ToHexAddr method should require %d gas, got %d", + GasRequiredByMethod[method]+baseCost, + gasBech32ToHex) + + gasBech32ify := contract.RequiredGas(bech32ify.ID) + copy(method[:], bech32ify.ID[:4]) + baseCost = uint64(len(method)) * gasConfig.WriteCostPerByte + require.Equal( + t, + GasRequiredByMethod[method]+baseCost, + gasBech32ify, + "bech32ify method should require %d gas, got %d", + GasRequiredByMethod[method]+baseCost, + gasBech32ify) + + gasGetGasStabilityPoolBalance := contract.RequiredGas(getGasStabilityPoolBalance.ID) + copy(method[:], getGasStabilityPoolBalance.ID[:4]) + baseCost = uint64(len(method)) * gasConfig.WriteCostPerByte + require.Equal( + t, + GasRequiredByMethod[method]+baseCost, + gasGetGasStabilityPoolBalance, + "getGasStabilityPoolBalance method should require %d gas, got %d", + GasRequiredByMethod[method]+baseCost, + gasGetGasStabilityPoolBalance) + + // Can not happen, but check the gas for an invalid method. + // At runtime if the method does not exist in the ABI, it returns an error. + invalidMethodBytes := []byte("invalidMethod") + gasInvalidMethod := contract.RequiredGas(invalidMethodBytes) + baseCost = uint64(len(method)) * gasConfig.WriteCostPerByte + require.Equal( + t, + uint64(0), + gasInvalidMethod, + "invalid method should require %d gas, got %d", + uint64(0), + gasInvalidMethod) +} + +func Test_Bech32ToHexAddress(t *testing.T) { + /* + Configuration + */ + + var encoding ethermint.EncodingConfig + appCodec := encoding.Codec + k, _, _, _ := keeper.FungibleKeeper(t) + gasConfig := storetypes.TransientGasConfig() + + // Create contract and get ABI. + contract := NewIPrototypeContract(k, appCodec, gasConfig) + require.NotNil(t, contract, "NewIPrototypeContract() should not return a nil contract") + + abi := contract.Abi() + require.NotNil(t, abi, "contract ABI should not be nil") + + // Test Bech32HexAddr method. Should succeed. + methodID := abi.Methods[Bech32ToHexAddrMethodName] + args := make([]interface{}, 0) + args = append(args, "zeta1h8duy2dltz9xz0qqhm5wvcnj02upy887fyn43u") + + rawBytes, err := contract.Bech32ToHexAddr(&methodID, args) + require.NoError(t, err, "Bech32ToHexAddr should not return an error") + + // Discard the first 12 bytes, the address is the last 20 bytes. + addr := common.BytesToAddress(rawBytes[12:]) + require.Equal( + t, + common.HexToAddress("0xB9Dbc229Bf588A613C00BEE8e662727AB8121cfE"), + addr, + "Bech32ToHexAddr should return the correct address, got: %v", + addr, + ) + + // Test Bech32HexAddr method. Should fail with invalid argument type.. + args[0] = 1 + rawBytes, err = contract.Bech32ToHexAddr(&methodID, args) + require.Error(t, err, "expected invalid argument; wanted string; got: %T", args[0]) + + // Test Bech32HexAddr method. Should fail because it's not a valid bech32 address. + args[0] = "foobar" + rawBytes, err = contract.Bech32ToHexAddr(&methodID, args) + require.Error(t, err, "expected error; invalid bech32 address") + + // Test Bech32HexAddr method. Should fail with invalid prefix. + args[0] = "foobar1" + rawBytes, err = contract.Bech32ToHexAddr(&methodID, args) + require.Error(t, err, "expected error; invalid bech32 addresss") + + // Test Bech32HexAddr method. Should fail with invalid prefix. + args[0] = "foobar1foobar" + rawBytes, err = contract.Bech32ToHexAddr(&methodID, args) + require.Error(t, err, "expected error; decoding bech32 failed") + + // Test Bech32HexAddr method. Should fail with invalid number of arguments. + args = append(args, "second argument") + _, err = contract.Bech32ToHexAddr(&methodID, args) + require.Error(t, err, "expected invalid number of arguments; expected 1; got: 2") + require.IsType(t, &types.ErrInvalidNumberOfArgs{}, err, "expected error type: ErrInvalidNumberOfArgs, got: %T", err) + + // Test Bech32HexAddr method. Should fail with invalid address. + argsInvalid := make([]interface{}, 0) + argsInvalid = append(argsInvalid, "") + _, errInvalid := contract.Bech32ToHexAddr(&methodID, argsInvalid) + require.Error(t, errInvalid, "expected error invalid bech32 address: %v", argsInvalid[0]) +} + +func Test_Bech32ify(t *testing.T) { + /* + Configuration + */ + + var encoding ethermint.EncodingConfig + appCodec := encoding.Codec + k, _, _, _ := keeper.FungibleKeeper(t) + gasConfig := storetypes.TransientGasConfig() + + // Create contract and get ABI. + contract := NewIPrototypeContract(k, appCodec, gasConfig) + require.NotNil(t, contract, "NewIPrototypeContract() should not return a nil contract") + + abi := contract.Abi() + require.NotNil(t, abi, "contract ABI should not be nil") + + // Test Bech32ify method with a zeta HRP. + methodID := abi.Methods[Bech32ifyMethodName] + args := make([]interface{}, 0) + args = append(args, "zeta") + args = append(args, common.HexToAddress("0xB9Dbc229Bf588A613C00BEE8e662727AB8121cfE")) + + rawBytes, err := contract.Bech32ify(&methodID, args) + require.NoError(t, err, "Bech32ify prefix zeta should not return an error") + + // Manually extract the address from the raw bytes. + zetaAddr := string(rawBytes[64:107]) + require.Equal( + t, + "zeta1h8duy2dltz9xz0qqhm5wvcnj02upy887fyn43u", + string(zetaAddr), + "Bech32ify prefix zeta should return the correct address, got: %v", + zetaAddr, + ) + + // Test Bech32ify method with a cosmos HRP. + args[0] = "cosmos" + rawBytes, err = contract.Bech32ify(&methodID, args) + require.NoError(t, err, "Bech32ify prefix cosmos should not return an error") + + zetaAddr = string(rawBytes[64:107]) + require.Equal( + t, + "cosmos1h8duy2dltz9xz0qqhm5wvcnj02upy887lqaq", + string(zetaAddr), + "Bech32ify prefix cosmos should return the correct address, got: %v", + zetaAddr, + ) + + // Test for invalid number of arguments. + args = append(args, "third argument") + _, err = contract.Bech32ify(&methodID, args) + require.Error(t, err, "expected invalid number of arguments; expected 2; got: 3") + require.IsType(t, &types.ErrInvalidNumberOfArgs{}, err, "expected error type: ErrInvalidNumberOfArgs, got: %T", err) + + // Test for invalid bech32 human readable prefix. + argsInvalidBech32 := make([]interface{}, 0) + argsInvalidBech32 = append(argsInvalidBech32, 1337) + argsInvalidBech32 = append(argsInvalidBech32, common.HexToAddress("0xB9Dbc229Bf588A613C00BEE8e662727AB8121cfE")) + _, errInvalidBech32 := contract.Bech32ify(&methodID, argsInvalidBech32) + require.Error(t, errInvalidBech32, "expected error invalid bech32 human readable prefix (HRP)") + + // Test for invalid hex address. + argsInvalidHexAddress := make([]interface{}, 0) + argsInvalidHexAddress = append(argsInvalidHexAddress, "zeta") + argsInvalidHexAddress = append(argsInvalidHexAddress, 1337) + _, errInvalidHexAddress := contract.Bech32ify(&methodID, argsInvalidHexAddress) + require.Error(t, errInvalidHexAddress, "expected error invalid hex address") + + // Test for invalid bech32 human readable prefix. + argsInvalidEmptyPrefix := make([]interface{}, 0) + argsInvalidEmptyPrefix = append(argsInvalidEmptyPrefix, "") + argsInvalidEmptyPrefix = append( + argsInvalidEmptyPrefix, + common.HexToAddress("0xB9Dbc229Bf588A613C00BEE8e662727AB8121cfE"), + ) + _, errInvalidEmptyPrefix := contract.Bech32ify(&methodID, argsInvalidEmptyPrefix) + require.Error( + t, + errInvalidEmptyPrefix, + "expected error invalid bech32 human readable prefix (HRP). Please provide a either an account, validator or consensus address prefix (eg: cosmos, cosmosvaloper, cosmosvalcons)", + ) +} + +func Test_GetGasStabilityPoolBalance(t *testing.T) { + /* + Configuration + */ + + var encoding ethermint.EncodingConfig + appCodec := encoding.Codec + k, ctx, _, _ := keeper.FungibleKeeper(t) + gasConfig := storetypes.TransientGasConfig() + + // Create contract and get ABI. + contract := NewIPrototypeContract(k, appCodec, gasConfig) + require.NotNil(t, contract, "NewIPrototypeContract() should not return a nil contract") + + abi := contract.Abi() + require.NotNil(t, abi, "contract ABI should not be nil") + + // Test GetGasStabilityPoolBalance method. + // Only check the function is called correctly inside the contract, and it returns the expected error. + // Configuring a local environment for this contract would require deploying system contracts and gas pools. + // This method is tested thoroughly in the e2e tests. + methodID := abi.Methods[GetGasStabilityPoolBalanceName] + args := make([]interface{}, 0) + args = append(args, int64(1337)) + + _, err := contract.GetGasStabilityPoolBalance(ctx, &methodID, args) + require.Error( + t, + err, + "error calling fungible keeper: failed to get system contract variable: state variable not found", + ) + + // Test for invalid number of arguments. + args = append(args, "second argument") + _, err = contract.GetGasStabilityPoolBalance(ctx, &methodID, args) + require.Error(t, err, "expected invalid number of arguments; expected 2; got: 3") + require.IsType(t, &types.ErrInvalidNumberOfArgs{}, err, "expected error type: ErrInvalidNumberOfArgs, got: %T", err) + + // Test for invalid chainID. + argsInvalid := make([]interface{}, 0) + argsInvalid = append(argsInvalid, "foobar") + _, errInvalid := contract.GetGasStabilityPoolBalance(ctx, &methodID, argsInvalid) + require.Error(t, errInvalid, "expected int64, got: %T", argsInvalid[0]) + require.IsType( + t, + types.ErrInvalidArgument{}, + errInvalid, + "expected error type: ErrInvalidArgument, got: %T", + errInvalid, + ) +} + +func Test_InvalidMethod(t *testing.T) { + /* + Configuration + */ + + var encoding ethermint.EncodingConfig + appCodec := encoding.Codec + k, _, _, _ := keeper.FungibleKeeper(t) + gasConfig := storetypes.TransientGasConfig() + + // Create contract and get ABI. + contract := NewIPrototypeContract(k, appCodec, gasConfig) + require.NotNil(t, contract, "NewIPrototypeContract() should not return a nil contract") + + abi := contract.Abi() + require.NotNil(t, abi, "contract ABI should not be nil") + + // Test for non existent method. + _, doNotExist := abi.Methods["invalidMethod"] + require.False(t, doNotExist, "invalidMethod should not be present in the ABI") +} + +func Test_InvalidABI(t *testing.T) { + IPrototypeMetaData.ABI = "invalid json" + defer func() { + if r := recover(); r != nil { + require.IsType(t, &json.SyntaxError{}, r, "expected error type: json.SyntaxError, got: %T", r) + } + }() + + initABI() +} diff --git a/precompiles/types/errors.go b/precompiles/types/errors.go new file mode 100644 index 0000000000..0cc6928541 --- /dev/null +++ b/precompiles/types/errors.go @@ -0,0 +1,44 @@ +package types + +import "fmt" + +/* +Address related errors +*/ +type ErrInvalidAddr struct { + Got string +} + +func (e ErrInvalidAddr) Error() string { + return fmt.Sprintf("invalid address %s", e.Got) +} + +/* +Argument related errors +*/ +type ErrInvalidNumberOfArgs struct { + Got, Expect int +} + +func (e ErrInvalidNumberOfArgs) Error() string { + return fmt.Sprintf("invalid number of arguments; expected %d; got: %d", e.Expect, e.Got) +} + +type ErrInvalidArgument struct { + Got any +} + +func (e ErrInvalidArgument) Error() string { + return fmt.Sprintf("invalid argument: %s", e.Got.(string)) +} + +/* +Method related errors +*/ +type ErrInvalidMethod struct { + Method string +} + +func (e ErrInvalidMethod) Error() string { + return fmt.Sprintf("invalid method: %s", e.Method) +} diff --git a/precompiles/types/errors_test.go b/precompiles/types/errors_test.go new file mode 100644 index 0000000000..5693a450eb --- /dev/null +++ b/precompiles/types/errors_test.go @@ -0,0 +1,48 @@ +package types + +import "testing" + +func Test_ErrInvalidAddr(t *testing.T) { + e := ErrInvalidAddr{ + Got: "foo", + } + got := e.Error() + expect := "invalid address foo" + if got != expect { + t.Errorf("Expected %v, got %v", expect, got) + } +} + +func Test_ErrInvalidNumberOfArgs(t *testing.T) { + e := ErrInvalidNumberOfArgs{ + Got: 1, + Expect: 2, + } + got := e.Error() + expect := "invalid number of arguments; expected 2; got: 1" + if got != expect { + t.Errorf("Expected %v, got %v", expect, got) + } +} + +func Test_ErrInvalidArgument(t *testing.T) { + e := ErrInvalidArgument{ + Got: "foo", + } + got := e.Error() + expect := "invalid argument: foo" + if got != expect { + t.Errorf("Expected %v, got %v", expect, got) + } +} + +func Test_ErrInvalidMethod(t *testing.T) { + e := ErrInvalidMethod{ + Method: "foo", + } + got := e.Error() + expect := "invalid method: foo" + if got != expect { + t.Errorf("Expected %v, got %v", expect, got) + } +} diff --git a/precompiles/types/types.go b/precompiles/types/types.go new file mode 100644 index 0000000000..7bd4a57bfa --- /dev/null +++ b/precompiles/types/types.go @@ -0,0 +1,54 @@ +package types + +import ( + "math/big" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/zeta-chain/ethermint/x/evm/statedb" +) + +// Interface compliance. +var _ ExtStateDB = (*statedb.StateDB)(nil) +var _ Registrable = (*baseContract)(nil) +var _ BaseContract = (*baseContract)(nil) + +// ExtStateDB defines extra methods of statedb to support stateful precompiled contracts. +// It's used to persist changes into the store. +type ExtStateDB interface { + vm.StateDB + ExecuteNativeAction( + contract common.Address, + converter statedb.EventConverter, + action func(ctx sdk.Context) error, + ) error + CacheContext() sdk.Context +} + +type Registrable interface { + RegistryKey() common.Address +} + +type BaseContract interface { + Registrable +} + +// A baseContract implements Registrable and BaseContract interfaces. +type baseContract struct { + address common.Address +} + +func NewBaseContract(address common.Address) BaseContract { + return &baseContract{ + address: address, + } +} + +func (c *baseContract) RegistryKey() common.Address { + return c.address +} + +func BytesToBigInt(data []byte) *big.Int { + return big.NewInt(0).SetBytes(data[:]) +} diff --git a/precompiles/types/types_test.go b/precompiles/types/types_test.go new file mode 100644 index 0000000000..1193b55426 --- /dev/null +++ b/precompiles/types/types_test.go @@ -0,0 +1,26 @@ +package types + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +func Test_BaseContract(t *testing.T) { + addr := common.BytesToAddress([]byte{0x1}) + + contract := NewBaseContract(addr) + require.NotNil(t, contract) + + // By implementing RegistryKey() the baseContract struct implements Registrable. + // Registrable is the unique requisite to implement BaseContract as well. + require.Equal(t, addr, contract.RegistryKey()) +} + +func Test_BytesToBigInt(t *testing.T) { + data := []byte{0x1} + intData := BytesToBigInt(data) + require.NotNil(t, intData) + require.Equal(t, int64(1), intData.Int64()) +} diff --git a/scripts/bindings-stateful-precompiles.sh b/scripts/bindings-stateful-precompiles.sh new file mode 100755 index 0000000000..cd649ed24c --- /dev/null +++ b/scripts/bindings-stateful-precompiles.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +# Generic function to generate bindings +function bindings() { + cd $1 + go generate > /dev/null 2>&1 + echo "Generated bindings for $1" +} + +# List of bindings to generate +bindings ./precompiles/prototype +