之前文章 zkSync 帳戶抽象 (Account Abstraction) 程式範例說明了 zkSync Era 帳戶抽象的用法,這篇繼續說明另一個 EIP-4337 的擴充功能 - Paymaster。這在 zkSync Era 中也有原生的支持。雖然是帳戶抽象中的擴充功能,實際上並不是只有抽象帳戶才能使用,EOA 帳戶也能使用,是一個獨立的模組。
說明
Paymaster 主要的功能是能自訂交易發起人的 Gas 手續費支付方式。以往錢包需要 ETH 才能發送交易,透過 Paymaster 的機制可以解決這問題,可以用各種自訂的方式來代為支付 Gas。例如使用 USDC 等代幣來支付,甚至是免費代為支付。
運作機制是需要先使用智能合約實作 Paymaster,在合約中定義支付的方式,最後在發起的交易 Tx 中指定要使用的 Paymaster。
要實作一個 Paymaster,必須實作的介面如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| interface IPaymaster { function validateAndPayForPaymasterTransaction( bytes32 _txHash, bytes32 _suggestedSignedHash, Transaction calldata _transaction ) external payable returns (bytes4 magic, bytes memory context);
function postTransaction( bytes calldata _context, Transaction calldata _transaction, bytes32 _txHash, bytes32 _suggestedSignedHash, ExecutionResult _txResult, uint256 _maxRefundedGas ) external payable; }
|
介面可以參考官方 IPaymaster 原始碼。
類型
在發送交易的時候依據可以帶入不同的資料在 customData
的 paymasterParams
之中。
paymasterParams
包含:
- paymaster:Paymaster 合約地址。
- paymasterInput:包含類型和其他資訊的 bytes 資料,類似 calldata。
zksync 套件提供能生成 paymasterParams 的函式,所以這部分大概知道就好了,可以參考後面程式範例。
目前 Paymaster 有兩種類型:General 和 ApprovalBased。
Genernal
Genernal 類型的 paymasterInput
資料為如下的編碼:
1
| function general(bytes calldata data);
|
例如
1
| 0x8c5a344500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000
|
Genernal 類型的 EOA 帳號不會做任何額外的動作。
ApprovalBased
ApprovalBased 類型的 paymasterInput
資料為如下的編碼:
1 2 3 4 5
| function approvalBased( address _token, uint256 _minAllowance, bytes calldata _innerInput )
|
例如
1
| 0x949431dc000000000000000000000000111c3e89ce80e62ee88318c2804920d4c96f92bb000000000000000000000000000000000000000000000000120eb850e8d7c00000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000
|
ApprovalBased 類型的 EOA 帳號會同時執行類似 _token.approve(paymaster, _minAllowance)
的動作,讓 Paymaster 後續能夠扣款。
可以參考 Paymaster 官方文件。
前置準備
以下的範例原始碼放在 Github 上,可以直接參考。和之前文章一樣先實做一個簡單的智能合約,用來之後呼叫之用
1 2 3 4 5 6 7 8 9 10
| pragma solidity ^0.8.0;
contract Hello { address public caller;
function hi() external { caller = msg.sender; } }
|
接著安裝 zkSync 官方的套件 @matterlabs/zksync-contracts
,來進行後續開發:
1
| npm install --save-dev @matterlabs/zksync-contracts
|
繼承 IPaymaster
介面開始實作:
1 2 3 4 5 6 7 8
| pragma solidity ^0.8.0;
import "@matterlabs/zksync-contracts/l2/system-contracts/interfaces/IPaymaster.sol";
contract GeneralPaymaster is IPaymaster { }
|
後面會實作 General 和 ApprovalBased 兩種範例。
範例
以下的範例原始碼都放在 Github 上。
General
合約
GeneralPaymaster 原始碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| pragma solidity ^0.8.0;
import "@matterlabs/zksync-contracts/l2/system-contracts/Constants.sol"; import "@matterlabs/zksync-contracts/l2/system-contracts/interfaces/IPaymaster.sol"; import "@matterlabs/zksync-contracts/l2/system-contracts/interfaces/IPaymasterFlow.sol";
contract GeneralPaymaster is IPaymaster { using Address for address payable;
modifier onlyBootloader() { require(msg.sender == BOOTLOADER_FORMAL_ADDRESS, "Only bootloader can call this method"); _; }
function validateAndPayForPaymasterTransaction( bytes32, bytes32, Transaction calldata _transaction ) external payable onlyBootloader returns (bytes4 magic, bytes memory context) { 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.general.selector) { uint256 requiredETH = _transaction.gasLimit * _transaction.maxFeePerGas;
payable(BOOTLOADER_FORMAL_ADDRESS).sendValue(requiredETH); } else { revert("Unsupported paymaster flow in paymasterParams."); } }
function postTransaction( bytes calldata, Transaction calldata, bytes32, bytes32, ExecutionResult, uint256 ) external payable override onlyBootloader {}
receive() external payable {} }
|
部署
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| const helloArtifact = await deployer.loadArtifact('Hello'); const paymasterArtifact = await deployer.loadArtifact('GeneralPaymaster');
const hello = await deployer.deploy(helloArtifact);
const paymaster = await deployer.deploy(paymasterArtifact);
await ( await wallet.sendTransaction({ to: paymaster.address, value: ethers.utils.parseEther('0.01') }) ).wait();
const owner = Wallet.createRandom().connect(provider);
|
測試
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| const paymasterParams = utils.getPaymasterParams(paymaster.address, { type: 'General', innerInput: '0x' });
const gasLimit = await hello.connect(owner).estimateGas.hi( { customData: { paymasterParams, gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT } } ); const gasPrice = await provider.getGasPrice();
await ( await hello .connect(owner) .hi({ gasPrice, gasLimit, customData: { paymasterParams, gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT, } }) ).wait();
const caller = await hello.caller();
|
ApprovalBased
合約
真的要應用的話必須接上報價機制,參考官方使用 API3 範例。這裡只簡單示範,ApprovalPaymaster 原始碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
| pragma solidity ^0.8.0;
import "@matterlabs/zksync-contracts/l2/system-contracts/Constants.sol"; import "@matterlabs/zksync-contracts/l2/system-contracts/interfaces/IPaymaster.sol"; import "@matterlabs/zksync-contracts/l2/system-contracts/interfaces/IPaymasterFlow.sol";
contract ApprovalPaymaster is IPaymaster { using Address for address payable; using SafeERC20 for IERC20;
uint256 public price = 2000; address public allowedToken;
modifier onlyBootloader() { require(msg.sender == BOOTLOADER_FORMAL_ADDRESS, "Only bootloader can call this method"); _; }
constructor(address _erc20) { allowedToken = _erc20; }
function validateAndPayForPaymasterTransaction( bytes32, bytes32, Transaction calldata _transaction ) external payable onlyBootloader returns (bytes4 magic, bytes memory context) { 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) { (address token, uint256 amount, bytes memory data) = abi.decode( _transaction.paymasterInput[4:], (address, uint256, bytes) );
require(token == allowedToken, "Invalid token");
uint requiredETH = _transaction.gasLimit * _transaction.maxFeePerGas;
uint requiredToken = requiredETH * price; require(amount >= requiredToken, "Not the required amount of tokens sent");
address userAddress = address(uint160(_transaction.from)); IERC20(token).safeTransferFrom(userAddress, address(this), requiredToken);
payable(BOOTLOADER_FORMAL_ADDRESS).sendValue(requiredETH); } else { revert("Unsupported paymaster flow"); } }
function postTransaction( bytes calldata, Transaction calldata, bytes32, bytes32, ExecutionResult, uint256 ) external payable override onlyBootloader {}
receive() external payable {} }
|
部署
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| const tokenArtifact = await deployer.loadArtifact('MyToken'); const helloArtifact = await deployer.loadArtifact('Hello'); const paymasterArtifact = await deployer.loadArtifact('ApprovalPaymaster');
const token = await deployer.deploy(tokenArtifact);
const hello = await deployer.deploy(helloArtifact);
const paymaster = await deployer.deploy(paymasterArtifact, [token.address]);
await ( await wallet.sendTransaction({ to: paymaster.address, value: ethers.utils.parseEther('0.01') }) ).wait();
const owner = Wallet.createRandom().connect(provider);
await token.transfer(owner.address, ethers.utils.parseEther('1000'));
|
測試
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| const baseParams: any = { type: 'ApprovalBased', token: token.address, innerInput: '0x' }; let paymasterParams = utils.getPaymasterParams(paymaster.address, { ...baseParams, minimalAllowance: ethers.utils.parseEther('30') });
const gasLimit = await hello.connect(owner).estimateGas.hi( { customData: { paymasterParams, gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT } } ); const gasPrice = await provider.getGasPrice();
paymasterParams = utils.getPaymasterParams(paymaster.address, { ...baseParams, minimalAllowance: gasLimit.mul(gasPrice).mul(2000) });
await ( await hello .connect(owner) .hi({ gasPrice, gasLimit, customData: { paymasterParams: paymasterParams, gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT } }) ).wait();
const caller = await hello.caller();
|
ApprovalBased + AA
上面兩個範例是使用 EOA 錢包,也可以和 zksync Era 的 AA 同時使用。合約部分參考上面和之前的文章。
部署
基本上就是把兩個範例混在一起
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| const tokenArtifact = await deployer.loadArtifact('MyToken'); const helloArtifact = await deployer.loadArtifact('Hello'); const factoryArtifact = await deployer.loadArtifact('MyFactory'); const accountArtifact = await deployer.loadArtifact('MyAccount'); const paymasterArtifact = await deployer.loadArtifact('ApprovalPaymaster');
const token = await deployer.deploy(tokenArtifact);
const hello = await deployer.deploy(helloArtifact);
const paymaster = await deployer.deploy(paymasterArtifact, [token.address]);
await ( await wallet.sendTransaction({ to: paymaster.address, value: ethers.utils.parseEther('0.01') }) ).wait();
const factory = await deployer.deploy( factoryArtifact, [utils.hashBytecode(accountArtifact.bytecode)], undefined, [accountArtifact.bytecode] );
const owner = Wallet.createRandom().connect(provider);
const salt = ethers.constants.HashZero;
(await factory.deployAccount(salt, owner.address)).wait();
const abiCoder = new ethers.utils.AbiCoder(); const accountAddress = utils.create2Address( factory.address, await factory.aaBytecodeHash(), salt, abiCoder.encode(['address'], [owner.address]) );
await token.transfer(accountAddress, ethers.utils.parseEther('1000'));
|
測試
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| const baseParams: any = { type: 'ApprovalBased', token: token.address, innerInput: '0x' }; let paymasterParams = utils.getPaymasterParams(paymaster.address, { ...baseParams, minimalAllowance: ethers.utils.parseEther('30') });
let aaTx = await hello.populateTransaction.hi({ customData: { paymasterParams, gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT } }); const gasLimit = await provider.estimateGas(aaTx); const gasPrice = await provider.getGasPrice();
paymasterParams = utils.getPaymasterParams(paymaster.address, { ...baseParams, minimalAllowance: gasLimit.mul(gasPrice).mul(2000) });
aaTx = { ...aaTx, from: accountAddress, gasLimit, gasPrice, chainId: (await provider.getNetwork()).chainId, nonce: await provider.getTransactionCount(accountAddress), type: 113, customData: { paymasterParams, gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT } as types.Eip712Meta, value: ethers.BigNumber.from(0) };
const signer = new EIP712Signer(owner, aaTx.chainId!); aaTx.customData = { ...aaTx.customData, customSignature: await signer.sign(aaTx) }; (await provider.sendTransaction(utils.serialize(aaTx))).wait();
const caller = await hello.caller();
|
帳戶抽象運作流程
之前文章的流程沒有使用 Paymaster,帳戶抽象加上使用 Paymaster 的流程為下:
- 驗證 nonce 是否正確。
- 呼叫
validateTransaction()
驗證交易是否可執行,同時確認 nonce 標記為使用過。 - 呼叫
prepareForPaymaster()
,如果執行成功會,接著執行 Paymaster 的 validateAndPayForPaymasterTransaction()
,成功會繼續往下。 - 呼叫
executeTransaction()
執行交易。
實測結果
這是一個測試鏈上抽象帳戶使用 Paymaster 的交易,在區塊瀏覽器上可以看到發起者支付 MT 代幣,而 ETH Gas 手續費則由 Paymaster 來支付:
延伸閱讀
zkSync 帳戶抽象 (Account Abstraction) 程式範例
使用 Ethers.js 操作 zkSync Paymaster
使用 Web3.js 操作 zkSync Paymaster