Overview

EtherspotPaymaster is a smart contract that allows an external signer to sign a UserOperation and pay for the gas costs of executing that UserOperation. The paymaster signs to agree to pay for gas, and the wallet signs to prove identity and account ownership.

Version

Solidity pragma version ^0.8.12.

Global Variables

  • VALID_TIMESTAMP_OFFSET: A constant of type uint256 that represents a 20 second time offset used to validate timestamps.
  • SIGNATURE_OFFSET: A constant of type uint256 that represents an 84-byte signature offset.
  • COST_OF_POST: The pre-calculated cost of calling _postOp (required for gas calculation).

Imports

  • ECDSA: A contract from the OpenZeppelin library used for signature verification.
  • IERC20: A contract from the OpenZeppelin library used for interacting with ERC20 tokens.
  • SafeERC20: A contract from the OpenZeppelin library used for safe ERC20 token transfers.
  • Whitelist: Whitelist.sol smart contract used for whitelisting addresses.

Mappings

  • sponsorFunds: A mapping of type mapping(address => uint256) used to store the amount of sponsor funds transferred to the paymaster contract.
  • senderNonce: A mapping of type mapping(address => uint256) used to store the nonce of the sender.

Events

  • SponsorSuccessful: An event emitted when a sponsor successfully sponsors a user operation.
  • SponsorUnsuccessful: An event emitted when a sponsor is unsuccessful in sponsoring a user operation.

Constructor

  • constructor(IEntryPoint _entryPoint): A constructor that accepts an IEntryPoint parameter _entryPoint.

Public/External Functions

  • depositFunds() external payable: A function used to deposit funds to the paymaster.
    • Error EtherspotPaymaster:: Not enough balance: Checks that the sponsor has enough funds to deposit into paymaster contract.
  • withdrawFunds() address payable _sponsor, uint256 _amount) external: A function used to withdraw sponsor funds from paymaster.
    • Error EtherspotPaymaster:: can only withdraw own funds: Checks msg.sender matches the sponsor address provided.
    • Error EtherspotPaymaster:: not enough deposited funds: Checks amount is >= deposited funds for the given sponsor.
  • checkSponsorFunds(address _sponsor) public view returns (uint256): A function used to check the amount of sponsor funds transferred to the paymaster contract for a given sponsor.
  • function getHash(UserOperation calldata userOp, uint48 validUntil, uint48 validAfter) public view returns (bytes32): A function to return the hash to be sign off-chain (and validate on-chain) by a sponsor.
  • function parsePaymasterAndData(bytes calldata paymasterAndData) public pure returns (uint48 validUntil, uint48 validAfter, bytes calldata signature): Extracts validUntil, validAfter and signature from paymasterAndData passed in as input.

Internal Functions

  • _debitSponsor(address _sponsor, uint256 _amount) internal: A function used to debit a sponsor’s fund amount for gas costs once a transaction has been processed.
  • _creditSponsor: A function used to credit a sponsor’s deposited amount.
  • _pack(UserOperation calldata userOp): A function used to pack the user operation.
  • _validatePaymasterUserOp(UserOperation calldata userOp, bytes32 userOpHash, uint256 requiredPreFund): A function used to verify the external signer (sponsor) that signed the request. Debits the sponsor’s deposited balance by full requiredPreFund amount (credits back in _postOp).
    • Error EtherspotPaymaster:: invalid signature length in paymasterAndData: Triggered on incorrect signature length.
    • Error EtherspotPaymaster:: Sponsor paymaster funds too low: Checks sponsor has enough funds to pay the gas costs for a sponsored UserOperation.
  • _postOp(PostOpMode mode, bytes calldata context, uint256 actualGasCost) internal override: A function that overrides the _postOp function from BasePaymaster.sol that checks for a validated UserOperation and credits back any remaining funds after the gas cost for the UserOperation execution plus _postOp call.
    • Emits SponsorSuccessful(paymaster, sender, userOpHash) on successfully sponsored UserOperation.
    • Emits SponsorUnsuccessful(paymaster, sender, userOpHash) on unsuccessfully sponsored UserOperation.

Contract Source Code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.12;

/* solhint-disable reason-string */

import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "../../account-abstraction/contracts/core/UserOperationLib.sol";
import "./BasePaymaster.sol";
import "./Whitelist.sol";

/**
 * A sample paymaster that uses external service to decide whether to pay for the UserOp.
 * The paymaster trusts an external signer to sign the transaction.
 * The calling user must pass the UserOp to that external signer first, which performs
 * whatever off-chain verification before signing the UserOp.
 * Note that this signature is NOT a replacement for wallet signature:
 * - the paymaster signs to agree to PAY for GAS.
 * - the wallet signs to prove identity and account ownership.
 */
