Skip to content

feat: support optional spell execution in fork validation - #827

Closed
wischli wants to merge 8 commits into
livefrom
live_refactor-fork-validation
Closed

wischli wants to merge 8 commits into
livefrom
live_refactor-fork-validation

Conversation

@wischli

@wischli wischli commented May 26, 2026 •

Copy link
Copy Markdown
Contributor

Please note all staged V2Cleanings*.sol will be removed before merging this PR such that only these commits remain:

  1. 8a9527e
  2. 32893cf

Product requirements

  • Apply the SpellForkTest pre/post validation framework to V2CleaningsSpellTest so the spell cast is verified against the live-state baseline.
  • Expose the remaining hardcoded addresses in V2CleaningsSpell as named constants with source references so the spell script and test can use them without re-declaring addresses locally.
  • Feature creep: Fix a missing ETH-to-Pharos Chainlink adapter connection in the mainnet connections config.

Design notes

  • ValidationExecutor gains a runValidationCountErrors method: runs validators without reverting and returns the total error count. SpellForkTest uses this to snapshot the pre-cast error count and assert it did not increase post-cast — tolerating pre-existing live errors and only failing on regressions the spell introduces.
  • SpellForkTest is a new abstract base (in test/integration/spell/utils/) that spell fork tests inherit instead of Test. It calls _castSpell (child-defined) between a pre/post validator snapshot and an investment-flow diff. _customPostAssertions is an optional hook for spell-specific checks.
  • V2CleaningsSpellTest is migrated from Test to SpellForkTest: the old flat _testCase is replaced by _castSpell + _customPostAssertions; balance snapshots (_preTreasuryUsdc, etc.) are captured in instance variables during _castSpell and consumed in _customPostAssertions.
  • Spell CI is scoped to run only on the target networks (ETH, Base, Arbitrum) instead of all networks.

How to validate the V2 Cleanings Spell

You can check the V2Cleanings Fork CI checks in the Github Runner.

But to save you time, here are the important details. [PRE-EXISTING] means the failure was there both before AND after the spell ran. It is not gone post-cast; the spell simply didn't introduce or fix it.

Here's an excerpt:

Ethereum

