TimeCurve audit I-01: cap sale at 300 days; bound elapsed for TimeMath / LinearCharmPrice #124

Closed
opened 2026-05-04 04:22:29 +00:00 by PlasticDigits · 14 comments
PlasticDigits commented 2026-05-04 04:22:29 +00:00 (Migrated from gitlab.com)

Summary

Track smart contract audit finding I-01 (Long-running TimeCurve math can eventually revert for extreme elapsed/growth values) and implement an explicit 300-day wall-clock cap on TimeCurve sale “pricing time” so envelope + linear price math cannot hit PRB exp domain failures or uint256 overflows from unbounded elapsed.

Source: audits/audit_smartcontract_1777813071.md — § I-01 (TimeMath, LinearCharmPrice, TimeCurve).


Problem statement

Audit I-01 (verbatim scope)

  • TimeMath.currentMinBuy uses PRB-Math exp. Very large growthRateWad * elapsed (scaled by seconds-per-day) can overflow or exceed the exponent domain, causing view calls and buy to revert (latent DoS).
  • LinearCharmPrice.priceWad uses basePriceWad + dailyIncrementWad * elapsed / 1 day, which can overflow uint256 for extreme elapsed or dailyIncrementWad combinations.

The report notes this is not expected under current sale timing assumptions, but becomes a real liveness risk if a sale is extended or left open far beyond intended duration.

Product / protocol requirement (this issue)

Cap sales at 300 days of elapsed sale time (from onchain saleStart): after this bound, pricing inputs for the CHARM envelope and ICharmPrice must plateau (use min(elapsed, 300 days)) and interactive sale paths must not remain playable past that boundary unless explicitly redesigned.

Clarify in implementation PR whether hard stop means:

  1. Strong cap (recommended for I-01 + “300-day cap”): no buy / buyFor / WarBow CL8Y spend paths after block.timestamp >= saleStart + 300 days (and timer extension logic never pushes deadline beyond saleStart + 300 days); or
  2. Pricing-only cap: plateau min/max CHARM and price at 300 days but allow timer/game mechanics to continue (weaker alignment with “cap sales at 300 days” wording).

Default interpretation for this issue: (1) strong cap — “sales” end by 300 × 86 400 seconds after saleStart.


  1. Introduce an immutable or constant maximum sale calendar duration

    • Suggested name: MAX_SALE_ELAPSED_SEC = 300 * 86_400.
    • Prefer constant in TimeCurve if no storage migration is required; if governance needs flexibility later, use immutable set once in initialize with require(_max <= reasonableUpperBound).
  2. Single source of truth for “pricing elapsed”

    • Replace ad-hoc block.timestamp - saleStart in _buy / _charmBounds paths with a _elapsedCappedForSale() (or reuse _elapsedForCharmPricing with the cap applied consistently) so views, buy, and any charmPrice.priceWad(elapsed) callers share the same elapsed value.
  3. Enforce the 300-day wall-clock sale end

    • In _buy (and any parallel entrypoints: buyFor → _buy, WarBow paths that depend on sale liveness if applicable): require(block.timestamp < saleStart + MAX_SALE_ELAPSED_SEC, "TimeCurve: sale max duration") (exact revert string to be chosen for consistency with existing "TimeCurve: *" messages).
    • Timer extensions: clamp TimeMath.extendDeadline / extendDeadlineOrResetBelowThreshold results so deadline never exceeds saleStart + MAX_SALE_ELAPSED_SEC (otherwise buys could remain valid after the cap while pricing was plateaued — inconsistent UX and economics).
  4. Defense in depth in TimeMath (optional but valuable)

    • currentMinBuy: document maximum safe elapsed for a given growthRateWad or add an explicit require/saturation branch if you keep exp (team decision: revert with clear error vs saturate minBuy — saturation changes economics and must be documented).
  5. LinearCharmPrice

    • If dailyIncrementWad is governance-upgradable, add initializer / setter bounds so base + increment * cappedElapsed cannot wrap uint256 under the MAX_SALE_ELAPSED_SEC cap (cheap require on dailyIncrementWad vs type(uint256).max with mulDiv).
  6. Deploy-time alignment

    • Ensure timerCapSec, initialTimerSec, and extension math cannot encode a sale that logically outlives the 300-day product cap (document in PARAMETERS.md).

Files likely to change

Solidity (authoritative)

File Change
contracts/src/TimeCurve.sol Cap elapsed for _charmBounds / priceWad; require sale end by saleStart + 300d; clamp deadline updates to saleStart + 300d.
contracts/src/libraries/TimeMath.sol Optional: document domain bounds; optional min(elapsed, cap) helper if shared; optional explicit revert on excessive exponent before exp.
contracts/src/pricing/LinearCharmPrice.sol Optional upper bound on dailyIncrementWad (or safety comment + require in priceWad if uncapped governance is possible).
contracts/script/DeployDev.s.sol / contracts/script/UUPSDeployLib.sol If initialize signature gains a maxSaleDuration (immutable pattern), thread argument through deploy scripts.
contracts/PARAMETERS.md New row: max sale duration = 300 days; relationship to timerCapSec.

