Cross-Chain Vault Adapter
Uses CCIP View on GitHub

Adapter contract

Use this reference to check the exact signatures, access rules, revert conditions, and events of CrossChainERC4626Adapter. The hub chain is the chain where the vault and the adapter are deployed, and a source chain is any other chain that users send deposits and redemptions from. The adapter receives CCIP token transfers on the hub chain, deposits the asset into or redeems shares from an allowlisted ERC-4626 vault, and delivers the output on the hub chain or bridges it back to the source chain. For the same request explained step by step, read How it works and Failures and recovery.

This contract provides:

  • a CCIP receiver that runs the vault interaction in an isolated self-call and records failures instead of reverting
  • deposit and redeem routing, selected by the token the message carries
  • local delivery on the hub chain or return to the source chain, with a flat adapter fee on returns
  • cross-chain refund and local recovery for failed messages
  • per-source-chain Cross-Chain Verifier (CCV) and finality settings for CCIP 2.0 lanes
  • configuration split across DEFAULT_ADMIN_ROLE, FEE_SETTER_ROLE, and FEE_COLLECTOR_ROLE

Usage boundary

  • Only the CCIP Router set at construction (ROUTER) can call ccipReceive. Any other caller gets InvalidRouter.
  • processMessage is external only so that ccipReceive can wrap it in try/catch. Any caller other than the adapter itself gets OnlySelf.
  • The CCIP OffRamp calls supportsInterface to confirm the adapter is a CCIP receiver. On CCIP 2.0 lanes it also reads getCCVsAndFinalityConfig before it delivers a message.
  • Users do not call the adapter to deposit or redeem. They send a CCIP message from the source chain to the adapter's address, with the payload described in Message format.
  • Users and integrators call the integrator views and the two recovery functions. Anyone can call refundFailedMessage. Only the local refund address stored for a message can call recoverFailedMessageLocally.
  • Role holders call the configuration functions and the fee functions.
  • Deploy the adapter through the factory instead of calling the constructor directly. See Deploy the adapter.
  • The message-processing logic has no virtual hooks. To change it, fork the contract and edit processMessage and its private helpers. See Use it as-is or customize it.

Contract

Source: src/ccip/CrossChainERC4626Adapter.sol

PropertyValue
ContractCrossChainERC4626Adapter
typeAndVersionCrossChainERC4626Adapter 1.0.0
LicenseMIT
Audit statusAudited
Compilersolc 0.8.24, via_ir enabled, optimizer runs 1, EVM version cancun
Minimum EVMShanghai upgrade (PUSH0)

The audited bytecode is identical to a shanghai build, and PUSH0 is the only opcode it uses that pre-Shanghai chains lack. Deploy the adapter only on chains that support the Shanghai upgrade. Recompiling with other settings produces bytecode that differs from the audited build. The pinned settings are in foundry.toml.

The contract imports these dependencies:

Package
VersionImports
@chainlink/contracts-ccip2.0.0Client, ExtraArgsCodec, FinalityCodec, IRouterClient, IAny2EVMMessageReceiver, IAny2EVMMessageReceiverV2
@chainlink/contracts1.5.0ITypeAndVersion
OpenZeppelin Contracts5.0.2AccessControlEnumerable, ReentrancyGuard, IERC4626
OpenZeppelin Contracts4.8.3IERC20, SafeERC20

Inheritance

  • IAny2EVMMessageReceiverV2: ccipReceive and getCCVsAndFinalityConfig
  • AccessControlEnumerable (OpenZeppelin 5.0.2): roles and role enumeration. See Roles.
  • ReentrancyGuard (OpenZeppelin 5.0.2): the nonReentrant modifier on refundFailedMessage, recoverFailedMessageLocally, withdrawFee, and recoverNative
  • ITypeAndVersion: typeAndVersion

The adapter does not inherit CCIP's CCIPReceiver base contract. It implements the receiver interface and its own router check.

Constructor

A direct deployment runs this constructor:

constructor(address router_, address defaultAdmin, address feeSetter, address feeCollector)
Parameter
Type
Description
router_addressCCIP Router on the hub chain, stored as the immutable ROUTER. Get the address from the CCIP Directory for testnet or mainnet.
defaultAdminaddressReceives DEFAULT_ADMIN_ROLE, FEE_SETTER_ROLE, and FEE_COLLECTOR_ROLE.
feeSetteraddressReceives FEE_SETTER_ROLE.
feeCollectoraddressReceives FEE_COLLECTOR_ROLE.

Reverts with:

  • InvalidRouter(address(0)) when router_ is the zero address.
  • InvalidAdmin() when defaultAdmin is the zero address.
  • InvalidFeeSetter() when feeSetter is the zero address.
  • InvalidFeeCollector() when feeCollector is the zero address.

Emits:

  • RoleGranted for each new role grant: three for defaultAdmin, plus one each for feeSetter and feeCollector when they differ from defaultAdmin.

The constructor checks only that router_ is non-zero. It does not verify that the address is a CCIP Router, and ROUTER cannot change after deployment. A wrong router means deploying a new adapter.

A new adapter starts with deposits and redeems disabled, no chain configured, no vault allowlisted, no adapter fee, no CCV or inbound finality settings, the UNSET (legacy) return format on every lane, and no native gas token balance.

Deploy through the factory instead. It sets chain types, the vault allowlist entry, the deposit and redeem switches, and adapter fees in the same transaction, then grants each role only to its configured address. Through the factory, defaultAdmin receives the fee roles only if you also set it as feeSetter or feeCollector. See Role handoff.

Roles

The adapter defines two roles and inherits DEFAULT_ADMIN_ROLE:

RoleConstantValueGranted byCan call
AdminDEFAULT_ADMIN_ROLE0x00Constructor (defaultAdmin), factory (defaultAdmin), or an existing adminsetChainType, setTargetEnabled, setProcessingEnabled, setCCVsConfig, setInboundFinality, setEvmReturnLaneFormat, setEvmReturnRequestedFinality, recoverNative, and grantRole and revokeRole for all three roles
Fee setterFEE_SETTER_ROLEkeccak256("FEE_SETTER_ROLE")Constructor (feeSetter and defaultAdmin), factory (feeSetter only), or an adminsetAssetFee, setAssetFees
Fee collectorFEE_COLLECTOR_ROLEkeccak256("FEE_COLLECTOR_ROLE")Constructor (feeCollector and defaultAdmin), factory (feeCollector only), or an adminwithdrawFee

The adapter inherits role management from OpenZeppelin AccessControlEnumerable: grantRole, revokeRole, renounceRole, hasRole, getRoleAdmin, getRoleMember, and getRoleMemberCount. The adapter never changes role admins, so DEFAULT_ADMIN_ROLE is the admin of all three roles. Use getRoleMemberCount and getRoleMember to list the current holders of a role.

Role changes take effect in a single step. There is no pending-admin acceptance and no delay, so granting DEFAULT_ADMIN_ROLE to a wrong address hands over control in that transaction. renounceRole(role, callerConfirmation) requires callerConfirmation to equal the caller. If the last holder of DEFAULT_ADMIN_ROLE renounces it or has it revoked, no account can call the admin functions or change roles again. Fee setters and fee collectors keep their roles.

Message format

Every message sent to the adapter must carry exactly one token and a data field that decodes to Payload. For how a user builds the full CCIP message on the source chain, see What a user sends.

The adapter decodes message.data into this struct, shown here without its source comments:

struct Payload {
    address target;
    bytes32 beneficiary;
    uint256 minimumOut;
    uint256 deliveryAndRefund;
}
Field
Type
Description
targetaddressVault to call. Must be allowlisted with setTargetEnabled, or processing fails with InvalidTarget.
beneficiarybytes32Recipient of the output: on the hub chain for local delivery, on the source chain for a return. For an EVM address, left-pad it to 32 bytes; if the upper 96 bits are not zero, processing fails with InvalidEVMAddress.
minimumOutuint256Smallest acceptable output after the adapter fee, in output units: shares for a deposit, asset for a redemption. Below it, processing fails with MinimumOutputNotMet. 0 disables the check, but a zero output still fails with NoOutputReceived.
deliveryAndRefunduint256Delivery choice and local refund address, packed into one word as shown in the next table.

