ZKsync Era Features
ZKsync Era offers several unique features compared to Ethereum, requiring specific handling. This guide will help
you understand these differences and how to work with them using the zksync-ethers
library.
Key differences
- Account abstraction: ZKsync supports account abstraction, allowing accounts to have custom validation logic and enabling paymaster support.
- Deployment transactions: Contracts on ZKsync require bytecode to be passed in a separate field during deployment.
- Fee system: The fee system in ZKsync differs from Ethereum's, necessitating additional transaction fields.
These differences require extending standard Ethereum transactions with new custom fields. Such extended transactions are called EIP712 transactions since EIP712 is used to sign them. More details on EIP712 transactions can be found in the ZKstack transaction lifecycle.
Overrides in transactions
ethers.js
has a notion of overrides. For any on-chain transaction, ethers.js
finds the
optimal gasPrice
, gasLimit
, nonce
, and other important fields under the hood. But sometimes, you may need to
explicitly provide these values (e.g., setting a smaller gasPrice
or signing a transaction with a future nonce
).
In such cases, you can use an Overrides
object as the last parameter. This object lets you set fields
like gasPrice
, gasLimit
, nonce
, etc.
To make the SDK as flexible as possible, zksync-ethers
uses a customData
object within the overrides to supply
ZKsync-specific fields. Here's how you can do it:
const tx = {
to: '0x...',
value: ethers.utils.parseEther('0.01'),
overrides: {
gasPrice: ethers.utils.parseUnits('10', 'gwei'),
gasLimit: 21000,
nonce: 0,
customData: {
gasPerPubdata: BigNumberish,
factoryDeps: BytesLike[],
customSignature: BytesLike,
paymasterParams: {
paymaster: Address,
paymasterInput: BytesLike
}
}
}
};
CustomData fields
gasPerPubdata
: Specifies L2 gas per published byte.factoryDeps
: Array of contract bytecodes for deployment.customSignature
: Custom signature for the transaction.paymasterParams
: Parameters for using a paymaster.
customData
in overrides
is related to ZKsync (L2 gas, etc.).Example overrides
To deploy a contract with specific bytecode and a gas limit for published data:
{
customData: {
gasPerPubdata: "100",
factoryDeps: ["0xcde...12"],
}
}
To use a custom signature and paymaster:
{
customData: {
customSignature: "0x123456",
paymasterParams: {
paymaster: "0x8e1DC7E4Bb15927E76a854a92Bf8053761501fdC",
paymasterInput: "0x8c5a3445"
}
}
}
Encoding paymaster params
While the paymaster feature by itself does not impose any limitations on values of the paymasterInput
, the Matter
Labs team endorses certain types
of paymaster flows
that are processable by EOAs.
The ZKsync SDK provides a utility method that can be used to get the correctly formed paymasterParams
object:
const paymasterParams = utils.getPaymasterParams(testnetPaymaster, {
type: "ApprovalBased",
token,
minimalAllowance: fee,
innerInput: new Uint8Array(),
});
Using a paymaster with a contract method
Here is how you can call the setGreeting
method of a Contract
object greeter
and pay fees with a testnet paymaster:
const greeting = "a new greeting";
const tx = await greeter.populateTransaction.setGreeting(greeting);
const gasPrice = await sender.provider.getGasPrice();
const gasLimit = await greeter.estimateGas.setGreeting(greeting);
const fee = gasPrice.mul(gasLimit);
const paymasterParams = utils.getPaymasterParams(testnetPaymaster, {
type: "ApprovalBased",
token,
minimalAllowance: fee,
innerInput: new Uint8Array(),
});
const sentTx = await sender.sendTransaction({
...tx,
maxFeePerGas: gasPrice,
maxPriorityFeePerGas: 0n,
gasLimit,
customData: {
gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT,
paymasterParams,
},
});