Status (as of Stage 3 close + follow-up security pass): Phase 1 on-chain protocol is complete and internally audited. Escrow, EIP-712 hashing, cross-chain LayerZero V2 messaging, solver auction, and timeout-based recovery are all implemented and tested. The follow-up pass moved match validation (token + chain + amount) onto the destination chain so the matcher cannot bypass user minDestAmount / destToken (R-16), added the missing CONFIRM-leg source-EID guard (R-17), and segregated user ETH escrow from operator pre-fund so LayerZero fees can never debit user funds (R-18). Backend (Stage 4), frontend wiring (Stage 5), and testnet deployment (Stage 8) follow.
See also: Repository README · Architecture · MVP specification · Stage 1 audit · Stage 2 audit · Stage 3 audit · Stage 3 final review
| File | Status | Notes |
|---|---|---|
ChainPeerRegistry.sol |
✅ | Per-chain chainId → LayerZero EID table + (source, dest) route allowlist; owner-gated |
IntentSettler.sol |
✅ | OApp + EIP-712 + ReentrancyGuard. submitIntent (escrow + totalEthEscrow ledger), cancelIntent, executeMatching(localHash, remoteHash) (no matcher-trusted price params), _lzReceive dispatching EXECUTE_MATCH (token + chain + both-sides amount checked against trusted data) / CONFIRM (R-17 source-EID guard), refundIfLzTimeout (6 hr), openAuction, setSolverAuction, withdrawOperatorFunds (owner-only, escrow-floor protected). Packed IntentMeta storage. |
SolverAuction.sol |
✅ | Settler-gated setAuctionWindow, ECDSA-signed proposals over a chain-and-contract-bound digest, deterministic ranking, idempotent winner announcement, double-submit guard, DoS cap |
interfaces/IIntentSettler.sol |
✅ | Intent struct (ERC-7683 aligned + refundTo), IntentState enum (with Locked reserved for Phase 2B async settlement), IntentMeta packed struct, full event set |
interfaces/IChainPeerRegistry.sol |
✅ | Read interface for the registry |
interfaces/ISolverAuction.sol |
✅ | Minimal surface for IntentSettler ↔ auction integration |
libraries/IntentHash.sol |
✅ | EIP-712 type hash computation |
libraries/SafeTransfer.sol |
✅ | Native ETH transfer helper; ERC-20 ops use OZ SafeERC20 directly |
libraries/SignatureValidator.sol |
✅ | ECDSA tryRecover with safe error semantics |
| File | Tests | Notes |
|---|---|---|
IntentSettler.t.sol |
33 | submit (incl. totalEthEscrow tracking), cancel, openAuction, executeMatching (lifecycle gates only), refundIfLzTimeout |
IntentSettler.lz.t.sol |
18 | full cross-chain round-trip, full solver-auction round-trip, dropped-delivery → timeout refund, peer rejection, version/type rejection, R-01 source-EID rejection, explicit asymmetric-loss documentation, R-16 dest-side validation (token mismatch + both amount-below-min + wrong chain id), R-17 confirm-leg source-EID rejection, R-18 escrow-floor: return-leg fee never debits user escrow + withdrawOperatorFunds owner gate + escrow-floor enforcement, operatorBalance view |
IntentSettler.solver.t.sol |
5 | IntentSettler ↔ SolverAuction wiring, gating, executeMatching from Auctioning state |
IntentSettler.invariant.t.sol |
9 | 3 property-fuzz × 256 runs + 6 stateful invariants × 256 runs × ~500 calls ≈ 768k random call sequences (includes new totalEthEscrowFloor invariant — balance never dips below escrow ledger) |
IntentHash.t.sol |
5 | EIP-712 parity (on-chain ↔ off-chain), hashIntent view matches submitIntent storage key |
SolverAuction.t.sol |
21 | window setup, signed proposals, ranking, finalisation, gating, getProposals aggregate view |
ChainPeerRegistry.t.sol |
6 | owner / EID / route configuration |
Integration.t.sol |
3 | stack-deploys, submit-then-cancel, submit-match-auction lifecycle |
mocks/ |
— | MockERC20, MockUSDT (non-bool returns), MockLzEndpoint (test-only MessageQueued event + deliverInbound() helper let backend E2E tests relay messages between two endpoint instances on separate Anvil chains) |
Total: 100 unit/fuzz/integration tests + 6 stateful invariants × 256 runs × ~500 calls. All passing.
forge build— clean (45 contracts, no errors)forge test— 100/100 passingforge fmt --check— clean- Slither (
--filter-paths "lib/|test/" --exclude-low --exclude-informational) — 0 medium+ findings across 41 contracts (after R-03 false-positive suppression and R-01/R-02/R-16/R-17/R-18 fixes from the Stage 3 final review + follow-up security pass)
- Solidity:
0.8.26(foundry.toml) - Optimizer: enabled, 200 runs
- Format: line length 120, tab width 4
remappings.txt:forge-std/=lib/forge-std/src/@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/@layerzerolabs/oapp-evm/=lib/devtools/packages/oapp-evm/@layerzerolabs/lz-evm-protocol-v2/=lib/LayerZero-v2/packages/layerzero-v2/evm/protocol/@layerzerolabs/lz-evm-messagelib-v2/=lib/LayerZero-v2/packages/layerzero-v2/evm/messagelib/
cd contracts
# Install dependencies (vendored libs are gitignored, fetched on demand)
forge install OpenZeppelin/openzeppelin-contracts@v5.1.0 --no-git --shallow
forge install LayerZero-Labs/devtools --no-git --shallow
forge install LayerZero-Labs/LayerZero-v2 --no-git --shallow
# Build + test
forge build
forge testTo refresh forge-std from upstream instead of the vendored copy:
forge install foundry-rs/forge-std --no-git --shallow- Multi-chain by config, not by code. Same bytecode on every chain. EIDs and route allowlists live in
ChainPeerRegistrystorage. Adding a new chain isforge script Deploy.s.sol+setPeer+ registry config — no contract change. - P2P-first matching, bonded-solver fallback.
executeMatchingaccepts bothPendingandAuctioningstate — auction is a discovery layer, not a settlement lock. Phase 2A introduces solver bonding (production-proven via Across at $15B+ volume). - Versioned cross-chain payloads. Every LayerZero message starts with
(uint8 messageVersion, uint8 messageType). Old peers reject unknown versions cleanly; new versions can ship without breaking V1 peers. - Atomic settlement Phase 1. Source goes Pending → Matched → Settled. Destination goes Pending → Settled (no observable Locked window). The
Lockedenum slot is reserved for Phase 2B async-settlement designs (HTLC, optimistic). - Recovery before correctness loss. If LayerZero fails to deliver, source user can self-refund via
refundIfLzTimeoutafter 6 hours. The asymmetric-loss class (rare) is fully solved in Phase 2A by the bonded-solver model — see Stage 3 final review § R-06. - No proxy, no admin rescue, no Pausable. Decentralisation > recovery. The only privileged role is
Ownable.owner, which controlssetPeer,setSolverAuction, andChainPeerRegistryconfig — must be transferred to multisig before mainnet.
- Stage 1 audit (escrow + EIP-712 + state machine):
docs/STAGE_1_AUDIT.md - Stage 2 audit (LayerZero OApp):
docs/STAGE_2_AUDIT.md - Stage 3 audit (
SolverAuctionintegration):docs/STAGE_3_AUDIT.md - Stage 3 final review (R-01 to R-15, doc cross-reference, tooling roadmap):
docs/STAGE_3_FINAL_REVIEW.md
External audit + Echidna + Mythril + Halmos are scheduled for Stage 7 (security hardening) before testnet deployment.
PRs that change state machines, token flows, or cross-chain message shapes should cite the relevant audit and update tests accordingly.