deliveryAndRefund uses this bit layout:

Bits
Field
Meaning
0returnToSourceChain1 bridges the output back to message.sourceChainSelector. 0 delivers it to beneficiary on the hub chain.
1 to 160localRefundAddressHub-chain address that can call recoverFailedMessageLocally if processing fails. address(0) disables local recovery.
161 to 255UnusedIgnored on decode and not validated.

The adapter enforces these rules on every message:

  • message.destTokenAmounts must hold exactly one entry. Otherwise processing fails with InvalidTokenCount.
  • message.data must be exactly CCIP_MESSAGE_PAYLOAD_LENGTH (128) bytes, the length of abi.encode over the four static fields. Otherwise processing fails with InvalidPayloadLength.
  • The token selects the operation. The vault's asset() means deposit. The vault address, which is also the share token, means redeem. Any other token fails with InvalidTargetToken.

This Solidity snippet builds message.data the same way as the _encodePayload and _packDeliveryAndRefund helpers in the repository's adapter tests:

uint256 deliveryAndRefund = (uint256(uint160(localRefundAddress)) << 1) | (returnToSourceChain ? 1 : 0);

bytes memory data = abi.encode(
    vault,                                  // target
    bytes32(uint256(uint160(beneficiary))), // beneficiary, left-padded EVM address
    minimumOut,
    deliveryAndRefund
);

In TypeScript, encode the same four values as the ABI tuple (address, bytes32, uint256, uint256) with viem or ethers.

Types

All types below are declared inside CrossChainERC4626Adapter. Messages and token amounts use Client.Any2EVMMessage and Client.EVMTokenAmount from the CCIP Client library.

ChainType

The adapter records a chain family for each remote chain selector with this enum:

enum ChainType {
    NONE,
    EVM,
    SVM
}
Value
Meaning
NONE (0)Selector not configured. Messages from it fail with InvalidChain, and preview reverts for it when the amount is not zero.
EVM (1)EVM chain family. Return legs and refunds to this selector use an EVM receiver encoding and the lane's EVM extraArgs format.
SVM (2)Solana Virtual Machine chain family.

EvmReturnExtraArgsFormat

The adapter encodes extraArgs for return legs and refunds to an EVM chain according to this enum, set per destination with setEvmReturnLaneFormat:

enum EvmReturnExtraArgsFormat {
    UNSET,
    LEGACY_EXTRA_ARGS_V2,
    GENERIC_EXTRA_ARGS_V3_BASIC
}
ValueEncodingUse on
UNSET (0)Same as LEGACY_EXTRA_ARGS_V2. This is the default and cannot be written with setEvmReturnLaneFormat.CCIP 1.6 lanes
LEGACY_EXTRA_ARGS_V2 (1)Client.GenericExtraArgsV2 with gasLimit 0 and allowOutOfOrderExecution trueCCIP 1.6 lanes
GENERIC_EXTRA_ARGS_V3_BASIC (2)ExtraArgsCodec._getBasicEncodedExtraArgsV3(0, requestedFinality), with the finality from evmReturnRequestedFinality. See ExtraArgsCodec.CCIP 2.0 lanes

ErrorCode

The adapter tracks the processing outcome of each inbound message ID with this enum:

enum ErrorCode {
    NONE,
    BASIC,
    RESOLVED
}
ValueMeaning
NONE (0)Not received yet, or processed successfully. A MessageSucceeded event for the message ID tells the two apart.
BASIC (1)Processing failed. The adapter holds the inbound tokens, and a cross-chain refund or local recovery is available.
RESOLVED (2)A failed message was refunded cross-chain or recovered locally. A MessageRefunded or MessageRecoveredLocally event for the message ID tells which.

Payload

Payload is the struct encoded in message.data. See Message format.

FailedMessageRecord

When processing fails, the adapter stores this record for the message ID:

struct FailedMessageRecord {
    uint64 sourceChainSelector;
    bytes sender;
    Client.EVMTokenAmount[] destTokenAmounts;
    address localRefundAddress;
}
FieldTypeDescription
sourceChainSelectoruint64Chain the message came from. Cross-chain refunds go back to this chain.
senderbytesOriginal message.sender. CCIP encodes an EVM sender as 32 bytes. Cross-chain refunds go to this address.
destTokenAmountsClient.EVMTokenAmount[]Tokens and amounts the adapter received and now holds.
localRefundAddressaddressDecoded from deliveryAndRefund when the payload is exactly 128 bytes, otherwise address(0). The only address that can call recoverFailedMessageLocally.

The record does not include message.data. Read it with getFailedMessageRecord.

CCVConfig

The adapter stores its CCIP 2.0 verifier policy per source chain in this struct:

struct CCVConfig {
    address[] requiredCCVs;
    address[] optionalCCVs;
    uint8 optionalThreshold;
}
FieldTypeDescription
requiredCCVsaddress[]CCVs that must all attest to a message. address(0) stands for the lane's default CCVs.
optionalCCVsaddress[]CCVs from which at least optionalThreshold must attest.
optionalThresholduint8Number of optional CCVs that must attest. Greater than 0 when optionalCCVs is non-empty.

Set it with setCCVsConfig and read it with getCCVsAndFinalityConfig.

State variables

Every public state variable has a getter with the same name. The adapter declares these:

GetterReturnsDescription
ROUTER()addressImmutable CCIP Router. The only caller allowed into ccipReceive, and the router used for getFee and ccipSend on return legs and refunds.
typeAndVersion()stringConstant "CrossChainERC4626Adapter 1.0.0".
DEFAULT_ADMIN_ROLE()bytes32Inherited constant 0x00.
FEE_SETTER_ROLE()bytes32Constant keccak256("FEE_SETTER_ROLE").
FEE_COLLECTOR_ROLE()bytes32Constant keccak256("FEE_COLLECTOR_ROLE").
CCIP_MESSAGE_PAYLOAD_LENGTH()uint256Constant 128. Required length of message.data.
depositsEnabled()boolWhether deposits are processed, for every vault on the adapter.
redeemsEnabled()boolWhether redemptions are processed, for every vault on the adapter.
inboundFinality(uint64 sourceChainSelector)bytes4Allowed finality for messages from the source chain on CCIP 2.0 lanes. Default 0x00000000 accepts only messages that wait for finality.
chains(uint64 chainSelector)ChainTypeChain family of a remote selector. NONE means not configured.
enabledTargets(address target)boolWhether a vault is allowlisted.
assetFees(uint64 destinationChainSelector, address bridgedToken)uint256Adapter fee in the asset's smallest units, keyed by the return chain and the token bridged back.
collectedFees(address asset)uint256Adapter fees accrued and not yet withdrawn.
refundChainFamilySnapshot(bytes32 messageId)ChainTypeChain family recorded when a message failed. Cross-chain refunds use it instead of the current chains value. Deleted when the message resolves.
messageErrorCode(bytes32 messageId)ErrorCodeProcessing outcome of an inbound message.
evmReturnExtraArgsFormat(uint64 destinationChainSelector)EvmReturnExtraArgsFormatReturn and refund extraArgs format for a destination chain.
evmReturnRequestedFinality(uint64 destinationChainSelector, address token)bytes4Requested finality for return legs and refunds of token to a destination, used only on GENERIC_EXTRA_ARGS_V3_BASIC lanes.

ccvConfigs and failedMessageRecords are internal. Read them through getCCVsAndFinalityConfig and getFailedMessageRecord.

The adapter accepts the native gas token through receive() external payable. That balance pays the CCIP fees for return legs. The admin withdraws it with recoverNative.

Receiver functions

ccipReceive

Receives a message from the CCIP Router and processes it in an isolated self-call.

function ccipReceive(Client.Any2EVMMessage calldata message) external onlyRouter

