Skip to content

test: pin every flare fork test at block 37796420 - #218

Merged
thedavidmeister merged 4 commits into
mainfrom
2026-10-07-flare-fork-block-37796420
Oct 7, 2026
Merged

thedavidmeister merged 4 commits into
mainfrom
2026-10-07-flare-fork-block-37796420

Conversation

@thedavidmeister

Copy link
Copy Markdown
Collaborator

test/fork/ForkConstants.sol's BLOCK_NUMBER moves 31843105 -> 37796420, which
is the value FLRETH_BLOCK_NUMBER already had. The suite now forks flare at a
single block, and nothing in it reads account state ~23 months back.

That matters outside this repo. rainix's rpc_preflight network table pins
flare's archive_blocks to 31_000_000 because of this repo's old block — its
own comment says so: "rain.flare forks at 31_843_105 in ~19 test files (~23
months back) — the deepest historical read in the org outside rain.deploy's
genesis search, and Flare public nodes are the most prune-happy." The
RPC_URL_FLARE_FORK candidates are not archive that deep. Once this merges that
entry can come forward, as its own rainix PR after this one: probing shallower
than a live pin is exactly what the field exists to prevent, so the order is not
optional. The rest of the org was re-checked against default branches before
opening this: every vm.createSelectFork with a flare block pin is in this
repo; rain.tofu.erc20-decimals forks flare with createSelectFork(network)
and no block, i.e. at latest; rainlanguage/flow only plumbs FLARE_RPC_URL
through a workflow; and raindex's RAINDEX_START_BLOCK_FLARE is 63011168, far
ahead of 37796420.

What changed

FtsoRegistry, FtsoV2 and FeeCalculator resolve to identical addresses at
both blocks, so no plumbing changed. Only the values they serve did, and every
block-relative expectation is re-derived from chain at 37796420 — not from
whatever the code produced:

31843105 37796420
ETH/USD 2525.74849 2729.116
BTC/USD 67694.11308 98162.81
XRP/USD 0.53163 2.6189
USDT/USD 0.99919 0.99997
sFLR per FLR 0.877817288626455057 0.775006975937778042
FtsoV2 ETH/USD (LTS, wei) 2522.575 2729.116
ETH/USD feed timestamp 1729795768 1740156916

The four pair expectations (ETH/BTC, BTC/ETH, ETH/FLR, FLR/ETH) are the
quotients of those same chain reads; they carry full coefficients because the
quotients do not terminate. FLR/USD at the new block is 0.02039, which is what
makes ETH/FLR 133845.80676802354.

testFtsoV2LTSGetFeedPaid was not a stale literal

It warps forward by the governance timelock to enact the fee change, then read
the feed with a staleness timeout of a bare 3600 — so the timelock consumed
the entire freshness budget, leaving nothing for the gap between the feed's
timestamp and the fork block's own timestamp.

At 31843105 that gap was exactly zero: block.timestamp == 1729795768 == the
feed timestamp. That is the only reason the test ever passed. At 37796420 the
block is one second after the feed update — cast block 37796420 -f timestamp
is 1740156917 against a feed timestamp of 1740156916 — so
block.timestamp + timelock > feedTimestamp + 3600 and the read reverts
StalePrice(1740156916, 3600) before any fee is charged.

The fix makes the dependency explicit rather than re-pinning it: the timeout is
now timelock + 3600, where timelock is the value the test already reads from
GovernanceSettings.getTimelock() on chain, so nothing new is hardcoded and a
future timelock change cannot re-break it the same way. What the test measures
is the fee behaviour it was written to measure: underpayment reverts with no
revert data (OutOfFunds), exact payment returns the feed value, overpayment is
stranded in the consumer. Nothing was weakened, nothing deleted, and the
staleness boundary itself is still pinned by the four dedicated boundary tests
(M04/M05 below), which keep their own exact 3600 and 0 timeouts.

Housekeeping

The first two commits here are #217 — forge lint -D warnings and slither .
both being red on main for reasons unrelated to the fork block (a view
keyword, ten scoped forge-lint disables, and then the slither suppressions those
disables displaced). They are merged in so this branch's own static job reports
on the fork change instead of on #217's breakage; they drop out of this diff the
moment #217 merges, and this PR is still self-contained if it does not. Review
#217 there, not here.

