Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions contracts/contracts/ExampleHelloWorld.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
16 changes: 16 additions & 0 deletions contracts/contracts/interfaces/IHelloWorld.sol
Original file line number Diff line number Diff line change
@@ -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;
}
42 changes: 42 additions & 0 deletions contracts/contracts/test/ExampleHelloWorldTest.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
63 changes: 63 additions & 0 deletions contracts/test/hello_world.ts
Original file line number Diff line number Diff line change
@@ -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
)
})
})
23 changes: 23 additions & 0 deletions precompile/contracts/helloworld/README.md
Original file line number Diff line number Diff line change
@@ -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 <https://docs.avax.network/subnets/hello-world-precompile-tutorial> 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`
77 changes: 77 additions & 0 deletions precompile/contracts/helloworld/config.go
Original file line number Diff line number Diff line change
@@ -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
}
88 changes: 88 additions & 0 deletions precompile/contracts/helloworld/config_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
1 change: 1 addition & 0 deletions precompile/contracts/helloworld/contract.abi
Original file line number Diff line number Diff line change
@@ -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"}]
Loading