Installing Foundry
On your command line, run:
curl -L https://raw.githubusercontent.com/matter-labs/foundry-zksync/main/install-foundry-zksync | bash
Create a New Project
forge init
forge build --zksync
This will create a new simple Foundry project with a Counter
contract.
Set Configs
A full baseline Foundry example can be found on our GitHub
here.
Set your foundry.toml
and .env
as follows:
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
auto_detect_solc = true
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
[etherscan]
sepoliaTestnet = { key = "${ETHERSCAN_API_KEY}", url = "${SEPOLIA_VERIFIER_URL}", chain = 11155111 }
testnet = { key = "${ETHERSCAN_API_KEY}", url = "${SOPHON_TESTNET_VERIFIER_URL}", chain = 531050104 }
sophon = { key = "${ETHERSCAN_API_KEY}", url = "${SOPHON_VERIFIER_URL}", chain = 50104 }
ethereum = { key = "${ETHERSCAN_API_KEY}", url = "${VERIFIER_URL}", chain = 1 }
[rpc_endpoints]
sepoliaTestnet = "${SEPOLIA_RPC_URL}"
testnet = "${SOPHON_TESTNET_RPC_URL}"
sophon = "${SOPHON_RPC_URL}"
ethereum = "${RPC_URL}"
In order to obtain a Sophscan API Key, you need to create an account and get an API Key
here.
Counter Contract Deployment
Basic Deployment
If you run into deployment or interaction issues, always check for possible short-circuit errors
caused by environment variables not being properly exposed to the foundry script. You can do so by
running source .env
before running the script or check the variable in your env by running echo $VARIABLE_NAME
.
source .env && forge create ./src/Counter.sol:Counter \
--rpc-url testnet --private-key $PRIVATE_KEY --zksync
Deployment and Verification
Same as Basic Deployment but you append the --verify
flag
This will only verify on Sophscan
source .env && forge create ./src/Counter.sol:Counter \
--rpc-url testnet --private-key $PRIVATE_KEY --zksync --verify
Deployment Using Paymaster
Same as Basic Deployment but you append the --zk-paymaster-address
flag
The paymaster allows you to perform gasless transactions, so there’s no need to have a SOPH
balance
source .env && forge create ./src/Counter.sol:Counter \
--rpc-url testnet --private-key $PRIVATE_KEY --zksync \
--zk-paymaster-address $PAYMASTER_ADDRESS \
--zk-paymaster-input $(cast calldata "general(bytes)" "0x")
Deployment With Paymaster and Verification (All-in-One)
source .env && forge create ./src/Counter.sol:Counter \
--rpc-url testnet --private-key $PRIVATE_KEY --zksync \
--zk-paymaster-address $PAYMASTER_ADDRESS \
--zk-paymaster-input $(cast calldata "general(bytes)" "0x") --verify
Deploy Contracts Using Foundry Scripts
Deployment Using Script
source .env && forge script ./script/Counter.s.sol \
--rpc-url testnet --private-key $PRIVATE_KEY --zksync --broadcast
Deployment Using Script and Verification
Same as Deployment Using Script but you append the --verify
flag
This will only verify on Sophscan
source .env && forge create ./src/Counter.sol:Counter \
--rpc-url testnet --private-key $PRIVATE_KEY --zksync --verify
Deployment Using Script With Paymaster
Same as Deployment Using Script but you append the --zk-paymaster-address
flag
This will only verify on Sophscan
To use the paymaster within a script, you have 2 options: using a cheatcode or making a low-level call:
Using a Foundry Cheatcode
-
Install forge-zksync-std library
-
Use the vmExt.zkUsePaymaster cheatcode
-
For your convenience, you can copy-paste the following script:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Script, console} from "forge-std/Script.sol";
import {Counter} from "../src/Counter.sol";
import {TestExt} from "lib/forge-zksync-std/src/TestExt.sol";
contract CounterScript is Script, TestExt {
Counter public counter;
function setUp() public {}
function run() public {
vm.startBroadcast();
// Encode paymaster input
bytes memory paymaster_encoded_input = abi.encodeWithSelector(
bytes4(keccak256("general(bytes)")),
bytes("0x")
);
vmExt.zkUsePaymaster(vm.envAddress("PAYMASTER_ADDRESS"), paymaster_encoded_input);
counter = new Counter();
vm.stopBroadcast();
}
}
You can now run the following command
source .env && forge script ./script/Counter.s.sol \
--rpc-url testnet --private-key $PRIVATE_KEY --zksync --broadcast
Using a Low-Level Call (in Case You Can’t Install the Cheatcode Library)
For your convenience, you can copy-paste the following script:
Here we use address(vm).call
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Script, console} from "forge-std/Script.sol";
import {Counter} from "../src/Counter.sol";
import {TestExt} from "lib/forge-zksync-std/src/TestExt.sol";
contract CounterScript is Script, TestExt {
Counter public counter;
function setUp() public {}
function run() public {
vm.startBroadcast();
// Encode paymaster input
bytes memory paymaster_encoded_input = abi.encodeWithSelector(
bytes4(keccak256("general(bytes)")),
bytes("0x")
);
(bool success, ) = address(vm).call(
abi.encodeWithSignature(
"zkUsePaymaster(address,bytes)",
vm.envAddress("PAYMASTER_ADDRESS"),
paymaster_encoded_input
)
);
require(success, "zkUsePaymaster() call failed");
counter = new Counter();
vm.stopBroadcast();
}
}
You can now run the following command:
source .env && forge script ./script/Counter.s.sol \
--rpc-url testnet --private-key $PRIVATE_KEY --zksync --broadcast
Deployment Using Script With Paymaster and Verification
Same as Deployment Using Script With Paymaster but you append the --verify
flag
This will only verify on Sophscan
source .env && forge script ./script/Counter.s.sol \
--rpc-url testnet --private-key $PRIVATE_KEY --zksync --broadcast --verify
Contract Verification
If you want to verify an already deployed contract, you can do so on Sophscan (an Etherscan-like explorer) and/or on Sophon’s Explorer.
On Sophscan
source .env && forge verify-contract COUNTER_CONTRACT_ADDRESS \
./src/Counter.sol:Counter:Counter --watch --rpc-url testnet --zksync
On Sophon’s Explorer
forge verify-contract COUNTER_CONTRACT_ADDRESS ./src/Counter.sol:Counter \
--watch --verifier zksync --verifier-url $TESTNET_VERIFIER_URL --zksync
Using Libraries
If your contract relies on external libraries, you need to build it with the linked libraries during deployment. For example:
forge build --zksync --libraries ./contracts/util/SignatureChecker.sol:SignatureChecker:0xb0A2cf27Bf984bd0c56dCFb37C9DA0F2c5028844
This ensures the library is properly linked during the build and deploy process, enabling verification to succeed.
Contracts With Constructor Params
If your contract receives constructor params, you can use the --constructor-args
flag:
Modify your Counter contract to receive a constructor param:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
contract Counter {
uint256 public number;
constructor(uint256 _number) {
number = _number;
}
function setNumber(uint256 newNumber) public {
number = newNumber;
}
function increment() public {
number++;
}
}
You can now run:
forge create ./src/CounterWithConstructorParams.sol:Counter \
--rpc-url testnet --private-key $PRIVATE_KEY --zksync \
--constructor-args "$(cast abi-encode "constructor(uint256)" "123")"
To verify or use the paymaster, you can use the flags --verify
and/or ----zk-paymaster-address
as explained above.
Check if a contract is whitelisted by the paymaster
Before making calls to a newly deployed contract using a paymaster, it’s important to verify that the contract has been added to the paymaster’s whitelist. Our automated contract whitelisting service typically takes 2-3 seconds to whitelist new contracts under normal network conditions. While optional, checking the whitelist status before proceeding makes deployment scripts more robust by ensuring contracts are ready to interact with the paymaster.
The code below provides a reference implementation for checking the whitelist status:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Script, console} from "forge-std/Script.sol";
import {Counter} from "../src/Counter.sol";
import {TestExt} from "lib/forge-zksync-std/src/TestExt.sol";
interface IRestrictionContract {
function contractWhitelist(address) external view returns (bool);
}
contract CounterScript is Script, TestExt {
Counter public counter;
function setUp() public {}
function run() public {
vm.startBroadcast();
address restrictionContractAddress = vm.envAddress("RESTRICTION_CONTRACT");
address paymasterAddress = vm.envAddress("PAYMASTER_ADDRESS");
uint256 waitTime = 5_000; // wait time in milliseconds
uint256 maxAttempts = 5;
bool isWhitelisted = false;
uint256 attempts = 0;
if (restrictionContractAddress != address(0)) {
IRestrictionContract restrictionContract = IRestrictionContract(restrictionContractAddress);
while (!isWhitelisted && attempts < maxAttempts) {
try restrictionContract.contractWhitelist(address(this)) returns (bool whitelisted) {
isWhitelisted = whitelisted;
if (isWhitelisted) {
console.log("Contract is whitelisted on restriction contract!");
break;
} else {
attempts++;
if (attempts < maxAttempts) {
console.log("Contract not yet whitelisted. Waiting before retrying...");
vm.sleep(waitTime);
} else {
console.log("Contract still not whitelisted after max attempts. Proceeding anyway.");
}
}
} catch {
console.log("Error checking whitelist status. Retrying...");
attempts++;
if (attempts < maxAttempts) {
vm.sleep(waitTime);
}
}
}
}
// Encode paymaster input
bytes memory paymaster_encoded_input = abi.encodeWithSelector(
bytes4(keccak256("general(bytes)")),
bytes("0x")
);
(bool success, ) = address(vm).call(
abi.encodeWithSignature(
"zkUsePaymaster(address,bytes)",
paymasterAddress,
paymaster_encoded_input
)
);
require(success, "zkUsePaymaster() call failed");
counter = new Counter();
vm.stopBroadcast();
}
}