Set advanced pool hooks using Foundry

Guide Versions

This guide is available in multiple versions. Choose the one that matches your needs.

CCIP v2 token pools support an optional AdvancedPoolHooks contract being attached to them.
This contract can run certain checks :

  1. before tokens are locked/burned (source chain)
  2. before tokens are released/minted (destination chain)

If no hooks contract is attached, the token pool keeps its existing behavior.

Hooks are enforced only when both of these aspects are configured:

  • The token pool points to the AdvancedPoolHooks contract.
  • The hooks contract authorizes that token pool as a caller.

Authorize the pool before attaching hooks so the first hook call does not revert. Hooks are configured per token pool, per chain, which means that: you need to repeat this setup for any other pool where you want hooks enforced.

Note: Remember. Your CCT token can be deployed on multiple chains, with each chain having its own token pool.

In this tutorial you will:

  1. Review the current hooks state on your deployed token pool.
  2. Send a baseline transfer (with no hooks attached). This should succeed.
  3. Deploy an AdvancedPoolHooks contract with your deployer address on the allowlist.
  4. Authorize the pool as a caller on the hooks contract and attach the hooks contract to the token pool.
  5. Verify the hooks attachment, authorized callers, and allowlist state.
  6. Remove your address from the allowlist.
  7. Attempt a transfer (expected to revert with SenderNotAllowed(address)).
  8. Detach the hooks contract.
  9. Send a transfer again (expected to succeed).
  10. Manage the allowlist over time (add, remove, and check addresses).

Before You Begin

1 Set up your development environment
  1. Install Node.js and npm:

    • Make sure you have Node.js v22.10.0 or above installed. If not, install Node.js v22.10.0 using their documentation.
    • npm is bundled with Node.js. If npm is unavailable, reinstall or update Node.js.
  2. Install Foundry: If you haven't already, follow the instructions in the Foundry documentation to install Foundry.
    Verify the installation by running the following command:

Terminal
forge --version
  1. Install/Update Chainlink ccip-cli:
Terminal
npm install -g @chainlink/ccip-cli
ccip-cli --version
  1. Clone the repository and navigate to the project directory:
CCIP 2.0 template

Clone the CCIP 2.0 template for a smoother setup.

Terminal
git clone https://github.com/smartcontractkit/docs-cct-foundry.git
cd docs-cct-foundry
  1. Create an encrypted Foundry keystore, if you haven't already:
Terminal
cast wallet import your_keystore_name --interactive
  1. Create a .env file by copying the .env.example file, and fill in the required values:
Terminal
cp .env.example .env
.env
# Keystore name (created via `cast wallet import`)
KEYSTORE_NAME=<your_default_keystore_name>

# RPC URLs (add the ones you need)
ETHEREUM_SEPOLIA_RPC_URL=your_eth_sepolia_rpc
ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL=your_arbitrum_sepolia_rpc

# Etherscan API key (required only if you pass --verify to deployment scripts)
ETHERSCAN_API_KEY=your_etherscan_api_key

This tutorial uses a single wallet for every deployment, configuration update, and test transfer: the wallet stored in KEYSTORE_NAME.

Complete List of Supported Chains

View the complete list of supported chains in the HelperConfig.s.sol file.

  1. To make sure your terminal has access to these variables, run the following command:
Terminal
source .env
  1. Build the project:
Terminal
npm install && forge build

Tutorial

1 Confirm prerequisites, addresses, and permissions

This tutorial assumes you have already deployed tokens and token pools and configured a working lane. If not, complete one of the registration tutorials first:

Export the addresses for the chain you are configuring:

Terminal
export ETHEREUM_SEPOLIA_TOKEN=0x...
export ETHEREUM_SEPOLIA_TOKEN_POOL=0x...
export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN=0x...
export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN_POOL=0x...

