Audit CL8Y outflow policy exceptions and hardening #70

Closed
opened 2026-04-26 08:31:45 +00:00 by PlasticDigits · 6 comments
PlasticDigits commented 2026-04-26 08:31:45 +00:00 (Migrated from gitlab.com)

Summary

CL8Y flow audit report: docs/onchain/cl8y-flow-audit.md on main at commit 8eb3784.

The audit applies the policy that CL8Y flowing into app contracts is generally low risk, while CL8Y flowing out of app contracts should be owner/admin executed, without a timelock, so it can receive manual review.

Findings

  • FeeSink.withdraw on EcosystemTreasury, CL8YProtocolTreasury, and DoubLPIncentives matches the manual-review model: WITHDRAWER_ROLE, no timelock.
  • Several current CL8Y outflows are public or app-contract-called and need an explicit approved exception or a code change: buy-time routing through FeeRouter, public FeeRouter.distributeFees, WarBow burns, RabbitTreasury.withdraw, RabbitTreasury.receiveFee burn leg, and prize payouts through TimeCurve.distributePrizes / PodiumPool.payPodiumPayout.
  • Highest-priority gap: TimeCurve.distributePrizes is public once reservePodiumPayoutsEnabled is enabled, and the frontend exposes it as a public action. Owner/admin controls the enable flag but not the payout execution call.
  • Concrete hardening item: TimeCurveBuyRouter.buyViaKumbaya refunds the full router CL8Y balance to the current caller. Normal exact-output dust is expected, but accidental/pre-existing CL8Y can be swept by the next caller.
  • No hidden production rescue, sweep, or fallback CL8Y outflow path was found.

Recommendations

  • Decide and document the approved exception set for public/user-driven CL8Y outflows.
  • Make TimeCurve.distributePrizes owner/admin-only or add an explicit operator role if payout execution must be manually reviewed.
  • Harden TimeCurveBuyRouter CL8Y refunds to only refund same-transaction surplus or revert on pre-existing CL8Y.
  • Constrain or explicitly document FeeRouter.distributeFees behavior, and alert on direct/manual calls or unexpected router balances.
  • Confirm production admin custody for WITHDRAWER_ROLE, GOVERNOR_ROLE, DEFAULT_ADMIN_ROLE, and TimeCurve owner: multisig/hardware-backed, explicitly not timelocked for final value movement.
  • Add monitoring for Withdrawn(token == CL8Y), SinksUpdated, ReservePodiumPayoutsEnabled, BuyFeeRoutingEnabled, and Upgraded events.
  • Add a non-Anvil deploy guard against wiring mock CL8Y as acceptedAsset or reserveAsset.
## Summary CL8Y flow audit report: `docs/onchain/cl8y-flow-audit.md` on `main` at commit `8eb3784`. The audit applies the policy that CL8Y flowing into app contracts is generally low risk, while CL8Y flowing out of app contracts should be owner/admin executed, without a timelock, so it can receive manual review. ## Findings - `FeeSink.withdraw` on `EcosystemTreasury`, `CL8YProtocolTreasury`, and `DoubLPIncentives` matches the manual-review model: `WITHDRAWER_ROLE`, no timelock. - Several current CL8Y outflows are public or app-contract-called and need an explicit approved exception or a code change: buy-time routing through `FeeRouter`, public `FeeRouter.distributeFees`, WarBow burns, `RabbitTreasury.withdraw`, `RabbitTreasury.receiveFee` burn leg, and prize payouts through `TimeCurve.distributePrizes` / `PodiumPool.payPodiumPayout`. - Highest-priority gap: `TimeCurve.distributePrizes` is public once `reservePodiumPayoutsEnabled` is enabled, and the frontend exposes it as a public action. Owner/admin controls the enable flag but not the payout execution call. - Concrete hardening item: `TimeCurveBuyRouter.buyViaKumbaya` refunds the full router CL8Y balance to the current caller. Normal exact-output dust is expected, but accidental/pre-existing CL8Y can be swept by the next caller. - No hidden production `rescue`, `sweep`, or fallback CL8Y outflow path was found. ## Recommendations - Decide and document the approved exception set for public/user-driven CL8Y outflows. - Make `TimeCurve.distributePrizes` owner/admin-only or add an explicit operator role if payout execution must be manually reviewed. - Harden `TimeCurveBuyRouter` CL8Y refunds to only refund same-transaction surplus or revert on pre-existing CL8Y. - Constrain or explicitly document `FeeRouter.distributeFees` behavior, and alert on direct/manual calls or unexpected router balances. - Confirm production admin custody for `WITHDRAWER_ROLE`, `GOVERNOR_ROLE`, `DEFAULT_ADMIN_ROLE`, and `TimeCurve` owner: multisig/hardware-backed, explicitly not timelocked for final value movement. - Add monitoring for `Withdrawn(token == CL8Y)`, `SinksUpdated`, `ReservePodiumPayoutsEnabled`, `BuyFeeRoutingEnabled`, and `Upgraded` events. - Add a non-Anvil deploy guard against wiring mock CL8Y as `acceptedAsset` or `reserveAsset`.
PlasticDigits commented 2026-04-27 05:27:46 +00:00 (Migrated from gitlab.com)

