Frontend: thread estimateGas + 30% buffer through every wagmi writeContractAsync call site #176

Closed
opened 2026-05-07 02:05:59 +00:00 by Brouie · 3 comments
Brouie commented 2026-05-07 02:05:59 +00:00 (Migrated from gitlab.com)

Severity: P2 — defense-in-depth across all wagmi write paths

split out of #160 follow-up per @PlasticDigits — "We should estimate gas as some wallets do not correctly estimate gas, and this also allows us to add a gas buffer of 30% in case the contract state changes and consumes more gas than expected".

current state at HEAD 284c15c

5 files contain wagmi write paths via writeContractAsync:

frontend/src/pages/timeCurveArena/useTimeCurveArenaModel.tsx        (17 sites)
frontend/src/pages/timecurve/useTimeCurveSaleSession.ts             (11 sites)
frontend/src/lib/timeCurveKumbayaSingleTx.ts                        (5 sites)
frontend/src/pages/referrals/ReferralRegisterSection.tsx            (4 sites)
frontend/src/pages/timecurve/TimeCurveProtocolWarbowRefreshSection.tsx (2 sites — 1 hook, 1 write)

existing infrastructure (partially in place)

frontend/src/lib/estimateContractGas.ts exposes estimateGasUnits(...) — wraps viem estimateContractGas, returns bigint | undefined on best-effort basis. does not apply any buffer. used at 6 call sites in useTimeCurveArenaModel.tsx for display-only preflight (state vars setGasBuy, setGasWarbowSteal, etc.) — the estimated value is never passed to writeContractAsync.

frontend/src/lib/simulateContractWrite.ts exposes simulateWriteContract(...) — wraps viem simulateContract, used in Arena flow for revert-detection preflight.

so:

  • helpers exist for estimate + simulate
  • buffer is not applied anywhere
  • estimates are not threaded into the actual writeContractAsync({ ..., gas }) parameter
  • 4 of 5 write-path files do not even invoke the existing helpers

what dev asked for

  1. estimateGas before submit — protect against wallets that estimate gas incorrectly (some wallets default to a low ceiling that reverts on dynamic-size args)
  2. 30% buffer on top of the estimate — protect against state drift between estimate and submit (e.g. another buy lands between the two, increasing the gas needed for the path-dependent branches)

fix shape

option A — extend the existing helper

add estimateGasWithBuffer(params, bufferPct = 30) to lib/estimateContractGas.ts:

export async function estimateGasWithBuffer(
  params: EstParams,
  bufferPct = 30,
): Promise<bigint | undefined> {
  const raw = await estimateGasUnits(params);
  if (raw === undefined) return undefined;
  return (raw * BigInt(100 + bufferPct)) / 100n;
}

then thread the result into writeContractAsync({ ..., gas: bufferedGas }) at every write site that does not already pass an explicit gas. fallback: if the helper returns undefined, omit the gas field — wagmi falls back to wallet-provided estimation, same as today. no behavior regression on the happy-path RPC.

option B — new wrapper helper

a wrapper submitWithBufferedGas(writeContractAsync, params, bufferPct = 30) that internalizes the estimate + buffer + writeContractAsync call, with consistent error UX via friendlyRevertFromUnknown. less code at call sites; more upfront migration cost across 39 call sites.

where to apply

