diff --git a/content/tutorials/guide-web3js/_dir.yml b/content/tutorials/guide-web3js/_dir.yml index c3a70959..a7dadaed 100644 --- a/content/tutorials/guide-web3js/_dir.yml +++ b/content/tutorials/guide-web3js/_dir.yml @@ -1,5 +1,5 @@ title: Using web3.js to interact with ZKsync -featured: true +featured: false authors: - name: ChainSafe url: https://web3js.org/ diff --git a/content/tutorials/permissionless-paymaster/10.index.md b/content/tutorials/permissionless-paymaster/10.index.md new file mode 100644 index 00000000..795e6ef0 --- /dev/null +++ b/content/tutorials/permissionless-paymaster/10.index.md @@ -0,0 +1,454 @@ +--- +title: Integrate permissionless multi-signer paymaster into your Dapp +description: Permissionless paymaster allows multiple Dapps to sponsor gas for their users using signature verification seamlessly. +--- + +This tutorial shows you how to integrate this paymaster in your Dapp. + +In this tutorial, we will : + +- Create a signer address that will sign paymaster data for gas sponsorship. +- Deposit gas funds and add the signer address in the permissionless paymaster. +- Set up a basic frontend to interact with the ZKsync network. +- Create a function to sign EIP-712 standard paymaster data by the signer. +- Integrate paymaster data in frontend to sponsor transactions for users. + +## Prerequisites + +- A [Node.js](https://nodejs.org/en/download) installation running at minimum Node.js version 18. +- Use the `yarn` or `npm` package manager. We recommend using `yarn`. + To install `yarn`, follow the [Yarn installation guide](https://yarnpkg.com/getting-started/install). +- You are a bit 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). +- Get some ZKSync Sepolia Testnet ETH [using one of these faucets](https://docs.zksync.io/ecosystem/network-faucets). +- 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). + +::callout + +- Some background knowledge on the concepts helpful for the tutorial: + + - [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) + +:: + +## Overview +The permissionless multi-signer paymaster enables ZKsync Dapps to sponsor gas for their users through signature verification. +Signature-based verification allows Dapps to setup **custom logic** for gas sponsorship. + +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 or relying on any 3rd party. + +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 the backend server or Zyfi API with relevant data to get the signer's signature. + - It is recommended that the 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 are 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 the 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 address +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 +const {Wallet} = require("zksync-ethers"); + +const signer = Wallet.createRandom(); + +console.log("Signer address: " + signer.address); +console.log("Signer private key: " + signer.privateKey); +``` + +## 2. Deposit gas funds and add the signer address +Navigate to ZKsync Sepolia Testnet Explorer: [0xc1B0E2edC4cCaB51A764D7Dd8121CBf58C4D9E40](https://sepolia.explorer.zksync.io/address/0xc1B0E2edC4cCaB51A764D7Dd8121CBf58C4D9E40#contract) +Using a **different address** with test funds, call the `depositAndAddSigner()` function with 0.1 ether +and the signer address as the function arguments. +![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 signer can only be linked to one manager at a time.* + +- *A manager can be a signer address too. (not recommended)* + +:: + +## 3. Setup a basic frontend project + +We're going to use the [React.js web framework](https://react.dev/) to build this app. +In addition, we're going to use the `zksync-ethers` SDK, `ethers@v5`, and `Next.js`. +In order to focus on paymaster, we have bootstrapped a [template using `zksync-cli create`](https://github.com/ondefy/basic-frontend-template.git). +We'll just need to implement the methods to sign and integrate paymaster into the frontend. +Let's start! + +1. Clone the template project and `cd` into the folder. + + ```sh + git clone https://github.com/ondefy/basic-frontend-template.git + cd ./basic-frontend-template + ``` + +2. Make sure you are on the `main` branch(use `git branch` to check) and spin up the project. + + ```bash + yarn + yarn dev + ``` + +Navigate to `http://localhost:3000/` in a browser to see the application running. + +![frotend](/images/permissionless-paymaster/frontend.png) + +This is a simple template that allows users to approve a certain amount of DAI tokens to a spender address on ZKsync Sepolia Testnet. + +*Our goal for this tutorial is to sponsor this approval transaction using the paymaster.* + +--- + +## 4. Create a 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 + ) + ); +``` + +::callout{icon="i-heroicons-exclamation-triangle"} +For the sake of this tutorial, we will be creating the signing function in the repo itself at *"src/backend/getSignatureApiCall.ts"* path. +

