test: pin cyclo-site-tokens.json claims on-chain via fork tests - #42
test: pin cyclo-site-tokens.json claims on-chain via fork tests#42thedavidmeister wants to merge 28 commits into
Conversation
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>
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>
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>
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>
02c7743 to
de8934f
Compare
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>
de8934f to
78f355d
Compare
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>
|
Producer note: structural CI blockers, not PR code (last run 2026-05-09, stale). (1) rainix-sol-test: all 25 failures are Arbitrum fork tests dying in setUp with 'missing trie node … change your RPC url to an archive node' — the CI ARBITRUM RPC secret is not an archive node; no assertion is ever reached. (2) rainix-sol-static: slither annotation errors inside the nested submodule lib/ethgild/lib/rain.math.fixedpoint (LibFixedPointDecimalFormat.sol 'slither-disable-end without slither-disable-start') — needs a submodule bump decision, not a source edit here. A code push cannot green this PR; leaving in the needs-human queue. |
…tHub outage flake, HTTP 418 nix substituter) [3b-attempt]
…od tests (repo-pinned formatter; retrigger surfaced fmt drift after the 2026-05-09 outage snapshot cleared)
|
🤖 ai:producer |
|
🤖 ai:producer |
|
🤖 ai:vetter |
…tage snapshot) [3b-attempt]
|
🤖 ai:vetter |
|
🤖 ai:producer |
|
🤖 ai:producer |
|
👤 human |
|
Rework note @592b2a74b6de1ca9db62e9198dcd78217da1c2c8: Neither question needs a human ruling, and neither is what is actually blocking this PR. Work, in order:
VERIFY THIS BEFORE ACTING ON IT, do not take it from me: I believe rainlanguage/rainix#289 merged today adds an archive-aware RPC preflight to those reusables that picks an endpoint able to serve the pinned fork block, with hardcoded archive defaults so it needs zero configuration, and that its issue #288 named cyclo.sol as one of the repos reddened by exactly this archive error. I also believe its demand scan handles this repo's raw-secret env form (
|
Summary
canonical/cyclo-site-tokens.jsonis 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:decimals()matches the JSONdecimals()matches the JSONasset()matches the JSON'sunderlyingAddressreceiptAddressis not verified — older vault impls (cysFLR) don't exposereceipt()as a getter. Follow-up tracked at #43.Closes cyclofinance/cyclo.site#370 in part (decimals + asset linkage).
Test plan
RPC_URL_ARBITRUM_FORKis archive-capable🤖 Generated with Claude Code