Skip to content

test: pin cyclo-site-tokens.json claims on-chain via fork tests#42

Open
thedavidmeister wants to merge 25 commits into
mainfrom
2026-05-05-cyclo-site-decimals-fork-test
Open

test: pin cyclo-site-tokens.json claims on-chain via fork tests#42
thedavidmeister wants to merge 25 commits into
mainfrom
2026-05-05-cyclo-site-decimals-fork-test

Conversation

@thedavidmeister

@thedavidmeister thedavidmeister commented May 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

canonical/cyclo-site-tokens.json is the single source of truth for every cyToken's vault, decimals, underlying, underlying decimals, receipt, and chain. cyclo.site will consume this same JSON in a follow-up PR; for now both repos stay in sync by hand and the fork test guards the on-chain claims for the values it can verify.

For every entry on the test's chain (filtered by chainId), the fork test asserts:

  • vault decimals() matches the JSON
  • underlying decimals() matches the JSON
  • vault asset() matches the JSON's underlyingAddress

receiptAddress is not verified — older vault impls (cysFLR) don't expose receipt() as a getter. Follow-up tracked at #43.

Closes cyclofinance/cyclo.site#370 in part (decimals + asset linkage).

Test plan

  • Flare suite locally: 1 test, all entries (cysFLR / cyWETH / cyFXRP) pass on a public RPC
  • Arbitrum suite locally: same trie-state error the existing prod arbitrum tests hit on a non-archive RPC; CI's RPC_URL_ARBITRUM_FORK is archive-capable
  • CI green

🤖 Generated with Claude Code

thedavidmeister and others added 2 commits May 5, 2026 12:21
Two fork-test files (one per chain) that assert IERC20Metadata.decimals()
on every cyToken vault and its underlying ERC20 matches the value
hardcoded in cyclofinance/cyclo.site:src/lib/stores.ts. Drift in either
direction fails CI here, so PR review on either repo surfaces the
discrepancy.

Coverage:
  Flare: cysFLR (sFLR), cyWETH (Stargate WETH), cyFXRP (FAsset XRP)
  Arbitrum: cyWETH/cyWBTC/cycbBTC/cyLINK/cyDOT/cyUNI/cyPEPE/cyENA/cyARB/
    cywstETH/cyXAUt/cyPYTH (.pyth variants)

Local: Flare suite 6/6 pass on a public RPC. Arbitrum requires an
archive-capable RPC (the non-archive public RPC fails for the pinned
PROD_TEST_BLOCK_NUMBER_ARBITRUM with the same trie-state error the
existing Arbitrum prod tests get). CI RPC_URL_ARBITRUM_FORK is archive-
capable.

Closes cyclofinance/cyclo.site#370 (in part — covers the decimals
verification half; the broader on-chain-claim verification asset()/
receipt() linkage stays open under that issue).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the prior hardcoded decimals tests with a fixture-driven
approach. canonical/cyclo-site-tokens.json is the single source of
truth for every cyToken's vault address, decimals, underlying address,
underlying decimals, receipt address, and chain. cyclo.site is
expected to consume this same JSON in a follow-up PR; for now both
repos are kept in sync by hand and the foundry test guards the
on-chain claims for the values it can verify.

For every entry on the test's chain, the fork test now asserts:
  - vault decimals() matches the JSON
  - underlying decimals() matches the JSON
  - vault asset() matches the JSON's underlyingAddress

receiptAddress is left unverified — the older cysFLR vault
implementation doesn't expose receipt() as a getter. Tracked at #43.

Local: Flare suite passes on a public RPC. Arbitrum requires an
archive-capable RPC (same trie-state error the existing prod arbitrum
tests hit on a non-archive RPC); CI's RPC_URL_ARBITRUM_FORK is archive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@thedavidmeister thedavidmeister changed the title test: pin on-chain decimals against cyclo.site stores.ts claims test: pin cyclo-site-tokens.json claims on-chain via fork tests May 5, 2026
thedavidmeister and others added 3 commits May 5, 2026 12:52
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous pin (455000000) is outside the CI RPC's archive window;
all prod arbitrum suites fail at setUp() with "missing trie node /
state not available". Bump forward to a block the RPC can serve.

