Skip to content

Commit 283d9ca

Browse files
committed
add hello world precompile
1 parent 197588c commit 283d9ca

30 files changed

+1171
-10
lines changed

.github/workflows/sync.yml

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: Sync
2+
on:
3+
push:
4+
branches:
5+
- main
6+
7+
jobs:
8+
sync-branches:
9+
runs-on: ubuntu-latest
10+
name: Syncing branches
11+
steps:
12+
- name: Checkout
13+
uses: actions/checkout@v2
14+
- name: Set up Node
15+
uses: actions/setup-node@v1
16+
with:
17+
node-version: 12
18+
- name: Opening pull request
19+
id: pull
20+
uses: tretuna/[email protected]
21+
with:
22+
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
23+
FROM_BRANCH: "main"
24+
TO_BRANCH: "develop"

README.md

+101
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,111 @@ To get a comprehensive introduction to Precompile-EVM, take the Avalanche Academ
3434

3535
There is an example branch [hello-world-example](https://github.com/ava-labs/precompile-evm/tree/hello-world-example) in this repository. You can check the example branch to see how to register precompiles and test them.
3636

37+
### Clone the Repo
38+
39+
```zsh
40+
git clone https://github.com/ava-labs/precompile-evm.git
41+
cd precompile-evm/ # change directory to the precompile-evm/ directory
42+
```
43+
44+
### Checkout the `hello-world-example` Branch
45+
46+
```zsh
47+
git checkout hello-world-example
48+
49+
branch 'hello-world-example' set up to track 'origin/hello-world-example'.
50+
Switched to a new branch 'hello-world-example'
51+
```
52+
53+
### Install NodeJS Dependencies
54+
55+
First you have to `cd contracts/` and run `npm install` to get the dependencies.
56+
57+
```zsh
58+
cd contracts/ # change directory to the contracts/ directory
59+
npm install
60+
```
61+
62+
### Create a New Contract
63+
64+
`hello-world-example` branch has already a precompile contract called `HelloWorld.sol`. All necessary files were already created for you. You can check existing files and see how a fully implemented precompile should look like. If you'd like to redo steps to create a new precompile contract, you can follow the steps below.
65+
66+
Copy the existing `IHelloWorld.sol` interface to a new file called `IHolaMundo.sol`.
67+
68+
```zsh
69+
cd .. # change directory back to the root of the repo
70+
cp contracts/contracts/interfaces/IHelloWorld.sol contracts/contracts/interfaces/IHolaMundo.sol
71+
```
72+
73+
### Install `solc` and Confirm Dependency Version
74+
75+
Install the `solc` dependency.
76+
77+
```zsh
78+
brew install solidity
79+
```
80+
81+
Confirm `solc` is >=0.8.8.
82+
83+
```zsh
84+
solc --version
85+
86+
solc, the solidity compiler commandline interface
87+
Version: 0.8.17+commit.8df45f5f.Darwin.appleclang
88+
```
89+
90+
### Generate an `.abi`
91+
92+
Now generate a `.abi` from a `.sol` using `solc`.
93+
94+
Passing in the following flags
95+
96+
- `--abi`
97+
- ABI specification of the contracts.
98+
- `--base-path path`
99+
- Use the given path as the root of the source tree instead of the root of the filesystem.
100+
- `--include-path path`
101+
- Make an additional source directory available to the default import callback. Use this option if you want to import contracts whose location is not fixed in relation to your main source tree, e.g. third-party libraries installed using a package manager. Can be used multiple times. Can only be used if base path has a non-empty value.
102+
- `--output-dir path`
103+
- If given, creates one file per output component and contract/file at the specified directory.
104+
- `--overwrite`
105+
- Overwrite existing files (used together with `--output-dir`).
106+
107+
```zsh
108+
cd contracts/ # change directory to the contracts/ directory
109+
solc --abi contracts/interfaces/IHolaMundo.sol --output-dir abis --base-path . --include-path ./node_modules --overwrite
110+
111+
Compiler run successful. Artifact(s) can be found in directory "abis".
112+
```
113+
37114
### Generate Precompile Files
38115

39116
First, you need to create your precompile contract interface in the `contracts` directory and build the ABI. Then you can generate your precompile files with `./scripts/generate_precompile.sh --abi {abiPath} --out {outPath}`. This script installs the `precompilegen` tool from Subnet-EVM and runs it to generate your precompile.
40117

118+
```zsh
119+
cd .. # change directory back to the root directory of the repo
120+
./scripts/generate_precompile.sh --abi contracts/abis/IHolaMundo.abi --out holamundo/
121+
122+
Using branch: hello-world-example
123+
installing precompilegen from Subnet-EVM v0.5.2
124+
generating precompile with Subnet-EVM v0.5.2
125+
Precompile files generated successfully at: holamundo/
126+
```
127+
128+
Confirm that the new `holamundo/` directory has the appropriate files.
129+
130+
```zsh
131+
ls -lh helloworld
132+
133+
-rw-r--r-- 1 user group 2.3K Jul 5 13:26 README.md
134+
-rw-r--r-- 1 user group 2.3K Jul 5 13:26 config.go
135+
-rw-r--r-- 1 user group 2.8K Jul 5 13:26 config_test.go
136+
-rw-r--r-- 1 user group 963B Jul 5 13:26 contract.abi
137+
-rw-r--r-- 1 user group 8.1K Jul 5 13:26 contract.go
138+
-rw-r--r-- 1 user group 8.3K Jul 5 13:26 contract_test.go
139+
-rw-r--r-- 1 user group 2.7K Jul 5 13:26 module.go
140+
```
141+
41142
### Register Precompile
42143

43144
In `plugin/main.go` Subnet-EVM is already imported and ready to be Run from the main package. All you need to do is explicitly register your precompiles to Subnet-EVM in `plugin/main.go` and build it together with Subnet-EVM. Precompiles generated by `precompilegen` tool have a self-registering mechanism in their `module.go/init()` function. All you need to do is to force-import your precompile packprecompile package in `plugin/main.go`.

contracts/abis/IAllowList.abi

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"readAllowList","outputs":[{"internalType":"uint256","name":"role","type":"uint256"}],"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":"address","name":"addr","type":"address"}],"name":"setNone","outputs":[],"stateMutability":"nonpayable","type":"function"}]

