Smart Contract wallets have become very popular with the rise of account abstraction. Account abstraction allows you implementing any signature verification method, enabling your smartphones and computers to become transaction signers, instead of 12-word seed phrases everyone used to have. Additionally, many other programmable features can be attached to build more complex features (e.g spending limits).
In this article we are going to build a very simple smart account wallet on ZKsync, using native Account Abstraction and Passkeys. In general, the smart wallet we create will have following features:
Biometric authentication with Passkeys
Simple send money feature, supporting various tokens
Gasless transactions using Paymaster
Multicall to get multiple token balances from chain
Fully on-chain, without any need for off-chain server
As a smart contract architecture, we will be using Clave contracts. These account contracts are audited and production ready components for building any simple or complex wallet architecture. They are pretty similar to ERC-7579 smart contracts, but with a slightly different interface. The reason behind the difference is that Clave contracts were ready for production way before than the popularity of this standard. You can check out the audit reports of the contracts from the GitHub repository.
We will be focusing on more core and simple parts, which starts with deploying and ends with sending a simple transaction. Clave contracts are compatible with Passkeys and Figure 1 depicts how the signed transactions are reflected in blockchain.

The first step in building a smart wallet is of course deploying the smart contracts to blockchain we are using - ZKsync in our case. If you are not familiar with ZKsync, it is a zero-knowledge rollup on top of Ethereum. If you need to add the network to your wallet before deployment, you can do it from Chainlist.
https://chainlist.org/chain/324
Since we are going to use the Clave contracts, we need to clone the related repository from Github and proceed to deployment process. Here is the repository link on Github:
https://github.com/getclave/clave-contracts
After you clone the repository, you need to install the dependencies by running the install command with your package manager. I will be using npm since it is the most basic and compatible one. Running the following command will install the required dependencies.
As also written in the README.md of the clave-contracts repo, we need to set the private key for deployment on .env file. Since it is dangerous to push .env file to GitHub, it is ignored from git using .gitignore. Create a .env file on root of the cloned repository and add your private key in the following format:
PRIVATE_KEY=********** // Replace stars with your actual private key
Before moving to deployment step, you need to compile the smart contracts in your local machine. You need to run the following command to compile the smart contracts with hardhat:
npm run compile
I prepared a simple deployment script, called deploy-mvp.ts for most viable product (MVP). It will deploy the following modules for you and print you the addresses of contracts in the end.
BatchCaller - Used for sending batch transactions. We will not use this module, but it is required by some other modules. Therefore, we need to deploy it too.
Implementation - Main implementation for account
Registry - Used to record the deployed user accounts
GaslessPaymaster - Smart contract that helps implementing gasless transactions
Proxy - Proxy contracts are mostly used to have an upgradable structure
PasskeyValidator - Smart contract that validates Passkey signatures
Factory - Smart contract that creates (deploys) accounts
Please make sure that you have enough ETH balance on your wallet used for deployment.
You need to run the following command to deploy the contracts:
npm run deploy:mvp
The default network for the repository is ZKsync Sepolia. To deploy the contracts in ZKsync Era, you need to explicitly pass the network name. The command will be
npm run deploy:mvp -- --network zkSyncMainnet
After waiting for ~1 minute, you will get all the contract addresses in the form of object. The example output should be like below (of course, you will have different addresses):
{
batchCaller: '0x1513dB8DdC9420728bFb2830AE6784B26Ac9bf25',
implementation: '0x5627beD3bA7DFc5D9DbAa0122A52C7F22a2DD4D3',
registry: '0x7f273AF2576EA32309c32c9bae2b609B6e4484aC',
gaslessPaymaster: '0xF83F534153358AD6643B358AC3953f6467d5DAe7',
claveProxy: '0x3b633b071ABFf838d30D1a326744D8277Fad468c',
passkeyValidator: '0xDA63bBbc0A1a3F94e95c6bdd2DCB7B7112e3C635',
accountFactory: '0x281d01350B4449D6F4B3a58ce7F342c5221E1636',
}
That is it, you are done with the contracts side and good to go to next step - building the user interface and interacting with smart contracts using ethers.js
As explained, you are sending gasless transactions using the GaslessPaymaster contract. However, this contract should have ETH balance to pay gas instead of users. Therefore, you just need to send some Ether to this contract using any wallet.
Alternatively, you can also follow these steps:
Copy your gasless paymaster address after deployment. It is defined with
gaslessPaymasterkey on the returned object response of deploy:mvp command.Go to your wallet and select the network you deployed this contract to. In our case it is ZKsync Sepolia
Transfer some Ether amount. I will transfer around
$25worth of Ether, which will be more than enough for testing.

