Set advanced pool hooks using Hardhat
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/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 Hardhat template for a smoother setup.
git clone https://github.com/smartcontractkit/docs-cct-hardhat.git
cd docs-cct-hardhat
- Create a
.envfile by copying the.env.examplefile, and fill in the required values:
cp .env.example .env
# Keystore name (created via `npx hardhat keystore set`)
KEYSTORE_NAME=<your_default_keystore_name>
# RPC URLs (add the ones you need)
ETHEREUM_SEPOLIA_RPC_URL=your_eth_sepolia_rpc
ARBITRUM_SEPOLIA_RPC_URL=your_arbitrum_sepolia_rpc
# Etherscan API key (required only if you pass --verify to deployment tasks)
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 helper-config.ts file.
- To make sure your terminal has access to these variables, run the following command:
source .env
- Build the project:
npm install && npx hardhat compile
- Create an encrypted Hardhat keystore, if you haven't already:
npx hardhat keystore set PRIVATE_KEY
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 ARBITRUM_SEPOLIA_TOKEN=0x...
export ARBITRUM_SEPOLIA_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 task to check whether an AdvancedPoolHooks contract is currently attached to your token pool.
View the hooks query task on GitHub.
npx hardhat getAdvancedPoolHooks --network sepolia
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: npx hardhat deployAdvancedPoolHooks
Attach it with: npx hardhat updateAdvancedPoolHooks --newhook <address>
========================================
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 hardhat:$KEYSTORE_NAME \
--rpc "$ETHEREUM_SEPOLIA_RPC_URL" \
--rpc "$ARBITRUM_SEPOLIA_RPC_URL"
Expected output (example):
Fee: 130129888907619n = 0.000130129888907619 ETH
โ Enter password for Hardhat 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 task to deploy a new hooks contract.
View the hooks deployment task on GitHub.
Deployment flags:
--allowlist: CSV list of original outbound sender addresses allowed to initiate outbound transfers.--authorizedcallers: CSV list 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:
npx hardhat deployAdvancedPoolHooks \
--allowlist 0xYourDeployerAddress \
--authorizedcallers $ETHEREUM_SEPOLIA_TOKEN_POOL \
--network sepolia
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
โณ Deployment tx: 0x9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d
โ
AdvancedPoolHooks deployed successfully!
========================================
โ
Deployment Complete on Ethereum Sepolia!
========================================
AdvancedPoolHooks Address: 0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE
https://sepolia.etherscan.io/address/0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE
Deployment saved: 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: npx hardhat updateAdvancedPoolHooks --newhook <address> --network <network>
3. Manage allowlist: npx hardhat updateAllowList --poolhooks <address> --add "0xAddr" --network <network>
========================================
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 the updateAuthorizedCallers task:
View the authorized callers task on GitHub.
npx hardhat updateAuthorizedCallers \
--poolhooks $POOL_HOOKS \
--add $ETHEREUM_SEPOLIA_TOKEN_POOL \
--network sepolia
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
[Step 1] Applying authorized caller updates on Ethereum Sepolia
โณ Tx: 0xaaabbbcccdddeeefff000111222333444555666777888999aaabbbcccdddeeff
========================================
โ
Authorized callers updated on Ethereum Sepolia!
========================================
Pool Hooks: https://sepolia.etherscan.io/address/0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE
Transaction: 0xaaabbbcccdddeeefff000111222333444555666777888999aaabbbcccdddeeff
========================================
Now attach the hooks to your token pool using the updateAdvancedPoolHooks task:
View the hooks update task on GitHub.
| Flag | Required | Description |
|---|---|---|
--tokenpool | No | Token pool address (defaults to {CHAIN}_TOKEN_POOL env var) |
--newhook | Yes | Address of the deployed AdvancedPoolHooks contract |
npx hardhat updateAdvancedPoolHooks \
--newhook $POOL_HOOKS \
--network sepolia
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
[Step 1] Updating AdvancedPoolHooks on Ethereum Sepolia
โณ Tx: 0xbbccddeeff00112233445566778899aabbccddeeff00112233445566778899aa
โ
AdvancedPoolHooks updated successfully!
========================================
โ
Pool hooks updated on Ethereum Sepolia!
========================================
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 tasks 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:
npx hardhat getAdvancedPoolHooks --network sepolia
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.tsView the authorized callers query task on GitHub.
npx hardhat getAuthorizedCallers \
--poolhooks $POOL_HOOKS \
--network sepolia
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.tsView the allowlist query task on GitHub.
npx hardhat getAllowList \
--poolhooks $POOL_HOOKS \
--network sepolia
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.tsView the allowlist check task on GitHub.
npx hardhat isAllowListed \
--poolhooks $POOL_HOOKS \
--checkaddress 0xsenderaddress \
--network sepolia
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:
npx hardhat updateAllowList \
--poolhooks $POOL_HOOKS \
--remove "0xYourDeployerAddress" \
--network sepolia
Verify that your address is no longer allowlisted:
npx hardhat isAllowListed \
--poolhooks $POOL_HOOKS \
--checkaddress 0xYourDeployerAddress \
--network sepolia
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 hardhat:$KEYSTORE_NAME \
--rpc "$ETHEREUM_SEPOLIA_RPC_URL" \
--rpc "$ARBITRUM_SEPOLIA_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 (
getAdvancedPoolHooksshowsPOOL_HOOKS). - Your sender address is NOT on the allowlist (previous step returns NOT allowlisted).
- The token pool is an authorized caller on the hooks (
getAuthorizedCallersincludesETHEREUM_SEPOLIA_TOKEN_POOL).
9 Detach hooks
Detach hooks to disable enforcement. This sets the pool hooks address back to the zero address.
npx hardhat updateAdvancedPoolHooks \
--newhook 0x0000000000000000000000000000000000000000 \
--network sepolia
Verify hooks are detached:
npx hardhat getAdvancedPoolHooks --network sepolia
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 hardhat:$KEYSTORE_NAME \
--rpc "$ETHEREUM_SEPOLIA_RPC_URL" \
--rpc "$ARBITRUM_SEPOLIA_RPC_URL"
Expected output (example):
Fee: 130129888907619n = 0.000130129888907619 ETH
โ Enter password for Hardhat 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 task to add or remove addresses from the allowlist. Only the hooks contract owner can modify the allowlist.
View the allowlist update task on GitHub.
| Flag | Required | Description |
|---|---|---|
--poolhooks | Yes (v2) | Address of the AdvancedPoolHooks contract |
--add | No | Comma-separated addresses to add to the allowlist |
--remove | No | Comma-separated addresses to remove from the allowlist |
Add an address:
npx hardhat updateAllowList \
--poolhooks $POOL_HOOKS \
--add "0xYourDeployerAddress" \
--network sepolia
Your output should look something like this:
========================================
๐ Update AllowList
========================================
Chain: Ethereum Sepolia
Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
Pool Hooks: 0xA11c0FFeE0bAdA55CAfEBaBeDeaDBeEfF00DBABE
Action: Update allowlist
========================================
[Step 1] Updating allowlist on Ethereum Sepolia
โณ Tx: 0xccddeeff00112233445566778899aabbccddeeff00112233445566778899aabb
โ
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:
npx hardhat updateAllowList \
--poolhooks $POOL_HOOKS \
--remove "0xYourDeployerAddress" \
--network sepolia
Verify the updated allowlist by re-running the getAllowList or isAllowListed tasks from the previous step.