Calls this.processMessage(message) inside try/catch.

  • On success, emits MessageSucceeded. messageErrorCode stays NONE.
  • On a revert, sets messageErrorCode to BASIC, stores a FailedMessageRecord, records the source chain's family in refundChainFamilySnapshot, and emits MessageFailed. If chains has no entry for the source chain, the family is inferred from the sender's format. The one exception, a malformed target word, is listed under Reverts with.

When processing fails, every effect of processMessage rolls back: the vault call, the fee, and any outbound send. The adapter keeps the inbound tokens. CCIP execution still succeeds, so the CCIP Explorer shows the message as Success. The catch is bare, so the adapter does not store or emit the revert reason. Nothing retries the message, and re-enabling a vault or chain does not reprocess it. See Why the CCIP Explorer shows Success.

Access: ROUTER only.

Parameters:

ParameterTypeDescription
messageClient.Any2EVMMessage calldataInbound message: message ID, source chain selector, sender, data, and delivered tokens.

Returns: none.

Reverts with:

  • InvalidRouter(msg.sender) when the caller is not ROUTER.
  • Out of gas when the gas limit set on the source chain does not cover the call. CCIP marks the message failed. Recover it with manual execution at a higher gas limit.
  • A decode revert in the catch block when a 128-byte payload has a target word with non-zero upper 12 bytes. The message can never execute. See Failures with no recovery path.

When ccipReceive reverts, CCIP reverts the token transfer and marks the message failed at the CCIP level. See Failures the adapter can't catch.

Emits:

  • MessageSucceeded(messageId) after the events from a successful processMessage.
  • MessageFailed(messageId) when processMessage reverts.

processMessage

Validates an inbound message, calls the vault, and delivers the output.

function processMessage(Client.Any2EVMMessage calldata message)
    external
    onlySelf
    onlyValidChain(message.sourceChainSelector)

Runs in this order:

  1. Requires the caller to be the adapter, and chains[message.sourceChainSelector] to be configured (not NONE).
  2. Requires exactly one entry in message.destTokenAmounts.
  3. Requires message.data to be exactly 128 bytes, then decodes it as Payload.
  4. Requires enabledTargets[payload.target].
  5. Requires a non-zero token amount.
  6. Reads asset() from the vault and routes by token:
    • Asset (deposit): requires depositsEnabled. When returning to the source chain, subtracts the adapter fee from the input. Calls deposit(assets, address(this)) with the remaining amount.
    • Share token (redeem): requires redeemsEnabled. Calls redeem(shares, address(this), address(this)). When returning to the source chain, subtracts the adapter fee from the redeemed assets.
  7. Requires a non-zero output of at least minimumOut, then emits TargetProcessed.
  8. Delivers the output. With returnToSourceChain set, it sends a CCIP message to beneficiary on message.sourceChainSelector. Otherwise it transfers the output to beneficiary on the hub chain and emits LocalTokenDelivered.

The vault sees the adapter as the caller, receiver, and owner. The adapter fee is read from assetFees[message.sourceChainSelector][bridgedToken] when the message executes. bridgedToken is the share token for a deposit return and the asset for a redemption return. Fees accrue in collectedFees[asset]. Local delivery pays no adapter fee.

The adapter pays each return leg's CCIP fee from its own native gas token balance. A return leg is a token-only CCIP message, and for an EVM destination it has these fields:

FieldValue
receiverabi.encode(beneficiary)
dataEmpty
tokenAmountsOne entry: the output token and the output amount after the adapter fee
extraArgsBuilt from the adapter's configuration, never from message data. GenericExtraArgsV3 with gas limit 0 and evmReturnRequestedFinality[destination][token] on a GENERIC_EXTRA_ARGS_V3_BASIC lane; otherwise GenericExtraArgsV2 with gas limit 0 and out-of-order execution allowed.
feeTokenaddress(0), so the CCIP fee is paid in the native gas token, never LINK

Access: the adapter itself (onlySelf).

Parameters:

ParameterTypeDescription
messageClient.Any2EVMMessage calldataInbound message, forwarded from ccipReceive.

Returns: none.

Reverts with (when called from ccipReceive, every revert below except OnlySelf and the decode revert becomes a failed message):

  • OnlySelf() when any address other than the adapter calls it.
  • InvalidChain(sourceChainSelector) when the source chain is not configured.
  • InvalidTokenCount(tokenCount) when the message does not carry exactly one token.
  • InvalidPayloadLength(length, 128) when message.data is not 128 bytes.
  • A decode revert without error data when the payload is 128 bytes but target does not fit in 160 bits. This case does not become a failed message. See ccipReceive and Failures with no recovery path.
  • InvalidTarget(target) when the vault is not allowlisted.
  • AmountIsZero() when the token amount is zero.
  • DepositsDisabled() or RedeemsDisabled() when that path is switched off.
  • InvalidTargetToken(target, token) when the token is neither the vault's asset nor its share token.
  • FeeExceedsAmount(amount, fee) when the adapter fee is greater than or equal to the deposit input or the redeemed assets.
  • NoOutputReceived() when the vault returns zero shares or zero assets.
  • MinimumOutputNotMet(minimumOut, actualOut) when the output after the fee is below minimumOut.
  • InvalidEVMAddress(beneficiary) when the upper 96 bits of beneficiary are not zero.
  • InsufficientNativeBalance(requiredFee, availableBalance) when the adapter's native gas token balance is below the router fee for the return leg.
  • Errors from the vault (for example, a deposit limit), the tokens, the CCIP Router, or a token pool (for example, a rate limit on the return lane).

Emits:

  • TargetProcessed after the vault call.
  • LocalTokenDelivered for local delivery, or MessageSent for a return leg.

These events roll back with the rest of processMessage when a later step reverts.


getCCVsAndFinalityConfig

Returns the verifier and finality policy that CCIP 2.0 OffRamps apply to messages from a source chain.

function getCCVsAndFinalityConfig(uint64 sourceChainSelector, bytes calldata)
    external
    view
    virtual
    returns (
        address[] memory requiredCCVs,
        address[] memory optionalCCVs,
        uint8 optionalThreshold,
        bytes4 allowedFinalityConfig
    )

Returns the stored CCVConfig and inboundFinality for sourceChainSelector. The second parameter, the sender, is unnamed and ignored, so every sender on a source chain gets the same policy.

On CCIP 2.0 lanes the OffRamp calls this function before ccipReceive. If a message lacks the required CCV attestations, or requests a finality that allowedFinalityConfig does not allow, the message fails at the CCIP level and the adapter never sees it. CCIP 1.6 lanes do not call this function.

Empty CCV lists with a zero threshold make the OffRamp use the lane's default CCVs. An allowedFinalityConfig of 0x00000000, the default, accepts only messages that wait for finality.

Access: anyone (view).

Parameters:

ParameterTypeDescription
sourceChainSelectoruint64Source chain of the inbound message.
(unnamed)bytes calldataSender on the source chain. Ignored.

Returns:

NameTypeDescription
requiredCCVsaddress[] memoryCCVs that must all attest.
optionalCCVsaddress[] memoryOptional CCVs.
optionalThresholduint8Number of optional CCVs that must attest.
allowedFinalityConfigbytes4inboundFinality[sourceChainSelector], in the FinalityCodec layout.

Reverts with: none.

Emits: none.


supportsInterface

Reports which interfaces the adapter implements, per ERC-165.

function supportsInterface(bytes4 interfaceId)
    public
    view
    virtual
    override(AccessControlEnumerable)
    returns (bool supported)

Returns true for IAny2EVMMessageReceiver, IAny2EVMMessageReceiverV2, IAccessControlEnumerable, IAccessControl, and IERC165. The CCIP OffRamp uses this check to decide whether to call ccipReceive, and on CCIP 2.0 lanes whether to read getCCVsAndFinalityConfig.

Access: anyone (view).

Parameters:

ParameterTypeDescription
interfaceIdbytes4ERC-165 interface identifier.

Returns:

NameTypeDescription
supportedbooltrue if the adapter supports the interface.

Reverts with: none.

Emits: none.

Integrator views

preview

Simulates a deposit or redemption, including the adapter fee, so a UI can show the expected output.

function preview(
    address token,
    address vaultTarget,
    uint256 amount,
    bool returnToSourceChain,
    uint64 assetFeeDestinationChainSelector
) external view returns (uint256 received)

Call it on the hub chain with the source chain's selector. It follows the routing and fee rules of processMessage and calls the vault's previewDeposit or previewRedeem.

  • Returns 0 instead of reverting when amount is 0, when the adapter fee is greater than or equal to the amount (deposit) or the redeemed assets (redemption), or when the vault preview returns 0.
  • Requires assetFeeDestinationChainSelector to be configured in chains, even when returnToSourceChain is false.
  • Does not apply minimumOut. Vault state and the adapter fee can change before the message executes, so derive minimumOut from this value with a tolerance. See Slippage protection.
  • Does not catch vault reverts. If asset(), previewDeposit, or previewRedeem reverts, preview reverts.

Access: anyone (view).

Parameters:

ParameterTypeDescription
tokenaddressInbound token: the vault's asset for a deposit, or the vault (share token) address for a redemption.
vaultTargetaddressAllowlisted vault, as in Payload.target.
amountuint256Inbound amount, in token units.
returnToSourceChainbooltrue applies the adapter fee for a return to the source chain. false simulates local delivery, with no adapter fee.
assetFeeDestinationChainSelectoruint64Source chain selector of the message. Must be configured in chains. Also keys assetFees when returnToSourceChain is true.

Returns:

NameTypeDescription
receiveduint256Expected output after the adapter fee: shares for a deposit, asset for a redemption. 0 when the request is not viable.

Reverts with:

  • InvalidTarget(vaultTarget) when the vault is not allowlisted. This check runs before the zero-amount check.
  • InvalidChain(assetFeeDestinationChainSelector) when the selector is not configured and amount is not zero.
  • DepositsDisabled() or RedeemsDisabled() when that path is switched off.
  • InvalidTargetToken(vaultTarget, token) when token is neither the asset nor the share token.
  • Errors from the vault's asset(), previewDeposit, or previewRedeem.

Emits: none.


checkRefundEligibility

Reports whether a failed message can be refunded cross-chain, and estimates the fee.

function checkRefundEligibility(bytes32 messageId)
    external
    view
    returns (bool canRefund, bytes32 originalSender, address token, uint256 tokenAmount, uint256 requiredFee)

Returns canRefund = false with zeroed fields when the message is not BASIC, when the stored sender is not exactly 32 bytes, or when no inbound token amount is positive. Otherwise it returns canRefund = true, the original sender, the first token with a non-zero amount, and requiredFee: the sum of one router getFee quote per non-zero token.

This view can revert. After the early checks it calls the router's getFee, which reverts when, for example, the router has no lane back to the source chain, the configured return format does not match the lane, a token pool does not allow the requested finality, or the token has no pool on that lane. Treat a revert as "not refundable right now".

requiredFee is an estimate. Send more than this as msg.value to refundFailedMessage; the adapter returns the excess.

Access: anyone (view).

Parameters:

ParameterTypeDescription
messageIdbytes32ID of the failed inbound message.

Returns:

NameTypeDescription
canRefundboolWhether a cross-chain refund is possible now, when the call does not revert.
originalSenderbytes32Sender of the original message. The refund goes to this address on the source chain.
tokenaddressFirst inbound token with a non-zero amount.
tokenAmountuint256Amount of token.
requiredFeeuint256Estimated total fee for all refund sends, in the native gas token.

Reverts with:

  • InvalidEVMAddress(originalSender) when the refund goes to an EVM chain and the upper 96 bits of the stored sender are not zero.
  • Router, OnRamp, and token pool errors from getFee. See Errors from dependencies.

Emits: none.


estimateRefundFee

Estimates the total fee, in the native gas token, to refund a failed message cross-chain.

function estimateRefundFee(bytes32 messageId) external view returns (uint256)

Sums one router getFee quote per inbound token with a non-zero amount, using the current router state. The estimate can be lower than the cost at execution, because refundFailedMessage re-quotes each send and fees can change in between. Pad msg.value above this value.

Access: anyone (view).

Parameters:

ParameterTypeDescription
messageIdbytes32ID of the failed inbound message.

Returns:

TypeDescription
uint256Estimated total fee for all refund sends, in the hub chain's native gas token.

Reverts with:

  • MessageNotFailed(messageId) when the message is not BASIC.
  • NoRefundableTokenAmounts() when no inbound token amount is positive.
  • InvalidSenderAddressFormat() when the stored sender is not exactly 32 bytes.
  • InvalidEVMAddress(originalSender) when the refund goes to an EVM chain and the upper 96 bits of the stored sender are not zero.
  • Router, OnRamp, and token pool errors from getFee.

Emits: none.


checkLocalRecoveryEligibility

Reports whether a failed message can be recovered on the hub chain, and by which address.

function checkLocalRecoveryEligibility(bytes32 messageId)
    external
    view
    returns (bool canRecover, address localRefundAddress, address token, uint256 tokenAmount)

Returns canRecover = false with zeroed fields when the message is not BASIC, when no local refund address was stored, or when no inbound token amount is positive. Otherwise it returns true, the local refund address, and the first token with a non-zero amount. It does not check the caller, and it never reverts.

Access: anyone (view).

Parameters:

ParameterTypeDescription
messageIdbytes32ID of the failed inbound message.

Returns:

NameTypeDescription
canRecoverboolWhether the local refund address can recover the message now.
localRefundAddressaddressThe only address that can call recoverFailedMessageLocally.
tokenaddressFirst inbound token with a non-zero amount.
tokenAmountuint256Amount of token.

Reverts with: none.

Emits: none.


getFailedMessageRecord

Returns the stored record of a failed message.

function getFailedMessageRecord(bytes32 messageId) external view returns (FailedMessageRecord memory record)

The record exists only while messageErrorCode[messageId] is BASIC. For a message that never failed, or one that was refunded or recovered, the call returns a record with zero values. Read messageErrorCode to tell these states apart.

Access: anyone (view).

Parameters:

ParameterTypeDescription
messageIdbytes32ID of the inbound message.

Returns:

NameTypeDescription
recordFailedMessageRecord memorySee FailedMessageRecord.

Reverts with: none.

Emits: none.

Recovery functions

refundFailedMessage

Sends the inbound tokens of a failed message back to the original sender on the source chain.

function refundFailedMessage(bytes32 messageId) external payable nonReentrant

Anyone can call it, and the caller pays the CCIP fees through msg.value. The recipient comes from the stored record, not from the caller. It is the original message.sender on sourceChainSelector. It is not the beneficiary and not the local refund address.

The function:

  1. Requires the message to be BASIC, at least one non-zero token amount, and a sender of exactly 32 bytes.
  2. Sets the message to RESOLVED and deletes its record and chain family snapshot.
  3. Sends each non-zero token in its own token-only CCIP message, with the same fields as a return leg (see processMessage). It uses the chain family recorded at failure time, the lane's return format, and evmReturnRequestedFinality for the inbound token: the asset for a failed deposit, the share token for a failed redemption.
  4. Requires msg.value to cover the total fee paid, then sends any excess msg.value back to the caller.

The adapter's own native gas token balance is not consumed. If any step reverts, the whole transaction reverts and the message stays BASIC. If the original sender is a contract, the tokens arrive at that contract's address on the source chain, and the contract is not called.

Access: anyone.

Parameters:

ParameterTypeDescription
messageIdbytes32ID of the failed inbound message.
msg.valuenative gas tokenFee for all refund sends. Start from estimateRefundFee or checkRefundEligibility and add a margin.

Returns: none.