contracts/abis/IHelloWorld.abi

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[{"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":"setNone","outputs":[],"stateMutability":"nonpayable","type":"function"}]

contracts/contracts/.gitkeep

Whitespace-only changes.
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
//SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.0;
3+
4+
import "./interfaces/IHelloWorld.sol";
5+
6+
address constant HELLO_WORLD_ADDRESS = 0x0300000000000000000000000000000000000000;
7+
8+
// ExampleHelloWorld shows how the HelloWorld precompile can be used in a smart contract.
9+
contract ExampleHelloWorld {
10+
IHelloWorld helloWorld = IHelloWorld(HELLO_WORLD_ADDRESS);
11+
12+
function sayHello() public view returns (string memory) {
13+
return helloWorld.sayHello();
14+
}
15+
16+
function setGreeting(string calldata greeting) public {
17+
helloWorld.setGreeting(greeting);
18+
}
19+
}

contracts/contracts/interfaces/.gitkeep

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity >=0.8.0;
4+
import "@avalabs/subnet-evm-contracts/contracts/interfaces/IAllowList.sol";
5+
6+
interface IHelloWorld is IAllowList {
7+
// sayHello returns the stored greeting string
8+
function sayHello() external view returns (string calldata result);
9+
10+
// setGreeting stores the greeting string
11+
function setGreeting(string calldata response) external;
12+
}

