> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/zkp2p/zkp2p-contracts/llms.txt
> Use this file to discover all available pages before exploring further.

# Post-Intent Hooks

> Learn how to use and create custom post-intent hooks for automated actions after trades

Post-intent hooks enable automated on-chain actions after successful intent fulfillment. This guide covers using existing hooks and creating custom ones.

## Overview

A post-intent hook is a smart contract that executes custom logic when an intent is fulfilled. Common use cases:

* **Cross-chain bridging**: Automatically bridge USDC to another chain
* **Token swapping**: Convert USDC to another token
* **Protocol deposits**: Deposit into yield protocols
* **Multi-step workflows**: Chain multiple actions together

Hooks are specified at intent creation and executed by the Orchestrator after fees are deducted.

## How Hooks Work

```mermaid theme={null}
sequenceDiagram
    participant Taker
    participant Orchestrator
    participant Escrow
    participant Hook as Post-Intent Hook
    participant Destination as Destination<br/>(Bridge/DEX/etc)
    
    Taker->>Orchestrator: fulfillIntent(hookData)
    Orchestrator->>Escrow: unlockAndTransferFunds()
    Escrow-->>Orchestrator: USDC transferred
    Orchestrator->>Orchestrator: Deduct fees
    Orchestrator->>Hook: execute(intent, netAmount, hookData)
    Hook->>Hook: Pull USDC from Orchestrator
    Hook->>Destination: Perform action (bridge/swap/etc)
    Destination-->>Hook: Confirmation
    Hook-->>Orchestrator: Execution complete
    Orchestrator-->>Taker: IntentFulfilled event
```

## Using the Across Bridge Hook

The `AcrossBridgeHook` automatically bridges USDC to another chain after fulfillment.

### Setting Up Bridge Intent

<Steps>
  <Step title="Prepare commitment data">
    Define destination chain parameters at signaling time:

    ```typescript theme={null}
    import { ethers } from "ethers";

    // Helper to convert address to bytes32
    const toBytes32 = (addr: string): string => {
      return ethers.utils.hexZeroPad(addr, 32);
    };

    // Destination parameters (committed at signal time)
    const bridgeCommitment = {
      destinationChainId: 10, // Optimism
      outputToken: toBytes32("0x..."), // USDC on Optimism
      recipient: toBytes32("0x..."), // Your address on Optimism
      minOutputAmount: ethers.utils.parseUnits("49", 6) // Min output after fees
    };

    // Encode commitment
    const commitmentData = ethers.utils.defaultAbiCoder.encode(
      [
        "tuple(uint256 destinationChainId, bytes32 outputToken, bytes32 recipient, uint256 minOutputAmount)"
      ],
      [bridgeCommitment]
    );
    ```
  </Step>

  <Step title="Signal intent with hook">
    Include the hook address and commitment in your intent:

    ```typescript theme={null}
    const ACROSS_HOOK_ADDRESS = "0x..."; // Deployed AcrossBridgeHook

    await orchestrator.signalIntent({
      escrow: ESCROW_ADDRESS,
      depositId: depositId,
      amount: ethers.utils.parseUnits("50", 6),
      to: recipientAddress, // Fallback recipient if bridge fails
      paymentMethod: paymentMethod,
      fiatCurrency: fiatCurrency,
      conversionRate: conversionRate,
      referrer: ethers.constants.AddressZero,
      referrerFee: 0,
      gatingServiceSignature: signature,
      signatureExpiration: expiration,
      postIntentHook: ACROSS_HOOK_ADDRESS, // Enable hook
      data: commitmentData // Bridge parameters
    });
    ```
  </Step>

  <Step title="Fulfill with bridge data">
    At fulfillment, provide just-in-time bridge parameters:

    ```typescript theme={null}
    // Get current Across quote from their API
    const acrossQuote = await fetch(
      `https://across.to/api/suggested-fees?...`
    ).then(r => r.json());

    // Prepare fulfill data
    const bridgeFulfillData = {
      intentHash: intentHash,
      outputAmount: ethers.utils.parseUnits("49.5", 6), // From Across API
      fillDeadlineOffset: 21600, // 6 hours
      exclusiveRelayer: toBytes32(acrossQuote.exclusiveRelayer),
      exclusivityParameter: acrossQuote.exclusivityDeadline
    };

    const hookData = ethers.utils.defaultAbiCoder.encode(
      [
        "tuple(bytes32 intentHash, uint256 outputAmount, uint32 fillDeadlineOffset, bytes32 exclusiveRelayer, uint32 exclusivityParameter)"
      ],
      [bridgeFulfillData]
    );

    // Fulfill with hook data
    await orchestrator.fulfillIntent({
      intentHash: intentHash,
      paymentProof: paymentProof,
      verificationData: verificationData,
      postIntentHookData: hookData // JIT bridge params
    });
    ```
  </Step>
</Steps>

### Fallback Behavior

The Across hook gracefully handles failures:

```typescript theme={null}
// If bridge fails (price moved, SpokePool reverted, etc.)
// Hook automatically transfers USDC to intent.to on source chain
// Emits FallbackTransfer event with reason