I have created a simple web application that implements client-side interactions with smart contracts we deployed. It is built with NextJS. NextJS is a very popular React framework for building interactive websites with high complexity. The system requirements for the NextJS are as below:
Node.js 18.18 or later. You can check your node version by running
node --versionmacOS, Windows (including WSL), and Linux are supported.
You can check out more about the NextJS from the link below.
Here is the GitHub repository link for the client application:
https://github.com/getclave/zksync-smart-wallet
After you clone the repository, you need to install the dependencies by running the install command with your package manager. I will again use npm
If you are curious about which packages we use for client application, here is the list:
@passwordless-id/webauthn - Working with Passkeys (Webauthn)
ethers - Simplifying contract interactions and cryptographic operations like hashing. We are using ethers version 5, since it is used by more people based on
npmdata.@tanstack/react-query - Handling RPC calls
react-icons - Easily displaying SVG icons
recoil - State management for React
zksync-ethers - Pretty much similar with ethers, but also contains some ZKsync Native AA interface features
cbor - Encode and parse data in the Concise Binary Object Representation (CBOR) data format (RFC8949).
react-qrcode-logo - Displaying Address QR Code
Since the Clave smart accounts need to be deployed during the registration, we need to setup the deployer private key on NextJS side too. NextJS provides a way sending API requests with its own router and we can safely put the deployer private key to our client .env file. Please make sure that your private key is the same with private key used for deploying smart contracts on section 2.3.
Firstly, create an .env file on the client repository and use the following format:
DEPLOYER_PRIVATE_KEY=****** // Replace stars with your actual private key
On section 2.5, we got the output of our deployed smart contracts. To setup those addresses on your client application, you need to follow these steps:
Copy the your deployed contracts output object
Go to
src/utils/contract.tsReplace the
contractsvariable with your addresses. Pasting the object will be enough (also visible on Figure 3)

You can start the client development server by running the command below. It will start the development server on https://localhost:3000
npm run dev
In this section, I will explain the logic of sending transaction. The only thing you should now about preparing transactions is that you should use Core util, located at src/utils/core.ts. The singleton class instance is exported, so that you can use the same core object everywhere.
To prepare the transaction you need to use the
getTransactionmethodTo sign and send the transaction you need to call
signAndSendmethod from output ofgetTransaction
Here is the example of getting a transaction instance, also sending it to network:
import { core } from '@/utils';
// Prepare transaction
const tx = await core.getTransaction({
to: "0xc1ECfC78959484df5472b20Cb7D43dC8c57C767A",
value: ethers.utils.parseEther("0.001"),
});
// Send transaction to ZKsync network
await tx.signAndSend();
The code above gets a transaction for sending 0.001 ETH to address 0xc1ECfC78959484df5472b20Cb7D43dC8c57C767A.
If you want to call a smart contract method, you need one additional step of preparing calldata of your transaction. This is possible with the following code block.
import { abiErc20, core } from '@/utils';
const USDC_ADDRESS = "0x235171e45abff2a15d117e3179df4cc35ebfae2f";
const USDC_DECIMALS = 6;
const RECEIVER = "0xc1ECfC78959484df5472b20Cb7D43dC8c57C767A";
const AMOUNT = "1";
// Prepare calldata
const calldata = core.getCalldata({
abi: ERC20_ABI,
method: 'transfer',
args: [
RECEIVER,
ethers.utils.parseUnits(AMOUNT, USDC_DECIMALS),
],
});
// Prepare transaction
const tx = await core.getTransaction({
to: selectedToken.address,
data: calldata,
});
// Send transaction to ZKsync network
await tx.signAndSend();
The code block above calls the transfer function from USDC contract to send 1 USDC to RECEIVER address.
There are some cases where you may want to send multiple transactions within the same transaction - batch transactions. Batch transactions are only possible with smart contract wallets, and currently, they are the main thing that makes Clave a perfect account abstraction wallet. The following core block allows you batching multiple transactions into a single one:
const RECEIVER_1 = "0xc1ECfC78959484df5472b20Cb7D43dC8c57C767A"
const RECEIVER_2 = "0x94E9b636d0f3BDc08019B450F7f2F4Ef5b4eb2Ca"
const AMOUNT = "0.001"
// Prepare transaction
const tx = await core.getBatchTransaction(
{
to: RECEIVER_1,
value: ethers.utils.parseEther(AMOUNT),
},
{
to: RECEIVER_2,
value: ethers.utils.parseEther(AMOUNT),
},
);
// Send transaction to ZKsync network
await tx.signAndSend();
The code above gets a batch transaction for sending 0.001 ETH to RECEIVER_1 and RECEIVER_2 addresses. You only sign once and send multiple transactions at the same time. By and also setting calldata parameter, you can call multiple methods from multiple smart contracts to make the user experience of you wallet better and better.