Reverts with:

  • MessageNotFailed(messageId) when the message is not BASIC: it was never received, it succeeded, or it is already resolved.
  • NoRefundableTokenAmounts() when no inbound token amount is positive.
  • InvalidSenderAddressFormat() when the stored sender is not exactly 32 bytes.
  • InvalidEVMAddress(originalSender) when the refund goes to an EVM chain and the upper 96 bits of the stored sender are not zero.
  • InsufficientNativeBalance(requiredFee, availableBalance) when the adapter's native gas token balance, which includes msg.value, is below the router fee for a send.
  • InsufficientRecoveryFee(requiredFee, providedFee) when msg.value is below the total fee paid.
  • RefundFailed() when sending the excess msg.value back to the caller fails.
  • ReentrancyGuardReentrantCall() on a reentrant call.
  • Router, OnRamp, and token pool errors from getFee or ccipSend.

Emits:

  • MessageSent for each token sent.
  • MessageRefunded(messageId, sourceChainSelector, originalSender).

recoverFailedMessageLocally

Transfers the inbound tokens of a failed message to its local refund address on the hub chain.

function recoverFailedMessageLocally(bytes32 messageId) external nonReentrant

Only the localRefundAddress stored in the failed-message record can call it. It costs no CCIP fee, and the function is not payable. It sets the message to RESOLVED, deletes the record and chain family snapshot, transfers each non-zero inbound token to the caller, and emits MessageRecoveredLocally. The tokens are the ones that arrived: the asset for a failed deposit, shares for a failed redemption.

The local refund address comes from the payload's deliveryAndRefund field and cannot be set or changed afterward. Whichever recovery function succeeds first resolves the message; the other then reverts with MessageNotFailed. See Local recovery.

Access: the stored localRefundAddress only.

Parameters:

ParameterTypeDescription
messageIdbytes32ID of the failed inbound message.

Returns: none.

Reverts with:

  • MessageNotFailed(messageId) when the message is not BASIC.
  • NoLocalRefundAddress(messageId) when no local refund address was stored.
  • UnauthorizedLocalRefund(caller, localRefundAddress) when the caller is not the stored local refund address.
  • NoRefundableTokenAmounts() when no inbound token amount is positive.
  • ReentrancyGuardReentrantCall() on a reentrant call.
  • Token transfer errors.

Emits:

  • MessageRecoveredLocally(messageId, localRefundAddress).

Configuration functions

setChainType

Configures how the adapter treats a remote chain selector.

function setChainType(uint64 chainSelector, ChainType chainType) external onlyRole(DEFAULT_ADMIN_ROLE)

Writes chains[chainSelector]. The adapter processes inbound messages only from selectors set to a value other than NONE, and uses the value to encode return legs to that selector. Set EVM for an EVM source chain, such as Arbitrum Sepolia (selector 3478487238524512106) for an adapter on Ethereum Sepolia.

Setting a selector to NONE does not stop CCIP delivery. Messages that arrive afterward fail with InvalidChain and become failed messages that users must refund or recover. Cross-chain refunds of earlier failures use the chain family recorded at failure time. The function does not validate the selector.

Access: DEFAULT_ADMIN_ROLE.

Parameters:

ParameterTypeDescription
chainSelectoruint64CCIP chain selector of the remote chain.
chainTypeChainTypeEVM, SVM, or NONE to reject messages from it.

Returns: none.

Reverts with:

  • AccessControlUnauthorizedAccount(account, neededRole) when the caller lacks the role.

Emits:

  • ChainTypeSet(chainSelector, chainType).

setTargetEnabled

Adds a vault to the allowlist or removes it.

function setTargetEnabled(address target, bool enabled) external onlyRole(DEFAULT_ADMIN_ROLE)

Writes enabledTargets[target]. The payload's target selects the vault, so one adapter can serve several vaults. The function accepts any non-zero address and does not check that it is an ERC-4626 vault. Enable only vaults whose asset and share token behave as standard ERC-20 tokens, without fee-on-transfer or rebasing. See Vault and token compatibility.

Disabling a vault does not stop delivery. Messages that arrive for it fail with InvalidTarget and become failed messages.

Access: DEFAULT_ADMIN_ROLE.

Parameters:

ParameterTypeDescription
targetaddressVault address.
enabledbooltrue to allowlist, false to remove.

Returns: none.

Reverts with:

  • InvalidTarget(address(0)) when target is the zero address.
  • AccessControlUnauthorizedAccount(account, neededRole) when the caller lacks the role.

Emits:

  • TargetEnabled(target, enabled).

setProcessingEnabled

Switches deposits and redemptions on or off for every vault on the adapter.

function setProcessingEnabled(bool depositsEnabled_, bool redeemsEnabled_) external onlyRole(DEFAULT_ADMIN_ROLE)

Sets depositsEnabled and redeemsEnabled. Both start as false on a directly deployed adapter.

This is not a pause. CCIP keeps delivering, and messages that arrive while their path is off, including messages already in flight, fail with DepositsDisabled or RedeemsDisabled and become failed messages. Switching the path back on does not reprocess them. See What counts as a failure.

Access: DEFAULT_ADMIN_ROLE.

Parameters:

ParameterTypeDescription
depositsEnabled_boolWhether to process deposits.
redeemsEnabled_boolWhether to process redemptions.

Returns: none.

Reverts with:

  • AccessControlUnauthorizedAccount(account, neededRole) when the caller lacks the role.

Emits:

  • ProcessingEnabledSet(depositsEnabled, redeemsEnabled).

setCCVsConfig

Sets the CCV policy for messages from a source chain on CCIP 2.0 lanes.

function setCCVsConfig(
    uint64 sourceChainSelector,
    address[] calldata requiredCCVs,
    address[] calldata optionalCCVs,
    uint8 optionalThreshold_
) external onlyRole(DEFAULT_ADMIN_ROLE)

Replaces the stored CCVConfig for sourceChainSelector, which getCCVsAndFinalityConfig returns to the OffRamp. CCIP 1.6 lanes ignore it. Empty lists with a zero threshold mean the lane's default CCVs. In requiredCCVs, address(0) stands for the lane's default CCVs.

The function validates the lists:

  • optionalThreshold_ must be greater than 0 when optionalCCVs is not empty, and no greater than optionalCCVs.length.
  • No address can appear twice in one list, or in both lists.
  • optionalCCVs cannot contain address(0).

It does not check that the CCVs exist or serve the lane. If a required CCV never attests, messages from that source chain fail at the CCIP level before they reach the adapter. Fix the configuration, then retry them with manual execution.

Access: DEFAULT_ADMIN_ROLE.

Parameters:

ParameterTypeDescription
sourceChainSelectoruint64Source chain the policy applies to.
requiredCCVsaddress[] calldataCCVs that must all attest.
optionalCCVsaddress[] calldataOptional CCVs.
optionalThreshold_uint8Number of optional CCVs that must attest.

Returns: none.

Reverts with:

  • OptionalCCVsRequirePositiveThreshold(optionalCCVCount) when optionalCCVs is not empty and the threshold is 0.
  • InvalidOptionalThreshold(optionalThreshold, optionalCCVCount) when the threshold is greater than the number of optional CCVs.
  • DuplicateCCV(ccv) when an address repeats within or across the lists.
  • InvalidOptionalCCV() when optionalCCVs contains address(0).
  • AccessControlUnauthorizedAccount(account, neededRole) when the caller lacks the role.

Emits:

  • CCVsConfigSet(sourceChainSelector, requiredCCVs, optionalCCVs, optionalThreshold).

setInboundFinality

Sets which requested finality the adapter accepts for messages from a source chain on CCIP 2.0 lanes.

function setInboundFinality(uint64 sourceChainSelector, bytes4 allowedFinalityConfig)
    external
    onlyRole(DEFAULT_ADMIN_ROLE)

Writes inboundFinality[sourceChainSelector], which getCCVsAndFinalityConfig returns as allowedFinalityConfig. CCIP 2.0 OffRamps compare each message's requested finality against it; CCIP 1.6 lanes ignore it. The default, 0x00000000, accepts only messages that wait for finality, so faster-than-finality messages from that source chain fail at the CCIP level.

