ERC-4337: 계정 추상화 - 테스트를 통한 Paymaster와 LegacyTokenPaymaster의 동작 이해

ERC-4337: 계정 추상화 · 5/7
  1. ERC-4337: 계정 추상화
  2. 테스트를 통한 Account와 EntryPoint의 동작 이해
  3. 테스트 수정 사항
  4. 테스트를 통한 Account Factory의 동작 이해
  5. 테스트를 통한 Paymaster와 LegacyTokenPaymaster의 동작 이해
  6. 테스트를 통한 Aggregator의 동작 이해
  7. 간단 정리

테스트에 참고한 컨트랙트

이전 게시글에서 이어집니다.


BasePaymaster의 특징

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.23;

/* solhint-disable reason-string */

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import "../interfaces/IPaymaster.sol";
import "../interfaces/IEntryPoint.sol";
import "./UserOperationLib.sol";
/**
 * Helper class for creating a paymaster.
 * provides helper methods for staking.
 * Validates that the postOp is called only by the entryPoint.
 */
abstract contract BasePaymaster is IPaymaster, Ownable {
    IEntryPoint public immutable entryPoint;

    uint256 internal constant PAYMASTER_VALIDATION_GAS_OFFSET = UserOperationLib.PAYMASTER_VALIDATION_GAS_OFFSET;
    uint256 internal constant PAYMASTER_POSTOP_GAS_OFFSET = UserOperationLib.PAYMASTER_POSTOP_GAS_OFFSET;
    uint256 internal constant PAYMASTER_DATA_OFFSET = UserOperationLib.PAYMASTER_DATA_OFFSET;

    constructor(IEntryPoint _entryPoint) Ownable(msg.sender) {
        _validateEntryPointInterface(_entryPoint);
        entryPoint = _entryPoint;
    }

    //sanity check: make sure this EntryPoint was compiled against the same
    // IEntryPoint of this paymaster
    function _validateEntryPointInterface(IEntryPoint _entryPoint) internal virtual {
        require(IERC165(address(_entryPoint)).supportsInterface(type(IEntryPoint).interfaceId), "IEntryPoint interface mismatch");
    }

    /// @inheritdoc IPaymaster
    function validatePaymasterUserOp(
        PackedUserOperation calldata userOp,
        bytes32 userOpHash,
        uint256 maxCost
    ) external override returns (bytes memory context, uint256 validationData) {
        _requireFromEntryPoint();
        return _validatePaymasterUserOp(userOp, userOpHash, maxCost);
    }

    /**
     * Validate a user operation.
     * @param userOp     - The user operation.
     * @param userOpHash - The hash of the user operation.
     * @param maxCost    - The maximum cost of the user operation.
     */
    function _validatePaymasterUserOp(
        PackedUserOperation calldata userOp,
        bytes32 userOpHash,
        uint256 maxCost
    ) internal virtual returns (bytes memory context, uint256 validationData);

    /// @inheritdoc IPaymaster
    function postOp(
        PostOpMode mode,
        bytes calldata context,
        uint256 actualGasCost,
        uint256 actualUserOpFeePerGas
    ) external override {
        _requireFromEntryPoint();
        _postOp(mode, context, actualGasCost, actualUserOpFeePerGas);
    }

    /**
     * Post-operation handler.
     * (verified to be called only through the entryPoint)
     * @dev If subclass returns a non-empty context from validatePaymasterUserOp,
     *      it must also implement this method.
     * @param mode          - Enum with the following options:
     *                        opSucceeded - User operation succeeded.
     *                        opReverted  - User op reverted. The paymaster still has to pay for gas.
     *                        postOpReverted - never passed in a call to postOp().
     * @param context       - The context value returned by validatePaymasterUserOp
     * @param actualGasCost - Actual gas used so far (without this postOp call).
     * @param actualUserOpFeePerGas - the gas price this UserOp pays. This value is based on the UserOp's maxFeePerGas
     *                        and maxPriorityFee (and basefee)
     *                        It is not the same as tx.gasprice, which is what the bundler pays.
     */
    function _postOp(
        PostOpMode mode,
        bytes calldata context,
        uint256 actualGasCost,
        uint256 actualUserOpFeePerGas
    ) internal virtual {
        (mode, context, actualGasCost, actualUserOpFeePerGas); // unused params
        // subclass must override this method if validatePaymasterUserOp returns a context
        revert("must override");
    }

    /**
     * Add a deposit for this paymaster, used for paying for transaction fees.
     */
    function deposit() public payable {
        entryPoint.depositTo{value: msg.value}(address(this));
    }

    /**
     * Withdraw value from the deposit.
     * @param withdrawAddress - Target to send to.
     * @param amount          - Amount to withdraw.
     */
    function withdrawTo(
        address payable withdrawAddress,
        uint256 amount
    ) public onlyOwner {
        entryPoint.withdrawTo(withdrawAddress, amount);
    }

    /**
     * Add stake for this paymaster.
     * This method can also carry eth value to add to the current stake.
     * @param unstakeDelaySec - The unstake delay for this paymaster. Can only be increased.
     */
    function addStake(uint32 unstakeDelaySec) external payable onlyOwner {
        entryPoint.addStake{value: msg.value}(unstakeDelaySec);
    }

    /**
     * Return current paymaster's deposit on the entryPoint.
     */
    function getDeposit() public view returns (uint256) {
        return entryPoint.balanceOf(address(this));
    }

    /**
     * Unlock the stake, in order to withdraw it.
     * The paymaster can't serve requests once unlocked, until it calls addStake again
     */
    function unlockStake() external onlyOwner {
        entryPoint.unlockStake();
    }

    /**
     * Withdraw the entire paymaster's stake.
     * stake must be unlocked first (and then wait for the unstakeDelay to be over)
     * @param withdrawAddress - The address to send withdrawn value.
     */
    function withdrawStake(address payable withdrawAddress) external onlyOwner {
        entryPoint.withdrawStake(withdrawAddress);
    }

    /**
     * Validate the call is made from a valid entrypoint
     */
    function _requireFromEntryPoint() internal virtual {
        require(msg.sender == address(entryPoint), "Sender not EntryPoint");
    }
}
  • 추상(abstract) 컨트랙트로, 상속과 오버라이딩을 통해 _validatePaymasterUserOp 함수와 _postOp 함수를 구현해야 합니다.
  • 신뢰하는 EntryPoint로부터만 요청을 받습니다.
  • 검증 단계에서 validatePaymasterUserOp가 실행되고 사용자 작업이 실행되고 나면 후속 조치로 postOp가 실행됩니다.
  • EntryPoint 컨트랙트로 이더를 예치, 인출하는 기능과 이더를 스테이킹, 언스테이킹하는 기능이 구현되어 있습니다.

SimplePaymaster 구현

 BasePaymaster가 추상 컨트랙트로 작성되었으므로 이를 상속하여 화이트리스트에 기반하여 수수료를 대납해 주는 SimplePaymaster를 다음과 같이 간단하게 구현하였습니다. 

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.24;

import {IEntryPoint} from "account-abstraction/interfaces/IEntryPoint.sol";
import {BasePaymaster} from "account-abstraction/core/BasePaymaster.sol";
import {PackedUserOperation} from "account-abstraction/interfaces/PackedUserOperation.sol";
import {UserOperationLib} from "account-abstraction/core/UserOperationLib.sol";

