QA Anvil time-warp vs wall-clock swapDeadline breaks buyViaKumbaya (AnvilKumbayaFixture Expired) #83

Closed
opened 2026-04-27 07:23:59 +00:00 by PlasticDigits · 10 comments
PlasticDigits commented 2026-04-27 07:23:59 +00:00 (Migrated from gitlab.com)

Summary

Follow-up from #75 (indexer entry_pay_asset='eth' evidence via live buyViaKumbaya) and the #82 note: on a QA Anvil stack that had used evm_increaseTime / anvil_increaseTime to move chain time, the last-mile signed buyViaKumbaya tx reverted on AnvilKumbayaRouter Expired() even though source, integration tests, and Postgres roundtrip already PASS and the frontend reached the swap router with correct wiring.

This is a chain-time vs deadline encoding mismatch in the Kumbaya single-tx path under artificial time warping, not a user-facing bug on normal-clock networks.

Root cause (repo investigation)

  1. Swap deadline is wall-clock based
    swapDeadlineUnixSec in frontend/src/lib/timeCurveKumbayaSwap.ts sets
    Math.floor(Date.now() / 1000) + bufferSec (default buffer 600s).
    The same helper is used from useTimeCurveSaleSession, useTimeCurveArenaModel, and timeCurveKumbayaSingleTx.ts when building buyViaKumbaya(..., swapDeadline, ...).

  2. The Anvil fixture enforces deadline against block.timestamp
    In contracts/src/fixtures/AnvilKumbayaFixture.sol, AnvilKumbayaRouter.exactOutput does
    if (block.timestamp > params.deadline) revert Expired();
    before executing the swap.
    TimeCurveBuyRouter.buyViaKumbaya passes that swapDeadline through unchanged to the Kumbaya router (TimeCurveBuyRouter.sol → kumbayaRouter.exactOutput).

  3. QA stacks advance block.timestamp without moving the browser clock
    e.g. contracts/script/anvil_rich_state.sh calls cast rpc anvil_increaseTime in warp_to_at_least / warp_past_timcurve_deadline to bring chain time to (or past) onchain TimeCurve.deadline and for post-end flows.
    After a large warp, block.timestamp can be many minutes or hours ahead of Date.now(), so a deadline computed as Date.now() + 600 is still in the past relative to block.timestamp at tx execution → Expired().

  4. Contrast: elsewhere the app already reasons about chain time
    e.g. useTimeCurveArenaModel documents that display / phase logic should track block.timestamp, not wall-clock drift (blockChainSec, effectiveLedgerSec). The Kumbaya swap deadline is still the exception: it does not use head block time + buffer.

Why mainnet / typical testnets are unaffected

On live chains, head block.timestamp tracks real time closely; Date.now() + 600 is usually ahead of inclusion-time block.timestamp, so the Uniswap-style deadline check passes. The failure mode is specific to Anvil (or any environment) that jumps chain time far ahead of wall time.

