Uniswap v4
Building Cross-Chain Swap: Hook + CCIP
Learn how to integrate Uniswap v4 hooks with Chainlink CCIP to build secure, trust-minimized cross-chain swaps using burn-and-mint mechanics.
Uniswap v4 hooks unlock the ability to extend pool logic beyond single-chain swaps. By combining them with Chainlink’s Cross-Chain Interoperability Protocol (CCIP), we can build a system where a swap on one chain triggers a mint on another. In this example, a user swaps ETH for a “bridged token” on Ethereum Sepolia. Still, instead of receiving the token locally, the hook burns it and instructs the destination chain (Unichain Sepolia) to mint tokens to the user’s account.
This is not just a technical curiosity. It demonstrates how Uniswap hooks can natively integrate cross-chain liquidity movement, a pattern critical for multi-chain DeFi.
Contract Architecture and Dependencies
The architecture spans two chains: the source (where the Uniswap v4 pool and hook reside) and the destination (hosting the receiver). Key dependencies include:
- Uniswap v4 Core:
IPoolManager,PoolKey,BalanceDelta,Currency, andHooksfor pool interactions. - Uniswap v4 Periphery:
BaseHookas the base for custom hook logic. - Chainlink CCIP:
IRouterClient,Client, andCCIPReceiverfor cross-chain messaging. - OpenZeppelin:
ERC20andAccessControlfor the bridged token. - Custom:
BridgedTokenwith mint/burn roles.
On deployment:
- Deploy
BridgedTokenon both chains with an identical name/symbol for consistency. - On source: Grant
BURN_ROLEto the hook. - On destination: Grant
MINT_ROLEto the receiver. - Configure the hook with the CCIP router, LINK token, and bridged token addresses.
- Set up allowlists on the receiver for source chain selectors and senders.
This setup ensures controlled access and prevents unauthorized minting or messaging.
Operational Flow: Swap to Cross-Chain Bridge
The end-to-end flow integrates Uniswap v4 swaps with CCIP:
- A user initiates a swap on the source chain's Uniswap v4 pool (ETH to
BridgedToken), providinghookDataasabi.encode(destChainSelector, destReceiver, finalRecipient). - The pool executes the swap, yielding a positive
amount1delta (token output). - The
afterSwapHook triggers: validate conditions, transfer and burn tokens, construct and send the CCIP message funded by LINK. - CCIP relays the message to the destination chain.
- The
BridgeReceiverreceives and validates the message, then mints the equivalent amount to the final recipient.
The BridgedToken Contract
The BridgedToken serves as the cross-chain asset, implementing a standard ERC20 with access-controlled minting and burning. It uses OpenZeppelin's AccessControl to define MINT_ROLE and BURN_ROLE, restricting these operations to authorized contracts (the hook on source, receiver on destination).
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.24;
3
4// BridgedToken.sol - Deploy this on BOTH source and destination chains.
5import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
6import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
7
8contract BridgedToken is ERC20, AccessControl {
9 bytes32 public constant MINT_ROLE = keccak256("MINT_ROLE");
10 bytes32 public constant BURN_ROLE = keccak256("BURN_ROLE");
11
12 constructor(string memory name, string memory symbol) ERC20(name, symbol) {
13 _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
14 }
15
16 function mint(address to, uint256 amount) external {
17 require(hasRole(MINT_ROLE, msg.sender), "Caller is not a minter");
18 _mint(to, amount);
19 }
20
21 function burn(address from, uint256 amount) external {
22 require(hasRole(BURN_ROLE, msg.sender), "Caller is not a burner");
23 _burn(from, amount);
24 }
25}
26The constructor initializes the token and grants admin rights to the deployer, who can then assign roles. The mint and burn functions enforce role checks before invoking internal ERC20 methods, ensuring only the hook or receiver can alter supply. This design maintains token integrity across chains by mirroring supply changes via CCIP messages.
The CrossChainBridgeHook Contract
Deployed on the source chain, CrossChainBridgeHook extends Uniswap v4's BaseHook to intercept post-swap events. It activates only for ETH-to-token swaps (zeroForOne), burns the output tokens, and sends a CCIP message to trigger minting on the destination. The hook decodes hookData provided during the swap to extract the destination chain selector, receiver address, and final recipient.
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.24;
3
4// Source Chain: CrossChainBridgeHook.sol
5
6// ------------------- Uniswap v4 Core -------------------
7import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol";
8import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
9import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
10import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
11import {
12 Currency,
13 CurrencyLibrary
14} from "@uniswap/v4-core/src/types/Currency.sol";
15
16// ------------------- Uniswap v4 Periphery -------------------
17import {BaseHook} from "@uniswap/v4-periphery/src/base/hooks/BaseHook.sol";
18
19// ------------------- Chainlink CCIP -------------------
20import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol";
21import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
22import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
23
24// ------------------- Custom Token -------------------
25import {BridgedToken} from "./BridgedToken.sol"; // Must expose burn(address from, uint256 amount)
26
27contract CrossChainBridgeHook is BaseHook {
28 using CurrencyLibrary for Currency;
29
30 address public immutable ccipRouter;
31 address public immutable linkToken;
32 BridgedToken public immutable bridgedToken;
33
34 constructor(
35 IPoolManager _poolManager,
36 address _ccipRouter,
37 address _linkToken,
38 address _bridgedToken
39 ) BaseHook(_poolManager) {
40 ccipRouter = _ccipRouter;
41 linkToken = _linkToken;
42 bridgedToken = BridgedToken(_bridgedToken);
43 }
44
45 // ------------------- Hook Permissions -------------------
46 function getHookPermissions()
47 public
48 pure
49 override
50 returns (Hooks.Permissions memory)
51 {
52 return
53 Hooks.Permissions({
54 beforeInitialize: false,
55 afterInitialize: false,
56 beforeAddLiquidity: false,
57 afterAddLiquidity: false,
58 beforeRemoveLiquidity: false,
59 afterRemoveLiquidity: false,
60 beforeSwap: false,
61 afterSwap: true,
62 beforeDonate: false,
63 afterDonate: false,
64 beforeSwapReturnDelta: false,
65 afterSwapReturnDelta: false,
66 afterAddLiquidityReturnDelta: false,
67 afterRemoveLiquidityReturnDelta: false
68 });
69 }
70
71 // ------------------- After Swap Hook -------------------
72 function afterSwap(
73 address sender,
74 PoolKey calldata key,
75 IPoolManager.SwapParams calldata params,
76 BalanceDelta delta,
77 bytes calldata hookData
78 ) external override onlyByManager returns (bytes4, int128) {
79 // Only trigger for ETH -> Token swaps (zeroForOne = true, token1 output > 0)
80 if (
81 !key.currency0.isNative() ||
82 !params.zeroForOne ||
83 delta.amount1() <= 0
84 ) {
85 return (this.afterSwap.selector, 0);
86 }
87
88 (
89 uint64 destChainSelector,
90 address destReceiver,
91 address finalRecipient
92 ) = abi.decode(hookData, (uint64, address, address));
93
94 uint256 amountToBridge = uint256(int256(delta.amount1()));
95
96 // Pull output tokens from sender
97 bridgedToken.transferFrom(sender, address(this), amountToBridge);
98
99 // Burn tokens now held by this hook
100 bridgedToken.burn(address(this), amountToBridge);
101
102 // Construct CCIP message
103 Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({
104 receiver: abi.encode(destReceiver),
105 data: abi.encode(finalRecipient, amountToBridge), // instruct destination chain to mint
106 tokenAmounts: new Client.EVMTokenAmount[](0), // no tokens sent
107 extraArgs: Client._argsToBytes(
108 Client.EVMExtraArgsV1({gasLimit: 200_000})
109 ),
110 feeToken: linkToken
111 });
112
113 IRouterClient router = IRouterClient(ccipRouter);
114
115 uint256 fees = router.getFee(destChainSelector, message);
116
117 // Approve LINK fees and send message
118 IERC20(linkToken).approve(address(router), fees);
119 router.ccipSend(destChainSelector, message);
120
121 return (this.afterSwap.selector, 0);
122 }
123}
124
125In getHookPermissionsThe hook specifies activation only in afterSwap, allowing it to process swap outputs without interfering elsewhere. The afterSwap function first validates the swap type: it checks for native ETH as currency0, zeroForOne direction, and positive token1 delta. It then decodes hookData (passed via the swap call) to retrieve bridging parameters.
Post-validation, the hook transfers the output tokens from the swap sender to itself using transferFrom, then burns them via the BridgedToken's burn method. This reduces supply on the source chain. Subsequently, it constructs a Client.EVM2AnyMessage with the receiver ABI-encoded, data encoding the recipient and amount for minting, no token transfers (since burning handles supply), and gas limits for execution. Fees are calculated via getFeeLINK is approved, and the message is sent using ccipSend. The function returns the selector and zero delta adjustment, as no further pool modifications are needed.
This hook effectively transforms a local swap into a cross-chain bridge, leveraging Uniswap's delta for precise amount bridging.
The BridgeReceiver Contract
On the destination chain, BridgeReceiver inherits from Chainlink's CCIPReceiver to handle incoming messages. It validates the source chain and sender via allowlists, decodes the message data, and mints tokens to the final recipient.
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.24;
3
4// Destination Chain: BridgeReceiver.sol
5// Receives CCIP message and mints BridgedToken to the final recipient.
6
7import {CCIPReceiver} from "@chainlink/contracts-ccip/src/v0.8/ccip/applications/CCIPReceiver.sol";
8import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
9
10import {BridgedToken} from "./BridgedToken.sol"; // Import the token
11
12contract BridgeReceiver is CCIPReceiver {
13 BridgedToken public immutable bridgedToken;
14
15 mapping(uint64 => bool) public allowlistedSourceChains;
16 mapping(address => bool) public allowlistedSenders;
17
18 event TokensMinted(address recipient, uint256 amount);
19
20 constructor(address _ccipRouter, address _bridgedToken) CCIPReceiver(_ccipRouter) {
21 bridgedToken = BridgedToken(_bridgedToken);
22 }
23
24 function allowlistSourceChain(uint64 _sourceChainSelector, bool allowed) external {
25 allowlistedSourceChains[_sourceChainSelector] = allowed;
26 }
27
28 function allowlistSender(address _sender, bool allowed) external {
29 allowlistedSenders[_sender] = allowed;
30 }
31
32 function _ccipReceive(Client.Any2EVMMessage memory message) internal override {
33 uint64 sourceChainSelector = message.sourceChainSelector;
34 address sender = abi.decode(message.sender, (address));
35
36 require(allowlistedSourceChains[sourceChainSelector], "Source chain not allowlisted");
37 require(allowlistedSenders[sender], "Sender not allowlisted");
38
39 (address finalRecipient, uint256 amount) = abi.decode(message.data, (address, uint256));
40
41 // Mint the tokens
42 bridgedToken.mint(finalRecipient, amount);
43
44 emit TokensMinted(finalRecipient, amount);
45 }
46}
47The constructor sets the CCIP router and bridged token. Allowlist functions enable admin control over trusted sources. In _ccipReceive (overridden from CCIPReceiver), the function extracts the source selector and sender, enforcing allowlist requirements via require. It decodes the message data to obtain the recipient and amount, then calls mint on BridgedToken. An event TokensMinted logs the operation for transparency.
This receiver ensures secure, permissioned minting, completing the bridge by restoring the token supply on the destination.
This mechanism achieves cross-chain atomicity without direct token transfers, relying on burn-mint semantics. Gas limits (e.g., 200,000) ensure reliable execution, while allowlists mitigate unauthorized access. Developers should monitor CCIP fees and LINK balances to maintain operability.
This implementation demonstrates Uniswap v4's extensibility, blending AMM efficiency with cross-chain capabilities for enhanced DeFi composability.


