ZkSynk Building a custom paymaster Smart Contract | Деплой смарт контракта ЗкСинк*
Требования к серверу:
Также нам понадобится:
- кошелек метамаск с 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
Код:
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
#вывод, как на скрине ниже
Напоминаю, что это пятый контракт из серии, не забывайте выполнить первые и подписывайтесь!