// Listen for fallback
orchestrator.on("IntentFulfilled", (intentHash, recipient, amount, isManual) => {
  if (recipient === ACROSS_HOOK_ADDRESS) {
    // Check AcrossBridgeInitiated event
    console.log("Bridge initiated successfully");
  } else {
    // Check FallbackTransfer event
    console.log("Bridge failed, received on source chain");
  }
});
```

## Creating Custom Hooks

### Hook Interface

Implement the `IPostIntentHook` interface:

```solidity theme={null}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

import { IOrchestrator } from "./interfaces/IOrchestrator.sol";
import { IPostIntentHook } from "./interfaces/IPostIntentHook.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

contract MyCustomHook is IPostIntentHook {
    using SafeERC20 for IERC20;

    address public immutable orchestrator;
    IERC20 public immutable usdc;

    constructor(address _orchestrator, address _usdc) {
        orchestrator = _orchestrator;
        usdc = IERC20(_usdc);
    }

    /**
     * @notice Execute custom logic after intent fulfillment
     * @param _intent The fulfilled intent data
     * @param _amountNetFees USDC amount after protocol/referrer fees
     * @param _fulfillIntentData Custom data from fulfillIntent call
     */
    function execute(
        IOrchestrator.Intent memory _intent,
        uint256 _amountNetFees,
        bytes calldata _fulfillIntentData
    ) external override {
        require(msg.sender == orchestrator, "Unauthorized");

        // Pull USDC from orchestrator
        usdc.safeTransferFrom(orchestrator, address(this), _amountNetFees);

        // Decode custom data
        // ... your custom logic here ...

        // Must consume exactly _amountNetFees
        // Orchestrator enforces this requirement
    }
}
```

### Example: Yield Deposit Hook

Automatically deposit USDC into a yield protocol:

```solidity theme={null}
pragma solidity ^0.8.18;

import { IPostIntentHook } from "./interfaces/IPostIntentHook.sol";
import { IOrchestrator } from "./interfaces/IOrchestrator.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

interface IYieldProtocol {
    function deposit(uint256 amount, address recipient) external;
}

contract YieldDepositHook is IPostIntentHook {
    using SafeERC20 for IERC20;

    address public immutable orchestrator;
    IERC20 public immutable usdc;
    IYieldProtocol public immutable yieldProtocol;

    event YieldDepositExecuted(
        bytes32 indexed intentHash,
        address indexed recipient,
        uint256 amount
    );

    constructor(
        address _orchestrator,
        address _usdc,
        address _yieldProtocol
    ) {
        orchestrator = _orchestrator;
        usdc = IERC20(_usdc);
        yieldProtocol = IYieldProtocol(_yieldProtocol);
    }

    function execute(
        IOrchestrator.Intent memory _intent,
        uint256 _amountNetFees,
        bytes calldata _fulfillIntentData
    ) external override {
        require(msg.sender == orchestrator, "Unauthorized");

        // Pull USDC from orchestrator
        usdc.safeTransferFrom(orchestrator, address(this), _amountNetFees);

        // Approve yield protocol
        usdc.safeApprove(address(yieldProtocol), _amountNetFees);

        // Deposit to yield protocol on behalf of intent.to
        yieldProtocol.deposit(_amountNetFees, _intent.to);

        emit YieldDepositExecuted(
            keccak256(abi.encode(_intent)),
            _intent.to,
            _amountNetFees
        );
    }
}
```

### Example: Token Swap Hook

Swap USDC to another token via a DEX:

```solidity theme={null}
pragma solidity ^0.8.18;

import { IPostIntentHook } from "./interfaces/IPostIntentHook.sol";
import { IOrchestrator } from "./interfaces/IOrchestrator.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

interface IUniswapRouter {
    function swapExactTokensForTokens(
        uint256 amountIn,
        uint256 amountOutMin,
        address[] calldata path,
        address to,
        uint256 deadline
    ) external returns (uint256[] memory amounts);
}