contract SimplePaymaster is BasePaymaster {
    error SenderNotWhitelisted(address sender);
    error MaxCostExceeded(uint256 cost);

    using UserOperationLib for PackedUserOperation;

    uint256 constant SIG_VALIDATION_FAILED = 1;
    uint256 constant SIG_VALIDATION_SUCCESS = 0;

    uint256 private _maxCost;
    mapping(address => bool) private _whitelisted;

    event Whitelisted(address indexed account, bool state);
    event MaxCostChanged(uint256 maxCost);

    constructor(IEntryPoint _entryPoint, uint256 maxCost_) BasePaymaster(_entryPoint) {
        _maxCost = maxCost_;
    }

    function setWhitelisted(address account, bool state) external onlyOwner {
        _whitelisted[account] = state;
        emit Whitelisted(account, state);
    }

    function setWhitelistedBatch(address[] calldata accounts, bool state) external onlyOwner {
        for (uint256 i = 0; i < accounts.length; i++) {
            _whitelisted[accounts[i]] = state;
            emit Whitelisted(accounts[i], state);
        }
    }

    function isWhitelisted(address account) public view returns (bool) {
        return _whitelisted[account];
    }

    function setMaxCost(uint256 maxCost_) external onlyOwner {
        _maxCost = maxCost_;
        emit MaxCostChanged(maxCost_);
    }

    /**
     * Payment validation: check if paymaster agrees to pay.
     * Must verify sender is the entryPoint.
     * Revert to reject this request.
     * Note that bundlers will reject this method if it changes the state, unless the paymaster is trusted (whitelisted).
     * The paymaster pre-pays using its deposit, and receive back a refund after the postOp method returns.
     * @param userOp          - The user operation.
     * @param userOpHash      - Hash of the user's request data.
     * @param maxCost         - The maximum cost of this transaction (based on maximum gas and gas price from userOp).
     * @return context        - Value to send to a postOp. Zero length to signify postOp is not required.
     * @return validationData - Signature and time-range of this operation, encoded the same as the return
     *                          value of validateUserOperation.
     *                          <20-byte> sigAuthorizer - 0 for valid signature, 1 to mark signature failure,
     *                                                    other values are invalid for paymaster.
     *                          <6-byte> validUntil - last timestamp this operation is valid. 0 for "indefinite"
     *                          <6-byte> validAfter - first timestamp this operation is valid
     *                          Note that the validation code cannot use block.timestamp (or block.number) directly.
     */
    function _validatePaymasterUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 maxCost)
        internal
        view
        override
        returns (bytes memory context, uint256 validationData)
    {
        (userOpHash); // silence warnings (unused variables)

        address sender = userOp.getSender();

        if (!isWhitelisted(sender)) {
            revert SenderNotWhitelisted(sender);
        }

        if (maxCost > _maxCost) {
            revert MaxCostExceeded(maxCost);
        }

        return (abi.encode(sender), SIG_VALIDATION_SUCCESS);
    }

    /**
     * Post-operation handler.
     * Must verify sender is the entryPoint.
     * @param mode          - Enum with the following options:
     *                        opSucceeded - User operation succeeded.
     *                        opReverted  - User op reverted. The paymaster still has to pay for gas.
     *                        postOpReverted - never passed in a call to postOp().
     * @param context       - The context value returned by validatePaymasterUserOp
     * @param actualGasCost - Actual gas used so far (without this postOp call).
     * @param actualUserOpFeePerGas - the gas price this UserOp pays. This value is based on the UserOp's maxFeePerGas
     *                        and maxPriorityFee (and basefee)
     *                        It is not the same as tx.gasprice, which is what the bundler pays.
     */
    function _postOp(PostOpMode mode, bytes calldata context, uint256 actualGasCost, uint256 actualUserOpFeePerGas)
        internal
        virtual
        override
    {
        (mode, actualGasCost, actualUserOpFeePerGas); // silence warnings (unused variables
        address sender = _decodeContext(context);

        if (!isWhitelisted(sender)) {
            revert SenderNotWhitelisted(sender);
        }
    }

    function _encodeContext(address sender) internal pure returns (bytes memory) {
        return abi.encode(sender);
    }

    function _decodeContext(bytes memory context) internal pure returns (address) {
        return abi.decode(context, (address));
    }
}

 _validatePaymasterUserOp 함수의 구현은 다음과 같습니다.

  • sender가 화이트리스트에 등록되지 않았다면 검증 작업은 revert됩니다.
  • 대납할 금액 maxCost가 컨트랙트의 소유자가 정한 _maxCost보다 크다면 검증 작업은 revert 됩니다.
  • 검증이 성공하면 sender를 abi 인코딩하여 context 값으로, SIG_VALIDATION_SUCCESS(0)을 validationData로 반환합니다. 
function _validatePaymasterUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 maxCost)
    internal
    view
    override
    returns (bytes memory context, uint256 validationData)
{
    (userOpHash); // silence warnings (unused variables)

    address sender = userOp.getSender();

    if (!isWhitelisted(sender)) {
        revert SenderNotWhitelisted(sender);
    }

    if (maxCost > _maxCost) {
        revert MaxCostExceeded(maxCost);
    }

    return (_encodeContext(sender), SIG_VALIDATION_SUCCESS);
}

 _postOp 함수의 구현은 다음과 같습니다.

  • 사용자 작업의 성공 여부(mode)나 실제 청구된 가스비 등은 일단 무시합니다.
  • context 값을 디코딩하여 sender의 주소를 가져오고 sender가 화이트리스트에 등록되어 있는지 다시 한번 확인합니다. 등록되어 있지 않다면 revert 됩니다.
function _postOp(PostOpMode mode, bytes calldata context, uint256 actualGasCost, uint256 actualUserOpFeePerGas)
    internal
    virtual
    override
{
    (mode, actualGasCost, actualUserOpFeePerGas); // silence warnings (unused variables
    address sender = _decodeContext(context);

    if (!isWhitelisted(sender)) {
        revert SenderNotWhitelisted(sender);
    }
}

LegacyTokenPaymaster의 특징

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.23;

/* solhint-disable reason-string */

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "../core/BasePaymaster.sol";
import "../core/UserOperationLib.sol";
import "../core/Helpers.sol";

/**
 * A sample paymaster that defines itself as a token to pay for gas.
 * The paymaster IS the token to use, since a paymaster cannot use an external contract.
 * Also, the exchange rate has to be fixed, since it can't reference an external Uniswap or other exchange contract.
 * subclass should override "getTokenValueOfEth" to provide actual token exchange rate, settable by the owner.
 * Known Limitation: this paymaster is exploitable when put into a batch with multiple ops (of different accounts):
 * - while a single op can't exploit the paymaster (if postOp fails to withdraw the tokens, the user's op is reverted,
 *   and then we know we can withdraw the tokens), multiple ops with different senders (all using this paymaster)
 *   in a batch can withdraw funds from 2nd and further ops, forcing the paymaster itself to pay (from its deposit)
 * - Possible workarounds are either use a more complex paymaster scheme (e.g. the DepositPaymaster) or
 *   to whitelist the account and the called method ids.
 */
contract LegacyTokenPaymaster is BasePaymaster, ERC20 {
    using UserOperationLib for PackedUserOperation;

    //calculated cost of the postOp
    uint256 constant public COST_OF_POST = 15000;

    address public immutable theFactory;

    constructor(address accountFactory, string memory _symbol, IEntryPoint _entryPoint) ERC20(_symbol, _symbol) BasePaymaster(_entryPoint) {
        theFactory = accountFactory;
        //make it non-empty
        _mint(address(this), 1);

        //owner is allowed to withdraw tokens from the paymaster's balance
        _approve(address(this), msg.sender, type(uint256).max);
    }

    /**
     * helpers for owner, to mint and withdraw tokens.
     * @param recipient - the address that will receive the minted tokens.
     * @param amount - the amount it will receive.
     */
    function mintTokens(address recipient, uint256 amount) external onlyOwner {
        _mint(recipient, amount);
    }

    /**
     * transfer paymaster ownership.
     * owner of this paymaster is allowed to withdraw funds (tokens transferred to this paymaster's balance)
     * when changing owner, the old owner's withdrawal rights are revoked.
     */
    function transferOwnership(address newOwner) public override virtual onlyOwner {
        // remove allowance of current owner
        _approve(address(this), owner(), 0);
        super.transferOwnership(newOwner);
        // new owner is allowed to withdraw tokens from the paymaster's balance
        _approve(address(this), newOwner, type(uint256).max);
    }

    //Note: this method assumes a fixed ratio of token-to-eth. subclass should override to supply oracle
    // or a setter.
    function getTokenValueOfEth(uint256 valueEth) internal view virtual returns (uint256 valueToken) {
        return valueEth / 100;
    }

    /**
      * validate the request:
      * if this is a constructor call, make sure it is a known account.
      * verify the sender has enough tokens.
      * (since the paymaster is also the token, there is no notion of "approval")
      */
    function _validatePaymasterUserOp(PackedUserOperation calldata userOp, bytes32 /*userOpHash*/, uint256 requiredPreFund)
    internal view override returns (bytes memory context, uint256 validationData) {
        uint256 tokenPrefund = getTokenValueOfEth(requiredPreFund);

        uint256 postOpGasLimit = userOp.unpackPostOpGasLimit();
        require( postOpGasLimit > COST_OF_POST, "TokenPaymaster: gas too low for postOp");

        if (userOp.initCode.length != 0) {
            _validateConstructor(userOp);
            require(balanceOf(userOp.sender) >= tokenPrefund, "TokenPaymaster: no balance (pre-create)");
        } else {

            require(balanceOf(userOp.sender) >= tokenPrefund, "TokenPaymaster: no balance");
        }

        return (abi.encode(userOp.sender), SIG_VALIDATION_SUCCESS);
    }

    // when constructing an account, validate constructor code and parameters
    // we trust our factory (and that it doesn't have any other public methods)
    function _validateConstructor(PackedUserOperation calldata userOp) internal virtual view {
        address factory = address(bytes20(userOp.initCode[0 : 20]));
        require(factory == theFactory, "TokenPaymaster: wrong account factory");
    }

    /**
     * actual charge of user.
     * this method will be called just after the user's TX with mode==OpSucceeded|OpReverted (account pays in both cases)
     * BUT: if the user changed its balance in a way that will cause  postOp to revert, then it gets called again, after reverting
     * the user's TX , back to the state it was before the transaction started (before the validatePaymasterUserOp),
     * and the transaction should succeed there.
     */
    function _postOp(PostOpMode mode, bytes calldata context, uint256 actualGasCost, uint256 actualUserOpFeePerGas) internal override {
        //we don't really care about the mode, we just pay the gas with the user's tokens.
        (mode);
        address sender = abi.decode(context, (address));
        uint256 charge = getTokenValueOfEth(actualGasCost + COST_OF_POST * actualUserOpFeePerGas);
        //actualGasCost is known to be no larger than the above requiredPreFund, so the transfer should succeed.
        _transfer(sender, address(this), charge);
    }
}
  • 그 자체로 ERC-20 토큰 컨트랙트이면서 Paymaster 컨트랙트입니다.
  • _validatePaymasterUserOp 함수에서 사용자가 대납을 요구한 비용을 토큰으로 치환하여 사용자의 토큰 잔액이 tokenPrefund 이상인지 검증합니다.
  • 만약 sender가 initCode를 통해 새로 생선 된 스마트 계정이라면, _validateConstructor 함수를 통해 Paymaster가 신뢰하는 Factory로부터 생성된 스마트 계정인지 검증합니다.
  • 이더와 토큰의 교환비는 100:1로 하드코딩 되어 있습니다.
  • _postOp 함수는 후속 조치로 실제로 사용된 수수료 + Paymaster에서 부과하는 수수료만큼의 토큰을 스마트 계정에서 인출해 갑니다.