contract EtherspotPaymaster is BasePaymaster, Whitelist, ReentrancyGuard {
    using ECDSA for bytes32;
    using UserOperationLib for UserOperation;

    uint256 private constant VALID_TIMESTAMP_OFFSET = 20;
    uint256 private constant SIGNATURE_OFFSET = 84;
    // calculated cost of the postOp
    uint256 private constant COST_OF_POST = 40000;

    mapping(address => uint256) private _sponsorBalances;

    event SponsorSuccessful(address paymaster, address sender);

    constructor(IEntryPoint _entryPoint) BasePaymaster(_entryPoint) {}

    function depositFunds() external payable nonReentrant {
        _creditSponsor(msg.sender, msg.value);
        entryPoint.depositTo{value: msg.value}(address(this));
    }

    function withdrawFunds(uint256 _amount) external nonReentrant {
        require(
            getSponsorBalance(msg.sender) >= _amount,
            "EtherspotPaymaster:: not enough deposited funds"
        );
        _debitSponsor(msg.sender, _amount);
        entryPoint.withdrawTo(payable(msg.sender), _amount);
    }

    function getSponsorBalance(address _sponsor) public view returns (uint256) {
        return _sponsorBalances[_sponsor];
    }

    function _debitSponsor(address _sponsor, uint256 _amount) internal {
        _sponsorBalances[_sponsor] -= _amount;
    }

    function _creditSponsor(address _sponsor, uint256 _amount) internal {
        _sponsorBalances[_sponsor] += _amount;
    }

    function _pack(
        UserOperation calldata userOp
    ) internal pure returns (bytes32) {
        return
            keccak256(
                abi.encode(
                    userOp.getSender(),
                    userOp.nonce,
                    keccak256(userOp.initCode),
                    keccak256(userOp.callData),
                    userOp.callGasLimit,
                    userOp.verificationGasLimit,
                    userOp.preVerificationGas,
                    userOp.maxFeePerGas,
                    userOp.maxPriorityFeePerGas
                )
            );
    }

    /**
     * return the hash we're going to sign off-chain (and validate on-chain)
     * this method is called by the off-chain service, to sign the request.
     * it is called on-chain from the validatePaymasterUserOp, to validate the signature.
     * note that this signature covers all fields of the UserOperation, except the "paymasterAndData",
     * which will carry the signature itself.
     */
    function getHash(
        UserOperation calldata userOp,
        uint48 validUntil,
        uint48 validAfter
    ) public view returns (bytes32) {
        //can't use userOp.hash(), since it contains also the paymasterAndData itself.

        return
            keccak256(
                abi.encode(
                    _pack(userOp),
                    block.chainid,
                    address(this),
                    validUntil,
                    validAfter
                )
            );
    }

    /**
     * verify our external signer signed this request.
     * the "paymasterAndData" is expected to be the paymaster and a signature over the entire request params
     * paymasterAndData[:20] : address(this)
     * paymasterAndData[20:84] : abi.encode(validUntil, validAfter)
     * paymasterAndData[84:] : signature
     */
    function _validatePaymasterUserOp(
        UserOperation calldata userOp,
        bytes32 /*userOpHash*/,
        uint256 requiredPreFund
    ) internal override returns (bytes memory context, uint256 validationData) {
        (requiredPreFund);

        (
            uint48 validUntil,
            uint48 validAfter,
            bytes calldata signature
        ) = parsePaymasterAndData(userOp.paymasterAndData);
        // ECDSA library supports both 64 and 65-byte long signatures.
        // we only "require" it here so that the revert reason on invalid signature will be of "EtherspotPaymaster", and not "ECDSA"
        require(
            signature.length == 64 || signature.length == 65,
            "EtherspotPaymaster:: invalid signature length in paymasterAndData"
        );
        bytes32 hash = ECDSA.toEthSignedMessageHash(
            getHash(userOp, validUntil, validAfter)
        );
        address sig = userOp.getSender();

        // check for valid paymaster
        address sponsorSig = ECDSA.recover(hash, signature);

        // don't revert on signature failure: return SIG_VALIDATION_FAILED
        if (!_check(sponsorSig, sig)) {
            return ("", _packValidationData(true, validUntil, validAfter));
        }

        uint256 costOfPost = userOp.maxFeePerGas * COST_OF_POST;
        uint256 totalPreFund = requiredPreFund + costOfPost;

        // check sponsor has enough funds deposited to pay for gas
        require(
            getSponsorBalance(sponsorSig) >= totalPreFund,
            "EtherspotPaymaster:: Sponsor paymaster funds too low"
        );

        // debit requiredPreFund amount
        _debitSponsor(sponsorSig, totalPreFund);

        // no need for other on-chain validation: entire UserOp should have been checked
        // by the external service prior to signing it.
        return (
            abi.encode(sponsorSig, sig, totalPreFund, costOfPost),
            _packValidationData(false, validUntil, validAfter)
        );
    }

    function parsePaymasterAndData(
        bytes calldata paymasterAndData
    )
        public
        pure
        returns (uint48 validUntil, uint48 validAfter, bytes calldata signature)
    {
        (validUntil, validAfter) = abi.decode(
            paymasterAndData[VALID_TIMESTAMP_OFFSET:SIGNATURE_OFFSET],
            (uint48, uint48)
        );
        signature = paymasterAndData[SIGNATURE_OFFSET:];
    }

    function _postOp(
        PostOpMode,
        bytes calldata context,
        uint256 actualGasCost
    ) internal override {
        (
            address paymaster,
            address sender,
            uint256 totalPrefund,
            uint256 costOfPost
        ) = abi.decode(context, (address, address, uint256, uint256));
        _creditSponsor(paymaster, totalPrefund - (actualGasCost + costOfPost));
        emit SponsorSuccessful(paymaster, sender);
    }
}

License

This contract is licensed under the MIT license.