QA Anvil time-warp vs wall-clock swapDeadline breaks buyViaKumbaya (AnvilKumbayaFixture Expired) #83
Labels
No labels
agent:fix_bugfix
agent:fix_conflicts
agent:fix_security
agent:gap_analysis
agent:implement
agent:open_issues
agents
agent:security_audit
agent:verify
area
timecurve
arena
arena-v2
audit
block:feature_review
block:security
bug
contracts
design
devops
docs
documentation
enhancement
epic
feature
frontend
indexer
low priority
megaeth
operations
ops
performance
QA
qa checklist
referrals
removal
security
smart-contracts
tech-debt
testing
time-arena
timecurve
visual-qa
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
code/yieldomega#83
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Follow-up from #75 (indexer
entry_pay_asset='eth'evidence via livebuyViaKumbaya) and the #82 note: on a QA Anvil stack that had usedevm_increaseTime/anvil_increaseTimeto move chain time, the last-mile signedbuyViaKumbayatx reverted onAnvilKumbayaRouterExpired()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)
Swap deadline is wall-clock based
swapDeadlineUnixSecinfrontend/src/lib/timeCurveKumbayaSwap.tssetsMath.floor(Date.now() / 1000) + bufferSec(default buffer 600s).The same helper is used from
useTimeCurveSaleSession,useTimeCurveArenaModel, andtimeCurveKumbayaSingleTx.tswhen buildingbuyViaKumbaya(..., swapDeadline, ...).The Anvil fixture enforces deadline against
block.timestampIn
contracts/src/fixtures/AnvilKumbayaFixture.sol,AnvilKumbayaRouter.exactOutputdoesif (block.timestamp > params.deadline) revert Expired();before executing the swap.
TimeCurveBuyRouter.buyViaKumbayapasses thatswapDeadlinethrough unchanged to the Kumbaya router (TimeCurveBuyRouter.sol→kumbayaRouter.exactOutput).QA stacks advance
block.timestampwithout moving the browser clocke.g.
contracts/script/anvil_rich_state.shcallscast rpc anvil_increaseTimeinwarp_to_at_least/warp_past_timcurve_deadlineto bring chain time to (or past) onchainTimeCurve.deadlineand for post-end flows.After a large warp,
block.timestampcan be many minutes or hours ahead ofDate.now(), so a deadline computed asDate.now() + 600is still in the past relative toblock.timestampat tx execution →Expired().Contrast: elsewhere the app already reasons about chain time
e.g.
useTimeCurveArenaModeldocuments that display / phase logic should trackblock.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.timestamptracks real time closely;Date.now() + 600is usually ahead of inclusion-timeblock.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)
swapDeadline = latestHeadBlockTimestamp + bufferSec(and optionally a small client-side margin), instead of
Date.now() + bufferSec.timeCurveKumbayaSwap.tsand/or the call sites inuseTimeCurveSaleSession,useTimeCurveArenaModel, andsubmitKumbayaSingleTxBuyso single-tx and UI-built txs share one rule.useBlock({ watch: true })in components, orgetBlock/getBlockNumberinsubmitKumbayaSingleTxBuyvia the wagmiConfig(must run immediately before building calldata, like other submit-time invariants in #82).block.timestampchecks).B — QA / infra: ordering and expectations
anvil_increaseTimeis used, either:buyViaKumbayalive evidence before any large time warp in the session, oranvil_reset/ equivalent) for the Kumbaya proof after warping, oranvil_rich_state.sh(or similar) before browser Kumbaya flows should be reordered or split.C — Fixture-only relaxation (low priority, risks prod parity)
Concrete follow-ups (for the chosen approach)
anvil_increaseTimeis applied, then a freshgetBlock+buyViaKumbayastill succeeds.Cross-links
entry_pay_assetforBuyViaKumbaya— #67TimeCurveBuyRouter/ Kumbaya — #65, #66, #78assigned to @PlasticDigits
Shipped (Option A) — verify request for @brouie
Merged to
main: KumbayaswapDeadlinenow usesgetBlock({ blockTag: 'latest' }).timestamp + 600sviafetchSwapDeadlineUnixSecinstead ofDate.now(), for single-txbuyViaKumbaya, two-stepexactOutput, Simple + Arena. Deadline is read immediately before the swap write (after wrap/approve) and insubmitKumbayaSingleTxBuyafter 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
anvil_rich_state/ largeanvil_increaseTimesoblock.timestamp≫ wall clock; connect wallet; ETH → buyViaKumbaya on Simple or Arena — expect no routerExpired()from deadline alone.buy: WithtimeCurveBuyRouter == 0, run ETH/USDM two-step path — deadline fetch runs after wrap/approve, beforeexactOutput.SKIP_ANVIL_RICH_STATE=1stack or mainnet-like RPC — buys still succeed (deadline still ~600s ahead of head time).cd frontend && npx vitest run src/lib/timeCurveKumbayaSwap.test.ts(CI runs fullnpm 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.Verified on
5dd20d8end-to-end. Encoding is correct, signing off.Source review
fetchSwapDeadlineUnixSec(cfg, 600) wired into all three call sites:
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:
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:
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:
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:
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.
mentioned in issue #75
cc @PlasticDigits (forgot to tag on the close note above)
mentioned in issue #84
mentioned in issue #58
mentioned in issue #38
mentioned in issue #100
mentioned in issue #251