2024.07.08
Uniswap v4のhook実装
こんにちは。次世代システム研究室のT.M です。
注意
本稿は2024/7/8 時点の情報です。情報が更新されていることがありますので、ご注意ください。
Uniswap はUniswap Labs. の商標です。
はじめに
Uniswap の新しいプロトコルであるv4の構想が発表された際、プロトコルについて解説しました。あれから1年が経過し、後数ヶ月でリリースされるというところまで開発されました。本稿では、v4での主要な新機能であるHooksを試してみました。
v4新機能
Hooksを含む新機能の解説については以前の記事を参照してください。
サンプルコード
v4-templateというリポジトリが公開されており、Hooksを使ったコントラクトを開発するためのテンプレートとなるコードがあります。そのリポジトリにはサンプルとして、beforeSwap、afterSwap、beforeAddLiquidity、afterAddLiquidity のhookが実行されるたびにカウンターがインクリメントされるCounterコントラクトが実装されており、また、そのコントラクトをデプロイするためのスクリプトも提供されています。
Counterコントラクト
v4-peripheryにあるBaseHook コントラクトを実装したコントラクトでHooksを実装することができます。getHookPermissions() 関数をoverrideすることで、どのHookを実装しているかを示しています。
実装したいHook関数をoverrideにより実装をすることができます。下記コードでは、beforeSwap 時にbeforeSwapCount という値がインクリメントされています。
// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {BaseHook} from "v4-periphery/BaseHook.sol"; import {Hooks} from "v4-core/src/libraries/Hooks.sol"; import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol"; import {PoolKey} from "v4-core/src/types/PoolKey.sol"; import {PoolId, PoolIdLibrary} from "v4-core/src/types/PoolId.sol"; import {BalanceDelta} from "v4-core/src/types/BalanceDelta.sol"; import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "v4-core/src/types/BeforeSwapDelta.sol"; contract Counter is BaseHook { using PoolIdLibrary for PoolKey; mapping(PoolId => uint256 count) public beforeSwapCount; constructor(IPoolManager _poolManager) BaseHook(_poolManager) {} function getHookPermissions() public pure override returns (Hooks.Permissions memory) { return Hooks.Permissions({ beforeInitialize: false, afterInitialize: false, beforeAddLiquidity: true, afterAddLiquidity: false, beforeRemoveLiquidity: true, afterRemoveLiquidity: false, beforeSwap: true, afterSwap: true, beforeDonate: false, afterDonate: false, beforeSwapReturnDelta: false, afterSwapReturnDelta: false, afterAddLiquidityReturnDelta: false, afterRemoveLiquidityReturnDelta: false }); } function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, bytes calldata) external override returns (bytes4, BeforeSwapDelta, uint24) { beforeSwapCount[key.toId()]++; return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); } ... }
デプロイスクリプト
どのhookが有効であるかは、コントラクトのアドレスによって決まります。アドレスは160ビットであり、そのうち左から2番目から13番目までが各hookの有効フラグになっています。例えば、左から8番目のビットが1のアドレスは、beforeSwapのhookが有効になっているコントラクトです。どのビットがどのhookに対応しているかはドキュメントをご覧ください。
一般的なコントラクトのデプロイ方法では、コントラクトをデプロイするアドレスとそのアカウントのnonceによって決まります。そのため、任意のアドレスでコントラクトをデプロイすることができません。つまり、任意のhookを有効にしたコントラクトを1回でデプロイすることができません。
一般的なコントラクトのデプロイ方法は、CREATEというopcodeを使ったものであり、この方法ではコントラクトをデプロイするアドレスとそのnonceによってコントラクトアドレスが決まります。nonceの変更を繰り返すことで希望のアドレスのコントラクトをデプロイすることができますが、ガス代がかかってしまいます。そこで、CREATE2というopcodeを使ってコントラクトをデプロイすることで、希望のアドレスのコントラクトをデプロイします。この方法では、デプロイするアドレスとコントラクトのバイナリとsaltによってコントラクトアドレスが決定します。saltはnonceと異なり容易に変更することができるので、希望のアドレスになるまで繰り返すことができます。
HookMinterというライブラリのfind 関数で希望のフラグを持つコントラクトアドレスとなるsaltを計算します。希望のコントラクトアドレスになるまでsaltを変更してアドレスを計算しています。ただし、実装はドキュメントと異なり右から14個のビットがフラグとして使われています。フラグの場所や個数が異なるため注意が必要です。
library HookMiner { uint160 constant FLAG_MASK = 0x3FFF; uint256 constant MAX_LOOP = 100_000; function find(address deployer, uint160 flags, bytes memory creationCode, bytes memory constructorArgs) internal view returns (address, bytes32) { address hookAddress; bytes memory creationCodeWithArgs = abi.encodePacked(creationCode, constructorArgs); uint256 salt; for (salt; salt < MAX_LOOP; salt++) { hookAddress = computeAddress(deployer, salt, creationCodeWithArgs); if (uint160(hookAddress) & FLAG_MASK == flags && hookAddress.code.length == 0) { return (hookAddress, bytes32(salt)); } } revert("HookMiner: could not find salt"); } ... }
下記はデプロイスクリプトです。
uint160 flags = uint160( Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG | Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG ); (address hookAddress, bytes32 salt) = HookMiner.find(CREATE2_DEPLOYER, flags, type(Counter).creationCode, abi.encode(address(POOLMANAGER))); vm.broadcast(); Counter counter = new Counter{salt: salt}(IPoolManager(address(POOLMANAGER)));
動作確認
今回はanvilを用いて動作確認を行いました。
anvil起動
anvilを起動します。起動時のログにある秘密鍵をメモしておきます。
anvil
スクリプト実行
anvil起動時のログに表示された秘密鍵を設定して、スクリプトを実行します。このスクリプト内で、各種コントラクトのデプロイやswapが行われています。
forge script script/Anvil.s.sol \ --rpc-url http://localhost:8545 \ --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \ --broadcast
ログ確認
スクリプトで実行されたトランザクションのログを確認します。そのログからCounterコントラクトとPoolIDを調べます。
transactionTypeがCREATE2で、contractNameがCounterのcontractAddressがCounterのコントラクトアドレスです。
contractNameがPoolManagerで、functionがinitializeであるトランザクションのhashを調べ、そのhashのreceiptを確認します。そのreceiptのdataの最初の32バイトがPoolIdになります。
{ "hash": "0x52bcead131549391802b61a439cad9c3cd0fd5ffa61c65e22a4da81e06663361", "transactionType": "CREATE2", "contractName": "Counter", "contractAddress": "0x597f9395a55e7f6247fd4b1e69fb67e561624ac0", "function": null, "arguments": [ "0x5FbDB2315678afecb367f032d93F642f64180aa3" ], "transaction": { "from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c", "gas": "0x10fdb5", "value": "0x0", "input": "...", "nonce": "0x1", "chainId": "0x7a69" }, "additionalContracts": [], "isFixedGasLimit": false }, ... { "hash": "0x928c34e8eecc9f5be4796c0f496a93f451ccd45cf1c84e75ff1ef44b8b32bdfb", "transactionType": "CALL", "contractName": "PoolManager", "contractAddress": "0x5fbdb2315678afecb367f032d93f642f64180aa3", "function": "initialize((address,address,uint24,int24,address),uint160,bytes)", "arguments": [ "(0x0165878A594ca255338adfa4d48449f69242Eb8F, 0x5FC8d32690cc91D4c39d9d3abcBD16989F875707, 3000, 60, 0x597f9395a55e7F6247Fd4B1E69FB67e561624ac0)", "79228162514264337593543950336", "0x" ], "transaction": { "from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", "to": "0x5fbdb2315678afecb367f032d93f642f64180aa3", "gas": "0x139d9", "value": "0x0", "input": "...", "nonce": "0x9", "chainId": "0x7a69" }, "additionalContracts": [], "isFixedGasLimit": false }, ... { "status": "0x1", "cumulativeGasUsed": "0xd699", "logs": [ { "address": "0x5fbdb2315678afecb367f032d93f642f64180aa3", "topics": [ "0x3fd553db44f207b1f41348cfc4d251860814af9eadc470e8e7895e4d120511f4", "0x0000000000000000000000000165878a594ca255338adfa4d48449f69242eb8f", "0x0000000000000000000000005fc8d32690cc91d4c39d9d3abcbd16989f875707" ], "data": "0x461fcbcdd004d5b25a8018ab175181b1b2afd2501c198f240ae9790e045faadd0000000000000000000000000000000000000000000000000000000000000bb8000000000000000000000000000000000000000000000000000000000000003c000000000000000000000000597f9395a55e7f6247fd4b1e69fb67e561624ac0", "blockHash": "0x0d940d89a2e858bf1c74fa2bee32067b4f7c5201402019ea51ac3d29949da69c", "blockNumber": "0x7", "blockTimestamp": "0x668ace41", "transactionHash": "0x928c34e8eecc9f5be4796c0f496a93f451ccd45cf1c84e75ff1ef44b8b32bdfb", "transactionIndex": "0x0", "logIndex": "0x0", "removed": false } ], "logsBloom": "...", "type": "0x2", "transactionHash": "0x928c34e8eecc9f5be4796c0f496a93f451ccd45cf1c84e75ff1ef44b8b32bdfb", "transactionIndex": "0x0", "blockHash": "0x0d940d89a2e858bf1c74fa2bee32067b4f7c5201402019ea51ac3d29949da69c", "blockNumber": "0x7", "gasUsed": "0xd699", "effectiveGasPrice": "0x1d72b318", "blobGasPrice": "0x1", "from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", "to": "0x5fbdb2315678afecb367f032d93f642f64180aa3", "contractAddress": null, "root": "0x894e4a0ad77ebd486961fdf3593b42447711329df92b23cf703b02c9572aaed4" } ...
動作確認
Counterコントラクトに対して、beforeSwapCount(bytes32) をパラメータをPoolIdでcallします。そうすると1が返ってきました。1回swapをしたので、正しい値が返ってきたことがわかりました。
cast call 0x597f9395a55e7f6247fd4b1e69fb67e561624ac0 \ "beforeSwapCount(bytes32)(uint256)" 0x461fcbcdd004d5b25a8018ab175181b1b2afd2501c198f240ae9790e045faadd \ --rpc-url http://localhost:8545 1
まとめ
Uniswap の新しいプロトコルv4 の機能であるhookの実装方法を確認し、動作確認をしました。ドキュメントと実装に相違のある部分がありましたが、正しく動作することを確認できました。
次世代システム研究室では、グループ全体のインテグレーションを支援してくれるアーキテクトを募集しています。インフラ設計、構築経験者の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ募集職種一覧からご応募をお願いします。
皆さんのご応募をお待ちしています。
参考
グループ研究開発本部の最新情報をTwitterで配信中です。ぜひフォローください。
Follow @GMO_RD