contracts/contracts/test/.gitkeep

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.0;
3+
4+
import "../ExampleHelloWorld.sol";
5+
import "../interfaces/IHelloWorld.sol";
6+
import "@avalabs/subnet-evm-contracts/contracts/test/AllowListTest.sol";
7+
8+
contract ExampleHelloWorldTest is AllowListTest {
9+
IHelloWorld helloWorld = IHelloWorld(HELLO_WORLD_ADDRESS);
10+
11+
function step_getDefaultHelloWorld() public {
12+
ExampleHelloWorld example = new ExampleHelloWorld();
13+
address exampleAddress = address(example);
14+
15+
assertRole(helloWorld.readAllowList(exampleAddress), AllowList.Role.None);
16+
assertEq(example.sayHello(), "Hello World!");
17+
}
18+
19+
function step_doesNotSetGreetingBeforeEnabled() public {
20+
ExampleHelloWorld example = new ExampleHelloWorld();
21+
address exampleAddress = address(example);
22+
23+
assertRole(helloWorld.readAllowList(exampleAddress), AllowList.Role.None);
24+
25+
try example.setGreeting("testing") {
26+
assertTrue(false, "setGreeting should fail");
27+
} catch {} // TODO should match on an error to make sure that this is failing in the way that's expected
28+
}
29+
30+
function step_setAndGetGreeting() public {
31+
ExampleHelloWorld example = new ExampleHelloWorld();
32+
address exampleAddress = address(example);
33+
34+
assertRole(helloWorld.readAllowList(exampleAddress), AllowList.Role.None);
35+
helloWorld.setEnabled(exampleAddress);
36+
assertRole(helloWorld.readAllowList(exampleAddress), AllowList.Role.Enabled);
37+
38+
string memory greeting = "testgreeting";
39+
example.setGreeting(greeting);
40+
assertEq(example.sayHello(), greeting);
41+
}
42+
}

contracts/hardhat.config.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import "@nomiclabs/hardhat-waffle"
22
import "@nomiclabs/hardhat-ethers"
3-
import "./tasks.ts"
3+
import "./tasks"
44

55
// HardHat users must populate these environment variables in order to connect to their subnet-evm instance
66
// Since the blockchainID is not known in advance, there's no good default to use and we use the C-Chain here.

contracts/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
},
3333
"license": "BSD-3-Clause",
3434
"scripts": {
35+
"build": "rm -rf dist/ && tsc -b",
3536
"compile": "npx hardhat compile",
3637
"console": "npx hardhat console",
3738
"test": "npx hardhat test",

contracts/scripts/.gitkeep

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import {
2+
Contract,
3+
ContractFactory
4+
} from "ethers"
5+
import { ethers } from "hardhat"
6+
7+
const main = async (): Promise<any> => {
8+
const contractFactory: ContractFactory = await ethers.getContractFactory("ExampleHelloWorld")
9+
const contract: Contract = await contractFactory.deploy()
10+
11+
await contract.deployed()
12+
console.log(`Contract deployed to: ${contract.address}`)
13+
}
14+
15+
main()
16+
.then(() => process.exit(0))
17+
.catch(error => {
18+
console.error(error)
19+
process.exit(1)
20+
})

contracts/tasks.ts

+60-1
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,28 @@ import { task } from "hardhat/config"
22
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"
33
import { BigNumber } from "ethers"
44

5+
const HELLO_WORLD_ADDRESS = "0x0300000000000000000000000000000000000000"
6+
7+
const ROLES = {
8+
0: "None",
9+
1: "Enabled",
10+
2: "Admin",
11+
}
12+
13+
const getRole = async (allowList, address) => {
14+
const role = await allowList.readAllowList(address)
15+
console.log(`${address} has role: ${ROLES[role.toNumber()]}`)
16+
}
17+
18+
// npx hardhat accounts --network local
519
task("accounts", "Prints the list of accounts", async (args, hre): Promise<void> => {
620
const accounts: SignerWithAddress[] = await hre.ethers.getSigners()
721
accounts.forEach((account: SignerWithAddress): void => {
822
console.log(account.address)
923
})
1024
})
1125

26+
// npx hardhat balances --network local
1227
task("balances", "Prints the list of account balances", async (args, hre): Promise<void> => {
1328
const accounts: SignerWithAddress[] = await hre.ethers.getSigners()
1429
for (const account of accounts) {
@@ -19,7 +34,7 @@ task("balances", "Prints the list of account balances", async (args, hre): Promi
1934
}
2035
})
2136

22-
37+
// npx hardhat balance --network local --address [address]
2338
task("balance", "get the balance")
2439
.addParam("address", "the address you want to know balance of")
2540
.setAction(async (args, hre) => {
@@ -28,5 +43,49 @@ task("balance", "get the balance")
2843
console.log(`balance: ${balanceInCoin} Coin`)
2944
})
3045

46+
// npx hardhat helloWorld:readRole --network local --address [address]
47+
task("helloWorld:readRole", "Gets the network enabled allow list")
48+
.addParam("address", "the address you want to know the allowlist role for")
49+
.setAction(async (args, hre) => {
50+
const allowList = await hre.ethers.getContractAt("IHelloWorld", HELLO_WORLD_ADDRESS)
51+
await getRole(allowList, args.address)
52+
})
53+
54+
// npx hardhat helloWorld:addEnabled --network local --address [address]
55+
task("helloWorld:addEnabled", "Adds the enabled on the allow list")
56+
.addParam("address", "the address you want to add as a enabled")
57+
.setAction(async (args, hre) => {
58+
const allowList = await hre.ethers.getContractAt("IHelloWorld", HELLO_WORLD_ADDRESS)
59+
// ADD CODE BELOW
60+
await allowList.setEnabled(args.address)
61+
await getRole(allowList, args.address)
62+
})
63+
64+
// npx hardhat helloWorld:addAdmin --network local --address [address]
65+
task("helloWorld:addAdmin", "Adds an admin on the allowlist")
66+
.addParam("address", "the address you want to add as a admin")
67+
.setAction(async (args, hre) => {
68+
const allowList = await hre.ethers.getContractAt("IHelloWorld", HELLO_WORLD_ADDRESS)
69+
await allowList.setAdmin(args.address)
70+
await getRole(allowList, args.address)
71+
})
72+
73+
// npx hardhat helloWorld:sayHello --network local
74+
task("helloWorld:sayHello", "Says hello")
75+
.setAction(async (args, hre) => {
76+
const helloWorld = await hre.ethers.getContractAt("IHelloWorld", HELLO_WORLD_ADDRESS)
77+
const result = await helloWorld.sayHello()
78+
console.log(result)
79+
})
80+
81+
// npx hardhat helloWorld:setGreeting --network local --greeting [greeting]
82+
task("helloWorld:setGreeting", "Says hello")
83+
.addParam("greeting", "the greeting string you want to set")
84+
.setAction(async (args, hre) => {
85+
const helloWorld = await hre.ethers.getContractAt("IHelloWorld", HELLO_WORLD_ADDRESS)
86+
const result = await helloWorld.setGreeting(args.greeting)
87+
console.log(result)
88+
})
89+
3190

3291

contracts/test/.gitkeep

Whitespace-only changes.

contracts/test/hello_world.ts

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// (c) 2019-2022, Ava Labs, Inc. All rights reserved.
2+
// See the file LICENSE for licensing terms.
3+
4+
import { ethers } from "hardhat"
5+
import { test } from "@avalabs/subnet-evm-contracts"
6+
7+
// make sure this is always an admin for hello world precompile
8+
const ADMIN_ADDRESS = "0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC"
9+
const HELLO_WORLD_ADDRESS = "0x0300000000000000000000000000000000000000"
10+
11+
describe("ExampleHelloWorldTest", function () {
12+
this.timeout("30s")
13+
14+
beforeEach('Setup DS-Test contract', async function () {
15+
const signer = await ethers.getSigner(ADMIN_ADDRESS)
16+
const helloWorldPromise = ethers.getContractAt("IHelloWorld", HELLO_WORLD_ADDRESS, signer)
17+
18+
return ethers.getContractFactory("ExampleHelloWorldTest", { signer })
19+
.then(factory => factory.deploy())
20+
.then(contract => {
21+
this.testContract = contract
22+
return contract.deployed().then(() => contract)
23+
})
24+
.then(() => Promise.all([helloWorldPromise]))
25+
.then(([helloWorld]) => helloWorld.setAdmin(this.testContract.address))
26+
.then(tx => tx.wait())
27+
})
28+
29+
test("should gets default hello world", ["step_getDefaultHelloWorld"])
30+
31+
test("should not set greeting before enabled", "step_doesNotSetGreetingBeforeEnabled")
32+
33+
test("should set and get greeting with enabled account", "step_setAndGetGreeting")
34+
});

0 commit comments

Comments
 (0)