Currently the industry standard Solidity smart contract test and deploy frameworks are Hardhat and Truffle. They both use JavaScript to test and deploy solidity smart contracts. Another smart contract testing platform by the name of Foundry. The advantage that Foundry provides is that all tests are also written solidity creating a smoother developer experience. With Foundry, the engineer does not need to switch languages to write tests and deploy contracts..
Currently only Hardhat has developed functionality for zkSync. The purpose of this repository is to create functionality with Foundry to fully test, compile and deploy smart contracts on zkSync using only Solidity, as well as interact with those contracts once deployed.
- Quick Start / Installation
- v0.0 Feature Set
- Environment Variables
- Blockchain Interaction
- Bridging Assets
- Compilation
- Deployment
- Contract Interaction
- Deploy and Interact with
SimpleFactory.sol
- Account Abstraction Multisig example
Clone these two repos to the same directory:
- foundry-zksync - this is our application, we will be building this in the following steps
- sample-fzksync-project - this is the sample project that contains the smart contract to be compiled
# make working directory and cd anywhere on filesystem
$ mkdir fzksync && cd fzksync
# clone foundry-zksync
$ git clone https://github.com/matter-labs/foundry-zksync.git
# clone fzksync-project
$ git clone https://github.com/sammyshakes/sample-fzksync-project.git
# cd into foundry-zksync and build
$ cd foundry-zksync
$ cargo build -p foundry-cli
We need to establish the functionality we want for release v0.0 of this implementation. Below we will specify the exact features to accomplish our v0.0 release.
- Compile smart contracts with zksolc compiler
- Deploy smart contracts to zkSync Testnet or Local Test Node
- Bridge assets L1 <-> L2
- Make contract calls to deployed contracts on zkSync Testnet or Local Test Node
- Send transactions to deployed contracts on zkSync Testnet or Local Test Node
NOTE: All commands are entered from the project root folder
By providing the following environment variables in the .env
file at the PROJECT-ROOT
folder, the --rpc-url
and --chain
flags can be ommitted in command lines.
# ETH_RPC_URL can be used to replace --rpc-url in command line
ETH_RPC_URL=http://localhost:3050
# ZKSYNC_RPC_URL can be used to replace --l2-url in command line for zk-deposit
ZKSYNC_RPC_URL=https://zksync2-testnet.zksync.dev
# CHAIN can be used to replace --chain in command line
# Local: 270, Testnet: 280
CHAIN=270
Follow these instructions to set up local docker node
# chain id local node
../foundry-zksync/target/debug/zkcast chain-id --rpc-url http://localhost:3050
# output:
270
#TESTNET
# chain id testnet
../foundry-zksync/target/debug/zkcast chain-id --rpc-url https://zksync2-testnet.zksync.dev:443
# output:
280
# client
../foundry-zksync/target/debug/zkcast client --rpc-url https://zksync2-testnet.zksync.dev:443
# output:
zkSync/v2.0
# get account L2 eth balance
../foundry-zksync/target/debug/zkcast balance 0x42C7eF198f8aC9888E2B1b73e5B71f1D4535194A --rpc-url https://zksync2-testnet.zksync.dev:443
# output:
447551277794355871
# gas price
../foundry-zksync/target/debug/zkcast gas-price --rpc-url https://zksync2-testnet.zksync.dev:443
# output:
250000000
# timestamp of latest block
../foundry-zksync/target/debug/zkcast age --block latest --rpc-url https://zksync2-testnet.zksync.dev:443
# output:
Mon May 1 16:11:07 2023
# get latest block:
../foundry-zksync/target/debug/zkcast block latest --rpc-url https://zksync2-testnet.zksync.dev:443
# output:
baseFeePerGas 250000000
difficulty 0
extraData 0x
gasLimit 4294967295
gasUsed 40277767
hash 0x6c5b7c9b82b48bd77c0f506d74ed32aec6ab5c52e6c9c604ee8825a0b4a68289
logsBloom 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
miner 0x0000000000000000000000000000000000000000
mixHash 0x0000000000000000000000000000000000000000000000000000000000000000
nonce 0x0000000000000000
number 5024177
parentHash 0x9fbb3c9e5ef3b7807152367eeab5759cce14c290118de0e9011777a640cd7068
receiptsRoot 0x0000000000000000000000000000000000000000000000000000000000000000
sealFields []
sha3Uncles 0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347
size 0
stateRoot 0x0000000000000000000000000000000000000000000000000000000000000000
timestamp 1682957640
totalDifficulty 0
l1BatchNumber null
l1BatchTimestamp null
zkcast zk-deposit <TO> <AMOUNT> <TOKEN> --rpc-url <RPC-URL> --l2-url <L2URL> --chain <CHAIN-ID> --private-key <PRIVATE-KEY>
NOTE: Leave <TOKEN>
blank to bridge ETH
Usage: zkcast zk-deposit <TO> <AMOUNT> --rpc-url <ETH_RPC_URL> --l2-url <L2URL> [OPTIONS] [BRIDGE] [TIP]
Arguments:
<TO>
The destination of the transaction.
<AMOUNT>
Amount of token to deposit.
[BRIDGE]
The address of a custom bridge to call.
[TIP]
Optional fee that the user can choose to pay in addition to the regular transaction fee.
Options:
-z, --l2-url <L2URL>
The zkSync RPC Layer 2 endpoint. Can be provided via the env var ZKSYNC_RPC_URL or --l2-url from the command line.
NOTE: For Deposits, ETH_RPC_URL, or --rpc-url should be set to the Layer 1 RPC URL
[env: ZKSYNC_RPC_URL=https://zksync2-testnet.zksync.dev]
--token <TOKEN>
Token to bridge. Leave blank for ETH.
-h, --help
Print help (see a summary with '-h')
../foundry-zksync/target/debug/zkcast zkdeposit 0x36615Cf349d7F6344891B1e7CA7C72883F5dc049 1000000 --rpc-url http://localhost:8545 --l2-url http://localhost:3050 --private-key 7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110 --chain 270
Bridging assets....
Transaction Hash: 0x55793df0a636aedd098309e3487c6d9ec0910422d5b9f0bdbdf764bc82dc1b9f
zkcast zk-send --withdraw <TO> --amount <AMOUNT> <TOKEN> --rpc-url <RPC-URL> --private-key <PRIVATE-KEY>
Arguments:
[TO] The withdraw recipient.
Bridging options:
-w, --withdraw For L2 -> L1 withdrawals.
--token <TOKEN> Token to bridge. Leave blank for ETH.
-a, --amount <AMOUNT> Amount of token to bridge. Required value when bridging
../foundry-zksync/target/debug/zkcast zk-send --withdraw 0x36615Cf349d7F6344891B1e7CA7C72883F5dc049 --amount 1000000 --rpc-url http://localhost:3050 --private-key 7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110 --chain 270
Bridging assets....
Transaction Hash: 0x94ef9e2eed345dcfef6f0b4f953f431b8edc6760e29b598a7cf446dab18d6317
v0.0 Command:
Compile smart contracts to zkEvm bytecode and store compile output files into a logical directory structure <PROJECT-ROOT>/zkout/
for easy retrieval for other components of the application.
Compiler subcommands for zkSync
Usage:
zkforge zk-build [OPTIONS]
Options:
--use-zksolc Specify zksolc compiler version (default if left blank)
--is-system Enable the system contract compilation mode.
--force-evmla Sets the EVM legacy assembly pipeline forcibly
-h, --help Print help
It is necessary for some contracts, including those that deploy other contracts (such as factory contracts), to be compiled using the --is-sytem
flag. These contracts should be placed in the src/is-system/
folder, if the folder does not exist, manually create it:
To compile with only default compiler options (v1.3.9):
../foundry-zksync/target/debug/zkforge zk-build
zksolc
compiler version can optionally be configured using --use-zksolc
flag:
../foundry-zksync/target/debug/zkforge zkb --use-zksolc v1.3.8
zksolc
compiler artifacts can be found in the output folder:
<PROJECT-ROOT>/zkout/<CONTRACT_FILENAME>
Example terminal output:
NOTE: Currently, until forge remappings
are implemented, import paths must be relative to the contract importing it:
In this example, SimpleFactory.sol
is in the src/is-system/
folder.
v0.0 Command:
Manage deployments in the native foundry/zkforge fashion, using the zkforge zk-create
command.
Deploy smart contracts to zksync.
Usage: zkforge zk-create <CONTRACT> [OPTIONS] --rpc-url <RPC-URL> --chain <CHAIN-ID> --private-key <PRIVATE-KEY>
Options:
-h, --help
Print help (see a summary with '-h')
ZkCreate options:
--constructor-args <ARGS>...
The constructor arguments.
--constructor-args-path <FILE>
The path to a file containing the constructor arguments.
<CONTRACT>
The contract identifier in the form `<path>:<contractname>`.
ZkSync Features:
--factory-deps <FACTORY-DEPS>...
The factory dependencies in the form `<path>:<contractname>`.
To Deploy src/Greeter.sol
to zksync local node:
../foundry-zksync/target/debug/zkforge zkc src/Greeter.sol:Greeter --constructor-args "ZkSync + Pineapple" --private-key 7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110 --rpc-url http://localhost:3050 --chain 270
Deploying contract...
+-------------------------------------------------+
Contract successfully deployed to address: 0xa1b809005e589f81de6ef9f48d67e35606c05fc3
Transaction Hash: 0x34782985ba7c70b6bc4a8eb2b95787baec29356171fdbb18608037a2fcd7eda8
Gas used: 168141
Effective gas price: 250000000
Block Number: 249
+-------------------------------------------------+
v0.0 Commands:
Interact with deployed contracts in the native foundry/zkforge fashion using the CLI zkcast zk-send
command:
Sign and publish a zksync transaction.
Usage: zkcast zk-send [OPTIONS] [TO] [SIG] [ARGS]...
Arguments:
[TO] The destination of the transaction.
[SIG] The signature of the function to call.
[ARGS]... The arguments of the function to call.
Options:
-h, --help Print help (see a summary with '-h')
Bridging options:
-d, --deposit For L1 -> L2 deposits.
-w, --withdraw For L2 -> L1 withdrawals.
--token <TOKEN> Token to bridge. Leave blank for ETH.
-a, --amount <AMOUNT> Amount of token to bridge. Required value when bridging
- Retrieving and interacting with chain data, for example, block numbers and gas estimates
- Interact with deployed contracts on (zkSync Testnet or Local Docker Node)
zkcast call <CONTRACT_ADDRESS> <FUNCTION_SIG> --rpc-url <RPC-URL>
../foundry-zksync/target/debug/zkcast call 0x97b985951fd3e0c1d996421cc783d46c12d00082 "greet()(string)" --rpc-url http://localhost:3050
ZkSync + Pineapple
zkcast zk-send <CONTRACT_ADDRESS> <FUNCTION_SIG> <FUNCTION_ARGS> --rpc-url <RPC-URL> --private-key <PRIVATE-KEY> --chain <CHAIN-ID>
../foundry-zksync/target/debug/zkcast zk-send 0x97b985951fd3e0c1d996421cc783d46c12d00082 "setGreeting(string)" "Killer combo!" --rpc-url http://localhost:3050 --private-key 7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110 --chain 270
Sending transaction....
Transaction Hash: 0x7651fba8ddeb624cca93f89da493675ccbc5c6d36ee25ed620b07424ce338552
../foundry-zksync/target/debug/zkcast call 0x97b985951fd3e0c1d996421cc783d46c12d00082 "greet()(string)" --rpc-url http://localhost:3050
Killer combo!
SimpleFactory.sol
must be compiled with the is-system
flag, so they need to be placed in the src/is-sytem/
folder
../foundry-zksync/target/debug/zkforge zk-build
../foundry-zksync/target/debug/zkforge zkc src/SimpleFactory.sol:SimpleFactory --constructor-args 01000041691510d85ddfc6047cba6643748dc028636d276f09a546ab330697ef 010000238a587670be26087b7812eab86eca61e7c4014522bdceda86adb2e82f --factory-deps src/Child.sol:Child src/StepChild.sol:StepChild --private-key 7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110 --rpc-url http://localhost:3050 --chain 270
Deploying contract...
+-------------------------------------------------+
Contract successfully deployed to address: 0xa1b809005e589f81de6ef9f48d67e35606c05fc3
Transaction Hash: 0x34782985ba7c70b6bc4a8eb2b95787baec29356171fdbb18608037a2fcd7eda8
Gas used: 168141
Effective gas price: 250000000
Block Number: 249
+-------------------------------------------------+
../foundry-zksync/target/debug/zkcast zk-send 0x23cee3fb585b1e5092b7cfb222e8e873b05e9519 "newStepChild()" --rpc-url http://localhost:3050 --private-key 7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110 --chain 270
Sending transaction....
Transaction Hash: 0xa82a0636b71af058d4916d81868eebc41173ca07b78d30fe57f4b74e9294ef25
../foundry-zksync/target/debug/zkcast call 0x23cee3fb585b1e5092b7cfb222e8e873b05e9519 "stepChildren(uint256)(address)" 0 --rpc-url http://localhost:3050
StepChild.sol
deployed address:
0xbc88C5Cdfe2659ebDD5dbb7e1a695A4cb189Df96
Use zkcast call
to check initial state:
../foundry-zksync/target/debug/zkcast call 0xbc88C5Cdfe2659ebDD5dbb7e1a695A4cb189Df96 "isEnabled()(bool)" --rpc-url http://localhost:3050
false
Use zkcast zk-send
to modify state:
../foundry-zksync/target/debug/zkcast zk-send 0xbc88C5Cdfe2659ebDD5dbb7e1a695A4cb189Df96 "enable()" --rpc-url http://localhost:3050 --private-key 7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110 --chain 270
Sending transaction....
Transaction Hash: 0xe005e15e9f58b7dcdcc7b16a9d5c706ddef7a4c9cab82216ea944d5344ba01ae
Use zkcast call
to check modified state:
../foundry-zksync/target/debug/zkcast call 0xbc88C5Cdfe2659ebDD5dbb7e1a695A4cb189Df96 "isEnabled()(bool)" --rpc-url http://localhost:3050
true
This section compiles, deploys and interacts with the contracts from the zkSync Era Account Abstraction Multisig example
Contracts:
AAFactory.sol
needs to be compiled with the --is-system
flag because it will be interacting with system contracts to deploy the multisig wallets.
It needs to be placed in the src/is-sytem/
folder
# command line using zkforge zk-build
../foundry-zksync/target/debug/zkforge zk-build
AAFactory -> Bytecode Hash: "010000791703a54dbe2502b00ee470989c267d0f6c0d12a9009a947715683744"
Compiled Successfully
To deploy the factory we need the Bytecode Hash
of the TwoUserMultiSig.sol
contract to provide to the constructor of AAFactory.sol
:
constructor(bytes32 _aaBytecodeHash) {
aaBytecodeHash = _aaBytecodeHash;
}
Note: aaBytecodeHash
= Bytecode hash of TwoUserMultiSig.sol
To deploy a contract that deploys other contracts it is necessary to provide the bytecodes of the children contracts in the factory-deps
field of the transaction. This can be accomplished by using the --factory-deps
flag and providing the full contract path in the format: <path>:<contractname>
# command line using zkforge zk-create
../foundry-zksync/target/debug/zkforge zkc src/is-system/AAFactory.sol:AAFactory --constructor-args 010007572230f4df5b4e855ff48d4cdfffc9405522117d7e020ee42650223460 --factory-deps src/TwoUserMultiSig.sol:TwoUserMultisig --private-key 7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110 --rpc-url http://localhost:3050 --chain 270
Deploying contract...
+-------------------------------------------------+
Contract successfully deployed to address: 0xd5608cec132ed4875d19f8d815ec2ac58498b4e5
Transaction Hash: 0x0e6f55ff1619af8b3277853a8f2941d0481635880358316f03ae264e2de059ed
Gas used: 154379
Effective gas price: 250000000
Block Number: 291
+-------------------------------------------------+
Now that we have the AAFactory.sol
contract address we can call deployAccount
function to deploy a new TwoUserMultiSig.sol
instance.
Here is the interface of deployAccount
:
function deployAccount(bytes32 salt, address owner1, address owner2) external returns (address accountAddress)
we need to provide the two owner addresses for the newly deployed multisig:
owner1 = 0xa61464658AfeAf65CccaaFD3a512b69A83B77618
owner2 = 0x0D43eB5B8a47bA8900d84AA36656c92024e9772e
We are also just using a 0x00
value for the salt parameter. (you will need a unique value for salt for each instance that uses same owner wallets)
# command line using zkcast zk-send
../foundry-zksync/target/debug/zkcast zk-send 0xd5608cec132ed4875d19f8d815ec2ac58498b4e5 "deployAccount(bytes32,address,address)(address)" 0x00 0xa61464658AfeAf65CccaaFD3a512b69A83B77618 0x0D43eB5B8a47bA8900d84AA36656c92024e9772e --rpc-url http://localhost:3050 --private-key 7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110 --chain 270
Sending transaction....
Transaction Hash: 0x43a4dded84a12891dfae4124b42b9f091750e953193bd779a7e5e4d422909e73
0x03e50ec034f1d363de0add752c33d4831a2731bf, <---- Deployed contract address
Viola! The new TwoUserMultiSig.sol
contract has been deployed to:
0x03e50ec034f1d363de0add752c33d4831a2731bf
We can check the tx receipt using zkcast tx <TX-HASH>
../foundry-zksync/target/debug/zkcast tx 0x22364a3e191ad10013c5f20036e9696e743a4f686bc58a0106ef0b9e7592347c --rpc-url http://localhost:3050
blockHash 0x2f3e2be46a7cb9f9e9df503903990e6670e88224e52232c988b5a730c82d98c0
blockNumber 297
from 0x36615Cf349d7F6344891B1e7CA7C72883F5dc049
gas 217367
gasPrice 250000000
hash 0x43a4dded84a12891dfae4124b42b9f091750e953193bd779a7e5e4d422909e73
input 0x76fb8b650000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a61464658afeaf65cccaafd3a512b69a83b776180000000000000000000000000d43eb5b8a47ba8900d84aa36656c92024e9772e
nonce 147
r 0x16385d99ccaaa5e84bb97d76a0afb310350c2ca4165ed41d458efa80cd76d3bd
s 0x3ec55287f223e760b7dd82a676feece939832e4c5a3d73f3aa979bd2cd48801c
to 0xd5608cEC132ED4875D19f8d815EC2ac58498B4E5
transactionIndex 0
v 1
value 0
l1BatchNumber 149
l1BatchTxIndex 0
We can verify by using zkcast call
to call the public variables 'owner1' and 'owner2' on the newly deployed TwoUserMultiSig.sol
contract:
Verify owner1
:
# command line using zkcast call
../foundry-zksync/target/debug/zkcast call 0x03e50ec034f1d363de0add752c33d4831a2731bf "owner1()(address)" --rpc-url http://localhost:3050
0xa61464658AfeAf65CccaaFD3a512b69A83B77618
Verify owner2
:
# command line using zkcast call
../foundry-zksync/target/debug/zkcast call 0x03e50ec034f1d363de0add752c33d4831a2731bf "owner2()(address)" --rpc-url http://localhost:3050
0x0D43eB5B8a47bA8900d84AA36656c92024e9772e
Make sure that:
- you are using zksync specific methods (
zkcreate
notcreate
,zksend
notsend
) - that you've set the correct
--rpc-url
- that you have the proper contract address - the bytecodes in zkSync are different than in EVM - so the resulting contracts will be deployed at different addresses
If you get errors like (code: -32601, message: Method not found, data: None)
- you are probably using a send
method instead of zksend
.