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
16 changes: 12 additions & 4 deletions src/commands/shield.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ import {
computeShieldMaxAmount,
estimateShieldGasReserveWei,
refineShieldMaxAmount,
SHIELD_GAS_LIMIT,
shieldMaxFeeReserveWei,
} from "../utils/shield-max.js";
import {
assertTornadoDepositAmount,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -919,7 +927,7 @@ export function registerShieldCommand(program: Command): void {
data: call.data,
value: call.value,
},
2_000_000n
SHIELD_GAS_LIMIT
);
}
}
Expand Down Expand Up @@ -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 };
Expand Down
3 changes: 2 additions & 1 deletion src/lib/shield-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down
35 changes: 33 additions & 2 deletions src/utils/shield-max.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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). */
Expand Down
58 changes: 58 additions & 0 deletions tests/shield-max.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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)
);
});
});