Arena: intermittent approve tx failures when CL8Y allowance already sufficient (infinite approve)

Report

On the TimeCurve Arena page (/timecurve/arena), some wallet flows intermittently surface a failed or rejected approve transaction even when the participant already granted a large / “infinite” CL8Y allowance to TimeCurve (e.g. prior opt-in unlimited approve via Cl8yTimeCurveUnlimitedApprovalFieldset, or a wallet that previously approved type(uint256).max).

Affected surfaces (all share CL8Y approval plumbing):

  • Buy CHARM (CL8Y pay mode; also ETH/USDM legs approve WETH/USDM → Kumbaya router before CL8Y → TimeCurve approve)
  • WarBow hero actions: Steal, Guard, Revenge (CL8Y burn pulls via TimeCurve)

Investigation summary

Approval logic lives primarily in:

  • frontend/src/pages/timeCurveArena/useTimeCurveArenaModel.tsxhandleBuy + ensureTcAllowance
  • frontend/src/lib/cl8yTimeCurveApprovalPreference.ts — exact vs opt-in unlimited sizing (#143 (closed))
  • frontend/src/lib/timeCurveKumbayaSingleTx.ts — USDM → TimeCurveBuyRouter approve

Inconsistent allowance guard (likely contributor)

Buy path (handleBuy) correctly gates approval on the target allowance amount:

const approveAmt = cl8yTimeCurveApprovalAmountWei(totalPull, readCl8yTimeCurveUnlimitedApproval());
if (allow < approveAmt) { /* approve */ }

WarBow path (ensureTcAllowance) uses a different predicate:

if (allow < need) {
  const approveAmt = cl8yTimeCurveApprovalAmountWei(need, readCl8yTimeCurveUnlimitedApproval());
  /* always submits approve — no `allow < approveAmt` re-check */
}

Implications:

Scenario handleBuy ensureTcAllowance (WarBow)
allowneed but < need + 50 bps headroom (exact mode) May approve to refresh headroom Skips approve (may be OK for fixed WarBow burns)
allow is type(uint256).max Skips (allow < approveAmt false) Skips (allow < need false)
Stale RPC allowance == 0 while on-chain max Spurious approve submitted Spurious approve submitted

So WarBow and Buy are not using one shared helper; drift risk is higher on Arena WarBow buttons.

Hypotheses (ranked)

  1. Redundant approve still submitted after stale/zero allowance read (RPC fallback, account switch mid-poll, or wrong spender) → wallet shows a second approve; user rejects or simulation warns → perceived “approve failed”. With on-chain max allowance the tx is unnecessary.
  2. ensureTcAllowance uses allow < need instead of allow < approveAmt (#143 (closed) / inclusion headroom family related to #82 (closed)): edge cases around exact vs unlimited preference; WarBow steal with bypass sums need = stealBurn + bypassBurn — if allowance was sized for base steal only, approve is required (expected), but sizing should use the same approveAmt guard as buy.
  3. Spender mismatch: allowance granted to a non-proxy TimeCurve address (see #61 (closed) DeployDev implementation vs ERC-1967 proxy) while the app reads allowance(owner, proxy) → reads 0, prompts approve; rare on mainnet but possible on misconfigured forks.
  4. USDM / WETH legs (ETH/USDM buy): “infinite” router allowance but new maxIn from a refreshed Kumbaya quote exceeds prior finite approval — approve is required and can fail on non-standard tokens (USDT-style “must reset to 0”); less likely for dev MockCL8Y, worth checking production stable metadata.
  5. Double-submit / nonce contention: rapid double-click on Arena CTAs firing two approves before the first receipt lands.
  6. Unlimited preference off in UI but max allowance on-chain: should skip approve; if failure persists, capture tx revert data — may be unrelated wallet/RPC error surfaced on the approve step before the intended action.
  1. Extract shared helper e.g. ensureCl8yTimeCurveAllowance({ wagmiConfig, owner, token, timeCurve, needWei }) in frontend/src/lib/cl8yTimeCurveApprovalPreference.ts (or adjacent module) used by:
    • Arena handleBuy
    • Arena ensureTcAllowance / WarBow runners
    • Simple useTimeCurveSaleSession.submitBuy (already mirrors buy guard)
  2. Single guard: compute approveAmt = cl8yTimeCurveApprovalAmountWei(needWei, readCl8yTimeCurveUnlimitedApproval()) then if (allow < approveAmt) before writeContract(approve); early-return when needWei <= 0n.
  3. Optional optimization: if allow >= needWei and (allow === maxUint256 or allow >= approveAmt), skip approve without submitting (covers “already infinite” explicitly).
  4. Debug: behind existing Kumbaya buy debug flag, log { spender: tc, allow, needWei, approveAmt, unlimitedPref } when Arena submits approve.
  5. Tests (Vitest): table-driven cases for skip vs approve — especially allow = maxUint256, allow = needWei exact, allow = needWei - 1n, unlimited pref on/off.
  6. Docs: cross-link from wallet-connection §143 noting WarBow shares the same helper (today only buy path is explicit in the table).

Verification checklist

Setup

  • Wallet with CL8Y balance on target chain; Arena sale live (buyFeeRoutingEnabled true).
  • Note TimeCurve proxy address from Arena protocol accordion vs wallet token approval spender.

A. Already unlimited on-chain (max allowance)

  • Approve CL8Y → TimeCurve once with unlimited checkbox enabled (yieldomega.erc20.cl8yTimeCurveUnlimited.v1 = 1).
  • Buy CHARM (CL8Y): only one wallet prompt (buy), no second approve.
  • WarBow Steal (valid victim): only warbowSteal, no approve.
  • WarBow Guard / Revenge (when available): no approve.
  • Repeat after hard refresh (ensure no spurious approve from stale reads).

B. Exact allowance sufficient for next action

  • Disable unlimited checkbox; approve exact gross + headroom for one buy; complete buy.
  • Immediately run WarBow Guard with burn ≤ remaining allowance: should not require approve (or should bump headroom consistently if policy requires — document expected behavior after fix).

C. ETH / USDM buy legs

  • Pre-approve USDM (or WETH) to router with large allowance; run ETH/USDM buy with stable quote refresh: approve only when allowance < maxIn.
  • With router allowance already max, buy via single-tx buyViaKumbaya when configured: no redundant stable approve.

D. Regression / failure capture

  • If approve still fails: capture tx hash, revert reason, allowance(owner, TimeCurve) and allowance(owner, router) via explorer/cast, pay mode, and CTA (buy / steal / guard / revenge).
  • Confirm wallet chainId matches VITE_CHAIN_ID (#95 (closed)).
  • Confirm spender is proxy, not implementation (#61 (closed)).

E. Automated

  • npm test -- cl8yTimeCurveApprovalPreference (existing) + new helper tests after implementation.
  • Optional Anvil E2E: wallet with max allowance → Arena buy + WarBow action without approve in trace (Playwright / cast eth_getTransactionReceipt).
  • #143 (closed) ERC-20 approval sizing / unlimited opt-in
  • #82 (closed) submit-time sizing / allowance headroom
  • #144 (closed) wallet session drift during multi-step buy
  • INV-ERC20-APPROVAL-143 in docs/testing/invariants-and-business-logic.md
Edited by Mad Dev