티스토리 뷰

계정 추상화 시리즈

2024.04.18 - [블록체인/Ethereum] - ERC-4337: 계정 추상화 - 테스트를 통한 Account Factory의 동작 이해

2024.04.17 - [블록체인/Ethereum] - ERC-4337: 계정 추상화 - 테스트 수정 사항

2024.04.17 - [블록체인/Ethereum] - ERC-4337: 계정 추상화 - 테스트를 통한 Account와 EntryPoint의 동작 이해

2024.04.16 - [블록체인/Ethereum] - ERC-4337: 계정 추상화 - Account, EntryPoint, Paymaster


테스트에 참고한 컨트랙트

  • core/BasePaymaster.sol
  • samples/LegacyTokenPaymaster.sol
 

account-abstraction/contracts at develop · eth-infinitism/account-abstraction

Contribute to eth-infinitism/account-abstraction development by creating an account on GitHub.

github.com

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

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 : https://piatoss3612.tistory.com/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

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

전체 코드

 

aa-from-scratch/contracts at main · piatoss3612/aa-from-scratch

Contribute to piatoss3612/aa-from-scratch development by creating an account on GitHub.

github.com


참조

 

account-abstraction/contracts at develop · eth-infinitism/account-abstraction

Contribute to eth-infinitism/account-abstraction development by creating an account on GitHub.

github.com

 

최근에 올라온 글
최근에 달린 댓글
«   2024/11   »
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
Total
Today
Yesterday
글 보관함