The vault warnings also happen pre-validation, so no new issue. They are tracked here.

  ================================================================
       POST-CAST VALIDATION RESULTS
  ================================================================

  [PASS] RootPermissions
  [PASS] ContractWards
  [PASS] FileConfigurations
  [PASS] Endorsements
  [PASS] GuardianSafes
  [PASS] AdapterConfigurations
  [WARNING] Vaults
     Errors: 2

     Error #1:
     - Message:  0xF48256AbDDf96EcDDc4B3DbD23E8C1921f9761Ae not set as manager in BalanceSheet
     - Field:    balanceSheet.manager
     - Value:    0x13b51F42fFF4a4a6837b1af2e689672bF6d9c81a
     - Expected: true
     - Actual:   false

     Error #2:
     - Message:  PoolEscrow holding has reserved > total
     - Field:    escrow.holding
     - Value:    0xa81b3f5766F638B92498C32eBaF1126b08287952
     - Expected: total >= reserved
     - Actual:   total=29718690809720793978 reserved=79324528370958690288

  [PASS] HookPoolEscrow
  ================================================================
                           SUMMARY
  ================================================================
  Total Validators: 8
  Passed:           7
  Failed:           1
  Total Errors:     2
  ================================================================
  VALIDATING: 0x13b51F42fFF4a4a6837b1af2e689672bF6d9c81a [arkTest03] (Async)
  VALIDATING: 0x18Ab9fC0B2e4Fef9e0e03c8EC63BA287a3238257 [DeFi Janus Henderson Anemoy Treasury Fund Token] (Async)
  VALIDATING: 0x1AD3644A0834e7c9eD4aEc2660b0Ee2eA18A1f36 [DeFi Janus Henderson Anemoy Treasury Fund Token] (Async)
  VALIDATING: 0x314d8AEb02bB5f6b86D2Ac1feF4c5Fc1771e6817 [District Token] (Async)
  VALIDATING: 0x381f4F3B43C30B78C1f7777553236e57bB8AE9ff [Janus Henderson Anemoy Treasury Fund] (Async)
  VALIDATING: 0x4865BC9701fBD1207A7B50e2aF442C7DAf154c9c [DeFi Janus Henderson Anemoy AAA CLO Fund Token] (Async)
  VALIDATING: 0x4880799eE5200fC58DA299e965df644fBf46780B [Janus Henderson Anemoy AAA CLO Fund Token] (Async)
  VALIDATING: 0x559907981ed375b2D7eEa6108273D181216A10CC [DeFi Janus Henderson Anemoy AAA CLO Fund Token] (Async)
  VALIDATING: 0x74A739EA1Dc67c5a0179ebad665D1D3c4b80B712 [Anemoy Tokenized Apollo Diversified Credit Fund Token] (Async)
  VALIDATING: 0x82991B72f44Aca321be781B133Ab482423bE9E36 [DeFi Anemoy Tokenized Diversified Credit Fund Token] (SyncDepositAsyncRedeem)
  VALIDATING: 0xa81b3f5766F638B92498C32eBaF1126b08287952 [Apex Reserve] (Async)
  VALIDATING: 0xDdC268B70052683A2CaecC5D425C1f4351F11896 [Just Real Return Token] (Async)
  VALIDATING: 0xFE6920eB6C421f1179cA8c8d4170530CDBdfd77A [Janus Henderson Anemoy Treasury Fund] (Async)

  ================================================================
       INVESTMENT FLOW DIFF (pre-cast vs post-cast)
  ================================================================
  [PRE-EXISTING deposit] 0x13b51F42fFF4a4a6837b1af2e689672bF6d9c81a -> Custom error: 0xea8e4eb5
  [PRE-EXISTING redeem]  0x13b51F42fFF4a4a6837b1af2e689672bF6d9c81a -> Custom error: 0xea8e4eb5
  [PRE-EXISTING deposit] 0x1AD3644A0834e7c9eD4aEc2660b0Ee2eA18A1f36 -> Custom error: 0xc0fc8a8a
  [PRE-EXISTING redeem]  0x1AD3644A0834e7c9eD4aEc2660b0Ee2eA18A1f36 -> Custom error: 0xc0fc8a8a
  [PRE-EXISTING deposit] 0x559907981ed375b2D7eEa6108273D181216A10CC -> Custom error: 0xc0fc8a8a
  [PRE-EXISTING redeem]  0x559907981ed375b2D7eEa6108273D181216A10CC -> Custom error: 0xc0fc8a8a
  ================================================================
  V2Cleanings post-assertions: ethereum

Base

  ================================================================
       POST-CAST VALIDATION RESULTS
  ================================================================

  [PASS] RootPermissions
  [PASS] ContractWards
  [PASS] FileConfigurations
  [PASS] Endorsements
  [PASS] GuardianSafes
  [PASS] AdapterConfigurations
  [PASS] Vaults
  [PASS] HookPoolEscrow
  ================================================================
                           SUMMARY
  ================================================================
  Total Validators: 8
  Passed:           8
  Failed:           0
  Total Errors:     0

  ================================================================
       INVESTMENT FLOW DIFF (pre-cast vs post-cast)
  ================================================================
  ================================================================