Use these values throughout the tutorial:

  • ETHEREUM_SEPOLIA_TOKEN_POOL is the TokenPool v2 contract being configured.
  • POOL_HOOKS is the deployed AdvancedPoolHooks contract, exported after deployment.
  • ETHEREUM_SEPOLIA_TOKEN is the source-chain token that we will transfer across to the destination chain (Arbitrum Sepolia)
2 Review current pool hooks state

Use the GetAdvancedPoolHooks.s.sol script to check whether an AdvancedPoolHooks contract is currently attached to your token pool.

GetAdvancedPoolHooks.s.sol

View the hooks query script on GitHub.

Terminal
forge script \
  script/configure/allowlist/GetAdvancedPoolHooks.s.sol \
  --rpc-url $ETHEREUM_SEPOLIA_RPC_URL

Example output (no hooks attached):

Terminal
========================================
๐Ÿช Get Advanced Pool Hooks
========================================
Chain:        Ethereum Sepolia
Token Pool:   0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
Action:       View pool hooks
========================================

No AdvancedPoolHooks contract is attached to this pool.
Deploy one with DeployAdvancedPoolHooks.s.sol and attach it via UpdateAdvancedPoolHooks.s.sol.

========================================
Token Pool:   https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
========================================
3 Send a baseline transfer (no hooks attached)

This step confirms your lane works before enabling hooks. Make sure the previous step shows no hooks attached.

Export the Sepolia router address for ccip-cli:

Terminal
export ETHEREUM_SEPOLIA_ROUTER=0x...

Send a baseline transfer to confirm if the cross chain transfer succeeds without any hooks attached.

Terminal
ccip-cli send \
  --source ethereum-testnet-sepolia \
  --router $ETHEREUM_SEPOLIA_ROUTER \
  --dest ethereum-testnet-sepolia-arbitrum-1 \
  --transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=1.23 \
  --receiver 0xreceiveraddress \
  --wallet foundry:$KEYSTORE_NAME \
  --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \
  --rpc "$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL"

Expected output (example):

Terminal
Fee: 130129888907619n = 0.000130129888907619 ETH
โœ” Enter password for Foundry keystore 'PRIVATE_KEY'
๐Ÿš€ Sending message to 0xE23Fc63F47F08F58B9d7448d4CCE0bCDcc96d7F3 @ ethereum-testnet-sepolia-arbitrum-1 , tx => 0x3dc40bea29f3e3fc93ff8fce0dda45fd7f55a07ace5089646e018874c8b6745e , messageId => 0x4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293
CCIP Explorer: https://ccip.chain.link/msg/0x4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293
4 Deploy AdvancedPoolHooks

Use the DeployAdvancedPoolHooks.s.sol script to deploy a new hooks contract.

DeployAdvancedPoolHooks.s.sol

View the hooks deployment script on GitHub.

Deployment inputs:

  • ALLOWLIST: CSV or JSON array of original outbound sender addresses allowed to initiate outbound transfers.
  • AUTHORIZED_CALLERS: CSV or JSON array of token pool addresses permitted to invoke the hooks.

In this example, you will deploy hooks on Ethereum Sepolia with your deployer address on the allowlist, and authorize your token pool as a caller:

Terminal
ALLOWLIST=0xYourDeployerAddress \
AUTHORIZED_CALLERS=$ETHEREUM_SEPOLIA_TOKEN_POOL \
forge script \
script/configure/allowlist/DeployAdvancedPoolHooks.s.sol \
--rpc-url $ETHEREUM_SEPOLIA_RPC_URL \
--account $KEYSTORE_NAME \
--broadcast \
--verify

Your output should look something like this:

Terminal
========================================
๐Ÿ”’ Deploy Advanced Pool Hooks
========================================
Chain:        Ethereum Sepolia
Action:       Deploy pool hooks
========================================

Advanced Pool Hooks Parameters:
Allowlist Enabled:            Yes
Allowlist Size:               1
[0] 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994
Threshold Amount:             Disabled (0)
Policy Engine:                Disabled (0x0)
Authorized Callers Enabled:   Yes
Authorized Callers Size:      1
[0] 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9