QA

  • Discriminating tests: no new tests — this PR re-derives the values existing
    fork tests already assert, and each assertion is shown discriminating by the
    matrix below. Verified failing on base, re-run here rather than quoted:
    holding this branch's new literals at the old block — BLOCK_NUMBER back to
    31843105, nothing else touched — gives forge test exit 1 and
    72 tests passed, 13 failed, across 8 distinct test names
    (testFlareFtsoWordsFtsoCurrentExchangeRateHappyFork,
    testFlareFtsoWordsFtsoCurrentPricePairHappyFork,
    testFlareFtsoWordsFtsoCurrentPricePairHappyPrecisionFork,
    testFlareFtsoWordsFtsoCurrentPriceUsdHappyFork, testFtsoV2LTSGetFeed,
    testGetSFLRPerFLR18, testRunCurrentPricePairForkHappy,
    testRunForkCurrentPriceHappy). So every value this PR moves is load-bearing
    for at least one assertion. testFtsoV2LTSGetFeedPaid was the one that
    failed with StalePrice(1740156916, 3600) before the timeout fix and now
    passes. Measured on c325fb5 in the
    devshell at the CI rainix pin, exit statuses read unpiped: forge test exit
    0, Ran 16 test suites: 85 tests passed, 0 failed, 0 skipped — the same
    count as the baseline at the old block. CI on c325fb5: rainix-sol and
    Git is clean both green.

  • Mutations applied (mutation-probe on c325fb5 against
    .mutation-test/mutants.toml, exit 0, baseline green at 85 passed, 8
    mutants, 8 killed / 0 survived / 0 no-run / 0 harness errors). Killer
    names below are transcribed verbatim from that run; the tool reports up to
    five [FAIL] matches per mutant, so a list is the first five it saw and not
    a complete killer set, and a name repeated in a list is repeated in the
    tool's own output:

    mutant line -> mutation killed by (verbatim)
    M01 LibFtsoCurrentPriceUsd.sol return (price, decimals) -> (price + 1, decimals) testBytecodeHashMatchesDeployedCode, testFlareFtsoWordsFtsoCurrentPriceUsdHappyFork, testFlareFtsoWordsFtsoCurrentPricePairHappyFork, testFlareFtsoWordsFtsoCurrentPricePairHappyPrecisionFork, testRunCurrentPricePairForkHappy
    M02 LibOpFtsoCurrentPriceUsd.sol uint8(decimals) -> uint8(decimals) + 1 testBytecodeHashMatchesDeployedCode, testIntegrityPointers, testOpcodePointers, testSubParserOperandHandlers, testFlareFtsoWordsFtsoCurrentPricePairHappyFork
    M03 LibFtsoV2LTS.sol return value -> return value + 1 testFtsoV2LTSGetFeed, testFtsoV2LTSGetFeedPaid, testFtsoV2LTSGetFeed, testFtsoV2LTSGetFeedPaid
    M04 LibFtsoV2LTS.sol staleness > -> >= testFtsoV2LTSGetFeedExactTimeoutNotStale, testFtsoV2LTSGetFeedTimeoutZeroExactNotStale, testFtsoV2LTSGetFeedExactTimeoutNotStale, testFtsoV2LTSGetFeedTimeoutZeroExactNotStale
    M05 LibFtsoV2LTS.sol feed timestamp += 1 testFtsoV2LTSGetFeedJustStale, testFtsoV2LTSGetFeedStale, testFtsoV2LTSGetFeedTimeoutZeroStale, testFtsoV2LTSGetFeedJustStale, testFtsoV2LTSGetFeedStale
    M06 LibSceptreStakedFlare.sol return rate -> return rate + 1 testBytecodeHashMatchesDeployedCode, testGetSFLRPerFLR18, testFlareFtsoWordsFtsoCurrentExchangeRateHappyFork, testBytecodeHashMatchesDeployedCode, testFlareFtsoWordsFtsoCurrentExchangeRateHappyFork
    M07 LibOpFtsoCurrentPricePair.sol numerator.div(denominator) -> denominator.div(numerator) testBytecodeHashMatchesDeployedCode, testFlareFtsoWordsFtsoCurrentPricePairHappyFork, testFlareFtsoWordsFtsoCurrentPricePairHappyPrecisionFork, testRunCurrentPricePairForkHappy, testRunPairDerivationExact
    M08 LibFtsoCurrentPriceUsd.sol staleness > -> >= testBytecodeHashMatchesDeployedCode, testRunFtsoNotActiveA, testRunDecimalOverflow, testRunDecimalsBoundary, testRunHappy

    M04 and M05 are the ones that matter for the paid-fee fix: the staleness
    boundary and the exact feed timestamp are still pinned after the timeout
    change, by tests that did not change. testBytecodeHashMatchesDeployedCode
    appears in five of the eight lists and is kept here rather than filtered out,
    because filtering it would be editing the tool's output: it asserts
    address(FlareFtsoWords).codehash == BYTECODE_HASH, so it fires on any src/
    edit that moves bytecode and is not the discriminating killer for any of
    these behaviours. M03/M04/M05 are killed only by the LTS tests, with no
    bytecode-hash contribution at all.

  • Oracle: the chain at block 37796420, read independently of the libraries
    under test. The value table above was derived in the commit that changed it
    (FlareContractRegistry.getContractAddressByName ->
    FtsoRegistry.getFtsoBySymbol(sym) -> IFtso.getCurrentPriceWithDecimals()
    for ETH/BTC/XRP/USDT/FLR, all 5 decimals; FtsoV2.getFeedByIdInWei(ETH/USD)
    for the LTS value and its timestamp; IStakedFlr.getSharesByPooledFlr(1e18)
    for the sFLR rate; pair values checked as quotients of those reads rather
    than read back out of the Float), and every one of those literals is
    re-confirmed by the 85-test green run above, since each is an assertEq
    against a live read. The one number the fix itself turns on — the fork
    block's own timestamp, 1740156917, one second past the feed's 1740156916 —
    was re-read here directly with cast block 37796420 -f timestamp. The
    timelock is not asserted as a literal anywhere: the test reads it from
    GovernanceSettings.getTimelock().

  • Category check: the ask was (a) one fork block for rain.flare, (b) re-derive
    every block-relative expectation from chain, (c) fix the paid-fee failure
    without weakening a test; covered a, b, c. Not included and deliberately
    separate: the rainix archive_blocks move, which must follow this merge, and
    fix: get forge lint -D warnings green again #217, which is carried but reviewed on its own PR. No audit/
    mutation-test-scans.json entry is added — this was a re-run of an existing
    ledger for this PR's evidence, not a new scan, and the repo has no scan
    record to append to.

