diff --git a/src/commands/shield.ts b/src/commands/shield.ts index 2f18bca..551b9c7 100644 --- a/src/commands/shield.ts +++ b/src/commands/shield.ts @@ -87,6 +87,8 @@ import { computeShieldMaxAmount, estimateShieldGasReserveWei, refineShieldMaxAmount, + SHIELD_GAS_LIMIT, + shieldMaxFeeReserveWei, } from "../utils/shield-max.js"; import { assertTornadoDepositAmount, @@ -834,9 +836,15 @@ export function registerShieldCommand(program: Command): void { data: calls[0]!.data, value: calls[0]!.value, }, - 2_000_000n + SHIELD_GAS_LIMIT ); - const estimatedFeeWei = BigInt(feePreview.estimatedMax); + const estimatedFeeWei = shieldMaxFeeReserveWei({ + batch, + estimatedMaxWei: BigInt(feePreview.estimatedMax), + maxFeePerGasWei: feePreview.maxFeePerGasWei + ? BigInt(feePreview.maxFeePerGasWei) + : undefined, + }); const refined = refineShieldMaxAmount({ isEth: tokenMeta.isEth, protocol, @@ -919,7 +927,7 @@ export function registerShieldCommand(program: Command): void { data: call.data, value: call.value, }, - 2_000_000n + SHIELD_GAS_LIMIT ); } } @@ -1084,7 +1092,7 @@ export function registerShieldCommand(program: Command): void { to: call.to, data: call.data, value: call.value, - gas: 2_000_000n, + gas: SHIELD_GAS_LIMIT, } ); return { hash }; diff --git a/src/lib/shield-flow.ts b/src/lib/shield-flow.ts index 085d116..562124e 100644 --- a/src/lib/shield-flow.ts +++ b/src/lib/shield-flow.ts @@ -18,6 +18,7 @@ import { import { assertTornadoDepositAmount } from "../utils/tornado-pools.js"; import type { BalancesSnapshot } from "./balances-snapshot.js"; import { makePublicAccountsStorage } from "../utils/public-accounts"; +import { SHIELD_GAS_LIMIT } from "../utils/shield-max.js"; import { makeStealthAccountsStorage, parseStealthIndex, @@ -689,7 +690,7 @@ export async function broadcastShield(opts: { to: tx.to, data: tx.data, value: tx.value, - gas: 2_000_000n, + gas: SHIELD_GAS_LIMIT, }); return [{ type: "shield", hash }]; } finally { diff --git a/src/utils/shield-max.ts b/src/utils/shield-max.ts index b6b5a8d..5a30d36 100644 --- a/src/utils/shield-max.ts +++ b/src/utils/shield-max.ts @@ -3,8 +3,39 @@ import { makePublicClient } from "./rpc.js"; /** Same gas cap shield uses when broadcasting a single EOA deposit. */ export const SHIELD_GAS_LIMIT = 2_000_000n; +/** 1.3× pad on gas × maxFee so --amount-max survives fee ticks before send. */ +export const SHIELD_FEE_PAD_NUM = 13n; +export const SHIELD_FEE_PAD_DEN = 10n; + +export function padShieldFeeWei(feeWei: bigint): bigint { + return (feeWei * SHIELD_FEE_PAD_NUM) / SHIELD_FEE_PAD_DEN; +} + +/** Node checks `gasLimit × maxFee + value`; EOA broadcast pins gas to SHIELD_GAS_LIMIT. */ +export function eoaShieldFeeReserveWei(maxFeePerGas: bigint): bigint { + return padShieldFeeWei(SHIELD_GAS_LIMIT * maxFeePerGas); +} + +/** + * Wei to reserve when refining --amount-max after a live fee preview. + * EOA must not use a tighter `estimateGas` — the signed tx still sets + * `gas: SHIELD_GAS_LIMIT`, and the node charges the full limit at submission. + */ +export function shieldMaxFeeReserveWei(opts: { + batch: boolean; + estimatedMaxWei: bigint; + maxFeePerGasWei?: bigint; +}): bigint { + if (!opts.batch && opts.maxFeePerGasWei != null && opts.maxFeePerGasWei > 0n) { + const fromLimit = eoaShieldFeeReserveWei(opts.maxFeePerGasWei); + const fromEstimate = padShieldFeeWei(opts.estimatedMaxWei); + return fromLimit > fromEstimate ? fromLimit : fromEstimate; + } + return padShieldFeeWei(opts.estimatedMaxWei); +} + /** - * Conservative wei reserve for a shield (gas × fee × 1.2). + * Conservative wei reserve for a shield (gas × fee × pad). * Uses ~110% of latest base fee, same pattern as transfer --amount-max. */ export async function estimateShieldGasReserveWei( @@ -27,7 +58,7 @@ export async function estimateShieldGasReserveWei( "Could not determine gas price to compute shield --amount-max." ); } - return (SHIELD_GAS_LIMIT * maxFeePerGas * 12n) / 10n; + return eoaShieldFeeReserveWei(maxFeePerGas); } /** Floor `amount` down to a multiple of `minDenom` (0 if amount < min). */ diff --git a/tests/shield-max.test.ts b/tests/shield-max.test.ts index 4933ea1..23b6fa0 100644 --- a/tests/shield-max.test.ts +++ b/tests/shield-max.test.ts @@ -3,7 +3,13 @@ import { describe, it } from "node:test"; import { computeShieldMaxAmount, + eoaShieldFeeReserveWei, + padShieldFeeWei, refineShieldMaxAmount, + SHIELD_FEE_PAD_DEN, + SHIELD_FEE_PAD_NUM, + SHIELD_GAS_LIMIT, + shieldMaxFeeReserveWei, tornadoFloorToMinDenom, } from "../src/utils/shield-max.js"; @@ -181,3 +187,55 @@ describe("refineShieldMaxAmount", () => { ); }); }); + +describe("padShieldFeeWei / shieldMaxFeeReserveWei", () => { + it("pads 1.3×", () => { + assert.equal(SHIELD_FEE_PAD_NUM, 13n); + assert.equal(SHIELD_FEE_PAD_DEN, 10n); + assert.equal(padShieldFeeWei(1000n), 1300n); + }); + + it("EOA reserve uses the 2M broadcast gas cap, not a tighter estimateGas", () => { + const maxFeePerGas = 117_279_367n; + const estimateGas = 1_088_123n; + const estimatedMax = estimateGas * maxFeePerGas; + const reserved = shieldMaxFeeReserveWei({ + batch: false, + estimatedMaxWei: estimatedMax, + maxFeePerGasWei: maxFeePerGas, + }); + const nodeCheck = SHIELD_GAS_LIMIT * maxFeePerGas; + assert.equal(reserved, eoaShieldFeeReserveWei(maxFeePerGas)); + assert.ok(reserved >= nodeCheck); + assert.ok(reserved > estimatedMax); + }); + + it("leaves enough ETH for the node's gasLimit × maxFee check", () => { + const maxFeePerGas = 117_279_367n; + const balance = 16_235_115_418_919_695n; + const estimateGas = 1_088_123n; + const tooTight = estimateGas * maxFeePerGas; + const reserved = shieldMaxFeeReserveWei({ + batch: false, + estimatedMaxWei: tooTight, + maxFeePerGasWei: maxFeePerGas, + }); + const amount = refineShieldMaxAmount({ + isEth: true, + protocol: "railgun", + currentAmount: balance - tooTight, + ethBalance: balance, + estimatedFeeWei: reserved, + }); + const nodeCost = SHIELD_GAS_LIMIT * maxFeePerGas + amount; + assert.ok(nodeCost <= balance); + }); + + it("batch UserOp reserve pads the bundler estimate", () => { + const estimatedMax = 50_000_000_000_000n; + assert.equal( + shieldMaxFeeReserveWei({ batch: true, estimatedMaxWei: estimatedMax }), + padShieldFeeWei(estimatedMax) + ); + }); +});