Skip to content

OneTimeUsePaymaster

The OneTimeUsePaymaster provides maximum unlinkability by consuming the entire joining amount with each transaction.

How It Works

Single-Use Model

  • Each transaction uses the entire joining amount (e.g., 0.0001 ETH)
  • Nullifier is marked as used after one transaction
  • Requires new deposit for each subsequent transaction
  • Provides maximum unlinkability between transactions

State Tracking

mapping(uint256 => bool) public usedNullifiers;

Simple boolean tracking - once a nullifier is used, it cannot be used again.

Transaction Flow

1. User Joins Pool

// User calls deposit() with identity commitment
function deposit(uint256 identityCommitment) external payable {
    require(msg.value == JOINING_AMOUNT, "Incorrect joining amount"); // e.g., 0.0001 ETH
    // Add to Merkle tree
    // Emit Deposited event
}

2. Transaction Execution

function _validatePaymasterUserOp(...) internal override returns (bytes memory context, uint256 validationData) {
    // Verify ZK proof
    // Check nullifier not already used
    require(!usedNullifiers[proof.nullifier], "NullifierAlreadyUsed");
    
    // Return context for post-op
    return (abi.encode(userOpHash, proof.nullifier, sender), _packValidationData(false, 0, 0));
}

3. Post-Operation

function _postOp(
    PostOpMode /*mode*/,
    bytes calldata context,
    uint256 actualGasCost,
    uint256 actualUserOpFeePerGas
) internal virtual override {
    // Decode context:  userOpHash, nullifier, sender.
    (bytes32 userOpHash, uint256 nullifier, address sender) = abi.decode(
        context,
        (bytes32, uint256, address)
    );
    //  Mark nullifier as used
    usedNullifiers[nullifier] = true;
    // Calculate total cost, including postOp overhead.
    uint256 postOpGasCost = Constants.POSTOP_GAS_COST *
        actualUserOpFeePerGas;
    uint256 totalGasCost = actualGasCost + postOpGasCost;
 
    // Deduct the JOINING_AMOUNT from the pool's total deposits.
    totalDeposit -= JOINING_AMOUNT;
 
    emit UserOpSponsoredWithNullifier(
        sender,
        userOpHash,
        totalGasCost,
        nullifier
    );
}

Key Features

  • Maximum Unlinkability: Each transaction is completely unlinkable from previous ones
  • Single Use: Fresh nullifier required for each transaction
  • Simple State: No complex usage tracking
  • Full Consumption: Entire joining amount used per transaction

Contract Address

Base Sepolia: 0x4DACA5b0a5d10853F84bB400C5232E4605bc14A0

Source Code