Refactor: Split Project Into Core Transport + Application Layer
Objective
Refactor defensive-sender-receiver-starter into two cleanly separated layers:
- Core (
src/core/) — A reusable, payload-agnostic cross-chain transport layer with defensive message handling. Knows nothing about vaults, ERC4626, deposits, or redemptions. - Application (
src/app/) — The vault-specific deposit/redeem logic that consumes the core. Owns all payload encoding/decoding via codecs.
The core should be extractable into its own package/repo in the future without any vault references.
Target File Structure
1src/
2├── core/ # Reusable cross-chain transport
3│ ├── CCIPBase.sol # MOVED from src/ccip/CCIPBase.sol (unchanged)
4│ ├── CCIPReceiverCore.sol # MOVED from src/ccip/CCIPReceiverCore.sol (unchanged)
5│ ├── interfaces/
6│ │ ├── ICCIPBase.sol # MOVED from src/interfaces/ICCIPBase.sol
7│ │ └── ICCIPDefensiveReceiver.sol # MOVED from src/interfaces/ICCIPDefensiveReceiver.sol
8│ └── libraries/
9│ ├── FailedMessageHandler.sol # MOVED from src/ccip/libraries/FailedMessageHandler.sol
10│ └── UniversalSender.sol # MOVED from src/ccip/libraries/UniversalSender.sol
11│
12├── app/ # Vault-specific application layer
13│ ├── CrossChainVaultDepositor.sol # MOVED from src/CrossChainVaultDepositor.sol
14│ ├── CrossChainVaultRedeemer.sol # MOVED from src/CrossChainVaultRedeemer.sol
15│ ├── CrossChainVaultDepositAndRedeemer.sol # MOVED from src/CrossChainVaultDepositAndRedeemer.sol
16│ ├── CCIPVaultBase.sol # MOVED from src/ccip/CCIPVaultBase.sol (this is app-layer, not core)
17│ └── libraries/
18│ ├── VaultDepositCodec.sol # NEW — extracted from inline decode in CrossChainVaultDepositor
19│ ├── VaultRedeemCodec.sol # NEW — extracted from inline decode in CrossChainVaultRedeemer
20│ ├── VaultDepositLib.sol # MOVED from src/libraries/VaultDepositLib.sol
21│ ├── VaultRedeemLib.sol # MOVED from src/libraries/VaultRedeemLib.sol
22│ ├── VaultErrors.sol # MOVED from src/libraries/VaultErrors.sol
23│ ├── VaultEvents.sol # MOVED from src/libraries/VaultEvents.sol
24│ └── VaultConfigStorage.sol # MOVED from src/libraries/VaultConfigStorage.sol
25│
26├── mocks/ # STAYS (unchanged location)
27│ ├── MockCCIPRouter.sol
28│ ├── EnhancedMockRouter.sol
29│ ├── MockCCTPTokenMinter.sol
30│ └── MockERC20Token.sol
31│
32└── vaults/ # STAYS (unchanged location)
33 ├── SimpleERC4626Vault.sol
34 └── README.mdAlso update:
1examples/
2├── CustomCrossChainVaultDepositor.sol # Update imports
3└── CustomCrossChainVaultRedeemer.sol # Update imports
4
5test/ # Update all imports across all test files
6
7script/ # Update all imports across all script filesStep-by-Step Instructions
Step 1: Create the directory structure
1mkdir -p src/core/interfaces
2mkdir -p src/core/libraries
3mkdir -p src/app/librariesStep 2: Move core files
Move these files. They should NOT change internally (no logic changes), only their location changes:
| From | To |
|---|---|
src/ccip/CCIPBase.sol | src/core/CCIPBase.sol |
src/ccip/CCIPReceiverCore.sol | src/core/CCIPReceiverCore.sol |
src/ccip/libraries/FailedMessageHandler.sol | src/core/libraries/FailedMessageHandler.sol |
src/ccip/libraries/UniversalSender.sol | src/core/libraries/UniversalSender.sol |
src/interfaces/ICCIPBase.sol | src/core/interfaces/ICCIPBase.sol |
src/interfaces/ICCIPDefensiveReceiver.sol | src/core/interfaces/ICCIPDefensiveReceiver.sol |
Step 3: Move application files
| From | To |
|---|---|
src/CrossChainVaultDepositor.sol | src/app/CrossChainVaultDepositor.sol |
src/CrossChainVaultRedeemer.sol | src/app/CrossChainVaultRedeemer.sol |
src/CrossChainVaultDepositAndRedeemer.sol | src/app/CrossChainVaultDepositAndRedeemer.sol |
src/ccip/CCIPVaultBase.sol | src/app/CCIPVaultBase.sol |
Why CCIPVaultBase.sol moves to app, not core: Open this file and inspect it. It contains vault-specific custom errors and events (references to vaults, deposits, shares, redemptions, etc.). Despite living in src/ccip/, it is application-layer knowledge — the core transport should have no concept of vaults. If CCIPVaultBase.sol contains any errors or events that are genuinely transport-level (not vault-specific), split those out into a separate file in src/core/ and keep only the vault-specific ones in src/app/CCIPVaultBase.sol. If everything in it is vault-specific, move the whole file as-is. Additionally, consider whether CCIPVaultBase.sol should be merged into the existing VaultErrors.sol and VaultEvents.sol files to reduce redundancy — if the errors and events in CCIPVaultBase.sol overlap with or complement what’s already in those files, consolidate them. Use your judgment, but do not lose any error or event definitions in the process.
| src/libraries/VaultDepositLib.sol | src/app/libraries/VaultDepositLib.sol |
| src/libraries/VaultRedeemLib.sol | src/app/libraries/VaultRedeemLib.sol |
| src/libraries/VaultErrors.sol | src/app/libraries/VaultErrors.sol |
| src/libraries/VaultEvents.sol | src/app/libraries/VaultEvents.sol |
| src/libraries/VaultConfigStorage.sol | src/app/libraries/VaultConfigStorage.sol |
Step 4: Create codec libraries
Find wherever message.data or the CCIP message payload is decoded inline in CrossChainVaultDepositor._processMessageMemory (and the sender-side encoding if any exists). Extract that into a dedicated library.
How to find the fields for each codec:
- Open
CrossChainVaultDepositor.soland find_processMessageMemory. - Search for where
message.data(or thedatabytes parameter) is decoded — look forabi.decode(...)calls on the message data. - The types and variable names inside that
abi.decodecall ARE the codec’sPayloadstruct fields. - If the decode is not a single
abi.decodebut is spread across multiple lines or helper functions, trace the full decode chain and collect all fields that are extracted from the raw bytes. - Do the same for
CrossChainVaultRedeemer.sol.
For example, if you find this inside _processMessageMemory:
1(address vault, address beneficiary, uint64 destChain, uint256 minShares) =
2 abi.decode(message.data, (address, address, uint64, uint256));Then the codec becomes:
1struct Payload {
2 address vault;
3 address beneficiary;
4 uint64 destinationChainSelector;
5 uint256 minSharesOut;
6}Create src/app/libraries/VaultDepositCodec.sol:
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.26;
3
4library VaultDepositCodec {
5 uint8 constant VERSION = 1;
6
7 struct Payload {
8 // POPULATE THIS: use the exact fields found in the abi.decode call
9 // inside CrossChainVaultDepositor._processMessageMemory
10 }
11
12 function encode(Payload memory p) internal pure returns (bytes memory) {
13 return abi.encodePacked(VERSION, abi.encode(
14 // all fields from Payload struct, in order
15 ));
16 }
17
18 function decode(bytes memory data) internal pure returns (Payload memory) {
19 uint8 version = uint8(data[0]);
20
21 if (version == 1) {
22 bytes memory body = new bytes(data.length - 1);
23 for (uint256 i = 0; i < body.length; i++) {
24 body[i] = data[i + 1];
25 }
26 // abi.decode body using the same types as the Payload struct
27 // return Payload(...)
28 }
29
30 revert("UnsupportedVersion");
31 }
32}Create src/app/libraries/VaultRedeemCodec.sol following the same process — extract from CrossChainVaultRedeemer._processMessageMemory.
Then update CrossChainVaultDepositor._processMessageMemory and CrossChainVaultRedeemer._processMessageMemory to use the codec:
1import {VaultDepositCodec} from "./libraries/VaultDepositCodec.sol";
2
3// Inside _processMessageMemory:
4VaultDepositCodec.Payload memory p = VaultDepositCodec.decode(message.data);
5// Then use p.vault, p.beneficiary, etc. instead of inline decodingIMPORTANT: Handling CrossChainVaultDepositAndRedeemer:
This contract handles both deposit and redeem messages in a single receiver. Open CrossChainVaultDepositAndRedeemer._processMessageMemory and determine how it currently differentiates between deposit vs redeem messages. There are three likely patterns:
- It checks a field in the decoded data (e.g., an enum or uint8 action type) — if so, formalize that as the first byte type tag in both codecs.
- It checks the token type or some other heuristic — if so, refactor to use an explicit type tag byte prefix instead, which is more robust.
- It always expects the same payload shape — unlikely given it does both deposit and redeem, but if so, create a single combined codec.
The target pattern for the combined contract should be:
1function _processMessageMemory(..., bytes memory data, ...) internal override {
2 uint8 messageType = uint8(data[0]);
3 bytes memory payload = _sliceBytes(data, 1); // everything after first byte
4
5 if (messageType == 0x01) {
6 VaultDepositCodec.Payload memory p = VaultDepositCodec.decode(payload);
7 _handleDeposit(p, tokenAmounts);
8 } else if (messageType == 0x02) {
9 VaultRedeemCodec.Payload memory p = VaultRedeemCodec.decode(payload);
10 _handleRedeem(p, tokenAmounts);
11 } else {
12 revert UnknownMessageType();
13 }
14}If the standalone CrossChainVaultDepositor and CrossChainVaultRedeemer only ever receive one message type each, they can skip the type tag prefix and decode directly. The type tag is only needed for the combined contract. In that case, the combined contract’s sender-side must prepend the type tag, while the standalone contracts’ senders do not. Document this difference clearly in both codec libraries with a comment explaining when the type tag byte is present vs absent.
Step 5: Update all import paths
Every file that imports from the old paths needs updating. This is the bulk of the work.
Pattern for core files (internal imports between core files):
1// Old:
2import {CCIPBase} from "./CCIPBase.sol";
3import {FailedMessageHandler} from "./libraries/FailedMessageHandler.sol";
4
5// New (within core, use relative paths):
6import {CCIPBase} from "./CCIPBase.sol"; // if same directory
7import {FailedMessageHandler} from "./libraries/FailedMessageHandler.sol"; // if in core/Pattern for application files importing core:
1// Old:
2import {CCIPReceiverCore} from "./ccip/CCIPReceiverCore.sol";
3import {CCIPVaultBase} from "./ccip/CCIPVaultBase.sol";
4import {FailedMessageHandler} from "./ccip/libraries/FailedMessageHandler.sol";
5import {UniversalSender} from "./ccip/libraries/UniversalSender.sol";
6import {ICCIPDefensiveReceiver} from "./interfaces/ICCIPDefensiveReceiver.sol";
7import {VaultDepositLib} from "./libraries/VaultDepositLib.sol";
8
9// New:
10import {CCIPReceiverCore} from "../core/CCIPReceiverCore.sol";
11import {CCIPVaultBase} from "./CCIPVaultBase.sol";
12import {FailedMessageHandler} from "../core/libraries/FailedMessageHandler.sol";
13import {UniversalSender} from "../core/libraries/UniversalSender.sol";
14import {ICCIPDefensiveReceiver} from "../core/interfaces/ICCIPDefensiveReceiver.sol";
15import {VaultDepositLib} from "./libraries/VaultDepositLib.sol";Pattern for test files:
1// Old:
2import {CrossChainVaultDepositor} from "../src/CrossChainVaultDepositor.sol";
3import {MockCCIPRouter} from "../src/mocks/MockCCIPRouter.sol";
4
5// New:
6import {CrossChainVaultDepositor} from "../src/app/CrossChainVaultDepositor.sol";
7import {MockCCIPRouter} from "../src/mocks/MockCCIPRouter.sol"; // mocks didn't movePattern for script files:
1// Old:
2import {CrossChainVaultDepositor} from "../src/CrossChainVaultDepositor.sol";
3
4// New:
5import {CrossChainVaultDepositor} from "../src/app/CrossChainVaultDepositor.sol";Pattern for example files:
1// Old:
2import {CrossChainVaultDepositor} from "../src/CrossChainVaultDepositor.sol";
3
4// New:
5import {CrossChainVaultDepositor} from "../src/app/CrossChainVaultDepositor.sol";Step 6: Update remappings.txt
Add remappings for the new structure if helpful. This is optional but can make imports cleaner:
1@core/=src/core/
2@app/=src/app/If you add these, imports become:
1import {CCIPReceiverCore} from "@core/CCIPReceiverCore.sol";
2import {VaultDepositCodec} from "@app/libraries/VaultDepositCodec.sol";Decision: Only add remappings if the existing project already uses them heavily. Otherwise relative paths are fine and more explicit.
Step 7: Delete old empty directories
After moving all files, remove:
1rm -rf src/ccip/
2rm -rf src/interfaces/
3rm -rf src/libraries/Verify no files remain in these directories before deleting.
Step 8: Verify compilation
1forge buildFix any remaining import path issues until compilation succeeds.
Step 9: Run tests
1forge testAll existing tests should pass with zero logic changes. This is purely a structural refactor.
Validation Rules (How to Know You Did It Right)
src/core/has ZERO references to: vault, ERC4626, deposit, redeem, shares, VaultDepositLib, VaultRedeemLib, VaultErrors, VaultEvents, VaultConfigStorage, CCIPVaultBase, or any codec. Rungrep -ri "vault\|erc4626\|deposit\|redeem\|shares" src/core/and confirm zero results (excluding generic variable names likeaddressthat happen to appear — use judgment).src/app/imports fromsrc/core/but never the reverse. Core never imports from app. Rungrep -r "from.*app/" src/core/and confirm zero results.- Codec struct fields match the original inline decode. Open the original
abi.decodecall you extracted from each_processMessageMemory. Verify every field is present in the codec’sPayloadstruct with the same types and same order. No fields should be lost or reordered. - Codec libraries exist at
src/app/libraries/VaultDepositCodec.solandsrc/app/libraries/VaultRedeemCodec.solwith bothencodeanddecodefunctions. CrossChainVaultDepositor._processMessageMemoryusesVaultDepositCodec.decode(data)instead of inlineabi.decode.CrossChainVaultRedeemer._processMessageMemoryusesVaultRedeemCodec.decode(data)instead of inlineabi.decode.CrossChainVaultDepositAndRedeemer._processMessageMemoryroutes between codecs using a type tag or the same mechanism it originally used to differentiate message types.CCIPVaultBase.sollives insrc/app/, NOT insrc/core/. Confirm it contains only vault-specific errors/events. If any transport-level errors/events were found and split out to core, confirm they exist in a separate file undersrc/core/.- No error or event definitions were lost during the
CCIPVaultBase.solmove. If it was merged intoVaultErrors.sol/VaultEvents.sol, diff the combined result against the originals to confirm nothing was dropped. forge buildsucceeds with no errors.forge testpasses all existing tests with no failures.- No files remain in the old
src/ccip/,src/interfaces/, orsrc/libraries/directories. Runfind src/ccip src/interfaces src/libraries -type f 2>/dev/nulland confirm no output.
What NOT to Change
- Do not modify any contract logic, storage layout, function signatures, or behavior. This is a file organization and import path refactor only (plus codec extraction).
- Do not rename any contracts, libraries, interfaces, or functions.
- Do not change foundry.toml beyond what is needed for the new paths (if anything — the default
src = "src"should still work). - Do not touch
lib/dependencies. - Do not modify mock contracts or vault contracts beyond updating their import paths.
- Do not change test assertions or test logic — only update import paths.
Success Criteria
After the structural refactor is complete and all existing tests pass, complete the following additional work. This is NOT optional — the refactor is not done until all of these are delivered.
1. Test Coverage
1a. Core Transport Layer Tests (test/core/)
Create a dedicated test suite for the core in isolation, proving it works without any vault logic. These tests must import ONLY from src/core/ and mocks — never from src/app/.
First, create a MockProcessMessage contract that inherits CCIPReceiverCore with a minimal _processMessageMemory implementation (just stores the arguments or emits an event) to test the core in isolation without vault logic.
CCIPReceiverCore tests:
- Message received from allowed sender/chain → calls
_processMessagevirtual hook with correct arguments - Message received from disallowed sender → reverts
- Message received from disallowed chain → reverts
- Message processing reverts → message stored in
FailedMessageHandlerwith correct messageId, payload, and reason - Message processing reverts →
MessageFailedevent emitted with correct data - Message processing succeeds →
MessageProcessedevent emitted - Retry of failed message → calls
_processMessageagain with original payload - Retry of failed message that succeeds → message marked resolved in
FailedMessageHandler - Retry of failed message that reverts again → message remains in failed state with updated reason
- Retry of non-existent messageId → reverts
- Double processing of same messageId → reverts (if applicable)
- Access control on retry → only authorized role can call
TokenRecoverable tests (if this abstract exists in core):
- Recover ERC20 tokens that are not actively in use → succeeds, tokens transferred to recipient
- Recover native ETH → succeeds
- Attempt to recover tokens that
_getRecoverableBalanceexcludes → reverts or recovers zero - Access control → only authorized role can call recovery
- Recovery of zero balance → handles gracefully
FailedMessageHandler tests:
- Store a failed message → retrievable by messageId
- Store multiple failed messages → all retrievable
- Pagination of failed messages → returns correct pages
- Mark message as resolved → no longer appears as unresolved
- Store message with maximum payload size → no truncation
UniversalSender tests:
- Send to EVM chain → correct CCIP message constructed
- Send to Solana chain → correct CCIP message constructed with SVM encoding
- Send with zero token amount → handles correctly (reverts or sends message-only)
- Fee estimation → returns correct fee for given message
CCIPBase tests:
- Constructor sets router correctly
supportsInterfacereturns true for expected interfaces- Access control roles are set correctly on deployment
1b. Codec Tests (test/app/)
VaultDepositCodec tests:
- Encode then decode roundtrip → all fields match original values
- Decode with version 1 → correct fields extracted
- Decode with unsupported version → reverts
- Decode with truncated data → reverts
- Decode with extra trailing bytes → handles gracefully (does not corrupt fields)
- Encode produces deterministic output → same input always gives same bytes
- Fuzz test: encode(decode(data)) roundtrip with random valid inputs
VaultRedeemCodec tests:
- Same categories as VaultDepositCodec above, adapted for redeem fields
Cross-language codec test (if frontend SDK exists):
- If
frontend/lib/contains TypeScript encoding logic, add a Foundry FFI test that:
- Calls a Node script to encode a payload in TypeScript
- Passes the hex output to a Solidity test that decodes it via the codec
- Asserts all fields match
1c. Integration Tests
- Full end-to-end: encode payload → send via mock CCIP router → receiver processes → vault deposit/redeem executes
- Failed message flow: encode payload → send → processing reverts → stored as failed → retry → succeeds
- Recovery flow: tokens stuck in receiver →
_getRecoverableBalancereturns correct amount → recovery succeeds
1d. Coverage Target
Run forge coverage and report the results. Target:
src/core/— 100% line coverage, 100% branch coveragesrc/app/libraries/*Codec.sol— 100% line coverage, 100% branch coveragesrc/app/contracts — at least match the existing coverage level (do not regress)
2. Security Analysis (Trail of Bits Style)
Produce a file docs/audit/POST_REFACTOR_SECURITY_REVIEW.md containing a security review of the core transport layer. Write this from the perspective of a Trail of Bits auditor — assume adversarial users, composability risks, and edge cases. This review should be rigorous enough that an external auditor reading it would say “they already caught the obvious stuff.”
2a. Threat Model
Define the threat model for the core:
- Trust boundaries: What is trusted (CCIP router, admin roles) vs untrusted (message payloads, source chains, senders)?
- Assets at risk: Tokens in transit, tokens in failed message recovery, tokens stuck in the contract
- Adversary capabilities: Malicious sender on allowed chain, compromised admin, front-running, griefing
2b. Attack Surface Review
For each contract in src/core/, enumerate:
- External/public functions — who can call them, what state they modify
- Access control correctness — are roles assigned correctly, can they be escalated
- Reentrancy vectors — which functions make external calls before state updates, is
ReentrancyGuardapplied correctly - DoS vectors — can an attacker cause
ccipReceiveto revert in a way that permanently blocks message processing? Can they grief the failed message store (e.g., fill storage)? - Token handling — are
SafeERC20methods used consistently? Are allowances reset after use? Are fee-on-transfer tokens handled? - Failed message recovery — can an attacker exploit the retry mechanism? Can they front-run a retry? Can recovery be used to drain tokens that aren’t actually stuck?
2c. Invariants
List the invariants that must always hold for the core. Examples to adapt based on actual implementation:
- Every CCIP message is either processed successfully OR stored in
FailedMessageHandler. None are silently dropped. - A failed message can only be retried by an authorized role.
- A failed message can only be resolved once (no double-spend on retry).
_getRecoverableBalancemust never return more than the contract’s actual token balance.- Token recovery cannot extract tokens that are mid-flight in an unresolved failed message.
- The core never decodes or inspects
message.data— it passes bytes through opaquely.
Write these as Foundry invariant tests in test/core/invariants/CoreInvariants.t.sol where practical.
2d. Known Risks & Mitigations
Document any known risks that consumers of the core should be aware of:
- What happens if the CCIP router is upgraded or compromised?
- What happens if an admin key is compromised?
- What are the gas limits for
ccipReceiveand what happens if processing exceeds them? - What is the maximum payload size and what happens if it’s exceeded?
- Are there any centralization risks in the access control model?
- How does the contract behave under chain reorgs?
- What are the trust assumptions consumers inherit by using this core?
2e. Codec-Specific Security
- Can a malformed payload cause the codec to return incorrect but non-reverting results?
- Are there any ABI decoding edge cases with the version byte prefix (e.g., data that’s exactly 1 byte long)?
- If the combined contract uses a type tag, can an attacker send a deposit message to a redeem handler or vice versa?
- Can payload versioning be exploited by replaying old-version messages after an upgrade?
3. Reusability & Consumer Experience Analysis
Produce a file docs/CORE_CONSUMER_GUIDE.md that serves as the documentation someone would read when importing the core into a new project. This is the “developer experience” deliverable — the thing that makes the core actually useful as a standalone package.
3a. Quick Start
Show the minimal code needed to build a new application on top of the core:
1// Minimal example: a new receiver that just logs messages
2import {CCIPReceiverCore} from "@core/CCIPReceiverCore.sol";
3
4contract MyReceiver is CCIPReceiverCore {
5 constructor(address router) CCIPReceiverCore(router) {}
6
7 function _processMessageMemory(
8 // ... exact parameters from the actual virtual function
9 ) internal override {
10 // Your application logic here
11 }
12}Fill in the exact function signatures from the actual code — do not use placeholder parameters. A developer should be able to copy-paste this and have it compile.
3b. API Surface Documentation
For every virtual or override hook in the core that a consumer touches, document:
- Function signature with parameter descriptions
- When it’s called in the message lifecycle
- What the consumer must do in their implementation
- What the consumer must NOT do (e.g., “do not make external calls that could revert here unless you want the message to go to failed state”)
- Return value expectations if any
- Gas considerations — is there a gas ceiling the implementation should stay under?
3c. Integration Checklist
Provide a checklist a developer follows when building a new receiver:
- Inherit
CCIPReceiverCore - Implement
_processMessageMemorywith your application logic - Override
_getRecoverableBalanceto protect tokens that are actively in use - Override
recoverFailedTokensBackToSenderif default behavior needs customization - Define your codec library with
encodeanddecodefunctions - Add version byte to your codec from day one
- Configure allowed senders and source chains
- Set appropriate gas limits for your
_processMessageMemorylogic - Write roundtrip codec tests
- Write failed message + retry tests
- Write token recovery tests that verify active balances are protected
Adapt this list based on the actual core API — add or remove items as needed after inspecting the virtual hooks available.
3d. Anti-Patterns
Document what NOT to do when building on the core:
- Do not decode
message.datainside the core or modifyCCIPReceiverCore - Do not override
ccipReceiveor_ccipReceive— override_processMessageMemoryonly - Do not store application state in the core contracts’ storage slots
- Do not assume message ordering — CCIP does not guarantee order
- Do not put user-facing recovery logic in the core — that belongs in the application layer
- Do not use
tx.originor rely onmsg.senderinside_processMessageMemory— the caller is alwaysCCIPReceiverCore
Expand this list based on actual footguns discovered during the refactor.
3e. Separation Proof
Add a test file test/core/SeparationProof.t.sol that proves the core works without the application layer:
- Deploy
MockProcessMessage(the minimal mock receiver from section 1a) with no vault logic - Send a mock CCIP message through it
- Verify processing, failure handling, retry, and recovery all work
- This test file must NOT import anything from
src/app/
This test is the definitive proof that the core is independently deployable and functional.
3f. Package Extraction Readiness
Verify that src/core/ could be copied to a standalone repository and function independently:
- All imports within core are relative (no references to files outside
src/core/) - The only external dependencies are OpenZeppelin and Chainlink CCIP (no application-level deps)
- No hardcoded addresses, chain selectors, or environment-specific values in the core
- All configuration is passed via constructor or admin functions, nothing is baked in
Document the exact foundry.toml and remappings.txt that a standalone core repo would need.
4. Deliverables Checklist
When complete, the following files must exist and be non-empty:
New test files:
-
test/core/CCIPReceiverCoreTest.t.sol -
test/core/FailedMessageHandlerTest.t.sol -
test/core/UniversalSenderTest.t.sol -
test/core/CCIPBaseTest.t.sol -
test/core/TokenRecoverableTest.t.sol(if applicable based on whether this abstract exists) -
test/core/SeparationProof.t.sol -
test/core/invariants/CoreInvariants.t.sol -
test/app/VaultDepositCodecTest.t.sol -
test/app/VaultRedeemCodecTest.t.sol
New codec files:
-
src/app/libraries/VaultDepositCodec.sol -
src/app/libraries/VaultRedeemCodec.sol
New documentation:
-
docs/audit/POST_REFACTOR_SECURITY_REVIEW.md -
docs/CORE_CONSUMER_GUIDE.md
New mock (for core testing):
-
src/mocks/MockProcessMessage.sol(minimal CCIPReceiverCore implementation for isolation testing)
Updated files (imports only):
- All files in
test/compile and pass - All files in
script/compile - All files in
examples/compile
Final verification commands — run all of these, all must succeed:
1# Build
2forge build
3
4# All tests pass
5forge test -vvv
6
7# No core→app dependency
8grep -r "from.*app/" src/core/ && echo "FAIL: core imports app" || echo "PASS"
9
10# No vault references in core
11grep -ri "vault\|erc4626\|deposit\|redeem\|shares" src/core/ && echo "FAIL: vault refs in core" || echo "PASS"
12
13# Separation proof test passes in isolation
14forge test --match-path test/core/SeparationProof.t.sol -vvv
15
16# Coverage report
17forge coverage
18
19# Old directories cleaned up
20find src/ccip src/interfaces src/libraries -type f 2>/dev/null && echo "FAIL: old dirs not cleaned" || echo "PASS"