Tests

File Change
contracts/test/TimeCurve.t.sol Revert on buy after cap; deadline clamp; views plateau / remain callable; extension near boundary.
contracts/test/TimeMath.t.sol Regression: boundary elapsed with canonical growthRateWad; optional overflow dailyIncrement test if LinearCharmPrice is bounded.
contracts/test/LinearCharmPrice.t.sol If new dailyIncrementWad upper bound: reject path.
New (optional) TimeCurveSaleDurationCap.t.sol focused fuzz: elapsed → cap, timer edge cases.

Documentation

File Change
docs/product/primitives.md State 300-day max sale from saleStart; link to PARAMETERS.md.
docs/testing/invariants-and-business-logic.md New invariant row + GitLab link (this issue): max sale elapsed, deadline clamp, compat with #114 / #115 hero/timer copy.
docs/frontend/timecurve-views.md If UI must show “sale ended (max duration)” distinct from timer expired — short note (frontend may only read ended + timestamps).
docs/qa/YO-TimeCurve-QA-Checklist.md Checkbox: warp past saleStart + 300d → buys forbidden; charts use capped duration.
contracts/README.md One-line pointer to TimeMath / sale cap if architecture section lists risks mitigated.
skills/play-timecurve-doubloon/SKILL.md Update timer/duration bullets so play agents do not assume unbounded sale length.

Frontend / indexer (if behavior surfaces)

Area When to touch
frontend/src/lib/timeCurveMath.ts and call sites If client mirrors elapsed for previews — mirror min(elapsed, 300d) to match chain (avoid misleading bands when locally extrapolating).
frontend/src/pages/timecurve/TimeCurveLiveCharts.tsx saleDurationSec already derived from deadline - saleStart; confirm charts when deadline is clamped to saleStart + 300d.
indexer/src/chain_timer.rs Usually no logic change if only contract rules change; verify API still consistent.

Regression tests (concrete)

  1. TimeCurve — buy blocked after cap

    • Configure saleStart, warp to saleStart + 300 * 86400, expect buy reverts (and buyFor / router path if covered).
    • Just before cap, buy still succeeds under existing timer rules.
  2. TimeCurve — deadline clamp

    • Start sale; perform buys that would extend deadline past saleStart + 300d; assert deadline == saleStart + 300d (or min(..., cap)).
  3. TimeCurve — CHARM bounds / price views at boundary

    • Warp to saleStart + 299d vs saleStart + 301d: capped elapsed yields identical currentMinBuy / envelope (pricing plateau).
  4. TimeMath — no revert under canonical growth for elapsed == 300d

    • currentMinBuy(1e18, GROWTH_RATE_20PCT, 300 * 86400) returns finite minBuy (and minBuy <= type(uint256).max / WAD style sanity).
  5. (If implemented) TimeMath excessive exponent

    • elapsed far beyond cap with malicious growthRateWad in a unit test expecting clear revert (deploy-time bound test) vs satiation.
  6. LinearCharmPrice overflow regression

    • With dailyIncrementWad = type(uint256).max / 2, priceWad(300d) either reverts at setter or reverts in priceWad with documented bound.

Acceptance criteria

  • Onchain MAX_SALE_ELAPSED_SEC (or equivalent) is 300 days and documented in contracts/PARAMETERS.md and docs/product/primitives.md.
  • All buy / fee-routing sale interactions that must end with the sale respect block.timestamp < saleStart + 300 * 86400 (per chosen strong-cap interpretation).
  • deadline updates from TimeMath never exceed saleStart + 300 * 86400.
  • elapsed fed into _charmBounds and charmPrice.priceWad is capped consistently (_elapsedForCharmPricing / shared helper).
  • TimeMath.currentMinBuy cannot brick TimeCurve views at elapsed == 300d for **canonical growthRateWad (DeployDev / production default).
  • LinearCharmPrice cannot uint256 overflow on priceWad under elapsed ≤ 300d with allowed parameter bounds.
  • Forge tests added/updated and forge test passes for affected packages.
  • docs/testing/invariants-and-business-logic.md maps the new invariant; cross-links from audit I-01.

Multistep verification checklist

A. Local Forge

  1. cd contracts && forge test --match-contract TimeCurve -vv
  2. forge test --match-contract TimeMath -vv
  3. forge test --match-contract LinearCharmPrice -vv
  4. Full suite: forge test
  1. ./scripts/start-local-anvil-stack.sh (or repo-documented minimal deploy).
  2. cast call CHARM bound / price view functions at saleStart + 299d vs 301d (capped plateau + buy revert).
  3. If scripts/verify-*.sh touch TimeCurve timing, extend or add a small script that warps and asserts buy reverts post-cap.

C. Frontend / indexer smoke

  1. Start indexer + frontend against capped contract.
  2. Confirm protocol / simple / arena pages load without runaway client math (if TS mirrors elapsed).
  3. GET /v1/timecurve/chain-timer still consistent (deadline_sec ≤ sale_start_sec + 300d in seconds).