+**It is highly recommended, that signing should be done in a closed backend server for the safety of signer's private key.** +:: + +- Create a `.env` file in the root folder and add signer's private key as `NEXT_PUBLIC_SIGNER_PRIVATE_KEY`. Run `source .env` once done. + +``` [.env] +NEXT_PUBLIC_SIGNER_PRIVATE_KEY=0xabcabc... +``` + +- We will create a file `backend/getSignatureApiCall.ts` with `getSignature()` function that signs required data with the help of signer as below: + +```javascript [src/backend/getSignatureApiCall.tsx] +import {ethers, BigNumber} from "ethers"; +import {Contract, Wallet, Provider} from "zksync-ethers"; +import dotenv from "dotenv"; +dotenv.config(); + +export async function getSignature( + from: string, to: string, expirationTime: BigNumber, maxNonce: BigNumber, maxFeePerGas: BigNumber, gasLimit: BigNumber +){ + const rpcUrl = process.env.ZKSYNC_RPC_URL ?? 'https://sepolia.era.zksync.dev'; + const provider = new Provider(rpcUrl); + const signer = new Wallet(process.env.NEXT_PUBLIC_SIGNER_PRIVATE_KEY || "", provider); + +// Paymaster Sepolia Testnet address +const paymasterAddress = "0xc1B0E2edC4cCaB51A764D7Dd8121CBf58C4D9E40"; +const paymasterAbi = [ + "function eip712Domain() public 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 +); +// EIP-712 domain from the paymaster + const eip712Domain = await paymasterContract.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 the currentNonce of user is 5, maxNonce is set to 10. The signature will allowed to be 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 the safety of gas funds. +// Important: Signers should set expirationTime close enough to ensure safety of funds. + + return [paymasterAddress,(await signer._signTypedData(domain, types, values)), signer.address]; +} +``` + +## 5. Estimate gas and create paymaster data + +##### **5.1.** Add the `preparePaymasterParam()` function in the `src/components/WriteContract.tsx` file + +- This function will set expiration time, max nonce, gas fees, and gas limit and call the `getSignature()` function. + +::callout{icon="i-heroicons-exclamation-circle"} + +*Notice how we add 65K gas to the gas limit.* +

