Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 35 additions & 5 deletions packages/tornado-cash/src/paymaster/fee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@ interface UserOperationGasLimits {

const ERC20_TRANSFER_GAS = 100_000n;

// Execution-phase (callData) gas budgets used when a batch is consolidated into
// a single userOp. Each extra deposit becomes a direct `pool.withdraw` call
// executed by the ephemeral 7702 account — a groth16 verify plus the payout
// (ERC20 pools add a token transfer to the sender). `FORWARD_GAS` covers the
// final transfer of the accumulated balance to the recipient when the caller
// supplied no tailCalls. These are conservative ceilings; the actual limits are
// refined via bundler estimation when available.
const PER_DIRECT_WITHDRAW_GAS = 400_000n;
const PER_DIRECT_WITHDRAW_GAS_ERC20 = 500_000n;
const FORWARD_GAS = 60_000n;

const baseGasUnits: UserOperationGasLimits = {
preVerificationGas: 80_000n,
verificationGasLimit: 50_000n,
Expand All @@ -40,6 +51,30 @@ export function reasonableGasUnits(isERC20: boolean): UserOperationGasLimits {
};
}

/**
* Gas units for a consolidated batch withdrawal (one userOp for the whole
* batch). deposit[0] is sponsored by the paymaster (its verify/payout stays in
* paymasterVerificationGasLimit, as in `reasonableGasUnits`); the remaining
* `extraWithdrawals` deposits run as direct `pool.withdraw` calls in the
* execution phase, so their gas belongs to callGasLimit. When the caller
* supplied tailCalls we keep the base callGasLimit as headroom for them;
* otherwise we only need the synthesized forward transfer.
*/
export function reasonableGasUnitsForBatch(
isERC20: boolean,
extraWithdrawals: number,
hasUserTailCalls: boolean,
): UserOperationGasLimits {
const base = reasonableGasUnits(isERC20);
const perWithdraw = isERC20 ? PER_DIRECT_WITHDRAW_GAS_ERC20 : PER_DIRECT_WITHDRAW_GAS;
const executionTail = hasUserTailCalls ? base.callGasLimit : FORWARD_GAS;

return {
...base,
callGasLimit: BigInt(extraWithdrawals) * perWithdraw + executionTail,
};
}

// The fee amount the paymaster expects to break even
export function computeMinimumViableFee(reasonableGasUnits: UserOperationGasLimits, maxFeePerGas: bigint) {

Expand Down Expand Up @@ -68,10 +103,5 @@ export function computeMinimumViableFee(reasonableGasUnits: UserOperationGasLimi
);
const requiredPrefund = requiredGas * maxFeePerGas;

console.log("totalGasUnits", requiredGas);
console.log("maxFeePerGas", maxFeePerGas);
console.log("requiredPrefund", requiredPrefund);
console.log("multiply", 1.2);

return multiply(requiredPrefund);
}
32 changes: 31 additions & 1 deletion packages/tornado-cash/src/paymaster/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
type BundlerClient,
} from 'viem/account-abstraction';
import { SIMPLE_7702_EXECUTE_ABI } from '../data/abis/account.abi';
import { BuildSignedTornadoUserOpParams, SerializedAuth, SerializedUserOperation } from '../interfaces/user-ops.interface';
import { BuildSignedTornadoUserOpParams, SerializedAuth, SerializedUserOperation, UserOpGasLimits } from '../interfaces/user-ops.interface';

/**
* EntryPoint v0.8 canonical Simple7702Account implementation. The ephemeral
Expand Down Expand Up @@ -91,6 +91,36 @@ export function ephemeralSenderAddress(privateKey: Hex): Address {
return privateKeyToAccount(privateKey).address;
}

/**
* Bundler gas estimation (`eth_estimateUserOperationGas`) for an already-built,
* serialized userOp. Used to size a consolidated batch userOp — whose execution
* phase runs N-1 extra `pool.withdraw` calls — tightly instead of guessing. The
* op must carry a valid `paymasterData`/`eip7702Auth` so the bundler can
* simulate the paymaster's validation and the delegated execution.
*
* Not on viem's bundler action surface for our serialized shape, so we issue the
* raw request and parse the returned limits ourselves. Callers should treat this
* as best-effort and fall back to static limits on failure.
*/
export async function estimateUserOperationGas(
client: BundlerClient,
op: SerializedUserOperation,
entryPoint: Address,
): Promise<UserOpGasLimits> {
const result = (await client.request({
method: 'eth_estimateUserOperationGas',
params: [op, entryPoint],
} as any)) as any;

return {
callGasLimit: BigInt(result.callGasLimit),
verificationGasLimit: BigInt(result.verificationGasLimit),
preVerificationGas: BigInt(result.preVerificationGas),
paymasterVerificationGasLimit: BigInt(result.paymasterVerificationGasLimit ?? 0),
paymasterPostOpGasLimit: BigInt(result.paymasterPostOpGasLimit ?? 0),
};
}

/**
* Builds and signs a paymaster-sponsored withdrawal userOp for an ephemeral
* 7702 sender, returning it serialized for the broadcast phase.
Expand Down
Loading
Loading