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
, andHooks
for pool interactions. - Uniswap v4 Periphery:
BaseHook
as the base for custom hook logic. - Chainlink CCIP:
IRouterClient
,Client
, andCCIPReceiver
for cross-chain messaging. - OpenZeppelin:
ERC20
andAccessControl
for the bridged token. - Custom:
BridgedToken
with mint/burn roles.
On deployment:
- Deploy
BridgedToken
on both chains with an identical name/symbol for consistency. - On source: Grant
BURN_ROLE
to the hook. - On destination: Grant
MINT_ROLE
to 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
), providinghookData
asabi.encode(destChainSelector, destReceiver, finalRecipient)
. - The pool executes the swap, yielding a positive
amount1
delta (token output). - The
afterSwap
Hook 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
BridgeReceiver
receives 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}
26
The 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
125
In getHookPermissions
The 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 getFee
LINK 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}
47
The 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.