Verified locally at the new pin: 44/45 prod arbitrum tests pass. The
one regression is testProdCycloVaultCanMintArbitrum, a fuzz test that
panics with division-by-zero at calldata 0x3a99 (15001) — the vault
state at this block produces a zero in the mint preview's divisor.
This is state-sensitive and pre-exists my changes; flag to address
separately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pyth feeds aren't all fresh at the pinned block. Per-vault helper
mints if previewMint succeeds, otherwise pins the full revert chain:
the bare try/catch in `_nextId()` swallows `StalePrice()` and returns
id=0, which divides-by-zero in `_calculateMint`. Assert both ends —
the inner Pyth selector and the outer Panic(0x12) — so the test
documents the swallow site as well as passing deterministically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
thedavidmeister and others added 4 commits May 6, 2026 10:00
Public Arbitrum RPC keeps a bounded archive window. The previous pin
459580000 was outside that window and surfaced as universal trie-state
errors on CI. Pin to a recent block so CI's archive RPC has full state
for the tests to run against.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`testProdCycloVaultMintRevertsOnStalePythSwallowArbitrum` mocks the
WETH oracle's `price()` to revert with `StalePrice()`, then asserts
`previewMint` surfaces `Panic(0x12)`. Real vault, real `_nextId()`
try/catch — only the oracle revert is forced, so the test is
independent of which Pyth feeds happen to be fresh at the pinned
block.

Mutation-tested: changing the expected panic from 0x12 to 0x11 makes
the test fail with the exact mismatch shown by forge — confirming the
assertion is sensitive to div-by-zero specifically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hardcoded prices break every pin bump because the value depends on
the exact block. The public RPC has a bounded archive — older pins
get pruned, so we can't even probe historical prices to update
the constants. Switch to: oracle either returns a positive price
or reverts with `StalePrice()`. The test still confirms the oracle
is wired up and not in some unknown failure mode, while staying
robust across pin moves and Pyth feed timing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
At pin 459885576 only WBTC ($80830.6380941) and XAUT ($4626.78794311)
have fresh Pyth pushes; the other 10 feeds are older than the 1800s
stale threshold. Hardcode the fresh prices precisely and pin the stale
ones with `vm.expectRevert(StalePrice())`. Each oracle is still
exercised; the staleness pattern at this pin is captured deterministically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
thedavidmeister and others added 13 commits May 7, 2026 13:35
CI's archive RPC has a bounded window — pin 459885576 was pruned in the
hours since it was set, surfacing as universal trie-state errors across
the whole arbitrum suite. Bump pin to a fresh head-500. At this block
WBTC, UNI and XAUT are fresh (precise prices hardcoded); the other 9
feeds are stale (`vm.expectRevert(StalePrice)`).

This is a recurring-maintenance pattern until cyclo.sol#51 lands a
script to refresh prices automatically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously only decimals + asset linkage were verified. Add:
- vault.symbol() == JSON.symbol
- underlying.symbol() == JSON.underlyingSymbol
- JSON.name == JSON.symbol (the site uses `name` as a display label
  that mirrors `symbol`; on-chain `vault.name()` is a longer rendered
  string and is not directly comparable)

