Sponsorship Paymaster Specific Vectors

In this page, we cover vulnerabilities that may occur in sponsorship paymasters. The structure of sponsorship paymasters can vary depending on the service.

Gas fee coverage without proper validation

Paymaster is not charity.

There are paymasters that cover users’ gas fees for promotions and other purposes. To sponsor for gas fees, the paymaster needs to check if it will cover the specific UserOperation. However, some paymasters rely on web2-level validation, believing it to be sufficient and assuming that other users won’t be able to use them.

Example Case:

This sponsorship paymaster is confident that only the users they have approved at the web2 level can use it. Because of this, the paymaster covers all incoming UserOperations without any extra checks.

However, a user discovered that validation only occurs at the web2 level. The user then skipped the web2 service linked to the paymaster and directly submitted a UserOperation to the Entrypoint, using the paymaster for free.

  • Mitgation : Use the signature of an approved signer to verify the UserOperation, or include additional checks such as restricting access to specific senders.


dApps can impact the reputation of the paymaster.

We're closing.

There is a singleton sponsorship paymaster that provides services to multiple dApps. Each dApp’s deposited funds are managed using a mapping variable, with each dApp’s address tracked as a paymasterId. Transactions using the paymaster are validated by a signer authorized by the paymaster contract and are checked in the validatePaymasterUserOp function.

Exmaple Case:

When the dApp administrator decides to stop the service, they withdraw all the funds they had deposited in the paymaster. However, UserOperations that had already passed the first validation and are waiting in the mempool may fail during the second validation because the dApp’s balance is now empty. This causes the singleton paymaster to lose reputation due to the failure during the bundler’s second validation.

Example Code

src/vulnerablePaymaster.sol

contract SponsorshipPaymaster is
    BasePaymaster,
    ...
    ...
{
    ...
    ...

    mapping(address => uint256) public paymasterIdBalances;

    constructor(
        ...
    )
    
    
    function withdrawTo(address payable withdrawAddress, uint256 amount) external override nonReentrant {
        // No delay for withdraw logic....
        if (withdrawAddress == address(0)) revert CanNotWithdrawToZeroAddress();
        uint256 currentBalance = paymasterIdBalances[msg.sender];
        if (amount > currentBalance) {
            revert InsufficientFundsInGasTank();
        }
        paymasterIdBalances[msg.sender] = currentBalance - amount;
        entryPoint.withdrawTo(withdrawAddress, amount);
        emit GasWithdrawn(msg.sender, withdrawAddress, amount);
    }
    
    
    function _validatePaymasterUserOp(
        PackedUserOperation calldata userOp,
        bytes32 userOpHash,
        uint256 requiredPreFund
    )
        internal
        view
        override
        returns (bytes memory context, uint256 validationData)
    {
        ...
        ...
        

        if (effectiveCost > paymasterIdBalances[paymasterId]) { // checks balance here
            revert InsufficientFundsForPaymasterId();
        }
        
        ...
        ...
        
        return (context, _packValidationData(false, validUntil, validAfter));
    }
    

  • Mitigation : Set a delay in the withdrawal process

src/goodPaymaster.sol

    ...

function withdrawTo(address payable withdrawAddress, uint256 amount) external override nonReentrant {
        // No delay for withdraw logic....
        if (withdrawAddress == address(0)) revert CanNotWithdrawToZeroAddress();
        uint256 currentBalance = paymasterIdBalances[msg.sender];
        if (amount > currentBalance) {
            revert InsufficientFundsInGasTank();
        }
        
        // set timestamp when withdraw for the first time
+       if (withdrawalTimestamps[msg.sender] == 0) {
+       withdrawalTimestamps[msg.sender] = block.timestamp + withdrawalDelay;
+       }

        // check delay
+       require(block.timestamp >= withdrawalTimestamps[msg.sender], "Withdrawal delay not yet passed");

        paymasterIdBalances[msg.sender] = currentBalance - amount;
        entryPoint.withdrawTo(withdrawAddress, amount);
        emit GasWithdrawn(msg.sender, withdrawAddress, amount);

        //reset timestamp
+       withdrawalTimestamps[msg.sender] = 0;
    }
    
    ...
    ...


Lack of gas limit in a single transaction

When making a wish to a genie, wish for the biggest one.

Some paymasters cover users’ gas fees for promotions and other reasons. This paymaster tracks how many transactions each user makes, allowing them up to n free transactions. It trusts that users will use the free service fairly.

Example Case:

However, a clever but malicious user notices this and sets the maximum possible gas limit within a single transaction. Since the paymaster only checks the number of transactions, it approves the user’s request without any suspicion. The user continues to submit transactions with the maximum gas limit, eventually draining the paymaster.

Example Code

function validateAndPayForPaymasterTransaction(
        bytes32 /**_txHash*/,
        bytes32 /**_suggestedSignedHash*/,
        Transaction calldata _transaction
    ) external payable onlyBootloader returns (bytes4 magic, bytes memory context) {
    
        ...
        ... // No logic exists to limit the amount of gas requested by the user.
        ...
        
            // Then, check the user sponsorship limit and decrease
            // txAmount represents the number of transactions requested by the user.
            uint256 txAmount = userSponsored[userAddress]; 
            if (txAmount >= userLimit) revert Errors.USER_LIMIT_REACHED();
            userSponsored[userAddress]++;
        
        ...
        ... // No logic exists to limit the amount of gas requested by the user.
        
    }

  • Mitigation : Assign a specific amount of gas to each user instead of a set number of transactions, OR set a maximum gasLimit for each transaction

Last updated