Arbitrum

  ================================================================
       POST-CAST VALIDATION RESULTS
  ================================================================

  [PASS] RootPermissions
  [PASS] ContractWards
  [PASS] FileConfigurations
  [PASS] Endorsements
  [PASS] GuardianSafes
  [PASS] AdapterConfigurations
  [PASS] Vaults
  [PASS] HookPoolEscrow
  ================================================================
                           SUMMARY
  ================================================================
  Total Validators: 8
  Passed:           8
  Failed:           0
  Total Errors:     0
  ================================================================
  VALIDATING: 0x04FFdBd63626942D5CaBf12120805465B7A17547 [Janus Henderson Anemoy Treasury Fund] (Async)
  VALIDATING: 0x3a7d08B8B8E22251eFb112868A1640DF2FDBCb83 [DeFi Janus Henderson Anemoy Treasury Fund Token] (Async)
  VALIDATING: 0x3ADa03eb5F298052d00C9E71fAAC7E21ea99E1CE [Arkonix Proving Ground] (Async)
  VALIDATING: 0x3D5d48b342666205cDb8662c6107d6A5133Fad65 [Arkonix Test Vault] (Async)
  VALIDATING: 0x4f64092b28c64Bd316376C5735e08E96018DA911 [Arkonix Playground] (Async)
  VALIDATING: 0x6d9A159a1EA29F2a5EBE825f30a59AbbeFB74a82 [Emplifai Vault] (Async)
  VALIDATING: 0x74da710Aade13B0A4Bc961c2A76C166ffE12e067 [Arkonix Conservative] (Async)
  VALIDATING: 0x837200badC7174Da395d9F4B4854713620F198B0 [HalalCryptoCapital] (Async)
  VALIDATING: 0x8d36811409Ce921f89776B72159Df29b61e1B8Ff [Arkonix Aggressive] (Async)
  VALIDATING: 0xBcdEF6F4465B5a3e9122946a352D164Da7612354 [Janus Henderson Anemoy AAA CLO Fund Token] (Async)
  VALIDATING: 0xE16722b52fE814f5D551a12F0EC89574A130A0B7 [L'Investisseur Musulman] (Async)
  VALIDATING: 0xe897E7F16e8F4ed568A62955b17744bCB3207d6E [DeFi Janus Henderson Anemoy AAA CLO Fund Token] (Async)
  VALIDATING: 0xEaE33E9F53fC405F834d4678E6c07A2523b2126E [Odins Reserve] (Async)

  ================================================================
       INVESTMENT FLOW DIFF (pre-cast vs post-cast)
  ================================================================
  [PRE-EXISTING deposit] 0x4f64092b28c64Bd316376C5735e08E96018DA911 -> Custom error: 0x9c9d8823
  [PRE-EXISTING redeem]  0x4f64092b28c64Bd316376C5735e08E96018DA911 -> Custom error: 0x9c9d8823
  ================================================================
  V2Cleanings post-assertions: arbitrum

@github-actions github-actions Bot added audit:needed Requires audit review scope:contracts Modifies deployed or deployable contract code scope:env Deployment addresses and network environment configs scope:scripts Deploy or operational scripts that transact onchain scope:tests Test-only changes (fuzz, invariant, unit, fork) labels May 26, 2026
@github-actions

Copy link
Copy Markdown

Auto-labeled: scope:contracts, scope:tests, scope:scripts, scope:env, audit:needed. Please verify and add type/version labels manually.

@wischli wischli removed audit:needed Requires audit review scope:scripts Deploy or operational scripts that transact onchain labels May 26, 2026
@lemunozm

Copy link
Copy Markdown
Contributor

Amazing!

Just as NOTE, we'll need to move later the live branch to the internal repo, so makes sure we merge this commit first.

To be care: we're adding main content in this PR that should be done on live branch. I think we should only add the validator part avoiding fix: add missing ETH<>pharos Chainlink connection basically

@wischli

wischli commented May 26, 2026

Copy link
Copy Markdown
Contributor Author

Amazing!

Just as NOTE, we'll need to move later the live branch to the internal repo, so makes sure we merge this commit first.

Yeah I assumed it was intended to be missing for a less complicated repo restructure.

To be care: we're adding main content in this PR that should be done on live branch. I think we should only add the validator part avoiding fix: add missing ETH<>pharos Chainlink connection basically

Why omit the connections commit? It represents the live condition and without it, the fork validation correctly logs issues for the ETH<>Pharos lane to a mismatch in live to local configuration of the adapter setup. I rather regard this as an oversight we missed to apply to live branch.

@lemunozm

Copy link
Copy Markdown
Contributor

Why omit the connections commit? It represents the live condition and without it, the fork validation correctly logs issues for the ETH<>Pharos lane to a mismatch in live to local configuration of the adapter setup. I rather regard this as an oversight we missed to apply to live branch.

Agree it should be reachable from this branch, but not in this PR. The ideal workflow should be:

  1. Rebase live branch to take the ETH<>Pharos addition => we keep that content from a clean history where it was merged
  2. Add this PR (which will be merged as a single commit), containing only spell related things, and not bundle with already merged work in main

@lemunozm lemunozm left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm re-reviewing this, and not sure if we're mixing architectures with tests and validators.

Your current set up extends the spell test with validators. It runs the validators from the test.

My last thought about the validators were that they could be fully split from tests. I think one thing is "test the spell itself" and another thing is "validate the environment after the spell".

In the Validator example the idea was the following:

// test/integration/spell/example-spell/ExampleTest.t.sol
contract ExampleTest {
    string chainName = "ethereum";
    BaseValidator[] pre;
    BaseValidator[] cache;
    BaseValidator[] post;

    constructor() {
        // Add pre validators:
        pre.push(new Validate_PreExample());

        // Add cache validators:
        cache.push(new Validate_CacheExample());

        // Add post validators:
        post.push(new Validate_PostExample());
    }

    function testExample() external {
        ValidationExecutor executor = new ValidationExecutor(chainName, "example");
        executor.runPreValidation(pre, false);
        executor.runCacheValidation(cache);

        // Here goes the spell

        executor.runPostValidation(post, testContractsFromConfig(Env.load(chainName)));
        // You can also use testContractsFromConfig(fullDeployer)
    }
}

In this case, and in my head (open to listen your thoughts), I would leave the V2Cleanings.t.sol as they were, just focus on tests. And I would add a new ValidatorTest like the above, just calling the spell execution.

Wondering also if you found some blocker with this schema

@wischli

wischli commented May 28, 2026

Copy link
Copy Markdown
Contributor Author

@lemunozm You're right. I rushed in order to prio quick validatio. The current shape conflates the two and that's the right thing to pull apart.

Three things shaped the current PR that I don't think your sketch covers as-stated, so I'd want to fold them in rather than just split the test:

  1. Regression tolerance. Live mainnet has pre-existing structural validator errors on ETH today (BalanceSheet manager gap on a test vault, PoolEscrow reserved > total). runPostValidation(post, latest) hard-fails on any error, so we'd be red on day 1 blaming a spell that didn't touch those things. My runValidationCountErrors in this PR is a workaround for the same problem. The clean shape IMO would be something like ValidationExecutor.runValidationCapturing(validators) returning ValidationResult[] without reverting, plus runValidationDiffPost(validators, baseline) that hard-fails only on errors present post that weren't present pre.

  2. Per-vault investment flow regression. This is the part I'm least sure how to fit into the validator framework. It's not really a state invariant, it's the per-vault end-to-end behavioral diff that runs InvestmentFlowExecutor pre-cast and post-cast and compares results vault-by-vault. Wrapping it as a BaseValidator works on paper but the cache would have to encode InvestmentFlowResult[] per vault, which feels off. I'd lean on a small FlowRegression helper library (snapshot + assertNoRegressions) used from the test orchestrator rather than the validator framework.

  3. runPostValidation(validators, TestContracts memory latest) expects newly-deployed addresses. Migration spells like V2CleaningsSpell don't deploy fresh core contracts, so latest is either the same as live (misuse of the API) or we add a migration-aware post mode. The runValidationDiffPost from point 1 sidesteps this because it doesn't need latest.

IMO, the rough plan building on your sketch would be:

  • Revert V2Cleanings.t.sol to its original spell-functional form (per your suggestion).
  • Add V2CleaningsValidatorTest.t.sol + Validate_PreV2Cleanings.sol + Validate_PostV2Cleanings.sol as the validator-based env regression prototype. Spell-specific pre/post pairs live here, structured as ValidationErrors rather than loose assertEqs.
  • Extend ValidationExecutor with runValidationCapturing and runValidationDiffPost.
  • Add FlowRegression.sol helper for the per-vault flow diff (used from the test orchestrator, not as a validator).
  • Rename SpellForkTest to SpellRegressionTest, scoped to env regression only. Thin orchestrator around the framework methods + the flow helper, no more spell-specific hooks.

Future spells then become a spell test file (focused, like today) + validator test file (thin orchestrator using the new framework methods + spell-specific pre/post validators). For demonstration/reference purposes, I would migrate V2Cleanings to the new pattern here. WDYT?

@lemunozm

lemunozm commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Thanks for showing this points! True we can iterate forward this scheme.

I agree with your proposal.

Just regarding point 1. I think we can impl runValidationCountErrors as a call to _execute() if we modify _execute() to return the error count instead of a boolean.

Regarding point 2. Also unsure right now how to fit this, so let's iterate with your thoughts.

Regarding point 3. I think you just need to pass the current contracts already exists (testContractsFromConfig(Env.load(chainName))) or just an empty struct. Basically, a validator that validates a spell that does no add any new contract should never read ctx.latest because makes no sense in this context where no new contracts were added.

@github-actions github-actions Bot added audit:needed Requires audit review scope:scripts Deploy or operational scripts that transact onchain labels Jun 3, 2026
@wischli

wischli commented Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @lemunozm, all implemented. V2Cleanings is migrated to the new pattern and green on eth/base/arb (focused spell test + validator regression test), ExampleTest unchanged and green.

I think we can impl runValidationCountErrors as a call to _execute() if we modify _execute() to return the error count

With the per-error diff in, runValidationCountErrors is gone entirely, so nothing consumes an error count anymore (the phase reports already log Total Errors). The DRY part is in tho: all run methods now share a single _run core.

One deviation from what I sketched: returning ValidationResult[] across an external call trips stack-too-deep with optimizer_runs=1 (and we don't want via_ir). So the baseline never crosses an ABI boundary. captureErrorBaseline(validators) stores serialized error-identity keys (keccak256(name|field|value), actual excluded since e.g. balances drift) in the executor's own storage, and runValidationDiffPost(validators) diffs against it on the same executor instance. The pre-existing balanceSheet.manager error is reported as pre-existing, which is correct.

For point 2, it ended up as a FlowRegression mixin that both SpellRegressionTest and InvestmentFlowForkTest inherit. Same codegen reason (i.e. passing EnvConfig and the result arrays externally blows the stack), and it removes the _queryVaults duplication on the way.

a validator that validates a spell that does no add any new contract should never read ctx.latest

Agree. Added a runPostValidation(validators) overload without latest that leaves ctx.contracts.latest as the zero struct, so a validator reading it fails loudly instead of silently aliasing live. The two-arg overload stays for spells that actually deploy contracts.

Also added an author guide for the pattern to the validation README. The 009 Plume adapter spell could be the second consumer (_runInvestmentFlowsDiff() = false since it does not touch vaults).

WDYT on the executor-storage baseline shape?

├── V2CleaningsValidatorTest.t.sol # extends SpellRegressionTest
└── validators/
└── Validate_Example.sol # Example: PRE query, cache, POST read
└── Validate_V2Cleanings.sol # Pre (soft) / Cache / Post (hard) validators

@lemunozm lemunozm Jun 3, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

NIT. Maybe we don't need to maintain the current set up here. Initially this was to show how to organize things, but does not need a real representation. we can just write:

└── <other>/

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah, definitely can be removed now as it was just a showcase that I want to ref to Claude when doing the next spell post spell fork assertion.

# Spell Validation Framework

Validates Centrifuge protocol state before and after executing governance spells. Queries the GraphQL indexer to ensure no blocking operations exist (PRE) and that state is correctly preserved (POST).
Validates Centrifuge protocol state before and after executing governance spells. Queries the GraphQL indexer to ensure no blocking operations exist (PRE) and that state is correctly preserved (POST), and provides a spell-agnostic **environment regression** harness (`SpellRegressionTest`) that tolerates pre-existing live errors and fails only on regressions a spell introduces.

@lemunozm lemunozm Jun 3, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think the changes in this README and base validators should be done in main instead. Only validators that really use the current ABI should be in live, WDYT?

In my mind everything under utils should be generic enough to be in main, and everything under v2-cleanings should be in live.

Wondering if FlowRegression should also be in a subfolder like v2-cleanings. Looks like this is an actual validator in some way. At least it's attached to some ABI, IIUC. EDIT: My bad, it's ABI independent

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agree with the split in principle, generic framework in main, ABI-bound validators in live.

However, none of it exists on main today, test/integration/spell/utils and test/integration/fork are live-only. So this is less "move this PR's README and base validator delta over" and more "port the whole framework", i.e. BaseValidator, ValidationExecutor, TestContracts, InvestmentFlowExecutor, example-spell and the README.

Two ABI couplings to solve in that port. SpellRegressionTest's default _structuralValidators() imports the 8 fork validators, which by your own criterion stay in live, so on main that default would need to become abstract with live providing the set. And TestContracts plus InvestmentFlowExecutor compile against whatever src they sit on, on main they would track the dev ABI while spells always run against the deployed one, so we would pay for keeping them compiling on both branches.

I'm not sure what the preferred order is TBH

  1. Either we merge this PR without any V2Cleanings content against live, then rebase live against main and cherry-pick relevant validators from live to main
  2. Or we push validation infra to main and the actual fork validators to live after rebasing to main.

The second one is cleaner probably but requires more work.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

However, none of it exists on main today, test/integration/spell/utils and test/integration/fork are live-only

AFAICT, test/integration/spell/utils already exists in main. At least the validator infrastructure part.

No strong opinion about the preferred order at all! What is simple for you. As far as we get things correctly placed I'm fine with both solutions 😄

@lemunozm

lemunozm commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

WDYT on the executor-storage baseline shape?

Everything from your last comment looks good to me! Just I don't really follow what do you refer with this, can you expand?

@wischli

wischli commented Jun 4, 2026

Copy link
Copy Markdown
Contributor Author

WDYT on the executor-storage baseline shape?

Everything from your last comment looks good to me! Just I don't really follow what do you refer with this, can you expand?

Sorry, should have expanded on that. "Executor-storage baseline" is how the structural validator baseline survives the spell cast.

The regression diff needs the pre-cast errors available post-cast. The natural API (capture returns ValidationResult[], post takes it back as an argument) doesn't compile here: ABI-coding that nested struct array across an external call hits stack-too-deep with optimizer_runs=1, and we don't want via_ir. So the test creates one ValidationExecutor pre-cast and reuses the same instance post-cast, with the baseline living in the executor's own storage as a compact string of error identity keys, keccak256(name|field|value) per error:

exec = new ValidationExecutor(network, "v2cleanings");
exec.captureErrorBaseline(validators);       // pre-cast: runs validators, stores their error keys in exec storage
spell.cast();
exec.runValidationDiffPost(freshValidators); // post-cast: re-runs, diffs against the stored keys

Post-cast errors whose key is in the baseline are PRE-EXISTING (tolerated), new keys are REGRESSION (test fails), baseline keys that disappeared are IMPROVED. Nothing nested ever crosses an ABI boundary and nothing is stored to the disk. The spell-specific cache validators still use the file-backed CacheStore as before, this only concerns the structural baseline.

The question was just whether you are fine with that shape vs e.g. file-caching the baseline too. Happy to adjust if you prefer the cache route, but IMO storage is the simpler one since it needs no key management and no cleanup.

@lemunozm

lemunozm commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Ok, I think now understand! Yeah, I think the storage is good. The downside will be that we need to handle it in the infrastructure, right? But I think this is ok. It's just another utility for validator implementators.

Maybe something to consider. What if for the spell we want to test it in an anvil fork? In that scenario spell.cast() will be in a separated environment and we'll need anyways to use the file-catching the stuff. We had similar issues in the v3.1 migration with anvil, because the tx are called from outside

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown

Coverage after merging live_refactor-fork-validation into live will be

98.46%

Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
src/adapters
   AxelarAdapter.sol97.67%90%100%100%118
   ChainlinkAdapter.sol98.11%91.67%100%100%95
   LayerZeroAdapter.sol98.41%90%100%100%124
   RecoveryAdapter.sol100%100%100%100%
   WormholeAdapter.sol97.67%90%100%100%108
src/admin
   OpsGuardian.sol100%100%100%100%
   ProtocolGuardian.sol100%100%100%100%
   Root.sol100%100%100%100%
   TokenRecoverer.sol100%100%100%100%
src/core/hub
   Accounting.sol97.92%96%100%98.31%134, 137
   Holdings.sol97.71%88.89%100%100%118, 243, 82
   Hub.sol93.98%81.13%93.02%97.54%269, 287, 305, 339, 343, 374–375, 413, 498–499, 544, 549, 586, 601, 87
   HubHandler.sol98%90.91%100%100%71
   HubRegistry.sol92.39%76.67%100%100%118, 124, 130, 35, 46, 79, 99
   ShareClassManager.sol98.81%95.45%100%100%42
src/core/libraries
   PricingLib.sol100%100%100%100%
src/core/messaging
   GasService.sol96.55%100%87.50%96.20%106, 135, 145
   Gateway.sol100%100%100%100%
   MessageDispatcher.sol99.60%98.55%100%100%733
   MessageProcessor.sol82.42%60.26%100%99.01%101, 103, 109, 112, 114, 125, 130, 139, 142, 145, 157, 160, 163, 168, 178, 181, 190, 193, 196, 209, 220, 225, 228, 232, 83–84, 87–88, 91–92, 97, 99
   MultiAdapter.sol100%100%100%100%
src/core/messaging/libraries
   MessageLib.sol100%100%100%100%
src/core/spoke
   BalanceSheet.sol97.47%96.97%92.59%98.55%291, 348, 57
   PoolEscrow.sol100%100%100%100%
   ShareToken.sol92.41%60%94.44%98.04%100, 112, 144, 146, 32
   Spoke.sol97.46%89.71%100%100%132, 132–133, 133, 135, 92–93
   VaultRegistry.sol93.88%85.71%100%98.18%54, 60–62, 98–99
src/core/spoke/factories
   PoolEscrowFactory.sol100%100%100%100%
   TokenFactory.sol100%100%100%100%
src/core/utils
   BatchedMulticall.sol100%100%100%100%
   ContractUpdater.sol100%100%100%100%
src/deployment
   ActionBatchers.sol84.80%62.50%80%86.46%438, 440–441, 443, 443–444, 446, 448, 448–449, 453, 453–454, 456, 459, 459–460, 462, 465, 465–466, 469, 472, 472, 474, 476, 508–512, 517–518, 520–521, 523–524
src/hooks
   BaseTransferHook.sol100%100%100%100%
   FreelyTransferable.sol92.31%80%100%100%37
   FreezeOnly.sol100%100%100%100%
   FullRestrictions.sol95.24%88.89%100%100%47
   RedemptionRestrictions.sol85.71%50%100%100%37
src/hooks/libraries
   UpdateRestrictionMessageLib.sol90%50%100%100%40, 61, 82
src/managers/hub
   NAVManager.sol100%100%100%100%
   SimplePriceManager.sol100%100%100%100%
src/managers/spoke
   MerkleProofManager.sol97.01%87.50%100%100%103, 110
   OnOfframpManager.sol100%100%100%100%
   QueueManager.sol100%100%100%100%
src/managers/spoke/decoders
   BaseDecoder.sol100%100%100%100%
   CircleDecoder.sol83.33%100%100%75%22
   VaultDecoder.sol100%100%100%100%
src/misc
   Auth.sol100%100%100%100%
   ERC20.sol100%100%100%100%
   Escrow.sol56.25%33.33%100%66.67%17, 19, 23–24, 24, 24, 26
   Multicall.sol91.67%66.67%100%100%19
   Recoverable.sol100%100%100%100%
   ReentrancyProtection.sol90%75%100%100%24
src/misc/libraries
   ArrayLib.sol100%100%100%100%
   BitmapLib.sol100%100%100%100%
   BytesLib.sol90.27%56%100%100%109, 120, 131, 14, 142, 153, 16, 164, 175, 186, 87
   CastLib.sol95.24%66.67%100%100%10, 34
   EIP712Lib.sol100%100%100%100%
   ExcessivelySafeCallLib.sol100%100%100%100%
   MathLib.sol93.46%76.19%100%97.33%34–35, 44, 46, 48, 50, 52
   MerkleProofLib.sol100%100%100%100%
   SafeTransferLib.sol96.97%92.86%100%100%75
   SignatureLib.sol95.24%80%100%100%17
   StringLib.sol100%100%100%100%
   TransientArrayLib.sol100%100%100%100%
   TransientBytesLib.sol100%100%100%100%
   TransientStorageLib.sol100%100%100%100%
src/utils
   RefundEscrow.sol100%100%100%100%
   RefundEscrowFactory.sol100%100%100%100%
   SubsidyManager.sol100%100%100%100%
src/valuations
   IdentityValuation.sol100%100%100%100%
   OracleValuation.sol100%100%100%100%
src/vaults
   AsyncRequestManager.sol96.85%86.36%100%99.64%195, 198, 201, 204, 215, 227, 295, 328, 455, 460, 505, 573, 580
   AsyncVault.sol96.25%83.33%95%98.15%146, 47
   BaseVaults.sol93.50%80.77%95.24%95.45%124, 137, 239, 309–310, 85–86, 86, 86–88
   BatchRequestManager.sol100%100%100%100%
   SyncDepositVault.sol100%100%100%100%
   SyncManager.sol98.58%92.59%100%100%67, 72
   VaultRouter.sol87.37%58.82%100%91.53%107, 107, 109–110, 110, 112, 149, 70, 73–74, 86–87
src/vaults/factories
   AsyncVaultFactory.sol93.75%50%100%100%32
   SyncDepositVaultFactory.sol95%50%100%100%40
src/vaults/libraries
   RequestCallbackMessageLib.sol89.58%50%100%100%104, 139, 38, 57, 77
   RequestMessageLib.sol89.74%50%100%100%37, 55, 72, 89

@wischli

wischli commented Jun 8, 2026

Copy link
Copy Markdown
Contributor Author

Ok, I think now understand! Yeah, I think the storage is good. The downside will be that we need to handle it in the infrastructure, right? But I think this is ok. It's just another utility for validator implementators.

Maybe something to consider. What if for the spell we want to test it in an anvil fork? In that scenario spell.cast() will be in a separated environment and we'll need anyways to use the file-catching the stuff. We had similar issues in the v3.1 migration with anvil, because the tx are called from outside

Good catch, that's the right scenario to poke at 😅

For the in-process forge test we have today, there's no infra to handle. The executor is created, writes its baseline, and gets diffed all in one forge test run with spell.cast() pranked in the process. There, storage survives because it never leaves the process IMO.

It's true your anvil case is different and you're right there. If cast() is a broadcast tx and capture/diff are separate forge invocations, the executor's in-memory storage from the capture run is gone in the diff run, unless we broadcast the executor onto the anvil node and carry its address. Worth noting the spell-specific pre/cache/post validators already persist through CacheStore via vm.writeFile, so those would already work. It's only the structural baseline (storage) and the pre-cast flow results that are in-process right now.

Though IMO moving to that model is more than the baseline. _snapshotFlows and the flow diff lean on vm.snapshotState, which doesn't map to a real anvil either, so the whole flow-regression layer would need rework, not just swapping storage for a file. So I'd keep storage for the in-process test now as it's the simplest thing and it's working. The file-cache baseline variant already exists from an earlier cut, so if we go the anvil-broadcast route we flip baseline plus preFlows onto CacheStore (same layer the spell validators already use) and it should be a small change. IMO, let's merge this and worry about it once we hit such a migration.

@lemunozm

lemunozm commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Seems fair to me!

@wischli

wischli commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Closing because outdated

@wischli wischli closed this Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

audit:needed Requires audit review scope:contracts Modifies deployed or deployable contract code scope:env Deployment addresses and network environment configs scope:scripts Deploy or operational scripts that transact onchain scope:tests Test-only changes (fuzz, invariant, unit, fork)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants