Technical Details

DApps can integrate this paymaster within 35 lines of code. This only concerns for Dapps who want to manage signer themselves.

1. What paymaster data would the signer key sign on?

  • Paymaster validates upon EIP-712 type signatures.

(_domainSeparator +
hash(
    SIGNATURE_TYPEHASH,
    _from,
    _to,
    _expirationTime,
    _maxNonce,
    _maxFeePerGas,
    _gasLimit
))

_from : The user address the signer wants to sponsor.

_to: The target contract user address is interacting i.e. Dapp's contract.

_expirationTime : Timestamp post which the signature expires.

_maxNonce : Nonce of the user(_from) post which signature cannot be replayed.

_maxFeePerGas : Current gas price returned by the provider.

_gasLimit : Gas limit required by the transaction. Paymaster cost 60K gas overhead. Hence, should be added while setting close gasLimit.

  • Following code represents what the exact values the are required to be signed by the signer(point 3 & 4 as per previous integration flow diagram):

// 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];
}

2. What extra data will be send with transaction for paymaster data?

  • Once you get the signature, you simply need to add custom data to the user transaction as below(point 5 as per previous integration diagram) :

// 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,
    },
  });

Further documentation on this paymaster will be available soon here & on https://code.zksync.io . Zyfi is also working on an SDK to reduces the above code into 5 lines of code.

Miscellaneous

  1. _maxNonce allows flexibility to Dapps by allowing signature replay in a secure constrained way. Signer should ensure maxNonce is not too big from the current nonce of the user and _expirationTime is not too far from the current timestamp. If _maxNonce is set to current nonce of the user, then signature cannot be replayed at all.

    Check here:

    PermissionlessPaymaster.sol
            // 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();

  2. ZKsync might allow arbitary nonce ordering in future. To ensure surety over nonce of a user, you can add one more check by calling getMinNonce on the NonceHolder system contract of ZKsync. For more details, check docs here & here.

  3. This paymaster has gas overhead of 51K-60K gas, which is quite nominal compare to other paymaster gas overhead. Signer should ensure to add this overhead i.e. 60K in the _gasLimit, if there are setting it close to the actual required gas.

  4. Do not worry to set the _gasLimit high. All extra ETH spent from the manager's gas funds are refunded back to the manager.

Markup charge

Zyfi Dao will have the ability to set the markup fee percent similar to Uniswap fee switch. Currently, markup is set to 0 percent.

Once activated, a markup percent will be charged on each transaction on the required ETH (gasPrice*gasLimit) of the transaction and will be deducted from the respective manager's balance.

This markup will incentivise the DAO to promote paymaster evolution.

FAQs

1. Are there any fees for interacting with this paymaster other than potential markup fee set by the DAO? No, this paymaster is meant for common good. No fees on deposit, withdrawal, adding signers.

2. What if the private key of our signer gets leaked? You will need to quickly remove/replace the leaked signer address from the paymaster. A leaked signer private key can drain gas funds of the related manager.

3. As a manager(Dapp), are my funds at risk if private key of un-related signer address is leaked? Only the manager’s gas funds related to the leaked signer address will be at risk. Rest all the manager funds will be safe.

4. ZKsync processes refunds for extra gas fees paid in each transaction. As a manager, would I be receiving those refunds that is ideally deducted from my balance? Yes, this paymaster solves the refund issue innovatively. Each manager’s balance will be updated with exact refund amount during the next paymaster interaction. Hence, all refunds are added back to the respective manager's balance.

Last updated