Documentation menu

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.gasLimit gas. Buy it on top of the transfer floor in your executor options - the bridge enforces floor + hook.gasLimit at 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 emit HookSucceeded(...).
  • 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 payload or gasLimit with target = address(0) is rejected at send time with InvalidHook.

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.

Feedback