L-01: TimeCurveBuyRouter — fail fast when sale scheduled but not yet live (audit_smartcontract_1777813071) #118
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#118
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?
Source
Internal smart contract audit finding L-01 in
audits/audit_smartcontract_1777813071.md(2026-05-03).Summary
TimeCurveBuyRouter.buyViaKumbayavalidates that the TimeCurve sale is “scheduled” (saleStart() != 0), not ended, and beforedeadline(), but it does not enforce that the sale is actually live (block.timestamp >= saleStart()). WithstartSaleAt(epoch)(scheduled sale start — GitLab #114),epochcan be in the future. The router may still run path validation, WETH wrap / stable pull,kumbayaRouter.exactOutput, and refunds beforeTimeCurve.buyForreverts with"TimeCurve: sale not live".Impact
buyForreverts and the whole transaction rolls back (including the swap and refunds).buyViaKumbayabeforesaleStart.TimeCurve.buy/buy, which already requireblock.timestamp >= saleStartearly.Severity aligns with audit Low: operational / UX / gas, not direct theft.
Current behavior (evidence)
In
contracts/src/TimeCurveBuyRouter.sol,buyViaKumbayagates sale phase as:There is no
block.timestamp >= tc.saleStart()check.TimeCurveenforces live window on buys, e.g.require(block.timestamp >= saleStart, "TimeCurve: sale not live");in_buy.Reference direct-buy regression:
test_startSaleAt_future_epoch_buys_revert_until_liveincontracts/test/TimeCurve.t.sol(GitLab #114).Recommended fix
Onchain (primary): Extend the router’s sale-phase guard so pre-open scheduled sales revert before any swap:
saleStart() > 0, addblock.timestamp < tc.saleStart()to the revert condition (same predicate family asTimeCurve), reusingTimeCurveBuyRouter__BadSalePhaseso selectors and UX remain stable — or introduce a dedicated error (e.g.TimeCurveBuyRouter__SaleNotLive) only if product wants distinct decoding.Suggested combined guard (conceptual):
BadSalePhasewhen:saleStart == 0,ended,block.timestamp < saleStart, orblock.timestamp >= deadline.NatSpec: Update the
buyViaKumbaya/ contract dev notes to state the router fails closed for not-yet-live scheduled sales at entry (not only atTimeCurve).Documentation: Briefly note in Kumbaya / buy-router docs that
buyViaKumbayaaligns withTimeCurvelive-window rules for fail-fast gas behavior.Regression tests (recommended)
Unit / integration (Forge) — add a test in
contracts/test/TimeCurveBuyRouter.t.sol(or a focused test file) that:TimeCurve.startSaleAt(block.timestamp + delta)withdelta > 0(sale scheduled, not live).vm.expectRevert(TimeCurveBuyRouter.TimeCurveBuyRouter__BadSalePhase.selector)(or the chosen error) onbuyViaKumbayawithout expecting"TimeCurve: sale not live"— proving the router bails beforebuyFor.vm.warp(saleStart)and assert a successfulbuyViaKumbayacredits CHARM (mirror success path oftest_buyViaKumbaya_stable_creditsAlice/ ETH variant).Optional fork / Anvil script path: If
contracts/test/VerifyTimeCurveBuyRouterAnvil.t.solis extended, ensureSKIP_ANVIL_RICH_STATE/ timing assumptions still leaveblock.timestamp >= saleStartwhere success is intended; add a negative case only if the harness can warp cheaply without breaking other assumptions.CI:
cd contracts && FOUNDRY_PROFILE=ci forge test(perdocs/testing/strategy.md).Files likely to change
contracts/src/TimeCurveBuyRouter.solblock.timestamp < saleStart()(or equivalent) to sale-phase check; optional NatSpec.contracts/test/TimeCurveBuyRouter.t.solsaleStart→ early revert; warp → success.docs/integrations/kumbaya.mddocs/testing/invariants-and-business-logic.mdbuyFor/ companion router or #65/#66 linking this fix + test name / invariant id.audits/audit_smartcontract_1777813071.mdBadSalePhaseonly, confirm messaging still makes sense.Out of scope unless regression appears: indexer (
BuyViaKumbayaschema unchanged), bots.Acceptance criteria
buyViaKumbayareverts with the documented router error whensaleStart > 0,!ended,block.timestamp < saleStart, andblock.timestamp < deadline(i.e. scheduled but not yet live). NoexactOutputcall is required to observe the revert (gas stays bounded vs current “swap then revert” path).buyViaKumbayastill succeeds forblock.timestamp >= saleStartunder existing test conditions (stable + ETH paths unchanged).saleStart+ warp-to-success.docs/testing/invariants-and-business-logic.md(or equivalent test map) references the new coverage.Verification checklist — implementer
TimeCurve._buy/buyForsale live checks for parity.TimeCurveBuyRouter.buyViaKumbayaimmediately after loadingTimeCurve tc(beforecurrentCharmBoundsWad/ pricing / path / swap).forge test --match-contract TimeCurveBuyRouterthen fullforge testincontracts/.forge coverageor CI profile if required by team for router edits.Verification checklist — reviewer
kumbayaRouterwhenblock.timestamp < saleStart(static read of ordering in diff).deadlineorendedsemantics.docs/integrations/kumbaya.mdfor consistency withTimeCurve“sale not live” language.Verification checklist — release / ops
TimeCurveBuyRouterbytecode; if router is immutable per deployment, document behavior change for integrators (“pre-openbuyViaKumbayanow reverts early withBadSalePhase”).buyViaKumbayacalls drops.mentioned in commit
e9bbcc7181Merged to main (
66098c9— mergefix/gl118-buy-router-fail-fast-sale-live). Leaving issue open for verification./cc @brouie
Summary
TimeCurveBuyRouter.buyViaKumbayanow treatsblock.timestamp < saleStart()(scheduled sale not yet live perstartSaleAt) likeTimeCurvebuy: revertTimeCurveBuyRouter__BadSalePhasebeforeexactOutput/ refunds /buyFor— aligns with audit L-01 / #118.TimeCurveBuyRouterScheduledNotLive118Test— pre-liveBadSalePhase(stable + ETH);vm.warp(saleStart)then success. RefactoredTimeCurveBuyRouterFixtureso #118 tests do not inherit unrelated happy-path tests.INV-BUYROUTER-118,docs/integrations/kumbaya.md,skills/README.md, audit § L-01 addressed note; guardrail §13 merged with #117 rescue/refund wording.Verification checklist for @brouie
contracts/src/TimeCurveBuyRouter.sol: sale-phase check loadssaleStartonce and includesblock.timestamp < saleStart_ahead ofcurrentCharmBoundsWad/ swap (ordering: nokumbayaRouteruntil live).forge test --match-contract 'TimeCurveBuyRouter(Test|ScheduledNotLive118Test)|VerifyTimeCurveBuyRouterAnvil'passes (13 tests including #117 router tests).BadSalePhaseremains the revert for “not yet live”; selectors unchanged for clients decodingBadSalePhaseonly.docs/integrations/kumbaya.md“scheduled sale / fail-fast” +INV-BUYROUTER-118table matchesTimeCurvesale not live semantics.Note: local
contracts/outowned by root causedPermission deniedon this runner when compiling underyieldomega/contracts; CI / normal checkout should use a writableout/— verification above ran via full compile in a clean worktree.mentioned in commit
9905b25c9cverified on commit
e9bbcc7. fix landed clean.source ordering (TimeCurveBuyRouter.sol):
saleStart_read + 4-condition guard (saleStart == 0 || ended || block.timestamp < saleStart) → revertsTimeCurveBuyRouter__BadSalePhasekumbayaRouter.exactOutput(...)swap callguard fires before pricing or swap. spec confirmed: revert path does not touch kumbayaRouter when block.timestamp < saleStart.
forge tests all pass:
test_buyViaKumbaya_badSalePhase_whenScheduledNotLive_eth— pre-open ETH path revertstest_buyViaKumbaya_badSalePhase_whenScheduledNotLive_stable— pre-open stable path revertstest_buyViaKumbaya_eth_succeeds_afterWarpToSaleStart— warp-then-success ETH pathtest_buyViaKumbaya_stable_succeeds_afterWarpToSaleStart— warp-then-success stable pathimplementer + reviewer + release-ops checklist all pass at the source/test level.
mentioned in issue #207