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
1 change: 1 addition & 0 deletions src/components/swap/SwapForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export function SwapForm() {
slippage: form.slippage,
toTokenDecimals: form.toToken?.decimals ?? null,
barterPreGasOutputAmount: form.barterPreGasOutputAmount,
barterGasEstimation: form.barterGasEstimation,
toTokenPrice: form.toPrice,
ethPrice: form.ethPrice,
isEthOutput,
Expand Down
88 changes: 87 additions & 1 deletion src/hooks/__tests__/miles-math.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
*/

import { describe, it, expect } from "vitest"
import { computeSurplusEth } from "../use-estimated-miles"
import { computeSurplusEth, predictGasLimit } from "../use-estimated-miles"

// ──────────────────────────────────────────────────────────────────────────
// Constants — must match use-estimated-miles.ts
Expand Down Expand Up @@ -562,3 +562,89 @@ describe("operator-tunable slippage cap", () => {
}
})
})

// ──────────────────────────────────────────────────────────────────────────
// predictGasLimit — per-swap gasLimit prediction, mirrors the backend's
// `mev-commit/tools/preconf-rpc/fastswap/fastswap.go` formula. Frontend uses
// the same numbers so the bid the user sees and the bid the executor submits
// match. Floor at 400_000 was added in backend commit b2d13572 to avoid
// EIP-150 OOG on simple routes; the frontend mirrors it.
// ──────────────────────────────────────────────────────────────────────────
describe("predictGasLimit", () => {
const FALLBACK_AVG = 450_000n
const WRAPPER_PERMIT = 135_000n
const WRAPPER_ETH = 152_000n
const FLOOR = 400_000n

it("permit path with barter present, scaled above floor", () => {
// 200k × 2.5 = 500k + 135k = 635k > 400k → 635k
expect(predictGasLimit(200_000, true, FALLBACK_AVG)).toBe(500_000n + WRAPPER_PERMIT)
})

it("ETH path with barter present, scaled above floor", () => {
// 200k × 2.5 = 500k + 152k = 652k > 400k → 652k
expect(predictGasLimit(200_000, false, FALLBACK_AVG)).toBe(500_000n + WRAPPER_ETH)
})

it("permit path scaled below floor → returns floor", () => {
// 50k × 2.5 = 125k + 135k = 260k < 400k → 400k
expect(predictGasLimit(50_000, true, FALLBACK_AVG)).toBe(FLOOR)
})

it("ETH path scaled below floor → returns floor", () => {
// 50k × 2.5 = 125k + 152k = 277k < 400k → 400k
expect(predictGasLimit(50_000, false, FALLBACK_AVG)).toBe(FLOOR)
})

it("permit path right at the floor boundary (260k → 395k → floor)", () => {
// 104k × 2.5 = 260k + 135k = 395k < 400k → 400k
expect(predictGasLimit(104_000, true, FALLBACK_AVG)).toBe(FLOOR)
// 106k × 2.5 = 265k + 135k = 400k = floor → still 400k (>, not >=, so floor)
expect(predictGasLimit(106_000, true, FALLBACK_AVG)).toBe(FLOOR)
// 107k × 2.5 = 267.5k → floor(267500) + 135k = 402_500 > 400_000 → 402_500
expect(predictGasLimit(107_000, true, FALLBACK_AVG)).toBe(267_500n + WRAPPER_PERMIT)
})

it("missing barter (undefined) falls back to avg gas limit", () => {
expect(predictGasLimit(undefined, true, FALLBACK_AVG)).toBe(FALLBACK_AVG)
expect(predictGasLimit(undefined, false, FALLBACK_AVG)).toBe(FALLBACK_AVG)
})

it("invalid barter values (NaN, Infinity, 0, negative) fall back to avg", () => {
expect(predictGasLimit(Number.NaN, true, FALLBACK_AVG)).toBe(FALLBACK_AVG)
expect(predictGasLimit(Number.POSITIVE_INFINITY, true, FALLBACK_AVG)).toBe(FALLBACK_AVG)
expect(predictGasLimit(0, true, FALLBACK_AVG)).toBe(FALLBACK_AVG)
expect(predictGasLimit(-100, true, FALLBACK_AVG)).toBe(FALLBACK_AVG)
})

it("Math.floor in barter × 2.5 (no rounding up)", () => {
// 100_001 × 2.5 = 250_002.5 → floor = 250_002. + 135k = 385_002 < 400k → 400k
expect(predictGasLimit(100_001, true, FALLBACK_AVG)).toBe(FLOOR)
// 110_001 × 2.5 = 275_002.5 → floor = 275_002. + 135k = 410_002 > 400k → 410_002
expect(predictGasLimit(110_001, true, FALLBACK_AVG)).toBe(275_002n + WRAPPER_PERMIT)
})

it("realistic permit-path swap (barter ~120k → 435k limit)", () => {
// 120_000 × 2.5 = 300_000 + 135_000 = 435_000
expect(predictGasLimit(120_000, true, FALLBACK_AVG)).toBe(435_000n)
})

it("multi-hop swap (barter ~350k → 1.01M limit)", () => {
// 350_000 × 2.5 = 875_000 + 135_000 = 1_010_000
expect(predictGasLimit(350_000, true, FALLBACK_AVG)).toBe(1_010_000n)
})

it("p75 gas-used envelope: predictedGasLimit × 0.77 stays above realized p50", () => {
// p75 of `gas_used / gas_limit` across 46 post-floor permit-path swaps
// (2026-05-11 → 2026-05-13). Picked over mean/p50 so gas cost is rarely
// under-predicted — realized miles meet or exceed the badge estimate.
// Spot-check: applied to a representative predicted limit (~435k for a
// 120k-gas barter route), p75 lands at the upper realized envelope
// (~330k–340k), comfortably above the p50 realized gasUsed of ~295k.
const P75_RATIO = 0.77
const predictedLimit = predictGasLimit(120_000, true, FALLBACK_AVG) // 435k
const predictedUsed = Math.floor(Number(predictedLimit) * P75_RATIO)
expect(predictedUsed).toBeGreaterThan(295_000) // > realized p50
expect(predictedUsed).toBeLessThan(360_000) // ≈ realized p75-p80 envelope
})
})
20 changes: 20 additions & 0 deletions src/hooks/use-barter-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,14 @@ interface UseBarterValidationReturn {
* value (older deployment, or ETH path where they're equal).
*/
barterPreGasOutputAmount: bigint | undefined
/**
* Barter's raw `gasEstimation` for the current route. Drives the miles
* estimator's per-swap predicted gasLimit (mirrors the backend formula in
* `mev-commit/tools/preconf-rpc/fastswap/fastswap.go`: `max(400_000,
* floor(gasEstimation × 2.5) + wrapper)`). When undefined the estimator
* falls back to the Edge Config rolling average.
*/
barterGasEstimation: number | undefined
/**
* True when Barter's /route endpoint has failed for the current inputs at least
* UNAVAILABLE_ERROR_THRESHOLD times in a row. Callers should block swap submission
Expand Down Expand Up @@ -125,6 +133,7 @@ export function useBarterValidation({
const [barterPreGasOutputAmount, setBarterPreGasOutputAmount] = useState<bigint | undefined>(
undefined
)
const [barterGasEstimation, setBarterGasEstimation] = useState<number | undefined>(undefined)
const [barterUnavailable, setBarterUnavailable] = useState(false)
/**
* True when the most recent barter response produced an out-of-band shortfall
Expand Down Expand Up @@ -157,6 +166,7 @@ export function useBarterValidation({
setSanityGated(false)
setBarterAmountOut(undefined)
setBarterPreGasOutputAmount(undefined)
setBarterGasEstimation(undefined)
setBarterUnavailable(false)
setSettled(true)
lastSettledKeyRef.current = ""
Expand All @@ -182,6 +192,7 @@ export function useBarterValidation({
setSanityGated(false)
setBarterAmountOut(undefined)
setBarterPreGasOutputAmount(undefined)
setBarterGasEstimation(undefined)
setBarterUnavailable(false)
setSettled(true)
lastSettledKeyRef.current = ""
Expand All @@ -196,6 +207,7 @@ export function useBarterValidation({
setSanityGated(false)
setBarterAmountOut(undefined)
setBarterPreGasOutputAmount(undefined)
setBarterGasEstimation(undefined)
// Do NOT reset barterUnavailable here — if we're in an outage, leaving it true
// across input changes avoids "swap button enables for 300ms then blocks again"
// flicker. Successful validation below clears it.
Expand Down Expand Up @@ -238,6 +250,7 @@ export function useBarterValidation({
if (Math.abs(shortfallRaw) > SANITY_GATE_PCT) {
setBarterAmountOut(undefined)
setBarterPreGasOutputAmount(undefined)
setBarterGasEstimation(undefined)
setShortfallPct(0)
setSanityGated(true)
setBarterUnavailable(false)
Expand All @@ -248,6 +261,11 @@ export function useBarterValidation({

setBarterAmountOut(barterOut)
setBarterPreGasOutputAmount(barterPreGas)
setBarterGasEstimation(
Number.isFinite(route.gasEstimation) && route.gasEstimation > 0
? route.gasEstimation
: undefined
)
setShortfallPct(Math.max(0, shortfallRaw))
setSanityGated(false)
setBarterUnavailable(false)
Expand All @@ -273,6 +291,7 @@ export function useBarterValidation({
// and mark settled so the UI stops spinning.
setBarterAmountOut(undefined)
setBarterPreGasOutputAmount(undefined)
setBarterGasEstimation(undefined)
setShortfallPct(0)
setBarterUnavailable(true)
setSettled(true)
Expand Down Expand Up @@ -336,6 +355,7 @@ export function useBarterValidation({
isValidating: !isCurrent || !settled,
barterAmountOut: isCurrent ? barterAmountOut : undefined,
barterPreGasOutputAmount: isCurrent ? barterPreGasOutputAmount : undefined,
barterGasEstimation: isCurrent ? barterGasEstimation : undefined,
barterUnavailable: isCurrent && barterUnavailable,
}
}
Loading
Loading