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, andFEE_COLLECTOR_ROLE
Usage boundary
- Only the CCIP Router set at construction (
ROUTER) can callccipReceive. Any other caller getsInvalidRouter. processMessageisexternalonly so thatccipReceivecan wrap it intry/catch. Any caller other than the adapter itself getsOnlySelf.- The CCIP OffRamp calls
supportsInterfaceto confirm the adapter is a CCIP receiver. On CCIP 2.0 lanes it also readsgetCCVsAndFinalityConfigbefore 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 callrecoverFailedMessageLocally. - 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
virtualhooks. To change it, fork the contract and editprocessMessageand its private helpers. See Use it as-is or customize it.
Contract
Source: src/ccip/CrossChainERC4626Adapter.sol
| Property | Value |
|---|---|
| Contract | CrossChainERC4626Adapter |
typeAndVersion | CrossChainERC4626Adapter 1.0.0 |
| License | MIT |
| Audit status | Audited |
| Compiler | solc 0.8.24, via_ir enabled, optimizer runs 1, EVM version cancun |
| Minimum EVM | Shanghai 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 | Version | Imports |
|---|---|---|
@chainlink/contracts-ccip | 2.0.0 | Client, ExtraArgsCodec, FinalityCodec, IRouterClient, IAny2EVMMessageReceiver, IAny2EVMMessageReceiverV2 |
@chainlink/contracts | 1.5.0 | ITypeAndVersion |
| OpenZeppelin Contracts | 5.0.2 | AccessControlEnumerable, ReentrancyGuard, IERC4626 |
| OpenZeppelin Contracts | 4.8.3 | IERC20, SafeERC20 |
Inheritance
IAny2EVMMessageReceiverV2:ccipReceiveandgetCCVsAndFinalityConfigAccessControlEnumerable(OpenZeppelin 5.0.2): roles and role enumeration. See Roles.ReentrancyGuard(OpenZeppelin 5.0.2): thenonReentrantmodifier onrefundFailedMessage,recoverFailedMessageLocally,withdrawFee, andrecoverNativeITypeAndVersion: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_ | address | CCIP Router on the hub chain, stored as the immutable ROUTER. Get the address from the CCIP Directory for testnet or mainnet. |
defaultAdmin | address | Receives DEFAULT_ADMIN_ROLE, FEE_SETTER_ROLE, and FEE_COLLECTOR_ROLE. |
feeSetter | address | Receives FEE_SETTER_ROLE. |
feeCollector | address | Receives FEE_COLLECTOR_ROLE. |
Reverts with:
InvalidRouter(address(0))whenrouter_is the zero address.InvalidAdmin()whendefaultAdminis the zero address.InvalidFeeSetter()whenfeeSetteris the zero address.InvalidFeeCollector()whenfeeCollectoris the zero address.
Emits:
RoleGrantedfor each new role grant: three fordefaultAdmin, plus one each forfeeSetterandfeeCollectorwhen they differ fromdefaultAdmin.
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:
| Role | Constant | Value | Granted by | Can call |
|---|---|---|---|---|
| Admin | DEFAULT_ADMIN_ROLE | 0x00 | Constructor (defaultAdmin), factory (defaultAdmin), or an existing admin | setChainType, setTargetEnabled, setProcessingEnabled, setCCVsConfig, setInboundFinality, setEvmReturnLaneFormat, setEvmReturnRequestedFinality, recoverNative, and grantRole and revokeRole for all three roles |
| Fee setter | FEE_SETTER_ROLE | keccak256("FEE_SETTER_ROLE") | Constructor (feeSetter and defaultAdmin), factory (feeSetter only), or an admin | setAssetFee, setAssetFees |
| Fee collector | FEE_COLLECTOR_ROLE | keccak256("FEE_COLLECTOR_ROLE") | Constructor (feeCollector and defaultAdmin), factory (feeCollector only), or an admin | withdrawFee |
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 |
|---|---|---|
target | address | Vault to call. Must be allowlisted with setTargetEnabled, or processing fails with InvalidTarget. |
beneficiary | bytes32 | Recipient 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. |
minimumOut | uint256 | Smallest 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. |
deliveryAndRefund | uint256 | Delivery choice and local refund address, packed into one word as shown in the next table. |
deliveryAndRefund uses this bit layout:
Bits | Field | Meaning |
|---|---|---|
| 0 | returnToSourceChain | 1 bridges the output back to message.sourceChainSelector. 0 delivers it to beneficiary on the hub chain. |
| 1 to 160 | localRefundAddress | Hub-chain address that can call recoverFailedMessageLocally if processing fails. address(0) disables local recovery. |
| 161 to 255 | Unused | Ignored on decode and not validated. |
The adapter enforces these rules on every message:
message.destTokenAmountsmust hold exactly one entry. Otherwise processing fails withInvalidTokenCount.message.datamust be exactlyCCIP_MESSAGE_PAYLOAD_LENGTH(128) bytes, the length ofabi.encodeover the four static fields. Otherwise processing fails withInvalidPayloadLength.- 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 withInvalidTargetToken.
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
}
| Value | Encoding | Use 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 true | CCIP 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
}
| Value | Meaning |
|---|---|
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;
}
| Field | Type | Description |
|---|---|---|
sourceChainSelector | uint64 | Chain the message came from. Cross-chain refunds go back to this chain. |
sender | bytes | Original message.sender. CCIP encodes an EVM sender as 32 bytes. Cross-chain refunds go to this address. |
destTokenAmounts | Client.EVMTokenAmount[] | Tokens and amounts the adapter received and now holds. |
localRefundAddress | address | Decoded 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;
}
| Field | Type | Description |
|---|---|---|
requiredCCVs | address[] | CCVs that must all attest to a message. address(0) stands for the lane's default CCVs. |
optionalCCVs | address[] | CCVs from which at least optionalThreshold must attest. |
optionalThreshold | uint8 | Number 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:
| Getter | Returns | Description |
|---|---|---|
ROUTER() | address | Immutable CCIP Router. The only caller allowed into ccipReceive, and the router used for getFee and ccipSend on return legs and refunds. |
typeAndVersion() | string | Constant "CrossChainERC4626Adapter 1.0.0". |
DEFAULT_ADMIN_ROLE() | bytes32 | Inherited constant 0x00. |
FEE_SETTER_ROLE() | bytes32 | Constant keccak256("FEE_SETTER_ROLE"). |
FEE_COLLECTOR_ROLE() | bytes32 | Constant keccak256("FEE_COLLECTOR_ROLE"). |
CCIP_MESSAGE_PAYLOAD_LENGTH() | uint256 | Constant 128. Required length of message.data. |
depositsEnabled() | bool | Whether deposits are processed, for every vault on the adapter. |
redeemsEnabled() | bool | Whether redemptions are processed, for every vault on the adapter. |
inboundFinality(uint64 sourceChainSelector) | bytes4 | Allowed finality for messages from the source chain on CCIP 2.0 lanes. Default 0x00000000 accepts only messages that wait for finality. |
chains(uint64 chainSelector) | ChainType | Chain family of a remote selector. NONE means not configured. |
enabledTargets(address target) | bool | Whether a vault is allowlisted. |
assetFees(uint64 destinationChainSelector, address bridgedToken) | uint256 | Adapter fee in the asset's smallest units, keyed by the return chain and the token bridged back. |
collectedFees(address asset) | uint256 | Adapter fees accrued and not yet withdrawn. |
refundChainFamilySnapshot(bytes32 messageId) | ChainType | Chain 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) | ErrorCode | Processing outcome of an inbound message. |
evmReturnExtraArgsFormat(uint64 destinationChainSelector) | EvmReturnExtraArgsFormat | Return and refund extraArgs format for a destination chain. |
evmReturnRequestedFinality(uint64 destinationChainSelector, address token) | bytes4 | Requested 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)insidetry/catch.
- On success, emits
MessageSucceeded.messageErrorCodestaysNONE.- On a revert, sets
messageErrorCodetoBASIC, stores aFailedMessageRecord, records the source chain's family inrefundChainFamilySnapshot, and emitsMessageFailed. Ifchainshas no entry for the source chain, the family is inferred from the sender's format. The one exception, a malformedtargetword, is listed under Reverts with.When processing fails, every effect of
processMessagerolls 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. Thecatchis 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:
| Parameter | Type | Description |
|---|---|---|
message | Client.Any2EVMMessage calldata | Inbound message: message ID, source chain selector, sender, data, and delivered tokens. |
Returns: none.
Reverts with:
InvalidRouter(msg.sender)when the caller is notROUTER.- 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
catchblock when a 128-byte payload has atargetword 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 successfulprocessMessage.MessageFailed(messageId)whenprocessMessagereverts.
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:
- Requires the caller to be the adapter, and
chains[message.sourceChainSelector]to be configured (notNONE).- Requires exactly one entry in
message.destTokenAmounts.- Requires
message.datato be exactly 128 bytes, then decodes it asPayload.- Requires
enabledTargets[payload.target].- Requires a non-zero token amount.
- 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. Callsdeposit(assets, address(this))with the remaining amount.- Share token (redeem): requires
redeemsEnabled. Callsredeem(shares, address(this), address(this)). When returning to the source chain, subtracts the adapter fee from the redeemed assets.- Requires a non-zero output of at least
minimumOut, then emitsTargetProcessed.- Delivers the output. With
returnToSourceChainset, it sends a CCIP message tobeneficiaryonmessage.sourceChainSelector. Otherwise it transfers the output tobeneficiaryon the hub chain and emitsLocalTokenDelivered.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.bridgedTokenis the share token for a deposit return and the asset for a redemption return. Fees accrue incollectedFees[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:
| Field | Value |
|---|---|
receiver | abi.encode(beneficiary) |
data | Empty |
tokenAmounts | One entry: the output token and the output amount after the adapter fee |
extraArgs | Built 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. |
feeToken | address(0), so the CCIP fee is paid in the native gas token, never LINK |
Access: the adapter itself (onlySelf).
Parameters:
| Parameter | Type | Description |
|---|---|---|
message | Client.Any2EVMMessage calldata | Inbound 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)whenmessage.datais not 128 bytes.- A decode revert without error data when the payload is 128 bytes but
targetdoes not fit in 160 bits. This case does not become a failed message. SeeccipReceiveand Failures with no recovery path. InvalidTarget(target)when the vault is not allowlisted.AmountIsZero()when the token amount is zero.DepositsDisabled()orRedeemsDisabled()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 belowminimumOut.InvalidEVMAddress(beneficiary)when the upper 96 bits ofbeneficiaryare 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:
TargetProcessedafter the vault call.LocalTokenDeliveredfor local delivery, orMessageSentfor 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
CCVConfigandinboundFinalityforsourceChainSelector. 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 thatallowedFinalityConfigdoes 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
allowedFinalityConfigof0x00000000, the default, accepts only messages that wait for finality.
Access: anyone (view).
Parameters:
| Parameter | Type | Description |
|---|---|---|
sourceChainSelector | uint64 | Source chain of the inbound message. |
| (unnamed) | bytes calldata | Sender on the source chain. Ignored. |
Returns:
| Name | Type | Description |
|---|---|---|
requiredCCVs | address[] memory | CCVs that must all attest. |
optionalCCVs | address[] memory | Optional CCVs. |
optionalThreshold | uint8 | Number of optional CCVs that must attest. |
allowedFinalityConfig | bytes4 | inboundFinality[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
trueforIAny2EVMMessageReceiver,IAny2EVMMessageReceiverV2,IAccessControlEnumerable,IAccessControl, andIERC165. The CCIP OffRamp uses this check to decide whether to callccipReceive, and on CCIP 2.0 lanes whether to readgetCCVsAndFinalityConfig.
Access: anyone (view).
Parameters:
| Parameter | Type | Description |
|---|---|---|
interfaceId | bytes4 | ERC-165 interface identifier. |
Returns:
| Name | Type | Description |
|---|---|---|
supported | bool | true 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
processMessageand calls the vault'spreviewDepositorpreviewRedeem.
- Returns
0instead of reverting whenamountis0, when the adapter fee is greater than or equal to the amount (deposit) or the redeemed assets (redemption), or when the vault preview returns0.- Requires
assetFeeDestinationChainSelectorto be configured inchains, even whenreturnToSourceChainisfalse.- Does not apply
minimumOut. Vault state and the adapter fee can change before the message executes, so deriveminimumOutfrom this value with a tolerance. See Slippage protection.- Does not catch vault reverts. If
asset(),previewDeposit, orpreviewRedeemreverts,previewreverts.
Access: anyone (view).
Parameters:
| Parameter | Type | Description |
|---|---|---|
token | address | Inbound token: the vault's asset for a deposit, or the vault (share token) address for a redemption. |
vaultTarget | address | Allowlisted vault, as in Payload.target. |
amount | uint256 | Inbound amount, in token units. |
returnToSourceChain | bool | true applies the adapter fee for a return to the source chain. false simulates local delivery, with no adapter fee. |
assetFeeDestinationChainSelector | uint64 | Source chain selector of the message. Must be configured in chains. Also keys assetFees when returnToSourceChain is true. |
Returns:
| Name | Type | Description |
|---|---|---|
received | uint256 | Expected 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 andamountis not zero.DepositsDisabled()orRedeemsDisabled()when that path is switched off.InvalidTargetToken(vaultTarget, token)whentokenis neither the asset nor the share token.- Errors from the vault's
asset(),previewDeposit, orpreviewRedeem.
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 = falsewith zeroed fields when the message is notBASIC, when the stored sender is not exactly 32 bytes, or when no inbound token amount is positive. Otherwise it returnscanRefund = true, the original sender, the first token with a non-zero amount, andrequiredFee: the sum of one routergetFeequote 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".
requiredFeeis an estimate. Send more than this asmsg.valuetorefundFailedMessage; the adapter returns the excess.
Access: anyone (view).
Parameters:
| Parameter | Type | Description |
|---|---|---|
messageId | bytes32 | ID of the failed inbound message. |
Returns:
| Name | Type | Description |
|---|---|---|
canRefund | bool | Whether a cross-chain refund is possible now, when the call does not revert. |
originalSender | bytes32 | Sender of the original message. The refund goes to this address on the source chain. |
token | address | First inbound token with a non-zero amount. |
tokenAmount | uint256 | Amount of token. |
requiredFee | uint256 | Estimated 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
getFeequote per inbound token with a non-zero amount, using the current router state. The estimate can be lower than the cost at execution, becauserefundFailedMessagere-quotes each send and fees can change in between. Padmsg.valueabove this value.
Access: anyone (view).
Parameters:
| Parameter | Type | Description |
|---|---|---|
messageId | bytes32 | ID of the failed inbound message. |
Returns:
| Type | Description |
|---|---|
uint256 | Estimated total fee for all refund sends, in the hub chain's native gas token. |
Reverts with:
MessageNotFailed(messageId)when the message is notBASIC.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 = falsewith zeroed fields when the message is notBASIC, when no local refund address was stored, or when no inbound token amount is positive. Otherwise it returnstrue, 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:
| Parameter | Type | Description |
|---|---|---|
messageId | bytes32 | ID of the failed inbound message. |
Returns:
| Name | Type | Description |
|---|---|---|
canRecover | bool | Whether the local refund address can recover the message now. |
localRefundAddress | address | The only address that can call recoverFailedMessageLocally. |
token | address | First inbound token with a non-zero amount. |
tokenAmount | uint256 | Amount 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]isBASIC. For a message that never failed, or one that was refunded or recovered, the call returns a record with zero values. ReadmessageErrorCodeto tell these states apart.
Access: anyone (view).
Parameters:
| Parameter | Type | Description |
|---|---|---|
messageId | bytes32 | ID of the inbound message. |
Returns:
| Name | Type | Description |
|---|---|---|
record | FailedMessageRecord memory | See 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 originalmessage.senderonsourceChainSelector. It is not the beneficiary and not the local refund address.The function:
- Requires the message to be
BASIC, at least one non-zero token amount, and a sender of exactly 32 bytes.- Sets the message to
RESOLVEDand deletes its record and chain family snapshot.- 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, andevmReturnRequestedFinalityfor the inbound token: the asset for a failed deposit, the share token for a failed redemption.- Requires
msg.valueto cover the total fee paid, then sends any excessmsg.valueback 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:
| Parameter | Type | Description |
|---|---|---|
messageId | bytes32 | ID of the failed inbound message. |
msg.value | native gas token | Fee for all refund sends. Start from estimateRefundFee or checkRefundEligibility and add a margin. |
Returns: none.
Reverts with:
MessageNotFailed(messageId)when the message is notBASIC: 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 includesmsg.value, is below the router fee for a send.InsufficientRecoveryFee(requiredFee, providedFee)whenmsg.valueis below the total fee paid.RefundFailed()when sending the excessmsg.valueback to the caller fails.ReentrancyGuardReentrantCall()on a reentrant call.- Router, OnRamp, and token pool errors from
getFeeorccipSend.
Emits:
MessageSentfor 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
localRefundAddressstored in the failed-message record can call it. It costs no CCIP fee, and the function is notpayable. It sets the message toRESOLVED, deletes the record and chain family snapshot, transfers each non-zero inbound token to the caller, and emitsMessageRecoveredLocally. 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
deliveryAndRefundfield and cannot be set or changed afterward. Whichever recovery function succeeds first resolves the message; the other then reverts withMessageNotFailed. See Local recovery.
Access: the stored localRefundAddress only.
Parameters:
| Parameter | Type | Description |
|---|---|---|
messageId | bytes32 | ID of the failed inbound message. |
Returns: none.
Reverts with:
MessageNotFailed(messageId)when the message is notBASIC.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 thanNONE, and uses the value to encode return legs to that selector. SetEVMfor an EVM source chain, such as Arbitrum Sepolia (selector3478487238524512106) for an adapter on Ethereum Sepolia.Setting a selector to
NONEdoes not stop CCIP delivery. Messages that arrive afterward fail withInvalidChainand 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:
| Parameter | Type | Description |
|---|---|---|
chainSelector | uint64 | CCIP chain selector of the remote chain. |
chainType | ChainType | EVM, 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'stargetselects 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
InvalidTargetand become failed messages.
Access: DEFAULT_ADMIN_ROLE.
Parameters:
| Parameter | Type | Description |
|---|---|---|
target | address | Vault address. |
enabled | bool | true to allowlist, false to remove. |
Returns: none.
Reverts with:
InvalidTarget(address(0))whentargetis 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
depositsEnabledandredeemsEnabled. Both start asfalseon 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
DepositsDisabledorRedeemsDisabledand become failed messages. Switching the path back on does not reprocess them. See What counts as a failure.
Access: DEFAULT_ADMIN_ROLE.
Parameters:
| Parameter | Type | Description |
|---|---|---|
depositsEnabled_ | bool | Whether to process deposits. |
redeemsEnabled_ | bool | Whether 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
CCVConfigforsourceChainSelector, whichgetCCVsAndFinalityConfigreturns to the OffRamp. CCIP 1.6 lanes ignore it. Empty lists with a zero threshold mean the lane's default CCVs. InrequiredCCVs,address(0)stands for the lane's default CCVs.The function validates the lists:
optionalThreshold_must be greater than 0 whenoptionalCCVsis not empty, and no greater thanoptionalCCVs.length.- No address can appear twice in one list, or in both lists.
optionalCCVscannot containaddress(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:
| Parameter | Type | Description |
|---|---|---|
sourceChainSelector | uint64 | Source chain the policy applies to. |
requiredCCVs | address[] calldata | CCVs that must all attest. |
optionalCCVs | address[] calldata | Optional CCVs. |
optionalThreshold_ | uint8 | Number of optional CCVs that must attest. |
Returns: none.
Reverts with:
OptionalCCVsRequirePositiveThreshold(optionalCCVCount)whenoptionalCCVsis 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()whenoptionalCCVscontainsaddress(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], whichgetCCVsAndFinalityConfigreturns asallowedFinalityConfig. 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
FinalityCodeclayout. Bit 16 is thesafetag flag, and the low 16 bits are a block depth. A message that requests thesafetag 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:
| Parameter | Type | Description |
|---|---|---|
sourceChainSelector | uint64 | Source chain the setting applies to. |
allowedFinalityConfig | bytes4 | Allowed 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)
destinationChainSelectoris the source chain of the inbound messages whose outputs and refunds go back to it. SetGENERIC_EXTRA_ARGS_V3_BASICon CCIP 2.0 lanes, such as Arbitrum Sepolia for an adapter on Ethereum Sepolia. Leave the format unset or setLEGACY_EXTRA_ARGS_V2on CCIP 1.6 lanes.Setting
GENERIC_EXTRA_ARGS_V3_BASICon 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.
UNSETcannot be written. To return to the legacy encoding, setLEGACY_EXTRA_ARGS_V2.
Access: DEFAULT_ADMIN_ROLE.
Parameters:
| Parameter | Type | Description |
|---|---|---|
destinationChainSelector | uint64 | Chain that return legs and refunds go to. |
format | EvmReturnExtraArgsFormat | LEGACY_EXTRA_ARGS_V2 or GENERIC_EXTRA_ARGS_V3_BASIC. |
Returns: none.
Reverts with:
InvalidEvmReturnExtraArgsFormat()whenformatisUNSET.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)
tokenis 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
destinationChainSelectorto already beGENERIC_EXTRA_ARGS_V3_BASIC. On any other format, passing0x00000000clears 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:
| Value | Meaning |
|---|---|
0x00000000 | Wait for finality. This is the default. |
0x00010000 | Wait for the safe tag. |
0x00000001 to 0x0000FFFF | Wait 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:
| Parameter | Type | Description |
|---|---|---|
destinationChainSelector | uint64 | Chain that return legs and refunds go to. |
token | address | Token bridged on the leg. Must be non-zero. |
requestedFinalityForV3 | bytes4 | Requested finality, in the FinalityCodec layout. |
Returns: none.
Reverts with:
InvalidTarget(address(0))whentokenis the zero address.UnexpectedRequestedFinalityForLegacyFormat(requestedFinalityForV3)when the value is non-zero and the lane format is notGENERIC_EXTRA_ARGS_V3_BASIC.RequestedFinalityCanOnlyHaveOneMode(encodedFinality)fromFinalityCodecwhen 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), with0x00000000when 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
InsufficientNativeBalanceuntil someone funds it again.
Access: DEFAULT_ADMIN_ROLE.
Parameters:
| Parameter | Type | Description |
|---|---|---|
recipient | address | Receives the native gas token balance. |
Returns: none.
Reverts with:
InvalidRecipient()whenrecipientis the zero address.AmountIsZero()when the balance is zero.RecoverNativeFailed()when the transfer torecipientfails.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 withsetChainType. Set0to remove a fee. See Fees.
Access: FEE_SETTER_ROLE.
Parameters:
| Parameter | Type | Description |
|---|---|---|
destinationChainSelector | uint64 | Chain the output returns to, which is the source chain of the inbound message. |
bridgedToken | address | Token bridged back, which is the share token for deposits and the asset for redemptions. |
fee | uint256 | Fee in the asset's smallest units. |
Returns: none.
Reverts with:
InvalidTarget(address(0))whenbridgedTokenis 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
setAssetFeerow with the same validation. A zero token in any row reverts the whole call.
Access: FEE_SETTER_ROLE.
Parameters:
| Parameter | Type | Description |
|---|---|---|
destinationChainSelectors | uint64[] calldata | Return chain per row. |
bridgedTokens | address[] calldata | Token bridged back per row. |
fees | uint256[] calldata | Fee per row, in the asset's smallest units. |
Returns: none.
Reverts with:
FeeConfigLengthMismatch()when the three arrays differ in length.InvalidTarget(address(0))when anybridgedTokensentry 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]byamountand 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:
| Parameter | Type | Description |
|---|---|---|
asset | address | Asset whose fees to withdraw. |
recipient | address | Receives the fees. |
amount | uint256 | Amount, in the asset's smallest units. |
Returns: none.
Reverts with:
InvalidRecipient()whenrecipientis the zero address.AmountIsZero()whenamountis zero.InsufficientFeeBalance(availableBalance, requestedAmount)whenamountexceedscollectedFees[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
);
| Event | Emitted when | Use it to |
|---|---|---|
MessageSucceeded | processMessage completes inside ccipReceive. | Mark an inbound message as processed. |
MessageFailed | processMessage 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. |
TargetProcessed | The vault deposit or redemption completes, before delivery. | Record the inbound amount and the output after the adapter fee for each message. |
LocalTokenDelivered | The output is transferred to the beneficiary on the hub chain. | Confirm local delivery. |
MessageSent | The 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. |
MessageRefunded | refundFailedMessage completes. destinationChainSelector is the original source chain and beneficiary the original sender. | Close a failed message as refunded. |
MessageRecoveredLocally | recoverFailedMessageLocally completes. | Close a failed message as recovered on the hub chain. |
TargetEnabled | setTargetEnabled runs. | Review the vault allowlist. |
ChainTypeSet | setChainType runs. | Review which source chains are configured. |
ProcessingEnabledSet | setProcessingEnabled runs. | Warn users before they send a request that will fail. |
AssetFeeSet | setAssetFee runs, or once per row of setAssetFees. | Update the adapter fee shown in a UI. |
CCVsConfigSet | setCCVsConfig runs. | Review the CCV policy per source chain. |
InboundFinalitySet | setInboundFinality runs. | Review which faster-than-finality modes are accepted. |
EvmReturnLaneFormatSet | setEvmReturnLaneFormat runs. | Review return formats per lane. |
EvmReturnRequestedFinalitySet | setEvmReturnRequestedFinality runs. | Review return finality per lane and token. |
FeeWithdrawn | withdrawFee runs. | Reconcile fee withdrawals. |
NativeRecovered | recoverNative 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.
| Error | Raised by | Meaning | Becomes a failed message |
|---|---|---|---|
InvalidRouter | Constructor, ccipReceive | The router is the zero address at construction, or the caller of ccipReceive is not ROUTER. | No |
InvalidAdmin | Constructor | defaultAdmin is the zero address. The factory reuses this error. | No |
InvalidFeeSetter | Constructor | feeSetter is the zero address. The factory reuses this error. | No |
InvalidFeeCollector | Constructor | feeCollector is the zero address. The factory reuses this error. | No |
InvalidRecipient | withdrawFee, recoverNative | The recipient is the zero address. | No |
InvalidChain | processMessage, preview | The source chain selector is NONE in chains. | Yes from processMessage |
OnlySelf | processMessage | An address other than the adapter called processMessage. | No |
AmountIsZero | processMessage, withdrawFee, recoverNative | The inbound amount, the withdrawal amount, or the native gas token balance is zero. | Yes from processMessage |
InvalidEVMAddress | processMessage, refundFailedMessage, estimateRefundFee, checkRefundEligibility | The beneficiary or the original sender has non-zero upper 96 bits. | Yes from processMessage |
InvalidSenderAddressFormat | refundFailedMessage, estimateRefundFee | The stored sender is not exactly 32 bytes. | No |
InsufficientNativeBalance | processMessage (return leg), refundFailedMessage | The native gas token balance available for a send is below the router fee. | Yes from processMessage |
InsufficientRecoveryFee | refundFailedMessage | msg.value is below the total fee paid for the refund. | No |
InsufficientFeeBalance | withdrawFee | amount exceeds collectedFees[asset]. | No |
MessageNotFailed | refundFailedMessage, recoverFailedMessageLocally, estimateRefundFee | The message is not BASIC. | No |
InvalidTarget | processMessage, preview, setTargetEnabled, setAssetFee, setAssetFees, setEvmReturnRequestedFinality | The vault is not allowlisted, or a setter received the zero address. | Yes from processMessage |
InvalidTargetToken | processMessage, preview | The token is neither the vault's asset nor its share token. | Yes from processMessage |
InvalidTokenCount | processMessage | The message does not carry exactly one token. | Yes |
InvalidPayloadLength | processMessage | message.data is not 128 bytes. | Yes |
FeeConfigLengthMismatch | setAssetFees | The three arrays differ in length. | No |
InvalidEvmReturnExtraArgsFormat | setEvmReturnLaneFormat | The format is UNSET. | No |
UnexpectedRequestedFinalityForLegacyFormat | setEvmReturnRequestedFinality | A non-zero finality was set for a lane whose format is not GENERIC_EXTRA_ARGS_V3_BASIC. | No |
InvalidOptionalThreshold | setCCVsConfig | The threshold is greater than the number of optional CCVs. | No |
OptionalCCVsRequirePositiveThreshold | setCCVsConfig | Optional CCVs were given with a threshold of 0. | No |
DuplicateCCV | setCCVsConfig | An address repeats within or across the CCV lists. | No |
InvalidOptionalCCV | setCCVsConfig | optionalCCVs contains the zero address. | No |
DepositsDisabled | processMessage, preview | Deposits are switched off. | Yes from processMessage |
RedeemsDisabled | processMessage, preview | Redemptions are switched off. | Yes from processMessage |
FeeExceedsAmount | processMessage | The adapter fee is greater than or equal to the deposit input or the redeemed assets. preview returns 0 instead. | Yes |
MinimumOutputNotMet | processMessage | The output after the adapter fee is below minimumOut. | Yes |
NoOutputReceived | processMessage | The vault returned zero shares or zero assets. | Yes |
RefundFailed | refundFailedMessage | Sending the excess msg.value back to the caller failed. | No |
RecoverNativeFailed | recoverNative | The native gas token transfer to the recipient failed. | No |
NoRefundableTokenAmounts | refundFailedMessage, recoverFailedMessageLocally, estimateRefundFee | No inbound token amount is positive. | No |
NoLocalRefundAddress | recoverFailedMessageLocally | No local refund address was stored for the message. | No |
UnauthorizedLocalRefund | recoverFailedMessageLocally | The 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:
| Error | Source | Raised when |
|---|---|---|
AccessControlUnauthorizedAccount(address account, bytes32 neededRole) | OpenZeppelin AccessControl | The caller lacks the role for a configuration or fee function, or the admin role for grantRole or revokeRole. |
AccessControlBadConfirmation() | OpenZeppelin AccessControl | renounceRole is called with an account other than the caller. |
ReentrancyGuardReentrantCall() | OpenZeppelin ReentrancyGuard | refundFailedMessage, recoverFailedMessageLocally, withdrawFee, or recoverNative is re-entered. |
RequestedFinalityCanOnlyHaveOneMode(bytes4 encodedFinality) | FinalityCodec, in setEvmReturnRequestedFinality | The requested finality combines a flag with a block depth or sets more than one flag. |
UnsupportedDestinationChain(uint64 destChainSelector) | CCIP Router, in getFee and ccipSend | The hub-chain router has no lane to the return or refund destination. |
BadARMSignal() | CCIP Router, in ccipSend | The 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 ccipSend | The 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.
Related contracts
- Factory contract: deploys and configures adapters
- Limitations: what the adapter does not do
IAny2EVMMessageReceiverV2: the CCIP 2.0 receiver interface the adapter implementsClient: CCIP message structs andGenericExtraArgsV2ExtraArgsCodec:GenericExtraArgsV3encoding for return legs on CCIP 2.0 lanesCCIPReceiver: CCIP's base receiver, which the adapter does not inheritCCVConfigValidation: CCV list rules applied by CCIP- OpenZeppelin
AccessControlEnumerable: role management - EIP-4626: the tokenized vault standard