ZkSynk Building a custom paymaster Smart Contract | Деплой смарт контракта ЗкСинк
Это уже пятый смарт контракт, так что если еще не деплоили ни одного, то можно начать с этого, этого, этого и этого
Требования к серверу:
я взяла СPХ31 хетцнере
Также нам понадобится:
- кошелек метамаск с ETH Goerli
- Тестовая сеть zksynk era testnet (можно подключить тут)
- Тестовые токенамы ETH в сети zksynk (кран тут)
- Приватный ключ от метамаска (не используйте кошельки с ральными деньгами!)
- RPC ETH1 goerli (можно взять на инфуре)
Подготавливаем сервер:
sudo apt-get update -y && sudo apt upgrade -y && sudo apt-get install make build-essential unzip lz4 gcc git jq chrony -y curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash source ~/.bashrc nvm -v nvm install v16.16.0 node -v #вывод - v16.16.0
corepack enable corepack prepare [email protected] --activate
mkdir custom-paymaster-tutorial && cd custom-paymaster-tutorial yarn init -y yarn add -D typescript ts-node ethers@^5.7.2 zksync-web3 @ethersproject/web @ethersproject/hash hardhat @matterlabs/hardhat-zksync-solc @matterlabs/hardhat-zksync-deploy @matterlabs/zksync-contracts @openzeppelin/contracts mkdir contracts deploy nano hardhat.config.ts #вставляем: import { HardhatUserConfig } from "hardhat/config"; import "@matterlabs/hardhat-zksync-deploy"; import "@matterlabs/hardhat-zksync-solc"; const config: HardhatUserConfig = { zksolc: { version: "1.3.10", // Use latest available in https://github.com/matter-labs/zksolc-bin/ compilerSource: "binary", settings: {}, }, defaultNetwork: "zkSyncTestnet", networks: { hardhat: { zksync: true, }, zkSyncTestnet: { url: "https://testnet.era.zksync.dev", ethNetwork: "goerli", // Can also be the RPC URL of the network (e.g. `https://goerli.infura.io/v3/<API_KEY>`) zksync: true, }, }, solidity: { version: "0.8.17", }, }; export default config;
Paymaster contract
nano contracts/MyPaymaster.sol #вставляем: // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IPaymaster, ExecutionResult, PAYMASTER_VALIDATION_SUCCESS_MAGIC} from "@matterlabs/zksync-contracts/l2/system-contracts/interfaces/IPaymaster.sol"; import {IPaymasterFlow} from "@matterlabs/zksync-contracts/l2/system-contracts/interfaces/IPaymasterFlow.sol"; import {TransactionHelper, Transaction} from "@matterlabs/zksync-contracts/l2/system-contracts/libraries/TransactionHelper.sol"; import "@matterlabs/zksync-contracts/l2/system-contracts/Constants.sol"; contract MyPaymaster is IPaymaster { uint256 constant PRICE_FOR_PAYING_FEES = 1; address public allowedToken; modifier onlyBootloader() { require( msg.sender == BOOTLOADER_FORMAL_ADDRESS, "Only bootloader can call this method" ); // Continue execution if called from the bootloader. _; } constructor(address _erc20) { allowedToken = _erc20; } function validateAndPayForPaymasterTransaction( bytes32, bytes32, Transaction calldata _transaction ) external payable onlyBootloader returns (bytes4 magic, bytes memory context) { // By default we consider the transaction as accepted. magic = PAYMASTER_VALIDATION_SUCCESS_MAGIC; require( _transaction.paymasterInput.length >= 4, "The standard paymaster input must be at least 4 bytes long" ); bytes4 paymasterInputSelector = bytes4( _transaction.paymasterInput[0:4] ); if (paymasterInputSelector == IPaymasterFlow.approvalBased.selector) { // While the transaction data consists of address, uint256 and bytes data, // the data is not needed for this paymaster (address token, uint256 amount, bytes memory data) = abi.decode( _transaction.paymasterInput[4:], (address, uint256, bytes) ); // Verify if token is the correct one require(token == allowedToken, "Invalid token"); // We verify that the user has provided enough allowance address userAddress = address(uint160(_transaction.from)); address thisAddress = address(this); uint256 providedAllowance = IERC20(token).allowance( userAddress, thisAddress ); require( providedAllowance >= PRICE_FOR_PAYING_FEES, "Min allowance too low" ); // Note, that while the minimal amount of ETH needed is tx.gasPrice * tx.gasLimit, // neither paymaster nor account are allowed to access this context variable. uint256 requiredETH = _transaction.gasLimit * _transaction.maxFeePerGas; try IERC20(token).transferFrom(userAddress, thisAddress, amount) {} catch (bytes memory revertReason) { // If the revert reason is empty or represented by just a function selector, // we replace the error with a more user-friendly message if (revertReason.length <= 4) { revert("Failed to transferFrom from users' account"); } else { assembly { revert(add(0x20, revertReason), mload(revertReason)) } } } // The bootloader never returns any data, so it can safely be ignored here. (bool success, ) = payable(BOOTLOADER_FORMAL_ADDRESS).call{ value: requiredETH }(""); require( success, "Failed to transfer tx fee to the bootloader. Paymaster balance might not be enough." ); } else { revert("Unsupported paymaster flow"); } } function postTransaction( bytes calldata _context, Transaction calldata _transaction, bytes32, bytes32, ExecutionResult _txResult, uint256 _maxRefundedGas ) external payable override onlyBootloader { // Refunds are not supported yet. } receive() external payable {} }
nano contracts/MyERC20.sol #вставляем: // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract MyERC20 is ERC20 { uint8 private _decimals; constructor( string memory name_, string memory symbol_, uint8 decimals_ ) ERC20(name_, symbol_) { _decimals = decimals_; } function mint(address _to, uint256 _amount) public returns (bool) { _mint(_to, _amount); return true; } function decimals() public view override returns (uint8) { return _decimals; } }
Компилируем и деплоим контракты:
cd deploy nano deploy-paymaster.ts #вставляем: import { utils, Provider, Wallet } from "zksync-web3"; import * as ethers from "ethers"; import { HardhatRuntimeEnvironment } from "hardhat/types"; import { Deployer } from "@matterlabs/hardhat-zksync-deploy"; export default async function (hre: HardhatRuntimeEnvironment) { const provider = new Provider("https://testnet.era.zksync.dev"); // The wallet that will deploy the token and the paymaster // It is assumed that this wallet already has sufficient funds on zkSync const wallet = new Wallet("<PRIVATE-KEY>"); // The wallet that will receive ERC20 tokens const emptyWallet = Wallet.createRandom(); console.log(`Empty wallet's address: ${emptyWallet.address}`); console.log(`Empty wallet's private key: ${emptyWallet.privateKey}`); const deployer = new Deployer(hre, wallet); // Deploying the ERC20 token const erc20Artifact = await deployer.loadArtifact("MyERC20"); const erc20 = await deployer.deploy(erc20Artifact, [ "MyToken", "MyToken", 18, ]); console.log(`ERC20 address: ${erc20.address}`); // Deploying the paymaster const paymasterArtifact = await deployer.loadArtifact("MyPaymaster"); const paymaster = await deployer.deploy(paymasterArtifact, [erc20.address]); console.log(`Paymaster address: ${paymaster.address}`); console.log("Funding paymaster with ETH"); // Supplying paymaster with ETH await ( await deployer.zkWallet.sendTransaction({ to: paymaster.address, value: ethers.utils.parseEther("0.06"), }) ).wait(); let paymasterBalance = await provider.getBalance(paymaster.address); console.log(`Paymaster ETH balance is now ${paymasterBalance.toString()}`); // Supplying the ERC20 tokens to the empty wallet: await // We will give the empty wallet 3 units of the token: (await erc20.mint(emptyWallet.address, 3)).wait(); console.log("Minted 3 tokens for the empty wallet"); console.log(`Done!`); }
Заменить <PRIVATE-KEY> на приватный ключ от вашего метамаска
yarn hardhat compile yarn hardhat deploy-zksync --script deploy-paymaster.ts #вывод должен быть такой:
Тут у нас создался новый кошелек, на который было сминчено 3 токена, запоминаем:
2я строка - <EMPTY_WALLET_PRIVATE_KEY>
3я строка - <TOKEN_ADDRESS>
4я строка - <PAYMASTER_ADDRESS>
nano use-paymaster.ts #вставляем: import { Provider, utils, Wallet } from "zksync-web3"; import * as ethers from "ethers"; import { HardhatRuntimeEnvironment } from "hardhat/types"; // Put the address of the deployed paymaster here const PAYMASTER_ADDRESS = "<PAYMASTER_ADDRESS>"; // Put the address of the ERC20 token here: const TOKEN_ADDRESS = "<TOKEN_ADDRESS>"; // Wallet private key const EMPTY_WALLET_PRIVATE_KEY = "<EMPTY_WALLET_PRIVATE_KEY>"; function getToken(hre: HardhatRuntimeEnvironment, wallet: Wallet) { const artifact = hre.artifacts.readArtifactSync("MyERC20"); return new ethers.Contract(TOKEN_ADDRESS, artifact.abi, wallet); } export default async function (hre: HardhatRuntimeEnvironment) { const provider = new Provider("https://testnet.era.zksync.dev"); const emptyWallet = new Wallet(EMPTY_WALLET_PRIVATE_KEY, provider); // const paymasterWallet = new Wallet(PAYMASTER_ADDRESS, provider); // Obviously this step is not required, but it is here purely to demonstrate that indeed the wallet has no ether. const ethBalance = await emptyWallet.getBalance(); if (!ethBalance.eq(0)) { throw new Error("The wallet is not empty!"); } console.log( `ERC20 token balance of the empty wallet before mint: ${await emptyWallet.getBalance( TOKEN_ADDRESS )}` ); let paymasterBalance = await provider.getBalance(PAYMASTER_ADDRESS); console.log(`Paymaster ETH balance is ${paymasterBalance.toString()}`); const erc20 = getToken(hre, emptyWallet); const gasPrice = await provider.getGasPrice(); // Encoding the "ApprovalBased" paymaster flow's input const paymasterParams = utils.getPaymasterParams(PAYMASTER_ADDRESS, { type: "ApprovalBased", token: TOKEN_ADDRESS, // set minimalAllowance as we defined in the paymaster contract minimalAllowance: ethers.BigNumber.from(1), // empty bytes as testnet paymaster does not use innerInput innerInput: new Uint8Array(), }); // Estimate gas fee for mint transaction const gasLimit = await erc20.estimateGas.mint(emptyWallet.address, 5, { customData: { gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT, paymasterParams: paymasterParams, }, }); const fee = gasPrice.mul(gasLimit.toString()); console.log("Transaction fee estimation is :>> ", fee.toString()); console.log(`Minting 5 tokens for empty wallet via paymaster...`); await ( await erc20.mint(emptyWallet.address, 5, { // paymaster info customData: { paymasterParams: paymasterParams, gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT, }, }) ).wait(); console.log( `Paymaster ERC20 token balance is now ${await erc20.balanceOf( PAYMASTER_ADDRESS )}` ); paymasterBalance = await provider.getBalance(PAYMASTER_ADDRESS); console.log(`Paymaster ETH balance is now ${paymasterBalance.toString()}`); console.log( `ERC20 token balance of the empty wallet after mint: ${await emptyWallet.getBalance( TOKEN_ADDRESS )}` ); }
Заменить <EMPTY_WALLET_PRIVATE_KEY>, <TOKEN_ADDRESS> и <PAYMASTER_ADDRESS> на свои значения
yarn hardhat deploy-zksync --script use-paymaster.ts #вывод, как на скрине ниже