Destination hooks
A transfer can carry an optional hook: a contract call executed on the destination chain immediately after the tokens are minted or unlocked. Hooks let an integration deliver tokens and act on them - deposit into a vault, complete a purchase, notify a protocol - in one cross-chain message.
The Hook struct
struct Hook {
address target; // contract to call on the destination chain
uint128 gasLimit; // gas forwarded to the hook call
bytes payload; // opaque bytes passed through to the receiver
}Pass it to the extended overloads of quoteSend* / send*:
bridge.sendOriginal(
dstEid,
token,
amount,
recipient,
isFirstTransfer,
Hook({ target: vault, gasLimit: 200_000, payload: abi.encode(...) }),
options
);A hook with target = address(0) and gasLimit = 0 is the empty hook - the plain overloads without a Hook parameter use it implicitly.
Receiving a hook
The target must implement IOmniseaOFTHookReceiver (ERC-20 bridge) or the ONFT equivalent:
interface IOmniseaOFTHookReceiver {
function omReceive(
bytes32 guid, // LayerZero message guid
uint32 srcEid, // source endpoint id
uint8 assetKind, // 1 = ERC20
uint32 originalChainId, // asset's home chain
address originalToken, // asset's original address
address localToken, // the token just delivered on THIS chain
address sender, // source-chain sender
address recipient, // destination recipient
uint256 value, // amount (or tokenId for ONFTs)
bytes calldata payload // your opaque payload
) external;
}localToken is the address that actually moved on the destination - the original (if this is the home chain) or the representation.
Execution semantics - read carefully
- The hook runs after the recipient already received the tokens. The token delivery is never contingent on hook success.
- The hook is called with exactly
hook.gasLimitgas. Buy it on top of the transfer floor in your executor options - the bridge enforcesfloor + hook.gasLimitat send time. - A reverting hook does not fail the transfer. The bridge emits
HookFailed(guid, target, localToken, recipient, value, gasLimit, payloadHash, reasonHash)and the transfer still completes. Successful hooks emitHookSucceeded(...). - Hooks must therefore be best-effort by design: never build a flow where funds are unsafe if the hook does not run. Make the hook idempotent and recoverable (e.g. allow re-triggering its action manually).
- A non-empty
payloadorgasLimitwithtarget = address(0)is rejected at send time withInvalidHook.
Quoting with a hook
Hook gas increases the LayerZero fee. Quote with the same hook struct and options you will send with:
uint256 fee = bridge.quoteSendOriginal(dstEid, token, amount, recipient, isFirstTransfer, hook, options);Pattern: deposit-on-arrival
contract VaultHook is IOmniseaOFTHookReceiver {
function omReceive(
bytes32, uint32, uint8, uint32, address,
address localToken, address, address recipient,
uint256 value, bytes calldata
) external {
// Tokens are already in `recipient`'s wallet; this hook pattern
// assumes the recipient pre-approved this contract.
IERC20(localToken).transferFrom(recipient, address(this), value);
_depositFor(recipient, localToken, value);
}
}Validate msg.sender is the Omnisea bridge in production hooks if your handler must only run for genuine deliveries.