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 :

  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/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 Hardhat template for a smoother setup.

Terminal
git clone https://github.com/smartcontractkit/docs-cct-hardhat.git
cd docs-cct-hardhat
  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 `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.

Complete List of Supported Chains

View the complete list of supported chains in the helper-config.ts 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 && npx hardhat compile
  1. Create an encrypted Hardhat keystore, if you haven't already:
Terminal
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:

Terminal
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_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 task to check whether an AdvancedPoolHooks contract is currently attached to your token pool.

getAdvancedPoolHooks.ts

View the hooks query task on GitHub.

Terminal
npx hardhat getAdvancedPoolHooks --network sepolia

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:  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:

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 hardhat:$KEYSTORE_NAME \
  --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \
  --rpc "$ARBITRUM_SEPOLIA_RPC_URL"

Expected output (example):

Terminal
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.

deployAdvancedPoolHooks.ts

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:

Terminal
npx hardhat deployAdvancedPoolHooks \
  --allowlist 0xYourDeployerAddress \
  --authorizedcallers $ETHEREUM_SEPOLIA_TOKEN_POOL \
  --network sepolia

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
โณ 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:

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 the updateAuthorizedCallers task:

updateAuthorizedCallers.ts

View the authorized callers task on GitHub.

Terminal
npx hardhat updateAuthorizedCallers \
  --poolhooks $POOL_HOOKS \
  --add $ETHEREUM_SEPOLIA_TOKEN_POOL \
  --network sepolia

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

[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:

updateAdvancedPoolHooks.ts

View the hooks update task on GitHub.

FlagRequiredDescription
--tokenpoolNoToken pool address (defaults to {CHAIN}_TOKEN_POOL env var)
--newhookYesAddress of the deployed AdvancedPoolHooks contract
Terminal
npx hardhat updateAdvancedPoolHooks \
  --newhook $POOL_HOOKS \
  --network sepolia

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

[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:

Terminal
npx hardhat getAdvancedPoolHooks --network sepolia

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.ts

View the authorized callers query task on GitHub.

Terminal
npx hardhat getAuthorizedCallers \
  --poolhooks $POOL_HOOKS \
  --network sepolia

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.ts

View the allowlist query task on GitHub.

Terminal
npx hardhat getAllowList \
  --poolhooks $POOL_HOOKS \
  --network sepolia

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.ts

View the allowlist check task on GitHub.

Terminal
npx hardhat isAllowListed \
  --poolhooks $POOL_HOOKS \
  --checkaddress 0xsenderaddress \
  --network sepolia

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
npx hardhat updateAllowList \
  --poolhooks $POOL_HOOKS \
  --remove "0xYourDeployerAddress" \
  --network sepolia

Verify that your address is no longer allowlisted:

Terminal
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.

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 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 (getAdvancedPoolHooks 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 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
npx hardhat updateAdvancedPoolHooks \
  --newhook 0x0000000000000000000000000000000000000000 \
  --network sepolia

Verify hooks are detached:

Terminal
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.

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 hardhat:$KEYSTORE_NAME \
  --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \
  --rpc "$ARBITRUM_SEPOLIA_RPC_URL"

Expected output (example):

Terminal
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.

updateAllowList.ts

View the allowlist update task on GitHub.

FlagRequiredDescription
--poolhooksYes (v2)Address of the AdvancedPoolHooks contract
--addNoComma-separated addresses to add to the allowlist
--removeNoComma-separated addresses to remove from the allowlist

Add an address:

Terminal
npx hardhat updateAllowList \
  --poolhooks $POOL_HOOKS \
  --add "0xYourDeployerAddress" \
  --network sepolia

Your output should look something like this:

Terminal
========================================
๐Ÿ“ 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:

Terminal
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.

What's next

Get the latest Chainlink content straight to your inbox.