Proposed solutions (pick or combine)

  • Change: When the wallet is connected to an RPC, compute
    swapDeadline = latestHeadBlockTimestamp + bufferSec
    (and optionally a small client-side margin), instead of Date.now() + bufferSec.
  • Where: Centralize in timeCurveKumbayaSwap.ts and/or the call sites in useTimeCurveSaleSession, useTimeCurveArenaModel, and submitKumbayaSingleTxBuy so single-tx and UI-built txs share one rule.
  • Data source: useBlock({ watch: true }) in components, or getBlock / getBlockNumber in submitKumbayaSingleTxBuy via the wagmi Config (must run immediately before building calldata, like other submit-time invariants in #82).
  • Tradeoff: Slightly more RPC coupling; on very stale heads, deadline follows chain (still desirable for onchain block.timestamp checks).

B — QA / infra: ordering and expectations

  • Document in the relevant playbooks (E2E / manual QA, not a code change in this task): for stacks where anvil_increaseTime is used, either:
    • run buyViaKumbaya live evidence before any large time warp in the session, or
    • use a fresh Anvil (or anvil_reset / equivalent) for the Kumbaya proof after warping, or
    • avoid warping for the slice that must produce the indexer row (aligns with “normal-clock chains unaffected”).
  • Scripts: Any automation that currently chains anvil_rich_state.sh (or similar) before browser Kumbaya flows should be reordered or split.

C — Fixture-only relaxation (low priority, risks prod parity)

  • Option: Dev-only / test-only router behavior (e.g. mock “infinite” deadline) weakens parity with production routers that enforce deadlines — not recommended unless only used in isolated tests, not the shared AnvilKumbaya fixture that mirrors production interfaces.

Concrete follow-ups (for the chosen approach)

  • If A: Implementation + a short regression test matrix: wall-aligned chain (today’s behavior) and a manual or scripted case where anvil_increaseTime is applied, then a fresh getBlock + buyViaKumbaya still succeeds.
  • If B: Update the agent/QA workflow for #75 evidence and cross-link from #82 / #75 as needed.
  • Indexer: entry_pay_asset for BuyViaKumbaya — #67
  • Submit-time sizing — #82
  • TimeCurveBuyRouter / Kumbaya — #65, #66, #78
## Summary Follow-up from [#75](https://gitlab.com/PlasticDigits/yieldomega/-/issues/75) (indexer `entry_pay_asset='eth'` evidence via live `buyViaKumbaya`) and the [#82](https://gitlab.com/PlasticDigits/yieldomega/-/issues/82) note: on a **QA Anvil stack** that had used **`evm_increaseTime` / `anvil_increaseTime`** to move chain time, the **last-mile signed `buyViaKumbaya` tx** reverted on **`AnvilKumbayaRouter` `Expired()`** even though **source, integration tests, and Postgres roundtrip already PASS** and the **frontend reached the swap router** with correct wiring. This is a **chain-time vs deadline encoding** mismatch in the **Kumbaya single-tx path** under **artificial time warping**, not a user-facing bug on **normal-clock** networks. ## Root cause (repo investigation) 1. **Swap deadline is wall-clock based** `swapDeadlineUnixSec` in `frontend/src/lib/timeCurveKumbayaSwap.ts` sets `Math.floor(Date.now() / 1000) + bufferSec` (default buffer **600s**). The same helper is used from `useTimeCurveSaleSession`, `useTimeCurveArenaModel`, and `timeCurveKumbayaSingleTx.ts` when building `buyViaKumbaya(..., swapDeadline, ...)`. 2. **The Anvil fixture enforces deadline against `block.timestamp`** In `contracts/src/fixtures/AnvilKumbayaFixture.sol`, `AnvilKumbayaRouter.exactOutput` does `if (block.timestamp > params.deadline) revert Expired();` before executing the swap. `TimeCurveBuyRouter.buyViaKumbaya` passes that `swapDeadline` through unchanged to the Kumbaya router (`TimeCurveBuyRouter.sol` → `kumbayaRouter.exactOutput`). 3. **QA stacks advance `block.timestamp` without moving the browser clock** e.g. `contracts/script/anvil_rich_state.sh` calls `cast rpc anvil_increaseTime` in `warp_to_at_least` / `warp_past_timcurve_deadline` to bring **chain time** to (or past) onchain `TimeCurve.deadline` and for post-end flows. After a large warp, **`block.timestamp` can be many minutes or hours *ahead* of `Date.now()`**, so a deadline computed as `Date.now() + 600` is **still in the past** relative to `block.timestamp` at tx execution → **`Expired()`**. 4. **Contrast: elsewhere the app already reasons about chain time** e.g. `useTimeCurveArenaModel` documents that display / phase logic should track **`block.timestamp`**, not wall-clock drift (`blockChainSec`, `effectiveLedgerSec`). The Kumbaya **swap deadline** is still the exception: it does **not** use head block time + buffer. ## Why mainnet / typical testnets are unaffected On live chains, **head `block.timestamp`** tracks **real time** closely; `Date.now() + 600` is usually **ahead** of inclusion-time `block.timestamp`, so the Uniswap-style deadline check passes. The failure mode is **specific to Anvil (or any environment) that jumps chain time far ahead of wall time**. ## Proposed solutions (pick or combine) ### A — **Align swap deadline with chain time (recommended for correctness)** - **Change:** When the wallet is connected to an RPC, compute `swapDeadline = latestHeadBlockTimestamp + bufferSec` (and optionally a small client-side margin), instead of `Date.now() + bufferSec`. - **Where:** Centralize in `timeCurveKumbayaSwap.ts` and/or the call sites in `useTimeCurveSaleSession`, `useTimeCurveArenaModel`, and `submitKumbayaSingleTxBuy` so **single-tx and UI-built txs** share one rule. - **Data source:** `useBlock({ watch: true })` in components, or `getBlock` / `getBlockNumber` in `submitKumbayaSingleTxBuy` via the wagmi `Config` (must run **immediately before** building calldata, like other submit-time invariants in [#82](https://gitlab.com/PlasticDigits/yieldomega/-/issues/82)). - **Tradeoff:** Slightly more RPC coupling; on very stale heads, deadline follows chain (still desirable for onchain `block.timestamp` checks). ### B — **QA / infra: ordering and expectations** - **Document** in the relevant playbooks (E2E / manual QA, not a code change in this task): for stacks where **`anvil_increaseTime`** is used, either: - run **`buyViaKumbaya` live evidence *before* any large time warp** in the session, **or** - use a **fresh Anvil** (or `anvil_reset` / equivalent) for the Kumbaya proof after warping, **or** - **avoid warping** for the slice that must produce the indexer row (aligns with “normal-clock chains unaffected”). - **Scripts:** Any automation that currently chains `anvil_rich_state.sh` (or similar) *before* browser Kumbaya flows should be reordered or split. ### C — **Fixture-only relaxation (low priority, risks prod parity)** - **Option:** Dev-only / test-only router behavior (e.g. mock “infinite” deadline) **weakens** parity with production routers that enforce deadlines — **not recommended** unless only used in isolated tests, not the shared AnvilKumbaya fixture that mirrors production interfaces. ## Concrete follow-ups (for the chosen approach) - **If A:** Implementation + a short regression test matrix: **wall-aligned chain** (today’s behavior) **and** a manual or scripted case where `anvil_increaseTime` is applied, then a **fresh** `getBlock` + `buyViaKumbaya` still succeeds. - **If B:** Update the **agent/QA** workflow for [#75](https://gitlab.com/PlasticDigits/yieldomega/-/issues/75) evidence and cross-link from [#82](https://gitlab.com/PlasticDigits/yieldomega/-/issues/82) / [#75](https://gitlab.com/PlasticDigits/yieldomega/-/issues/75) as needed. ## Cross-links - Indexer: `entry_pay_asset` for `BuyViaKumbaya` — [#67](https://gitlab.com/PlasticDigits/yieldomega/-/issues/67) - Submit-time sizing — [#82](https://gitlab.com/PlasticDigits/yieldomega/-/issues/82) - `TimeCurveBuyRouter` / Kumbaya — [#65](https://gitlab.com/PlasticDigits/yieldomega/-/issues/65), [#66](https://gitlab.com/PlasticDigits/yieldomega/-/issues/66), [#78](https://gitlab.com/PlasticDigits/yieldomega/-/issues/78)
PlasticDigits commented 2026-04-27 07:24:00 +00:00 (Migrated from gitlab.com)

assigned to @PlasticDigits

assigned to @PlasticDigits
PlasticDigits commented 2026-04-27 07:54:25 +00:00 (Migrated from gitlab.com)

Shipped (Option A) — verify request for @brouie

Merged to main: Kumbaya swapDeadline now uses getBlock({ blockTag: 'latest' }).timestamp + 600s via fetchSwapDeadlineUnixSec instead of Date.now(), for single-tx buyViaKumbaya, two-step exactOutput, Simple + Arena. Deadline is read immediately before the swap write (after wrap/approve) and in submitKumbayaSingleTxBuy after any USDM approve.

Docs / cross-links: invariants — #83, kumbaya.md — QA warp + stacks table + Option B, timecurve-views, play skills + skills/README.md, contributor guardrails item 24.

Issue left open for your sign-off.

Checklist for @brouie

  • Warped Anvil + ETH single-tx: Run default stack (or any chain) that has executed anvil_rich_state / large anvil_increaseTime so block.timestamp ≫ wall clock; connect wallet; ETH → buyViaKumbaya on Simple or Arena — expect no router Expired() from deadline alone.
  • USDM single-tx: Same stack if fixtures support USDM; confirm approve (if any) still precedes deadline fetch and tx succeeds.
  • Two-step Kumbaya + buy: With timeCurveBuyRouter == 0, run ETH/USDM two-step path — deadline fetch runs after wrap/approve, before exactOutput.
  • Unwarped regression: Fresh SKIP_ANVIL_RICH_STATE=1 stack or mainnet-like RPC — buys still succeed (deadline still ~600s ahead of head time).
  • Unit tests: cd frontend && npx vitest run src/lib/timeCurveKumbayaSwap.test.ts (CI runs full npm test).

Option B (workflows without this build, or ordering): see kumbaya.md § QA Anvil time warp — fresh Anvil, SKIP_ANVIL_RICH_STATE=1, evidence before warp, or split automation.

## Shipped (Option A) — verify request for @brouie **Merged to `main`:** Kumbaya **`swapDeadline`** now uses **`getBlock({ blockTag: 'latest' }).timestamp + 600s`** via [`fetchSwapDeadlineUnixSec`](https://gitlab.com/PlasticDigits/yieldomega/-/blob/main/frontend/src/lib/timeCurveKumbayaSwap.ts) instead of **`Date.now()`**, for **single-tx `buyViaKumbaya`**, **two-step `exactOutput`**, Simple + Arena. Deadline is read **immediately before** the swap write (after wrap/approve) and in **`submitKumbayaSingleTxBuy`** after any USDM approve. **Docs / cross-links:** [invariants — #83](https://gitlab.com/PlasticDigits/yieldomega/-/blob/main/docs/testing/invariants-and-business-logic.md), [kumbaya.md — QA warp + stacks table + Option B](https://gitlab.com/PlasticDigits/yieldomega/-/blob/main/docs/integrations/kumbaya.md), [timecurve-views](https://gitlab.com/PlasticDigits/yieldomega/-/blob/main/docs/frontend/timecurve-views.md), play skills + [`skills/README.md`](https://gitlab.com/PlasticDigits/yieldomega/-/blob/main/skills/README.md), contributor guardrails item 24. Issue left **open** for your sign-off. ### Checklist for @brouie - [ ] **Warped Anvil + ETH single-tx:** Run default stack (or any chain) that has executed **`anvil_rich_state`** / large **`anvil_increaseTime`** so **`block.timestamp` ≫ wall clock**; connect wallet; **ETH → buyViaKumbaya** on Simple or Arena — expect **no** router **`Expired()`** from deadline alone. - [ ] **USDM single-tx:** Same stack if fixtures support USDM; confirm approve (if any) still precedes deadline fetch and tx succeeds. - [ ] **Two-step Kumbaya + `buy`:** With **`timeCurveBuyRouter == 0`**, run ETH/USDM two-step path — deadline fetch runs **after** wrap/approve, **before** `exactOutput`. - [ ] **Unwarped regression:** Fresh **`SKIP_ANVIL_RICH_STATE=1`** stack or mainnet-like RPC — buys still succeed (deadline still ~600s ahead of head time). - [ ] **Unit tests:** `cd frontend && npx vitest run src/lib/timeCurveKumbayaSwap.test.ts` (CI runs full `npm test`). **Option B** (workflows without this build, or ordering): see [kumbaya.md § QA Anvil time warp](https://gitlab.com/PlasticDigits/yieldomega/-/blob/main/docs/integrations/kumbaya.md#qa-anvil-time-warp-and-swap-deadline-issue-83) — fresh Anvil, **`SKIP_ANVIL_RICH_STATE=1`**, evidence before warp, or split automation.
Brouie commented 2026-04-28 02:33:42 +00:00 (Migrated from gitlab.com)

Verified on 5dd20d8 end-to-end. Encoding is correct, signing off.

Source review

fetchSwapDeadlineUnixSec(cfg, 600) wired into all three call sites:

  • frontend/src/lib/timeCurveKumbayaSingleTx.ts line 102 - single-tx buyViaKumbaya (after USDM approve gate)
  • frontend/src/pages/timecurve/useTimeCurveSaleSession.ts line 934 - two-step exactOutput (Simple, after wrap/approve)
  • frontend/src/pages/timeCurveArena/useTimeCurveArenaModel.tsx line 2131 - Arena two-step

No leftover Date.now() in any swap/kumbaya path (grep across frontend/src/lib + pages/timecurve returns empty).

Unit tests

cd frontend && npx vitest run src/lib/timeCurveKumbayaSwap.test.ts:

  • 3/3 passing

Full suite: 30 files / 196 tests passing (was 28/188 pre-#83, +2 files +8 tests).

Calldata evidence - chain-aligned encoding under warp

Setup: SKIP_ANVIL_RICH_STATE=1 stack + cast rpc evm_increaseTime 3600 followed by evm_mine to put block.timestamp far ahead of wall clock, Kumbaya fixtures deployed, browser ETH single-tx via Simple page.

Decoded the resulting buyViaKumbaya calldata via cast --calldata-decode:

  • charmWad: 4526468211341193721
  • codeHash: 0x00..00
  • plant: false
  • payKind: 0 (PAY_ETH)
  • swapDeadline: 1777344978
  • maxIn: 4717574000335640
  • path: WETH to CL8Y

swapDeadline = 1777344978, chain_ts at getBlock was 1777344378, so deadline = chain + 600 exactly. Browser console debug print at the same submit confirmed fetchSwapDeadlineUnixSec returned 1777344978 while Math.floor(Date.now()/1000) + 600 was 1777342111 - clean ~2867s gap proving the deadline followed chain head, not wall clock.

Unwarped regression

Fresh SKIP_ANVIL_RICH_STATE=1 stack, Kumbaya fixtures deployed, ETH single-tx via Simple page, slider at ~2.1 CHARM:

  • Tx succeeded clean
  • +120s by 0xf39fd6...2266 shown on timer overlay
  • Recent Buys: 0xf39fd6...2266 / 2.103852668695576051 / +120s / tx 0x15982fc2...

USDM single-tx + two-step

Did not run live USDM repro. Source path is the same submitKumbayaSingleTxBuy with payWith === usdm branch - adds an erc20 approve gate before the same fetchSwapDeadlineUnixSec call, so coverage is by equivalence. Two-step paths in useTimeCurveSaleSession.ts:934 and useTimeCurveArenaModel.tsx:2131 both call fetchSwapDeadlineUnixSec AFTER wrap/approve and BEFORE exactOutput - order verified via source. Flag if you want a live USDM run.

One heads-up - buffer drift on warped Anvil

Posting for awareness, not blocking sign-off.

On the warped stack, encoding was correct (see calldata above) but the tx still hit Expired() because ~743s of chain time elapsed between getBlock and tx inclusion. Block timestamps:

  • block 14: 1777343657
  • block 15: 1777343671
  • block 16: 1777344378 (getBlock here, deadline = +600 = 1777344978)
  • block 17: 1777345121 (tx mined here, > deadline by 143s)

Probably a combination of debug pauses + MetaMask prompt sitting open during diagnosis. With normal sub-30s confirm the 600s buffer is plenty (unwarped repro above mined cleanly). The kumbaya.md QA Anvil time warp Option B runbook covers exactly this case.

If you ever want the buffer tunable per-env or wider on Anvil-only, easy enhancement, but not needed for ship.

Closing #83.

Verified on 5dd20d8 end-to-end. Encoding is correct, signing off. ## Source review fetchSwapDeadlineUnixSec(cfg, 600) wired into all three call sites: - frontend/src/lib/timeCurveKumbayaSingleTx.ts line 102 - single-tx buyViaKumbaya (after USDM approve gate) - frontend/src/pages/timecurve/useTimeCurveSaleSession.ts line 934 - two-step exactOutput (Simple, after wrap/approve) - frontend/src/pages/timeCurveArena/useTimeCurveArenaModel.tsx line 2131 - Arena two-step No leftover Date.now() in any swap/kumbaya path (grep across frontend/src/lib + pages/timecurve returns empty). ## Unit tests cd frontend && npx vitest run src/lib/timeCurveKumbayaSwap.test.ts: - 3/3 passing Full suite: 30 files / 196 tests passing (was 28/188 pre-#83, +2 files +8 tests). ## Calldata evidence - chain-aligned encoding under warp Setup: SKIP_ANVIL_RICH_STATE=1 stack + cast rpc evm_increaseTime 3600 followed by evm_mine to put block.timestamp far ahead of wall clock, Kumbaya fixtures deployed, browser ETH single-tx via Simple page. Decoded the resulting buyViaKumbaya calldata via cast --calldata-decode: - charmWad: 4526468211341193721 - codeHash: 0x00..00 - plant: false - payKind: 0 (PAY_ETH) - swapDeadline: 1777344978 - maxIn: 4717574000335640 - path: WETH to CL8Y swapDeadline = 1777344978, chain_ts at getBlock was 1777344378, so deadline = chain + 600 exactly. Browser console debug print at the same submit confirmed fetchSwapDeadlineUnixSec returned 1777344978 while Math.floor(Date.now()/1000) + 600 was 1777342111 - clean ~2867s gap proving the deadline followed chain head, not wall clock. ## Unwarped regression Fresh SKIP_ANVIL_RICH_STATE=1 stack, Kumbaya fixtures deployed, ETH single-tx via Simple page, slider at ~2.1 CHARM: - Tx succeeded clean - +120s by 0xf39fd6...2266 shown on timer overlay - Recent Buys: 0xf39fd6...2266 / 2.103852668695576051 / +120s / tx 0x15982fc2... ## USDM single-tx + two-step Did not run live USDM repro. Source path is the same submitKumbayaSingleTxBuy with payWith === usdm branch - adds an erc20 approve gate before the same fetchSwapDeadlineUnixSec call, so coverage is by equivalence. Two-step paths in useTimeCurveSaleSession.ts:934 and useTimeCurveArenaModel.tsx:2131 both call fetchSwapDeadlineUnixSec AFTER wrap/approve and BEFORE exactOutput - order verified via source. Flag if you want a live USDM run. ## One heads-up - buffer drift on warped Anvil Posting for awareness, not blocking sign-off. On the warped stack, encoding was correct (see calldata above) but the tx still hit Expired() because ~743s of chain time elapsed between getBlock and tx inclusion. Block timestamps: - block 14: 1777343657 - block 15: 1777343671 - block 16: 1777344378 (getBlock here, deadline = +600 = 1777344978) - block 17: 1777345121 (tx mined here, > deadline by 143s) Probably a combination of debug pauses + MetaMask prompt sitting open during diagnosis. With normal sub-30s confirm the 600s buffer is plenty (unwarped repro above mined cleanly). The kumbaya.md QA Anvil time warp Option B runbook covers exactly this case. If you ever want the buffer tunable per-env or wider on Anvil-only, easy enhancement, but not needed for ship. Closing #83.
Brouie (Migrated from gitlab.com) closed this issue 2026-04-28 02:34:23 +00:00
Brouie commented 2026-04-28 02:45:25 +00:00 (Migrated from gitlab.com)

mentioned in issue #75

mentioned in issue #75
Brouie commented 2026-04-28 02:45:45 +00:00 (Migrated from gitlab.com)

cc @PlasticDigits (forgot to tag on the close note above)

cc @PlasticDigits (forgot to tag on the close note above)
PlasticDigits commented 2026-04-28 03:21:40 +00:00 (Migrated from gitlab.com)

mentioned in issue #84

mentioned in issue #84
Brouie commented 2026-04-28 03:34:35 +00:00 (Migrated from gitlab.com)

mentioned in issue #58

mentioned in issue #58
Brouie commented 2026-04-28 08:08:46 +00:00 (Migrated from gitlab.com)

mentioned in issue #38

mentioned in issue #38
PlasticDigits commented 2026-04-30 05:25:10 +00:00 (Migrated from gitlab.com)

mentioned in issue #100

mentioned in issue #100
PlasticDigits commented 2026-05-27 07:07:50 +00:00 (Migrated from gitlab.com)

mentioned in issue #251

mentioned in issue #251
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#83
No description provided.