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.tsx—handleBuy+ensureTcAllowancefrontend/src/lib/cl8yTimeCurveApprovalPreference.ts— exact vs opt-in unlimited sizing (#143 (closed))frontend/src/lib/timeCurveKumbayaSingleTx.ts— USDM →TimeCurveBuyRouterapprove
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) |
|---|---|---|
allow ≥ need 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)
- Redundant approve still submitted after stale/zero
allowanceread (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. ensureTcAllowanceusesallow < needinstead ofallow < approveAmt(#143 (closed) / inclusion headroom family related to #82 (closed)): edge cases around exact vs unlimited preference; WarBow steal with bypass sumsneed = stealBurn + bypassBurn— if allowance was sized for base steal only, approve is required (expected), but sizing should use the sameapproveAmtguard as buy.- Spender mismatch: allowance granted to a non-proxy
TimeCurveaddress (see #61 (closed) DeployDev implementation vs ERC-1967 proxy) while the app readsallowance(owner, proxy)→ reads0, prompts approve; rare on mainnet but possible on misconfigured forks. - USDM / WETH legs (ETH/USDM buy): “infinite” router allowance but new
maxInfrom 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 devMockCL8Y, worth checking production stable metadata. - Double-submit / nonce contention: rapid double-click on Arena CTAs firing two approves before the first receipt lands.
- 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.
Recommended fixes
- Extract shared helper e.g.
ensureCl8yTimeCurveAllowance({ wagmiConfig, owner, token, timeCurve, needWei })infrontend/src/lib/cl8yTimeCurveApprovalPreference.ts(or adjacent module) used by:- Arena
handleBuy - Arena
ensureTcAllowance/ WarBow runners - Simple
useTimeCurveSaleSession.submitBuy(already mirrors buy guard)
- Arena
- Single guard: compute
approveAmt = cl8yTimeCurveApprovalAmountWei(needWei, readCl8yTimeCurveUnlimitedApproval())thenif (allow < approveAmt)beforewriteContract(approve); early-return whenneedWei <= 0n. - Optional optimization: if
allow >= needWeiand (allow === maxUint256orallow >= approveAmt), skip approve without submitting (covers “already infinite” explicitly). - Debug: behind existing Kumbaya buy debug flag, log
{ spender: tc, allow, needWei, approveAmt, unlimitedPref }when Arena submits approve. - Tests (Vitest): table-driven cases for skip vs approve — especially
allow = maxUint256,allow = needWeiexact,allow = needWei - 1n, unlimited pref on/off. - 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 (
buyFeeRoutingEnabledtrue). - Note
TimeCurveproxy 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 secondapprove. - WarBow Steal (valid victim): only
warbowSteal, noapprove. - 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
buyViaKumbayawhen configured: no redundant stable approve.
D. Regression / failure capture
- If approve still fails: capture tx hash, revert reason,
allowance(owner, TimeCurve)andallowance(owner, router)via explorer/cast, pay mode, and CTA (buy / steal / guard / revenge). - Confirm wallet
chainIdmatchesVITE_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
approvein trace (Playwright / casteth_getTransactionReceipt).
Related
- #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-143indocs/testing/invariants-and-business-logic.md
Edited by Mad Dev