contract SwapHook is IPostIntentHook {
    using SafeERC20 for IERC20;

    struct SwapParams {
        address outputToken;      // Token to swap to
        uint256 minOutputAmount;  // Minimum tokens out (slippage protection)
        address[] path;           // Swap path
    }

    address public immutable orchestrator;
    IERC20 public immutable usdc;
    IUniswapRouter public immutable router;

    constructor(
        address _orchestrator,
        address _usdc,
        address _router
    ) {
        orchestrator = _orchestrator;
        usdc = IERC20(_usdc);
        router = IUniswapRouter(_router);
    }

    function execute(
        IOrchestrator.Intent memory _intent,
        uint256 _amountNetFees,
        bytes calldata _fulfillIntentData
    ) external override {
        require(msg.sender == orchestrator, "Unauthorized");

        // Decode swap params from _intent.data (committed at signal time)
        SwapParams memory params = abi.decode(_intent.data, (SwapParams));

        // Pull USDC
        usdc.safeTransferFrom(orchestrator, address(this), _amountNetFees);

        // Approve router
        usdc.safeApprove(address(router), _amountNetFees);

        // Execute swap
        router.swapExactTokensForTokens(
            _amountNetFees,
            params.minOutputAmount,
            params.path,
            _intent.to,           // Send output tokens to intent recipient
            block.timestamp + 300 // 5 min deadline
        );
    }
}
```

## Hook Registration

Before use, hooks must be whitelisted in the `PostIntentHookRegistry`:

```typescript theme={null}
import { PostIntentHookRegistry } from "@typechain/PostIntentHookRegistry";

// Registry owner adds hook
const registry = new ethers.Contract(
  REGISTRY_ADDRESS,
  PostIntentHookRegistry_ABI,
  ownerSigner
) as PostIntentHookRegistry;

await registry.addPostIntentHook(MY_HOOK_ADDRESS);

// Verify registration
const isWhitelisted = await registry.isWhitelistedHook(MY_HOOK_ADDRESS);
console.log("Hook whitelisted:", isWhitelisted);
```

<Warning>
  Only governance can add hooks to the registry. Submit a proposal to get your hook whitelisted.
</Warning>

## Testing Hooks

```typescript theme={null}
import { expect } from "chai";
import { ethers } from "hardhat";

describe("MyCustomHook", () => {
  let hook: Contract;
  let orchestrator: Contract;
  let usdc: Contract;

  beforeEach(async () => {
    // Deploy mocks
    const [owner, taker] = await ethers.getSigners();
    
    const USDC = await ethers.getContractFactory("USDCMock");
    usdc = await USDC.deploy();

    const Orchestrator = await ethers.getContractFactory("OrchestratorMock");
    orchestrator = await Orchestrator.deploy();

    const Hook = await ethers.getContractFactory("MyCustomHook");
    hook = await Hook.deploy(orchestrator.address, usdc.address);

    // Setup
    await usdc.transfer(orchestrator.address, ethers.utils.parseUnits("100", 6));
  });

  it("should execute custom logic", async () => {
    const amount = ethers.utils.parseUnits("50", 6);

    // Approve hook to pull from orchestrator
    await usdc.connect(orchestrator).approve(hook.address, amount);

    // Build mock intent
    const intent = {
      owner: taker.address,
      to: taker.address,
      amount: amount,
      // ... other fields
    };

    // Execute hook
    await hook.connect(orchestrator).execute(
      intent,
      amount,
      "0x" // hookData
    );

    // Verify results
    // ... check balances, events, state changes
  });
});
```

## Best Practices

<CardGroup cols={2}>
  <Card title="Exact Consumption" icon="equals">
    Always consume exactly `_amountNetFees`. Orchestrator enforces this.
  </Card>

  <Card title="Authorization" icon="lock">
    Only allow orchestrator to call `execute()`. Revert for other callers.
  </Card>

  <Card title="Graceful Failures" icon="shield-check">
    Handle errors gracefully - consider fallback transfers like AcrossBridge.
  </Card>

  <Card title="Gas Efficiency" icon="gauge-high">
    Optimize gas usage - takers pay for hook execution.
  </Card>
</CardGroup>

## Security Considerations

<AccordionGroup>
  <Accordion title="Reentrancy Protection">
    Hooks are called during `fulfillIntent` which has reentrancy guards. Additional protection in hooks is optional but recommended.
  </Accordion>

  <Accordion title="Allowance Management">
    Reset token allowances to 0 after operations to prevent residual allowance exploits.
  </Accordion>

  <Accordion title="Balance Checks">
    Verify exact token consumption with before/after balance snapshots.
  </Accordion>

  <Accordion title="Input Validation">
    Validate all decoded parameters from `_intent.data` and `_fulfillIntentData`.
  </Accordion>
</AccordionGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="Testing Guide" icon="flask" href="/guides/testing">
    Learn how to test your custom hooks
  </Card>

  <Card title="Hook Registry" icon="list" href="/contracts/post-intent-hook-registry">
    View the PostIntentHookRegistry contract
  </Card>
</CardGroup>
