Contract Deployment and Interaction
This guide shows you how to deploy a smart contract to ZKsync Era
and call its methods using the zksync-web3-rs
SDK.
This is what we're going to do:
- Deploy, and verify a smart contract on ZKsync Sepolia Testnet that stores a greeting message.
- Call a
view
method that retrieves the greeting message. - Perform a transaction that updates the greeting message.
This entire tutorial can be easily run using
cargo run --example contract-deployment
which is available under the examples/
directory in the zksync-web3-rs
.Prerequisites
This tutorial assumes that you know how to create a rust project and add zksync-web3-rs
as a dependency.
We recommend having followed the Rust SDK Getting started tutorial first.
Also, since our SDK does not provide wrappers for the compiler, building the Greeter
contract is out of the scope
of this tutorial. We will provide the ABI and compilation output together with the source code for the smart contract.
The Greeter contract
In this tutorial, we are going to work with the Greeter.sol
contract.
//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;
}
}
Connecting to ZKsync Era
To interact with the ZKsync Era network users need to know the endpoint of the operator node. In this tutorial,
we will be using the ZKsync Era In-Memory Node from matter-labs/era-test-node
.
The test node a ZKsync Era node (L2) on port 8011
. You can connect to the ZKsync Era
network using the following code:
use std::str::FromStr;
use zksync_web3_rs::providers::{Middleware, Provider};
use zksync_web3_rs::signers::{LocalWallet, Signer};
use zksync_web3_rs::ZKSWallet;
// This is the default url for a local `era-test-node` instance.
static ERA_PROVIDER_URL: &str = "http://127.0.0.1:8011";
// This is the private key for one of the rich wallets that come bundled with the era-test-node.
static PRIVATE_KEY: &str = "7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110";
let zk_wallet = {
let era_provider = Provider::try_from(ERA_PROVIDER_URL).unwrap();
let chain_id = era_provider.get_chainid().await.unwrap();
let l2_wallet = LocalWallet::from_str(PRIVATE_KEY)
.unwrap()
.with_chain_id(chain_id.as_u64());
ZKSWallet::new(l2_wallet, None, Some(era_provider.clone()), None).unwrap()
};
Deploy the Greeter contract
Before we begin, you'll need the compiled contract bytecode ./Greeter.bin
and
its Application Binary Interface (ABI) ./Greeter.abi
. These files define the contract's code and
its interface, respectively.
- The ABI file defines the interface and methods of the smart contract. Download it from here
and place it on
./Greeter.abi
. - The bytecode file contains the compiled binary code of the Greeter smart contract.
Download it from here and place it on
./Greeter.bin
. - Use the following snippet to deploy the contract to ZKsync Era.
use ethers::abi::Abi;
use zksync_web3_rs::zks_wallet::DeployRequest;
static CONTRACT_BIN: &str = include_str!("./Greeter.bin");
static CONTRACT_ABI: &str = include_str!("./Greeter.abi");
// Deploy contract:
let contract_address = {
// Read both files from disk:
let abi = Abi::load(CONTRACT_ABI.as_bytes()).unwrap();
let contract_bin = hex::decode(CONTRACT_BIN).unwrap().to_vec();
// DeployRequest sets the parameters for the constructor call and the deployment transaction.
let request = DeployRequest::with(abi, contract_bin, vec!["Hey".to_owned()])
.from(zk_wallet.l2_address());
// Send the deployment transaction and wait until we receive the contract address.
let address = zk_wallet.deploy(&request).await.unwrap();
println!("Contract address: {:#?}", address);
address
};
Calling the greet()
view method
In this section, we'll show you how to interact with the Greeter smart contract that you previously
deployed to ZKsync Era. Specifically, we'll demonstrate how to call the greet()
view method of
the contract, which allows you to retrieve a greeting message from the blockchain.
The following Rust code snippet demonstrates how to call the greet()
method and display the returned message:
use zksync_web3_rs::zks_wallet::CallRequest;
// Call the greet view method:
{
let era_provider = zk_wallet.get_era_provider().unwrap();
let call_request = CallRequest::new(contract_address, "greet()(string)".to_owned());
let greet = ZKSProvider::call(era_provider.as_ref(), &call_request)
.await
.unwrap();
println!("greet: {}", greet[0]);
}
Updating the Greeting Message with setGreeting
In this section, we'll guide you through the process of updating the greeting message stored in
the Greeter smart contract. The setGreeting
method is used to change the greeting
message.
Since it alters the state of the contract on the blockchain, it requires a signed transaction.
The following code snippet sends a signed transaction that calls the setGreeting
method.
The transaction is signed using the l2_wallet
stored in the ZKSWallet
.
// Perform a signed transaction calling the setGreeting method
{
let receipt = zk_wallet
.get_era_provider()
.unwrap()
.clone()
.send_eip712(
&zk_wallet.l2_wallet,
contract_address,
"setGreeting(string)",
Some(["Hello".into()].into()),
None,
)
.await
.unwrap()
.await
.unwrap()
.unwrap();
println!(
"setGreeting transaction hash {:#?}",
receipt.transaction_hash
);
};
After successfully updating the greeting message, you may want to confirm that the change has taken effect. To do this, we call the greet() method again and display the updated greeting message:
{
let era_provider = zk_wallet.get_era_provider().unwrap();
let call_request = CallRequest::new(contract_address, "greet()(string)".to_owned());
let greet = ZKSProvider::call(era_provider.as_ref(), &call_request)
.await
.unwrap();
println!("greet: {}", greet[0]);
}
Advanced Interaction with Smart Contracts Using Rust
In this section, we will explore a more complex scenario involving interactions with a smart contract.
We will demonstrate how to read a struct called proposal comprised of multiple fields of different types
including different integer types.
We also introduce a nested map to record the status of what we call voting
on proposal
and demonstrate how you can retrieve its value for a particular address.
Smart Contract Setup in Solidity
First, let's suppose the corresponding data structures and mappings are defined as follows in your Solidity contract:
// Defines a proposal specifying payment amount, target address, vote count, and execution status.
struct Proposal {
address target;
uint256 amount;
uint8 votes;
bool executed;
}
// Mapping from a unique identifier (bytes32) to a Proposal.
mapping(bytes32 => Proposal) public proposals;
// Nested mapping to track whether an address has voted on a proposal and the content of the vote (yes/no).
mapping(address => mapping(bytes32 => bool)) public voted;
Contract Interaction in Rust
To interact with these contract elements, we use the ethers-rs library. Here's how to set up the Rust structures and make calls to the contract:
use ethers::abi::Uint;
use ethers::contract::EthAbiType;
use ethers::types::Address;
// Rust representation of the Solidity Proposal struct.
// Note `uint8` type for `votes` is also seen as `Uint` here.
#[derive(Debug, Default, PartialEq, Eq, EthAbiType)]
pub struct Proposal {
target: Address,
amount: Uint,
votes: Uint,
executed: bool,
}
// ABI strings for interacting with the contract functions.
pub const CONTRACT_PROPOSALS_CALL: &str = "proposals(bytes32)(address,uint256,uint8,bool)";
// Note how the public getter for a nested map in solidity is constructed below.
pub const CONTRACT_VOTED_CALL: &str = "voted(address,bytes32)(bool)";
Calling the Contract
Here is an example of how you can read a proposal and check the voting status:
use ethers::abi::Detokenize;
async fn interact_with_contract() {
// Construct parameters for the proposals call.
let proposals_parameters = vec![a_unique_proposal_identifier_for_our_example.clone()];
let proposals_call_request = CallRequest::new(
contract_address,
CONTRACT_PROPOSALS_CALL.to_string()
).function_parameters(proposals_parameters);
// Make the call to fetch the proposal details.
let proposal_tokens = ZKSProvider::call(era_provider.as_ref(), &proposals_call_request)
.await
.unwrap();
let proposal = Proposal::from_tokens(proposal_tokens)?;
println!("proposal: {:?}", proposal);
// Assume you are checking if your own wallet has voted.
let voted_parameters = vec![
hex::encode(zks_wallet.l2_address()),
a_unique_proposal_identifier_for_our_example.clone(),
];
let voted_call_request = CallRequest::new(
contract_address,
CONTRACT_VOTED_CALL.to_string()
).function_parameters(voted_parameters);
// Make the call to check the voting status.
let voted_tokens = ZKSProvider::call(era_provider.as_ref(), &voted_call_request)
.await
.unwrap();
let voted = bool::from_tokens(voted_tokens).unwrap();
println!("I have voted on the proposal? {}", voted);
}