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 :
- before tokens are locked/burned (source chain)
- 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
AdvancedPoolHookscontract. - 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:
- Review the current hooks state on your deployed token pool.
- Send a baseline transfer (with no hooks attached). This should succeed.
- Deploy an
AdvancedPoolHookscontract with your deployer address on the allowlist. - Authorize the pool as a caller on the hooks contract and attach the hooks contract to the token pool.
- Verify the hooks attachment, authorized callers, and allowlist state.
- Remove your address from the allowlist.
- Attempt a transfer (expected to revert with
SenderNotAllowed(address)). - Detach the hooks contract.
- Send a transfer again (expected to succeed).
- Manage the allowlist over time (add, remove, and check addresses).
Before You Begin
1 Set up your development environment
-
Install Node.js and npm:
- Make sure you have
Node.js v22.10.0or above installed. If not, installNode.js v22.10.0using their documentation. npmis bundled with Node.js. Ifnpmis unavailable, reinstall or update Node.js.
- Make sure you have
-
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:
forge --version
- Install/Update Chainlink
ccip-cli:
npm install -g @chainlink/ccip-cli
ccip-cli --version
- Clone the repository and navigate to the project directory:
Clone the CCIP 2.0 template for a smoother setup.
git clone https://github.com/smartcontractkit/docs-cct-foundry.git
cd docs-cct-foundry
- Create an encrypted Foundry keystore, if you haven't already:
cast wallet import your_keystore_name --interactive
- Create a
.envfile by copying the.env.examplefile, and fill in the required values:
cp .env.example .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.
View the complete list of supported chains in the HelperConfig.s.sol file.
- To make sure your terminal has access to these variables, run the following command:
source .env
- Build the project:
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:
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_POOLis theTokenPoolv2 contract being configured.POOL_HOOKSis the deployedAdvancedPoolHookscontract, exported after deployment.ETHEREUM_SEPOLIA_TOKENis 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.
View the hooks query script on GitHub.
forge script \
script/configure/allowlist/GetAdvancedPoolHooks.s.sol \
--rpc-url $ETHEREUM_SEPOLIA_RPC_URL
Example output (no hooks attached):
========================================
๐ช 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:
export ETHEREUM_SEPOLIA_ROUTER=0x...
Send a baseline transfer to confirm if the cross chain transfer succeeds without any hooks attached.
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):
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.
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:
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:
========================================
๐ 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:
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:
View the authorized callers script on GitHub.
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:
========================================
๐ 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:
View the hooks update script on GitHub.
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:
========================================
๐ 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:
forge script \
script/configure/allowlist/GetAdvancedPoolHooks.s.sol \
--rpc-url $ETHEREUM_SEPOLIA_RPC_URL
Expected output should now show your hooks address:
========================================
๐ช 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.solView the authorized callers query script on GitHub.
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:
========================================
๐ 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.solView the allowlist query script on GitHub.
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:
========================================
๐ 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.solView the allowlist check script on GitHub.
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:
========================================
๐ 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:
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:
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.
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.solshowsPOOL_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.solincludesETHEREUM_SEPOLIA_TOKEN_POOL).
9 Detach hooks
Detach hooks to disable enforcement. This sets the pool hooks address back to the zero address.
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:
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.
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):
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.
View the allowlist update script on GitHub.
| Env var | Required | Description |
|---|---|---|
POOL_HOOKS | Yes (v2) | Address of the AdvancedPoolHooks contract |
ADD_ADDRESSES | No | CSV or JSON array of addresses to add |
REMOVE_ADDRESSES | No | CSV or JSON array of addresses to remove |
Add an address:
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:
========================================
๐ 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:
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.