🤖 Generated with Claude Code

thedavidmeister and others added 4 commits October 7, 2026 15:34
`BLOCK_NUMBER` moves 31843105 -> 37796420, which is already
`FLRETH_BLOCK_NUMBER`, so the whole suite forks flare at one block and
nothing reads state ~23 months back any more. `FtsoRegistry`, `FtsoV2` and
`FeeCalculator` resolve to the same addresses at both blocks; only the
values they serve changed, so every block-relative expectation is
re-derived from chain at 37796420:

  ETH/USD    2525.74849  -> 2729.116
  BTC/USD   67694.11308  -> 98162.81
  XRP/USD       0.53163  -> 2.6189
  USDT/USD      0.99919  -> 0.99997
  sFLR/FLR   0.877817288626455057 -> 0.775006975937778042
  FtsoV2 ETH/USD LTS feed 2522.575 -> 2729.116, timestamp
  1729795768 -> 1740156916

`testFtsoV2LTSGetFeedPaid` needed more than a new literal. It warps
forward by the governance timelock (3600s) to enact the fee change, then
reads the feed with a 3600s staleness timeout — so the timelock consumed
the entire freshness budget and left nothing for the gap between the feed
timestamp and the fork block's own timestamp. At 31843105 that gap was
exactly zero (block.timestamp == 1729795768 == the feed timestamp), which
is the only reason it ever passed; at 37796420 the block is one second
after the feed update and the read reverts StalePrice by that one second,
before any fee is charged. The timeout now covers the timelock it warps
through, which is what the test always meant.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The rainix pin the sol workflows run moved on 2026-09-29; rain.flare last
pushed to main on 2026-09-14, so nothing has exercised the static job
since and `forge lint -D warnings` is red on main independently of any
open branch. Two separate causes, and the first hides the second.

solc aborts first. `forge lint` only sees the files slither's build
skipped, so in CI it compiles test/ fresh and `-D warnings` denies solc's
own diagnostics: two `Function state mutability can be restricted to
view` on the flrETH expectRevert wrappers. Locally they never appear,
because a previous `forge build` already cached those artifacts — the
only faithful local reproduction is `forge clean` first.

