Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add tutorial for Permissionless Multisigner paymaster #37

Closed
wants to merge 21 commits into from
Closed
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
306 changes: 306 additions & 0 deletions content/tutorials/permissionless-paymaster/10.index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
---
title: Integrate permissionless multi-signer paymaster into your Dapp
description: Permissionless paymaster allows multiple Dapps to seamlessly sponsor gas for their users using signature verification.
---

This tutorial shows you how to integrate this paymaster in your Dapp.

You will:

- Create a signer address that will sign paymaster data for gas sponsorship.
- Deposit gas funds and add the signer address.
- Sign EIP-712 standard signature on the paymaster data by the signer.
- Send the transaction with paymaster data & signature.

## Prerequisites

- Make sure you have a basic front-end integration ready, you can create a standard frontend [using zksync-cli](https://docs.zksync.io/build/zksync-cli/creating-projects).
sarahschwartz marked this conversation as resolved.
Show resolved Hide resolved
- A [Node.js](https://nodejs.org/en/download) installation running at minimum Node.js version 18.
- You are already familiar with paymaster integration on ZKsync Era.
If not, please refer to the first section of the [paymaster introduction](https://docs.zksync.io/build/quick-start/paymasters-introduction).
- Some background knowledge on the concepts covered by the tutorial would be helpful too. Have a look at the following
docs:
- [EIP-712 standard for typed message signing](https://eips.ethereum.org/EIPS/eip-712).
- [Transaction lifecycle](https://docs.zksync.io/zk-stack/concepts/transaction-lifecycle#eip-712-0x71) on ZKsync Era.
- [Gas estimation for transactions](https://docs.zksync.io/build/developer-reference/fee-model#gas-estimation-for-transactions) guide.
- [Introduction to system contracts especially NonceHolder](https://docs.zksync.io/build/developer-reference/era-contracts/system-contracts#nonceholder).
- You know how to get your [private key from your MetaMask wallet](https://support.metamask.io/hc/en-us/articles/360015289632-How-to-export-an-account-s-private-key).

## Project

A similar version of the integration code is [available on GitHub here](https://github.com/ondefy/permissionless-multisigner-paymaster/blob/main/deploy/interact.ts).

::callout{icon="i-heroicons-exclamation-circle"}
This tutorial does not contain the entire frontend code required for the Dapp.
Instead it covers just the important parts required by the Dapp to integrate this paymaster easily.
::

## Overview
The permissionless multi-signer paymaster serves as a public good, enabling ZKsync Dapps to seamlessly sponsor gas for their users through signature verification.
Dapps can begin utilizing this paymaster by simply depositing funds and adding a signer address. Thus removing the need to deploy a paymaster at all.

There are 2 primary actors involved:

**Manager**: Fully managed by the Dapp, responsible for depositing/withdrawing gas funds, and adding or removing signer addresses.

**Signers**: Managed by the Dapp or a trusted third party like Zyfi API. A signer’s signature is required to access gas funds by the Dapp’s user.

## Multi-signer
This paymaster allows the manager to set multiple signers through which users can have access to the gas funds. Hence, it is a one-to-many relationship.
![manager-signer-relation-diagram](/images/permissionless-paymaster/manager-signer.jpg)

## Integration
Below the diagram provides the flow of the integration:

1. Dapp decides on custom logic for each user. Let's assume that Dapp decides to sponsor gas for every approve transaction.

2. Dapp calls backend server or Zyfi API with relevant data to get the signer's signature.
- It is recommended that signer's signing part is done on a secure backend server of the Dapp.

3. The signer's key signs this paymaster data and returns the signature and signer address to the Dapp's frontend.

4. Paymaster address and required data with signature is added to the transaction blob in the frontend.

5. User gets transaction signature request pop-up on their wallet. User only signs the transaction and transaction is sent on-chain.

6. The paymaster validates the signature, identifies the manager related to the signer,
deducts gas fees from the manager's balance, and pays for the user's transaction

![flow](/images/permissionless-paymaster/flowDiagram.jpg)

For this tutorial, we will use paymaster deployed on ZKsync sepolia testnet : [0xc1B0E2edC4cCaB51A764D7Dd8121CBf58C4D9E40](https://sepolia.explorer.zksync.io/address/0xc1B0E2edC4cCaB51A764D7Dd8121CBf58C4D9E40#transactions)

## 1. Create a signer
The paymaster will verify signature based on this signer address.
The private key of this signer address should be stored securely by the Dapp.

- Here is an easy way to create one:

```javascript
import { Wallet } from "zksync-ethers";
const signer = Wallet.createRandom();
hoshiyari420 marked this conversation as resolved.
Show resolved Hide resolved
```

## 2. Deposit gas funds and add the signer address
Navigate to ZKsync Sepolia Testnet Explorer: [0xc1B0E2edC4cCaB51A764D7Dd8121CBf58C4D9E40](https://sepolia.explorer.zksync.io/address/0xc1B0E2edC4cCaB51A764D7Dd8121CBf58C4D9E40#contract).
Call `depositAndAddSigner()` function with 0.01 ether and the signer address.
hoshiyari420 marked this conversation as resolved.
Show resolved Hide resolved
![deposit and add a signer](/images/permissionless-paymaster/depositAndAddSigner.png)

::callout{icon="i-heroicons-exclamation-circle"}
The depositor address is considered a "manager". A manager can deposit/withdraw gas funds and add/remove signers at any given time.

*A manager can be a signer address too. (not recommended)*
::

## 3. Create function to sign EIP-712 typed paymaster data
This paymaster verifies the signature signed by the signer address on the below data.
On successful validation, it allows sponsorship for the user using manager's deposited gas funds.

```solidity
bytes32 messageHash = keccak256(
abi.encode(
SIGNATURE_TYPEHASH,
_from,
_to,
_expirationTime,
_maxNonce,
_maxFeePerGas,
_gasLimit
)
);
```

- Create a `getSignature()` function in your backend(recommended)

```javascript
// Example code
import {BigNumber, Contract, Wallet} from "zksync-ethers";
hoshiyari420 marked this conversation as resolved.
Show resolved Hide resolved
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);
hoshiyari420 marked this conversation as resolved.
Show resolved Hide resolved
// 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];
}
```

## 4. Add paymaster data and signature into transaction blob

- Before sending user transaction, you will need to add customData related to paymaster.
- Funds are deducted as per : `gasPrice`*`gasLimit`. Dapp should ensure enough funds are deposited to avoid failures.
- Note: For simplicity purposes, we use "DappContract" variable which is expected to be defined as per Dapp's need.

```javascript
// This is example code. Direct copy/paste won't work
sarahschwartz marked this conversation as resolved.
Show resolved Hide resolved
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,
},
});

```

::callout{icon="i-heroicons-exclamation-circle"}

- The gas funds are deducted from the manager's balance related to the signer on successful verification.

- Hence, if a signer's private key is leaked, the respective manager will need to replace/remove the signer immediately.

::

- Adding correct the `customData(paymasterParams : paymaster address & innerInputs)`, the user shall get a signature request pop-up for transaction.
Here the user can verify that gas is not being paid from their end and only a signature is required.
![transaction](/images/permissionless-paymaster/signatureRequest.png)

::callout{icon="i-heroicons-check-circle"}
Paymaster is successfully integrated.
::

## Refunds
ZKsync refunds ETH to the paymaster for the unused gas.
All refunded ETH are added back to the respective manager's balance in the next paymaster transaction. Hence, solving the refund issue for every manager.

## Notes

1. `_maxNonce` introduces 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](https://github.com/ondefy/permissionless-multisigner-paymaster/blob/2436a3fd8c401e607b89960d903dc70ca3670ed0/contracts/paymasters/PermissionlessPaymaster.sol#L199-L203)

```solidity

// 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 [arbitrary nonce ordering](https://docs.zksync.io/zk-stack/components/zksync-evm/bootloader#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](https://github.com/matter-labs/era-contracts/blob/f4ae6a1b90e2c269542848ada44de669a5009290/system-contracts/contracts/interfaces/INonceHolder.sol#L17).
For more details, check docs [here](https://docs.zksync.io/build/developer-reference/era-contracts/system-contracts#nonceholder) & [here](https://docs.zksync.io/sdk/js/ethers/api/v5/types#accountnonceordering).

3. This paymaster has gas overhead of 52K-62K gas, which is quite nominal compare to other paymaster gas overhead.
Signer should ensure to add this overhead(65K) in the `_gasLimit`, if there are setting it close to the actual required gas.

4. Setting `_gasLimit` a bit high would not be an issue at all as extra Eth sent are refunded back to manager's balance.

## Other functionalities

1. Manager can `replaceSigner`, `addSigner`, `batchAddSigners`, `removeSigner`, `batchRemoveSigners`
`depositAndAddSigner`,`deposit`,`withdraw`, `withdrawFull` & `withdrawAndRemoveSigners`

2. `depositOnBehalf` and `rescueTokens` are public functions.

3. A signer can call selfRevokeSigner to revoke their signing privileges.

4. To check the latest balance of manager including refunded Eth, call `getLatestManagerBalance`

## Common Errors

1. Paymaster validation errors:
- All paymaster related errors can be found [here].(https://github.com/ondefy/permissionless-multisigner-paymaster/blob/main/contracts/libraries/Errors.sol)
- One of the common signature validation error is that `gasLimit` and `gasPrice` is not set exact in transaction as per signed by the signer.

2. Panic due to not enough balance of the manager:
- Please try depositing additional ETH to the manager's balance in paymaster so it has enough funds to pay for the transaction.
- You can use [ZKsync native bridge or ecosystem partners](https://zksync.io/explore#bridges) (make sure Sepolia testnet is supported by selected bridge).

3. If transactions appear with status "Failed" in the [ZKsync explorer](https://sepolia.explorer.zksync.io/),
please reach out to us on [our Discord](https://discord.com/invite/KHchZXmv8Q).

## Learn More

- To learn more about this permissionless paymaster, check out
[documentation](https://docs.zyfi.org/permissionless-multi-signer-paymaster/public-good).
- To learn more about the `zksync-ethers` SDK, check out its
[documentation](https://docs.zksync.io/sdk/js/ethers).
- To learn more about the ZKsync hardhat plugins, check out their
[documentation](https://docs.zksync.io/build/tooling/hardhat/getting-started).
29 changes: 29 additions & 0 deletions content/tutorials/permissionless-paymaster/_dir.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
title: Integrate Permissionless Multi-signer Paymaster in your Dapp
featured: true
hoshiyari420 marked this conversation as resolved.
Show resolved Hide resolved
authors:
- name: Zyfi
url: https://zyfi.org
avatar: https://avatars.githubusercontent.com/u/94560147?s=200&v=4
github_repo: https://github.com/ondefy/permissionless-multisigner-paymaster
tags:
- paymaster
- gas sponsorship
- tutorial
- eip-712
summary:
Integrate Zyfi's signature based permissionless multi-signer paymaster in your Dapp to sponsor gas funds for your
users.
description:
Zyfi's new permissionless paymaster allows protocols to integrate paymaster into their ecosytem without the need to
deploy paymaster or trusting 3rd party.

what_you_will_learn:
- How permissionless multisignature paymaster works
- How to integrate this paymaster in your Dapp
- How to sign EIP-712 signature data required for this paymaster
- How to send transactions through this paymaster
updated: 2024-07-26
tools:
- zksync-cli
- zksync-ethers
- Hardhat
2 changes: 2 additions & 0 deletions cspell-config/cspell-blockchain.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
Aave
abis
arithmetization
arrayify
BoxUups
Buterin
dapp
Expand Down Expand Up @@ -67,3 +68,4 @@ Zeeve
Zerion
Zetta
Zonic
Zyfi
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.