D. Documentation / governance

  1. Operator runbook / stage3 docs mention 300-day sale ceiling next to timerCapSec / initialTimerSec.
  2. Audit remediation table (if maintained) marks I-01 as addressed with link to merge request.

References


Labels / relationships (for maintainers)

Suggested labels: security, contracts, timecurve, audit, tech-debt

Related docs/issues: GitLab #114 (startSaleAt), #115 (pre-open countdown), #48 (hero timer / phase derivation).

## Summary Track **smart contract audit finding [I-01](https://gitlab.com/PlasticDigits/yieldomega/-/blob/main/audits/audit_smartcontract_1777813071.md)** (*Long-running TimeCurve math can eventually revert for extreme elapsed/growth values*) and implement an **explicit 300-day wall-clock cap** on TimeCurve sale “pricing time” so envelope + linear price math cannot hit PRB `exp` domain failures or `uint256` overflows from unbounded elapsed. **Source:** `audits/audit_smartcontract_1777813071.md` — § I-01 (TimeMath, LinearCharmPrice, TimeCurve). --- ## Problem statement ### Audit I-01 (verbatim scope) - **`TimeMath.currentMinBuy`** uses PRB-Math **`exp`**. Very large **`growthRateWad * elapsed`** (scaled by seconds-per-day) can **overflow** or **exceed the exponent domain**, causing **view calls and `buy` to revert** (latent DoS). - **`LinearCharmPrice.priceWad`** uses **`basePriceWad + dailyIncrementWad * elapsed / 1 day`**, which can **overflow `uint256`** for extreme **`elapsed`** or **`dailyIncrementWad`** combinations. The report notes this is **not expected under current sale timing assumptions**, but becomes a **real liveness risk** if a sale is **extended or left open** far beyond intended duration. ### Product / protocol requirement (this issue) **Cap sales at 300 days** of elapsed sale time (from onchain **`saleStart`**): after this bound, pricing inputs for the CHARM envelope and `ICharmPrice` must **plateau** (use **`min(elapsed, 300 days)`**) **and** interactive sale paths must **not** remain playable past that boundary unless explicitly redesigned. Clarify in implementation PR whether **hard stop** means: 1. **Strong cap (recommended for I-01 + “300-day cap”):** no `buy` / `buyFor` / WarBow CL8Y spend paths after **`block.timestamp >= saleStart + 300 days`** (and timer extension logic never pushes **`deadline`** beyond **`saleStart + 300 days`**); **or** 2. **Pricing-only cap:** plateau min/max CHARM and price at 300 days but allow timer/game mechanics to continue (weaker alignment with “cap sales at 300 days” wording). **Default interpretation for this issue:** **(1) strong cap** — “sales” end by **300 × 86 400** seconds after **`saleStart`**. --- ## Recommended fix (onchain) 1. **Introduce an immutable or constant maximum sale calendar duration** - Suggested name: **`MAX_SALE_ELAPSED_SEC`** = **`300 * 86_400`**. - Prefer **`constant`** in `TimeCurve` if no storage migration is required; if governance needs flexibility later, use **immutable** set once in **`initialize`** with **`require(_max <= reasonableUpperBound)`**. 2. **Single source of truth for “pricing elapsed”** - Replace ad-hoc **`block.timestamp - saleStart`** in `_buy` / `_charmBounds` paths with a **`_elapsedCappedForSale()`** (or reuse **`_elapsedForCharmPricing`** with the cap applied consistently) so **views**, **`buy`**, and **any** `charmPrice.priceWad(elapsed)` callers share the **same** elapsed value. 3. **Enforce the 300-day wall-clock sale end** - In **`_buy`** (and any parallel entrypoints: **`buyFor` → `_buy`**, WarBow paths that depend on sale liveness if applicable): **`require(block.timestamp < saleStart + MAX_SALE_ELAPSED_SEC, "TimeCurve: sale max duration")`** (exact revert string to be chosen for consistency with existing `"TimeCurve: *"` messages). - **Timer extensions:** clamp **`TimeMath.extendDeadline` / `extendDeadlineOrResetBelowThreshold`** results so **`deadline` never exceeds `saleStart + MAX_SALE_ELAPSED_SEC`** (otherwise buys could remain valid after the cap while pricing was plateaued — inconsistent UX and economics). 4. **Defense in depth in `TimeMath` (optional but valuable)** - **`currentMinBuy`:** document **maximum safe** **`elapsed`** for a given **`growthRateWad`** or add an explicit **`require`/saturation** branch if you keep **`exp`** (team decision: **revert with clear error** vs **saturate minBuy** — saturation changes economics and must be documented). 5. **`LinearCharmPrice`** - If **`dailyIncrementWad`** is governance-upgradable, add **initializer / setter bounds** so **`base + increment * cappedElapsed`** cannot wrap **`uint256`** under the **`MAX_SALE_ELAPSED_SEC`** cap (cheap **`require`** on **`dailyIncrementWad`** vs **`type(uint256).max`** with **`mulDiv`**). 6. **Deploy-time alignment** - Ensure **`timerCapSec`**, **`initialTimerSec`**, and extension math cannot **encode** a sale that logically outlives the 300-day product cap (document in `PARAMETERS.md`). --- ## Files likely to change ### Solidity (authoritative) | File | Change | |------|--------| | [`contracts/src/TimeCurve.sol`](contracts/src/TimeCurve.sol) | Cap **`elapsed`** for **`_charmBounds`** / **`priceWad`**; **`require`** sale end by **`saleStart + 300d`**; clamp **`deadline`** updates to **`saleStart + 300d`**. | | [`contracts/src/libraries/TimeMath.sol`](contracts/src/libraries/TimeMath.sol) | Optional: **document** domain bounds; optional **`min(elapsed, cap)`** helper if shared; optional **explicit revert** on excessive exponent before **`exp`**. | | [`contracts/src/pricing/LinearCharmPrice.sol`](contracts/src/pricing/LinearCharmPrice.sol) | Optional **upper bound** on **`dailyIncrementWad`** (or **safety** comment + **`require`** in **`priceWad`** if uncapped governance is possible). | | [`contracts/script/DeployDev.s.sol`](contracts/script/DeployDev.s.sol) / [`contracts/script/UUPSDeployLib.sol`](contracts/script/UUPSDeployLib.sol) | If **`initialize`** signature gains a **`maxSaleDuration`** (immutable pattern), thread argument through deploy scripts. | | [`contracts/PARAMETERS.md`](contracts/PARAMETERS.md) | New row: **max sale duration = 300 days**; relationship to **`timerCapSec`**. | ### Tests | File | Change | |------|--------| | [`contracts/test/TimeCurve.t.sol`](contracts/test/TimeCurve.t.sol) | Revert on buy after cap; **`deadline`** clamp; views plateau / remain callable; extension near boundary. | | [`contracts/test/TimeMath.t.sol`](contracts/test/TimeMath.t.sol) | Regression: boundary **`elapsed`** with canonical **`growthRateWad`**; optional overflow **`dailyIncrement`** test if `LinearCharmPrice` is bounded. | | [`contracts/test/LinearCharmPrice.t.sol`](contracts/test/LinearCharmPrice.t.sol) | If new **`dailyIncrementWad` upper bound**: reject path. | | **New** (optional) | **`TimeCurveSaleDurationCap.t.sol`** focused fuzz: **`elapsed` → cap**, timer edge cases. | ### Documentation | File | Change | |------|--------| | [`docs/product/primitives.md`](docs/product/primitives.md) | State **300-day max sale** from **`saleStart`**; link to **`PARAMETERS.md`**. | | [`docs/testing/invariants-and-business-logic.md`](docs/testing/invariants-and-business-logic.md) | New invariant row + GitLab link (this issue): **max sale elapsed**, **`deadline` clamp**, compat with **#114 / #115** hero/timer copy. | | [`docs/frontend/timecurve-views.md`](docs/frontend/timecurve-views.md) | If UI must show **“sale ended (max duration)”** distinct from **timer expired** — short note (frontend may only read **`ended`** + timestamps). | | [`docs/qa/YO-TimeCurve-QA-Checklist.md`](docs/qa/YO-TimeCurve-QA-Checklist.md) | Checkbox: warp past **`saleStart + 300d`** → **buys forbidden**; charts use capped duration. | | [`contracts/README.md`](contracts/README.md) | One-line pointer to **`TimeMath` / sale cap** if architecture section lists risks mitigated. | | [`skills/play-timecurve-doubloon/SKILL.md`](skills/play-timecurve-doubloon/SKILL.md) | Update timer/duration bullets so play agents do not assume unbounded sale length. | ### Frontend / indexer (if behavior surfaces) | Area | When to touch | |------|----------------| | [`frontend/src/lib/timeCurveMath.ts`](frontend/src/lib/timeCurveMath.ts) and call sites | If client mirrors **`elapsed`** for previews — mirror **`min(elapsed, 300d)`** to match chain (avoid misleading bands when locally extrapolating). | | [`frontend/src/pages/timecurve/TimeCurveLiveCharts.tsx`](frontend/src/pages/timecurve/TimeCurveLiveCharts.tsx) | **`saleDurationSec`** already derived from **`deadline - saleStart`**; confirm charts when **`deadline` is clamped** to **`saleStart + 300d`**. | | [`indexer/src/chain_timer.rs`](indexer/src/chain_timer.rs) | Usually **no logic change** if only contract rules change; verify **API** still consistent. | --- ## Regression tests (concrete) 1. **`TimeCurve` — buy blocked after cap** - Configure **`saleStart`**, warp to **`saleStart + 300 * 86400`**, expect **`buy`** reverts (and **`buyFor`** / router path if covered). - Just before cap, **`buy`** still succeeds under existing timer rules. 2. **`TimeCurve` — deadline clamp** - Start sale; perform buys that would extend **`deadline`** past **`saleStart + 300d`**; assert **`deadline == saleStart + 300d`** (or **`min(..., cap)`**). 3. **`TimeCurve` — CHARM bounds / price views at boundary** - Warp to **`saleStart + 299d`** vs **`saleStart + 301d`**: capped **`elapsed`** yields **identical** **`currentMinBuy`** / envelope (**pricing plateau**). 4. **`TimeMath` — no revert under canonical growth for `elapsed == 300d`** - **`currentMinBuy(1e18, GROWTH_RATE_20PCT, 300 * 86400)`** returns finite **`minBuy`** (and **`minBuy <= type(uint256).max / WAD`** style sanity). 5. **(If implemented) `TimeMath` excessive exponent** - **`elapsed`** far beyond cap with malicious **`growthRateWad`** in a **unit test** expecting **clear revert** (deploy-time bound test) vs satiation. 6. **`LinearCharmPrice` overflow regression** - With **`dailyIncrementWad = type(uint256).max / 2`**, **`priceWad(300d)`** either **reverts at setter** or **reverts in `priceWad`** with documented bound. --- ## Acceptance criteria - [ ] Onchain **`MAX_SALE_ELAPSED_SEC`** (or equivalent) is **300 days** and **documented** in `contracts/PARAMETERS.md` and `docs/product/primitives.md`. - [ ] All **`buy`** / fee-routing sale interactions that must end with the sale respect **`block.timestamp < saleStart + 300 * 86400`** (per chosen **strong-cap** interpretation). - [ ] **`deadline`** updates from **`TimeMath`** never exceed **`saleStart + 300 * 86400`**. - [ ] **`elapsed`** fed into **`_charmBounds`** and **`charmPrice.priceWad`** is **capped consistently** (`_elapsedForCharmPricing` / shared helper). - [ ] **`TimeMath.currentMinBuy`** cannot brick **`TimeCurve`** views at **`elapsed == 300d`** for **canonical **`growthRateWad`** (DeployDev / production default). - [ ] **`LinearCharmPrice`** cannot **`uint256`** overflow on **`priceWad`** under **`elapsed ≤ 300d`** with **allowed** parameter bounds. - [ ] Forge tests added/updated and **`forge test`** passes for affected packages. - [ ] `docs/testing/invariants-and-business-logic.md` maps the new invariant; cross-links from audit I-01. --- ## Multistep verification checklist ### A. Local Forge 1. `cd contracts && forge test --match-contract TimeCurve -vv` 2. `forge test --match-contract TimeMath -vv` 3. `forge test --match-contract LinearCharmPrice -vv` 4. Full suite: `forge test` ### B. Anvil / dev stack (optional but recommended) 1. `./scripts/start-local-anvil-stack.sh` (or repo-documented minimal deploy). 2. `cast call` **CHARM bound / price** view functions at **`saleStart + 299d`** vs **`301d`** (capped plateau + buy revert). 3. If **`scripts/verify-*.sh`** touch TimeCurve timing, extend or add a **small script** that warps and asserts **`buy`** reverts post-cap. ### C. Frontend / indexer smoke 1. Start indexer + frontend against capped contract. 2. Confirm **protocol / simple / arena** pages load without runaway client math (if TS mirrors elapsed). 3. `GET /v1/timecurve/chain-timer` still consistent (**deadline_sec ≤ sale_start_sec + 300d** in seconds). ### D. Documentation / governance 1. Operator runbook / **`stage3`** docs mention **300-day sale ceiling** next to **`timerCapSec` / `initialTimerSec`**. 2. Audit remediation table (if maintained) marks **I-01** as addressed with link to merge request. --- ## References - Audit: [`audits/audit_smartcontract_1777813071.md`](audits/audit_smartcontract_1777813071.md) (**I-01**) - `TimeMath.currentMinBuy`: [`contracts/src/libraries/TimeMath.sol`](contracts/src/libraries/TimeMath.sol) - `LinearCharmPrice.priceWad`: [`contracts/src/pricing/LinearCharmPrice.sol`](contracts/src/pricing/LinearCharmPrice.sol) - `TimeCurve` elapsed + buy: [`contracts/src/TimeCurve.sol`](contracts/src/TimeCurve.sol) --- ## Labels / relationships (for maintainers) Suggested labels: `security`, `contracts`, `timecurve`, `audit`, `tech-debt` Related docs/issues: **GitLab #114** (`startSaleAt`), **#115** (pre-open countdown), **#48** (hero timer / phase derivation).
PlasticDigits commented 2026-05-04 06:02:25 +00:00 (Migrated from gitlab.com)

