Testing Solidity Smart Contracts with Zombienet
In this tutorial, we will demonstrate how to deploy your parachain using Zombienet, and test the functionalities of your parachain.
Below are the main steps of this demo: . Deploy our parachain against the locally simulated rococo testnet by Zombienet. . Deploy a Solidity smart contract on our parachain. . Successfully invoke the Solidity smart contract deployed on our parachain.
Step 1: Deploy The Parachain
-
git clone https://github.com/OpenZeppelin/polkadot-runtime-templates
-
move to evm template directory
cd evm-template
-
replace the content of
zombinet-config/devnet.toml
with:[relaychain] chain = "rococo-local" default_command = "./bin-v1.6.0/polkadot" [[relaychain.nodes]] name = "alice" validator = true [[relaychain.nodes]] name = "bob" validator = true [relaychain.genesis.runtimeGenesis.patch.configuration.config] scheduling_lookahead = 2 [relaychain.genesis.runtimeGenesis.patch.configuration.config.async_backing_params] max_candidate_depth = 3 allowed_ancestry_len = 2
-
build the zombienet:
./scripts/zombienet.sh build
if you came across this error (click to expand):
error[E0635]: unknown feature `stdsimd` --> /Users/ozgunozerk/.cargo/registry/src/index.crates.io-6f17d22bba15001f/ahash-0.7.6/src/lib.rs:33:42 | 33 | #![cfg_attr(feature = "stdsimd", feature(stdsimd))]
Cd
into thepolkadot-sdk
directory (the path should be visible on the error message), and run the below command to updateahash
:cargo update -p [email protected]
or if you came across this error (click to expand):
assertion failed [block != nullptr]: BasicBlock requested for unrecognized address
just re-run 🙂
-
run the zombinet:
./scripts/zombienet.sh devnet
-
build it with
async-backing
feature:cargo build --release --features="async-backing"
-
copy the
Direct Link
fromAlice’s
tab from Zombienet TUI: -
Open the link with Chrome: PolkadotJS. As of 2024 July, it doesn’t work on Safari and Brave, and it is buggy on Firefox.
-
Reserve a
ParaId
on Zombienet:-
Go toÂ
Network
 >ÂParachains
-
Go toÂ
Parathreads
 tab -
Click theÂ
+ ParaId
 button -
Save aÂ
parachain id
 for the further usage. -
ClickÂ
Submit
 andÂSign and Submit
(you can useAlice
as the account).
-
-
Preparing necessary files to become a Parachain:
-
Generate a plain chainspec:
./target/release/evm-template-node build-spec --disable-default-bootnode > plain-parachain-chainspec.json
-
Edit the chainspec:
-
UpdateÂ
name
,Âid
 andÂprotocolId
 to unique values (optional). -
ChangeÂ
para_id
 andÂparachainInfo.parachainId
 fromÂ1000
 to the previously saved parachain id (probably 2000 if that’s your first time ;) ). -
Edit theÂ
evmChainId.chainId
 to the value of your choice. Try to select a value that has no existing EVM chain assigned to it (should be ok to leave as is for the most cases).
