feat(sdk): add pre-send destination-liquidity preflight that mandatorily simulates the destination pool's releaseOrMint for token transfers - #301
Conversation
…ily simulates the destination pool's releaseOrMint for token transfers, blocks on any revert with one typed error carrying the raw revert, and ships exhaustive native pool-error ABI coverage
|
You must have Developer access to commit code to Chainlink Labs on Vercel. If you contact an administrator and receive Developer access, commit again to see your changes. Learn more: https://vercel.com/docs/accounts/team-members-and-roles/access-roles#team-level-roles |
andrevmatos
left a comment
There was a problem hiding this comment.
Review: request changes
Destination releaseOrMint simulation is the right primitive for liquidity, lockbox, rate-limit, RMN, mint-authority, and pool-hook checks. But the message this PR reconstructs is not always the message the OnRamp will emit — it can both falsely approve and falsely block sends. Findings below are inline where the code is in the diff; three cross-cutting items are here in the body.
🔴 Default CLI sends abort on CCIPDestSimulationUnavailableError (ccip-cli/src/commands/send.ts:382, not in diff)
Gas estimation runs by default (estimateGasLimit == null || > -100), and the catch rethrows everything except CCIPMethodUnsupportedError. CCIPDestSimulationUnavailableError is explicitly the inconclusive case (no dest RPC / transport failure) — per the stated design goal ("error only when a definitive condition is met"), the default policy should be warn and continue, reserving failure for definitive on-chain verdicts or explicit --estimate-gas-limit / --only-estimate.
🟡 Pre-existing, worth fixing in this PR or as follow-up: receiver finality lookup (ccip-sdk/src/evm/index.ts:2687, not in diff)
The finality check queries the receiver contract directly and defaults permissively ({finalityDepth: 1, finalitySafe: true}) on any error, while the OffRamp defaults non-v2 receivers to wait-for-finality and composes receiver/pool/lane defaults. OffRamp.getCCVsForMessage(bytes) exists precisely for offchain use and is the canonical resolver — prefer it. (This block pre-exists the PR, so not a regression; flagged because this PR is the right pipeline to route it through.)
Missing destination gates (the original ask's dest-only v2 conditions)
- OffRamp lane gates are never checked. The pool-direct simulation bypasses the OffRamp, so nothing validates
OffRamp.getSourceChainConfig(sourceSelector):isEnabled, RMN verification state, and whether the sending OnRamp is in the OffRamp's allowedonRampslist. A message on a disabled lane or from a non-allowed ramp passes this preflight and strands. This is a cheap view call with the already-resolvedoffRamp, and a definitive on-chain condition → should block. - Required CCVs / executors are not honored. Pool
getRequiredCCVs, receiver-required CCVs, and lane defaults are never compared against the user'sccvsextraArgs.OffRamp.getCCVsForMessagereturns required/optional/threshold for offchain use — but it takes the encoded message, so it only becomes trustworthy after the message-construction fixes inline below (destTokenAmount, tokenArgs, tokenReceiver, preserved emitted fields).
Suggested landing order
- The "blocks valid sends" class: offchain-data pools false-block (
simulate.ts:159), probe error swallow (simulate.ts:141), CLI policy (send.ts:382). - The "simulates the wrong message" class:
destTokenAmount(gas.ts:283), preserved emitted fields (gas.ts:270),tokenArgs/tokenReceiver(simulate.ts:322,index.ts:2531). - Error-model fixes: source-revert swallow (
gas.ts:289), non-EVM liquidity fallback (chain.ts), typed-error misclassification (index.ts:2471), transient-set fix (simulate.ts:48). - Missing gates: OffRamp source-config check, then CCV check via
getCCVsForMessage; docs reframe (pre-send-validation.mdx) last.
Required check ownership (proposed)
| Stage | Checks that belong there |
|---|---|
| Source chain | Router/lane enabled, RMN, sender allowlist, source pool registration/remote config, outbound rate/capacity, finality support, source pool required CCVs, fee/balance/approval |
| Destination chain | OffRamp source enabled + allowed OnRamp, receiver finality, receiver/pool/lane CCV requirements, pool compatibility, source-pool wiring, inbound rate/capacity, lockbox liquidity, mint authority, RMN, pool hooks |
| Post-send/manual exec | Exact emitted amount, source pool data, token receiver, offchain token data, verifier results, receiver execution |
| Inconclusive pre-send | Future attestations/proofs, executor liveness, concurrent state changes, future receiver state |
Validation note: this is a static trace against the diff (61cd778..HEAD) and the contracts in chainlink-ccip (OnRamp.sol, OffRamp.sol, LombardTokenPool.sol, CCTPThroughCCVTokenPool.sol). The current tests encode two of the assumptions flagged here (gross amount = message amount; decimals fallback after failed source simulation), so green CI does not cover these paths — please add cases for fee-charging pools, CCTP-v1/Lombard-v1 lanes, and post-send/manual-exec inputs.
…s report preflight unavailable, post-fee/post-send message fidelity, typed source-pool revert, restored generic liquidity layer + OffRamp lane gates, unified transient taxonomy, error-only pool ABIs, CLI warns on inconclusive preflight, docs reframe
Resolve three add/add conflicts: - ccip-sdk/src/evm/index.ts: union the ../gas.ts import (EstimateMessageInput + GetRequiredCCVsMessage/GetRequiredCCVsResult); keep both new method blocks — main's getRequiredCCVs() override and this branch's destination-pool simulation methods. - ccip-sdk/src/index.ts: keep main's getRequiredCCVs + its type re-exports alongside this branch's ./evm/simulate.ts export block. main's rewrite of the estimateReceiveExecution finality gate (now delegating to getRequiredCCVs) auto-merged and is preserved. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Coverage Report |
This pull request introduces significant improvements to the gas estimation documentation and SDK, particularly around pre-send validation and error handling for CCIP transfers. The main focus is on clarifying and enhancing how
estimateReceiveExecutionnot only estimates gas but also validates transfer executability on the destination chain, helping developers avoid stuck transfers. Additionally, new error types and lower-level simulation primitives are documented and implemented, and the guides are reorganized to highlight these features.Documentation and Guide Improvements:
pre-send-validation.mdx, detailing howestimateReceiveExecutionvalidates that a transfer can execute on the destination chain before sending, including error handling and lower-level simulation primitives.gas-estimation.mdxto clarify that on EVM chains,estimateReceiveExecutionsimulatesccipReceiveand returns gas units, while on Solana it estimates compute units; also documented that the method validates executability and throws errors if the transfer would be stuck. [1] [2]gas-estimation.mdxfor consistency and clarity, including error and method reference tables. [1] [2] [3]SDK and Error Handling Enhancements:
CCIPDestExecutionRevertErrorandCCIPDestSimulationUnavailableError, improving error reporting when a transfer would fail on the destination. [1] [2]Chainclass with a newsimulateLockOrBurnmethod, allowing simulation of source pool logic to obtain accurate data for destination pool validation.