Technical Details
DApps can integrate this paymaster within 35 lines of code. This only concerns for Dapps who want to manage signer themselves.
Last updated
// Example code
// ethers v5
import {BigNumber, Contract, Wallet} from "zksync-ethers";
export async function getSignature(
from: string, to: string, expirationTime: BigNumber, maxNonce: BigNumber, maxFeePerGas: BigNumber, gasLimit: BigNumber, paymaster: Contract
){
const signer = new Wallet(process.env.SIGNER_PRIVATE_KEY, provider);
// EIP-712 domain from the paymaster
const eip712Domain = await paymaster.eip712Domain();
const domain = {
name: eip712Domain[1],
version: eip712Domain[2],
chainId: eip712Domain[3],
verifyingContract: eip712Domain[4],
}
const types = {
PermissionLessPaymaster: [
{ name: "from", type: "address"},
{ name: "to", type: "address"},
{ name: "expirationTime", type: "uint256"},
{ name: "maxNonce", type: "uint256"},
{ name: "maxFeePerGas", type: "uint256"},
{ name: "gasLimit", type: "uint256"}
]
};
// -------------------- IMPORTANT --------------------
const values = {
from, // User address
to, // Your dapp contract address which the user will interact
expirationTime, // Expiration time post which the signature expires
maxNonce, // Max nonce of user after which signature becomes invalid
maxFeePerGas, // Current max gas price
gasLimit // Max gas limit you want to allow to your user. Ensure to add 60K gas for paymaster overhead.
}
// Note: MaxNonce allows the signature to be replayed.
// For eg: If currentNonce of user is 5, maxNonce is set to 10. Signature will allowed to replayed for nonce 6,7,8,9,10 on the same `to` address by the same user.
// This is to provide flexibility to Dapps to ensure signature works if users have multiple transactions running.
// Important: Signers are recommended to set maxNonce as current nonce of the user or as close as possible to ensure safety of gas funds.
// Important : Signers should set expirationTime is close enough to ensure safety of funds.
// Signer wallet will already defined in the code
return [(await signer._signTypedData(domain, types, values)), signer.address];
}// This is example code. Direct copy/paste won't work
import {utils, provider, Contract, BigNumber} from "zksync-ethers";
const paymasterAddress = "0x1fc6AAd6FFc4b26229a29432FbC4b65d5A5e462b";
const paymasterAbi = ["function eip712Domain() external view returns (bytes1 fields,string memory name,string memory version,uint256 chainId,address verifyingContract,bytes32 salt,uint256[] memory extensions);"];
const paymasterContract = new Contract(paymasterAddress, paymasterAbi, provider);
// Below part can be managed in getSignature() as well.
// ------------------------------------------------------------------------------------
// Note: Do not set maxNonce too high than current to avoid unwanted signature replay.
// Consider maxNonce is as replayLimit. And setting maxNonce to currentNonce means 0 replay.
// Get the maxNonce allowed to user. Here we ensure it's currentNonce.
const maxNonce = await provider.getNonce(userAddress);
// You can also check for min Nonce from the NonceHolder System contract to fully ensure as ZKsync support arbitrary nonce.
// -----------------
// const nonceHolderAddress = "0x0000000000000000000000000000000000008003";
// const nonceHolderAbi = ["function getMinNonce(address _address) external view returns (uint256)"];
// const nonceHolderContract = new Contract(nonceHolderAddress, nonceHolderAbi, provider);
// const maxNonce = await nonceHolderContract.callStatic.getMinNonce(userAddress);
// -----------------
// Get the expiration time. Here signature will be valid upto 60 sec.
const expirationTime = BigNumber.from((await provider.getBlock).timestamp + 60);
// Get the current gas price.
const maxFeePerGas = await provider.getGasPrice();
// Set the gasLimit. Here, Dapp would know range of gas a function could cost and add 60K top up for paymaster overhead..
// Setting 215K (For eg: 150K function gas cost + 65K paymaster overhead)
// It will refunded anyways, so not an issue if Dapps set more.
const gasLimit = 215_000;
// ------------------------------------------------------------------------------------
const [signature, signerAddress] = await getSignature(userAddress,DappContract.address,expirationTime, maxNonce, maxFeePerGas, gasLimit, paymasterContract);
// We encode the extra data to be sent to paymaster
// Notice how it's not required to provide from, to, maxFeePerGas and gasLimit as per signature above.
// That's because paymaster will get it from the transaction struct directly to ensure it's the correct user.
const innerInput = ethers.utils.arrayify(
abiCoder.encode(
["uint256","uint256","address","bytes"],
[expirationTime, // As used in above signature
maxNonce, // As used in above signature
signerAddress, // The signer address
signature]), // Signature created in the above snippet. get from API server
);
// getPaymasterParams function is available in zksync-ethers
const paymasterParams = utils.getPaymasterParams(
paymasterAddress, // Paymaster address
{
type: "General",
innerInput: innerInputs
});
// Send the transaction with paymaster data.
// Users will get transaction signature pop-up
const tx = await DappContract.<function>([args..],{
maxFeePerGas, // Ensure it's same as used for signature
gasLimit, // Ensure it's same as used for signature
customData:{
paymasterParams, // Paymaster address + paymaster data with signature.
gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT,
},
});
// Validate that the transaction generated by the API is not expired
if (block.timestamp > expirationTime)
revert Errors.PM_SignatureExpired();
// Validate that the nonce is not higher than the maximum allowed
if (_transaction.nonce > maxNonce) revert Errors.PM_InvalidNonce();