The value uses the FinalityCodec layout. Bit 16 is the safe tag flag, and the low 16 bits are a block depth. A message that requests the safe tag is accepted when the flag is set. A message that requests N blocks is accepted when the configured depth is non-zero and N is at least that depth. A message that waits for finality is always accepted. The function performs no validation.

Access: DEFAULT_ADMIN_ROLE.

Parameters:

ParameterTypeDescription
sourceChainSelectoruint64Source chain the setting applies to.
allowedFinalityConfigbytes4Allowed finality, in the FinalityCodec layout.

Returns: none.

Reverts with:

  • AccessControlUnauthorizedAccount(account, neededRole) when the caller lacks the role.

Emits:

  • InboundFinalitySet(sourceChainSelector, allowedFinalityConfig).

setEvmReturnLaneFormat

Sets the extraArgs format for return legs and refunds to an EVM destination chain.

function setEvmReturnLaneFormat(uint64 destinationChainSelector, EvmReturnExtraArgsFormat format)
    external
    onlyRole(DEFAULT_ADMIN_ROLE)

destinationChainSelector is the source chain of the inbound messages whose outputs and refunds go back to it. Set GENERIC_EXTRA_ARGS_V3_BASIC on CCIP 2.0 lanes, such as Arbitrum Sepolia for an adapter on Ethereum Sepolia. Leave the format unset or set LEGACY_EXTRA_ARGS_V2 on CCIP 1.6 lanes.

Setting GENERIC_EXTRA_ARGS_V3_BASIC on a CCIP 1.6 lane makes return legs and refunds revert at the router. Return-to-source messages then become failed messages, and cross-chain refunds to that chain revert until the format is corrected.

UNSET cannot be written. To return to the legacy encoding, set LEGACY_EXTRA_ARGS_V2.

Access: DEFAULT_ADMIN_ROLE.

Parameters:

ParameterTypeDescription
destinationChainSelectoruint64Chain that return legs and refunds go to.
formatEvmReturnExtraArgsFormatLEGACY_EXTRA_ARGS_V2 or GENERIC_EXTRA_ARGS_V3_BASIC.

Returns: none.

Reverts with:

  • InvalidEvmReturnExtraArgsFormat() when format is UNSET.
  • AccessControlUnauthorizedAccount(account, neededRole) when the caller lacks the role.

Emits:

  • EvmReturnLaneFormatSet(destinationChainSelector, format).

setEvmReturnRequestedFinality

Sets the finality that return legs and refunds of one token request on a CCIP 2.0 lane.

function setEvmReturnRequestedFinality(
    uint64 destinationChainSelector,
    address token,
    bytes4 requestedFinalityForV3
) external onlyRole(DEFAULT_ADMIN_ROLE)

token is the token bridged on that leg. It is the share token for deposit returns and failed-redemption refunds, and the asset for redemption returns and failed-deposit refunds.

A non-zero value requires the lane format for destinationChainSelector to already be GENERIC_EXTRA_ARGS_V3_BASIC. On any other format, passing 0x00000000 clears the stored value. On a V3 lane, the value must select exactly one mode. The token pools on the lane must allow the requested finality. If they do not, the router rejects the send, return legs become failed messages, and refunds revert.

The requested finality takes one of these values:

ValueMeaning
0x00000000Wait for finality. This is the default.
0x00010000Wait for the safe tag.
0x00000001 to 0x0000FFFFWait for that many blocks.

Other single flags in bits 17 to 31 pass the adapter's validation, but token pools and CCVs reject modes they do not implement.

Access: DEFAULT_ADMIN_ROLE.

Parameters:

ParameterTypeDescription
destinationChainSelectoruint64Chain that return legs and refunds go to.
tokenaddressToken bridged on the leg. Must be non-zero.
requestedFinalityForV3bytes4Requested finality, in the FinalityCodec layout.

Returns: none.

Reverts with:

  • InvalidTarget(address(0)) when token is the zero address.
  • UnexpectedRequestedFinalityForLegacyFormat(requestedFinalityForV3) when the value is non-zero and the lane format is not GENERIC_EXTRA_ARGS_V3_BASIC.
  • RequestedFinalityCanOnlyHaveOneMode(encodedFinality) from FinalityCodec when the value combines a flag with a block depth or sets more than one flag.
  • AccessControlUnauthorizedAccount(account, neededRole) when the caller lacks the role.

Emits:

  • EvmReturnRequestedFinalitySet(destinationChainSelector, token, requestedFinalityForV3), with 0x00000000 when the call clears the value on a non-V3 lane.

recoverNative

Sends the adapter's entire native gas token balance to a recipient.

function recoverNative(address recipient) external nonReentrant onlyRole(DEFAULT_ADMIN_ROLE)

There is no amount parameter, so the call always sends the full balance. Afterward the adapter cannot pay CCIP fees, so return-to-source messages fail with InsufficientNativeBalance until someone funds it again.

Access: DEFAULT_ADMIN_ROLE.

Parameters:

ParameterTypeDescription
recipientaddressReceives the native gas token balance.

Returns: none.

Reverts with:

  • InvalidRecipient() when recipient is the zero address.
  • AmountIsZero() when the balance is zero.
  • RecoverNativeFailed() when the transfer to recipient fails.
  • ReentrancyGuardReentrantCall() on a reentrant call.
  • AccessControlUnauthorizedAccount(account, neededRole) when the caller lacks the role.

Emits:

  • NativeRecovered(recipient, amount).

Fee functions

setAssetFee

Sets the adapter fee for returns of one token to one chain.

function setAssetFee(uint64 destinationChainSelector, address bridgedToken, uint256 fee)
    external
    onlyRole(FEE_SETTER_ROLE)

Writes assetFees[destinationChainSelector][bridgedToken]. The adapter fee is a flat amount in the asset's smallest units on both paths. It applies only when the output returns to the source chain.

  • Deposit return: the key is the share token (the vault address). The fee is taken from the inbound asset before the deposit.
  • Redemption return: the key is the asset. The fee is taken from the redeemed assets.

There is no upper bound. A fee greater than or equal to the amount makes those messages fail with FeeExceedsAmount. The fee is read when a message executes, so a change applies to messages already in flight. You can set a fee before configuring the chain with setChainType. Set 0 to remove a fee. See Fees.

Access: FEE_SETTER_ROLE.

Parameters:

ParameterTypeDescription
destinationChainSelectoruint64Chain the output returns to, which is the source chain of the inbound message.
bridgedTokenaddressToken bridged back, which is the share token for deposits and the asset for redemptions.
feeuint256Fee in the asset's smallest units.

Returns: none.

Reverts with:

  • InvalidTarget(address(0)) when bridgedToken is the zero address.
  • AccessControlUnauthorizedAccount(account, neededRole) when the caller lacks the role.

Emits:

  • AssetFeeSet(destinationChainSelector, bridgedToken, fee).

setAssetFees

Sets several adapter fees in one call.

function setAssetFees(
    uint64[] calldata destinationChainSelectors,
    address[] calldata bridgedTokens,
    uint256[] calldata fees
) external onlyRole(FEE_SETTER_ROLE)

Each index applies one setAssetFee row with the same validation. A zero token in any row reverts the whole call.

Access: FEE_SETTER_ROLE.

Parameters:

ParameterTypeDescription
destinationChainSelectorsuint64[] calldataReturn chain per row.
bridgedTokensaddress[] calldataToken bridged back per row.
feesuint256[] calldataFee per row, in the asset's smallest units.

Returns: none.

Reverts with:

  • FeeConfigLengthMismatch() when the three arrays differ in length.
  • InvalidTarget(address(0)) when any bridgedTokens entry is the zero address.
  • AccessControlUnauthorizedAccount(account, neededRole) when the caller lacks the role.

Emits:

  • AssetFeeSet(destinationChainSelector, bridgedToken, fee) for each row.

withdrawFee

Transfers accrued adapter fees to a recipient.