[Step 1] Deploying AdvancedPoolHooks on Ethereum Sepolia
AdvancedPoolHooks deployed at: 0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE
https://sepolia.etherscan.io/address/0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE
โœ… AdvancedPoolHooks deployed successfully!

========================================
โœ… Deployment Complete on Ethereum Sepolia!
========================================
AdvancedPoolHooks Address: 0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE
https://sepolia.etherscan.io/address/0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE

Deployment saved: script/deployments/advanced-pool-hooks/ETHEREUM_SEPOLIA/1746652800-AdvancedPoolHooks.json

Configuration Summary:
Allowlist:                    Enabled
Threshold:                    Disabled
Policy Engine:                Disabled
Authorized Callers:           Enabled

Next Steps:
1. When deploying a TokenPool, pass this hooks address as the 'poolHooks' parameter
2. Attach to an existing pool: TOKEN_POOL=<address> NEW_HOOK=<address> forge script script/configure/allowlist/UpdateAdvancedPoolHooks.s.sol --rpc-url $RPC_URL --account $KEYSTORE_NAME --broadcast
3. Manage allowlist: POOL_HOOKS=<address> ADD_ADDRESSES="0xAddr" forge script script/configure/allowlist/UpdateAllowList.s.sol --rpc-url $RPC_URL --account $KEYSTORE_NAME --broadcast
========================================

Export the hooks address for subsequent commands:

Terminal
export POOL_HOOKS=0xHooksAddress
5 Authorize the pool and attach hooks

Authorize the token pool as a caller on the hooks contract before attaching hooks so the first hook call does not revert.

If you need to authorize the pool separately, use UpdateAuthorizedCallers.s.sol:

UpdateAuthorizedCallers.s.sol

View the authorized callers script on GitHub.

Terminal
POOL_HOOKS=$POOL_HOOKS \
  ADD_ADDRESSES=$ETHEREUM_SEPOLIA_TOKEN_POOL \
  forge script \
  script/configure/authorized-callers/UpdateAuthorizedCallers.s.sol \
  --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \
  --account $KEYSTORE_NAME \
  --broadcast

Your output should look something like this:

Terminal
========================================
๐Ÿ“ Update Authorized Callers
========================================
Chain:        Ethereum Sepolia
Pool Hooks:   0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE
Action:       Update authorized callers
========================================

Adding 1 caller(s):
[0] 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9

========================================
โœ… Authorized callers updated on Ethereum Sepolia!
========================================
Pool Hooks:   0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE
Pool Hooks:   https://sepolia.etherscan.io/address/0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE
========================================

Now attach the hooks to your token pool using UpdateAdvancedPoolHooks.s.sol:

UpdateAdvancedPoolHooks.s.sol

View the hooks update script on GitHub.

Terminal
NEW_HOOK=$POOL_HOOKS \
  forge script \
  script/configure/allowlist/UpdateAdvancedPoolHooks.s.sol \
  --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \
  --account $KEYSTORE_NAME \
  --broadcast

Your output should look something like this:

Terminal
========================================
๐Ÿ”„ Update Advanced Pool Hooks
========================================
Chain:        Ethereum Sepolia
Token Pool:   0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
Action:       Update pool hooks
========================================

New Pool Hooks: 0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE

โœ… AdvancedPoolHooks updated successfully!

========================================
โœ… Pool hooks updated on Ethereum Sepolia!
========================================
Token Pool:     0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
New Pool Hooks: 0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE
Token Pool:     https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
New Pool Hooks: https://sepolia.etherscan.io/address/0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE
========================================
6 Verify the setup

Run the following read-only scripts to verify wiring. These checks confirm configuration state only;the next step attempts a transfer that is expected to revert.

1. Confirm hooks are attached to the pool:

Terminal
forge script \
  script/configure/allowlist/GetAdvancedPoolHooks.s.sol \
  --rpc-url $ETHEREUM_SEPOLIA_RPC_URL

Expected output should now show your hooks address:

Terminal
========================================
๐Ÿช Get Advanced Pool Hooks
========================================
Chain:        Ethereum Sepolia
Token Pool:   0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
Action:       View pool hooks
========================================

โœ… AdvancedPoolHooks:
   0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE

========================================
Token Pool:   https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
========================================

2. Confirm the pool is an authorized caller on the hooks:

GetAuthorizedCallers.s.sol

View the authorized callers query script on GitHub.

Terminal
POOL_HOOKS=$POOL_HOOKS \
  forge script \
  script/configure/authorized-callers/GetAuthorizedCallers.s.sol \
  --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \
  --account $KEYSTORE_NAME

Your output should look something like this:

Terminal
========================================
๐Ÿ”Ž Get Authorized Callers
========================================
Chain:        Ethereum Sepolia
Pool Hooks:   0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE
Action:       View authorized callers
========================================

Authorized Callers count: 1
[0] 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
========================================
Pool Hooks:   https://sepolia.etherscan.io/address/0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE
========================================

3. View the current allowlist:

GetAllowList.s.sol

View the allowlist query script on GitHub.

Terminal
POOL_HOOKS=$POOL_HOOKS \
  forge script \
  script/configure/allowlist/GetAllowList.s.sol \
  --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \
  --account $KEYSTORE_NAME

Your output should look something like this:

Terminal
========================================
๐Ÿ”Ž Get AllowList
========================================
Chain:        Ethereum Sepolia
Pool Hooks:   0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE
Action:       View allowlist
========================================

AllowList count: 1
0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994
========================================
Pool Hooks:   https://sepolia.etherscan.io/address/0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE
========================================

4. (Optional): Check if a particular sender is allowlisted:

IsAllowListed.s.sol

View the allowlist check script on GitHub.

Terminal
POOL_HOOKS=$POOL_HOOKS \
CHECK_ADDRESS=0xsenderaddress \
forge script \
script/configure/allowlist/IsAllowListed.s.sol \
--rpc-url $ETHEREUM_SEPOLIA_RPC_URL

Your output should look something like this:

Terminal
========================================
๐Ÿ”Ž Is AllowListed?
========================================
Chain:        Ethereum Sepolia
Pool Hooks:   0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE
Check Address: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994
Action:       Check allowlist
========================================

โœ… Address IS allowlisted.
========================================
Pool Hooks:   https://sepolia.etherscan.io/address/0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE
========================================
7 Remove your address from the allowlist

In this step, you remove your own address from the hooks allowlist. This sets up the next transfer to fail.

Remove your deployer address from the allowlist:

Terminal
POOL_HOOKS=$POOL_HOOKS \
  REMOVE_ADDRESSES="0xYourDeployerAddress" \
  forge script \
  script/configure/allowlist/UpdateAllowList.s.sol \
  --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \
  --account $KEYSTORE_NAME \
  --broadcast

Verify that your address is no longer allowlisted:

Terminal
POOL_HOOKS=$POOL_HOOKS \
CHECK_ADDRESS=0xYourDeployerAddress \
forge script \
script/configure/allowlist/IsAllowListed.s.sol \
--rpc-url $ETHEREUM_SEPOLIA_RPC_URL
8 Attempt a transfer (expected failure)

This step attempts a token transfer and is expected to fail because you removed your sender address from the allowlist. Hooks are configured per token pool, per chain, although this does not attach hooks on any destination-chain pool.

Terminal
ccip-cli send \
  --source ethereum-testnet-sepolia \
  --router $ETHEREUM_SEPOLIA_ROUTER \
  --dest ethereum-testnet-sepolia-arbitrum-1 \
  --transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=1.23 \
  --receiver 0xYourDeployerAddress \
  --wallet foundry:$KEYSTORE_NAME \
  --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \
  --rpc "$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL"