Foundry 테스트 구성

Paymaster.t.sol 파일 생성

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.24;

import {Test, console} from "forge-std/Test.sol";
import {IEntryPoint} from "account-abstraction/interfaces/IEntryPoint.sol";
import {SimpleEntryPoint} from "../src/SimpleEntryPoint.sol";
import {SenderCreator} from "account-abstraction/core/SenderCreator.sol";
import {SimpleAccountFactory} from "../src/SimpleAccountFactory.sol";
import {SimpleAccount} from "../src/SimpleAccount.sol";
import {SimplePaymaster} from "../src/SimplePaymaster.sol";
import {LegacyTokenPaymaster} from "account-abstraction/samples/LegacyTokenPaymaster.sol";
import {PackedUserOperation} from "account-abstraction/interfaces/PackedUserOperation.sol";
import {Counter} from "../src/Counter.sol";
import {UserOpUtils} from "./utils/UserOpUtils.sol";
import "./utils/Artifacts.sol";

contract PaymasterTest is Test {
	...
}
  • Test, console : 테스트를 위한 컨트랙트와 라이브러리
  • IEntryPoint, SimpleEntryPoint : EntryPoint 컨트랙트와 인터페이스
  • SenderCreator : EntryPoint에게 sender 생성 요청을 받아 Factory로 전달하는 브로커
  • SimpleAccountFactory  : 스마트 계정의 팩토리 컨트랙트
  • SimpleAccount  : 계정 추상화 - 스마트 계정 구현체
  • SimplePaymaster : 방금 BasePaymaster를 상속하여 구현한 화이트리스트 기반 Paymaster
  • LegacyTokenPaymaster : 토큰으로 스마트 계정에 비용을 청구하는 Paymaster
  • PackedUserOperation : UserOperation을 패킹한 구조체 타입
  • Counter : Foundry 프로젝트 생성 시에 딸려오는 기본 컨트랙트로, 간단한 작업 요청에 사용하기 위한 용도
  • UserOpUtils : 테스트용 UserOp를 생성하고 서명하기 위한 유틸리티 컨트랙트. test/utils/UserOpUtils.sol 파일 참고
  • Artifacts : /posts/162/ 참고. 컨트랙트를 배포해서 사용해도 무방. 이 방식을 사용한다고 해서 별다른 이점은 없습니다.

테스트 변수 및 이벤트 선언

error SenderNotWhitelisted(address sender);
error FailedOp(uint256 opIndex, string reason);
error FailedOpWithRevert(uint256 opIndex, string reason, bytes inner);

uint256 public constant SALT = 10000;
uint256 public constant INITIAL_MAX_COST = 1 ether;
uint256 public constant PAYMASTER_DEPOSIT = 10 ether;

SimpleEntryPoint public entryPoint;
SenderCreator public senderCreator;
SimpleAccountFactory public factory;
SimpleAccount public impl;
SimplePaymaster public paymaster;
LegacyTokenPaymaster public tokenPaymaster;
Counter public counter;
UserOpUtils public utils;

uint256 public ownerPrivateKey = uint256(keccak256("owner"));
address public owner;
address public deployer;
address public beneficiary;
  • INITIAL_MAX_COST : 최대 1 이더의 가스비를 대납할 수 있도록 설정한 값입니다. PackedUserOperation을 생성할 때 다른 값들을 기본적으로 높게 잡아놔서 1 이더로 여유롭게 잡아놨습니다.
  • PAYMASTER_DEPOSIT : Paymaster에서 EntryPoint로 예치될 예치금의 액수입니다.

setUp 함수

function setUp() public {
    owner = vm.addr(ownerPrivateKey);
    vm.label(owner, "Owner");
    vm.deal(owner, 100 ether);

    deployer = makeAddr("deployer");
    vm.label(deployer, "Deployer");
    vm.deal(deployer, 100 ether);

    beneficiary = makeAddr("beneficiary");
    vm.label(beneficiary, "Beneficiary");
    vm.deal(beneficiary, 1 ether);

    entryPoint = SimpleEntryPoint(payable(ENTRYPOINT_ADDRESS));
    vm.etch(address(entryPoint), ENTRYPOINT_BYTECODE);
    vm.label(address(entryPoint), "EntryPoint");

    senderCreator = SenderCreator(SENDERCREATOR_ADDRESS);
    vm.etch(address(senderCreator), SENDERCREATOR_BYTECODE);
    vm.label(address(senderCreator), "SenderCreator");

    factory = SimpleAccountFactory(FACTORY_ADDRESS);
    vm.etch(address(factory), FACTORY_BYTECODE);
    vm.label(address(factory), "SimpleAccountFactory");

    impl = SimpleAccount(payable(IMPL_ADDRESS));
    vm.etch(address(impl), IMPL_BYTECODE);
    vm.label(address(impl), "SimpleAccountImpl");

    vm.startPrank(deployer);
    paymaster = new SimplePaymaster(entryPoint, INITIAL_MAX_COST);
    vm.label(address(paymaster), "Paymaster");
    
    tokenPaymaster = new LegacyTokenPaymaster(address(factory), "LTP", entryPoint);
    vm.label(address(tokenPaymaster), "LegacyTokenPaymaster");

    counter = new Counter();
    vm.label(address(counter), "Counter");

    utils = new UserOpUtils();
    vm.label(address(utils), "UserOpUtils");

    paymaster.deposit{value: PAYMASTER_DEPOSIT}();
    tokenPaymaster.deposit{value: PAYMASTER_DEPOSIT}();

    vm.stopPrank();

    assertEq(entryPoint.balanceOf(address(paymaster)), PAYMASTER_DEPOSIT);
}

컨트랙트 배포에 이어 setUp 함수의 하단에서 SimplePaymaster와 LegacyTokenPaymaster의 deposit 함수를 통해 EntryPoint 컨트랙트에 10 이더를 예치하는 코드가 실행됩니다.

배포된 컨트랙트 목록

  • EntryPoint
  • SenderCreator
  • SimpleAccountFactory
  • SimpleAccount
  • SimplePaymaster
  • LegacyTokenPaymaster
  • Counter
  • UserOpUtils

테스트

[CASE 1] 화이트리스트에 등록된 sender의 수수료 대납

function test_HandleOpsWithWhitelistedAccountAndEthPayment() public {
    SimpleAccount account = factory.createAccount(owner, SALT);
    PackedUserOperation[] memory ops = getrUserOps(address(account), address(paymaster));

    vm.prank(deployer);
    paymaster.setWhitelisted(address(account), true);

    uint256 counterBefore = counter.number();
    uint256 depositBefore = entryPoint.balanceOf(address(paymaster));
    uint256 beneficiaryBalanceBefore = beneficiary.balance;

    entryPoint.handleOps(ops, payable(beneficiary));

    assertEq(counter.number(), counterBefore + 1);
    assertLt(entryPoint.balanceOf(address(paymaster)), depositBefore);
    assertGt(beneficiary.balance, beneficiaryBalanceBefore);
}

1. owner의 주소와 SALT를 사용해 스마트 계정을 생성합니다. handleOps 함수를 호출하는 과정에서 생성하는 것도 가능합니다만, 이 경우에는 initCode를 추가해 주셔야 합니다.

SimpleAccount account = factory.createAccount(owner, SALT);

2. handleOps 함수의 인수로 들어갈 ops를 생성합니다.

PackedUserOperation[] memory ops = getrUserOps(address(account), address(paymaster));

 getUserOps 함수는 다음과 같습니다. 이전에 PackedUserOperation을 생성하던 방식에서 Paymaster를 사용하기 위해 paymasterAndData가 추가되었습니다.