function withdrawFee(address asset, address recipient, uint256 amount)
    external
    nonReentrant
    onlyRole(FEE_COLLECTOR_ROLE)

Reduces collectedFees[asset] by amount and transfers that amount. Partial withdrawals are allowed. The function withdraws only fees the adapter has accounted for. The adapter has no general ERC-20 sweep, so tokens sent to it directly, outside a CCIP message, cannot be recovered.

Access: FEE_COLLECTOR_ROLE.

Parameters:

ParameterTypeDescription
assetaddressAsset whose fees to withdraw.
recipientaddressReceives the fees.
amountuint256Amount, in the asset's smallest units.

Returns: none.

Reverts with:

  • InvalidRecipient() when recipient is the zero address.
  • AmountIsZero() when amount is zero.
  • InsufficientFeeBalance(availableBalance, requestedAmount) when amount exceeds collectedFees[asset].
  • Token transfer errors, for example when the adapter holds less of the asset than its accounting allows.
  • ReentrancyGuardReentrantCall() on a reentrant call.
  • AccessControlUnauthorizedAccount(account, neededRole) when the caller lacks the role.

Emits:

  • FeeWithdrawn(asset, recipient, amount).

Events

The adapter declares these 17 events:

event MessageSent(
    bytes32 indexed messageId,
    uint64 indexed destinationChainSelector,
    ChainType indexed chainType,
    bytes32 beneficiary,
    address token,
    uint256 amount,
    uint256 fee
);

event MessageSucceeded(bytes32 indexed messageId);

event MessageFailed(bytes32 indexed messageId);

event MessageRefunded(
    bytes32 indexed messageId, uint64 indexed destinationChainSelector, bytes32 indexed beneficiary
);

event MessageRecoveredLocally(bytes32 indexed messageId, address indexed localRefundAddress);

event TargetProcessed(
    bytes32 indexed messageId,
    address indexed target,
    address indexed inputToken,
    address outputToken,
    uint256 inputAmount,
    uint256 outputAmount
);

event TargetEnabled(address indexed target, bool enabled);
event ChainTypeSet(uint64 indexed chainSelector, ChainType chainType);

event ProcessingEnabledSet(bool depositsEnabled, bool redeemsEnabled);
event AssetFeeSet(uint64 indexed destinationChainSelector, address indexed bridgedToken, uint256 fee);
event CCVsConfigSet(
    uint64 indexed sourceChainSelector, address[] requiredCCVs, address[] optionalCCVs, uint8 optionalThreshold
);

event InboundFinalitySet(uint64 indexed sourceChainSelector, bytes4 allowedFinalityConfig);

event EvmReturnLaneFormatSet(uint64 indexed destinationChainSelector, EvmReturnExtraArgsFormat format);

event EvmReturnRequestedFinalitySet(
    uint64 indexed destinationChainSelector, address indexed token, bytes4 requestedFinalityForV3
);

event FeeWithdrawn(address indexed asset, address indexed recipient, uint256 amount);

event NativeRecovered(address indexed recipient, uint256 amount);

event LocalTokenDelivered(
    bytes32 indexed messageId, address indexed token, address indexed beneficiary, uint256 amount
);
EventEmitted whenUse it to
MessageSucceededprocessMessage completes inside ccipReceive.Mark an inbound message as processed.
MessageFailedprocessMessage reverts and the adapter records a failed message.Alert on failures and offer the user a cross-chain refund or local recovery. The CCIP Explorer still shows Success.
TargetProcessedThe vault deposit or redemption completes, before delivery.Record the inbound amount and the output after the adapter fee for each message.
LocalTokenDeliveredThe output is transferred to the beneficiary on the hub chain.Confirm local delivery.
MessageSentThe adapter sends a CCIP message: a return leg, or one leg of a cross-chain refund.Track the outbound message ID on the CCIP Explorer and the fee paid in the native gas token. messageId here is the outbound ID.
MessageRefundedrefundFailedMessage completes. destinationChainSelector is the original source chain and beneficiary the original sender.Close a failed message as refunded.
MessageRecoveredLocallyrecoverFailedMessageLocally completes.Close a failed message as recovered on the hub chain.
TargetEnabledsetTargetEnabled runs.Review the vault allowlist.
ChainTypeSetsetChainType runs.Review which source chains are configured.
ProcessingEnabledSetsetProcessingEnabled runs.Warn users before they send a request that will fail.
AssetFeeSetsetAssetFee runs, or once per row of setAssetFees.Update the adapter fee shown in a UI.
CCVsConfigSetsetCCVsConfig runs.Review the CCV policy per source chain.
InboundFinalitySetsetInboundFinality runs.Review which faster-than-finality modes are accepted.
EvmReturnLaneFormatSetsetEvmReturnLaneFormat runs.Review return formats per lane.
EvmReturnRequestedFinalitySetsetEvmReturnRequestedFinality runs.Review return finality per lane and token.
FeeWithdrawnwithdrawFee runs.Reconcile fee withdrawals.
NativeRecoveredrecoverNative runs.Raise an alert, because the adapter can no longer pay for return legs until it is funded again.

TargetProcessed, LocalTokenDelivered, and the return-leg MessageSent are emitted inside processMessage. When processing fails they roll back, so a failed message emits only MessageFailed. On success, the inbound message's TargetProcessed and MessageSucceeded appear in the same transaction as the return leg's MessageSent, which links the inbound and outbound message IDs.

The adapter also inherits RoleGranted and RoleRevoked from OpenZeppelin AccessControl. Watch them for role changes. RoleAdminChanged is part of the ABI, but the adapter never emits it because it never changes a role's admin.

Errors

Adapter errors

The adapter declares these 35 custom errors:

error InvalidRouter(address router);
error InvalidAdmin();
error InvalidFeeSetter();
error InvalidFeeCollector();
error InvalidRecipient();
error InvalidChain(uint64 chainSelector);
error OnlySelf();
error AmountIsZero();
error InvalidEVMAddress(bytes32 beneficiary);
error InvalidSenderAddressFormat();
error InsufficientNativeBalance(uint256 requiredFee, uint256 availableBalance);
error InsufficientRecoveryFee(uint256 requiredFee, uint256 providedFee);
error InsufficientFeeBalance(uint256 availableBalance, uint256 requestedAmount);
error MessageNotFailed(bytes32 messageId);
error InvalidTarget(address target);
error InvalidTargetToken(address target, address token);
error InvalidTokenCount(uint256 tokenCount);
error InvalidPayloadLength(uint256 length, uint256 expected);
error FeeConfigLengthMismatch();
error InvalidEvmReturnExtraArgsFormat();
error UnexpectedRequestedFinalityForLegacyFormat(bytes4 requestedFinalityForV3);
error InvalidOptionalThreshold(uint8 optionalThreshold, uint256 optionalCCVCount);
error OptionalCCVsRequirePositiveThreshold(uint256 optionalCCVCount);
error DuplicateCCV(address ccv);
error InvalidOptionalCCV();
error DepositsDisabled();
error RedeemsDisabled();
error FeeExceedsAmount(uint256 amount, uint256 fee);
error MinimumOutputNotMet(uint256 minimumOut, uint256 actualOut);
error NoOutputReceived();
error RefundFailed();
error RecoverNativeFailed();
error NoRefundableTokenAmounts();
error NoLocalRefundAddress(bytes32 messageId);
error UnauthorizedLocalRefund(address caller, address localRefundAddress);

"Becomes a failed message" is yes when the error is raised while processMessage handles an inbound message, because ccipReceive catches it and records a failed message. Errors raised by views, recovery functions, and setters revert the call directly.