all 5 files listed above. priority order based on user-facing risk:

  1. useTimeCurveArenaModel.tsx — buy paths, WarBow paths (path-dependent gas, dynamic args)
  2. TimeCurveProtocolWarbowRefreshSection.tsx — refreshWarbowPodium with address[] (extreme state-dependence — see #172)
  3. useTimeCurveSaleSession.ts — multi-step approval + buy flows
  4. timeCurveKumbayaSingleTx.ts — single-tx buy via Kumbaya router
  5. ReferralRegisterSection.tsx — referral registration (lowest risk, simple state mutation)

interaction with #172

#172 is the CRITICAL gas-blowup design ticket on refreshWarbowPodium / finalizeWarbowPodium. that ticket changes the architecture; this ticket protects every other write path today. they are independent — landing this does not close #172 (estimateGas + buffer cannot enable a 50B-gas tx; it just refuses to submit one that would fail).

on refreshWarbowPodium specifically, this ticket adds the soft-cap defense layer that #172 acceptance criterion 2 calls for ("single-tx submit path retired or soft-capped to protect callers from sending >5k candidates accidentally").

acceptance criteria

  • helper that returns gas estimate × (1 + buffer/100) with bufferPct defaulting to 30
  • all 5 write-path files use the buffered helper for writeContractAsync({ ..., gas: ... })
  • fallback path on helper-returns-undefined is preserved (omit gas, let wagmi default)
  • new Vitest unit coverage on the buffer math (e.g. raw 100k → buffered 130k at default; raw undefined → undefined)
  • npm run typecheck + npm test clean

origin

split from #160 follow-up per @PlasticDigits direct request. complementary to #172 (CRITICAL gas-blowup design), #173 (cosmetic double-call), #174 (silent truncation), #175 (Vitest coverage gap).

cc @PlasticDigits

**Severity:** P2 — defense-in-depth across all wagmi write paths split out of #160 follow-up per @PlasticDigits — "We should estimate gas as some wallets do not correctly estimate gas, and this also allows us to add a gas buffer of 30% in case the contract state changes and consumes more gas than expected". ## current state at HEAD `284c15c` 5 files contain wagmi write paths via `writeContractAsync`: ``` frontend/src/pages/timeCurveArena/useTimeCurveArenaModel.tsx (17 sites) frontend/src/pages/timecurve/useTimeCurveSaleSession.ts (11 sites) frontend/src/lib/timeCurveKumbayaSingleTx.ts (5 sites) frontend/src/pages/referrals/ReferralRegisterSection.tsx (4 sites) frontend/src/pages/timecurve/TimeCurveProtocolWarbowRefreshSection.tsx (2 sites — 1 hook, 1 write) ``` ## existing infrastructure (partially in place) `frontend/src/lib/estimateContractGas.ts` exposes `estimateGasUnits(...)` — wraps viem `estimateContractGas`, returns `bigint | undefined` on best-effort basis. **does not apply any buffer.** used at 6 call sites in `useTimeCurveArenaModel.tsx` for **display-only preflight** (state vars `setGasBuy`, `setGasWarbowSteal`, etc.) — the estimated value is never passed to `writeContractAsync`. `frontend/src/lib/simulateContractWrite.ts` exposes `simulateWriteContract(...)` — wraps viem `simulateContract`, used in Arena flow for revert-detection preflight. so: - helpers exist for estimate + simulate - buffer is not applied anywhere - estimates are not threaded into the actual `writeContractAsync({ ..., gas })` parameter - 4 of 5 write-path files do not even invoke the existing helpers ## what dev asked for 1. **estimateGas before submit** — protect against wallets that estimate gas incorrectly (some wallets default to a low ceiling that reverts on dynamic-size args) 2. **30% buffer on top of the estimate** — protect against state drift between estimate and submit (e.g. another buy lands between the two, increasing the gas needed for the path-dependent branches) ## fix shape ### option A — extend the existing helper add `estimateGasWithBuffer(params, bufferPct = 30)` to `lib/estimateContractGas.ts`: ``` export async function estimateGasWithBuffer( params: EstParams, bufferPct = 30, ): Promise<bigint | undefined> { const raw = await estimateGasUnits(params); if (raw === undefined) return undefined; return (raw * BigInt(100 + bufferPct)) / 100n; } ``` then thread the result into `writeContractAsync({ ..., gas: bufferedGas })` at every write site that does not already pass an explicit `gas`. fallback: if the helper returns `undefined`, omit the `gas` field — wagmi falls back to wallet-provided estimation, same as today. no behavior regression on the happy-path RPC. ### option B — new wrapper helper a wrapper `submitWithBufferedGas(writeContractAsync, params, bufferPct = 30)` that internalizes the estimate + buffer + writeContractAsync call, with consistent error UX via `friendlyRevertFromUnknown`. less code at call sites; more upfront migration cost across 39 call sites. ### where to apply all 5 files listed above. priority order based on user-facing risk: 1. `useTimeCurveArenaModel.tsx` — buy paths, WarBow paths (path-dependent gas, dynamic args) 2. `TimeCurveProtocolWarbowRefreshSection.tsx` — refreshWarbowPodium with `address[]` (extreme state-dependence — see #172) 3. `useTimeCurveSaleSession.ts` — multi-step approval + buy flows 4. `timeCurveKumbayaSingleTx.ts` — single-tx buy via Kumbaya router 5. `ReferralRegisterSection.tsx` — referral registration (lowest risk, simple state mutation) ## interaction with #172 #172 is the CRITICAL gas-blowup design ticket on `refreshWarbowPodium` / `finalizeWarbowPodium`. that ticket changes the architecture; this ticket protects every other write path **today**. they are independent — landing this does not close #172 (estimateGas + buffer cannot enable a 50B-gas tx; it just refuses to submit one that would fail). on `refreshWarbowPodium` specifically, this ticket adds the soft-cap defense layer that #172 acceptance criterion 2 calls for ("single-tx submit path retired or soft-capped to protect callers from sending >5k candidates accidentally"). ## acceptance criteria - [ ] helper that returns gas estimate × (1 + buffer/100) with `bufferPct` defaulting to 30 - [ ] all 5 write-path files use the buffered helper for `writeContractAsync({ ..., gas: ... })` - [ ] fallback path on helper-returns-undefined is preserved (omit `gas`, let wagmi default) - [ ] new Vitest unit coverage on the buffer math (e.g. raw 100k → buffered 130k at default; raw undefined → undefined) - [ ] `npm run typecheck` + `npm test` clean ## origin split from #160 follow-up per @PlasticDigits direct request. complementary to #172 (CRITICAL gas-blowup design), #173 (cosmetic double-call), #174 (silent truncation), #175 (Vitest coverage gap). cc @PlasticDigits
Brouie commented 2026-05-07 02:09:59 +00:00 (Migrated from gitlab.com)

mentioned in issue #160

mentioned in issue #160
Brouie commented 2026-05-07 04:13:13 +00:00 (Migrated from gitlab.com)

mentioned in merge request !378

mentioned in merge request !378
PlasticDigits commented 2026-05-07 12:00:03 +00:00 (Migrated from gitlab.com)

mentioned in commit 8eb5f34cfb

mentioned in commit 8eb5f34cfb7a9be50909af79a753a64b39b5d03f
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-05-07 12:00:03 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
code/yieldomega#176
No description provided.