function getrUserOps(address accountAddr, address paymasterAddr) internal view returns (PackedUserOperation[] memory) {
    uint256 nonce = entryPoint.getNonce(accountAddr, 0);

    bytes memory callData = abi.encodeWithSelector(
        SimpleAccount.execute.selector, address(counter), 0, abi.encodeWithSelector(counter.increment.selector)
    );

    PackedUserOperation memory packedUserOp = utils.packUserOp(accountAddr, nonce, callData);

    bytes memory paymasterAndData = utils.packPaymasterAndData(address(paymasterAddr), 20000, 15000);

    packedUserOp.paymasterAndData = paymasterAndData;

    bytes32 userOpHash = entryPoint.getUserOpHash(packedUserOp);

    bytes memory signature = utils.signUserOp(ownerPrivateKey, userOpHash);

    packedUserOp.signature = signature;

    PackedUserOperation[] memory ops = new PackedUserOperation[](1);
    ops[0] = packedUserOp;

    return ops;
}

 paymasterAndData는 Paymaster의 주소 20바이트, validationGasLimit 16바이트, 그리고 postOpGasLimit 16바이트를 패킹하여 생성할 수 있습니다.

  • validationGasLimit : Paymaster 검증 단계에서의 가스 한도
  • postOpGasLimit : 작업을 실행하고 postOp를 실행하는 단계에서의 가스 한도 
function packPaymasterAndData(address paymaster, uint256 validationGasLimit, uint256 postOpGasLimit)
    public
    pure
    returns (bytes memory paymasterAndData)
{
    paymasterAndData =
        abi.encodePacked(paymaster, bytes16(uint128(validationGasLimit)), bytes16(uint128(postOpGasLimit)));
}

3. Paymaster 컨트랙트의 소유자(deployer)가 스마트 계정의 주소(account)를 화이트리스트에 등록합니다.

vm.prank(deployer);
paymaster.setWhitelisted(address(account), true);

4. handleOps 함수가 호출되기 전의 상태를 일부 기록하고 handleOps 함수를 호출합니다.

uint256 counterBefore = counter.number();
uint256 depositBefore = entryPoint.balanceOf(address(paymaster));
uint256 beneficiaryBalanceBefore = beneficiary.balance;

entryPoint.handleOps(ops, payable(beneficiary));

5. handleOps 함수의 검증 루프가 실행됩니다. 스마트 계정의 validateUserOp가 성공적으로 실행되고 난 뒤, paymasterAndData가 설정되어 있으므로 _validatePaymasterPrepayment 함수가 호출됩니다.

bytes memory context;
if (mUserOp.paymaster != address(0)) {
    (context, paymasterValidationData) = _validatePaymasterPrepayment(
        opIndex,
        userOp,
        outOpInfo,
        requiredPreFund
    );
}

 _validatePaymasterPrepayment 함수에서는 Paymaster 컨트랙트의 validatePaymasterUserOp 함수가 호출됩니다. 앞서 살펴봤다시피, validatePaymasterUserOp 함수에서는 sender가 화이트리스트에 등록되어 있는지 확인하고 requiredPreFund(maxCost)가 owner가 지정한 _maxCost를 넘어서는지 확인합니다. 그리고 sender의 주소를 인코딩하여 context로, 0을 validationData로 반환합니다.

function _validatePaymasterPrepayment(
    uint256 opIndex,
    PackedUserOperation calldata op,
    UserOpInfo memory opInfo,
    uint256 requiredPreFund
) internal returns (bytes memory context, uint256 validationData) {
    unchecked {
        uint256 preGas = gasleft();
        MemoryUserOp memory mUserOp = opInfo.mUserOp;
        address paymaster = mUserOp.paymaster;
        DepositInfo storage paymasterInfo = deposits[paymaster];
        uint256 deposit = paymasterInfo.deposit;
        if (deposit < requiredPreFund) {
            revert FailedOp(opIndex, "AA31 paymaster deposit too low");
        }
        paymasterInfo.deposit = deposit - requiredPreFund;
        uint256 pmVerificationGasLimit = mUserOp.paymasterVerificationGasLimit;
        try IPaymaster(paymaster).validatePaymasterUserOp{gas: pmVerificationGasLimit}(
            op, opInfo.userOpHash, requiredPreFund
        ) returns (bytes memory _context, uint256 _validationData) {
            context = _context;
            validationData = _validationData;
        } catch {
            revert FailedOpWithRevert(opIndex, "AA33 reverted", Exec.getReturnData(REVERT_REASON_MAX_LEN));
        }
        if (preGas - gasleft() > pmVerificationGasLimit) {
            revert FailedOp(opIndex, "AA36 over paymasterVerificationGasLimit");
        }
    }
}

6. 검증 루프가 마무리되고 실행 루프가 실행됩니다.

for (uint256 i = 0; i < opslen; i++) {
    collected += _executeUserOp(i, ops[i], opInfos[i]);
}

7. 사용자 작업이 성공하고 나면 또는 실패한 경우의 일부 케이스에 _postExecution 함수가 실행됩니다.

function _postExecution(IPaymaster.PostOpMode mode, UserOpInfo memory opInfo, bytes memory context, uint256 actualGas)
    private
    returns (uint256 actualGasCost)
{
    uint256 preGas = gasleft();
    unchecked {
        address refundAddress;
        MemoryUserOp memory mUserOp = opInfo.mUserOp;
        uint256 gasPrice = getUserOpGasPrice(mUserOp);

        address paymaster = mUserOp.paymaster;
        if (paymaster == address(0)) {
            refundAddress = mUserOp.sender;
        } else {
            refundAddress = paymaster;
            if (context.length > 0) {
                actualGasCost = actualGas * gasPrice;
                if (mode != IPaymaster.PostOpMode.postOpReverted) {
                    try IPaymaster(paymaster).postOp{gas: mUserOp.paymasterPostOpGasLimit}(
                        mode, context, actualGasCost, gasPrice
                    ) {
                        // solhint-disable-next-line no-empty-blocks
                    } catch {
                        bytes memory reason = Exec.getReturnData(REVERT_REASON_MAX_LEN);
                        revert PostOpReverted(reason);
                    }
                }
            }
        }
        actualGas += preGas - gasleft();

        // Calculating a penalty for unused execution gas
        {
            uint256 executionGasLimit = mUserOp.callGasLimit + mUserOp.paymasterPostOpGasLimit;
            uint256 executionGasUsed = actualGas - opInfo.preOpGas;
            // this check is required for the gas used within EntryPoint and not covered by explicit gas limits
            if (executionGasLimit > executionGasUsed) {
                uint256 unusedGas = executionGasLimit - executionGasUsed;
                uint256 unusedGasPenalty = (unusedGas * PENALTY_PERCENT) / 100;
                actualGas += unusedGasPenalty;
            }
        }

        actualGasCost = actualGas * gasPrice;
        uint256 prefund = opInfo.prefund;
        if (prefund < actualGasCost) {
            if (mode == IPaymaster.PostOpMode.postOpReverted) {
                actualGasCost = prefund;
                emitPrefundTooLow(opInfo);
                emitUserOperationEvent(opInfo, false, actualGasCost, actualGas);
            } else {
                assembly ("memory-safe") {
                    mstore(0, INNER_REVERT_LOW_PREFUND)
                    revert(0, 32)
                }
            }
        } else {
            uint256 refund = prefund - actualGasCost;
            _incrementDeposit(refundAddress, refund);
            bool success = mode == IPaymaster.PostOpMode.opSucceeded;
            emitUserOperationEvent(opInfo, success, actualGasCost, actualGas);
        }
    } // unchecked
}

Paymaster의 주소가 zero address가 아니고 context의 길이가 0보다 큰 경우, 후속 조치를 위해 Paymaster의 postOp 함수를 호출합니다. postOp 함수는 context를 디코딩하여 sender의 주소를 구한 뒤, sender가 화이트리스트에 등록되어 있는지 재확인을 합니다.

if (paymaster == address(0)) {
    refundAddress = mUserOp.sender;
} else {
    refundAddress = paymaster;
    if (context.length > 0) {
        actualGasCost = actualGas * gasPrice;
        if (mode != IPaymaster.PostOpMode.postOpReverted) {
            try IPaymaster(paymaster).postOp{
                gas: mUserOp.paymasterPostOpGasLimit
            }(mode, context, actualGasCost, gasPrice)
            // solhint-disable-next-line no-empty-blocks
            {} catch {
                bytes memory reason = Exec.getReturnData(REVERT_REASON_MAX_LEN);
                revert PostOpReverted(reason);
            }
        }
    }
}

8. 여차저차 handleOps 함수가 종료되고 나면 다음 사항들을 확인할 수 있습니다.

  • Counter의 number가 handleOps를 실행하기 전보다 1만큼 증가된 상태여야 합니다.
  • Paymaster의 예치금이 handleOps를 실행하기 전보다 감소된 상태여야 합니다.
  • beneficiary의 이더 잔액이 handleOps를 실행하기 전보다 증가된 상태여야 합니다. 
assertEq(counter.number(), counterBefore + 1);
assertLt(entryPoint.balanceOf(address(paymaster)), depositBefore);
assertGt(beneficiary.balance, beneficiaryBalanceBefore);

테스트

$ forge test --mt test_HandleOpsWithWhitelistedAccountAndEthPayment -vvvv
[⠢] Compiling...
[⠰] Compiling 2 files with 0.8.24
[⠔] Solc 0.8.24 finished in 4.13s
Compiler run successful!

