Mismanagement of funds

In this page, we cover potential bugs that may occur during the fund management process.

Missing fund withdrawal logic

Users have the right to reclaim their funds!

A mechanism is needed to allow users to withdraw the funds they have deposited.

Example Case:

When a user accidentally sends tokens or ETH to the paymaster, or when there is logic that allows assets to be deposited using the deposit function, the user’s funds may become locked if there’s NO mechanism within the paymaster to withdraw the received tokens/ETH.

  • Mitigation : Add withdraw function that is based on onlyOwner or msg.sender

 + function withdraw(...) onlyOwner {...}

or

+ function withdraw(...) {
+    deposit[msg.sender] -= amount;
+} 


Balance changes must be updated whenever there is a change in the flow of funds. If balance changes aren’t correctly updated, it can cause mismatches in funds and lead to issues like double spending.

If the user is managing withdrawals through a variable and receives a refund, the value of the variable must be updated to reflect the refunded amount. If this update is not performed correctly, it could potentially lead to a double withdrawal vulnerability.

Example case:

The paymaster is set up to calculate the gas fees during the validation phase and then allow any leftover funds to be withdrawn in advance during the transaction for purposes like swaps or minting. However, in the postOp function, where the final balance is adjusted and refunded, the _withdrawableETH variable—which tracks the amount available for withdrawal—was not cleared after refunding. This oversight allows users to call the withdraw function again, letting them receive the refund a second time.

Example Code:

src/vulnerablePaymaster.sol


function validatePaymasterUserOp(UserOperation calldata userOp, bytes32, uint256 maxCost) external onlyEntryPoint
        returns (bytes memory context, uint256 validationData) {
       ...
       ...

        _withdrawableETH[userOp.sender] += withdrawAmount - maxCost;
        
        ...
    }


function postOp(IPaymaster.PostOpMode mode, bytes calldata context, uint256 actualGasCost) external onlyEntryPoint {
    
    ...
    ...
    ...
    
    uint256 excess = _withdrawableETH[account] + (withheld - actualGasCost);
    
    if (excess > 0) {
        _withdraw(address(0), account, excess);
    }
    ...
}
 

  • Mitigation : Update the mapping variable that manages balances when a refund is processed in the postOp function.

function postOp(IPaymaster.PostOpMode mode, bytes calldata context, uint256 actualGasCost) external onlyEntryPoint {
    
    ...
    ...
    ...
    
    uint256 excess = _withdrawableETH[account] + (withheld - actualGasCost);
    + delete _withdrawableETH[account];
    
    if (excess > 0) {
        _withdraw(address(0), account, excess);
    }
}


Duplicate update of balance changes

Counterfeiting is illegal.

When the balance in the paymaster changes, applying the balance change in multiple places can create differences between the actual amount the paymaster has and the recorded balance.

Example Case:

Whenever _postOp is called, the owner’s balance increases by the amount of gas used in addition to the gas processed. If the owner withdraws even a small amount of ETH, inconsistencies can occur between the EP deposit and the balance recorded in the paymaster, potentially preventing some users from accessing their full deposits.

Example Code:

src/vulnerablePaymaster.sol

function _postOp(PostOpMode /* mode */, bytes calldata context, uint256 actualGasCost) internal override {
        (address account, uint256 gasPricePostOp) = abi.decode(context, (address, uint256));
        
        uint256 ethCost = (actualGasCost + COST_OF_POST * gasPricePostOp);
        balances[account] -= ethCost;
        contractSpent[account] += ethCost;
        balances[owner()] += ethCost; // This part causes inconsistency
    }

  • Mitigation : Eliminate code that causes redundant balance updates.

function _postOp(PostOpMode /* mode */, bytes calldata context, uint256 actualGasCost) internal override {
        (address account, uint256 gasPricePostOp) = abi.decode(context, (address, uint256));
        
        uint256 ethCost = (actualGasCost + COST_OF_POST * gasPricePostOp);
        balances[account] -= ethCost;
        contractSpent[account] += ethCost;
-       balances[owner()] += ethCost; // REMOVE
    }

Last updated