Mismanagement of funds
In this page, we cover potential bugs that may occur during the fund management process.
Missing fund withdrawal logic
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 ononlyOwner
ormsg.sender
+ function withdraw(...) onlyOwner {...}
or
+ function withdraw(...) {
+ deposit[msg.sender] -= amount;
+}
Missing updates to balance-related variables upon fund changes
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
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