TimeCurve: replace startSale() with owner-only startSaleAt(uint256 epoch) #114

Closed
opened 2026-05-03 07:23:40 +00:00 by PlasticDigits · 15 comments
PlasticDigits commented 2026-05-03 07:23:40 +00:00 (Migrated from gitlab.com)

Summary

Replace TimeCurve.startSale() with startSaleAt(uint256 epoch):

  • onlyOwner (today startSale() is external with no onlyOwner — lock this down as part of the change).
  • Revert when the sale has already started (keep the saleStart == 0 / equivalent invariant; document the exact error string).
  • Set saleStart = epoch (Unix seconds — align naming with TimeCurve.sol: saleStart is seconds).
  • deadline: derive consistently from the chosen start instant (today: deadline = block.timestamp + initialTimerSec). With a scheduled start, the natural analogue is deadline = epoch + initialTimerSec so the initial window length does not depend on when the owner’s tx is mined. Call out in implementation if a different rule is chosen.
  • Emit SaleStarted with the same event shape if possible; the first argument should match saleStart (today it is block.timestamp, which equals saleStart at call time).

Product / rationale

Operators need to announce a fixed chain-time start independent of mempool / block production at “go.” Frontend and indexer already treat saleStart as authoritative for phase UX; scheduling it explicitly avoids drifting “start now” semantics.

Open acceptance questions (implementer)

  • epoch validation (e.g. must be future vs block.timestamp, tolerance for slight past timestamps for same-block ordering, alignment with indexer “chain now”).
  • epoch == saleStart and interaction with initialTimerSec / extensions after first buy (unchanged; only anchor changes).

Repo touch-points (everything currently tied to startSale or sale bootstrap)

Onchain (contracts/)

Area Notes
contracts/src/TimeCurve.sol Implementation: new startSaleAt, saleStart write, deadline, SaleStarted payload, onlyOwner, insufficient-launched-token checks unchanged.
contracts/src/TimeCurveBuyRouter.sol No startSale call — only saleStart() reads; unchanged unless semantics of saleStart / phase change.

Interfaces / scripts (contracts/script/)

File Notes
contracts/script/DeployDev.s.sol Calls tc.startSale() after funding — switch to startSaleAt(...) with an explicit timestamp (e.g. current block time for local/dev parity, or env-driven for staging).
contracts/script/AnvilSameBlockDrill.s.sol tc.startSale() → startSaleAt.
contracts/script/SimulateAnvilRichState.s.sol Local ITimeCurve does not declare startSale today; add startSaleAt only if this script ever needs to start a sale (likely not).

Tests (contracts/test/)

All tc.startSale() / t.startSale() call sites need updating; non-exhaustive list from current tree:

New / extended Forge tests should cover onlyOwner, double-start revert, epoch vs deadline math, and (if enforced) epoch not too far in the past.

Indexer (indexer/)