With those fixed solc passes and the ten forge-lint findings behind it
become reachable, each getting the scoped disable plus its reason that
the workflow's own comment asks for rather than a wholesale silence:

  block-timestamp  x2  comparing against block.timestamp IS the staleness
                       check these two libraries implement
  unsafe-typecast  x6  bytes32("<word name>") on ASCII literals far under
                       32 bytes, and the literal is what the test asserts
  unsafe-typecast  x2  bytes21() over a 21-byte allocation, and bytes1()
                       whose whole job is to keep the leading byte

No behavior changes: 85 passed / 0 failed, and `forge clean` followed by
`forge lint -D warnings` is now silent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`slither .` exits 255 on this branch and the 255 is this branch's doing.
`SlitherCore.has_ignore_comment` reads exactly `min(lines) - 1` for a finding,
so `//slither-disable-next-line timestamp` only suppresses while it is the line
immediately above the flagged code. Two of the ten scoped
`//forge-lint: disable-next-line(block-timestamp)` comments the previous commit
added landed between an existing slither comment and its `if`, pushing the
slither comment to `min(lines) - 2` and silently unsuppressing both `timestamp`
findings. Two results under the default `--fail-on pedantic` is `sys.exit(-1)`.

Slither's region form carries no adjacency requirement — `has_ignore_comment`
matches on `start < lines[0] and end > lines[-1]` — so both sites take
`//slither-disable-start timestamp` / `//slither-disable-end timestamp` around
the `if` and leave the forge-lint comment where forge lint needs it. That is
the shape `src/interface/IDineroFlrEth.sol` already uses for the same
collision. No detector is excluded and `slither.config.json` is untouched.

The two `ERROR:Slither:` lines on the same CI run are an older, separate bug
that pre-dates both branches. `parse_ignore_comments` does
`end_match[0].split(",")`, so a bare `//slither-disable-end` asks for the range
of a check literally named `""`, which no `-start` ever opened: it logs
"slither-disable-end without slither-disable-start", returns, and leaves
`naming-convention` open from line 6 to infinity, after which the second pass
over the file logs "Consecutive slither-disable-starts". Hence the end comment
now names its detector. Closing the range surfaces nothing new, because
`tokensPerLST` is already valid mixedCase; only `LSTPerToken` ever needed it.

Unpiped exits in the devshell at the CI pin, after `slither .`'s own
`forge clean` so `forge lint` compiles test/ fresh the way CI does:

  slither .                   0   (0 result(s) found, no ERROR lines)
  forge fmt --check           0
  forge lint -D warnings      0
  pre-commit --all-files      0
  rainix-sol-single-contract  0
  comment-loc-cap             0   (src 1631 against a cap of 1874)

What separates the two causes: with only the `-end` detector name fixed and the
two timestamp sites left as they were, `slither .` is still 255 with the same 2
results and no ERROR lines at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…k change

#217 fixes `forge lint -D warnings` and `slither .` being red on `main` for
reasons unrelated to the fork block. Carried here so the static job on this
branch reports on the fork change rather than on #217's breakage; it drops out
of this diff the moment #217 merges.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 7, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 49 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: d75e354a-ba88-4095-b686-d9e76ac2e7f9
📥 Commits

Reviewing files that changed from the base of the PR and between ff883aa and c325fb5.

📒 Files selected for processing (15)
  • src/interface/IDineroFlrEth.sol
  • src/lib/lts/LibFtsoV2LTS.sol
  • src/lib/price/LibFtsoCurrentPriceUsd.sol
  • test/fork/ForkConstants.sol
  • test/src/concrete/FlareFtsoWords.ftsoCurrentPricePair.t.sol
  • test/src/concrete/FlareFtsoWords.ftsoCurrentPriceUsd.t.sol
  • test/src/concrete/FlareFtsoWords.pointers.t.sol
  • test/src/concrete/FlareFtsoWords.sflrCurrentExchangeRate.t.sol
  • test/src/concrete/FlareFtsoWords.wordOpcodeAlignment.t.sol
  • test/src/lib/flreth/LibDineroFlrEth.t.sol
  • test/src/lib/lts/LibFtsoV2LTS.t.sol
  • test/src/lib/lts/LibFtsoV2LTSFeedIds.t.sol
  • test/src/lib/op/LibOpFtsoCurrentPricePair.t.sol
  • test/src/lib/op/LibOpFtsoCurrentPriceUsd.t.sol
  • test/src/lib/sflr/LibSceptreStakedFlare.t.sol
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@thedavidmeister
thedavidmeister merged commit 7a394aa into main Oct 7, 2026
5 of 7 checks passed
@linear

linear Bot commented Oct 7, 2026

Copy link
Copy Markdown

RAI-2983

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.

1 participant