Frontend buy panel: stale charmWad in calldata between slider read and tx submit causes 'execution reverted' on live-clock chain #82
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#82
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?
Found Monday 2026-04-27 during browser walkthrough on live local-anvil stack (chain 31337, fresh deploy + Kumbaya fixtures, Vite frontend at http://127.0.0.1:5173).
Summary
The Simple page Buy CHARM panel built calldata with charmWad = 5503000000000000000 (5.503e18) when the slider showed 5.503 CHARM. Across the screenshots from approve → submit, the slider visibly auto-corrected from 5.503 to 5.501 to 5.5 as the upper-bound math drifted with each new block. The submitted tx broadcast against a state where charmWad was outside the new currentCharmBoundsWad envelope and reverted with bare 'Execution reverted for an unknown reason'.
Repro
State during repro
Root cause hypothesis
When the slider settles, the panel computes charmWad once from currentCharmBoundsWad() at that instant. By the time the user signs the buy tx and the broadcast lands ~1-3 blocks later, the upper bound has tightened (charm bounds in TimeCurve.sol grow with block.timestamp), so:
The reverted tx is a contract-level guard at TimeCurve.sol L500 / L514 / etc.: require(charmWad >= currentMinBuy && charmWad <= currentMaxBuy, ...) but with the bare-revert flavor (no string in the require message OR optimizer stripped, not yet checked). Either way the user-visible result is unhelpful.
Confirming the contract path is fine
Direct cast send from the same wallet with a rounded charmWad of 5.0e18 succeeded:
So the contract logic is healthy. The bug is purely in the frontend: it ships stale calldata after a stable-state quote.
Suggested fixes
Severity
MEDIUM. Not a security hole, but a real UX trap for any user buying near the upper bound on a chain with normal block production. On MegaETH (much faster blocks than 12s mainnet) this will be MORE common, not less. Worth a fix before mainnet.
Out of scope
Not blocking the smaller buys we just verified work. CL8Y direct buy via cast is fully functional. The buyViaKumbaya ETH/USDM path (#74) likely has similar exposure since it also computes CL8Y from charmWad × currentPricePerCharmWad, so the fix should cover both.
cc @PlasticDigits
Confirmed: same revert pattern reproduces on the #74 ETH single-tx path via TimeCurveBuyRouter.buyViaKumbaya.
Repro
State during repro
Conclusion
The 3% slippage cap mentioned in the panel ('Kumbaya route uses a fixed 3% max slippage cap on routed input') applies only to the Kumbaya swap leg, NOT to the TimeCurve charmWad bound check downstream. So #82's root cause covers both pay paths:
The fix needs to apply the re-quote / clamp logic in the shared CHARM amount path (whatever the underlying useTimeCurveSaleSession buyState builder is), not just the CL8Y branch.
Validation: when we use a clean integer charmWad
cast send buy(uint256) 5e18 from same wallet succeeded earlier (status=1, charmWeight=5e18). So the contract is healthy. The bug is purely the frontend's stale-calldata-on-submit issue.
Severity reaffirmed
MEDIUM, exactly as in original. ETH path repro means this isn't just a CL8Y-edge issue — it bites the whole buy pipeline.
mentioned in issue #74
mentioned in issue #76
mentioned in commit
5366f74e69mentioned in commit
04d5054b5eImplemented GitLab #82 on
main(merge5366f74).What changed
readFreshTimeCurveBuySizing(frontend/src/lib/timeCurveBuySubmitSizing.ts) runs immediately before building calldata inuseTimeCurveSaleSession(Simple) anduseTimeCurveArenaModel(Arena), covering CL8Ybuy, two-step Kumbaya +buy, andbuyViaKumbaya.maxCharmWad(CHARM_SUBMIT_UPPER_SLACK_BPS = 50); CHARM from CL8Y remains integer floor infinalizeCharmSpendForBuy(documented).friendlyRevertFromUnknown(err, { buySubmit: true })maps generic execution failures to band-shift guidance.Docs / skills
docs/frontend/timecurve-views.md#buy-charm-submit-fresh-bounds-issue-82docs/testing/invariants-and-business-logic.md#timecurve-buy-charm-submit-fresh-bounds-issue-82docs/integrations/kumbaya.md(single-tx section + #82 note)skills/verify-yo-timecurve-buy-charm-submit/SKILL.md/cc @brouie — please verify on a live-ticking stack (Anvil or faster blocks).
Verification checklist
buyViaKumbaya: same near-max case with Kumbaya env + non-zerotimeCurveBuyRouter→ success or clear error.cd frontend && npx vitest run src/lib/timeCurveBuySubmitSizing.test.ts src/lib/revertMessage.test.tsIssue left open for your sign-off.
Verified on
5366f74with both unit tests and live signed buy at MAX slider.Unit tests
cd frontend && npx vitest run src/lib/timeCurveBuySubmitSizing.test.ts src/lib/revertMessage.test.ts:
Live buy at MAX slider — the danger case from the original repro
Setup: live local-anvil stack at
1bdbda8+ DeployKumbayaAnvilFixtures +5366f74frontend, MetaMask wallet 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 connected.Steps:
Result:
That's the exact case that previously reverted with bare 'Execution reverted for an unknown reason' in the original repro on this same stack ~30 min ago. The submit-time re-quote + 99.5% upper clamp absorbed all band drift between slider settle and tx broadcast.
Friendly revert UX (item 3) — not exercised because no revert occurred
Did not encounter a wallet-only generic revert in this session because the clamp prevented the bound-check from firing. The friendlyRevertFromUnknown helper is covered by the 4 unit tests in revertMessage.test.ts.
Closing #82.
One small follow-up note from continued live testing: when the chain has been time-warped via evm_increaseTime in QA testing, the buyViaKumbaya flow can hit AnvilKumbayaFixture.sol Expired() (custom error 0x203d82d8) because the frontend's swapDeadline is computed from wall-clock time, not from chain time. On a normal-clock chain (mainnet, MegaETH, normal Anvil tick) this won't matter. On QA stacks with evm_increaseTime in the mix it does. Filing as a context note here rather than a new issue since this is a QA-only artifact, not a user-facing bug. The friendly revert UX panel surfaces 'Execution reverted with reason: custom error 0x203d82d8' so the dev-friendly path through #82's revertMessage helper is working as designed for this edge.
mentioned in issue #83
mentioned in issue #84
mentioned in commit
e3409cd84cFollow-up on reopened edge case (lower band)
Merged:
e3409cdonmain.What changed
The original #82 fix added 99.5% slack under live max
maxCharmWad; live testing showed an occasional lower-band failure (~1.002 CHARM on Path A): min can rise between submit-time re-quote and inclusion, socharmWadcould still sit at the old floor and revert.This update adds symmetric headroom above live min: effective floor = 100.5% of freshly read
minCharmWad(CHARM_SUBMIT_LOWER_HEADROOM_BPS = 50, same bps scale as the upper slack).reconcileFreshBuySizingFromReadspasseseffectiveMinCharmWadForSubmit…effectiveMaxCharmWadForSubmitintofinalizeCharmSpendForBuy. If both margins cannot be satisfied (very tight band), the panel returns the existing-style tight band message.Bare-revert UX:
friendlyRevertFromUnknown(..., { buySubmit: true })copy now suggests nudging the CHARM amount (not only “lower”), matching rare residual edge cases where retry after one block or a small slider move fixes the tx.Docs / agent cross-links:
docs/frontend/timecurve-views.md,docs/testing/invariants-and-business-logic.md(#82 row),docs/integrations/kumbaya.md,skills/verify-yo-timecurve-buy-charm-submit/SKILL.md,skills/README.md, contributor guardrail item 23 (.cursor/skills/yieldomega-guardrails/SKILL.md).Automated verification (ran locally)
cd frontend && npx vitest run src/lib/timeCurveBuySubmitSizing.test.ts src/lib/revertMessage.test.ts— 9/9 pass.Checklist for @brouie (issue stays open for your sign-off)
buyViaKumbayawhen the router is wired.main.Thanks — ping when you’ve had a chance to verify.
mentioned in issue #38
Live frontend verification on
7b0f37d/e3409cd, items 1 and 2 PASS.Setup
Live local-anvil stack with #84's kumbaya_local_anvil_env helper applied (Path B from yesterday's #84 close: SKIP_ANVIL_RICH_STATE=1 + YIELDOMEGA_DEPLOY_KUMBAYA=1). Sale LIVE, chain ts caught up to wall on first tx of the session. Two test wallets exercised today: deployer 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 (acct #0 with genesis CL8Y) and personal test wallet 0xc46b15f4B56489a16F561c22D5F0BA8bdCa80650 (funded via cast: 1 ETH for gas + 1000 CL8Y mint).
Item 2 — Upper / mid-band buy on deployer wallet
Live band at submit time: 1.23 - 12.3 CL8Y. Slider at ~5.49 CL8Y (5.108 CHARM, near mid-band). Clicked BUY CHARM.
Result: PASS clean.
The 99.5% upper clamp from CHARM_SUBMIT_UPPER_SLACK_BPS=50 carried the original #82 fix forward as expected.
Item 1 — Lower edge near-MIN buy on personal test wallet
Same stack, same band 1.23 - 12.3 CL8Y. Slider dragged all the way LEFT to lower bound. Submit-time slider value was 1.235871778395428 CL8Y → 1.149 CHARM (essentially riding the live minimum).
Result: PASS clean.
This is the case that previously could revert in the original #82 lower-edge repro (band shifts up between re-quote and tx inclusion, leaving charmWad below the new floor). The 100.5% lower headroom from CHARM_SUBMIT_LOWER_HEADROOM_BPS=50 absorbed the drift cleanly. Effective floor at submit was ~1.225 CL8Y (100.5% of the read currentMin), so even though the slider sat right at the displayed live min, the calldata floor margin held.
Item 4 — Friendly revert copy
Earlier in the session, before I noticed MetaMask had stale nonce state from a different wallet account, a couple of attempts reverted at the simulation layer and surfaced the revised friendlyRevertFromUnknown copy:
Plus an earlier-band variant where the panel suggested nudging the amount up. Both copies match the spec ("nudging the CHARM amount, not only lowering" + "band movement / nudge + retry"). Item 4 PASS.
Item 3 — Kumbaya buyViaKumbaya near-min + near-max
In progress, waiting on per-wallet buy cooldown to clear. Will append evidence here once exercised. Path: switch pay-mode chip to ETH on the same Simple Buy CHARM panel, slider near min then near max, single-signature MetaMask prompt to TimeCurveBuyRouter (0x998abeb...3613E), wallet pays in ETH with the 3% Kumbaya slippage cushion baked into amountIn.
Side observation — Anvil + MetaMask gotcha (not a #82 issue)
When MetaMask's "Activity tab data" is stale from a previous session against the same chain (e.g. 31337 anvil from the day before), the Confirm click can silently no-op without broadcasting a tx. Anvil reports nonce 0 for the wallet, MetaMask's pending-nonce tracker has a different value carried over, the broadcast gets dropped at the wallet layer with no UI feedback. Fix: MetaMask Settings → Advanced → "Clear activity tab data" for the network, OR remove + re-add the network entry. After the reset, all subsequent buys broadcast cleanly.
Worth flagging because users on prod (MegaETH) would only hit this if MM's activity for that chain ID is stale across a chain reset, which shouldn't happen on a live chain. Just a QA-environment artifact for future sessions on rebooted Anvil instances.
Vitest
Re-ran today on
7b0f37d:cd frontend && npx vitest run src/lib/timeCurveBuySubmitSizing.test.ts src/lib/revertMessage.test.ts → 9/9 pass (5 sizing + 4 revert message).
Will append item 3 evidence after cooldown + Kumbaya path runs.
cc @PlasticDigits
mentioned in issue #88
Item 3 (Kumbaya buyViaKumbaya) and remaining verification — PASS. Closing.
Item 3 — Kumbaya path via TimeCurveBuyRouter
Exercised both ETH and USDM pay modes through 0x998abeb3E57409262aE5b751f60747921B33613E (TimeCurveBuyRouter) on the same SKIP+KUMBAYA stack as items 1/2.
ETH path:
USDM path:
The Kumbaya 3% max slippage cushion + the #82-followup CHARM_SUBMIT_LOWER/UPPER_HEADROOM_BPS=50 clamps work in concert: lower-edge attempts that drift below the floor surface the friendly nudge copy at the helper layer (timeCurveBuySubmitSizing), and mid-band buys execute clean.
Item 4 — Friendly revert copy
Two distinct copies surfaced in the session:
Both copies match revertMessage.ts expectations. Both surface band-shift directional language with a nudge cue, not "try a smaller amount." Both have a dismiss button.
Confirmation summary across all 4 items
Plus vitest 9/9 PASS on the helpers.
Closure recommendation
Closing #82. The lower-band CHARM headroom fix on
e3409cdships behavior matching the spec. Live verification confirms the helper clamps absorb band drift between slider read and tx inclusion, and the friendly revert UX surfaces correct directional language when the band moves past the clamp window.cc @PlasticDigits
mentioned in issue #58
mentioned in issue #39
mentioned in issue #100
mentioned in issue #216
mentioned in issue #224
mentioned in issue #227
mentioned in issue #264
mentioned in issue #303