Closes more of cyclofinance/cyclo.site#370. Open: #43 (receiptAddress
linkage) plus the per-field issues to pull remaining JSON fields
on-chain (#44#50).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
assertOnChainMatchesJson now also checks:
- block.chainid matches the expected chainId (catches mistakenly
  forking to the wrong chain)
- entry.networkName matches the expected network name passed in
  by each per-chain test (Flare → "Flare", Arbitrum → "Arbitrum One")

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
JSON `name` was previously a UI display label that mirrored `symbol`.
Replace with the actual on-chain `vault.name()` value so the JSON is
canonical for both fields. The site can use `symbol` for display and
`name` for the long form (or both); the choice is now site-side.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously vaultAddress was only verified implicitly — getter calls on
it would have failed if it were zero or an EOA. Add explicit checks:
`vaultAddress != address(0)` and `vaultAddress.code.length > 0`. These
fire with a clear, address-specific error before the getter chain runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each test populates a `knownVaults` mapping in setUp from
`LibCycloProdVault.sol` constants. The test then asserts every JSON
entry's `vaultAddress` is in that set. A typo or stray address in
the JSON now fails loudly with a clear per-entry error.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Trailing-underscore parameter naming was avoiding shadowing the local
`vault` of type CycloVault. Drop the convention by renaming the
address-typed parameters to `vaultAddress`/`oracleAddress`; the typed
local stays as `vault`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cysFLR's older vault impl (PROD_FLARE_VAULT_IMPLEMENTATION_CYSFLR)
does not expose `receipt()` as a getter so it stays skipped (open at
cyclo.sol#43). Every other entry now has its `receiptAddress` pinned
against `vault.receipt()` on-chain.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cysFLR's older vault impl doesn't expose `receipt()` as a getter, but
the receipt itself exposes `manager()` which returns the vault. Add
`IReceiptV3(receipt).manager() == vault` as a universal check that
works for every entry, including cysFLR. The forward-direction
`vault.receipt()` check still runs for non-cysFLR entries on top.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lib (`assertOnChainMatchesJson`):
- no two entries on the same chain share the same vaultAddress or
  receiptAddress
- the receipt supports the ERC1155 interface (via type(IERC1155).interfaceId
  through ERC165 supportsInterface)

Per-chain test files:
- vaultAddress is a 1167 proxy to the expected vault impl with the
  expected CBOR-trimmed codehash (Arbitrum: V2 across the board;
  Flare: cysFLR special, cyWETH on V1, cyFXRP on V2)
- vault.priceOracle() matches the expected prod oracle constant per
  vault
- receiptAddress is a 1167 proxy to the expected receipt impl with the
  expected codehash (mirrors the per-vault impl mapping)

`active` is left unasserted — it is UI state with no on-chain notion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each per-chain test now declares which vaults are expected to be
active in setUp via `expectedActive[vault] = true`. The default
(uninitialised mapping) is `false`, so vaults not listed are
expected inactive. The loop asserts `entry.active ==
expectedActive[entry.vaultAddress]`, surfacing any drift between
the JSON and the test as a clear per-entry failure.

Currently active: cysFLR, cyWETH, cyFXRP.ftso (Flare); cyWETH.pyth,
cyWBTC.pyth, cyARB.pyth (Arbitrum). All others inactive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors the existing vaultAddress check: each of underlyingAddress
and receiptAddress is non-zero and has bytecode. Catches typos that
make an entry point at an EOA or zero before the getter chain runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Existing check is JSON ⊆ constants. Add the inverse: every prod
vault constant in LibCycloProdVault.sol must also have a matching
JSON entry, unless explicitly excluded. Catches the case where a
new vault constant lands but the JSON wasn't updated.

cyJOULE is intentionally excluded — its constant is kept for
historical bytecode tests but it is not listed on cyclo.site.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@thedavidmeister thedavidmeister force-pushed the 2026-05-05-cyclo-site-decimals-fork-test branch from 02c7743 to de8934f Compare May 9, 2026 14:14
Calling `initialize` again on every entry's vault and receipt must
revert with the canonical OZ string. Pass well-formed init data
(`CycloVaultConfig` for vaults, manager `address` for receipts) so
the OZ `initializer` modifier fires before any abi-decode in the
function body, regardless of which impl version (cysFLR / V1 / V2)
is behind the proxy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@thedavidmeister thedavidmeister force-pushed the 2026-05-05-cyclo-site-decimals-fork-test branch from de8934f to 78f355d Compare May 9, 2026 14:15
thedavidmeister and others added 2 commits May 9, 2026 18:17
Belt-and-braces JSON-internal pairing: the on-chain `vault.symbol()`
formula composes from the asset symbol, so the existing
`vault.symbol() == entry.symbol` and `underlying.symbol() ==
entry.underlyingSymbol` checks already enforce this transitively.
The explicit prefix check guards the JSON pairing directly so a
symbol/underlyingSymbol drift surfaces with a clear message.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Flare: exactly 3 entries (cysFLR, cyWETH, cyFXRP.ftso).
Arbitrum: exactly 12 entries (cyWETH.pyth, cyWSTETH.pyth, cyWBTC.pyth,
cyCBBTC.pyth, cyLINK.pyth, cyDOT.pyth, cyUNI.pyth, cyPEPE.pyth,
cyPYTH.pyth, cyENA.pyth, cyARB.pyth, cyXAUT.pyth).

Catches accidental JSON deletion or stray addition. The reverse
coverage check (every prod constant has a JSON entry) plus this count
together pin both directions: count = constants - exclusions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fork-test cyclo.site/stores.ts token config against on-chain contracts

1 participant