# Faster-Than-Finality (FTF) - dApps
Source: https://docs.chain.link/ccip/concepts/execution-latency/ftf-dapps
Last Updated: 2025-05-19

> For the complete documentation index, see [llms.txt](/llms.txt).

## 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` / `getFee` reverts, 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 `CCIPReceiver` base contract returns full finality from `getCCVsAndFinalityConfig`, so it cannot accept FTF messages.
  - To receive FTF messages, use the approach shown below.

![Diagram showing how legacy and V2 receivers handle FTF traffic differently.](/images/ccip/concepts/faster-than-finality/ftf-dapp-destination-receivers.png)

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](https://github.com/smartcontractkit/chainlink-ccip/blob/main/chains/evm/contracts/libraries/FinalityCodec.sol).

### 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 to `0x0000FFFF` (65,535 blocks). For example, `0x00000005` waits 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`](https://github.com/smartcontractkit/chainlink-ccip/blob/main/chains/evm/contracts/libraries/ExtraArgsCodec.sol) serializes the whole struct into the bytes you set as `message.extraArgs`.

Example:

```solidity
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.

```solidity
// 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](/ccip/directory). See [FTF - Token Issuers](/ccip/concepts/execution-latency/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:

1. Full finality (`0x00000000`) is always accepted, regardless of `allowedFinalityConfig`.
2. Block-depth FTF is accepted when `allowedDepth` is non-zero and `requestedDepth >= 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):

```solidity
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)**

```solidity
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](https://developers.circle.com/cctp/concepts/finality-and-block-confirmations), 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](https://developers.circle.com/cctp/concepts/finality-and-block-confirmations#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 `requestedFinalityConfig` sets the finality requirement for the Committee Verifier. For example, if `requestedFinalityConfig` requests 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, 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](https://developers.circle.com/cctp/concepts/fast-transfer-allowance) 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].amount` at send equals USDC received on destination for FTF transfers.
- In `ccipReceive`, use `message.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

> **CAUTION: Waiting for finality will not rescue a mismatched FTF message**
>
> Even after the source chain reaches full finality, an FTF-tagged message whose `requestedFinalityConfig` the
> receiver's `allowedFinalityConfig` does not admit **remains unexecutable**. The OffRamp checks the requested finality
> carried in the message (`message.finality`), not whether real finality has since been reached. The message only
> becomes executable once the receiver's `allowedFinalityConfig` is updated to admit it (or the message is abandoned).
> There is no automatic downgrade to full finality.

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.

1. 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).
2. For transfers that are data-only, receivers should ideally allowlist specific senders and set the `allowedFinalityConfig` to 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 `messageId`s 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.

> **CAUTION: Disclaimer**
>
> Chainlink CCIP is an interoperability messaging protocol. Chainlink does not hold or transfer any assets. The
> performance and behaviour of applications using Chainlink CCIP may depend on coding, engineering, configuration, and
> other technical implementation choices made by developers, token issuers, Cross-Chain Verifiers, and other
> participants. Users remain responsible for evaluating, configuring, testing, deploying, operating, and maintaining
> their own applications and integrations, including assessing any applicable operational, security, technical, and
> legal or regulatory risks. Please review the [Chainlink Terms of Service](https://chain.link/terms) which provides
> important information and disclosures. By using Chainlink CCIP, you expressly acknowledge and agree to accept these
> terms. Cross-Chain Verifiers (CCVs) may be operated by third parties. The security, availability, governance, and
> operational profile of a CCV varies depending on the verifier selected. Users are solely responsible for evaluating
> any CCVs used in connection with their applications or integrations and determining whether they are appropriate for
> their intended use case. This code represents an example of using a Chainlink product or service. It is provided "AS
> IS" and "AS AVAILABLE" without warranties of any kind, has not been audited, and may omit checks or error handling.
> Each party intending to use this reference implementation must perform its own audits, security and code review, and
> testing before any production deployment and ensure the operation and performance of such code matches expectations.
> Neither Chainlink Labs, the Chainlink Foundation, nor Chainlink node operators are responsible for outcomes due to
> errors in this example or how it is deployed or operated. Use of the Chainlink Network is subject to the Chainlink
> Foundation Terms of Service, which provides important information and disclosures. By using this code, you acknowledge
> and agree to these terms.