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

fix: update permissionless paymaster tutorial & l1-l2 #47

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 2 additions & 3 deletions content/tutorials/how-to-send-l1-l2-transaction/_dir.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@ tags:
- smart contracts
- how-to
- L2-L1 communication
summary: Learn how to send a transaction from Ethereum that to ZKsync
description:
This how-to guide explains how to send a transaction from Ethereum that interacts with a contract deployed on ZKsync.
summary: Learn how to send a transaction from Ethereum to ZKsync
description: This how-to guide shows how to send a transaction from Ethereum to a contract on ZKsync.
what_you_will_learn:
- How L1-L2 communication works
- How to send a transaction from ZKsync Ethereum to ZKsync.
Expand Down
155 changes: 81 additions & 74 deletions content/tutorials/permissionless-paymaster/10.index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
For the utmost security, it is recommended that the signer's signature be sent to Dapp's secure backend server.
For security best-practices, it is recommended to create the signer's signature on a 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change **The User to **The user for consistency


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)

Expand Down Expand Up @@ -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
Expand All @@ -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];
}
Expand Down Expand Up @@ -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();
Expand All @@ -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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would lead to error

```

## 6. Add paymaster data to the transaction
Expand All @@ -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);
Copy link
Contributor

Choose a reason for hiding this comment

The 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,
Expand Down
Loading