Hello world
Hello world
This guide shows you how to deploy a smart contract to zkSync and build a dApp that interacts with it using the zkSync development toolbox.
This is what we're going to do:
- Build, deploy, and verify a smart contract on zkSync Era testnet that stores a greeting message.
- Build a dApp that retrieves and updates the greeting message.
- Allow users to change the greeting message on the smart contract via the app.
- Show you how to implement the testnet paymaster that allows users to pay transaction fees with ERC20 tokens instead of ETH.
Prerequisites
- Make sure your machine satisfies the system requirements.
- Download and install Node.
- Download and install
nvm
to change the running Node version to latest use commandnvm use --lts
. - Use the
yarn
ornpm
package manager. We recommend usingyarn
. To installyarn
, follow the Yarn installation guide. - A wallet with sufficient Göerli
ETH
on L1 to pay for bridging funds to zkSync and deploying smart contracts. You can get Göerli ETH from the following faucets: - ERC20 tokens on zkSync are required for the testnet paymaster. Get testnet
ETH
for zkSync Era using bridges to bridge funds to zkSync. Use any Goerli swap to get the ERC 20 token you need in exchange for testnetETH
- for example Maverik Testnet Swap. - You know how to get your private key from your MetaMask wallet.
Local zkSync Testing with zksync-cli
Skip the hassle for test ETH by using zksync-cli
for local testing. Simply execute npx zksync-cli dev start
to initialize a local zkSync development environment, which includes local Ethereum and zkSync nodes. This method allows you to test contracts without requesting external testnet funds. Explore more in the zksync-cli documentation.
Build and deploy the Greeter contract
Project available in Atlas IDE
This entire tutorial can be run in under a minute using Atlas. Atlas is a smart contract IDE that lets you write, deploy, and interact with contracts from your browser. Open this project in Atlas.
Initialize the project
- Scaffold a new project by running the command:
npx zksync-cli create greeter-example --template hardhat_solidity
This creates a new zkSync Era project called greeter-example
with a basic Greeter
contract and all the zkSync plugins and configurations.
Hardhat plugins
Learn more about the zkSync Era plugins for Hardhat here
- Navigate into the project directory:
cd greeter-example
- Configure Your Private Key:
Rename the .env.example
file to .env
and then enter your private key:
WALLET_PRIVATE_KEY=YourPrivateKeyHere...
Your private key will be used for paying the costs of deploying the smart contract.
Compile and deploy the Greeter contract
We store all the smart contracts' *.sol
files in the contracts
folder. The deploy
folder contains all scripts related to deployments.
- The included
contracts/Greeter.sol
contract has following code:
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.8;
contract Greeter {
string private greeting;
constructor(string memory _greeting) {
greeting = _greeting;
}
function greet() public view returns (string memory) {
return greeting;
}
function setGreeting(string memory _greeting) public {
greeting = _greeting;
}
}
- Compile the contract with the following command:
yarn hardhat compile
- The zkSync-CLI provides a deployment script in
/deploy/deploy.ts
:
import { deployContract } from "./utils";
// An example of a basic deploy script
// It will deploy a Greeter contract to selected network
// as well as verify it on Block Explorer if possible for the network
export default async function () {
const contractArtifactName = "Greeter";
const constructorArguments = ["Hi there!"];
await deployContract(contractArtifactName, constructorArguments);
}
that utilizes the utils/deployContract
function for deploying contracts:
export const deployContract = async (contractArtifactName: string, constructorArguments?: any[], options?: DeployContractOptions) => {
const log = (message: string) => {
if (!options?.silent) console.log(message);
};
log(`\nStarting deployment process of "${contractArtifactName}"...`);
const wallet = options?.wallet ?? getWallet();
const deployer = new Deployer(hre, wallet);
const artifact = await deployer.loadArtifact(contractArtifactName).catch((error) => {
if (error?.message?.includes(`Artifact for contract "${contractArtifactName}" not found.`)) {
console.error(error.message);
throw `⛔️ Please make sure you have compiled your contracts or specified the correct contract name!`;
} else {
throw error;
}
});
// Estimate contract deployment fee
const deploymentFee = await deployer.estimateDeployFee(artifact, constructorArguments || []);
log(`Estimated deployment cost: ${formatEther(deploymentFee)} ETH`);
// Check if the wallet has enough balance
await verifyEnoughBalance(wallet, deploymentFee);
// Deploy the contract to zkSync
const contract = await deployer.deploy(artifact, constructorArguments);
const constructorArgs = contract.interface.encodeDeploy(constructorArguments);
const fullContractSource = `${artifact.sourceName}:${artifact.contractName}`;
// Display contract deployment info
log(`\n"${artifact.contractName}" was successfully deployed:`);
log(` - Contract address: ${contract.address}`);
log(` - Contract source: ${fullContractSource}`);
log(` - Encoded constructor arguments: ${constructorArgs}\n`);
if (!options?.noVerify && hre.network.config.verifyURL) {
log(`Requesting contract verification...`);
await verifyContract({
address: contract.address,
contract: fullContractSource,
constructorArguments: constructorArgs,
bytecode: artifact.bytecode,
});
}
return contract;
};
Run the deployment script with:
yarn hardhat deploy-zksync --script deploy.ts
Request-Rate Exceeded message
- This message is caused by using the default RPC endpoints provided by ethers.
- To avoid this, use your own Goerli RPC endpoint.
- Find multiple node providers here.
You should see something like this:
Starting deployment process of "Greeter"...
Estimated deployment cost: 0.0001089505 ETH
"Greeter" was successfully deployed:
- Contract address: 0xB127802183DEA4458D92CAF1319574d7e6534B8b
- Contract source: contracts/Greeter.sol:Greeter
- Encoded constructor arguments: 0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000094869207468657265210000000000000000000000000000000000000000000000
Requesting contract verification...
Your verification ID is: 47094
Contract successfully verified on zkSync block explorer!
Congratulations! You have deployed and verified a smart contract to zkSync Era Testnet 🎉
Now visit the zkSync block explorer and search with the contract address to confirm the deployment.
Build the front-end dApp
Set up the project
Info
- We use the
Vue
web framework for the tutorial front end (the process is similar to other frameworks). - In order to focus on the
zksync-web3
SDK, we provide a prebuilt template. - Once set up, we add code that interacts with the smart contract we just deployed.
- Clone the template and
cd
into the folder.
git clone https://github.com/matter-labs/tutorials
cd tutorials/hello-world/frontend
- Spin up the project.
yarn
yarn serve
Navigate to http://localhost:8080/
in a browser to see the running application.
Connect accounts to the dApp
Smart accounts
Enabling smart accounts allows you to onboard Argent account abstraction wallet users that have been using the first version of zkSync.
- Use this library to verify your smart account compatibility.
- Follow this guide to add Argent login to your dApp.
Externally owned accounts (EOAs)
In order to interact with dApps built on zkSync, connect the MetaMask wallet to the zkSync Era Testnet.
- Follow this guide to connect Metamask to zkSync.
Please note, that login functionality for "Hello, world" will be implemented in the next steps.
Bridge funds to L2
- Use bridges to bridge funds to zkSync.
- Use the third party faucets to get some test tokens in your account.
Note
When bridging from mainnet to a smart account (e.g. Argent) on zkSync Era, you must specify the address of your L2 wallet by clicking on Deposit to another address on zkSync Era Mainnet.
Project structure
In the ./src/App.vue
file, in the methods:
section, you will see template code that stores the application.
Most of the code is provided. You have to complete the TODO: sections.
methods: {
initializeProviderAndSigner() {
// TODO: initialize provider and signer based on `window.ethereum`
},
async getGreeting() {
// TODO: return the current greeting
return "";
},
async getFee() {
// TODO: return formatted fee
return "";
},
async getBalance() {
// Return formatted balance
return "";
},
async getOverrides() {
if (this.selectedToken.l1Address != ETH_L1_ADDRESS) {
// TODO: Return data for the paymaster
}
return {};
},
...
Beneath the <script>
tag, there are placeholders for the address of your deployed Greeter
contract: GREETER_CONTRACT_ADDRESS
, and the path to its ABI: GREETER_CONTRACT_ABI
.
<script>
// eslint-disable-next-line
const GREETER_CONTRACT_ADDRESS = ""; // TODO: insert the Greeter contract address here
// eslint-disable-next-line
const GREETER_CONTRACT_ABI = []; // TODO: Complete and import the ABI
Add the libraries
- From the
greeter-tutorial-starter
root, install the dependencies.
yarn add ethers@^5.7.2 zksync-web3
- Add the library imports under
<script>
inApp.vue
.
<script>
import {} from "zksync-web3";
import {} from "ethers";
// eslint-disable-next-line
const GREETER_CONTRACT_ADDRESS = ""; // TODO: insert the Greeter contract address here
// eslint-disable-next-line
const GREETER_CONTRACT_ABI = []; // TODO: Complete and import the ABI
Add the ABI and contract address
Info
- To interact with a smart contract deployed to zkSync, we need its ABI.
- ABI stands for Application Binary Interface and is json which describes the contract's variable and function, names and types.
Create the
./src/abi.json
file. You may find one in the repo, but it's good practice to use the one you created instead.Copy/paste the contract's ABI from the
./artifacts-zk/contracts/Greeter.sol/Greeter.json
file in the hardhat project folder from the previous section intoabi.json
. The file should look something like this:
[
{
"inputs": [
{
"internalType": "string",
"name": "_greeting",
"type": "string"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "greet",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "string",
"name": "_greeting",
"type": "string"
}
],
"name": "setGreeting",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]
- Set the
GREETER_CONTRACT_ABI
to require the ABI file, and add the Greeter contract address.
// eslint-disable-next-line
const GREETER_CONTRACT_ADDRESS = "0x...";
// eslint-disable-next-line
const GREETER_CONTRACT_ABI = require("./abi.json");
Working with a Web3 Provider
- Go to the
initializeProviderAndSigner
function in./src/App.vue
. This function is called after the connection to Metamask is successful.
In this function we should:
- Initialize a
Web3Provider
and aSigner
to interact with zkSync. - Initialize the
Contract
object to interact with theGreeter
contract we just deployed.
- Import the necessary dependencies under the imports from before:
import { Contract, Web3Provider, Provider } from "zksync-web3";
- Initialise the provider, signer, and contract instances like this:
initializeProviderAndSigner() {
this.provider = new Provider('https://testnet.era.zksync.dev');
// Note that we still need to get the Metamask signer
this.signer = (new Web3Provider(window.ethereum)).getSigner();
this.contract = new Contract(
GREETER_CONTRACT_ADDRESS,
GREETER_CONTRACT_ABI,
this.signer
);
},
Retrieving the greeting
Fill in the function to retrieve the greeting from the smart contract:
async getGreeting() {
// Smart contract calls work the same way as in `ethers`
return await this.contract.greet();
},
The function looks like this:
initializeProviderAndSigner() {
this.provider = new Provider('https://testnet.era.zksync.dev');
// Note that we still need to get the Metamask signer
this.signer = (new Web3Provider(window.ethereum)).getSigner();
this.contract = new Contract(
GREETER_CONTRACT_ADDRESS,
GREETER_CONTRACT_ABI,
this.signer
);
},
async getGreeting() {
return await this.contract.greet();
},
After connecting the Metamask wallet to zkSync Era Testnet, you should see the following page:
The Select token dropdown menu allows you to choose which token to pay fees with.
Retrieving token balance and transaction fee
The easiest way to retrieve the user's balance is to use the Signer.getBalance
function.
- Add the necessary dependencies to the same place you added imports before:
// `ethers` is only used in this tutorial for its utility functions
import { ethers } from "ethers";
- Implement the function:
async getBalance() {
// Getting the balance for the signer in the selected token
const balanceInUnits = await this.signer.getBalance(this.selectedToken.l2Address);
// To display the number of tokens in the human-readable format, we need to format them,
// e.g. if balanceInUnits returns 500000000000000000 wei of ETH, we want to display 0.5 ETH the user
return ethers.utils.formatUnits(balanceInUnits, this.selectedToken.decimals);
},
- Estimate the fee:
async getFee() {
// Getting the amount of gas (gas) needed for one transaction
const feeInGas = await this.contract.estimateGas.setGreeting(this.newGreeting);
// Getting the gas price per one erg. For now, it is the same for all tokens.
const gasPriceInUnits = await this.provider.getGasPrice();
// To display the number of tokens in the human-readable format, we need to format them,
// e.g. if feeInGas*gasPriceInUnits returns 500000000000000000 wei of ETH, we want to display 0.5 ETH the user
return ethers.utils.formatUnits(feeInGas.mul(gasPriceInUnits), this.selectedToken.decimals);
},
Now, when you select the token to pay the fee, the balance and the expected fee for the transaction are available.
Click Refresh to recalculate the fee. The fee depends on the length of the message we want to store as the greeting.
It is possible to also click on the Change greeting button, but nothing will happen yet as we haven't implemented the function.
Updating the greeting
Update the function in App.vue
with the following code:
async changeGreeting() {
this.txStatus = 1;
try {
const txHandle = await this.contract.setGreeting(this.newGreeting, await this.getOverrides());
this.txStatus = 2;
// Wait until the transaction is committed
await txHandle.wait();
this.txStatus = 3;
// Update greeting
this.greeting = await this.getGreeting();
this.retreivingFee = true;
this.retreivingBalance = true;
// Update balance and fee
this.currentBalance = await this.getBalance();
this.currentFee = await this.getFee();
} catch (e) {
alert(JSON.stringify(e));
}
this.txStatus = 0;
this.retreivingFee = false;
this.retreivingBalance = false;
},
Now you can add a new greeting message and send it to the contract via a transaction with MetaMask. You will see the Greeter message change on the front end.
You now have a fully functional Greeter-dApp! However, it does not yet leverage any zkSync-specific features.
Note
Do you see a wallet_requestPermissions error?
Refresh your browser, or open the MetaMask extension on your browser and click Next or Cancel to resolve it.
Read more about wallet_requestPermissions
, in the MetaMask documentation.
Paying fees using testnet paymaster
The zkSync Era account abstraction feature allows you to integrate paymasters that can pay the fees entirely for you, or swap your tokens on the fly.
We will use the testnet paymaster that is provided on all zkSync Era testnets.
Info
The testnet paymaster allows users to pay fees in any ERC20 token with the exchange rate of Token:ETH of 1:1, i.e. one unit of the token for one wei of ETH.
This means that transaction fees in tokens with fewer decimals than ETH will be bigger; for example, USDC which has only 6 decimals. This is a known behaviour of the testnet paymaster, which was built for demonstration purposes only.
Paymasters on mainnet
The testnet paymaster is purely for demonstrating this feature and won't be available on mainnet.
When integrating your protocol on mainnet, you should follow the documentation of the paymaster you use, or create your own.
The getOverrides
function returns an empty object when users decide to pay with Ether but, when users select the ERC20 option, it should return the paymaster address and all the information required by it. This is how to do it:
- To retrieve the address of the testnet paymaster from the zkSync provider, add a new function
getOverrides
:
async getOverrides() {
if (this.selectedToken.l1Address != ETH_L1_ADDRESS) {
const testnetPaymaster = await this.provider.getTestnetPaymasterAddress();
// ..
}
return {};
}
Info
It is recommended to retrieve the testnet paymaster's address before each interaction as it may change.
- Add
utils
to the same import fromzksync-web3
SDK as before:
import { Contract, Web3Provider, Provider, utils } from "zksync-web3";
- We need to calculate how many tokens are required to process the transaction. Since the testnet paymaster exchanges any ERC20 token to ETH at a 1:1 rate, the amount is the same as the ETH amount in wei:
async getOverrides() {
if (this.selectedToken.l1Address != ETH_L1_ADDRESS) {
const testnetPaymaster = await this.provider.getTestnetPaymasterAddress();
const gasPrice = await this.provider.getGasPrice();
// estimate gasLimit via paymaster
const paramsForFeeEstimation = utils.getPaymasterParams(
testnetPaymaster,
{
type: "ApprovalBased",
minimalAllowance: ethers.BigNumber.from("1"),
token: this.selectedToken.l2Address,
innerInput: new Uint8Array(),
}
);
// estimate gasLimit via paymaster
const gasLimit = await this.contract.estimateGas.setGreeting(
this.newGreeting,
{
customData: {
gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT,
paymasterParams: paramsForFeeEstimation,
},
}
);
const fee = gasPrice.mul(gasLimit);
// ..
}
return {};
}
- Now, what is left is to encode the paymasterInput following the protocol requirements and return the needed overrides.
Copy/paste the following complete function:
async getOverrides() {
if (this.selectedToken.l1Address != ETH_L1_ADDRESS) {
const testnetPaymaster =
await this.provider.getTestnetPaymasterAddress();
const gasPrice = await this.provider.getGasPrice();
// estimate gasLimit via paymaster
const paramsForFeeEstimation = utils.getPaymasterParams(
testnetPaymaster,
{
type: "ApprovalBased",
minimalAllowance: ethers.BigNumber.from("1"),
token: this.selectedToken.l2Address,
innerInput: new Uint8Array(),
}
);
// estimate gasLimit via paymaster
const gasLimit = await this.contract.estimateGas.setGreeting(
this.newGreeting,
{
customData: {
gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT,
paymasterParams: paramsForFeeEstimation,
},
}
);
const fee = gasPrice.mul(gasLimit.toString());
const paymasterParams = utils.getPaymasterParams(testnetPaymaster, {
type: "ApprovalBased",
token: this.selectedToken.l2Address,
minimalAllowance: fee,
// empty bytes as testnet paymaster does not use innerInput
innerInput: new Uint8Array(),
});
return {
maxFeePerGas: gasPrice,
maxPriorityFeePerGas: ethers.BigNumber.from(0),
gasLimit,
customData: {
gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT,
paymasterParams,
},
};
}
return {};
},
- To use a list of ERC20 tokens, change the following line:
const allowedTokens = require("./eth.json");
to the following one:
const allowedTokens = require("./erc20.json");
The erc20.json
file contains a few tokens like DAI, USDC and wBTC.
Complete app
Now you should be able to update the greeting message with ETH or any of the available tokens.
- Select one of the ERC20 tokens to see the estimated fee:
- Click on the
Change greeting
button to update the message. Since thepaymasterParams
were supplied, the transaction will be anEIP712
(more on EIP712 here):
- Click "Sign" to send the transaction.
After the transaction is processed, the page updates the balances and the new greeting can be viewed.
You've paid for this transaction with an ERC20 token using the testnet paymaster 🎉
Learn more
- For an overview of security and best practices for developing on zkSync Era, refer to the Security and best practices page.
- To learn more about
zksync-web3
SDK, check out its documentation. - To learn more about the zkSync hardhat plugins, check out their documentation.