Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions docs/wiki/domains/tooling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
title: Tooling Domain
updated: 2026-08-03
type: domain
sources:
- packages/tooling/**
---

# Tooling Domain

## Boundary

`@reserve-protocol/tooling` is the private workspace home for operational scripts and
agent skills that review protocol activity. It consumes `@reserve-protocol/sdk` and
`@reserve-protocol/dtf-catalog`; nothing depends on it, and it publishes nothing. Logic
that other surfaces would need belongs in the SDK, not here.

## Shape

- One folder per workflow under `src/` (`rebalance-validation/`), with its checks in
`checks/` and its outside data providers in `sources/`.
- Each workflow has a CLI entry (`cli.ts`) run through `tsx`, and a skill in
`skills/<name>/SKILL.md` that says how to act on the output.
- Protocol reads, decoding and math go through the SDK. The package holds only the
review logic and the sources the SDK deliberately does not own.
Comment on lines +24 to +25

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the SDK boundary statement.

Line [24] says that decoding and math go through the SDK. The validator performs local decoding in recoverTokenInputs and local review math in ordersOfMagnitude and bandUsage. Narrow this statement to protocol reads and canonical calldata construction.

Suggested wording
- Protocol reads, decoding and math go through the SDK. The package holds only the
- review logic and the sources the SDK deliberately does not own.
+ Protocol reads and canonical calldata construction go through the SDK. Validator-
+ specific decoding and review math stay in this package, together with sources the
+ SDK deliberately does not own.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- Protocol reads, decoding and math go through the SDK. The package holds only the
review logic and the sources the SDK deliberately does not own.
- Protocol reads and canonical calldata construction go through the SDK. Validator-
specific decoding and review math stay in this package, together with sources the
SDK deliberately does not own.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/wiki/domains/tooling.md` around lines 24 - 25, Update the SDK boundary
statement in the tooling documentation to say that only protocol reads and
canonical calldata construction go through the SDK. Remove the claims that
decoding and math are SDK responsibilities, while preserving the existing
explanation of locally owned review logic and sources.


## Invariants

- Rebalance review is two passes: `disasters` gates (nonzero exit), `outcomes` informs.
Only disaster-pass failures can block a proposal.
- Disaster checks use data from outside Reserve (pool prices, third-party token
listings). The proposal is built from the Reserve API, so an API-vs-calldata
comparison cannot detect a wrong API price.
- Calldata correctness is established by re-deriving it through
`buildIndexDtfStartRebalanceArgs`, not by re-implementing the weight math.
- Every run prints the questions that cannot be checked mechanically; a clean report
is not a completed review.

## Encoding

`weight.spot` is `D27{tok/share}` (per share, so whole tokens per whole share is
`spot / 1e27 * 1e18 / 10**decimals`); `price` is `D27{nanoUSD/tok}` with
`low = p*(1-e)` / `high = p/(1-e)`, so price is `sqrt(low*high)`; `maxAuctionSize` is
`{tok}`, not USD.
1 change: 1 addition & 0 deletions docs/wiki/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ One line per page. Agents: start here, follow links, keep this list current on i

- [[sdk]] — core reads, mapping, namespaces/refs, and prepared-call boundary
- [[react-sdk]] — providers, query keys/options/hooks, and performance rules
- [[tooling]] — operational scripts and agent skills for protocol review
5 changes: 5 additions & 0 deletions docs/wiki/log.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,8 @@ Append-only chronological record: lessons, corrections, friction. Newest section
## 2026-07-22

- Multi-repo SDK/Register work exposed avoidable approval churn when only Register was writable. Start those sessions with both repositories as writable workspace roots (or their parent as the workspace); sibling read-only inspection does not need escalation, and write-heavy SDK verification should be batched into the release gate.

## 2026-08-03

- DTF rebalance validation moved into `packages/tooling` (an earlier prototype lived in Register and was dropped as out of scope there). Two lessons from running it against the live CMC20 August 2026 proposal: `weight.spot` is D27 per _share_, not per whole share, so trade sizing that treats it as whole-token units per share reports the entire basket as a sell; and `POST /rebalance/liquidity` returns `priceImpact` already in percent and signed by direction, so treating it as a fraction reports 11.6% impact as 1152%. Both were invisible to unit tests and only surfaced against real data — validate new checks against a known proposal, not just fixtures.
- Disaster-class checks in that package deliberately use non-Reserve data (DEXScreener pool prices, CoinGecko listings). Proposals are built from the Reserve API, so an API-vs-calldata comparison is self-consistent by construction and cannot catch a wrong price or a look-alike token address.
1 change: 1 addition & 0 deletions docs/wiki/progress.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Stage ledger. One row per stage; keep entries short. Verifier = exact fresh comm

| Stage | Status | Verifier | Review | Next |
| ----------------------------------------------------------------------- | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| DTF rebalance validation tooling (packages/tooling) | done (base origin/main) | scoped verify green: format:check + lint + docs:links + typecheck + `turbo run test --force` (tooling 10); CLI run on the live CMC20 proposal | correctness+complexity: self-review; fixed per-share unit scaling and percent-unit price impact, both caught against live data | trading desk answers the $10k/5%/$50k flags; decide the `ttl > launcher window` policy |
| vote-lock self-appreciating vault support (0.5.1) | human-review-required (base 9231139) | scoped verify green: format/lint/typecheck/test both packages (vote-lock.test 2, query-keys 12) + bundle + docs:links + wiki-lint | Dark + Light; blocker fixed (redeem builder missing from index-dtf/index.ts + index.ts barrels); multicall-order assertion added; patch changeset per Luis (0.5.1, though API additions are minor-shaped) | **Engineer review required** (redeem calldata builder + VoteLockState staking surface); then changesets release 0.5.1; register consumes via local link until published |
| 0.5.0 hardening release closeout | done (base f1ee8f5) | Node 24 `release:ci`: types + lint/format + live codegen + 394 tests (+17 live skipped) + build/bundle + 93 docs + catalog + 3 LICENSE-bearing tarballs; forced closeout gate + wiki-lint green | correctness+security+product+complexity: PR #27 reconciled; zero values, status validation, timestamp selection, namespace/ref/hook coverage, schema drift, publish gate, docs, and linked Register RED→GREEN verified | engineer review; commit/push; Changesets release to 0.5.0 |
| governance tie semantics + yield list state + rebalance hardening tests | done (base 588954e) | full gate on Node 24: forced builds + sdk-bundle + typecheck + lint + format + forced tests (sdk 287 passed/17 live skipped, react-sdk 72) + docs links + catalog checks; wiki-lint green | correctness+security+product+complexity: Dark+Light subagent pair; adopted boundary/mixed-flavor vectors and pinned zero-supply message; PENDING-expired labeling and detail QUORUM_NOT_REACHED split verified against Register reference, sent to backlog | Luis review (user-visible governance badge change); release patch |
Expand Down
47 changes: 47 additions & 0 deletions packages/tooling/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# @reserve-protocol/tooling

Operational tooling for reviewing protocol activity, built on `@reserve-protocol/sdk`
and `@reserve-protocol/dtf-catalog`. Private to the workspace — scripts and agent
skills, not a published surface.

## Rebalance validation

```bash
pnpm --filter @reserve-protocol/tooling validate:rebalance \
"https://app.reserve.org/bsc/index-dtf/cmc20/governance/proposal/<id>"
```

Exits 1 when a disaster check fails, 0 otherwise. The agent-facing procedure —
how to triage the output and who owns each warning — is
[`skills/validating-dtf-rebalances/SKILL.md`](./skills/validating-dtf-rebalances/SKILL.md).

### What it checks

Pass one, "preventing disasters" (blocking):

| Check | Catches |
| -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| single `startRebalance` on the DTF, routed through the governor whose timelock holds `REBALANCE_MANAGER` | a proposal that passes and then reverts, or a rebalance smuggled in with other actions |
| calldata re-derived through `@reserve-protocol/dtf-rebalance-lib` (via the SDK) | hand-edited weights, price ranges or rebalance limits |
| every held asset present in the calldata; additions and zero-weight exits surfaced | assets stranded outside the rebalance |
| encoded price vs the deepest-pool price, per asset | wrong decimals, a stale or wrong API price, the wrong token |
| proposed basket shares re-valued at pool prices | value shifted into the wrong place while each price still looks plausible |
| per-share units vs the last executed rebalance | order-of-magnitude weight jumps |
| basket addresses against a third-party address→coin map | look-alike and scam addresses |

Pass two, "optimizing outcomes" (informational): auction launcher window vs TTL
and the resulting permissionless tail, turnover as a share of AUM, legs above
$10,000, constituents with less than $50,000 of pooled liquidity, and price
impact per leg from the production `POST /rebalance/liquidity` route.

Prices and token identity in pass one come from sources outside Reserve
(DEXScreener, CoinGecko) on purpose: the proposal was built from the Reserve API,
so only an independent mark can catch that API being wrong.

### Encoding notes

`weight.spot` is `D27{tok/share}` — per _share_, so whole tokens per whole share is
`spot / 1e27 * 1e18 / 10**decimals`. `price` is `D27{nanoUSD/tok}` with
`low = p*(1-e)`, `high = p/(1-e)`, so the asset price is `sqrt(low*high)` and the
price-error preset is `1 - low/price`. `maxAuctionSize` is encoded in `{tok}`, not
USD. `limits.high` is `1/(1-basketError)` for TRACKING DTFs and `1e18` for NATIVE.
33 changes: 33 additions & 0 deletions packages/tooling/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "@reserve-protocol/tooling",
"version": "0.0.0",
"private": true,
"description": "Operational tooling and agent skills for DTF review workflows.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/reserve-protocol/dtf-interface",
"directory": "packages/tooling"
},
"type": "module",
"scripts": {
"clean": "rm -rf *.tsbuildinfo",
"test": "vitest run --passWithNoTests",
"typecheck": "tsc -p tsconfig.json --noEmit",
"validate:rebalance": "tsx src/rebalance-validation/cli.ts"
},
"dependencies": {
"@reserve-protocol/sdk": "workspace:*",
"viem": "catalog:"
},
"devDependencies": {
"@dtf-interface/tsconfig": "workspace:*",
"@types/node": "catalog:",
"tsx": "^4.20.6",
"typescript": "catalog:",
"vitest": "catalog:"
},
"engines": {
"node": ">=24"
}
}
68 changes: 68 additions & 0 deletions packages/tooling/skills/validating-dtf-rebalances/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
---
name: validating-dtf-rebalances
description: Use when asked to review, validate, or sanity-check an Index DTF rebalance governance proposal (an app.reserve.org `.../governance/proposal/<id>` link), before voting on or executing one.
---

# Validating DTF rebalances

A rebalance proposal encodes weights, price ranges and auction timing as calldata.
Two kinds of thing go wrong, and they are not equally bad:

- **Disasters** — the calldata moves real value into the wrong place: a mispriced
asset, wrong decimals, a scam or look-alike token address, the wrong governor.
These block the proposal.
- **Execution** — the trade is correct but fills badly: thin liquidity, high price
impact, an auction nobody opens. These need preparation, not a veto.

Run the disaster pass first and do not weigh execution findings against it.

## Steps

1. Run the checker on the proposal URL:

```bash
pnpm --filter @reserve-protocol/tooling validate:rebalance "<proposal url>"
```

Done when it prints a verdict line. Exit code 1 means a disaster check failed;
0 with warnings means execution risks only.

2. Resolve every `FAIL`. A failure is a claim about the calldata, so answer it
with the calldata: read the check's detail line, then confirm against an
explorer or an independent price source. Done when each failure is either a
fixed proposal or a written explanation of why the check is wrong here.

3. Take each `WARN` to the person who owns it. Done when each warning has a
named owner and an answer:
- trade above $10,000, price impact above 5%, or liquidity below $50,000 →
the trading desk, who decides whether to buy inventory before the auction.
- basket addition or removal → whoever owns the index mandate.
- permissionless tail (`ttl` beyond the launcher window) → the auction
launcher operator.
Comment on lines +35 to +41

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Assign an owner for turnover warnings.

The outcomes pass includes turnover, but this section assigns owners for trade size, price impact, liquidity, basket changes, and TTL only. Add an explicit owner and decision path for turnover warnings. Otherwise Step 3 can leave a WARN without a named owner.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/tooling/skills/validating-dtf-rebalances/SKILL.md` around lines 35 -
41, Add an explicit turnover-warning case to Step 3 alongside the existing WARN
ownership mappings, naming the responsible owner and the decision they must make
so every turnover WARN receives an owner and resolution path.


4. Answer the "still needs a human" questions the run prints. They cannot be
checked mechanically (is the constituent universe official? is this wrapper
canonical?), and a clean report without them is not a review. Done when each
is answered or explicitly deferred to a named person.

5. Report the verdict as: disaster pass result, then execution flags with owners,
then unanswered questions. Never report "looks good" while a question from
step 4 is open.

## Adding a check

Add a check when a real proposal could go wrong in a way the current run would
miss, not to restate something already covered.

- Independent data only for disaster checks. The proposal was built from the
Reserve API, so an API-vs-calldata comparison cannot detect a wrong API price;
pool prices and third-party token listings can. New disaster checks belong in
`src/rebalance-validation/checks/`, new outside sources in
`src/rebalance-validation/sources/`.
- A check needs a threshold that separates "wrong model of the world" from
"market moved". Order-of-magnitude comparisons and band-usage fractions do;
"looks different" does not.
- Failing is for things that make the proposal wrong. Anything about execution
quality is a warning, in the `outcomes` pass.
- Add a unit test for the pure math in `tests/`, and re-run the checker against a
known-good historical proposal to confirm it still passes.
Loading