对官方给的 Paymaster 部分示例的理解
官方的 Paymaster.sol 部分
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "./interfaces/IPaymaster.sol";
import "./interfaces/IPaymasterFlow.sol";
import "./L2ContractHelper.sol";
// 这是个假出纳员。它希望 paymasterInput 包含其“签名”以及所需的汇率
// 仅支持基于授权的 paymaster 流程
contract TestnetPaymaster is IPaymaster {
function validateAndPayForPaymasterTransaction(
bytes32,
bytes32,
Transaction calldata _transaction
) external payable returns (bytes4 magic, bytes memory context) {
// 默认交易已经被接收(应该是指通过交易验证,指差提供足够的交易费用即可执行)
// 应该是函数的函数选择器
magic = PAYMASTER_VALIDATION_SUCCESS_MAGIC;
// 只能由系统合约 bootloader 调用
require(
msg.sender == BOOTLOADER_ADDRESS,
"Only bootloader can call this contract"
);
// 输入大于 4 ,因为至少要有函数选择器
require(
_transaction.paymasterInput.length >= 4,
"The standard paymaster input must be at least 4 bytes long"
);
// 获得函数选择器
bytes4 paymasterInputSelector = bytes4(
_transaction.paymasterInput[0:4]
);
// 检查函数选择器是否对应 approvalBased 函数的函数选择器
if (paymasterInputSelector == IPaymasterFlow.approvalBased.selector) {
// While the actual data consists of address, uint256 and bytes data,
// the data is not needed for the testnet paymaster
// 数据由地址、uint256、字节数据组成,但 testnet 上的 paymaster 并不需要
(address token, uint256 amount, ) = abi.decode(
_transaction.paymasterInput[4:],
(address, uint256, bytes)
);
// 首先检查用户(交易的发起方)是否提供了足够的限额
address userAddress = address(uint160(_transaction.from));
address thisAddress = address(this);
uint256 providedAllowance = IERC20(token).allowance(
userAddress,
thisAddress
);
require(
providedAllowance >= amount,
"The user did not provide enough allowance"
);
// 测试网上的 paymaster 用 X wei 的 ETH 交换 X wei 的 token(1:1交换)
uint256 requiredETH = _transaction.gasLimit *
_transaction.maxFeePerGas;
if (amount < requiredETH) {
// 虽然条款中明确规定,用户少付了费用给 paymaster 交易将不会被接收(因为这可能会导致交易得不到足够的gas),
// 但我们并不希望交易会 revert,因为对于费用的估计,我们允许用户提供更少数量的资金来维持财产,
// 如果使用 X gas 能使交易成功,那么 X+1 gas 同样可以
magic = bytes4(0);
}
// 从用户处拉取所有的 tokens
try
IERC20(token).transferFrom(userAddress, thisAddress, amount)
{} catch (bytes memory revertReason) {
// 如果 revert 原因为空或仅由函数选择器表示,我们将用更用户友好的消息替换错误
//(应该就是让错误更加具有可读性)
if (revertReason.length <= 4) {
revert("Failed to transferFrom from users' account");
} else {
assembly {
revert(add(0x20, revertReason), mload(revertReason))
}
}
}
// bootloader 从不返回任何数据,所以可以安全的忽略
//(这里应该是项 bootloader 转账,提供足够的交易费用)
(bool success, ) = payable(BOOTLOADER_ADDRESS).call{
value: requiredETH
}("");
require(success, "Failed to transfer funds to the bootloader");
} else {
revert("Unsupported paymaster flow");
}
}
function postTransaction(
bytes calldata _context,
Transaction calldata _transaction,
bytes32,
bytes32,
ExecutionResult _txResult,
uint256 _maxRefundedGas
) external payable override {
// Nothing to do
}
receive() external payable {}
}
官方的交互示范脚本:
import { ethers } from "ethers";
import { Provider, Contract, utils, Signer } from "zksync-ethers";
// 希望执行合约的地址,过去部署的 ZeekMessages 合约的地址
const ZEEK_MESSAGES_CONTRACT_ADDRESS = "";
// ERC20 代币的合约地址
const TOKEN_CONTRACT_ADDRESS = "";
// Message to be sent to the contract
const NEW_MESSAGE = "This tx cost me no ETH!";
(async () => {
try {
// Note that the script needs the ABI which is generated from the compilation artifact.
// Make sure contract is compiled and artifacts are generated
const messagesContractArtifactsPath = `browser/artifacts/contracts/zksync_first.sol/ZeekMessages.json`;
const tokenContractArtifactsPath = `browser/artifacts/contracts/zksync_erc20.sol/TestToken.json`;
const messagesContractABI = JSON.parse(
await remix.call("fileManager", "getFile", messagesContractArtifactsPath)
);
const tokenContractABI = JSON.parse(
await remix.call("fileManager", "getFile", tokenContractArtifactsPath)
);
console.log("Sending a transaction via the testnet paymaster");
const browserProvider = new ethers.providers.Web3Provider(web3Provider);
const zkProvider = new Provider("https://sepolia.era.zksync.dev");
// const signer = (new ethers.providers.Web3Provider(web3Provider)).getSigner(0)
const zkSigner = Signer.from(browserProvider.getSigner(), zkProvider);
// const walletAddress = await signer.getAddress();
const walletAddress = await zkSigner.getAddress();
console.log(walletAddress);
// initialise messages and token contracts with address, abi and signer
// 初始化合约,通过合约地址、abi、签名者
const messagesContract = new Contract(
ZEEK_MESSAGES_CONTRACT_ADDRESS,
messagesContractABI.abi,
zkSigner
);
const tokenContract = new Contract(TOKEN_CONTRACT_ADDRESS, tokenContractABI.abi, zkSigner);
// 检索并打印当前钱包余额
let ethBalance = await zkProvider.getBalance(walletAddress);
console.log(`Account ${walletAddress} has ${ethers.utils.formatEther(ethBalance)} ETH`);
let tokenBalance = await tokenContract.balanceOf(walletAddress);
console.log(
`Account ${walletAddress} has ${ethers.utils.formatUnits(tokenBalance, 18)} tokens`
);
//检索测试网上的 paymaster 地址(应该是官方的示例合约地址)
const testnetPaymasterAddress = await zkProvider.getTestnetPaymasterAddress();
console.log(`Testnet paymaster address is ${testnetPaymasterAddress}`);
// 获取当前 gas 费用
const gasPrice = await zkProvider.getGasPrice();
console.log("gasPrice >> ", gasPrice);
// 明确 paymaster 的 gas 估计范围
//(应该是测试 1 个 ERC20 token 可代付的 gas 数量)
const paramsForFeeEstimation = utils.getPaymasterParams(testnetPaymasterAddress, {
type: "ApprovalBased",
token: TOKEN_CONTRACT_ADDRESS,
// 设置 minimalAllowance 为 1 来估计
// 授权支出的 erc20 代币数量
minimalAllowance: ethers.BigNumber.from(1),
// 空字节,因为测试网 paymaster 不使用 innerInput
innerInput: new Uint8Array(0),
});
// 通过 paymaster 估计 gasLimit
//(估计执行目标函数所需要花费的 gas Limit)
const gasLimit = await messagesContract.estimateGas.sendMessage(NEW_MESSAGE, {
customData: {
gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT,
paymasterParams: paramsForFeeEstimation,
},
});
console.log("gasLimit >> ", gasLimit);
// 使用 ETH 计算的费用将与使用测试网 paymaster 的 ERC20 代币相同
//(计算出所需要的 ETH 费用)
const fee = gasPrice * gasLimit;
console.log("Fee >>", fee);
// 新的 paymaster 的参数作为费用的 minimalAllowance(在合约中我们可以看到 token 和 ETH 是 1:1兑换的 )
const paymasterParams = utils.getPaymasterParams(testnetPaymasterAddress, {
type: "ApprovalBased",
token: TOKEN_CONTRACT_ADDRESS,
// provide estimated fee as allowance
minimalAllowance: fee,
// empty bytes as testnet paymaster does not use innerInput
innerInput: new Uint8Array(0),
});
// 完全覆盖对象,包含了 maxFeePerGas 和 maxPriorityFeePerGas
//(应该就是使用 paymaster 需要额外添加的交易参数)
const txOverrides = {
maxFeePerGas: gasPrice,
// 为优先打包愿意多付的 gas 费用
maxPriorityFeePerGas: "1",
gasLimit,
customData: {
gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT,
paymasterParams,
},
};
console.log("overrides >> ", txOverrides);
console.log(`Sign the transaction in your wallet`);
// 发送附有 paymaster 参数进行重载的交易
const txHandle = await messagesContract.sendMessage(NEW_MESSAGE, txOverrides);
console.log(
`Transaction ${txHandle.hash} with fee ${ethers.utils.formatUnits(
fee,
18
)} ERC20 tokens, sent via paymaster ${testnetPaymasterAddress}`
);
await txHandle.wait();
console.log(`Transaction processed`);
ethBalance = await zkProvider.getBalance(walletAddress);
tokenBalance = await tokenContract.balanceOf(walletAddress);
console.log(`Account ${walletAddress} now has ${ethers.utils.formatEther(ethBalance)} ETH`);
console.log(
`Account ${walletAddress} now has ${ethers.utils.formatUnits(tokenBalance, 18)} tokens`
);
console.log(`Done!`);
} catch (e) {
console.error("Error in script!");
console.error(e.message);
console.error(e);
}
})();
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!