# Configure a sender allowlist with AdvancedPoolHooks using Foundry
Source: https://docs.chain.link/ccip/evm/tutorials/cross-chain-tokens/configure-sender-allowlist-advanced-pool-hooks-foundry
Last Updated: 2026-05-05

> For the complete documentation index, see [llms.txt](/llms.txt).

## Guide Versions

- [Foundry](/ccip/evm/tutorials/cross-chain-tokens/configure-sender-allowlist-advanced-pool-hooks-foundry)

- [Hardhat](/ccip/evm/tutorials/cross-chain-tokens/configure-sender-allowlist-advanced-pool-hooks-hardhat)

CCIP 2.0 token pools support an optional [`AdvancedPoolHooks`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/AdvancedPoolHooks.sol) 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.

This tutorial covers the **sender allowlist** use case: only approved addresses may initiate outbound transfers on the source chain. A sender not on the list reverts the source transaction with `SenderNotAllowed`. For the other capabilities, see the [AdvancedPoolHooks concept page](/ccip/concepts/cross-chain-token/advanced-pool-hooks) or [Enforce ACE policies on CCIP token transfers](/ccip/evm/tutorials/cross-chain-tokens/enforce-ace-policies-foundry).

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](https://nodejs.org/en/download/).
   - `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](https://book.getfoundry.sh/getting-started/installation) to install Foundry. \
   Verify the installation by running the following command:

```bash filename="Terminal"
forge --version
```

3. Install/Update Chainlink `ccip-cli`:

```bash filename="Terminal"
npm install -g @chainlink/ccip-cli
ccip-cli --version
```

4. **Clone the repository and navigate to the project directory**:

[CCIP 2.0 template](https://github.com/smartcontractkit/docs-cct-foundry)

Clone the CCIP 2.0 template for a smoother setup.

```bash filename="Terminal"
git clone https://github.com/smartcontractkit/docs-cct-foundry.git
cd docs-cct-foundry
```

5. Create an encrypted Foundry keystore, if you haven't already:

```bash filename="Terminal"
cast wallet import your_keystore_name --interactive
```

> **NOTE: Note**
>
> 1. For more information on how to create an encrypted Foundry keystore, refer to the [Foundry documentation](https://www.getfoundry.sh/cast/wallet-operations#wallet-operations).
> 2. `cast wallet import` will throw an error if a keystore with the same name already exists.
> 3. Use `cast wallet list` to check previously created keystores.

6. Create a `.env` file by copying the `.env.example` file, and fill in the required values:

```bash filename="Terminal"
cp .env.example .env
```

```bash filename=".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](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/HelperConfig.s.sol)

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

7. To make sure your terminal has access to these variables, run the following command:

```bash filename="Terminal"
source .env
```

8. Build the project:

```bash filename="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:

- [Burn & Mint (Foundry)](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-burn-mint-foundry)
- [Lock & Mint (Foundry)](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-foundry)

Export the addresses for the chain you are configuring:

```bash filename="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`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/TokenPool.sol) 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)

> **CAUTION: Funding requirements**
>
> - **Sepolia ETH**: the wallet `KEYSTORE_NAME` needs ETH on Sepolia to pay gas for
>   transactions.
> - **Your token on Sepolia**: the same wallet needs some `ETHEREUM_SEPOLIA_TOKEN` to transfer
>   the tokens across to the destination chain (Arbitrum Sepolia).

> **NOTE: One-wallet assumption**
>
> 1. This tutorial assumes the wallet stored in `KEYSTORE_NAME` is the owner of the token pool and will be
>    the owner of the deployed `AdvancedPoolHooks` contract. 2. These roles can be distributed across multiple
>    wallets if needed.

> **NOTE: Target the right contract**
>
> - `AdvancedPoolHooks` attach to a **TokenPool v2** contract. In Lock & Mint, do not
>   confuse the token pool with the `ERC20LockBox` (this tutorial configures token pool hooks only).
> - Hooks are configured per token pool, per chain. Repeat this setup for any other pool where you want hooks
>   enforced.

> **CAUTION: Common failure cases**
>
> - **Missing function selector:** the target pool is likely not a TokenPool v2 contract and does not
>   support advanced hooks.
> - **Wrong signer / wrong owner:** if a write script reverts with `OnlyCallableByOwner()`,
>   the wallet stored in `KEYSTORE_NAME` is not the current owner of that contract.
> - **AllowListNotEnabled():** the hooks contract was deployed with an empty allowlist, so allowlist mode
>   is disabled for that deployment.
> - **Hook call reverts:** confirm the token pool is listed as an authorized caller on the hooks contract
>   before attaching hooks.
> - **SenderNotAllowed(address):** the transfer sender is not allowlisted as the original outbound
>   sender.

### 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](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/allowlist/GetAdvancedPoolHooks.s.sol)

View the hooks query script on GitHub.

```bash filename="Terminal"
forge script \
  script/configure/allowlist/GetAdvancedPoolHooks.s.sol \
  --rpc-url $ETHEREUM_SEPOLIA_RPC_URL
```

Example output (no hooks attached):

```bash filename="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
========================================
```

> **NOTE**
>
> 1. `getAdvancedPoolHooks()` is only available on TokenPool v2.0 and later. If the script reverts with a
>    missing function selector, the pool is a v1 contract and does not support hooks. 2. If your token pool already has
>    hooks attached, you can detach them by following the 'Detach hooks' section, to follow the tutorial.

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

> **NOTE: Router address**
>
> You can find the router address for your chain in the [CCIP
> Directory](https://docs.chain.link/ccip/directory/testnet).

Export the Sepolia router address for `ccip-cli`:

```bash filename="Terminal"
export ETHEREUM_SEPOLIA_ROUTER=0x...
```

> **NOTE: Token amounts in this tutorial**
>
> 1. Earlier Foundry scripts use smallest-unit amounts (wei-like). In the `ccip-cli` examples below we use a
>    compact, human-readable token amount (for example, `1.23`).
> 2. The wallet stored in `KEYSTORE_NAME` should have some of your tokens on Sepolia so it can send a
>    cross-chain transfer.

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

```bash filename="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):

```bash filename="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.

> **NOTE: Inspect script/input/advanced-pool-hooks.json before deployment**
>
> The `DeployAdvancedPoolHooks.s.sol` script reads `script/input/advanced-pool-hooks.json` when
> environment variables are omitted. Inspect the file and clear any stale addresses you do not intend to use. Because
> allowlist enablement is permanent, prefer setting `ALLOWLIST` and `AUTHORIZED_CALLERS` explicitly.

[DeployAdvancedPoolHooks.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/allowlist/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.

> **CAUTION: Allowlist enablement is permanent**
>
> 1. Allowlist mode is set at deployment time and **cannot be changed later**.
> 2. If you deploy with a non-empty `ALLOWLIST`, the allowlist is permanently enabled.
> 3. If you deploy with an empty allowlist, calling `applyAllowListUpdates()` later will revert with `AllowListNotEnabled()`.

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:

> **NOTE: Why we start by allowlisting the deployer**
>
> 1. You will first allowlist your deployer address so you can attach hooks without immediately blocking your own
>    transfers. 2. Later in the tutorial, you will remove your address from the allowlist to demonstrate an enforced
>    failure.

```bash filename="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:

```bash filename="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: history/advanced-pool-hooks/ethereum-testnet-sepolia/1746652800-BnM-T-BurnMint-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:

```bash filename="Terminal"
export POOL_HOOKS=0xHooksAddress
```

> **NOTE: Deployment address recovery**
>
> - A broadcast deploy records the hooks address in the project store (`project/<selectorName>.json`,
>   under `addresses.active.poolHooks` and the `deployments` entry), so later scripts resolve it
>   automatically with no manual export.
> - If you lose the session export, read the address from the project store, or from the append-only history ledger at `history/advanced-pool-hooks/<selectorName>/` (the `POOL_HOOKS` key in the JSON file).
> - For a pool or token deployed outside this repository, run `make adopt-token` to record it in the same
>   registry. Environment variable exports still work as temporary overrides, but they are not saved.

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

> **NOTE: Already authorized at deploy time?**
>
> If you passed `AUTHORIZED_CALLERS` when deploying the hooks contract (as shown in the previous step), your
> pool is already authorized. You can skip the authorization command and go directly to attaching the hooks.

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

[UpdateAuthorizedCallers.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/authorized-callers/UpdateAuthorizedCallers.s.sol)

View the authorized callers script on GitHub.

```bash filename="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:

```bash filename="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](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/allowlist/UpdateAdvancedPoolHooks.s.sol)

View the hooks update script on GitHub.

```bash filename="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:

```bash filename="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
========================================
```

> **NOTE: Access control**
>
> - Updating authorized callers requires the hooks contract **owner**.
> - Attaching or detaching hooks requires the token pool **owner**.
> - If either command reverts with `OnlyCallableByOwner()`, the wallet stored in `KEYSTORE_NAME` is not the current owner of the target contract.

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

```bash filename="Terminal"
forge script \
  script/configure/allowlist/GetAdvancedPoolHooks.s.sol \
  --rpc-url $ETHEREUM_SEPOLIA_RPC_URL
```

Expected output should now show your hooks address:

```bash filename="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](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/authorized-callers/GetAuthorizedCallers.s.sol)

View the authorized callers query script on GitHub.

```bash filename="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:

```bash filename="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](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/allowlist/GetAllowList.s.sol)

View the allowlist query script on GitHub.

```bash filename="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:

```bash filename="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](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/allowlist/IsAllowListed.s.sol)

View the allowlist check script on GitHub.

```bash filename="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:

```bash filename="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.

> **NOTE: Admin access vs transfer permissions**
>
> Updating the allowlist is admin-only (`onlyOwner`) and is not gated by allowlist membership. Even if you
> are the hooks admin, your transfers will still revert unless your sender address is on the allowlist.

Remove your deployer address from the allowlist:

```bash filename="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:

```bash filename="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.

```bash filename="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.

```bash filename="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:

```bash filename="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.

```bash filename="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):

```bash filename="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](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/allowlist/UpdateAllowList.s.sol)

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    |

> **NOTE: v2 pools require POOL\_HOOKS**
>
> Always pass `POOL_HOOKS`. The allowlist lives on the `AdvancedPoolHooks` contract, not on the
> pool itself.

**Add an address:**

```bash filename="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:

```bash filename="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:**

```bash filename="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.

> **CAUTION: Educational Example Disclaimer**
>
> Please note, this page contains community examples only — these are not Chainlink products or services and are not
> supported or maintained by Chainlink. This code represents an example of using a Chainlink product or service, and is
> intended for demonstration and educational purposes only. It is provided "AS IS" and "AS AVAILABLE" without warranties
> of any kind, may not have been audited, and may omit checks or error handling. Each party intending to use this
> example code does so entirely at their own risk and must perform its own audits, security and code review, key
> management, and testing before any production deployment and ensure the operation and performance of such code matches
> expectations. Neither Chainlink Labs nor the Chainlink Foundation deploys, operates, monitors, maintains or endorses
> any deployment of this code. Note that this is not a Chainlink product, feature or service, and there are no
> commitments made with respect to the code, including compatibility with future Chainlink releases. You should not rely
> on this code without first conducting your own technical, engineering, and security review. This code is also outside
> the scope of any Chainlink bug bounty programs. Neither Chainlink Labs, the Chainlink Foundation, nor Chainlink node
> operators are responsible for outcomes due to errors in this example or how it is deployed or operated, or liable for
> any resulting claims or damages. Use of the Chainlink Network is subject to the Chainlink Foundation [Terms of
> Service](https://chain.link/terms), which provides important information and disclosures. By using this code, you
> acknowledge and agree to these terms.