diff --git a/contracts/contracts/ExampleHelloWorld.sol b/contracts/contracts/ExampleHelloWorld.sol new file mode 100644 index 0000000000..b64fd0ac0c --- /dev/null +++ b/contracts/contracts/ExampleHelloWorld.sol @@ -0,0 +1,19 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./interfaces/IHelloWorld.sol"; + +address constant HELLO_WORLD_ADDRESS = 0x0300000000000000000000000000000000000000; + +// ExampleHelloWorld shows how the HelloWorld precompile can be used in a smart contract. +contract ExampleHelloWorld { + IHelloWorld helloWorld = IHelloWorld(HELLO_WORLD_ADDRESS); + + function sayHello() public view returns (string memory) { + return helloWorld.sayHello(); + } + + function setGreeting(string calldata greeting) public { + helloWorld.setGreeting(greeting); + } +} diff --git a/contracts/contracts/interfaces/IHelloWorld.sol b/contracts/contracts/interfaces/IHelloWorld.sol new file mode 100644 index 0000000000..8da5705f82 --- /dev/null +++ b/contracts/contracts/interfaces/IHelloWorld.sol @@ -0,0 +1,16 @@ +// (c) 2022-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// SPDX-License-Identifier: MIT + +pragma solidity >=0.8.0; +import "./IAllowList.sol"; + +interface IHelloWorld is IAllowList { + event GreetingChanged(address indexed sender, string oldGreeting, string newGreeting); + // sayHello returns the stored greeting string + function sayHello() external view returns (string calldata result); + + // setGreeting stores the greeting string + function setGreeting(string calldata response) external; +} diff --git a/contracts/contracts/test/ExampleHelloWorldTest.sol b/contracts/contracts/test/ExampleHelloWorldTest.sol new file mode 100644 index 0000000000..9dc426d8b5 --- /dev/null +++ b/contracts/contracts/test/ExampleHelloWorldTest.sol @@ -0,0 +1,42 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "../ExampleHelloWorld.sol"; +import "../interfaces/IHelloWorld.sol"; +import "./AllowListTest.sol"; + +contract ExampleHelloWorldTest is AllowListTest { + IHelloWorld helloWorld = IHelloWorld(HELLO_WORLD_ADDRESS); + + function step_getDefaultHelloWorld() public { + ExampleHelloWorld example = new ExampleHelloWorld(); + address exampleAddress = address(example); + + assertRole(helloWorld.readAllowList(exampleAddress), AllowList.Role.None); + assertEq(example.sayHello(), "Hello World!"); + } + + function step_doesNotSetGreetingBeforeEnabled() public { + ExampleHelloWorld example = new ExampleHelloWorld(); + address exampleAddress = address(example); + + assertRole(helloWorld.readAllowList(exampleAddress), AllowList.Role.None); + + try example.setGreeting("testing") { + assertTrue(false, "setGreeting should fail"); + } catch {} // TODO should match on an error to make sure that this is failing in the way that's expected + } + + function step_setAndGetGreeting() public { + ExampleHelloWorld example = new ExampleHelloWorld(); + address exampleAddress = address(example); + + assertRole(helloWorld.readAllowList(exampleAddress), AllowList.Role.None); + helloWorld.setEnabled(exampleAddress); + assertRole(helloWorld.readAllowList(exampleAddress), AllowList.Role.Enabled); + + string memory greeting = "testgreeting"; + example.setGreeting(greeting); + assertEq(example.sayHello(), greeting); + } +} diff --git a/contracts/test/hello_world.ts b/contracts/test/hello_world.ts new file mode 100644 index 0000000000..a4ef923613 --- /dev/null +++ b/contracts/test/hello_world.ts @@ -0,0 +1,63 @@ +// (c) 2019-2022, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +import { expect } from "chai" +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { Contract } from "ethers" +import { ethers } from "hardhat" +import { test } from "./utils" + +// make sure this is always an admin for hello world precompile +const ADMIN_ADDRESS = "0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC" +const HELLO_WORLD_ADDRESS = "0x0300000000000000000000000000000000000000" + +describe("ExampleHelloWorldTest", function () { + this.timeout("30s") + + beforeEach('Setup DS-Test contract', async function () { + const signer = await ethers.getSigner(ADMIN_ADDRESS) + const helloWorldPromise = ethers.getContractAt("IHelloWorld", HELLO_WORLD_ADDRESS, signer) + + return ethers.getContractFactory("ExampleHelloWorldTest", { signer }) + .then(factory => factory.deploy()) + .then(contract => { + this.testContract = contract + return contract.deployed().then(() => contract) + }) + .then(() => Promise.all([helloWorldPromise])) + .then(([helloWorld]) => helloWorld.setAdmin(this.testContract.address)) + .then(tx => tx.wait()) + }) + + test("should gets default hello world", ["step_getDefaultHelloWorld"]) + + test("should not set greeting before enabled", "step_doesNotSetGreetingBeforeEnabled") + + test("should set and get greeting with enabled account", "step_setAndGetGreeting") +}); + +describe("IHelloWorld events", function () { + let owner: SignerWithAddress + let contract: Contract + let defaultGreeting = "Hello, World!" + before(async function () { + owner = await ethers.getSigner(ADMIN_ADDRESS); + contract = await ethers.getContractAt("IHelloWorld", HELLO_WORLD_ADDRESS, owner) + + // reset greeting + let tx = await contract.setGreeting(defaultGreeting) + await tx.wait() + }); + + it("should emit GreetingChanged event", async function () { + let newGreeting = "helloprecompile" + await expect(contract.setGreeting(newGreeting) + ) + .to.emit(contract, "GreetingChanged").withArgs(owner.address, + // old greeting + defaultGreeting, + // new greeting + newGreeting + ) + }) +}) diff --git a/precompile/contracts/helloworld/README.md b/precompile/contracts/helloworld/README.md new file mode 100644 index 0000000000..d81e622b2b --- /dev/null +++ b/precompile/contracts/helloworld/README.md @@ -0,0 +1,23 @@ +There are some must-be-done changes waiting in the generated file. Each area requiring you to add your code is marked with CUSTOM CODE to make them easy to find and modify. +Additionally there are other files you need to edit to activate your precompile. +These areas are highlighted with comments "ADD YOUR PRECOMPILE HERE". +For testing take a look at other precompile tests in contract_test.go and config_test.go in other precompile folders. +See the tutorial in for more information about precompile development. + +General guidelines for precompile development: +1- Set a suitable config key in generated module.go. E.g: "yourPrecompileConfig" +2- Read the comment and set a suitable contract address in generated module.go. E.g: +ContractAddress = common.HexToAddress("ASUITABLEHEXADDRESS") +3- It is recommended to only modify code in the highlighted areas marked with "CUSTOM CODE STARTS HERE". Typically, custom codes are required in only those areas. +Modifying code outside of these areas should be done with caution and with a deep understanding of how these changes may impact the EVM. +4- Set gas costs in generated contract.go +5- Force import your precompile package in precompile/registry/registry.go +6- Add your config unit tests under generated package config_test.go +7- Add your contract unit tests under generated package contract_test.go +8- Additionally you can add a full-fledged VM test for your precompile under plugin/vm/vm_test.go. See existing precompile tests for examples. +9- Add your solidity interface and test contract to contracts/contracts +10- Write solidity contract tests for your precompile in contracts/contracts/test +11- Write TypeScript DS-Test counterparts for your solidity tests in contracts/test +12- Create your genesis with your precompile enabled in tests/precompile/genesis/ +13- Create e2e test for your solidity test in tests/precompile/solidity/suites.go +14- Run your e2e precompile Solidity tests with './scripts/run_ginkgo.sh` diff --git a/precompile/contracts/helloworld/config.go b/precompile/contracts/helloworld/config.go new file mode 100644 index 0000000000..60fb1c5443 --- /dev/null +++ b/precompile/contracts/helloworld/config.go @@ -0,0 +1,77 @@ +// Code generated +// This file is a generated precompile contract config with stubbed abstract functions. +// The file is generated by a template. Please inspect every code and comment in this file before use. + +package helloworld + +import ( + "github.com/ava-labs/subnet-evm/precompile/allowlist" + "github.com/ava-labs/subnet-evm/precompile/precompileconfig" + + "github.com/ethereum/go-ethereum/common" +) + +var _ precompileconfig.Config = &Config{} + +// Config implements the precompileconfig.Config interface and +// adds specific configuration for HelloWorld. +type Config struct { + allowlist.AllowListConfig + precompileconfig.Upgrade + // CUSTOM CODE STARTS HERE + // Add your own custom fields for Config here +} + +// NewConfig returns a config for a network upgrade at [blockTimestamp] that enables +// HelloWorld with the given [admins], [enableds] and [managers] members of the allowlist . +func NewConfig(blockTimestamp *uint64, admins []common.Address, enableds []common.Address, managers []common.Address) *Config { + return &Config{ + AllowListConfig: allowlist.AllowListConfig{ + AdminAddresses: admins, + EnabledAddresses: enableds, + ManagerAddresses: managers, + }, + Upgrade: precompileconfig.Upgrade{BlockTimestamp: blockTimestamp}, + } +} + +// NewDisableConfig returns config for a network upgrade at [blockTimestamp] +// that disables HelloWorld. +func NewDisableConfig(blockTimestamp *uint64) *Config { + return &Config{ + Upgrade: precompileconfig.Upgrade{ + BlockTimestamp: blockTimestamp, + Disable: true, + }, + } +} + +// Key returns the key for the HelloWorld precompileconfig. +// This should be the same key as used in the precompile module. +func (*Config) Key() string { return ConfigKey } + +// Verify tries to verify Config and returns an error accordingly. +func (c *Config) Verify(chainConfig precompileconfig.ChainConfig) error { + // Verify AllowList first + if err := c.AllowListConfig.Verify(chainConfig, c.Upgrade); err != nil { + return err + } + // CUSTOM CODE STARTS HERE + // Add your own custom verify code for Config here + // and return an error accordingly + return nil +} + +// Equal returns true if [s] is a [*Config] and it has been configured identical to [c]. +func (c *Config) Equal(s precompileconfig.Config) bool { + // typecast before comparison + other, ok := (s).(*Config) + if !ok { + return false + } + // CUSTOM CODE STARTS HERE + // modify this boolean accordingly with your custom Config, to check if [other] and the current [c] are equal + // if Config contains only Upgrade and AllowListConfig you can skip modifying it. + equals := c.Upgrade.Equal(&other.Upgrade) && c.AllowListConfig.Equal(&other.AllowListConfig) + return equals +} diff --git a/precompile/contracts/helloworld/config_test.go b/precompile/contracts/helloworld/config_test.go new file mode 100644 index 0000000000..f07cb75798 --- /dev/null +++ b/precompile/contracts/helloworld/config_test.go @@ -0,0 +1,88 @@ +// Code generated +// This file is a generated precompile config test with the skeleton of test functions. +// The file is generated by a template. Please inspect every code and comment in this file before use. + +package helloworld + +import ( + "testing" + + "github.com/ava-labs/subnet-evm/precompile/allowlist" + "github.com/ava-labs/subnet-evm/precompile/precompileconfig" + "github.com/ava-labs/subnet-evm/precompile/testutils" + "github.com/ava-labs/subnet-evm/utils" + + "github.com/ethereum/go-ethereum/common" + "go.uber.org/mock/gomock" +) + +// TestVerify tests the verification of Config. +func TestVerify(t *testing.T) { + admins := []common.Address{allowlist.TestAdminAddr} + enableds := []common.Address{allowlist.TestEnabledAddr} + managers := []common.Address{allowlist.TestManagerAddr} + tests := map[string]testutils.ConfigVerifyTest{ + "valid config": { + Config: NewConfig(utils.NewUint64(3), admins, enableds, managers), + ChainConfig: func() precompileconfig.ChainConfig { + config := precompileconfig.NewMockChainConfig(gomock.NewController(t)) + config.EXPECT().IsDurango(gomock.Any()).Return(true).AnyTimes() + return config + }(), + ExpectedError: "", + }, + // CUSTOM CODE STARTS HERE + // Add your own Verify tests here, e.g.: + // "your custom test name": { + // Config: NewConfig(utils.NewUint64(3), admins, enableds, managers), + // ExpectedError: ErrYourCustomError.Error(), + // }, + "invalid allow list config in hello world allowlist": { + Config: NewConfig(utils.NewUint64(3), admins, admins, nil), + ExpectedError: "cannot set address", + }, + } + // Verify the precompile with the allowlist. + // This adds allowlist verify tests to your custom tests + // and runs them all together. + // Even if you don't add any custom tests, keep this. This will still + // run the default allowlist verify tests. + allowlist.VerifyPrecompileWithAllowListTests(t, Module, tests) +} + +// TestEqual tests the equality of Config with other precompile configs. +func TestEqual(t *testing.T) { + admins := []common.Address{allowlist.TestAdminAddr} + enableds := []common.Address{allowlist.TestEnabledAddr} + managers := []common.Address{allowlist.TestManagerAddr} + tests := map[string]testutils.ConfigEqualTest{ + "non-nil config and nil other": { + Config: NewConfig(utils.NewUint64(3), admins, enableds, managers), + Other: nil, + Expected: false, + }, + "different type": { + Config: NewConfig(utils.NewUint64(3), admins, enableds, managers), + Other: precompileconfig.NewMockConfig(gomock.NewController(t)), + Expected: false, + }, + "different timestamp": { + Config: NewConfig(utils.NewUint64(3), admins, enableds, managers), + Other: NewConfig(utils.NewUint64(4), admins, enableds, managers), + Expected: false, + }, + "same config": { + Config: NewConfig(utils.NewUint64(3), admins, enableds, managers), + Other: NewConfig(utils.NewUint64(3), admins, enableds, managers), + Expected: true, + }, + // CUSTOM CODE STARTS HERE + // Add your own Equal tests here + } + // Run allow list equal tests. + // This adds allowlist equal tests to your custom tests + // and runs them all together. + // Even if you don't add any custom tests, keep this. This will still + // run the default allowlist equal tests. + allowlist.EqualPrecompileWithAllowListTests(t, Module, tests) +} diff --git a/precompile/contracts/helloworld/contract.abi b/precompile/contracts/helloworld/contract.abi new file mode 100644 index 0000000000..94b0074427 --- /dev/null +++ b/precompile/contracts/helloworld/contract.abi @@ -0,0 +1 @@ +[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":false,"internalType":"string","name":"oldGreeting","type":"string"},{"indexed":false,"internalType":"string","name":"newGreeting","type":"string"}],"name":"GreetingChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"role","type":"uint256"},{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":false,"internalType":"uint256","name":"oldRole","type":"uint256"}],"name":"RoleSet","type":"event"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"readAllowList","outputs":[{"internalType":"uint256","name":"role","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"sayHello","outputs":[{"internalType":"string","name":"result","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"setAdmin","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"setEnabled","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"response","type":"string"}],"name":"setGreeting","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"setManager","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"setNone","outputs":[],"stateMutability":"nonpayable","type":"function"}] \ No newline at end of file diff --git a/precompile/contracts/helloworld/contract.go b/precompile/contracts/helloworld/contract.go new file mode 100644 index 0000000000..758695ce35 --- /dev/null +++ b/precompile/contracts/helloworld/contract.go @@ -0,0 +1,253 @@ +// Code generated +// This file is a generated precompile contract config with stubbed abstract functions. +// The file is generated by a template. Please inspect every code and comment in this file before use. + +package helloworld + +import ( + "errors" + "fmt" + + "github.com/ava-labs/subnet-evm/accounts/abi" + "github.com/ava-labs/subnet-evm/precompile/allowlist" + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ava-labs/subnet-evm/vmerrs" + + _ "embed" + + "github.com/ethereum/go-ethereum/common" +) + +const ( + // Gas costs for each function. These are set to 1 by default. + // You should set a gas cost for each function in your contract. + // Generally, you should not set gas costs very low as this may cause your network to be vulnerable to DoS attacks. + // There are some predefined gas costs in contract/utils.go that you can use. + // This contract also uses AllowList precompile. + // You should also increase gas costs of functions that read from AllowList storage. + SayHelloGasCost uint64 = contract.ReadGasCostPerSlot + SetGreetingGasCost uint64 = contract.WriteGasCostPerSlot + allowlist.ReadAllowListGasCost +) + +// Singleton StatefulPrecompiledContract and signatures. +var ( + ErrCannotSetGreeting = errors.New("non-enabled cannot call setGreeting") + ErrInputExceedsLimit = errors.New("input string is longer than 32 bytes") + + // HelloWorldRawABI contains the raw ABI of HelloWorld contract. + //go:embed contract.abi + HelloWorldRawABI string + + HelloWorldABI = contract.ParseABI(HelloWorldRawABI) + + HelloWorldPrecompile = createHelloWorldPrecompile() + + storageKeyHash = common.BytesToHash([]byte("storageKey")) +) + +// GetHelloWorldAllowListStatus returns the role of [address] for the HelloWorld list. +func GetHelloWorldAllowListStatus(stateDB contract.StateDB, address common.Address) allowlist.Role { + return allowlist.GetAllowListStatus(stateDB, ContractAddress, address) +} + +// SetHelloWorldAllowListStatus sets the permissions of [address] to [role] for the +// HelloWorld list. Assumes [role] has already been verified as valid. +// This stores the [role] in the contract storage with address [ContractAddress] +// and [address] hash. It means that any reusage of the [address] key for different value +// conflicts with the same slot [role] is stored. +// Precompile implementations must use a different key than [address] for their storage. +func SetHelloWorldAllowListStatus(stateDB contract.StateDB, address common.Address, role allowlist.Role) { + allowlist.SetAllowListRole(stateDB, ContractAddress, address, role) +} + +// PackSayHello packs the include selector (first 4 func signature bytes). +// This function is mostly used for tests. +func PackSayHello() ([]byte, error) { + return HelloWorldABI.Pack("sayHello") +} + +// PackSayHelloOutput attempts to pack given result of type string +// to conform the ABI outputs. +func PackSayHelloOutput(result string) ([]byte, error) { + return HelloWorldABI.PackOutput("sayHello", result) +} + +// UnpackSayHelloOutput attempts to unpack given [output] into the string type output +// assumes that [output] does not include selector (omits first 4 func signature bytes) +func UnpackSayHelloOutput(output []byte) (string, error) { + res, err := HelloWorldABI.Unpack("sayHello", output) + if err != nil { + return "", err + } + unpacked := *abi.ConvertType(res[0], new(string)).(*string) + return unpacked, nil +} + +// GetGreeting returns the value of the storage key "storageKey" in the contract storage, +// with leading zeroes trimmed. +// This function is mostly used for tests. +func GetGreeting(stateDB contract.StateDB) string { + // Get the value set at recipient + value := stateDB.GetState(ContractAddress, storageKeyHash) + return string(common.TrimLeftZeroes(value.Bytes())) +} + +// sayHello is the reader fucntion that returns the value of greeting stored in the contract storage. +func sayHello(accessibleState contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + if remainingGas, err = contract.DeductGas(suppliedGas, SayHelloGasCost); err != nil { + return nil, 0, err + } + // no input provided for this function + + // CUSTOM CODE STARTS HERE + + // Get the current state + currentState := accessibleState.GetStateDB() + // Get the value set at recipient + value := GetGreeting(currentState) + packedOutput, err := PackSayHelloOutput(value) + if err != nil { + return nil, remainingGas, err + } + + // Return the packed output and the remaining gas + return packedOutput, remainingGas, nil +} + +// UnpackSetGreetingInput attempts to unpack [input] into the string type argument +// assumes that [input] does not include selector (omits first 4 func signature bytes) +// if [useStrictMode] is true, it will return an error if the length of [input] is not [common.HashLength] +func UnpackSetGreetingInput(input []byte, useStrictMode bool) (string, error) { + // Initially we had this check to ensure that the input was the correct length. + // However solidity does not always pack the input to the correct length, and allows + // for extra padding bytes to be added to the end of the input. Therefore, we have removed + // this check with the Durango. We still need to keep this check for backwards compatibility. + if useStrictMode && len(input) > common.HashLength { + return "", ErrInputExceedsLimit + } + res, err := HelloWorldABI.UnpackInput("setGreeting", input, useStrictMode) + if err != nil { + return "", err + } + unpacked := *abi.ConvertType(res[0], new(string)).(*string) + return unpacked, nil +} + +// PackSetGreeting packs [response] of type string into the appropriate arguments for setGreeting. +// the packed bytes include selector (first 4 func signature bytes). +// This function is mostly used for tests. +func PackSetGreeting(response string) ([]byte, error) { + return HelloWorldABI.Pack("setGreeting", response) +} + +// StoreGreeting sets the value of the storage key "storageKey" in the contract storage. +func StoreGreeting(stateDB contract.StateDB, input string) { + inputPadded := common.LeftPadBytes([]byte(input), common.HashLength) + inputHash := common.BytesToHash(inputPadded) + + stateDB.SetState(ContractAddress, storageKeyHash, inputHash) +} + +// setGreeting is a state-changer function that sets the value of the greeting in the contract storage. +func setGreeting(accessibleState contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + if remainingGas, err = contract.DeductGas(suppliedGas, SetGreetingGasCost); err != nil { + return nil, 0, err + } + if readOnly { + return nil, remainingGas, vmerrs.ErrWriteProtection + } + // do not use strict mode after Durango + useStrictMode := !contract.IsDurangoActivated(accessibleState) + // attempts to unpack [input] into the arguments to the SetGreetingInput. + // Assumes that [input] does not include selector + // You can use unpacked [inputStruct] variable in your code + inputStruct, err := UnpackSetGreetingInput(input, useStrictMode) + if err != nil { + return nil, remainingGas, err + } + + // Allow list is enabled and SetGreeting is a state-changer function. + // This part of the code restricts the function to be called only by enabled/admin addresses in the allow list. + // You can modify/delete this code if you don't want this function to be restricted by the allow list. + stateDB := accessibleState.GetStateDB() + // Verify that the caller is in the allow list and therefore has the right to call this function. + callerStatus := allowlist.GetAllowListStatus(stateDB, ContractAddress, caller) + if !callerStatus.IsEnabled() { + return nil, remainingGas, fmt.Errorf("%w: %s", ErrCannotSetGreeting, caller) + } + // allow list code ends here. + + // CUSTOM CODE STARTS HERE + // With Durango, you can emit an event in your state-changing precompile functions. + // Note: If you have been using the precompile before Durango, you should activate it only after Durango. + // Activating this code before Durango will result in a consensus failure. + // If this is a new precompile and never deployed before Durango, you can activate it immediately by removing + // the if condition. + // This example assumes that the HelloWorld precompile contract has been deployed before Durango. + if contract.IsDurangoActivated(accessibleState) { + // We will first read the old greeting. So we should charge the gas for reading the storage. + if remainingGas, err = contract.DeductGas(remainingGas, contract.ReadGasCostPerSlot); err != nil { + return nil, 0, err + } + oldGreeting := GetGreeting(stateDB) + + eventData := GreetingChangedEventData{ + OldGreeting: oldGreeting, + NewGreeting: inputStruct, + } + topics, data, err := PackGreetingChangedEvent(caller, eventData) + if err != nil { + return nil, remainingGas, err + } + // Charge the gas for emitting the event. + eventGasCost := GetGreetingChangedEventGasCost(eventData) + if remainingGas, err = contract.DeductGas(remainingGas, eventGasCost); err != nil { + return nil, 0, err + } + + // Emit the event + stateDB.AddLog( + ContractAddress, + topics, + data, + accessibleState.GetBlockContext().Number().Uint64(), + ) + } + + // setGreeting is the execution function + // "SetGreeting(name string)" and sets the storageKey + // in the string returned by hello world + StoreGreeting(stateDB, inputStruct) + + // This function does not return an output, leave this one as is + packedOutput := []byte{} + + // Return the packed output and the remaining gas + return packedOutput, remainingGas, nil +} + +// createHelloWorldPrecompile returns a StatefulPrecompiledContract with getters and setters for the precompile. +// Access to the getters/setters is controlled by an allow list for ContractAddress. +func createHelloWorldPrecompile() contract.StatefulPrecompiledContract { + var functions []*contract.StatefulPrecompileFunction + functions = append(functions, allowlist.CreateAllowListFunctions(ContractAddress)...) + + abiFunctionMap := map[string]contract.RunStatefulPrecompileFunc{ + "sayHello": sayHello, + "setGreeting": setGreeting, + } + + for name, function := range abiFunctionMap { + method, ok := HelloWorldABI.Methods[name] + if !ok { + panic(fmt.Errorf("given method (%s) does not exist in the ABI", name)) + } + functions = append(functions, contract.NewStatefulPrecompileFunction(method.ID, function)) + } + // Construct the contract with no fallback function. + statefulContract, err := contract.NewStatefulPrecompileContract(nil, functions) + if err != nil { + panic(err) + } + return statefulContract +} diff --git a/precompile/contracts/helloworld/contract_test.go b/precompile/contracts/helloworld/contract_test.go new file mode 100644 index 0000000000..0e7d377a1f --- /dev/null +++ b/precompile/contracts/helloworld/contract_test.go @@ -0,0 +1,379 @@ +// Code generated +// This file is a generated precompile contract test with the skeleton of test functions. +// The file is generated by a template. Please inspect every code and comment in this file before use. + +package helloworld + +import ( + "testing" + + "github.com/ava-labs/subnet-evm/core/state" + "github.com/ava-labs/subnet-evm/precompile/allowlist" + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ava-labs/subnet-evm/precompile/precompileconfig" + "github.com/ava-labs/subnet-evm/precompile/testutils" + "github.com/ava-labs/subnet-evm/utils" + "github.com/ava-labs/subnet-evm/vmerrs" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +// These tests are run against the precompile contract directly with +// the given input and expected output. They're just a guide to +// help you write your own tests. These tests are for general cases like +// allowlist, readOnly behaviour, and gas cost. You should write your own +// tests for specific cases. +const testGreeting = "test" +const longString = "a very long string that is longer than 32 bytes and will cause an error" + +var ( + tests = map[string]testutils.PrecompileTest{ + "calling sayHello from NoRole should succeed": { + Caller: allowlist.TestNoRoleAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t testing.TB) []byte { + input, err := PackSayHello() + require.NoError(t, err) + return input + }, + // This test is for a successful call. You can set the expected output here. + // CUSTOM CODE STARTS HERE + ExpectedRes: func() []byte { + // by default we don't Configure initial state for + // the module since Config is empty. + // This means we don't apply default greeting to the state. + res, err := PackSayHelloOutput("") + if err != nil { + panic(err) + } + return res + }(), + SuppliedGas: SayHelloGasCost, + ReadOnly: false, + ExpectedErr: "", + }, + "calling sayHello from Enabled should succeed": { + Caller: allowlist.TestEnabledAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t testing.TB) []byte { + input, err := PackSayHello() + require.NoError(t, err) + return input + }, + // This test is for a successful call. You can set the expected output here. + // CUSTOM CODE STARTS HERE + ExpectedRes: func() []byte { + // by default we don't Configure initial state for + // the module since Config is empty. + // This means we don't apply default greeting to the state. + res, err := PackSayHelloOutput("") + if err != nil { + panic(err) + } + return res + }(), + SuppliedGas: SayHelloGasCost, + ReadOnly: false, + ExpectedErr: "", + }, + "calling sayHello from Manager should succeed": { + Caller: allowlist.TestManagerAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t testing.TB) []byte { + input, err := PackSayHello() + require.NoError(t, err) + return input + }, + // This test is for a successful call. You can set the expected output here. + // CUSTOM CODE STARTS HERE + ExpectedRes: func() []byte { + // by default we don't Configure initial state for + // the module since Config is empty. + // This means we don't apply default greeting to the state. + res, err := PackSayHelloOutput("") + if err != nil { + panic(err) + } + return res + }(), + SuppliedGas: SayHelloGasCost, + ReadOnly: false, + ExpectedErr: "", + }, + "calling sayHello from Admin should succeed": { + Caller: allowlist.TestAdminAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t testing.TB) []byte { + input, err := PackSayHello() + require.NoError(t, err) + return input + }, + // This test is for a successful call. You can set the expected output here. + // CUSTOM CODE STARTS HERE + ExpectedRes: func() []byte { + // by default we don't Configure initial state for + // the module since Config is empty. + // This means we don't apply default greeting to the state. + res, err := PackSayHelloOutput("") + if err != nil { + panic(err) + } + return res + }(), + SuppliedGas: SayHelloGasCost, + ReadOnly: false, + ExpectedErr: "", + }, + "calling sayHello from NoRole with a config should return default greeting": { + Caller: allowlist.TestNoRoleAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + Config: NewConfig(utils.NewUint64(0), nil, nil, nil), + InputFn: func(t testing.TB) []byte { + input, err := PackSayHello() + require.NoError(t, err) + return input + }, + // This test is for a successful call. You can set the expected output here. + // CUSTOM CODE STARTS HERE + ExpectedRes: func() []byte { + res, err := PackSayHelloOutput(defaultGreeting) + if err != nil { + panic(err) + } + return res + }(), + SuppliedGas: SayHelloGasCost, + ReadOnly: false, + ExpectedErr: "", + }, + "insufficient gas for sayHello should fail": { + Caller: common.Address{1}, + InputFn: func(t testing.TB) []byte { + input, err := PackSayHello() + require.NoError(t, err) + return input + }, + SuppliedGas: SayHelloGasCost - 1, + ReadOnly: false, + ExpectedErr: vmerrs.ErrOutOfGas.Error(), + }, + "calling setGreeting from NoRole should fail": { + Caller: allowlist.TestNoRoleAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t testing.TB) []byte { + // CUSTOM CODE STARTS HERE + // set test input to a value here + input, err := PackSetGreeting(testGreeting) + require.NoError(t, err) + return input + }, + SuppliedGas: SetGreetingGasCost, + ReadOnly: false, + ExpectedErr: ErrCannotSetGreeting.Error(), + }, + "calling setGreeting from Enabled should succeed": { + Caller: allowlist.TestEnabledAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t testing.TB) []byte { + // CUSTOM CODE STARTS HERE + // set test input to a value here + input, err := PackSetGreeting(testGreeting) + require.NoError(t, err) + return input + }, + // This test is for a successful call. You can set the expected output here. + // CUSTOM CODE STARTS HERE + ExpectedRes: []byte{}, + SuppliedGas: SetGreetingGasCost + contract.ReadGasCostPerSlot + GetGreetingChangedEventGasCost(GreetingChangedEventData{ + OldGreeting: "", + NewGreeting: testGreeting, + }), + ReadOnly: false, + ExpectedErr: "", + AfterHook: func(t testing.TB, state contract.StateDB) { + greeting := GetGreeting(state) + require.Equal(t, greeting, testGreeting) + }, + }, + "calling setGreeting from Manager should succeed": { + Caller: allowlist.TestManagerAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t testing.TB) []byte { + // CUSTOM CODE STARTS HERE + // set test input to a value here + input, err := PackSetGreeting(testGreeting) + require.NoError(t, err) + return input + }, + // This test is for a successful call. You can set the expected output here. + // CUSTOM CODE STARTS HERE + ExpectedRes: []byte{}, + SuppliedGas: SetGreetingGasCost + contract.ReadGasCostPerSlot + GetGreetingChangedEventGasCost(GreetingChangedEventData{ + OldGreeting: "", + NewGreeting: testGreeting, + }), + ReadOnly: false, + ExpectedErr: "", + AfterHook: func(t testing.TB, state contract.StateDB) { + greeting := GetGreeting(state) + require.Equal(t, greeting, testGreeting) + }, + }, + "calling setGreeting from Admin should succeed": { + Caller: allowlist.TestAdminAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t testing.TB) []byte { + // CUSTOM CODE STARTS HERE + // set test input to a value here + input, err := PackSetGreeting(testGreeting) + require.NoError(t, err) + return input + }, + // This test is for a successful call. You can set the expected output here. + // CUSTOM CODE STARTS HERE + ExpectedRes: []byte{}, + SuppliedGas: SetGreetingGasCost + contract.ReadGasCostPerSlot + GetGreetingChangedEventGasCost(GreetingChangedEventData{ + OldGreeting: "", + NewGreeting: testGreeting, + }), + ReadOnly: false, + ExpectedErr: "", + AfterHook: func(t testing.TB, state contract.StateDB) { + greeting := GetGreeting(state) + require.Equal(t, greeting, testGreeting) + }, + }, + "readOnly setGreeting should fail": { + Caller: common.Address{1}, + InputFn: func(t testing.TB) []byte { + // CUSTOM CODE STARTS HERE + // set test input to a value here + var testInput string + input, err := PackSetGreeting(testInput) + require.NoError(t, err) + return input + }, + SuppliedGas: SetGreetingGasCost, + ReadOnly: true, + ExpectedErr: vmerrs.ErrWriteProtection.Error(), + }, + "insufficient gas for setGreeting should fail": { + Caller: allowlist.TestEnabledAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t testing.TB) []byte { + // CUSTOM CODE STARTS HERE + // set test input to a value here + input, err := PackSetGreeting(testGreeting) + require.NoError(t, err) + return input + }, + SuppliedGas: SetGreetingGasCost + contract.ReadGasCostPerSlot + GetGreetingChangedEventGasCost(GreetingChangedEventData{ + OldGreeting: "", + NewGreeting: testGreeting, + }) - 1, + ReadOnly: false, + ExpectedErr: vmerrs.ErrOutOfGas.Error(), + }, + // more custom tests + "store greeting then say hello from non-enabled address": { + Caller: allowlist.TestNoRoleAddr, + BeforeHook: func(t testing.TB, state contract.StateDB) { + allowlist.SetDefaultRoles(Module.Address)(t, state) + StoreGreeting(state, testGreeting) + }, + InputFn: func(t testing.TB) []byte { + input, err := PackSayHello() + require.NoError(t, err) + return input + }, + SuppliedGas: SayHelloGasCost, + ReadOnly: true, + ExpectedRes: func() []byte { + res, err := PackSayHelloOutput(testGreeting) + if err != nil { + panic(err) + } + return res + }(), + }, + "set a very long greeting from enabled address before Durango": { + Caller: allowlist.TestEnabledAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + // By default Durango is enabled in the tests. + ChainConfigFn: func(ctrl *gomock.Controller) precompileconfig.ChainConfig { + config := precompileconfig.NewMockChainConfig(ctrl) + config.EXPECT().IsDurango(gomock.Any()).Return(false).AnyTimes() + return config + }, + InputFn: func(t testing.TB) []byte { + longString := "a very long string that is longer than 32 bytes and will cause an error" + input, err := PackSetGreeting(longString) + require.NoError(t, err) + + return input + }, + SuppliedGas: SetGreetingGasCost, + ReadOnly: false, + ExpectedErr: ErrInputExceedsLimit.Error(), + }, + "set a very long greeting from enabled address after Durango": { + Caller: allowlist.TestEnabledAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t testing.TB) []byte { + input, err := PackSetGreeting(longString) + require.NoError(t, err) + + return input + }, + SuppliedGas: SetGreetingGasCost + contract.ReadGasCostPerSlot + GetGreetingChangedEventGasCost(GreetingChangedEventData{ + OldGreeting: "", + NewGreeting: longString, + }), + ReadOnly: false, + ExpectedErr: "", + ExpectedRes: []byte{}, + }, + } +) + +// TestHelloWorldRun tests the Run function of the precompile contract. +func TestHelloWorldRun(t *testing.T) { + // Run tests with allowlist tests. + // This adds allowlist run tests to your custom tests + // and runs them all together. + // Even if you don't add any custom tests, keep this. This will still + // run the default allowlist tests. + allowlist.RunPrecompileWithAllowListTests(t, Module, state.NewTestStateDB, tests) +} + +// TestPackUnpackGreetingChangedEventData tests the Pack/UnpackGreetingChangedEventData. +func TestPackUnpackGreetingChangedEventData(t *testing.T) { + // CUSTOM CODE STARTS HERE + // set test inputs with proper values here + var senderInput common.Address = common.Address{} + + dataInput := GreetingChangedEventData{ + OldGreeting: "", + NewGreeting: "", + } + + _, data, err := PackGreetingChangedEvent( + senderInput, + dataInput, + ) + require.NoError(t, err) + + unpacked, err := UnpackGreetingChangedEventData(data) + require.NoError(t, err) + require.Equal(t, dataInput, unpacked) +} + +func BenchmarkHelloWorld(b *testing.B) { + // Benchmark tests with allowlist tests. + // This adds allowlist run tests to your custom tests + // and benchmarks them all together. + // Even if you don't add any custom tests, keep this. This will still + // run the default allowlist tests. + allowlist.BenchPrecompileWithAllowList(b, Module, state.NewTestStateDB, tests) +} diff --git a/precompile/contracts/helloworld/event.go b/precompile/contracts/helloworld/event.go new file mode 100644 index 0000000000..b0e6cdb6f6 --- /dev/null +++ b/precompile/contracts/helloworld/event.go @@ -0,0 +1,90 @@ +// Code generated +// This file is a generated precompile contract config with stubbed abstract functions. +// The file is generated by a template. Please inspect every code and comment in this file before use. + +package helloworld + +import ( + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ethereum/go-ethereum/common" +) + +/* NOTE: Events can only be emitted in state-changing functions. So you cannot use events in read-only (view) functions. +Events are generally emitted at the end of a state-changing function with AddLog method of the StateDB. The AddLog method takes 4 arguments: + 1. Address of the contract that emitted the event. + 2. Topic hashes of the event. + 3. Encoded non-indexed data of the event. + 4. Block number at which the event was emitted. +The first argument is the address of the contract that emitted the event. +Topics can be at most 4 elements, the first topic is the hash of the event signature and the rest are the indexed event arguments. There can be at most 3 indexed arguments. +Topics cannot be fully unpacked into their original values since they're 32-bytes hashes. +The non-indexed arguments are encoded using the ABI encoding scheme. The non-indexed arguments can be unpacked into their original values. +Before packing the event, you need to calculate the gas cost of the event. The gas cost of an event is the base gas cost + the gas cost of the topics + the gas cost of the non-indexed data. +See Get{EvetName}EventGasCost functions for more details. +You can use the following code to emit an event in your state-changing precompile functions (generated packer might be different)): +topics, data, err := PackMyEvent( + topic1, + topic2, + data1, + data2, +) +if err != nil { + return nil, remainingGas, err +} +accessibleState.GetStateDB().AddLog( + ContractAddress, + topics, + data, + accessibleState.GetBlockContext().Number().Uint64(), +) +*/ + +// HelloWorldGreetingChanged represents a GreetingChanged non-indexed event data raised by the HelloWorld contract. +type GreetingChangedEventData struct { + OldGreeting string + NewGreeting string +} + +// GetGreetingChangedEventGasCost returns the gas cost of the event. +// The gas cost of an event is the base gas cost + the gas cost of the topics + the gas cost of the non-indexed data. +// The base gas cost and the gas cost of per topics are fixed and can be found in the contract package. +// The gas cost of the non-indexed data depends on the data type and the data size. +func GetGreetingChangedEventGasCost(data GreetingChangedEventData) uint64 { + gas := contract.LogGas // base gas cost + + // Add topics gas cost (2 topics) + // Topics always include the signature hash of the event. The rest are the indexed event arguments. + gas += contract.LogTopicGas * 2 + + // CUSTOM CODE STARTS HERE + // TODO: calculate gas cost for packing the data.oldGreeting according to the type. + // Keep in mind that the data here will be encoded using the ABI encoding scheme. + // So the computation cost might change according to the data type + data size and should be charged accordingly. + // i.e gas += LogDataGas * uint64(len(data.oldGreeting)) + gas += contract.LogDataGas * uint64(len(data.OldGreeting)) // * ... + // CUSTOM CODE ENDS HERE + // CUSTOM CODE STARTS HERE + // TODO: calculate gas cost for packing the data.newGreeting according to the type. + // Keep in mind that the data here will be encoded using the ABI encoding scheme. + // So the computation cost might change according to the data type + data size and should be charged accordingly. + // i.e gas += LogDataGas * uint64(len(data.newGreeting)) + gas += contract.LogDataGas * uint64(len(data.NewGreeting)) // * ... + // CUSTOM CODE ENDS HERE + + // CUSTOM CODE STARTS HERE + // TODO: do any additional gas cost calculation here (only if needed) + return gas +} + +// PackGreetingChangedEvent packs the event into the appropriate arguments for GreetingChanged. +// It returns topic hashes and the encoded non-indexed data. +func PackGreetingChangedEvent(sender common.Address, data GreetingChangedEventData) ([]common.Hash, []byte, error) { + return HelloWorldABI.PackEvent("GreetingChanged", sender, data.OldGreeting, data.NewGreeting) +} + +// UnpackGreetingChangedEventData attempts to unpack non-indexed [dataBytes]. +func UnpackGreetingChangedEventData(dataBytes []byte) (GreetingChangedEventData, error) { + eventData := GreetingChangedEventData{} + err := HelloWorldABI.UnpackIntoInterface(&eventData, "GreetingChanged", dataBytes) + return eventData, err +} diff --git a/precompile/contracts/helloworld/module.go b/precompile/contracts/helloworld/module.go new file mode 100644 index 0000000000..0ab997f570 --- /dev/null +++ b/precompile/contracts/helloworld/module.go @@ -0,0 +1,75 @@ +// Code generated +// This file is a generated precompile contract config with stubbed abstract functions. +// The file is generated by a template. Please inspect every code and comment in this file before use. + +package helloworld + +import ( + "fmt" + + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ava-labs/subnet-evm/precompile/modules" + "github.com/ava-labs/subnet-evm/precompile/precompileconfig" + + "github.com/ethereum/go-ethereum/common" +) + +var _ contract.Configurator = &configurator{} + +// ConfigKey is the key used in json config files to specify this precompile config. +// must be unique across all precompiles. +const ConfigKey = "helloWorldConfig" + +// ContractAddress is the defined address of the precompile contract. +// This should be unique across all precompile contracts. +// See precompile/registry/registry.go for registered precompile contracts and more information. +var ContractAddress = common.HexToAddress("0x0300000000000000000000000000000000000000") // SET A SUITABLE HEX ADDRESS HERE + +// Module is the precompile module. It is used to register the precompile contract. +var Module = modules.Module{ + ConfigKey: ConfigKey, + Address: ContractAddress, + Contract: HelloWorldPrecompile, + Configurator: &configurator{}, +} + +const defaultGreeting = "Hello World!" + +type configurator struct{} + +func init() { + // Register the precompile module. + // Each precompile contract registers itself through [RegisterModule] function. + if err := modules.RegisterModule(Module); err != nil { + panic(err) + } +} + +// MakeConfig returns a new precompile config instance. +// This is required to Marshal/Unmarshal the precompile config. +func (*configurator) MakeConfig() precompileconfig.Config { + return new(Config) +} + +// Configure configures [state] with the given [cfg] precompileconfig. +// This function is called by the EVM once per precompile contract activation. +// You can use this function to set up your precompile contract's initial state, +// by using the [cfg] config and [state] stateDB. +func (*configurator) Configure(chainConfig precompileconfig.ChainConfig, cfg precompileconfig.Config, state contract.StateDB, blockContext contract.ConfigurationBlockContext) error { + config, ok := cfg.(*Config) + if !ok { + return fmt.Errorf("incorrect config %T: %v", config, config) + } + // CUSTOM CODE STARTS HERE + + // This will be called in the first block where HelloWorld stateful precompile is enabled. + // 1) If BlockTimestamp is nil, this will not be called + // 2) If BlockTimestamp is 0, this will be called while setting up the genesis block + // 3) If BlockTimestamp is 1000, this will be called while processing the first block + // whose timestamp is >= 1000 + // + // Set the initial value under [common.BytesToHash([]byte("storageKey")] to "Hello World!" + StoreGreeting(state, defaultGreeting) + // AllowList is activated for this precompile. Configuring allowlist addresses here. + return config.AllowListConfig.Configure(chainConfig, ContractAddress, state, blockContext) +} diff --git a/precompile/registry/registry.go b/precompile/registry/registry.go index 490694d669..1c2e5b9c3f 100644 --- a/precompile/registry/registry.go +++ b/precompile/registry/registry.go @@ -18,6 +18,8 @@ import ( _ "github.com/ava-labs/subnet-evm/precompile/contracts/rewardmanager" _ "github.com/ava-labs/subnet-evm/precompile/contracts/warp" + + _ "github.com/ava-labs/subnet-evm/precompile/contracts/helloworld" // ADD YOUR PRECOMPILE HERE // _ "github.com/ava-labs/subnet-evm/precompile/contracts/yourprecompile" ) @@ -40,5 +42,6 @@ import ( // FeeManagerAddress = common.HexToAddress("0x0200000000000000000000000000000000000003") // RewardManagerAddress = common.HexToAddress("0x0200000000000000000000000000000000000004") // WarpAddress = common.HexToAddress("0x0200000000000000000000000000000000000005") +// HelloWorldAddress = common.HexToAddress("0x0300000000000000000000000000000000000000") // ADD YOUR PRECOMPILE HERE // {YourPrecompile}Address = common.HexToAddress("0x03000000000000000000000000000000000000??") diff --git a/tests/precompile/genesis/hello_world.json b/tests/precompile/genesis/hello_world.json new file mode 100644 index 0000000000..6d81a238bc --- /dev/null +++ b/tests/precompile/genesis/hello_world.json @@ -0,0 +1,49 @@ +{ + "config": { + "chainId": 99999, + "homesteadBlock": 0, + "eip150Block": 0, + "eip150Hash": "0x2086799aeebeae135c246c65021c82b4e15a2c451340993aacfd2751886514f0", + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "petersburgBlock": 0, + "istanbulBlock": 0, + "muirGlacierBlock": 0, + "feeConfig": { + "gasLimit": 20000000, + "minBaseFee": 1000000000, + "targetGas": 100000000, + "baseFeeChangeDenominator": 48, + "minBlockGasCost": 0, + "maxBlockGasCost": 10000000, + "targetBlockRate": 2, + "blockGasCostStep": 500000 + }, + "helloWorldConfig": { + "blockTimestamp": 0, + "adminAddresses": [ + "0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC" + ] + } + }, + "alloc": { + "8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC": { + "balance": "0x52B7D2DCC80CD2E4000000" + }, + "0x0Fa8EA536Be85F32724D57A37758761B86416123": { + "balance": "0x52B7D2DCC80CD2E4000000" + } + }, + "nonce": "0x0", + "timestamp": "0x0", + "extraData": "0x00", + "gasLimit": "0x1312D00", + "difficulty": "0x0", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase": "0x0000000000000000000000000000000000000000", + "number": "0x0", + "gasUsed": "0x0", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000" +} diff --git a/tests/precompile/solidity/suites.go b/tests/precompile/solidity/suites.go index 4aacb83c4b..74b732adc7 100644 --- a/tests/precompile/solidity/suites.go +++ b/tests/precompile/solidity/suites.go @@ -74,6 +74,14 @@ func RegisterAsyncTests() { runDefaultHardhatTests(ctx, blockchainID, "reward_manager") }) + ginkgo.It("hello world", ginkgo.Label("Precompile"), ginkgo.Label("HelloWorld"), func() { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + blockchainID := subnetsSuite.GetBlockchainID("hello_world") + runDefaultHardhatTests(ctx, blockchainID, "hello_world") + }) + // ADD YOUR PRECOMPILE HERE /* ginkgo.It("your precompile", ginkgo.Label("Precompile"), ginkgo.Label("YourPrecompile"), func() {