对 zksync 官方 Paymaster 示例代码的理解

  • nilliol
  • 更新于 2024-07-20 22:05
  • 阅读 787

对官方给的 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);
    }
})();
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
nilliol
nilliol
0xbe3e...29A9
web3 的学习者,寻找实习机会中。 博客地址:https://llwh2333.github.io