mentioned in commit 12b433d3bb

mentioned in commit 12b433d3bbf1e992726c349ce4967830499fe473
PlasticDigits commented 2026-04-27 05:28:34 +00:00 (Migrated from gitlab.com)

Implemented and merged the CL8Y outflow hardening work for issue #70 into main.

Merged/pushed commit: 2af5f562648895d643e81f0ff7dc149b7f5fe82b (Merge branch 'issue-70-cl8y-outflow-hardening'). The temporary local issue branch/worktree was removed after merge; origin now only has main.

Verification checklist

  • TimeCurve.distributePrizes is onlyOwner, so prize execution now requires the TimeCurve owner wallet, not just the payout enable flag.
  • Arena UI reads TimeCurve.owner() and disables the prize distribution action for non-owner wallets.
  • PodiumPool supports one-time setPrizePusher(TimeCurve) wiring; once set, payPodiumPayout only accepts the canonical pusher instead of any DISTRIBUTOR_ROLE holder.
  • DeployDev wires PodiumPool.setPrizePusher(address(tc)).
  • TimeCurveBuyRouter now requires a non-zero cl8yProtocolTreasury constructor arg and sends post-buyFor CL8Y surplus there instead of refunding/sweeping to the buyer.
  • WarBow CL8Y burn paths are documented as user-driven approved exceptions and covered by sink-exact fuzz tests.
  • Rabbit Treasury withdraw / receiveFee CL8Y policy alignment remains explicitly deferred with TODO NatSpec and audit docs.
  • Docs updated: CL8Y flow audit, final signoff/value movement runbook, invariant/test map, and WarBow play skill cross-links.
  • Commit hook passed: gitleaks and frontend tsc -b.
  • Targeted Forge verification passed: forge test --out out-agent --cache-path cache-agent --match-path 'test/{FeeSinks,TimeCurve,TimeCurveBuyRouter,TimeCurveWarBowCl8yBurns}.t.sol' → 90 passed, 0 failed.

Note: the first Forge run using the default contracts/out failed before tests because that directory is root-owned in the local checkout. Re-ran with writable out-agent / cache-agent, then removed those generated dirs before pushing.

Implemented and merged the CL8Y outflow hardening work for issue #70 into `main`. Merged/pushed commit: `2af5f562648895d643e81f0ff7dc149b7f5fe82b` (`Merge branch 'issue-70-cl8y-outflow-hardening'`). The temporary local issue branch/worktree was removed after merge; `origin` now only has `main`. ## Verification checklist - [x] `TimeCurve.distributePrizes` is `onlyOwner`, so prize execution now requires the TimeCurve owner wallet, not just the payout enable flag. - [x] Arena UI reads `TimeCurve.owner()` and disables the prize distribution action for non-owner wallets. - [x] `PodiumPool` supports one-time `setPrizePusher(TimeCurve)` wiring; once set, `payPodiumPayout` only accepts the canonical pusher instead of any `DISTRIBUTOR_ROLE` holder. - [x] `DeployDev` wires `PodiumPool.setPrizePusher(address(tc))`. - [x] `TimeCurveBuyRouter` now requires a non-zero `cl8yProtocolTreasury` constructor arg and sends post-`buyFor` CL8Y surplus there instead of refunding/sweeping to the buyer. - [x] WarBow CL8Y burn paths are documented as user-driven approved exceptions and covered by sink-exact fuzz tests. - [x] Rabbit Treasury `withdraw` / `receiveFee` CL8Y policy alignment remains explicitly deferred with TODO NatSpec and audit docs. - [x] Docs updated: CL8Y flow audit, final signoff/value movement runbook, invariant/test map, and WarBow play skill cross-links. - [x] Commit hook passed: `gitleaks` and frontend `tsc -b`. - [x] Targeted Forge verification passed: `forge test --out out-agent --cache-path cache-agent --match-path 'test/{FeeSinks,TimeCurve,TimeCurveBuyRouter,TimeCurveWarBowCl8yBurns}.t.sol'` → 90 passed, 0 failed. Note: the first Forge run using the default `contracts/out` failed before tests because that directory is root-owned in the local checkout. Re-ran with writable `out-agent` / `cache-agent`, then removed those generated dirs before pushing.
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-04-28 03:23:12 +00:00
PlasticDigits commented 2026-05-04 04:14:33 +00:00 (Migrated from gitlab.com)

mentioned in issue #116

mentioned in issue #116
PlasticDigits commented 2026-05-04 04:15:32 +00:00 (Migrated from gitlab.com)

mentioned in issue #117

mentioned in issue #117
PlasticDigits commented 2026-05-13 16:07:18 +00:00 (Migrated from gitlab.com)

mentioned in issue #207

mentioned in issue #207
PlasticDigits commented 2026-05-19 07:53:22 +00:00 (Migrated from gitlab.com)

mentioned in issue #217

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