Example 09: Pool-Based CCV Requirements on AdvancedPoolHooks
This example covers pool-based CCV (Cross-Chain Verifier) requirements on AdvancedPoolHooks, continuing the setup from Example 08.
Pool-Based CCVs
A token pool can require specific CCVs for its transfers. When a pool requires CCVs:
- the source OnRamp consults the pool’s
getRequiredCCVs(...)outbound and calls each CCV’sgetOutboundImplementationandforwardToVerifieron the source chain - the destination OffRamp resolves the same CCV addresses on the destination chain and calls
getInboundImplementationandverifyMessagebefore releasing or minting tokens
Because the destination OffRamp resolves the same address, a pool-required CCV must exist at the same address on both chains. Real CCVs are CREATE2-deployed to matching addresses on each chain.
AdvancedPoolHooks exposes this through per-remote-chain CCV configs:
outboundCCVs/inboundCCVs: base CCVs, always required for transfers to/from that chainthresholdOutboundCCVs/thresholdInboundCCVs: additional CCVs, required only when the transfer amount is at or abovethresholdAmountForAdditionalCCVsaddress(0)in a base list means “use the lane defaults alongside the other listed CCVs”
Threshold CCV lists require non-empty base lists (use address(0) in the base list if you want defaults below the threshold). Duplicates within and across lists are rejected.
What This Example Covers
- Deploy CrossChainToken +
AdvancedPoolHooks+ BurnMint pool on both chains (Example 08 steps 1-4). - Set pool CCV configs on both hooks.
- Send a token transfer and observe the CCV being consulted on both sides.
- Adjust the threshold amount and observe threshold CCVs becoming required.
- Clear the CCV config.
Scripts used:
script/examples/Example08.s.sol:DeployCCTBurnMintTokenAndPoolWithAdvancedPoolHookandExample08(setup, from Example 08)script/examples/Example09.s.sol:Example09SetPoolCCVConfigscript/examples/Example09.s.sol:Example09SetThresholdAmountscript/examples/Example06.s.sol:SendCCTTokenWithExtraArgsV3DefaultFinality(send)
Note on real CCVs
This starter kit does not deploy a real CCV. To try pool-based CCVs end to end on the live testnets, deploy your own CCV with the CCV starter kit and use its resolver address as
<CCV_ADDRESS>below. The short version:
- On-chain contracts, on both chains — use the on-chain contracts kit to deploy the CREATE2 factory, the
VersionedVerifierResolver, and the committee verifier, then apply the lane and signature config. The kit’s full flow walks the exactmakecommands; the resolver lands at the same address on both chains by construction (make deployments-checkasserts the parity).- Off-chain cell — use the off-chain kit to run the verifier and aggregator that actually attest messages (a Helm chart in Kubernetes, or the local docker-compose stack for testing). Without a running cell, the CCV exists on-chain but never attests, so messages requiring it will not execute.
Use the resolver address (not the committee verifier implementation) as
<CCV_ADDRESS>: a pool that lists the implementation address reverts during the fee quote.
Before You Start
Important: Keystore first
Use a local keystore account for script execution:
cast wallet import myAccount --interactive Enter private key: Enter password: `myAccount` keystore was saved successfully. Address: <YOUR_EOA_ADDRESS_SHOULD_APPEAR_HERE>This chapter assumes
--account myAccount.
Step 1: Deploy Token + Hook + Pool on Both Chains
Follow Example 08 steps 1-4 to deploy the token, hook and pool on both chains and link the lanes. Save from the logs:
<SEPOLIA_TOKEN_ADDRESS>,<SEPOLIA_ADVANCED_POOL_HOOK_ADDRESS>,<SEPOLIA_POOL_ADDRESS><AMOY_TOKEN_ADDRESS>,<AMOY_ADVANCED_POOL_HOOK_ADDRESS>,<AMOY_POOL_ADDRESS>
Step 2: Set Pool CCV Config on Sepolia
Require <CCV_ADDRESS> for both outbound and inbound traffic to Amoy:
FOUNDRY_PROFILE=cct forge script script/examples/Example09.s.sol:Example09SetPoolCCVConfig \
--rpc-url ethereumSepolia \
--account myAccount \
--broadcast \
--sig "run(address,uint64,address[],address[],address[],address[])" \
<SEPOLIA_ADVANCED_POOL_HOOK_ADDRESS> \
<AMOY_CHAIN_SELECTOR> \
"[<CCV_ADDRESS>]" \
"[]" \
"[<CCV_ADDRESS>]" \
"[]"
Step 3: Set Pool CCV Config on Amoy
Require the same <CCV_ADDRESS> for traffic to Sepolia:
FOUNDRY_PROFILE=cct forge script script/examples/Example09.s.sol:Example09SetPoolCCVConfig \
--rpc-url polygonAmoy \
--account myAccount \
--broadcast \
--sig "run(address,uint64,address[],address[],address[],address[])" \
<AMOY_ADVANCED_POOL_HOOK_ADDRESS> \
<SEPOLIA_CHAIN_SELECTOR> \
"[<CCV_ADDRESS>]" \
"[]" \
"[<CCV_ADDRESS>]" \
"[]"
Step 4: Send Transfer (Reuse Example06 Sender)
FOUNDRY_PROFILE=cct forge script script/examples/Example06.s.sol:SendCCTTokenWithExtraArgsV3DefaultFinality \
--rpc-url ethereumSepolia \
--account myAccount \
--broadcast \
--sig "run(address,uint64,address,address,uint256,uint32,address)" \
<SEPOLIA_ROUTER> \
<AMOY_CHAIN_SELECTOR> \
<RECEIVER_ON_AMOY> \
<SEPOLIA_TOKEN_ADDRESS> \
<AMOUNT> \
0 \
<FEE_TOKEN_ADDRESS>
The source OnRamp now consults <CCV_ADDRESS> outbound, and the destination OffRamp verifies the message with the same address before minting.
Note on execution with a self-run CCV
Until your CCV is onboarded into the CCIP indexer, the default executor cannot fetch its attestation, so the message sits at
UNTOUCHEDafter it is sent. Execute it yourself with the CCIP CLI (1.14.0 or later), reading the attestation directly from your own aggregator:ccip-cli manual-exec <SOURCE_TX_HASH> --verifiers grpcs://<YOUR_AGGREGATOR_HOST>:443See Test your setup in the CCV starter kit docs for the full walkthrough.
Step 5: Adjust the Threshold Amount
Threshold CCVs are required only for transfers at or above thresholdAmountForAdditionalCCVs. Example 08 deployed the hook with threshold 1, so threshold CCVs are effectively always required. Raise the threshold so they apply only to large transfers:
FOUNDRY_PROFILE=cct forge script script/examples/Example09.s.sol:Example09SetThresholdAmount \
--rpc-url ethereumSepolia \
--account myAccount \
--broadcast \
--sig "run(address,uint256)" \
<SEPOLIA_ADVANCED_POOL_HOOK_ADDRESS> \
10000000000000000000000
Repeat on Amoy for the inbound side:
FOUNDRY_PROFILE=cct forge script script/examples/Example09.s.sol:Example09SetThresholdAmount \
--rpc-url polygonAmoy \
--account myAccount \
--broadcast \
--sig "run(address,uint256)" \
<AMOY_ADVANCED_POOL_HOOK_ADDRESS> \
10000000000000000000000
To use threshold CCVs, set a config with non-empty threshold lists, for example base [<CCV_ADDRESS>] and threshold [<SECOND_CCV_ADDRESS>] (duplicates across lists are rejected).
Step 6: Clear the CCV Config
Re-run Example09SetPoolCCVConfig with empty lists to remove the config for that lane:
FOUNDRY_PROFILE=cct forge script script/examples/Example09.s.sol:Example09SetPoolCCVConfig \
--rpc-url ethereumSepolia \
--account myAccount \
--broadcast \
--sig "run(address,uint64,address[],address[],address[],address[])" \
<SEPOLIA_ADVANCED_POOL_HOOK_ADDRESS> \
<AMOY_CHAIN_SELECTOR> \
"[]" \
"[]" \
"[]" \
"[]"
Step 7: Verify Hook State
Verify the stored CCV config:
cast call <SEPOLIA_ADVANCED_POOL_HOOK_ADDRESS> "getCCVConfig(uint64)((address[],address[],address[],address[]))" <AMOY_CHAIN_SELECTOR> --rpc-url ethereumSepolia
cast call <AMOY_ADVANCED_POOL_HOOK_ADDRESS> "getCCVConfig(uint64)((address[],address[],address[],address[]))" <SEPOLIA_CHAIN_SELECTOR> --rpc-url polygonAmoy
Verify the threshold amount:
cast call <SEPOLIA_ADVANCED_POOL_HOOK_ADDRESS> "getThresholdAmount()(uint256)" --rpc-url ethereumSepolia
cast call <AMOY_ADVANCED_POOL_HOOK_ADDRESS> "getThresholdAmount()(uint256)" --rpc-url polygonAmoy
Monitor message status with the message ID on:
- https://ccip.chain.link