-
-
Generate a raw chainspec:
./target/release/evm-template-node build-spec --chain plain-parachain-chainspec.json --disable-default-bootnode --raw > raw-parachain-chainspec.json
-
Generate the genesis state:
./target/release/evm-template-node export-genesis-state --chain raw-parachain-chainspec.json > genesis-state
-
Generate the validation code:
./target/release/evm-template-node export-genesis-wasm --chain raw-parachain-chainspec.json > genesis-wasm
-
-
Registering the Parachain:
-
Go back to
polkadot.js.org/apps
(remember Chrome). Go toDeveloper/Sudo
. -
select
pasasSudoWrapper
andsudoScheduleParaInitialize(id, genesis)
-
enter the reserved id (2000) to
id
field -
enable
file upload
for bothgenesisHead
andvalidationCode
→ because we will upload files for these. -
select
Yes
forparaKind
→ meaning when we register our parachain, it will be a parachain rather than a parathread. -
drag&drop your
genesis-state
file generated in step10.d
intogenesisHead
field (good luck with the aiming) -
drag&drop your
genesis-wasm
file generated in10.e
intovalidationCode
field -
It should look like below:
-
Submit Sudo
!
-
-
copy the path to
chain-spec
from zombienet terminal fromBob
(beware, this file is changing every time you spin up a new zombienet): -
run the node, and provide the
chain-spec
you copied from the last step into--chain
part:-
be sure to clear your storage if you were running a node before
./target/release/evm-template-node \ --alice \ --collator \ --force-authoring \ --chain raw-parachain-chainspec.json \ --base-path storage/alice \ --port 40333 \ --rpc-port 8844 \ -- \ --execution wasm \ --chain /var/folders/...{redacted}.../rococo-local.json \ --port 30343 \ --rpc-port 9977
-
-
your node should be running without any problem, and should see block production in your node terminal!
Step 2: Deploy a Solidity Smart Contract
-
Install Foundry with:
curl -L [https://foundry.paradigm.xyz](https://foundry.paradigm.xyz/) | bash
-
have a smart contract file ready, any smart contract of your choice! We will go with a cute
HelloWorld.sol
smart contract for this tutorial:// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract HelloWorld { string public greeting = "Hello, World!"; function getGreeting() public view returns (string memory) { return greeting; } }
-
Create a new javascript project with the below files:
-
package.json
:{ "name": "ts-wallet", "version": "1.0.0", "description": "", "main": "index.js", "type": "module", "scripts": { "exec": "node index.js", "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "web3": "^4.8.0" } }
-
sanity_check.js
:import { Web3 } from "web3"; const web3 = new Web3("ws://127.0.0.1:8844"); console.log("Balance:"); web3.eth.getBalance("0xe04cc55ebee1cbce552f250e85c57b70b2e2625b").then(console.log); let raw = await web3.eth.accounts.signTransaction({ gas: 21000, gasPrice: 10000000000, from: "0xe04cc55ebee1cbce552f250e85c57b70b2e2625b", to: "0x7c98a1801f0B28dF559bCd828fc67Bd6ab558074", value: '100000000000000000' }, "0xcb6df9de1efca7a3998a8ead4e02159d5fa99c3e0d4fd6432667390bb4726854"); let res = await web3.eth.sendSignedTransaction(raw.rawTransaction); console.log("Transaction details:"); console.log(res);
-
invoke_smart_contract.js
:import { Web3 } from "web3"; import { MyAbi } from "./abi.js"; const web3 = new Web3("ws://127.0.0.1:8844"); let contract = new web3.eth.Contract(MyAbi, "0x4045F03B68919da2c440F023Fd7cE2982BfD3C03"); let s = await contract.methods.getGreeting().call(); console.log(s);
-
abi.js
:export var MyAbi = [ { "type": "function", "name": "getGreeting", "inputs": [], "outputs": [ { "name": "", "type": "string", "internalType": "string" } ], "stateMutability": "view" }, { "type": "function", "name": "greeting", "inputs": [], "outputs": [ { "name": "", "type": "string", "internalType": "string" } ], "stateMutability": "view" } ];
-
-
run the below command, and you should see the balance, and then the transaction details printed, proving everything works so far!
node sanity_check.js
-
open a terminal instance where the current directory has the
HelloWorld.sol
file, and run:forge create --rpc-url http://localhost:8844 --private-key 0xcb6df9de1efca7a3998a8ead4e02159d5fa99c3e0d4fd6432667390bb4726854 HelloWorld.sol:HelloWorld
-
don’t forget to copy the address this contract deployed to!
-
Step 3: Invoke The Solidity Smart Contract
-
replace the contract address in
invoke_smart_contract.js
with the address you copied! -
build the
abi
of the smart contract with:forge build --silent && jq '.abi' ./out/HelloWorld.sol/HelloWorld.json
-
Surprise! We already give you the abi of this in
abi.js
file in step3
. If you used another contract thanHelloWorld
, replace thatabi.js
file’s content with your contractsabi
. -
run the below command, and you should see your smart contract in action:
node invoke_smart_contract.js