-
Notifications
You must be signed in to change notification settings - Fork 13
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
fix: update permissionless paymaster tutorial & l1-l2 #47
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -55,21 +55,21 @@ This paymaster allows the manager to set multiple signers through which users ca | |
![manager-signer-relation-diagram](/images/permissionless-paymaster/manager-signer.jpg) | ||
|
||
## Integration | ||
Below the diagram provides the flow of the integration: | ||
The diagram below 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. | ||
1. Dapp decides on custom logic for each user. Let's assume that Dapp decides to sponsor gas for every approved 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. | ||
For the utmost security, it is recommended that the signer's signature be sent to Dapp's secure backend server. | ||
|
||
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. | ||
5. The user receives the transaction signature request pop-up on their wallet. **The User only signs the transaction**, and it is sent on-chain. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Change |
||
|
||
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 | ||
deducts gas fees from the manager's balance and pays for the user's transaction. | ||
|
||
![flow](/images/permissionless-paymaster/flowDiagram.jpg) | ||
|
||
|
@@ -176,42 +176,54 @@ 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 -------------------- | ||
from: string, | ||
to: string, | ||
expirationTime: BigNumber, m | ||
axNonce: 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"} | ||
] | ||
}; | ||
|
||
/** | ||
* Note: MaxNonce allows signature replay within the specified range. | ||
* Important: Set maxNonce close to the current nonce for gas funds safety. | ||
* Important: Set expirationTime close to the current time for safety. | ||
*/ | ||
|
||
const values = { | ||
from, // User address | ||
to, // Your dapp contract address which the user will interact | ||
|
@@ -220,11 +232,6 @@ const paymasterContract = new Contract( | |
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]; | ||
} | ||
|
@@ -270,26 +277,27 @@ const preparePaymasterParam = async (account:any, estimateGas: BigNumber) =>{ | |
// 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 currentTimestamp = BigNumber.from((await provider.getBlock("latest")).timestamp); | ||
const expirationTime = currentTimestamp.add(120); | ||
// Get the current gas price. | ||
const maxFeePerGas = await provider.getGasPrice(); | ||
|
@@ -303,48 +311,47 @@ const preparePaymasterParam = async (account:any, estimateGas: BigNumber) =>{ | |
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"], | ||
[ | ||
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 | ||
); | ||
] | ||
) // 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, | ||
} | ||
); | ||
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. | ||
In the `WriteContract` component, right before the `const tx = await contract.approve(spender, amount);` line, we will: | ||
|
||
1. Estimate gas | ||
2. Call the `preparePaymasterParam` function we created above | ||
|
||
with the snippet below: | ||
|
||
```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); | ||
|
||
const [account, paymasterParams, maxFeePerGas, gasLimit] = await preparePaymasterParam(account, estimateGas); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This would lead to error |
||
``` | ||
|
||
## 6. Add paymaster data to the transaction | ||
|
@@ -357,7 +364,7 @@ const preparePaymasterParam = async (account:any, estimateGas: BigNumber) =>{ | |
|
||
// Estimate gas | ||
const estimateGas = await contract.estimateGas.approve(spender,amount); | ||
const [paymasterParams, maxFeePerGas, gasLimit] = await preparePaymasterParam(account, estimateGas); | ||
const [account, paymasterParams, maxFeePerGas, gasLimit] = await preparePaymasterParam(account, estimateGas); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This would lead to error. |
||
|
||
const tx = await contract.approve(spender, amount,{ | ||
maxFeePerGas, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.