Ran 1 test for test/PayMaster.t.sol:PaymasterTest
[PASS] test_HandleOpsWithWhitelistedAccountAndEthPayment() (gas: 353757)
Traces:
  [353757] PaymasterTest::test_HandleOpsWithWhitelistedAccountAndEthPayment()
    ├─ [152583] SimpleAccountFactory::createAccount(Owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], 10000 [1e4])
    │   ├─ [112211] → new ERC1967Proxy@0x75fe56829E9d4867837306E3bDDdcdC066416865
    │   │   ├─ emit Upgraded(implementation: SimpleAccountImpl: [0xffD4505B3452Dc22f8473616d50503bA9E1710Ac])
    │   │   ├─ [47945] SimpleAccountImpl::initialize(Owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266]) [delegatecall]
    │   │   │   ├─ emit SimpleAccountInitialized(entryPoint: EntryPoint: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], owner: Owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266])
    │   │   │   ├─ emit Initialized(version: 1)
    │   │   │   └─ ← [Stop] 
    │   │   └─ ← [Return] 183 bytes of code
    │   └─ ← [Return] ERC1967Proxy: [0x75fe56829E9d4867837306E3bDDdcdC066416865]
    ├─ [2768] EntryPoint::getNonce(ERC1967Proxy: [0x75fe56829E9d4867837306E3bDDdcdC066416865], 0) [staticcall]
    │   └─ ← [Return] 0
    ├─ [2819] UserOpUtils::packUserOp(ERC1967Proxy: [0x75fe56829E9d4867837306E3bDDdcdC066416865], 0, 0xb61d27f60000000000000000000000001240fa2a84dd9157a0e76b5cfe98b1d52268b264000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000004d09de08a00000000000000000000000000000000000000000000000000000000) [staticcall]
    │   └─ ← [Return] PackedUserOperation({ sender: 0x75fe56829E9d4867837306E3bDDdcdC066416865, nonce: 0, initCode: 0x, callData: 0xb61d27f60000000000000000000000001240fa2a84dd9157a0e76b5cfe98b1d52268b264000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000004d09de08a00000000000000000000000000000000000000000000000000000000, accountGasLimits: 0x00000000000000000000000000030d400000000000000000000000000000c350, preVerificationGas: 21000 [2.1e4], gasFees: 0x0000000000000000000000003b9aca00000000000000000000000004a817c800, paymasterAndData: 0x, signature: 0x })
    ├─ [961] UserOpUtils::packPaymasterAndData(Paymaster: [0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b], 50000 [5e4], 50000 [5e4]) [staticcall]
    │   └─ ← [Return] 0x8ad159a275aee56fb2334dbb69036e9c7bacee9b0000000000000000000000000000c3500000000000000000000000000000c350
    ├─ [1966] EntryPoint::getUserOpHash(PackedUserOperation({ sender: 0x75fe56829E9d4867837306E3bDDdcdC066416865, nonce: 0, initCode: 0x, callData: 0xb61d27f60000000000000000000000001240fa2a84dd9157a0e76b5cfe98b1d52268b264000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000004d09de08a00000000000000000000000000000000000000000000000000000000, accountGasLimits: 0x00000000000000000000000000030d400000000000000000000000000000c350, preVerificationGas: 21000 [2.1e4], gasFees: 0x0000000000000000000000003b9aca00000000000000000000000004a817c800, paymasterAndData: 0x8ad159a275aee56fb2334dbb69036e9c7bacee9b0000000000000000000000000000c3500000000000000000000000000000c350, signature: 0x })) [staticcall]
    │   └─ ← [Return] 0x0c1f5a8df00a8b09966afba22359a21c1f711d760268abf2e3edd5bd59933bba
    ├─ [3993] UserOpUtils::signUserOp(907111799109225873672206001743429201758838553092777504370151546632448000192 [9.071e74], 0x0c1f5a8df00a8b09966afba22359a21c1f711d760268abf2e3edd5bd59933bba) [staticcall]
    │   ├─ [0] VM::sign("<pk>", 0xdede256a55145101b75dd99012d647b492de0e07e2803fded291ef969bad9e1b) [staticcall]
    │   │   └─ ← [Return] 27, 0x3489fe97fb76cd33a7252a3ae0b9b898fab530ae6863055a35e28c7f83ecc3ea, 0x12390d572346a181b615588e3e13febfd3c6414e502e70d82d8e785184e394fd
    │   └─ ← [Return] 0x3489fe97fb76cd33a7252a3ae0b9b898fab530ae6863055a35e28c7f83ecc3ea12390d572346a181b615588e3e13febfd3c6414e502e70d82d8e785184e394fd1b
    ├─ [0] VM::prank(Deployer: [0xaE0bDc4eEAC5E950B67C6819B118761CaAF61946])
    │   └─ ← [Return] 
    ├─ [26205] Paymaster::setWhitelisted(ERC1967Proxy: [0x75fe56829E9d4867837306E3bDDdcdC066416865], true)
    │   ├─ emit Whitelisted(account: ERC1967Proxy: [0x75fe56829E9d4867837306E3bDDdcdC066416865], state: true)
    │   └─ ← [Stop] 
    ├─ [2283] Counter::number() [staticcall]
    │   └─ ← [Return] 0
    ├─ [2545] EntryPoint::balanceOf(Paymaster: [0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b]) [staticcall]
    │   └─ ← [Return] 10000000000000000000 [1e19]
    ├─ [105678] EntryPoint::handleOps([PackedUserOperation({ sender: 0x75fe56829E9d4867837306E3bDDdcdC066416865, nonce: 0, initCode: 0x, callData: 0xb61d27f60000000000000000000000001240fa2a84dd9157a0e76b5cfe98b1d52268b264000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000004d09de08a00000000000000000000000000000000000000000000000000000000, accountGasLimits: 0x00000000000000000000000000030d400000000000000000000000000000c350, preVerificationGas: 21000 [2.1e4], gasFees: 0x0000000000000000000000003b9aca00000000000000000000000004a817c800, paymasterAndData: 0x8ad159a275aee56fb2334dbb69036e9c7bacee9b0000000000000000000000000000c3500000000000000000000000000000c350, signature: 0x3489fe97fb76cd33a7252a3ae0b9b898fab530ae6863055a35e28c7f83ecc3ea12390d572346a181b615588e3e13febfd3c6414e502e70d82d8e785184e394fd1b })], Beneficiary: [0x5c4d2bd3510C8B51eDB17766d3c96EC637326999])
    │   ├─ [5460] ERC1967Proxy::validateUserOp(PackedUserOperation({ sender: 0x75fe56829E9d4867837306E3bDDdcdC066416865, nonce: 0, initCode: 0x, callData: 0xb61d27f60000000000000000000000001240fa2a84dd9157a0e76b5cfe98b1d52268b264000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000004d09de08a00000000000000000000000000000000000000000000000000000000, accountGasLimits: 0x00000000000000000000000000030d400000000000000000000000000000c350, preVerificationGas: 21000 [2.1e4], gasFees: 0x0000000000000000000000003b9aca00000000000000000000000004a817c800, paymasterAndData: 0x8ad159a275aee56fb2334dbb69036e9c7bacee9b0000000000000000000000000000c3500000000000000000000000000000c350, signature: 0x3489fe97fb76cd33a7252a3ae0b9b898fab530ae6863055a35e28c7f83ecc3ea12390d572346a181b615588e3e13febfd3c6414e502e70d82d8e785184e394fd1b }), 0x0c1f5a8df00a8b09966afba22359a21c1f711d760268abf2e3edd5bd59933bba, 0)
    │   │   ├─ [4925] SimpleAccountImpl::validateUserOp(PackedUserOperation({ sender: 0x75fe56829E9d4867837306E3bDDdcdC066416865, nonce: 0, initCode: 0x, callData: 0xb61d27f60000000000000000000000001240fa2a84dd9157a0e76b5cfe98b1d52268b264000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000004d09de08a00000000000000000000000000000000000000000000000000000000, accountGasLimits: 0x00000000000000000000000000030d400000000000000000000000000000c350, preVerificationGas: 21000 [2.1e4], gasFees: 0x0000000000000000000000003b9aca00000000000000000000000004a817c800, paymasterAndData: 0x8ad159a275aee56fb2334dbb69036e9c7bacee9b0000000000000000000000000000c3500000000000000000000000000000c350, signature: 0x3489fe97fb76cd33a7252a3ae0b9b898fab530ae6863055a35e28c7f83ecc3ea12390d572346a181b615588e3e13febfd3c6414e502e70d82d8e785184e394fd1b }), 0x0c1f5a8df00a8b09966afba22359a21c1f711d760268abf2e3edd5bd59933bba, 0) [delegatecall]
    │   │   │   ├─ [3000] PRECOMPILES::ecrecover(0xdede256a55145101b75dd99012d647b492de0e07e2803fded291ef969bad9e1b, 27, 23764083315210779731292864328595571816955946046339784204112651346398036149226, 8242433629124017286329525379298637228692047074603340051719559689309765473533) [staticcall]
    │   │   │   │   └─ ← [Return] 0x0000000000000000000000007c8999dc9a822c1f0df42023113edb4fdd543266
    │   │   │   └─ ← [Return] 0
    │   │   └─ ← [Return] 0
    │   ├─ [3230] Paymaster::validatePaymasterUserOp(PackedUserOperation({ sender: 0x75fe56829E9d4867837306E3bDDdcdC066416865, nonce: 0, initCode: 0x, callData: 0xb61d27f60000000000000000000000001240fa2a84dd9157a0e76b5cfe98b1d52268b264000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000004d09de08a00000000000000000000000000000000000000000000000000000000, accountGasLimits: 0x00000000000000000000000000030d400000000000000000000000000000c350, preVerificationGas: 21000 [2.1e4], gasFees: 0x0000000000000000000000003b9aca00000000000000000000000004a817c800, paymasterAndData: 0x8ad159a275aee56fb2334dbb69036e9c7bacee9b0000000000000000000000000000c3500000000000000000000000000000c350, signature: 0x3489fe97fb76cd33a7252a3ae0b9b898fab530ae6863055a35e28c7f83ecc3ea12390d572346a181b615588e3e13febfd3c6414e502e70d82d8e785184e394fd1b }), 0x0c1f5a8df00a8b09966afba22359a21c1f711d760268abf2e3edd5bd59933bba, 7420000000000000 [7.42e15])
    │   │   └─ ← [Return] 0x00000000000000000000000075fe56829e9d4867837306e3bdddcdc066416865, 0
    │   ├─ emit BeforeExecution()
    │   ├─ [30750] EntryPoint::innerHandleOp(0xb61d27f60000000000000000000000001240fa2a84dd9157a0e76b5cfe98b1d52268b264000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000004d09de08a00000000000000000000000000000000000000000000000000000000, UserOpInfo({ mUserOp: MemoryUserOp({ sender: 0x75fe56829E9d4867837306E3bDDdcdC066416865, nonce: 0, verificationGasLimit: 200000 [2e5], callGasLimit: 50000 [5e4], paymasterVerificationGasLimit: 50000 [5e4], paymasterPostOpGasLimit: 50000 [5e4], preVerificationGas: 21000 [2.1e4], paymaster: 0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b, maxFeePerGas: 20000000000 [2e10], maxPriorityFeePerGas: 1000000000 [1e9] }), userOpHash: 0x0c1f5a8df00a8b09966afba22359a21c1f711d760268abf2e3edd5bd59933bba, prefund: 7420000000000000 [7.42e15], contextOffset: 1248, preOpGas: 62101 [6.21e4] }), 0x00000000000000000000000075fe56829e9d4867837306e3bdddcdc066416865)
    │   │   ├─ [22088] ERC1967Proxy::execute(Counter: [0x1240FA2A84dd9157a0e76B5Cfe98B1d52268B264], 0, 0xd09de08a)
    │   │   │   ├─ [21689] SimpleAccountImpl::execute(Counter: [0x1240FA2A84dd9157a0e76B5Cfe98B1d52268B264], 0, 0xd09de08a) [delegatecall]
    │   │   │   │   ├─ [20340] Counter::increment()
    │   │   │   │   │   └─ ← [Stop] 
    │   │   │   │   └─ ← [Stop] 
    │   │   │   └─ ← [Return] 
    │   │   ├─ [1245] Paymaster::postOp(0, 0x00000000000000000000000075fe56829e9d4867837306e3bdddcdc066416865, 84579000000000 [8.457e13], 1000000000 [1e9])
    │   │   │   └─ ← [Stop] 
    │   │   ├─ emit UserOperationEvent(userOpHash: 0x0c1f5a8df00a8b09966afba22359a21c1f711d760268abf2e3edd5bd59933bba, sender: ERC1967Proxy: [0x75fe56829E9d4867837306E3bDDdcdC066416865], paymaster: Paymaster: [0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b], nonce: 0, success: true, actualGasCost: 94466000000000 [9.446e13], actualGasUsed: 94466 [9.446e4])
    │   │   └─ ← [Return] 94466000000000 [9.446e13]
    │   ├─ [0] Beneficiary::fallback{value: 94466000000000}()
    │   │   └─ ← [Stop] 
    │   └─ ← [Stop] 
    ├─ [283] Counter::number() [staticcall]
    │   └─ ← [Return] 1
    ├─ [0] VM::assertEq(1, 1) [staticcall]
    │   └─ ← [Return] 
    ├─ [545] EntryPoint::balanceOf(Paymaster: [0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b]) [staticcall]
    │   └─ ← [Return] 9999905534000000000 [9.999e18]
    ├─ [0] VM::assertLt(9999905534000000000 [9.999e18], 10000000000000000000 [1e19]) [staticcall]
    │   └─ ← [Return] 
    ├─ [0] VM::assertGt(1000094466000000000 [1e18], 1000000000000000000 [1e18]) [staticcall]
    │   └─ ← [Return] 
    └─ ← [Stop] 

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 12.23ms (2.95ms CPU time)

