Faster-Than-Finality (FTF) - dApps
Risk Management for Applications (Data-only / Programmable Token Transfer Integrators)
Applications control finality at send time (via requestedFinalityConfig in ExtraArgsV3) and receive time (via allowedFinalityConfig on the destination receiver). Token issuers additionally control allowed finality on their pools at send time.
These gates are not symmetric:
- Source rejection:
ccipSend/getFeereverts, and no message is created. - Destination rejection: the message is already committed on the source chain. The OffRamp's finality check fails, the execution state becomes FAILURE, and the message can be re-executed once the receiver's finality configuration admits it.
Destination receivers
- Legacy receiver (implements only
IAny2EVMMessageReceiver):- Cannot accept FTF traffic under any circumstances. This is intentional: it prevents any contract that pre-dates CCIP 2.0 from inadvertently being exposed to reorg risk.
- V2 receiver (implements
IAny2EVMMessageReceiverV2):- Default: a receiver built on the
CCIPReceiverbase contract returns full finality fromgetCCVsAndFinalityConfig, so it cannot accept FTF messages. - To receive FTF messages, use the approach shown below.
- Default: a receiver built on the

These receiver rules apply only when the OffRamp calls the receiver. For pure token transfers (no data and a zero gas limit), the OffRamp does not call the receiver or check its finality configuration.
Finality configuration for cross-chain transfers
When you send a cross-chain transfer, you can specify how much finality you want the source chain to reach before the message is acted on. More finality means more security but longer wait times; less finality means faster transfers with more risk of the source block being reorganized.
This setting is encoded as a 4-byte value (bytes4).
| Role | Where | Rules |
|---|---|---|
| Requested (sender) | requestedFinalityConfig in ExtraArgsV3 | Exactly one mode |
| Allowed (receiver) | allowedFinalityConfig from getCCVsAndFinalityConfig | Can be a union of accepted modes |
In Solidity, the FinalityCodec encoding helpers build the value for you. Offchain, you can write the 4-byte value directly using the layout below. Code reference: FinalityCodec.
How the value is structured
The 32 bits are split into two halves:
- Lower 16 bits: block depth. A number from 1 to 65,535 meaning "wait this many blocks."
- Upper 16 bits: flags. Each flag is a named mode. Bit 16 is
WAIT_FOR_SAFE_FLAG("wait for the safe head"), and the other flag bits are reserved for future use. Flags are not activated in the protocol today.
There are three ways to express what you want:
- Wait for full finality: the value
0x00000000. This is the safest option and the default. It does not mean "zero blocks": a value of zero always means full finality. - Wait for N blocks: a value from
0x00000001(1 block) up to0x0000FFFF(65,535 blocks). For example,0x00000005waits for 5 blocks. - Wait for the safe head: the value
0x00010000(the safe flag set, no depth) is for future protocol use, not yet an activated choice.
Configuring the Application Sender (via ExtraArgs)
Finality is one of the message preferences you set in extraArgs. Here you specify the requested finality.
Step 1: Encode the finality. FinalityCodec produces a bytes4.
Step 2: Use the encoded finality in ExtraArgsV3. The bytes4 from Step 1 becomes the requestedFinalityConfig field of GenericExtraArgsV3, and ExtraArgsCodec serializes the whole struct into the bytes you set as message.extraArgs.
Example:
function sendWithBlockDepth(
uint64 destChainSelector,
address receiverOnDest,
bytes memory payload,
uint16 blockDepth // supplied by the caller; 0 = full finality, >= 1 = chosen block depth
) external returns (bytes32 messageId) {
address[] memory ccvs = new address[](0); // default verifier used
bytes[] memory ccvArgs = new bytes[](0); // MUST also be length 0
ExtraArgsCodec.GenericExtraArgsV3 memory args = ExtraArgsCodec.GenericExtraArgsV3({
gasLimit: s_gasLimit,
requestedFinalityConfig: FinalityCodec._encodeBlockDepth(blockDepth), // encoded from the param
ccvs: ccvs,
ccvArgs: ccvArgs,
executor: address(0),
executorArgs: "",
tokenReceiver: "",
tokenArgs: ""
});
bytes memory extraArgs = ExtraArgsCodec._encodeGenericExtraArgsV3(args);
// ... build EVM2AnyMessage, getFee, ccipSend ...
}
Checking the token pool's minimum finality before you send
The source token pool enforces its own minimum finality floor, set by the token issuer. Before you request a block depth in requestedFinalityConfig, read the pool's allowed finality so your getFee / ccipSend call does not revert with FinalityCodec.InvalidRequestedFinality.
Pools built on the TokenPool v2.0 base contract expose getAllowedFinalityConfig() as a view function returning bytes4. The function is not part of the IPoolV2 interface, so some V2 pools, such as USDCTokenPoolProxy, do not expose it.
// Minimal interface for the finality read (pools built on TokenPool v2.0 implement this).
interface ITokenPoolFinality {
function getAllowedFinalityConfig() external view returns (bytes4);
}
// Read the pool's minimum finality floor (view function; free only when called offchain).
bytes4 poolMinFinality = ITokenPoolFinality(pool).getAllowedFinalityConfig();
// Either request full finality (0x00000000 — always accepted),
// or request a block depth >= the pool's floor,
// except when the pool only allows full finality (the default), which rejects every block depth.
bytes4 requested = blockDepth == 0
? FinalityCodec.WAIT_FOR_FINALITY_FLAG
: FinalityCodec._encodeBlockDepth(blockDepth);
// Optional pre-check (same logic the pool runs internally):
// FinalityCodec._ensureRequestedFinalityAllowed(requested, poolMinFinality);
Admissibility rule (enforced inside getFee and ccipSend):
| Sender requests | Accepted? |
|---|---|
0x00000000 (full finality) | Yes, always, regardless of the pool's floor |
_encodeBlockDepth(N) | Yes only if the pool's floor is a non-zero block depth and N >= that floor |
_encodeBlockDepth(N) | Reverts with InvalidRequestedFinality if N < the pool's floor or the pool allows only full finality (the default) |
Find the token pool address for a given token and chain in the CCIP Directory. See FTF - Token Issuers for how token issuers configure this floor.
Configuring the Application Receiver (via enableChain)
Here you specify the allowed finality on the receiving side of the application, per source chain, because you may not want to receive FTF messages from some chains.
Step 1: Encode the finality. FinalityCodec produces a bytes4. Here, block depth = 1 allows all FTF messages as well as full finality messages.
For a receiver that receives tokens, this is the most flexible setting: it lets the token issuer's finality setting (the source chain token pool setting) control the risk.
FinalityCodec._ensureRequestedFinalityAllowed behaves like this:
- Full finality (
0x00000000) is always accepted, regardless ofallowedFinalityConfig. - Block-depth FTF is accepted when
allowedDepthis non-zero andrequestedDepth >= allowedDepth.
So with allowed = _encodeBlockDepth(1):
| Sender requests | Accepted? |
|---|---|
0x00000000 (full finality) | Yes (always) |
_encodeBlockDepth(1) | Yes (1 ≥ 1) |
_encodeBlockDepth(5) | Yes (5 ≥ 1) |
_encodeBlockDepth(65535) | Yes (max depth ≥ 1) |
Step 2: Configure inbound policy per source chain with enableChain(remoteChainSelector, extraArgs, allowedFinalityConfig), an owner-only function on CCIPClientExample:
enableChain(remoteChainSelector, extraArgs, allowedFinalityConfig)
| Parameter | Purpose on receiver |
|---|---|
remoteChainSelector | The source chain you are configuring inbound policy for |
extraArgs | Outbound ExtraArgs used when this contract sends to that chain, i.e., the receiver is also a sender. Inbound messages do not use it, but CCIPClientExample.ccipReceive reverts with InvalidChain if it is empty |
allowedFinalityConfig | Inbound finality policy for messages arriving from that chain |
Example 1: Pure inbound receiver
This contract never sends, but CCIPClientExample still needs non-empty extraArgs for each source chain before ccipReceive accepts messages from it (see the table above):
contract InboundOnlyReceiver is CCIPClientExample {
constructor(IRouterClient router, IERC20 feeToken)
CCIPClientExample(router, feeToken)
{}
/// @notice Configure inbound finality policy for a source chain.
/// extraArgs is unused here because this contract never sends outbound.
function configureInboundPolicy(
uint64 sourceChainSelector,
bytes4 allowedFinalityConfig
) external onlyOwner {
enableChain(sourceChainSelector, "", allowedFinalityConfig);
}
function _ccipReceive(Client.Any2EVMMessage memory message) internal override {
// your inbound logic
}
}
Example 2: Bidirectional contract (receive + send)
contract BidirectionalApp is CCIPClientExample {
struct LaneConfig {
uint32 outboundCallbackGasLimit; // gas billed for callback on the remote chain
bytes4 outboundRequestedFinality; // finality you request when sending TO that chain
bytes4 inboundAllowedFinality; // finality you accept when receiving FROM that chain
}
constructor(IRouterClient router, IERC20 feeToken)
CCIPClientExample(router, feeToken)
{}
function configureLane(
uint64 remoteChainSelector,
LaneConfig calldata config
) external onlyOwner {
bytes memory outboundExtraArgs = ExtraArgsCodec._getBasicEncodedExtraArgsV3(
config.outboundCallbackGasLimit,
config.outboundRequestedFinality
);
enableChain(
remoteChainSelector,
outboundExtraArgs,
config.inboundAllowedFinality
);
}
}
Faster-Than-Finality (FTF) USDC transfers
Like other FTF messages, USDC transfers can request FTF via requestedFinalityConfig in ExtraArgsV3. The behavior depends on whether the transfer is a pure token transfer or a token transfer with data and/or a non-zero user gas limit.
CCIP 2.0 supports FTF USDC transfers on lanes that integrate with Circle's CCTP. Finality for USDC token transfers is therefore governed by CCTP finality thresholds, whose block confirmations vary by chain. For transfers sent to a smart contract (with a non-zero message gas limit or with data), both the CCTP finality and the message's requested finality determine the overall speed.
The CCTP Verifier integrates with the CCTP smart contracts and Circle's CCTP attestation service.
1. Pure token transfers (no data, 0 user gas limit)
For USDC FTF transfers on lanes that Circle's CCTP supports, pure token transfers (no data, 0 user gas limit) use only the CCTP Verifier, not the Committee Verifier.
Finality is determined in this way:
message.finality == 0(WAIT_FOR_FINALITY_FLAG) → CCTP threshold 2000 → standard USDC transfers.message.finality != 0→ CCTP threshold 1000 → CCTP fast transfers.
So 0x00000001, 0x00000002, 0x0000000a, etc. all select the same CCTP fast path. The numeric depth is not passed through to Circle as "wait N blocks." Circle has two modes (standard and fast) and determines the number of block confirmations for CCTP fast transfers. See CCTP block confirmations and fast-transfer attestation times.
In this case, the message has no data and a user gas limit of 0, so the OffRamp does not call the destination receiver and does not check the receiver's allowedFinalityConfig.
2. Token transfers with data and/or non-0 user gas limit
For USDC FTF transfers on lanes that Circle's CCTP supports, token transfers with data and/or a non-0 user gas limit use both the CCTP Verifier and the Committee Verifier.
Finality is determined in this way:
message.finality == 0(WAIT_FOR_FINALITY_FLAG) → CCTP threshold 2000 → standard USDC transfers, and the default Committee Verifier waits for full finality to be reached before verifying.message.finality != 0→ CCTP threshold 1000 → CCTP fast transfers.- In this case, the block depth in
requestedFinalityConfigsets the finality requirement for the Committee Verifier. For example, ifrequestedFinalityConfigrequests a block depth of 10, the Committee Verifier waits for 10 blocks, not just 1. The 10 does not affect CCTP processing, which is binary (standard or fast): only the fact that the value is not 0 matters. - CCIP waits for all CCVs to finish verifying, so in this example the 10 blocks chosen by the user or dApp drive the overall latency, even if CCTP has already finished.
- In this case, the block depth in
In this case, set the receiver's allowedFinalityConfig so that it admits the sender's requestedFinalityConfig. Setting allowedFinalityConfig = 1 is the most flexible, since it allows all USDC FTF transfers (any block depth >= 1 in extraArgs). If the receiver's allowedFinalityConfig is > 1, make sure the source-side extraArgs requests at least that block depth.
Delivered amount vs sent amount for FTF USDC transfers
For standard USDC (requestedFinalityConfig = 0), CCTP uses the standard path with no CCTP fast-transfer fee. The USDC amount burned on source matches what CCTP mints to the recipient (subject to normal CCIP/pool fees quoted at send time).
For FTF USDC (any non-zero requestedFinalityConfig), Circle charges a fast-transfer fee on the destination when USDC is minted. CCTPVerifier passes a maxFee into depositForBurnWithHook on the source burn, and CCTP determines the actual fee deducted at mint time (basis points, lane-specific). The amount delivered on the destination chain can be less than the amount sent.
Implications for dApps:
- Do not assume
tokenAmounts[0].amountat send equals USDC received on destination for FTF transfers. - In
ccipReceive, usemessage.destTokenAmounts[0].amount. The OffRamp sets it from the actual balance credited to the token receiver after mint. - Programmable token transfer logic (accounting, minimum deposit checks, share minting) should key off the delivered amount, not the source send amount.
- The CCTP fee is separate from CCIP protocol fees.
Avoiding stranded inbound messages
The source-side pool gate and the destination-side receiver gate run at different points in the message lifecycle, so make sure a sender does not send an FTF message that the receiver will not admit. The source-side check happens synchronously inside ccipSend: if the pool's allowedFinality does not permit the request, the call reverts and no message is created. The destination-side check happens asynchronously when the OffRamp consults the receiver's getCCVsAndFinalityConfig to determine whether to admit the message for execution. If the receiver returns an allowedFinalityConfig that does not admit the message's requestedFinalityConfig, validation reverts via _ensureRequestedFinalityAllowed. There is no automatic downgrade to full finality; the strict admissibility check rejects the message under its declared terms.
When this happens, the OffRamp marks the message's execution state as FAILURE and emits an ExecutionStateChanged event. The message is not destroyed and can be retried, but every retry reverts (with NoStateProgressMade) until the receiver's allowedFinalityConfig is updated to admit the message's requested finality. Waiting for the source chain to fully finalize does not unstick the message, because the OffRamp checks the requested finality carried in the message itself, not whether actual finality has since been reached.
A sender can successfully send a message whose requestedFinalityConfig the destination receiver rejects.
Coordinate upstream: the receiver's allowedFinalityConfig must admit what pools, CCVs, and senders set.
- For transfers that involve tokens, setting it to block depth = 1 allows any FTF transfers to be received (but they have already been gated by the token issuer's finality setting on source).
- For transfers that are data-only, receivers should ideally allowlist specific senders and set the
allowedFinalityConfigto a safe value.
Best practices
Use V2 senders and receivers for FTF
Use a V2 sender and receiver pair for FTF. V1 receivers cannot accept FTF messages.
Coordinate finality policies
Coordinate the receiver policy with upstream pools, CCVs, and senders. Do not tighten allowedFinalityConfig while FTF messages are in flight.
Require full finality by default
Leave allowedFinalityConfig at WAIT_FOR_FINALITY_FLAG unless FTF is required on that lane. Do not enable FTF globally by default.
Gate FTF by sender
When only known partners should use fast delivery, explicitly gate FTF by sender. A non-zero allowedFinalityConfig for a source chain does not restrict FTF to allowlisted senders without additional checks.
Authenticate senders before non-idempotent operations
On open FTF lanes, authenticate message.sender before executing non-idempotent ccipReceive logic. Do not act on data from unknown senders when reorg duplicates could be harmful.
Configure finality per source chain
Set an appropriate finality policy for each source chain instead of reusing one policy across all chains.
Track processed messages
Track processed messageIds for non-idempotent effects. Do not assume that the OffRamp deduplicates retries caused by reorgs.
Implement the receiver interface and ERC-165
Implement both IAny2EVMMessageReceiver and ERC-165. Do not rely on ccipReceive alone; without ERC-165, the OffRamp might skip the callback even when data is non-empty.
Upgrade legacy receivers before enabling FTF
Upgrade to IAny2EVMMessageReceiverV2 before setting a non-zero inbound finality configuration. Legacy receivers do not support custom finality.
Plan recovery for stranded messages
Plan an owner-mediated recovery process for stranded messages. A mismatched FTF request does not become executable merely because the source chain reaches finality.
Verify third-party CCV behavior
Verify how each third-party CCV handles reorgs. Do not assume that every CCV implements the same reorg quarantine behavior as the Chainlink Committee Verifier.
Test FTF configurations
Test FTF scenarios on testnet before deployment. Do not deploy a permissive FTF configuration to mainnet without testing it first.