Frontend (frontend/)

  • Read paths use saleStart() only (frontend/src/lib/abis.ts timeCurveReadAbi). No startSale in timeCurveWriteAbi today; add startSaleAt(uint256) if operator UI / admin flows need typed calldata later.
  • No code search hits for startSale( string in TS — behavior change is indirect via saleStart values and countdown UX (docs/frontend/timecurve-views.md, issue #48). Update docs if scheduling introduces saleStartPending windows that are visibly long.

Bots (bots/)

Docs / runbooks / skills

Shell / tooling


Definition of done

  • TimeCurve: startSaleAt(uint256 epoch) with onlyOwner, single transition, saleStart, deadline, SaleStarted, tests.
  • Deploy scripts and Forge tests updated; CI green.
  • Indexer event decode + integration tests verified if SaleStarted emission changes.
  • Docs / launchplan / QA checklist / skills aligned with startSaleAt and operator flow.
  • bots/timecurve ABI updated if the bot ever needs the new function.
## Summary Replace `TimeCurve.startSale()` with **`startSaleAt(uint256 epoch)`**: - **`onlyOwner`** (today `startSale()` is **`external`** with **no** `onlyOwner` — lock this down as part of the change). - Revert when the sale **has already started** (keep the **`saleStart == 0`** / equivalent invariant; document the exact error string). - Set **`saleStart = epoch`** (Unix seconds — align naming with [`TimeCurve.sol`](contracts/src/TimeCurve.sol): `saleStart` is seconds). - **`deadline`**: derive consistently from the chosen start instant (today: `deadline = block.timestamp + initialTimerSec`). With a scheduled start, the natural analogue is **`deadline = epoch + initialTimerSec`** so the initial window length does not depend on when the owner’s tx is mined. Call out in implementation if a different rule is chosen. - Emit **`SaleStarted`** with the **same** event shape if possible; the first argument should match **`saleStart`** (today it is `block.timestamp`, which equals `saleStart` at call time). ## Product / rationale Operators need to **announce a fixed chain-time start** independent of mempool / block production at “go.” Frontend and indexer already treat **`saleStart`** as authoritative for phase UX; scheduling it explicitly avoids drifting “start now” semantics. ### Open acceptance questions (implementer) - **`epoch`** validation (e.g. must be **future vs `block.timestamp`**, tolerance for slight past timestamps for same-block ordering, alignment with indexer “chain now”). - **`epoch == saleStart`** and interaction with **`initialTimerSec`** / extensions after first buy (unchanged; only anchor changes). --- ## Repo touch-points (everything currently tied to **`startSale`** or sale bootstrap) ### Onchain (`contracts/`) | Area | Notes | |------|--------| | **[`contracts/src/TimeCurve.sol`](contracts/src/TimeCurve.sol)** | Implementation: new `startSaleAt`, `saleStart` write, **`deadline`**, **`SaleStarted`** payload, **`onlyOwner`**, insufficient-launched-token checks unchanged. | | **[`contracts/src/TimeCurveBuyRouter.sol`](contracts/src/TimeCurveBuyRouter.sol)** | No `startSale` call — only **`saleStart()`** reads; unchanged unless semantics of `saleStart` / phase change. | ### Interfaces / scripts (`contracts/script/`) | File | Notes | |------|--------| | **[`contracts/script/DeployDev.s.sol`](contracts/script/DeployDev.s.sol)** | Calls **`tc.startSale()`** after funding — switch to **`startSaleAt(...)`** with an explicit timestamp (e.g. current block time for local/dev parity, or env-driven for staging). | | **[`contracts/script/AnvilSameBlockDrill.s.sol`](contracts/script/AnvilSameBlockDrill.s.sol)** | **`tc.startSale()`** → **`startSaleAt`**. | | **[`contracts/script/SimulateAnvilRichState.s.sol`](contracts/script/SimulateAnvilRichState.s.sol)** | Local **`ITimeCurve`** does not declare `startSale` today; add **`startSaleAt`** only if this script ever needs to start a sale (likely not). | ### Tests (`contracts/test/`) All **`tc.startSale()`** / **`t.startSale()`** call sites need updating; **non-exhaustive** list from current tree: - [`contracts/test/TimeCurve.t.sol`](contracts/test/TimeCurve.t.sol) — includes **`test_startSale`**, **`test_startSale_reverts_twice`**, **`test_startSale_insufficient_launched_tokens_reverts`** and many fixtures. - [`contracts/test/TimeCurveBuyRouter.t.sol`](contracts/test/TimeCurveBuyRouter.t.sol) - [`contracts/test/TimeCurveWarBowCl8yBurns.t.sol`](contracts/test/TimeCurveWarBowCl8yBurns.t.sol) - [`contracts/test/TimeCurveReferral.t.sol`](contracts/test/TimeCurveReferral.t.sol) - [`contracts/test/DevStackIntegration.t.sol`](contracts/test/DevStackIntegration.t.sol) — asserts **`saleStart > 0`** / deadline ordering. - [`contracts/test/TimeCurveInvariant.t.sol`](contracts/test/TimeCurveInvariant.t.sol) - [`contracts/test/NonStandardERC20.t.sol`](contracts/test/NonStandardERC20.t.sol) - [`contracts/test/VerifyTimeCurveBuyRouterAnvil.t.sol`](contracts/test/VerifyTimeCurveBuyRouterAnvil.t.sol) — **reads** **`saleStart()`** only. New / extended Forge tests should cover **`onlyOwner`**, double-start revert, **`epoch` vs `deadline`** math, and (if enforced) **`epoch` not too far in the past**. ### Indexer (`indexer/`) - **`SaleStarted`** ABI in [`indexer/src/decoder.rs`](indexer/src/decoder.rs): signature unchanged if the event is kept; verify **decoded `startTimestamp`** still matches stored **`saleStart`** after emission change. - [`indexer/src/persist.rs`](indexer/src/persist.rs), [`indexer/tests/integration_stage2.rs`](indexer/tests/integration_stage2.rs) — regression if event args or persisted fields change. ### Frontend (`frontend/`) - Read paths use **`saleStart()`** only ([`frontend/src/lib/abis.ts`](frontend/src/lib/abis.ts) **`timeCurveReadAbi`**). No **`startSale`** in **`timeCurveWriteAbi`** today; add **`startSaleAt(uint256)`** if operator UI / admin flows need typed calldata later. - No code search hits for **`startSale(`** string in TS — behavior change is indirect via **`saleStart`** values and countdown UX ([`docs/frontend/timecurve-views.md`](docs/frontend/timecurve-views.md), issue **#48**). Update docs if scheduling introduces **saleStartPending** windows that are visibly long. ### Bots (`bots/`) - [`bots/timecurve/src/timecurve_bot/abis/timecurve.json`](bots/timecurve/src/timecurve_bot/abis/timecurve.json) includes **`saleStart`** read; regenerate or append **`startSaleAt`** if bots ever invoke it. ### Docs / runbooks / skills - [`docs/testing/invariants-and-business-logic.md`](docs/testing/invariants-and-business-logic.md) — **Sale start** row and **`startSale`** test map. - [`launchplan-timecurve.md`](launchplan-timecurve.md) — bootstrap step referencing **`startSale`**. - [`docs/operations/stage2-run-log.md`](docs/operations/stage2-run-log.md). - [`docs/qa/YO-TimeCurve-QA-Checklist.md`](docs/qa/YO-TimeCurve-QA-Checklist.md). - [`skills/play-timecurve-doubloon/SKILL.md`](skills/play-timecurve-doubloon/SKILL.md) — **`SaleStarted`** mention. ### Shell / tooling - Scripts under [`scripts/`](scripts/) delegate deploy to **`DeployDev`** ([`scripts/lib/anvil_deploy_dev.sh`](scripts/lib/anvil_deploy_dev.sh), [`scripts/start-local-anvil-stack.sh`](scripts/start-local-anvil-stack.sh)); no literal **`startSale`** in shell — behavior follows **`DeployDev`** changes. --- ## Definition of done - [ ] **`TimeCurve`**: `startSaleAt(uint256 epoch)` with **`onlyOwner`**, single transition, **`saleStart`**, **`deadline`**, **`SaleStarted`**, tests. - [ ] **Deploy scripts** and **Forge tests** updated; CI green. - [ ] **Indexer** event decode + integration tests verified if **`SaleStarted`** emission changes. - [ ] **Docs / launchplan / QA checklist / skills** aligned with **`startSaleAt`** and operator flow. - [ ] **`bots/timecurve`** ABI updated if the bot ever needs the new function. ## Related guardrails - TimeCurve frontend sale phase prefers consistent “chain now” vs **`saleStart`** — [issue #48](https://gitlab.com/PlasticDigits/yieldomega/-/issues/48), [`docs/frontend/timecurve-views.md`](docs/frontend/timecurve-views.md).
PlasticDigits commented 2026-05-03 07:46:31 +00:00 (Migrated from gitlab.com)

mentioned in commit 5edd13263f

mentioned in commit 5edd13263f5f45d50fe08ce456bf4e7a303103e5
PlasticDigits commented 2026-05-03 07:47:41 +00:00 (Migrated from gitlab.com)

Implemented on main (merge commit 06ffd0c) — #114

Summary

  • TimeCurve: Removed public startSale(); added **** startSaleAt(uint256 epoch): saleStart = epoch, deadline = epoch + initialTimerSec, SaleStarted(epoch, deadline, totalTokensForSale) (ABI shape unchanged).
  • Guards: epoch != 0, epoch >= block.timestamp, double-start (**`already started``), insufficient launched tokens (unchanged strings).
  • Live sale: buy / buyFor / WarBow CL8Y paths / claimWarBowFlag require block.timestamp >= saleStart → revert *TimeCurve: sale not live* if scheduled in the future.
  • Views: currentMinBuyAmount, currentMaxBuyAmount, currentCharmBoundsWad, currentPricePerCharmWad use elapsed = 0 until block.timestamp >= saleStart.
  • Deploy: DeployDev / AnvilSameBlockDrill call **startSaleAt(block.timestamp)**.
  • Tooling: timeCurveWriteAbi + bot timecurve.json include startSaleAt; docs / invariants / play + contributor skills cross-linked (INV: #timecurve-startsaleat-issue-114).

CI / verification run by implementer

  • forge test (210 tests)
  • cargo test in indexer/ (20 tests incl. integration)
  • npm ci + vitest run in frontend/ (254 tests)
  • Issue left open per request.

/cc @brouie — please sanity-check behaviour on a staging or local deploy.

Verification checklist for @brouie

  1. Deploy or use **DeployDev**: after fund + startSaleAt(block.timestamp), saleStart() equals that timestamp and deadline() == saleStart() + initialTimerSec().
  2. Non-owner startSaleAt reverts (OpenZpelin OwnableUnauthorizedAccount).
  3. Schedule future epoch: reads show opening curve until live; *buy* reverts *sale not live* until warp/chain reaches saleStart; then buys succeed.
  4. epoch == 0 → **invalid epoch*; epoch < block.timestamp → **epoch in past*.
  5. Second startSaleAt after start → **already started**.
  6. Indexer still decodes SaleStarted and persists startTimestamp == saleStart (shape unchanged).
  7. Frontend phase (saleStartPending) stays consistent with indexer hero clock vs saleStart (`timecurve-views.md`` scheduled starts anchor).
### Implemented on `main` (merge commit `06ffd0c`) — [#114](https://gitlab.com/PlasticDigits/yieldomega/-/issues/114) **Summary** - **`TimeCurve`**: Removed public `startSale()`; added **** `startSaleAt(uint256 epoch)`: `saleStart = epoch`, `deadline = epoch + initialTimerSec`, `SaleStarted(epoch, deadline, totalTokensForSale)` (ABI shape unchanged). - **Guards**: `epoch != 0`, `epoch >= block.timestamp`, double-start (**`already started``), insufficient launched tokens (**unchanged strings**). - **Live sale**: `buy` / `buyFor` / WarBow CL8Y paths / `claimWarBowFlag` require **`block.timestamp >= saleStart`** → revert **`TimeCurve: sale not live`\** if scheduled in the future. - **Views**: `currentMinBuyAmount`, `currentMaxBuyAmount`, `currentCharmBoundsWad`, `currentPricePerCharmWad` use **elapsed = 0** until `block.timestamp >= saleStart`. - **Deploy**: `DeployDev` / `AnvilSameBlockDrill` call **`startSaleAt(block.timestamp)`\**. - **Tooling**: `timeCurveWriteAbi` + bot `timecurve.json` include `startSaleAt`; docs / invariants / play + contributor skills cross-linked (**`INV:` [\#timecurve-startsaleat-issue-114](https://gitlab.com/PlasticDigits/yieldomega/-/blob/main/docs/testing/invariants-and-business-logic.md)**). **CI / verification run by implementer** - `forge test` (**210** tests) - `cargo test` in `indexer/` (**20** tests incl. integration) - `npm ci` + `vitest run` in `frontend/` (**254** tests) - Issue **left open** per request. /cc @brouie — please sanity-check behaviour on a staging or local deploy. **Verification checklist for @brouie** 1. [ ] Deploy or use **`DeployDev`\**: after fund + `startSaleAt(block.timestamp)`, `saleStart()` equals that timestamp and **`deadline() == saleStart() + initialTimerSec()`**. 2. [ ] **Non-owner** `startSaleAt` reverts (**OpenZpelin** `OwnableUnauthorizedAccount`). 3. [ ] Schedule **future** epoch: reads show opening curve until live; **`buy`\** reverts **`sale not live`\** until warp/chain reaches `saleStart`; then buys succeed. 4. [ ] `epoch == 0` → **`invalid epoch`\**; `epoch < block.timestamp` → **`epoch in past`\**. 5. [ ] Second `startSaleAt` after start → **`already started`\**. 6. [ ] Indexer still decodes `SaleStarted` and persists `startTimestamp == saleStart` (shape unchanged). 7. [ ] Frontend phase (`saleStartPending`) stays consistent with **indexer hero clock** vs `saleStart` ([`timecurve-views.md`` scheduled starts anchor](https://gitlab.com/PlasticDigits/yieldomega/-/blob/main/docs/frontend/timecurve-views.md)).
PlasticDigits commented 2026-05-03 07:48:14 +00:00 (Migrated from gitlab.com)

Follow-up — the previous automation note mangled some markdown (shell interpreted backticks).

Follow-up — the previous automation note mangled some markdown (shell interpreted backticks).
PlasticDigits commented 2026-05-03 07:48:32 +00:00 (Migrated from gitlab.com)

Merge SHA (correct): 06ffd0c on main (typo fix: not 06ffc0).

**Merge SHA (correct):** **06ffd0c** on **main** (typo fix: not 06ffc0).
PlasticDigits commented 2026-05-03 10:47:52 +00:00 (Migrated from gitlab.com)

mentioned in issue #115

mentioned in issue #115
PlasticDigits commented 2026-05-03 10:47:54 +00:00 (Migrated from gitlab.com)

marked this issue as related to #115

marked this issue as related to #115
Brouie commented 2026-05-04 00:48:45 +00:00 (Migrated from gitlab.com)

verified on 06ffd0c end-to-end with live deploy walk. forge: 210/210 pass. cargo test in indexer/: 19 unit + 1 integration pass, roundtrip_sale_started ok (item 6, ABI shape unchanged). vitest in frontend/: 258/258 pass. item 1: fresh deploy → saleStart=1777852330, deadline=1777938730, initialTimerSec=86400 → 1777852330+86400 == 1777938730 exact match per source line 330. item 2: anvil acct #1 calling startSaleAt → OwnableUnauthorizedAccount(0x70997970...79C8) per OZ Ownable. item 3 first half: redeployed with 600s future epoch, chain time 594s pre-start, owner buy attempt → Error("TimeCurve: sale not live") with hex 54696d6543757276653a2073616c65206e6f74206c697665 matching ASCII. item 3 second half: anvil_increaseTime 600s past saleStart → buy returns status 1 (success) gasUsed 644668. item 5: owner double startSaleAt → Error("TimeCurve: already started") (saleStart==0 guard at line 322 fires before items 4 guards which is why item 4 cant reach live without a 3rd fresh contract — covered by forge units test_startSale_reverts_twice etc + source review of lines 323-324). item 7: spun up vite against the pre-start deploy, /timecurve showed PRE-LAUNCH badge + "TimeCurve Opens In" copy + 9:46 countdown + Buy CHARM card disabled ("Coming soon"). After anvil_increaseTime 600s + browser refresh, transitioned to SALE LIVE badge + "Time Left" copy + 23:51 deadline countdown + Buy CHARM enabled with live 1.000530 CL8Y/CHARM ticker. phase swap clean end-to-end against indexer-anchored chain clock. note for future runs: had to comment out warp_to_timecurve_sale_start "${TC}" at line 234 of scripts/start-local-anvil-stack.sh to get a pre-start window for item 3a, since the script always lands chain time at saleStart by deploy completion and anvil_setNextBlockTimestamp rejects backward time moves. consider an opt-out env like YIELDOMEGA_DEV_SKIP_SALE_START_WARP=1 if you want this verification path repeatable without local script edits. all 7 items pass.

verified on `06ffd0c` end-to-end with live deploy walk. forge: 210/210 pass. cargo test in indexer/: 19 unit + 1 integration pass, `roundtrip_sale_started` ok (item 6, ABI shape unchanged). vitest in frontend/: 258/258 pass. **item 1**: fresh deploy → `saleStart=1777852330, deadline=1777938730, initialTimerSec=86400` → `1777852330+86400 == 1777938730` exact match per source line 330. **item 2**: anvil acct #1 calling `startSaleAt` → `OwnableUnauthorizedAccount(0x70997970...79C8)` per OZ Ownable. **item 3 first half**: redeployed with 600s future epoch, chain time 594s pre-start, owner buy attempt → `Error("TimeCurve: sale not live")` with hex `54696d6543757276653a2073616c65206e6f74206c697665` matching ASCII. **item 3 second half**: anvil_increaseTime 600s past saleStart → buy returns `status 1 (success)` gasUsed 644668. **item 5**: owner double `startSaleAt` → `Error("TimeCurve: already started")` (saleStart==0 guard at line 322 fires before items 4 guards which is why item 4 cant reach live without a 3rd fresh contract — covered by forge units `test_startSale_reverts_twice` etc + source review of lines 323-324). **item 7**: spun up vite against the pre-start deploy, `/timecurve` showed PRE-LAUNCH badge + "TimeCurve Opens In" copy + 9:46 countdown + Buy CHARM card disabled ("Coming soon"). After `anvil_increaseTime` 600s + browser refresh, transitioned to SALE LIVE badge + "Time Left" copy + 23:51 deadline countdown + Buy CHARM enabled with live 1.000530 CL8Y/CHARM ticker. phase swap clean end-to-end against indexer-anchored chain clock. **note for future runs**: had to comment out `warp_to_timecurve_sale_start "${TC}"` at line 234 of `scripts/start-local-anvil-stack.sh` to get a pre-start window for item 3a, since the script always lands chain time at saleStart by deploy completion and `anvil_setNextBlockTimestamp` rejects backward time moves. consider an opt-out env like `YIELDOMEGA_DEV_SKIP_SALE_START_WARP=1` if you want this verification path repeatable without local script edits. all 7 items pass.
Brouie commented 2026-05-04 02:42:58 +00:00 (Migrated from gitlab.com)

mentioned in issue #96

mentioned in issue #96
Brouie (Migrated from gitlab.com) closed this issue 2026-05-04 03:10:25 +00:00
PlasticDigits commented 2026-05-04 04:17:43 +00:00 (Migrated from gitlab.com)

mentioned in issue #118

mentioned in issue #118
PlasticDigits commented 2026-05-04 04:22:29 +00:00 (Migrated from gitlab.com)

mentioned in issue #124

mentioned in issue #124
PlasticDigits commented 2026-05-04 08:01:59 +00:00 (Migrated from gitlab.com)

mentioned in commit dadc16b3cd

mentioned in commit dadc16b3cdda1858a637b4889ef6555508794e47
PlasticDigits commented 2026-05-04 08:36:25 +00:00 (Migrated from gitlab.com)

mentioned in issue #136

mentioned in issue #136
PlasticDigits commented 2026-05-04 09:22:06 +00:00 (Migrated from gitlab.com)

mentioned in commit 6a4f054fe8

mentioned in commit 6a4f054fe84b16fd07493cf316cb8112b58c2625
PlasticDigits commented 2026-05-13 16:07:17 +00:00 (Migrated from gitlab.com)

mentioned in issue #207

mentioned in issue #207
PlasticDigits commented 2026-05-21 09:32:21 +00:00 (Migrated from gitlab.com)

mentioned in issue #232

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