ErrorRaised byMeaningBecomes a failed message
InvalidRouterConstructor, ccipReceiveThe router is the zero address at construction, or the caller of ccipReceive is not ROUTER.No
InvalidAdminConstructordefaultAdmin is the zero address. The factory reuses this error.No
InvalidFeeSetterConstructorfeeSetter is the zero address. The factory reuses this error.No
InvalidFeeCollectorConstructorfeeCollector is the zero address. The factory reuses this error.No
InvalidRecipientwithdrawFee, recoverNativeThe recipient is the zero address.No
InvalidChainprocessMessage, previewThe source chain selector is NONE in chains.Yes from processMessage
OnlySelfprocessMessageAn address other than the adapter called processMessage.No
AmountIsZeroprocessMessage, withdrawFee, recoverNativeThe inbound amount, the withdrawal amount, or the native gas token balance is zero.Yes from processMessage
InvalidEVMAddressprocessMessage, refundFailedMessage, estimateRefundFee, checkRefundEligibilityThe beneficiary or the original sender has non-zero upper 96 bits.Yes from processMessage
InvalidSenderAddressFormatrefundFailedMessage, estimateRefundFeeThe stored sender is not exactly 32 bytes.No
InsufficientNativeBalanceprocessMessage (return leg), refundFailedMessageThe native gas token balance available for a send is below the router fee.Yes from processMessage
InsufficientRecoveryFeerefundFailedMessagemsg.value is below the total fee paid for the refund.No
InsufficientFeeBalancewithdrawFeeamount exceeds collectedFees[asset].No
MessageNotFailedrefundFailedMessage, recoverFailedMessageLocally, estimateRefundFeeThe message is not BASIC.No
InvalidTargetprocessMessage, preview, setTargetEnabled, setAssetFee, setAssetFees, setEvmReturnRequestedFinalityThe vault is not allowlisted, or a setter received the zero address.Yes from processMessage
InvalidTargetTokenprocessMessage, previewThe token is neither the vault's asset nor its share token.Yes from processMessage
InvalidTokenCountprocessMessageThe message does not carry exactly one token.Yes
InvalidPayloadLengthprocessMessagemessage.data is not 128 bytes.Yes
FeeConfigLengthMismatchsetAssetFeesThe three arrays differ in length.No
InvalidEvmReturnExtraArgsFormatsetEvmReturnLaneFormatThe format is UNSET.No
UnexpectedRequestedFinalityForLegacyFormatsetEvmReturnRequestedFinalityA non-zero finality was set for a lane whose format is not GENERIC_EXTRA_ARGS_V3_BASIC.No
InvalidOptionalThresholdsetCCVsConfigThe threshold is greater than the number of optional CCVs.No
OptionalCCVsRequirePositiveThresholdsetCCVsConfigOptional CCVs were given with a threshold of 0.No
DuplicateCCVsetCCVsConfigAn address repeats within or across the CCV lists.No
InvalidOptionalCCVsetCCVsConfigoptionalCCVs contains the zero address.No
DepositsDisabledprocessMessage, previewDeposits are switched off.Yes from processMessage
RedeemsDisabledprocessMessage, previewRedemptions are switched off.Yes from processMessage
FeeExceedsAmountprocessMessageThe adapter fee is greater than or equal to the deposit input or the redeemed assets. preview returns 0 instead.Yes
MinimumOutputNotMetprocessMessageThe output after the adapter fee is below minimumOut.Yes
NoOutputReceivedprocessMessageThe vault returned zero shares or zero assets.Yes
RefundFailedrefundFailedMessageSending the excess msg.value back to the caller failed.No
RecoverNativeFailedrecoverNativeThe native gas token transfer to the recipient failed.No
NoRefundableTokenAmountsrefundFailedMessage, recoverFailedMessageLocally, estimateRefundFeeNo inbound token amount is positive.No
NoLocalRefundAddressrecoverFailedMessageLocallyNo local refund address was stored for the message.No
UnauthorizedLocalRefundrecoverFailedMessageLocallyThe caller is not the stored local refund address.No

Errors from dependencies

Callers can also see these errors from inherited contracts, libraries, and CCIP contracts:

ErrorSourceRaised when
AccessControlUnauthorizedAccount(address account, bytes32 neededRole)OpenZeppelin AccessControlThe caller lacks the role for a configuration or fee function, or the admin role for grantRole or revokeRole.
AccessControlBadConfirmation()OpenZeppelin AccessControlrenounceRole is called with an account other than the caller.
ReentrancyGuardReentrantCall()OpenZeppelin ReentrancyGuardrefundFailedMessage, recoverFailedMessageLocally, withdrawFee, or recoverNative is re-entered.
RequestedFinalityCanOnlyHaveOneMode(bytes4 encodedFinality)FinalityCodec, in setEvmReturnRequestedFinalityThe requested finality combines a flag with a block depth or sets more than one flag.
UnsupportedDestinationChain(uint64 destChainSelector)CCIP Router, in getFee and ccipSendThe hub-chain router has no lane to the return or refund destination.
BadARMSignal()CCIP Router, in ccipSendThe hub chain is cursed through the RMN contract, so sends revert until the curse is lifted.
InvalidRequestedFinality(bytes4 requestedFinality, bytes4 allowedFinality)FinalityCodec, raised by a token pool in getFee or ccipSendThe token pool does not allow the return leg's requested finality.

OnRamps, fee quoters, token pools, vaults, and tokens can raise other errors during a send or vault call. SafeERC20 from OpenZeppelin 4.8.3 reverts with string messages such as SafeERC20: ERC20 operation did not succeed. Inside processMessage, all of these are caught and the message becomes a failed message. In refund functions and views, they revert the call.

Security notes

Processing failures are caught. A revert in processMessage does not revert CCIP execution, except for the malformed payload described below. The adapter keeps the tokens as a failed message, and the CCIP Explorer shows Success. Detect failures with MessageFailed or messageErrorCode, not with the CCIP Explorer status. Nothing refunds or retries a failed message automatically.

No sender allowlist. Any sender on a chain configured in chains can deposit into or redeem from any allowlisted vault. getCCVsAndFinalityConfig ignores the sender. The adapter gates requests only by source chain, vault, and the deposit and redeem switches. The vault sees the adapter as its depositor, so a vault that allowlists depositors admits every cross-chain user once it allowlists the adapter.

Admin powers. DEFAULT_ADMIN_ROLE holders can change chain types, the vault allowlist, the deposit and redeem switches, CCV and finality policy, and return formats; withdraw the entire native gas token balance; and grant or revoke every role. FEE_SETTER_ROLE holders can change the adapter fee, which applies to messages already in flight; minimumOut is the user's protection against that. Every change takes effect in the transaction that makes it. No role can transfer the tokens of a failed message. Only refundFailedMessage and recoverFailedMessageLocally move them, and withdrawFee is limited to collectedFees.

Reentrancy. refundFailedMessage, recoverFailedMessageLocally, withdrawFee, and recoverNative use nonReentrant. Both recovery functions mark the message RESOLVED and delete its record before they move tokens.

The adapter pays CCIP fees only in the native gas token. Return legs pay the CCIP fee from the adapter's native gas token balance, never in LINK. When the balance is below the fee, return-to-source messages fail with InsufficientNativeBalance. Cross-chain refunds are paid from the caller's msg.value. The sender pays the source-chain CCIP fee in the native gas token or LINK.

Outputs return only to the source chain. Return legs and refunds go to the inbound message's source chain. The adapter cannot route output to a third chain. Refunds go to the original sender, so a refund for a message sent through a contract arrives at that contract. Return-leg extraArgs come from the adapter's configuration, never from message data.

Malformed payloads. A hand-built payload can make ccipReceive itself revert and strand the tokens permanently, as explained in Failures with no recovery path.

Token-only messages. A CCIP message with empty data and a gas limit of 0 is a token-only transfer. The OffRamp delivers the tokens to the adapter without calling ccipReceive, so the adapter records nothing and has no function that returns them. An ERC-20 transfer sent straight to the adapter ends up in the same state. See When funds can get stuck.

Forks. If you fork the adapter, keep the router check in ccipReceive, the self-call isolation of processMessage, failed-message storage and both recovery paths, and the beneficiary address checks. The failure handler, the catch path in ccipReceive, must never revert on any input. See Customizing the adapter.

Get the latest Chainlink content straight to your inbox.