MDX Limo
Refactor: Split Project Into Core Transport + Application Layer

Refactor: Split Project Into Core Transport + Application Layer

Objective

Refactor defensive-sender-receiver-starter into two cleanly separated layers:

  1. Core (src/core/) — A reusable, payload-agnostic cross-chain transport layer with defensive message handling. Knows nothing about vaults, ERC4626, deposits, or redemptions.
  2. 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 1112├── 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 2526├── mocks/ # STAYS (unchanged location) 27│ ├── MockCCIPRouter.sol 28│ ├── EnhancedMockRouter.sol 29│ ├── MockCCTPTokenMinter.sol 30│ └── MockERC20Token.sol 3132└── vaults/ # STAYS (unchanged location) 33 ├── SimpleERC4626Vault.sol 34 └── README.md

Also 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 files

Step-by-Step Instructions

Step 1: Create the directory structure

1mkdir -p src/core/interfaces 2mkdir -p src/core/libraries 3mkdir -p src/app/libraries

Step 2: Move core files

Move these files. They should NOT change internally (no logic changes), only their location changes:

FromTo
src/ccip/CCIPBase.solsrc/core/CCIPBase.sol
src/ccip/CCIPReceiverCore.solsrc/core/CCIPReceiverCore.sol
src/ccip/libraries/FailedMessageHandler.solsrc/core/libraries/FailedMessageHandler.sol
src/ccip/libraries/UniversalSender.solsrc/core/libraries/UniversalSender.sol
src/interfaces/ICCIPBase.solsrc/core/interfaces/ICCIPBase.sol
src/interfaces/ICCIPDefensiveReceiver.solsrc/core/interfaces/ICCIPDefensiveReceiver.sol

Step 3: Move application files

FromTo
src/CrossChainVaultDepositor.solsrc/app/CrossChainVaultDepositor.sol
src/CrossChainVaultRedeemer.solsrc/app/CrossChainVaultRedeemer.sol
src/CrossChainVaultDepositAndRedeemer.solsrc/app/CrossChainVaultDepositAndRedeemer.sol
src/ccip/CCIPVaultBase.solsrc/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:

  1. Open CrossChainVaultDepositor.sol and find _processMessageMemory.
  2. Search for where message.data (or the data bytes parameter) is decoded — look for abi.decode(...) calls on the message data.
  3. The types and variable names inside that abi.decode call ARE the codec’s Payload struct fields.
  4. If the decode is not a single abi.decode but is spread across multiple lines or helper functions, trace the full decode chain and collect all fields that are extracted from the raw bytes.
  5. 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 decoding

IMPORTANT: 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:

  1. 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.
  2. 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.
  3. 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 move

Pattern 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 build

Fix any remaining import path issues until compilation succeeds.

Step 9: Run tests

1forge test

All existing tests should pass with zero logic changes. This is purely a structural refactor.


Validation Rules (How to Know You Did It Right)

  1. src/core/ has ZERO references to: vault, ERC4626, deposit, redeem, shares, VaultDepositLib, VaultRedeemLib, VaultErrors, VaultEvents, VaultConfigStorage, CCIPVaultBase, or any codec. Run grep -ri "vault\|erc4626\|deposit\|redeem\|shares" src/core/ and confirm zero results (excluding generic variable names like address that happen to appear — use judgment).
  2. src/app/ imports from src/core/ but never the reverse. Core never imports from app. Run grep -r "from.*app/" src/core/ and confirm zero results.
  3. Codec struct fields match the original inline decode. Open the original abi.decode call you extracted from each _processMessageMemory. Verify every field is present in the codec’s Payload struct with the same types and same order. No fields should be lost or reordered.
  4. Codec libraries exist at src/app/libraries/VaultDepositCodec.sol and src/app/libraries/VaultRedeemCodec.sol with both encode and decode functions.
  5. CrossChainVaultDepositor._processMessageMemory uses VaultDepositCodec.decode(data) instead of inline abi.decode.
  6. CrossChainVaultRedeemer._processMessageMemory uses VaultRedeemCodec.decode(data) instead of inline abi.decode.
  7. CrossChainVaultDepositAndRedeemer._processMessageMemory routes between codecs using a type tag or the same mechanism it originally used to differentiate message types.
  8. CCIPVaultBase.sol lives in src/app/, NOT in src/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 under src/core/.
  9. No error or event definitions were lost during the CCIPVaultBase.sol move. If it was merged into VaultErrors.sol/VaultEvents.sol, diff the combined result against the originals to confirm nothing was dropped.
  10. forge build succeeds with no errors.
  11. forge test passes all existing tests with no failures.
  12. No files remain in the old src/ccip/, src/interfaces/, or src/libraries/ directories. Run find src/ccip src/interfaces src/libraries -type f 2>/dev/null and 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 _processMessage virtual hook with correct arguments
  • Message received from disallowed sender → reverts
  • Message received from disallowed chain → reverts
  • Message processing reverts → message stored in FailedMessageHandler with correct messageId, payload, and reason
  • Message processing reverts → MessageFailed event emitted with correct data
  • Message processing succeeds → MessageProcessed event emitted
  • Retry of failed message → calls _processMessage again 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 _getRecoverableBalance excludes → 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
  • supportsInterface returns 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:
  1. Calls a Node script to encode a payload in TypeScript
  2. Passes the hex output to a Solidity test that decodes it via the codec
  3. 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 → _getRecoverableBalance returns correct amount → recovery succeeds

1d. Coverage Target

Run forge coverage and report the results. Target:

  • src/core/ — 100% line coverage, 100% branch coverage
  • src/app/libraries/*Codec.sol — 100% line coverage, 100% branch coverage
  • src/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 ReentrancyGuard applied correctly
  • DoS vectors — can an attacker cause ccipReceive to revert in a way that permanently blocks message processing? Can they grief the failed message store (e.g., fill storage)?
  • Token handling — are SafeERC20 methods 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).
  • _getRecoverableBalance must 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 ccipReceive and 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 _processMessageMemory with your application logic
  • Override _getRecoverableBalance to protect tokens that are actively in use
  • Override recoverFailedTokensBackToSender if default behavior needs customization
  • Define your codec library with encode and decode functions
  • Add version byte to your codec from day one
  • Configure allowed senders and source chains
  • Set appropriate gas limits for your _processMessageMemory logic
  • 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.data inside the core or modify CCIPReceiverCore
  • Do not override ccipReceive or _ccipReceive — override _processMessageMemory only
  • 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.origin or rely on msg.sender inside _processMessageMemory — the caller is always CCIPReceiverCore

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"
Refactor: Split Project Into Core Transport + Application Layer | MDX Limo