test: pin cyclo-site-tokens.json claims on-chain via fork tests#42
Open
thedavidmeister wants to merge 25 commits into
Open
test: pin cyclo-site-tokens.json claims on-chain via fork tests#42thedavidmeister wants to merge 25 commits into
thedavidmeister wants to merge 25 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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