Expected result: the send transaction reverts. You should see an error that includes SenderNotAllowed(address).

If this step does not fail as expected, make sure:

  • Hooks are attached (GetAdvancedPoolHooks.s.sol shows POOL_HOOKS).
  • Your sender address is NOT on the allowlist (previous step returns NOT allowlisted).
  • The token pool is an authorized caller on the hooks (GetAuthorizedCallers.s.sol includes ETHEREUM_SEPOLIA_TOKEN_POOL).
9 Detach hooks

Detach hooks to disable enforcement. This sets the pool hooks address back to the zero address.

Terminal
NEW_HOOK=0x0000000000000000000000000000000000000000 \
  forge script \
  script/configure/allowlist/UpdateAdvancedPoolHooks.s.sol \
  --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \
  --account $KEYSTORE_NAME \
  --broadcast

Verify hooks are detached:

Terminal
forge script \
  script/configure/allowlist/GetAdvancedPoolHooks.s.sol \
  --rpc-url $ETHEREUM_SEPOLIA_RPC_URL
10 Send a transfer again (expected success)

With hooks detached, the token pool returns to its default behavior and your transfer should succeed again.

Terminal
ccip-cli send \
  --source ethereum-testnet-sepolia \
  --router $ETHEREUM_SEPOLIA_ROUTER \
  --dest ethereum-testnet-sepolia-arbitrum-1 \
  --transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=1.23 \
  --receiver 0xYourDeployerAddress \
  --wallet foundry:$KEYSTORE_NAME \
  --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \
  --rpc "$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL"

Expected output (example):

Terminal
Fee: 130129888907619n = 0.000130129888907619 ETH
โœ” Enter password for Foundry keystore 'PRIVATE_KEY'
๐Ÿš€ Sending message to 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994 @ ethereum-testnet-sepolia-arbitrum-1 , tx => 0x3dc40bea29f3e3fc93ff8fce0dda45fd7f55a07ace5089646e018874c8b6745e , messageId => 0x4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293
CCIP Explorer: https://ccip.chain.link/msg/0x4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293
11 Manage the allowlist

Use the UpdateAllowList.s.sol script to add or remove addresses from the allowlist. Only the hooks contract owner can modify the allowlist.

UpdateAllowList.s.sol

View the allowlist update script on GitHub.

Env varRequiredDescription
POOL_HOOKSYes (v2)Address of the AdvancedPoolHooks contract
ADD_ADDRESSESNoCSV or JSON array of addresses to add
REMOVE_ADDRESSESNoCSV or JSON array of addresses to remove

Add an address:

Terminal
POOL_HOOKS=$POOL_HOOKS \
  ADD_ADDRESSES="0xYourDeployerAddress" \
  forge script \
  script/configure/allowlist/UpdateAllowList.s.sol \
  --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \
  --account $KEYSTORE_NAME \
  --broadcast

Your output should look something like this:

Terminal
========================================
๐Ÿ“ Update AllowList
========================================
Chain:        Ethereum Sepolia
Token Pool:   0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
Pool Hooks:   0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE
Action:       Update allowlist
========================================

โœ… AllowList updated successfully!

========================================
โœ… Allowlist updated on Ethereum Sepolia!
========================================
Token Pool:   https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
Pool Hooks:   https://sepolia.etherscan.io/address/0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE
========================================

Remove an address:

Terminal
POOL_HOOKS=$POOL_HOOKS \
  REMOVE_ADDRESSES="0xYourDeployerAddress" \
  forge script \
  script/configure/allowlist/UpdateAllowList.s.sol \
  --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \
  --account $KEYSTORE_NAME \
  --broadcast

Verify the updated allowlist by re-running the GetAllowList.s.sol or IsAllowListed.s.sol scripts from the previous step.

What's next

Get the latest Chainlink content straight to your inbox.