+This is to facilitate paymaster overhead to ensure the transaction is passed successfully. +
+All extra ETH spent will be refunded back to the manager's balance, hence, setting the gas limit high would not result in loss of funds. + +:: + +- At the end, it returns `paymasterParams` data that we will add to the transaction. + +```javascript [src/components/WriteContract.tsx] +'use client' + +import { useState } from 'react'; +import { Contract, Provider, utils } from "zksync-ethers"; +import { useAsync } from '../hooks/useAsync'; +import { daiContractConfig } from './contracts' +import { useEthereum } from './Context'; +import { getSignature } from "../backend/getSignatureApiCall"; +import { BigNumber, ethers } from "ethers"; +const abiCoder = new ethers.utils.AbiCoder(); + +const preparePaymasterParam = async (account:any, estimateGas: BigNumber) =>{ + const rpcUrl = process.env.ZKSYNC_RPC_URL ?? "https://sepolia.era.zksync.dev"; + const provider = new Provider(rpcUrl); + // Below part can be managed in getSignature() as well. + // ------------------------------------------------------------------------------------ + // Note: Do not set maxNonce too high than current to avoid an unwanted signature replay. + // Consider maxNonce is as replayLimit. Setting maxNonce to currentNonce means 0 replays. + // Get the maxNonce allowed to user. Here we ensure it's currentNonce. + const maxNonce = BigNumber.from( + await provider.getTransactionCount(account.address || "") + ); + // 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 maxNonce2 = await nonceHolderContract.callStatic.getMinNonce( + account.address || "" + ); + console.log(maxNonce2.toString()); + // ----------------- + // Get the expiration time. Here signature will be valid up to 120 sec. + const currentTimestamp = BigNumber.from( + (await provider.getBlock("latest")).timestamp + ); + const expirationTime = currentTimestamp.add(120); + // Get the current gas price. + const maxFeePerGas = await provider.getGasPrice(); + // Add paymaster overhead gas to be on the safe side. + const gasLimit = estimateGas.add(65000); + // ------------------------------------------------------------------------------------ + const [paymasterAddress, signature, signerAddress] = await getSignature( + account.address.toString(), + daiContractConfig.address.toString(), + expirationTime, + maxNonce, + maxFeePerGas, + gasLimit + ); + console.log("Signer: " + signerAddress); + // 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 the 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 the above signature + maxNonce, // As used in the 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: innerInput, + } + ); + // Returns paymaster params, gas fee, gas limit + return [paymasterParams, maxFeePerGas, gasLimit]; + }; +``` + +##### **5.2** Estimate gas and call the above function + +- In the `WriteContract` component, we will estimate the gas and call the above created `preparePaymasterParam` function + before the contract call for approve transaction. + +```javascript [src/components/WriteContract.tsx] + // ------------- Add above approve function call + // Estimate gas + const estimateGas = await contract.estimateGas.approve(spender,amount); + const [paymasterParams, maxFeePerGas, gasLimit] = await preparePaymasterParam(account, estimateGas); + // -------------- + const tx = await contract.approve(spender, amount); + +``` + +## 6. Add paymaster data to the transaction + +- We will update the approve function call by adding `paymasterParams` in the `customData` of the user transaction as below. + +- Ensure to set `maxFeePerGas` and `gasLimit` as signed by the signer. + +```javascript [src/components/WriteContract.tsx] + + // Estimate gas + const estimateGas = await contract.estimateGas.approve(spender,amount); + const [paymasterParams, maxFeePerGas, gasLimit] = await preparePaymasterParam(account, estimateGas); + + const tx = await contract.approve(spender, amount,{ + maxFeePerGas, + gasLimit, + customData: { // Add custom data with paymaster params + paymasterParams, + gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT, + } + }); + +``` + +::callout{icon="i-heroicons-exclamation-circle"} + +- Funds are deducted as per: `gasPrice`*`gasLimit` upon successful verification. +Dapp should ensure enough funds are deposited to avoid failures. + +- If a signer's private key is leaked, the respective manager will need to replace/remove the signer immediately. + +:: + +- 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. +:: + +The final integration code should look like [`permissionless-paymaster-integration` branch of the template repo.](https://github.com/ondefy/basic-frontend-template/tree/permissionless-paymaster-integration) + +## 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**. + +## Notes + +1. `_maxNonce` introduces flexibility to Dapps by allowing signature replay in a secure constrained way. +The 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 the 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 the future. +To ensure surety over the 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 a gas overhead of 51K-59K gas, which is quite nominal compared to other paymaster gas overheads. +Signer should ensure to add this overhead(60K~65K) in the `_gasLimit`. + +4. `_gasLimit` should be set in the near range of the estimated gas required + paymaster gas overhead. + +## 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 the 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 errors is that `gasLimit` and `gasPrice` are not set exactly in the transaction as 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. Please reach out to us on [our Discord](https://discord.com/invite/KHchZXmv8Q) in case of other errors. + +## Learn More + +- Audited by [Cantina](https://www.zyfi.org/report-permissionless-paymaster-zyfi.pdf). +- 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). diff --git a/content/tutorials/permissionless-paymaster/_dir.yml b/content/tutorials/permissionless-paymaster/_dir.yml new file mode 100644 index 00000000..a7ba4c63 --- /dev/null +++ b/content/tutorials/permissionless-paymaster/_dir.yml @@ -0,0 +1,29 @@ +title: Integrate Permissionless Multi-signer Paymaster in your Dapp +featured: true +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 diff --git a/cspell-config/cspell-blockchain.txt b/cspell-config/cspell-blockchain.txt index 0fda76bf..9ea5e40d 100644 --- a/cspell-config/cspell-blockchain.txt +++ b/cspell-config/cspell-blockchain.txt @@ -1,6 +1,7 @@ Aave abis arithmetization +arrayify BoxUups Buterin dapp @@ -69,3 +70,4 @@ Zeeve Zerion Zetta Zonic +Zyfi \ No newline at end of file diff --git a/public/images/permissionless-paymaster/depositAndAddSigner.png b/public/images/permissionless-paymaster/depositAndAddSigner.png new file mode 100644 index 00000000..49b8b03a Binary files /dev/null and b/public/images/permissionless-paymaster/depositAndAddSigner.png differ diff --git a/public/images/permissionless-paymaster/flowDiagram.jpg b/public/images/permissionless-paymaster/flowDiagram.jpg new file mode 100644 index 00000000..d7e4f0c8 Binary files /dev/null and b/public/images/permissionless-paymaster/flowDiagram.jpg differ diff --git a/public/images/permissionless-paymaster/frontend.png b/public/images/permissionless-paymaster/frontend.png new file mode 100644 index 00000000..2b97f228 Binary files /dev/null and b/public/images/permissionless-paymaster/frontend.png differ diff --git a/public/images/permissionless-paymaster/manager-signer.jpg b/public/images/permissionless-paymaster/manager-signer.jpg new file mode 100644 index 00000000..bde6d5a4 Binary files /dev/null and b/public/images/permissionless-paymaster/manager-signer.jpg differ diff --git a/public/images/permissionless-paymaster/signatureRequest.png b/public/images/permissionless-paymaster/signatureRequest.png new file mode 100644 index 00000000..c2f9cdaf Binary files /dev/null and b/public/images/permissionless-paymaster/signatureRequest.png differ