logoDeveloper Hub

Generating Your Precompile

In this section, we will go over the process for automatically generating the template code which you can configure accordingly for your stateful precompile.

First, we must create the Solidity interface that we want our precompile to implement. This will be the HelloWorld Interface. It will have two simple functions, sayHello(), setGreeting() and an event GreetingChanged. These two functions will demonstrate the getting and setting respectively of a value stored in the precompile's state space.

The sayHello() function is a view function, meaning it does not modify the state of the precompile and returns a string result. The setGreeting() function is a state changer function, meaning it modifies the state of the precompile. The HelloWorld interface inherits IAllowList interface to use the allow list functionality.

For this tutorial, we will be working in a new branch in Subnet-EVM/Precompile-EVM repo.

cd $GOPATH/src/github.com/ava-labs/subnet-evm

Then checkout to a new branch:

git checkout -b hello-world-stateful-precompile

We will start off in this directory ./contracts/:

cd contracts/

Create a new file called IHelloWorld.sol and copy and paste the below code:

contracts/IHelloWorld.sol
// (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;
}

Now we have an interface that our precompile can implement! Let's create an ABI of our Solidity interface.

In the same directory, let's run:

solc --abi ./contracts/interfaces/IHelloWorld.sol -o ./abis

This generates the ABI code under ./abis/IHelloWorld.abi.

[
  {
    "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"
  }
]

As you can see the ABI also contains the IAllowList interface functions. This is because the IHelloWorld interface inherits from the IAllowList interface.

Note: The ABI must have named outputs in order to generate the precompile template.

Now that we have an ABI for the precompile gen tool to interact with, we can run the following command to generate our HelloWorld precompile files!

Let's go back to the root of the repository and run the PrecompileGen script helper:

cd ..

Both of these Subnet-EVM and Precompile-EVM have the same generate_precompile.sh script. The one in Precompile-EVM installs the script from Subnet-EVM and runs it.

./scripts/generate_precompile.sh --help
 
# output
Using branch: precompile-tutorial
NAME:
precompilegen - subnet-evm precompile generator tool
 
USAGE:
main [global options] command [command options] [arguments...]
 
VERSION:
1.10.26-stable
 
COMMANDS:
help, h Shows a list of commands or help for one command
 
GLOBAL OPTIONS:
 
    --abi value
          Path to the contract ABI json to generate, - for STDIN
 
    --out value
          Output folder for the generated precompile files, - for STDOUT (default =
          ./precompile/contracts/{pkg}). Test files won't be generated if STDOUT is used
    --pkg value
          Go package name to generate the precompile into (default = {type})
    --type value
          Struct name for the precompile (default = {abi file name})
MISC
    --help, -h                     (default: false)
          show help
    --version, -v                  (default: false)
          print the version
COPYRIGHT:
Copyright 2013-2022 The go-ethereum Authors

Now let's generate the precompile template files!

In Subnet-EVM precompile implementations reside under the ./precompile/contracts directory. Let's generate our precompile template in the ./precompile/contracts/helloworld directory, where helloworld is the name of the Go package we want to generate the precompile into.

./scripts/generate_precompile.sh --abi ./contracts/abis/IHelloWorld.abi --type HelloWorld --pkg helloworld

This generates a precompile template files contract.go, contract.abi, config.go, module.go, event.go and README.md files. README.md explains general guidelines for precompile development. You should carefully read this file before modifying the precompile template.

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.
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- If you have any event defined in your precompile, review the generated event.go file and set your event gas costs. You should also emit your event in your function in the contract.go file.
5- Set gas costs in generated contract.go
6- Force import your precompile package in precompile/registry/registry.go
7- Add your config unit tests under generated package config_test.go
8- Add your contract unit tests under generated package contract_test.go
9- Additionally you can add a full-fledged VM test for your precompile under plugin/vm/vm_test.go. See existing precompile tests for examples.
10- Add your solidity interface and test contract to contracts/contracts
11- Write solidity contract tests for your precompile in contracts/contracts/test
12- Write TypeScript DS-Test counterparts for your solidity tests in contracts/test
13- Create your genesis with your precompile enabled in tests/precompile/genesis/
14- Create e2e test for your solidity test in tests/precompile/solidity/suites.go
15- Run your e2e precompile Solidity tests with './scripts/run_ginkgo.sh`

Let's follow these steps and create our HelloWorld precompile.

Last updated on

On this page

No Headings
Edit on Github