Ran 1 test suite in 987.23ms (12.23ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

[CASE 2] 화이트리스트에 등록되지 않은 sender가 대납을 요청하는 경우

 case 1과는 달리, deployer가 account 주소를 화이트리스트에 등록하지 않았습니다. 이로 인해 Paymaster의 validatePaymasterUserOp 함수에서는 SenderNotWhitelisted 에러가 반환되고, handleOps에서는 FailedOpWithRevert 에러가 반환됩니다.

function test_RevertHandleOpsWithNotWhitelistedAccount() public {
    SimpleAccount account = factory.createAccount(owner, SALT);
    PackedUserOperation[] memory ops = getrUserOps(address(account), address(paymaster));

    bytes memory innerError = abi.encodeWithSelector(SenderNotWhitelisted.selector, address(account));

    vm.expectRevert(abi.encodeWithSelector(FailedOpWithRevert.selector, 0, "AA33 reverted", innerError));
    entryPoint.handleOps(ops, payable(beneficiary));
}

[CASE 3] 화이트리스트에는 등록되었으나, Paymaster의 예치금이 부족한 경우

function test_RevertHandleOpsWithInsufficientDeposit() public {
    SimpleAccount account = factory.createAccount(owner, SALT);
    PackedUserOperation[] memory ops = getrUserOps(address(account), address(paymaster));

    vm.startPrank(deployer);
    paymaster.setWhitelisted(address(account), true);
    paymaster.withdrawTo(payable(deployer), PAYMASTER_DEPOSIT);
    vm.stopPrank();

    vm.expectRevert(abi.encodeWithSelector(FailedOp.selector, 0, "AA31 paymaster deposit too low"));
    entryPoint.handleOps(ops, payable(beneficiary));
}

스마트 계정의 validateUserOp는 정상적으로 실행이 되지만, validatePaymasterUserOp 함수가 실행되기 전에 예치금이 너무 적다는 이유로 오류가 발생합니다.

[CASE 4] sender가 토큰으로 LegacyTokenPaymaster에 대납을 요청하는 경우

function test_HandleOpsWithLegacyTokenPayment() public {
    SimpleAccount account = factory.createAccount(owner, SALT);
    PackedUserOperation[] memory ops = getrUserOps(address(account), address(tokenPaymaster));

    vm.prank(deployer);
    tokenPaymaster.mintTokens(address(account), 1 ether);
    
    assertEq(tokenPaymaster.balanceOf(address(account)), 1 ether);

    uint256 counterBefore = counter.number();
    uint256 depositBefore = entryPoint.balanceOf(address(tokenPaymaster));
    uint256 accountTokenBalanceBefore = tokenPaymaster.balanceOf(address(account));
    uint256 beneficiaryBalanceBefore = beneficiary.balance;

    entryPoint.handleOps(ops, payable(beneficiary));

    assertEq(counter.number(), counterBefore + 1);
    assertLt(entryPoint.balanceOf(address(tokenPaymaster)), depositBefore);
    assertLt(tokenPaymaster.balanceOf(address(account)), accountTokenBalanceBefore);
    assertGt(beneficiary.balance, beneficiaryBalanceBefore);
}

1. 스마트 계정과 userOps를 생성하는 과정은 동일한데, 이번 경우에는 LegacyTokenPaymaster에게 대납을 요청할 것이므로 LegacyTokenPaymaster의 주소를 넣어 userOps를 생성합니다.

SimpleAccount account = factory.createAccount(owner, SALT);
PackedUserOperation[] memory ops = getrUserOps(address(account), address(tokenPaymaster));

2. 토큰과 이더의 교환을 통해 대납이 이루어지므로, 테스트가 성공하려면 스마트 계정이 일정량의 토큰을 가지고 있어야 합니다. 따라서 Paymaster의 소유자가 스마트 계정 주소로 1개의 토큰을 민팅해 줍니다.

vm.prank(deployer);
tokenPaymaster.mintTokens(address(account), 1 ether);

assertEq(tokenPaymaster.balanceOf(address(account)), 1 ether);

3. 이후의 실행과정은 SimplePaymaster와 유사하며,  LegacyTokenPaymaster의 구현에 의해 토큰을 이더로 교환하는 부분만 차이가 납니다. EntryPoint의 handleOps 함수를 실행하고 나면 다음 사항들을 확인할 수 있습니다.

  • Counter의 number가 handleOps를 실행하기 전보다 1만큼 증가된 상태여야 합니다.
  • LegacyTokenPaymaster 의 예치금이 handleOps를 실행하기 전보다 감소된 상태여야 합니다.
  • 스마트 계정의 토큰 잔액이 handleOps를 실행하기 전보다 감소된 상태여야 합니다.
  • beneficiary의 이더 잔액이 handleOps를 실행하기 전보다 증가된 상태여야 합니다. 
uint256 counterBefore = counter.number();
uint256 depositBefore = entryPoint.balanceOf(address(tokenPaymaster));
uint256 accountTokenBalanceBefore = tokenPaymaster.balanceOf(address(account));
uint256 beneficiaryBalanceBefore = beneficiary.balance;

entryPoint.handleOps(ops, payable(beneficiary));

assertEq(counter.number(), counterBefore + 1);
assertLt(entryPoint.balanceOf(address(tokenPaymaster)), depositBefore);
assertLt(tokenPaymaster.balanceOf(address(account)), accountTokenBalanceBefore);
assertGt(beneficiary.balance, beneficiaryBalanceBefore);

테스트

$ forge test --mt test_HandleOpsWithLegacyTokenPayment -vvvv
[⠒] Compiling...
No files changed, compilation skipped

Ran 1 test for test/PayMaster.t.sol:PaymasterTest
[PASS] test_HandleOpsWithLegacyTokenPayment() (gas: 369719)
Traces:
  [369719] PaymasterTest::test_HandleOpsWithLegacyTokenPayment()
    ├─ [152583] SimpleAccountFactory::createAccount(Owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], 10000 [1e4])
    │   ├─ [112211] → new ERC1967Proxy@0x75fe56829E9d4867837306E3bDDdcdC066416865
    │   │   ├─ emit Upgraded(implementation: SimpleAccountImpl: [0xffD4505B3452Dc22f8473616d50503bA9E1710Ac])
    │   │   ├─ [47945] SimpleAccountImpl::initialize(Owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266]) [delegatecall]
    │   │   │   ├─ emit SimpleAccountInitialized(entryPoint: EntryPoint: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], owner: Owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266])
    │   │   │   ├─ emit Initialized(version: 1)
    │   │   │   └─ ← [Stop] 
    │   │   └─ ← [Return] 183 bytes of code
    │   └─ ← [Return] ERC1967Proxy: [0x75fe56829E9d4867837306E3bDDdcdC066416865]
    ├─ [2768] EntryPoint::getNonce(ERC1967Proxy: [0x75fe56829E9d4867837306E3bDDdcdC066416865], 0) [staticcall]
    │   └─ ← [Return] 0
    ├─ [2819] UserOpUtils::packUserOp(ERC1967Proxy: [0x75fe56829E9d4867837306E3bDDdcdC066416865], 0, 0xb61d27f6000000000000000000000000ff2bd636b9fc89645c2d336aeade2e4abafe1ea5000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000004d09de08a00000000000000000000000000000000000000000000000000000000) [staticcall]
    │   └─ ← [Return] PackedUserOperation({ sender: 0x75fe56829E9d4867837306E3bDDdcdC066416865, nonce: 0, initCode: 0x, callData: 0xb61d27f6000000000000000000000000ff2bd636b9fc89645c2d336aeade2e4abafe1ea5000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000004d09de08a00000000000000000000000000000000000000000000000000000000, accountGasLimits: 0x00000000000000000000000000030d400000000000000000000000000000c350, preVerificationGas: 21000 [2.1e4], gasFees: 0x0000000000000000000000003b9aca00000000000000000000000004a817c800, paymasterAndData: 0x, signature: 0x })
    ├─ [961] UserOpUtils::packPaymasterAndData(LegacyTokenPaymaster: [0x1240FA2A84dd9157a0e76B5Cfe98B1d52268B264], 15000 [1.5e4], 20000 [2e4]) [staticcall]
    │   └─ ← [Return] 0x1240fa2a84dd9157a0e76b5cfe98b1d52268b26400000000000000000000000000003a9800000000000000000000000000004e20
    ├─ [1966] EntryPoint::getUserOpHash(PackedUserOperation({ sender: 0x75fe56829E9d4867837306E3bDDdcdC066416865, nonce: 0, initCode: 0x, callData: 0xb61d27f6000000000000000000000000ff2bd636b9fc89645c2d336aeade2e4abafe1ea5000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000004d09de08a00000000000000000000000000000000000000000000000000000000, accountGasLimits: 0x00000000000000000000000000030d400000000000000000000000000000c350, preVerificationGas: 21000 [2.1e4], gasFees: 0x0000000000000000000000003b9aca00000000000000000000000004a817c800, paymasterAndData: 0x1240fa2a84dd9157a0e76b5cfe98b1d52268b26400000000000000000000000000003a9800000000000000000000000000004e20, signature: 0x })) [staticcall]
    │   └─ ← [Return] 0xa2a6ccdcf0f95c300277e4b331604397f37ec1bc210a36f514f193d850eae9e2
    ├─ [3993] UserOpUtils::signUserOp(907111799109225873672206001743429201758838553092777504370151546632448000192 [9.071e74], 0xa2a6ccdcf0f95c300277e4b331604397f37ec1bc210a36f514f193d850eae9e2) [staticcall]
    │   ├─ [0] VM::sign("<pk>", 0xbc0a1436aa0f5f4c4ebd2e6506267db37fd3d2f1001c3552a13f2dd0ae82f1b8) [staticcall]
    │   │   └─ ← [Return] 27, 0xb2b14872b80d0306f356194e2d67b53ae119603b7c96320cc594620082c366cd, 0x2fbceb174cf54f7c1432a1e216774e0392d71fa7825c4a596b1c20256d8fb705
    │   └─ ← [Return] 0xb2b14872b80d0306f356194e2d67b53ae119603b7c96320cc594620082c366cd2fbceb174cf54f7c1432a1e216774e0392d71fa7825c4a596b1c20256d8fb7051b
    ├─ [0] VM::prank(Deployer: [0xaE0bDc4eEAC5E950B67C6819B118761CaAF61946])
    │   └─ ← [Return] 
    ├─ [31814] LegacyTokenPaymaster::mintTokens(ERC1967Proxy: [0x75fe56829E9d4867837306E3bDDdcdC066416865], 1000000000000000000 [1e18])
    │   ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: ERC1967Proxy: [0x75fe56829E9d4867837306E3bDDdcdC066416865], value: 1000000000000000000 [1e18])
    │   └─ ← [Stop] 
    ├─ [597] LegacyTokenPaymaster::balanceOf(ERC1967Proxy: [0x75fe56829E9d4867837306E3bDDdcdC066416865]) [staticcall]
    │   └─ ← [Return] 1000000000000000000 [1e18]
    ├─ [0] VM::assertEq(1000000000000000000 [1e18], 1000000000000000000 [1e18]) [staticcall]
    │   └─ ← [Return] 
    ├─ [2283] Counter::number() [staticcall]
    │   └─ ← [Return] 0
    ├─ [2545] EntryPoint::balanceOf(LegacyTokenPaymaster: [0x1240FA2A84dd9157a0e76B5Cfe98B1d52268B264]) [staticcall]
    │   └─ ← [Return] 10000000000000000000 [1e19]
    ├─ [597] LegacyTokenPaymaster::balanceOf(ERC1967Proxy: [0x75fe56829E9d4867837306E3bDDdcdC066416865]) [staticcall]
    │   └─ ← [Return] 1000000000000000000 [1e18]
    ├─ [112083] EntryPoint::handleOps([PackedUserOperation({ sender: 0x75fe56829E9d4867837306E3bDDdcdC066416865, nonce: 0, initCode: 0x, callData: 0xb61d27f6000000000000000000000000ff2bd636b9fc89645c2d336aeade2e4abafe1ea5000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000004d09de08a00000000000000000000000000000000000000000000000000000000, accountGasLimits: 0x00000000000000000000000000030d400000000000000000000000000000c350, preVerificationGas: 21000 [2.1e4], gasFees: 0x0000000000000000000000003b9aca00000000000000000000000004a817c800, paymasterAndData: 0x1240fa2a84dd9157a0e76b5cfe98b1d52268b26400000000000000000000000000003a9800000000000000000000000000004e20, signature: 0xb2b14872b80d0306f356194e2d67b53ae119603b7c96320cc594620082c366cd2fbceb174cf54f7c1432a1e216774e0392d71fa7825c4a596b1c20256d8fb7051b })], Beneficiary: [0x5c4d2bd3510C8B51eDB17766d3c96EC637326999])
    │   ├─ [5460] ERC1967Proxy::validateUserOp(PackedUserOperation({ sender: 0x75fe56829E9d4867837306E3bDDdcdC066416865, nonce: 0, initCode: 0x, callData: 0xb61d27f6000000000000000000000000ff2bd636b9fc89645c2d336aeade2e4abafe1ea5000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000004d09de08a00000000000000000000000000000000000000000000000000000000, accountGasLimits: 0x00000000000000000000000000030d400000000000000000000000000000c350, preVerificationGas: 21000 [2.1e4], gasFees: 0x0000000000000000000000003b9aca00000000000000000000000004a817c800, paymasterAndData: 0x1240fa2a84dd9157a0e76b5cfe98b1d52268b26400000000000000000000000000003a9800000000000000000000000000004e20, signature: 0xb2b14872b80d0306f356194e2d67b53ae119603b7c96320cc594620082c366cd2fbceb174cf54f7c1432a1e216774e0392d71fa7825c4a596b1c20256d8fb7051b }), 0xa2a6ccdcf0f95c300277e4b331604397f37ec1bc210a36f514f193d850eae9e2, 0)
    │   │   ├─ [4925] SimpleAccountImpl::validateUserOp(PackedUserOperation({ sender: 0x75fe56829E9d4867837306E3bDDdcdC066416865, nonce: 0, initCode: 0x, callData: 0xb61d27f6000000000000000000000000ff2bd636b9fc89645c2d336aeade2e4abafe1ea5000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000004d09de08a00000000000000000000000000000000000000000000000000000000, accountGasLimits: 0x00000000000000000000000000030d400000000000000000000000000000c350, preVerificationGas: 21000 [2.1e4], gasFees: 0x0000000000000000000000003b9aca00000000000000000000000004a817c800, paymasterAndData: 0x1240fa2a84dd9157a0e76b5cfe98b1d52268b26400000000000000000000000000003a9800000000000000000000000000004e20, signature: 0xb2b14872b80d0306f356194e2d67b53ae119603b7c96320cc594620082c366cd2fbceb174cf54f7c1432a1e216774e0392d71fa7825c4a596b1c20256d8fb7051b }), 0xa2a6ccdcf0f95c300277e4b331604397f37ec1bc210a36f514f193d850eae9e2, 0) [delegatecall]
    │   │   │   ├─ [3000] PRECOMPILES::ecrecover(0xbc0a1436aa0f5f4c4ebd2e6506267db37fd3d2f1001c3552a13f2dd0ae82f1b8, 27, 80824918996840894553822724821576997337440302536999582809795846337667134613197, 21592493670166774682370287800672793849125319473584435108482817468316376872709) [staticcall]
    │   │   │   │   └─ ← [Return] 0x0000000000000000000000007c8999dc9a822c1f0df42023113edb4fdd543266
    │   │   │   └─ ← [Return] 0
    │   │   └─ ← [Return] 0
    │   ├─ [2272] LegacyTokenPaymaster::validatePaymasterUserOp(PackedUserOperation({ sender: 0x75fe56829E9d4867837306E3bDDdcdC066416865, nonce: 0, initCode: 0x, callData: 0xb61d27f6000000000000000000000000ff2bd636b9fc89645c2d336aeade2e4abafe1ea5000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000004d09de08a00000000000000000000000000000000000000000000000000000000, accountGasLimits: 0x00000000000000000000000000030d400000000000000000000000000000c350, preVerificationGas: 21000 [2.1e4], gasFees: 0x0000000000000000000000003b9aca00000000000000000000000004a817c800, paymasterAndData: 0x1240fa2a84dd9157a0e76b5cfe98b1d52268b26400000000000000000000000000003a9800000000000000000000000000004e20, signature: 0xb2b14872b80d0306f356194e2d67b53ae119603b7c96320cc594620082c366cd2fbceb174cf54f7c1432a1e216774e0392d71fa7825c4a596b1c20256d8fb7051b }), 0xa2a6ccdcf0f95c300277e4b331604397f37ec1bc210a36f514f193d850eae9e2, 6120000000000000 [6.12e15])
    │   │   └─ ← [Return] 0x00000000000000000000000075fe56829e9d4867837306e3bdddcdc066416865, 0
    │   ├─ emit BeforeExecution()
    │   ├─ [38113] EntryPoint::innerHandleOp(0xb61d27f6000000000000000000000000ff2bd636b9fc89645c2d336aeade2e4abafe1ea5000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000004d09de08a00000000000000000000000000000000000000000000000000000000, UserOpInfo({ mUserOp: MemoryUserOp({ sender: 0x75fe56829E9d4867837306E3bDDdcdC066416865, nonce: 0, verificationGasLimit: 200000 [2e5], callGasLimit: 50000 [5e4], paymasterVerificationGasLimit: 15000 [1.5e4], paymasterPostOpGasLimit: 20000 [2e4], preVerificationGas: 21000 [2.1e4], paymaster: 0x1240FA2A84dd9157a0e76B5Cfe98B1d52268B264, maxFeePerGas: 20000000000 [2e10], maxPriorityFeePerGas: 1000000000 [1e9] }), userOpHash: 0xa2a6ccdcf0f95c300277e4b331604397f37ec1bc210a36f514f193d850eae9e2, prefund: 6120000000000000 [6.12e15], contextOffset: 1248, preOpGas: 61143 [6.114e4] }), 0x00000000000000000000000075fe56829e9d4867837306e3bdddcdc066416865)
    │   │   ├─ [22088] ERC1967Proxy::execute(Counter: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], 0, 0xd09de08a)
    │   │   │   ├─ [21689] SimpleAccountImpl::execute(Counter: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], 0, 0xd09de08a) [delegatecall]
    │   │   │   │   ├─ [20340] Counter::increment()
    │   │   │   │   │   └─ ← [Stop] 
    │   │   │   │   └─ ← [Stop] 
    │   │   │   └─ ← [Return] 
    │   │   ├─ [8608] LegacyTokenPaymaster::postOp(0, 0x00000000000000000000000075fe56829e9d4867837306e3bdddcdc066416865, 83621000000000 [8.362e13], 1000000000 [1e9])
    │   │   │   ├─ emit Transfer(from: ERC1967Proxy: [0x75fe56829E9d4867837306E3bDDdcdC066416865], to: LegacyTokenPaymaster: [0x1240FA2A84dd9157a0e76B5Cfe98B1d52268B264], value: 986210000000 [9.862e11])
    │   │   │   └─ ← [Stop] 
    │   │   ├─ emit UserOperationEvent(userOpHash: 0xa2a6ccdcf0f95c300277e4b331604397f37ec1bc210a36f514f193d850eae9e2, sender: ERC1967Proxy: [0x75fe56829E9d4867837306E3bDDdcdC066416865], paymaster: LegacyTokenPaymaster: [0x1240FA2A84dd9157a0e76B5Cfe98B1d52268B264], nonce: 0, success: true, actualGasCost: 97135000000000 [9.713e13], actualGasUsed: 97135 [9.713e4])
    │   │   └─ ← [Return] 97135000000000 [9.713e13]
    │   ├─ [0] Beneficiary::fallback{value: 97135000000000}()
    │   │   └─ ← [Stop] 
    │   └─ ← [Stop] 
    ├─ [283] Counter::number() [staticcall]
    │   └─ ← [Return] 1
    ├─ [0] VM::assertEq(1, 1) [staticcall]
    │   └─ ← [Return] 
    ├─ [545] EntryPoint::balanceOf(LegacyTokenPaymaster: [0x1240FA2A84dd9157a0e76B5Cfe98B1d52268B264]) [staticcall]
    │   └─ ← [Return] 9999902865000000000 [9.999e18]
    ├─ [0] VM::assertLt(9999902865000000000 [9.999e18], 10000000000000000000 [1e19]) [staticcall]
    │   └─ ← [Return] 
    ├─ [597] LegacyTokenPaymaster::balanceOf(ERC1967Proxy: [0x75fe56829E9d4867837306E3bDDdcdC066416865]) [staticcall]
    │   └─ ← [Return] 999999013790000000 [9.999e17]
    ├─ [0] VM::assertLt(999999013790000000 [9.999e17], 1000000000000000000 [1e18]) [staticcall]
    │   └─ ← [Return] 
    ├─ [0] VM::assertGt(1000097135000000000 [1e18], 1000000000000000000 [1e18]) [staticcall]
    │   └─ ← [Return] 
    └─ ← [Stop] 

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 8.96ms (3.80ms CPU time)

Ran 1 test suite in 677.62ms (8.96ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

정리

workflow
workflow
  • BasePaymaster를 기반으로 Paymaster를 자유롭게 구현할 수 있습니다.
  • context는 Paymaster의 validatePaymasterUserOp에서 생성되어 반환되며, 길이가 0이라면 postOp가 호출되지 않고 0보다 크다면 호출되므로, 후속 작업이 필요한 경우에 context를 활용합니다.
  • ERC-20 토큰을 사용한 대납이 가능하며, DEX를 사용하여 토큰을 이더로 교환하는 등의 추가적인 구현도 가능합니다. 

전체 코드


참조