mentioned in commit 0f6c4672da

mentioned in commit 0f6c4672da47d1bcca07104887cf6164df60e8ea
Brouie commented 2026-05-04 06:08:21 +00:00 (Migrated from gitlab.com)

mentioned in issue #117

mentioned in issue #117
Brouie commented 2026-05-04 06:39:33 +00:00 (Migrated from gitlab.com)

@PlasticDigits — pulled commit a574c63 (post your #123 push). 4 prior FoT failures all clear, but 2 new failures remain. one is your I-01 fix needing a small ordering tweak, one is a fuzz-caught warbow edge case.

Failure 1 — test_gitlab124_buy_reverts_after_wall_clock_cap (your I-01 test)

[FAIL: Error != expected error: TimeCurve: below min charms != TimeCurve: sale max elapsed exceeded]

scenario: warp to saleStart + MAX_SALE_ELAPSED_SEC + 1, attempt t.buy(1e18). expected the cap-exceeded revert. actual: TimeCurve: below min charms because by 300 days deep, dynamic charm minimum has grown well above 1e18, and the bounds check fires before the wall-clock cap check.

your test is correct — the spec said strong cap, and a buy attempt past the cap should revert with the cap message regardless of charm amount. fix is to move the MAX_SALE_ELAPSED_SEC guard earlier in _buy, before currentCharmBoundsWad is consulted.

Failure 2 — testFuzz_warbow_steal_burn_exact_to_sink (fuzz catch)

[FAIL: assertion failed: 0 != 1000000000000000000; counterexample: args=[21118, 57005]]

decoded counterexample: stealer = address(57005) = 0x000000000000000000000000000000000000dead — the burn sink address itself. when stealer == BURN_SINK, the warbow steal burn is a self-transfer and the test's sinkAfter - sinkBefore == WARBOW_STEAL_BURN_WAD invariant breaks. unlikely in practice (no real wallet uses 0xdead) but the contract semantically allows it.

cheap mitigation: require(msg.sender != BURN_SINK) on warbow paths, or vm.assume to filter the burn address from fuzz seeds (kicks the can but matches spec).

prior 4 FoT parity failures from #117/#123 batch all PASS now. 261 tests succeed total.

@PlasticDigits — pulled commit `a574c63` (post your #123 push). 4 prior FoT failures all clear, but 2 new failures remain. one is your I-01 fix needing a small ordering tweak, one is a fuzz-caught warbow edge case. ## Failure 1 — `test_gitlab124_buy_reverts_after_wall_clock_cap` (your I-01 test) ``` [FAIL: Error != expected error: TimeCurve: below min charms != TimeCurve: sale max elapsed exceeded] ``` scenario: warp to `saleStart + MAX_SALE_ELAPSED_SEC + 1`, attempt `t.buy(1e18)`. expected the cap-exceeded revert. actual: `TimeCurve: below min charms` because by 300 days deep, dynamic charm minimum has grown well above 1e18, and the bounds check fires before the wall-clock cap check. your test is correct — the spec said strong cap, and a buy attempt past the cap should revert with the cap message regardless of charm amount. fix is to move the `MAX_SALE_ELAPSED_SEC` guard earlier in `_buy`, before `currentCharmBoundsWad` is consulted. ## Failure 2 — `testFuzz_warbow_steal_burn_exact_to_sink` (fuzz catch) ``` [FAIL: assertion failed: 0 != 1000000000000000000; counterexample: args=[21118, 57005]] ``` decoded counterexample: `stealer = address(57005) = 0x000000000000000000000000000000000000dead` — the burn sink address itself. when stealer == BURN_SINK, the warbow steal burn is a self-transfer and the test's `sinkAfter - sinkBefore == WARBOW_STEAL_BURN_WAD` invariant breaks. unlikely in practice (no real wallet uses 0xdead) but the contract semantically allows it. cheap mitigation: `require(msg.sender != BURN_SINK)` on warbow paths, or `vm.assume` to filter the burn address from fuzz seeds (kicks the can but matches spec). prior 4 FoT parity failures from #117/#123 batch all PASS now. 261 tests succeed total.
Brouie commented 2026-05-04 07:17:29 +00:00 (Migrated from gitlab.com)

@PlasticDigits — dug into the failure trace, the failure is more nuanced than I first thought.

trace shows: _warpPastBuyCooldown warps to nextBuyAllowedAt[alice] = 28920000 which equals saleStart + MAX_SALE_ELAPSED_SEC exactly. then t.buy(1e18) runs at that timestamp.

contract guard at line 414: require(block.timestamp <= saleStart + MAX_SALE_ELAPSED_SEC, ...) — boundary inclusive, so 28920000 == 28920000 passes the cap check. then bounds check fires with elapsed = maxSec, dynamic minCharm has grown past 1e18 → "below min charms".

so the bug is one of:

  1. test bug: _warpPastBuyCooldown is clamping to exactly the cooldown timestamp which equals the cap boundary. should warp to max(nextBuyAllowedAt[alice], saleStart + MAX_SALE_ELAPSED_SEC) + 1 to land 1 second strictly past the cap.

  2. contract bug: line 414 should be strict < instead of <= if the spec intent is "sales end at exactly maxSec". but test_gitlab124_warbow_guard_reverts_after_wall passes with the same <= semantics, suggesting <= is the intended boundary.

most likely option 1. _warpPastBuyCooldown is the problem — it warps to exactly the cooldown end which collides with the cap when those two timestamps align. quick fix is to update the test scenario, not the contract.

still flagging since it's currently a CI-red test on main.

@PlasticDigits — dug into the failure trace, the failure is more nuanced than I first thought. trace shows: `_warpPastBuyCooldown` warps to `nextBuyAllowedAt[alice] = 28920000` which equals `saleStart + MAX_SALE_ELAPSED_SEC` exactly. then `t.buy(1e18)` runs at that timestamp. contract guard at line 414: `require(block.timestamp <= saleStart + MAX_SALE_ELAPSED_SEC, ...)` — boundary inclusive, so 28920000 == 28920000 passes the cap check. then bounds check fires with elapsed = maxSec, dynamic minCharm has grown past 1e18 → "below min charms". so the bug is one of: 1. **test bug**: `_warpPastBuyCooldown` is clamping to exactly the cooldown timestamp which equals the cap boundary. should warp to `max(nextBuyAllowedAt[alice], saleStart + MAX_SALE_ELAPSED_SEC) + 1` to land 1 second strictly past the cap. 2. **contract bug**: line 414 should be strict `<` instead of `<=` if the spec intent is "sales end at exactly maxSec". but `test_gitlab124_warbow_guard_reverts_after_wall` passes with the same `<=` semantics, suggesting `<=` is the intended boundary. most likely option 1. `_warpPastBuyCooldown` is the problem — it warps to exactly the cooldown end which collides with the cap when those two timestamps align. quick fix is to update the test scenario, not the contract. still flagging since it's currently a CI-red test on `main`.
Brouie commented 2026-05-04 07:24:21 +00:00 (Migrated from gitlab.com)

mentioned in issue #128

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

mentioned in commit dadc16b3cd

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

We want to allow last buys to come in on last second so <= is correct

We want to allow last buys to come in on last second so <= is correct
Brouie commented 2026-05-04 08:24:43 +00:00 (Migrated from gitlab.com)

ack — <= boundary is correct intent. so the failure is squarely in _warpPastBuyCooldown clamping to exactly saleStart + MAX_SALE_ELAPSED_SEC. test fix: warp to max(nextBuyAllowedAt[alice], saleStart + MAX_SALE_ELAPSED_SEC) + 1 so the buy attempt lands strictly past the cap. will retest after the test patch lands.

ack — `<=` boundary is correct intent. so the failure is squarely in `_warpPastBuyCooldown` clamping to exactly `saleStart + MAX_SALE_ELAPSED_SEC`. test fix: warp to `max(nextBuyAllowedAt[alice], saleStart + MAX_SALE_ELAPSED_SEC) + 1` so the buy attempt lands strictly past the cap. will retest after the test patch lands.
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 08:43:48 +00:00 (Migrated from gitlab.com)

mentioned in commit f5d5c1d482

mentioned in commit f5d5c1d482e92ce3fc1a74a2733ea9bb047f43fe
PlasticDigits commented 2026-05-04 09:16:28 +00:00 (Migrated from gitlab.com)

#124 follow-up (inclusive wall `\<=` at `saleStart + MAX_SALE_ELAPSED_SEC`)

Summary: Issue notes called out `_buy` ~L414 using `\<=` so last-second buys can succeed when the countdown still allows `block.timestamp \< deadline()` (typically after deadline clamps to `saleStart + MAX + 1`). Docs/spec wording that implied a strict wall was updated; no change from `\<=` to `\<` (per maintainer note on the issue).

Merged (already on `main`):

  • `TimeCurve.sol`: NatSpec on `MAX_SALE_ELAPSED_SEC` + short comment on the wall `require`.
  • Regression: `test_gitlab124_buy_succeeds_at_hard_wall_second_when_deadline_clamped` — second buy at `saleStart + MAX` after a buy that clamps `deadline`.
  • Docs / links: `docs/testing/invariants-and-business-logic.md` §124, `docs/product/primitives.md`, `contracts/PARAMETERS.md`, audit I-01 mitigation blurb, `skills/play-timecurve-doubloon/SKILL.md`, `skills/README.md`, `.cursor/skills/yieldomega-guardrails/SKILL.md`.

Verification checklist for @Brouie:

  1. `cd contracts && forge test --match-path test/TimeCurve.t.sol` — expect 101 passes including new `test_gitlab124_buy_succeeds_at_hard_wall_second_when_deadline_clamped`.
  2. Read §124 in `docs/testing/invariants-and-business-logic.md` — wall is `\<=`, timer interaction spelled out.
  3. Skim `contracts/PARAMETERS.md` TimeCurve row “Sale wall-clock cap”.
  4. (`TimeCurve._buy`) confirm wall check remains `require(block.timestamp \<= saleStart + MAX_SALE_ELAPSED_SEC, ...)` before CHARM bounds (cap message precedence).

Issue left open as requested.

## #124 follow-up (inclusive wall \`\\<=\` at \`saleStart + MAX_SALE_ELAPSED_SEC\`) **Summary:** Issue notes called out \`_buy\` ~L414 using \`\\<=\` so last-second buys can succeed when the **countdown** still allows \`block.timestamp \\< deadline()\` (typically after **deadline** clamps to \`saleStart + MAX + 1\`). Docs/spec wording that implied a **strict** wall was updated; **no** change from \`\\<=\` to \`\\<\` (per maintainer note on the issue). **Merged (already on \`main\`):** - \`TimeCurve.sol\`: NatSpec on \`MAX_SALE_ELAPSED_SEC\` + short comment on the wall \`require\`. - **Regression:** \`test_gitlab124_buy_succeeds_at_hard_wall_second_when_deadline_clamped\` — second buy at \`saleStart + MAX\` after a buy that clamps \`deadline\`. - **Docs / links:** \`docs/testing/invariants-and-business-logic.md\` §124, \`docs/product/primitives.md\`, \`contracts/PARAMETERS.md\`, audit **I-01** mitigation blurb, \`skills/play-timecurve-doubloon/SKILL.md\`, \`skills/README.md\`, \`.cursor/skills/yieldomega-guardrails/SKILL.md\`. **Verification checklist for @Brouie:** 1. \`cd contracts && forge test --match-path test/TimeCurve.t.sol\` — expect **101** passes including new \`test_gitlab124_buy_succeeds_at_hard_wall_second_when_deadline_clamped\`. 2. Read **§124** in [\`docs/testing/invariants-and-business-logic.md\`](https://gitlab.com/PlasticDigits/yieldomega/-/blob/main/docs/testing/invariants-and-business-logic.md) — wall is \`\\<=\`, timer interaction spelled out. 3. Skim [\`contracts/PARAMETERS.md\`](https://gitlab.com/PlasticDigits/yieldomega/-/blob/main/contracts/PARAMETERS.md) TimeCurve row “Sale wall-clock cap”. 4. (\`TimeCurve._buy\`) confirm wall check remains \`require(block.timestamp \\<= saleStart + MAX_SALE_ELAPSED_SEC, ...)\` before CHARM bounds (cap message precedence). Issue left **open** as requested.
PlasticDigits commented 2026-05-04 09:22:07 +00:00 (Migrated from gitlab.com)

mentioned in commit 6a4f054fe8

mentioned in commit 6a4f054fe84b16fd07493cf316cb8112b58c2625
Brouie commented 2026-05-05 01:50:24 +00:00 (Migrated from gitlab.com)

@PlasticDigits — verification PASS on the inclusive-wall fix. walked your checklist:

  1. forge test --match-path test/TimeCurve.t.sol green. test_gitlab124_buy_succeeds_at_hard_wall_second_when_deadline_clamped clean. full suite 310/310 on a421ba1.
  2. §124 in docs/testing/invariants-and-business-logic.md reads cleanly — wall is <=, timer interaction spelled out, deadline clamp at saleStart + MAX + 1 documented.
  3. contracts/PARAMETERS.md 'Sale wall-clock cap' row matches: 300 days, inclusive boundary, cap-message precedence.
  4. _buy ordering verified — require(block.timestamp <= saleStart + MAX_SALE_ELAPSED_SEC, ...) at L460 fires before require(charmWad >= minCharm, ...) at L466. post-cap buy now reverts with cap message instead of the misleading 'below min charms'. WarBow paths share the same pattern (L606/621/692/721) — consistent.
    <= semantics with deadline clamped to MAX + 1 lands the 'last buys can come in on last second' product intent without leaking elapsed past the 300d boundary into pricing math.
    ready for close on your side.
@PlasticDigits — verification PASS on the inclusive-wall fix. walked your checklist: 1. `forge test --match-path test/TimeCurve.t.sol` green. `test_gitlab124_buy_succeeds_at_hard_wall_second_when_deadline_clamped` clean. full suite 310/310 on `a421ba1`. 2. §124 in `docs/testing/invariants-and-business-logic.md` reads cleanly — wall is `<=`, timer interaction spelled out, deadline clamp at `saleStart + MAX + 1` documented. 3. `contracts/PARAMETERS.md` 'Sale wall-clock cap' row matches: 300 days, inclusive boundary, cap-message precedence. 4. `_buy` ordering verified — `require(block.timestamp <= saleStart + MAX_SALE_ELAPSED_SEC, ...)` at L460 fires before `require(charmWad >= minCharm, ...)` at L466. post-cap buy now reverts with cap message instead of the misleading 'below min charms'. WarBow paths share the same pattern (L606/621/692/721) — consistent. `<=` semantics with deadline clamped to `MAX + 1` lands the 'last buys can come in on last second' product intent without leaking elapsed past the 300d boundary into pricing math. ready for close on your side.
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-05-05 02:31:56 +00:00
PlasticDigits commented 2026-05-13 16:07:17 +00:00 (Migrated from gitlab.com)

mentioned in issue #207

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