LapTrinhBlockchain

Chia sẻ kiến thức về Lập Trình Blockchain

Kiến thức Blockchain, Kiến thức lập trình, Lập trình Blockchain, Lập trình NodeJs, Lập trình Smart Contract, Nâng cao Kiến thức

Tìm hiểu về Uniswap V4 và cách tương tác với Uniswap V4

Tìm hiểu về Uniswap V4 và cách tương tác với Uniswap V4

Tìm hiểu về Uniswap V4 và cách tương tác với Uniswap V4

Chia sẻ bài viết
5
(68)

Uniswap V4 là phiên bản mới nhất của giao thức Uniswap, một sàn giao dịch phi tập trung (DEX) hoạt động trên blockchain Ethereum, được ra mắt vào ngày 31 tháng 1 năm 2025. Đây là một bước tiến quan trọng trong hệ sinh thái DeFi, mang lại nhiều cải tiến so với các phiên bản trước (V1, V2, V3), tập trung vào tính tùy chỉnh, hiệu quả chi phí và khả năng mở rộng.

Đặc điểm nổi bật của Uniswap V4

Uniswap V4 chính thức ra mắt vào 31/1/2025, sau khi trải qua hàng trăm đóng góp từ cộng đồng, chín cuộc kiểm tra độc lập và một chương trình thưởng lỗi trị giá 15,5 triệu USD. Điều này đảm bảo tính bảo mật và độ tin cậy, với giao thức đã xử lý hơn 2,75 nghìn tỷ USD khối lượng giao dịch (V2 và V3) mà không có vụ hack nào.

Đặc điểm chính của Uniswap V4:

  • Hooks: Đây là tính năng cốt lõi, cho phép nhà phát triển gắn logic Solidity vào chu kỳ giao dịch, chẳng hạn trước hoặc sau khi tạo hồ, thêm/loại bỏ thanh khoản, giao dịch hoặc quyên góp. “Hooks” là các hợp đồng được triển khai, gọi bởi PoolManager.sol để thực thi không cần quyền truy cập, mở ra các ứng dụng như lệnh giới hạn, oracles tùy chỉnh, quản lý phí và tự động hóa thanh khoản. Theo bài viết phân tích, hơn 150 “hooks” đã được phát triển, bao gồm phí động và quản lý thanh khoản tự động.
  • Thiết kế Singleton: Tất cả trạng thái và hoạt động của hồ được quản lý bởi một hợp đồng thông minh duy nhất (PoolManager.sol). Điều này mang lại tiết kiệm gas, vì việc tạo hồ chỉ là cập nhật trạng thái thay vì triển khai hợp đồng mới, và giao dịch qua nhiều hồ không cần chuyển token cho các hồ trung gian.
  • Kế toán Flash: Sử dụng EIP-1153 Transient Storage để ghi nhận hiệu quả các thay đổi số dư (giao dịch, sửa đổi thanh khoản, quyên góp). Các thay đổi được đối chiếu với nhau, và người dùng chỉ trả cho sự thay đổi số dư cuối cùng, không cần giải quyết trung gian, theo bài viết trên Binance Academy.
  • Hỗ trợ ETH gốc: Không cần bọc ETH thành WETH, giúp tiết kiệm chi phí và đơn giản hóa giao dịch, khác với V2 và V3.
  • Phí động: Hỗ trợ điều chỉnh phí linh hoạt (tăng hoặc giảm), với tần suất cập nhật do nhà phát triển quyết định (mỗi giao dịch, mỗi khối, hoặc theo lịch như hàng tuần, hàng tháng). Điều này mở ra không gian thiết kế cho tối ưu hóa phí, phân phối giá trị và nghiên cứu, theo tài liệu whitepaper V4.
  • Tiết kiệm gas: Tạo pool mới rẻ hơn đáng kể, lên đến 99.99% so với các phiên bản trước, và giao dịch đa bước (multi-hop swaps) cũng tiết kiệm gas, đặc biệt cho cặp ETH.

Dưới đây là bảng so sánh chi tiết giữa các phiên bản của Uniswap: V1, V2, V3 và V4:

Bảng so sánh chi tiết giữa các phiên bản của Uniswap: V1, V2, V3 và V4

Tìm hiểu các Contract của Uniswap V4

Chi tiết địa chỉ contract, bạn có thể xem tại Uniswap v4 Deployments. Trên mỗi chain thì V4 bao gồm các contract sau:

  • PoolManager: Là Hợp đồng trung tâm trong V4, triển khai dưới dạng Singleton. Đây là nơi lưu trữ toàn bộ tài sản của Uniswap V4.
    • Chức năng chính:
      • Quản lý tất cả các pool thanh khoản => Là điểm truy cập duy nhất tới toàn bộ hệ thống pool
      • Lưu trữ trạng thái của pools (liquidity, fee, ticks…).
      • Định tuyến các lệnh swap, mint, burn, flash, v.v.
      • Gọi các hook tùy chỉnh nếu pool có đăng ký.
    • Ưu điểm:
      • Vì không cần triển khai hợp đồng riêng cho mỗi pool nên rất tiết kiệm gas.
      • Là điểm truy cập duy nhất tới toàn bộ hệ thống pool
  • PositionDescription: Lưu metadata cho một vị trị thanh khoản.
    • Chức năng chính:
      • Cho phép vị trí thanh khoản được mô tả chi tiết
      • Được gán vào NFT hoặc LP Token cho dễ hiển thị
    • Ứng dụng:
      • Hữu ích hiển thị trên giao diện người dùng
  • PositionManager: Quản lý các vị trí thanh khoản.
    • Chức năng chính:
      • Cung cấp giao diện thân thiện để người dùng mint, increase, decrease hoặc burn vị trí.
      • Đóng gói logic phức tạp của PoolManager để dễ sử dụng.
      • Thường kết hợp với NFT để đại diện cho LP position.
    • Ứng dụng:
      • cửa ngõ chính thức để tạo và quản lý vị trí thanh khoản.
      • Tạo trải nghiệm tương tự như V3 nhưng tối ưu hơn nhờ kiến trúc PoolManager và Hooks.
      • Hỗ trợ cả người dùng thông thường lẫn các lập trình viên xây chiến lược LP phức tạp.
  • Quoter: Cho phép ước lượng giá trước khi thực hiện giao dịch thực.
    • Chức năng chính:
      • Cung cấp lệnh swap giả lập để lấy thông tin số token nhận được, giá trị price impact…
      • Khồng cần gửi giao dịch on-chain
    • Ứng dụng:
      • Hay sử dụng cho Frontend để hiển thị thông tin trước khi người dùng swap.
  • StateView: Hợp đồng chỉ đọc giúp truy xuất toàn bộ dữ liệu hệ thống.
    • Chức năng chính:
      • Truy xuất dữ liệu chi tiết như Thanh khoản tại 1 tick, trạng thái Pool, vị trí LP,…
      • Hợp đồng này không có chức năng ghi.
    • Ứng dụng:
      • Được sử dụng để xây analytics dashboard, explorer hoặc frontend hiển thị dữ liệu
  • Universal Router: Là bộ định tuyến swap tổng quát, hỗ trợ nhiều giao thức.
    • Chức năng chính:
      • Kết hợp swap giữa Uniswap V2, V3, V4, permit2, aggregator…
      • Thực hiện giao dịch phức tạp trong một giao dịch duy nhất.
      • Hỗ trợ swap + add liquidity + unwrap trong 1 call.
    • Ưu điểm
      • Giúp người dùng chỉ cần tương tác với 1 contract duy nhất
  • Permit2: Giúp chuẩn hóa và tái sử dụng permi (ERC-2612). Hợp đồng có chức năng chính như sau:
    • Chức năng chính:
      • Cho phép người dùng cấp phép token bằng chữ ký (gasless approve).
      • Hỗ trợ nhiều token, thời gian hết hạn, revoke nhanh chóng.
    • Ứng dụng:
      • Giảm số lần approve phải thực hiện => Tiết kiệm
      • Dễ kết hợp với Universal Router

Để hiểu được toàn bộ Uniswap V4 thực sự không hề đơn giản với nhiều kiến thức trong đó, đòi hỏi rất nhiều thời gian tìm hiểu. Vì vậy, trong phần tiếp theo tôi chỉ tập trung tìm hiểu các vấn đề liên quan tới công việc của mình.

Liệt kê toàn bộ danh sách các Pool của Uniswap V4

Để tiện chúng ta sử dụng các contracts V4 trên mạng lưới Sepolia. Có 3 cách để liệt kê toàn bộ danh sách các pool của Uniswap V4. Cụ thể như sau:

Cách 1: Quét sự kiện Initialize trên PoolManager

Để lấy danh sách các pool, chúng ta quét sự kiện Initialize từ contract PoolManager. Chú ý là V4 không có sự kiện PoolCreated.

Comparison for Pool Creation Events

Code để lấy danh sách pool như sau:

const ethers = require("ethers");
const poolManagerAddress = "0xE03A1074c86CFeDd5C142C4F04F1a1536e203543"; // PoolManager on Sepolia
const abi = [{
    "inputs": [
        { "indexed": true, "name": "id", "type": "bytes32" },
        { "indexed": true, "name": "currency0", "type": "address" },
        { "indexed": true, "name": "currency1", "type": "address" },
        { "indexed": false, "name": "fee", "type": "uint24" },
        { "indexed": false, "name": "tickSpacing", "type": "int24" },
        { "indexed": false, "name": "hooks", "type": "address" },
        { "indexed": false, "name": "sqrtPriceX96", "type": "uint160" },
        { "indexed": false, "name": "tick", "type": "int24" }
    ],
    "name": "Initialize",
    "type": "event"
}];
const fromBlock = 7258946;
const batchBlock = 10000;

const provider = new ethers.JsonRpcProvider("https://0xrpc.io/sep");
const contract = new ethers.Contract(poolManagerAddress, abi, provider);

async function getAllPools() {
    let startBlock = fromBlock;
    let lastestBlock = await provider.getBlockNumber();
    console.log(new Date(), "Lastest Block:", lastestBlock);
    while (startBlock<lastestBlock) {
        let endBlock = startBlock + batchBlock;
        if (endBlock>lastestBlock) endBlock = lastestBlock;
        let events = await contract.queryFilter("Initialize", startBlock, endBlock);
        console.log(`Query from block ${startBlock} to ${endBlock}: ${events.length} events!`);
        events.forEach(event => {
            let poolId = event.args[0];
            let token0 = event.args[1];
            let token1 = event.args[2];
            let fee = event.args[3];
            let tickSpacing = event.args[4];
            let hooks = event.args[4];
            let sqrtPriceX96 = event.args[4];
            let tick = event.args[4];
            console.log(`    ${poolId}: token0=${token0} - token1=${token1} - fee: ${fee} - tickSpacing=${tickSpacing} - hooks=${hooks} - sqrtPriceX96=${sqrtPriceX96} - tick=${tick}`);
        });
        startBlock = endBlock;
    }
    console.log("DONE!!!!!!");
}

getAllPools();

Kết quả chạy như sau:

025-06-02T14:02:47.816Z Lastest Block: 8461165
Query from block 7258946 to 7268946: 0 events!
Query from block 7268946 to 7278946: 0 events!
Query from block 7278946 to 7288946: 0 events!
Query from block 7288946 to 7298946: 2 events!
    0xe72386bf74a00abb710ecc830fe48718e9388c19cd819561eb55e27866f6aa03: token0=0x15cF58144EF33af1e14b5208015d11F9143E27b9 - token1=0x212224D2F2d262cd093eE13240ca4873fcCBbA3C - fee: 3000 - tickSpacing=60 - hooks=0x0000000000000000000000000000000000000000 - sqrtPriceX96=79228162514264337593543950336 - tick=0
    0x4c4778a7e8d31837b979adc172750b4f752376352c892e3dc367edd535819db4: token0=0x0000000000000000000000000000000000000000 - token1=0x315b94E71E1cD4De733c9eBB6618ED1D1DFC4D4b - fee: 3000 - tickSpacing=60 - hooks=0x0000000000000000000000000000000000000000 - sqrtPriceX96=112610006690532521790371528938881024 - tick=283356
Query from block 7298946 to 7308946: 1 events!
    0x1a40856304f8d0d891bfc8570280956ebcf5c7084a64fefd38c3494d466e4623: token0=0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238 - token1=0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14 - fee: 1000 - tickSpacing=60 - hooks=0x0000000000000000000000000000000000000000 - sqrtPriceX96=1273116126588280379470247060237456 - tick=193702
Query from block 7308946 to 7318946: 3 events!
    0xc37ad701e4b3f46f533de89b1578ac0f8495ec43c5334833a768f78c74d7ecb7: token0=0x3710B91E7b4EB3bF7dbA04f0b6e87fa20674f333 - token1=0xe4f45f6b5463C86b2cF92C4b396075899E52D2A9 - fee: 3000 - tickSpacing=140 - hooks=0xA9A6a0A31ae9Ecc7D5D492C5AB47510aeeC60080 - sqrtPriceX96=79228162514264337593543950336 - tick=0
    0xd16ba3ef2451117441a1b14e50c1ead545a520cd596a1913758cded0f6ee4911: token0=0x8a8400f0c28661821C46906402622093fE391538 - token1=0xc3778d5680E96289eA1AfF72C389DF73747F00a9 - fee: 3000 - tickSpacing=140 - hooks=0x1a37Ab6e5a28f5008dfD3A6731E48dD630928080 - sqrtPriceX96=79228162514264337593543950336 - tick=0
    0x5a68103d1e367f21030785a3f4b6a9ed7638e0914150c9befbef1863cc63d14a: token0=0x8ef0Fa2D99534Dc7C31C35faC74205550324bf39 - token1=0x9b4Cb7b8835879a34F7178cC21a4516416546b1d - fee: 3000 - tickSpacing=140 - hooks=0x9Ca1889BF7551190Efe990159fD060B3efc64080 - sqrtPriceX96=79228162514264337593543950336 - tick=0
Query from block 7318946 to 7328946: 0 events!
Query from block 7328946 to 7338946: 0 events!
...

Cách này có ưu và nhược điểm như sau:

  • Ưu điểm:
    • Áp dụng cho hầu hết các Dex
    • Dữ liệu chính xác và trực tiếp từ Onchain
  • Nhược điểm:
    • Request này nặng và cần nhiều request nên dễ bị RPC Node chặn
    • Nhiều Node không hỗ trợ truy vấn logs trong quá khứ

Cách 2: Dùng Uniswap V4 Subgraph (GraphQL)

Sử dụng Subgraph thì việc truy vấn đơn giản hơn rất nhiều và có nhiều thông tin hơn. Nhưng để làm được thì Uniswap Labs hoặc cộng đồng phải đã triển khai subgraph cho V4 trên The Graph. Một số subgraph ở thời điểm hiện tại:

Ví dụ truy vấn:

{
  pools(first: 5) {
    id
    poolId
    currency0
    currency1
    fee
    tickSpacing
  }
}

Cách này có ưu và nhược điểm như sau:

  • Ưu điểm: Đơn giản, không cần code
  • Nhược điểm:
    • Không phải Dex nào cũng hỗ trợ SubGraph sử dụng GraphQL để truy vấn thông tin.
    • Một số Dex có hỗ trợ nhưng không public mà để sử dụng Private

Cách 3: Lọc dữ liệu thông qua Block Explorer

Vào block explorer 0xE03A1074c86CFeDd5C142C4F04F1a1536e203543, sang tab Events, chọn filter theo sự kiện Initialize. Sau đó export dữ liệu ra tệp csv rồi sử dụng Pythong hoặc NodeJs để xử lý.

Cách này có ưu và nhược điểm như sau:

  • Ưu điểm: Lấy dữ liệu dễ dàng
  • Nhược điểm:
    • Không phải Explorer nào cũng hỗ trợ
    • Nhiều Explorer hỗ trợ những mỗi lần Export không được nhiều nên khá mất công
    • Khi có giao dịch có event cần tìm thì cần phải code thêm để lấy thông tin

Lấy thông tin chi tiết một pool của Uniswap V4

Để tiện chúng ta sử dụng các contracts V4 trên mạng lưới Unichain. Một pool được định danh bằng Pool Id, ví dụ Pool 0x3258f413c7a88cda2fa8709a589d221a80f6574f63df5a5b6774485d8acc39d9, khi bắt sự kiện Initialize ta có thông tin pool như sau:

Pool Id: 0x3258f413c7a88cda2fa8709a589d221a80f6574f63df5a5b6774485d8acc39d9:
    Token0 (Currency0): 0x0000000000000000000000000000000000000000 (ETH)
    Token1 (Currency1): 0x078D782b760474a361dDA0AF3839290b0EF57AD6 (USDC)
    Fee               : 500
    TickSpacing       : 10
    Hooks             : 0x0000000000000000000000000000000000000000

Từ thông tin của Pool, ta có thể tính toán ra PoolId như đoạn mã trong tệp PoolId.sol:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {PoolKey} from "./PoolKey.sol";

type PoolId is bytes32;

/// @notice Library for computing the ID of a pool
library PoolIdLibrary {
    /// @notice Returns value equal to keccak256(abi.encode(poolKey))
    function toId(PoolKey memory poolKey) internal pure returns (PoolId poolId) {
        assembly ("memory-safe") {
            // 0xa0 represents the total size of the poolKey struct (5 slots of 32 bytes)
            poolId := keccak256(poolKey, 0xa0)
        }
    }
}

Từ PoolId, để lấy thông tin chi tiết của pool bạn có thể gọi hàm poolKeys() của PositionManager, trong đó ta chỉ sử dụng 25 bytes trên cùng của PoolId để làm tham số truyền vào:

Lấy thông tin của Pool từ Pool Id

Để lấy thông tin chi tiết hơn của Pool ở thời điểm hiện tại, chúng ta phải sử dụng đến contract StateView thông qua các interface. Trong các interface (các hàm) này thì ít nhất phải có 1 tham số là poolId.

Hàm getLiquidity() của Uniswap V4
Hàm getSlot0() của Uniswap V4
Hàm getTickBitmap() của Uniswap V4
Hàm getTickLiquidity() của Uniswap V4

Ngoài ra bạn có thể gọi hàm trực tiếp từ PoolManager thông qua gọi các hàm extsload().

Một vấn đề quan trọng không kém là ta cần lấy balance của token0token1 trong pool hiện tại, từ đó xác định xem pool hiện tại có tổng cộng bao nhiêu tài sản. Trong V2 ta có thể lấy được dễ dàng thông qua hàm getReserves(), trong V3 ta có thể lấy gần đúng bằng hàm token.balanceOf(poolAddr) (gần đúng vì còn có thể bao gồm cả fee) nhưng trong V4 thì tôi chưa biết cách nào để lấy được, mà chỉ có cách thực hiện tổng hợp toàn bộ các vùng thanh khoản. Việc này sẽ tốn nhiều tài nguyên tính toán.

Tính toán lượng token nhận được khi swap

Bài toán thực tế ta cần biết khi swap tự tokenA sang tokenB thì với một lượng tokenA ta nhận được bao nhiêu tokenB?

Để tiện thao tác, chúng ta sẽ làm việc với pool có id là 0x56a0cd87d81cc71184a4b2dd42964591bf351d0cfdded03f9bc75b7b559344ce, đây là pool USDC-AERO của UniswapV4 trên mạng lưới BASE.

Sử dụng Quoter

Để lấy được thông tin chúng ta cần biết 2 địa chỉ contract liên quan tới UniswapV4:

Thông qua PositionManager ta lấy được thông tin Pool như sau:

{
  currency0: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
  currency1: '0x940181a94a35a4569e4529a3cdfb74e38fd98631',
  fee: '10000',
  tickSpacing: '200',
  hooks: '0x0000000000000000000000000000000000000000'
}

Bây giờ chúng ta gọi tới hàm của Quoter:

  • Nếu bạn biết chính xác lượng token vào, cần tính lượng token nhận được thì bạn gọi hàm quoteExactInput()
  • Nếu bạn biết chính xác lượng token ra, cần tính lượng token đưa vào thì bạn gọi hàm quoteExactOutput()

Trong hai hàm trên, thì tham số là cấu trúc QuoteExactParams:

struct QuoteExactParams {
    Currency exactCurrency;
    PathKey[] path;
    uint128 exactAmount;
}

Trong cấu trên có trường path là phức tạp nhất, là mảng của cấu trúc PathKey như sau:

struct PathKey {
    Currency intermediateCurrency;
    uint24 fee;
    int24 tickSpacing;
    IHooks hooks;
    bytes hookData;
}

Bây giờ chúng ta sang phần “Write Contract” của contract “Uniswap V4: Quoter“, tới hàm quoteExactInput() nhập dữ liệu vào như sau:

exactCurrency: 0x833589fcd6edb6e08f4c7c32d4f71b54bda02913
path: [["0x940181a94a35a4569e4529a3cdfb74e38fd98631","10000","200", "0x0000000000000000000000000000000000000000", "0x"]]
exactAmount: 100000000

Sau đó kích nút Simulate, ta sẽ nhận được kết quả trả về như hình vẽ (100 USDC -> 225.718 AERO):

Gọi hàm quoteExactInput() của Uniswap V4: Quoter

Tương tự, ta tới hàm quoteExactOutput() nhập thông số y như trên và kích vào nút Simulate ta được kết quả như hình (276.585 AERO -> 100 USDC):

Tính toán offchain

Về cách tính toán thì hoàn toàn giống với Uniswap V3 nên tôi không hướng dẫn chi tiết trong bài này. Ở đây tôi chỉ hướng dẫn các lấy dữ liệu để phục vụ tính toán.

Để lấy thông tin như liquidity, sqrtPriceX96, tickBitmap,… thì có hai cách:

  • C1: Gọi trực tiếp hàm extsload() của PoolManager, sau đó bạn phải tự decode dữ liệu. Việc decode dữ liệu khá phức tạp, đòi hỏi bạn phải hiểu sau về cách lưu dữ liệu của Uniswap V4.
  • C2: Sử dụng các hàm của StateView. StateView cung cấp sẵn các interface giúp bạn dễ dàng lấy thông tin của một pool.

Các interface của StateView gồm:

# Get sqrtPriceX96, current tick and liquidity
function getLiquidity(PoolId poolId) external view returns (uint128 liquidity);
function getSlot0(PoolId poolId) external view returns (uint160 sqrtPriceX96, int24 tick, uint24 protocolFee, uint24 lpFee);

# For tick calculator
function getTickBitmap(PoolId poolId, int16 tick) external view returns (uint256 tickBitmap);
function getTickLiquidity(PoolId poolId, int24 tick) external view returns (uint128 liquidityGross, int128 liquidityNet);
function getTickInfo(PoolId poolId, int24 tick) external view returns (uint128 liquidityGross, int128 liquidityNet, uint256 feeGrowthOutside0X128, uint256 feeGrowthOutside1X128);

Chúng ta có thể thử thao tác với StateView của UniswapV4 trên Base. Chúng ta lấy thông tin của pool USDC-AERO với id là 0x56a0cd87d81cc71184a4b2dd42964591bf351d0cfdded03f9bc75b7b559344ce.

liquidity = 2346227587985183

sqrtPriceX96 = 122686800199928518949486224060964516

Từ thông tin có được chúng ta hoàn toàn có thể tính được amountOut từ amountIn, với amountIn này có xảy ra hiện tượng cross tick hay không.

Tham khảo:

Bài viết này có hữu ích với bạn?

Kích vào một biểu tượng ngôi sao để đánh giá bài viết!

Xếp hạng trung bình 5 / 5. Số phiếu: 68

Bài viết chưa có đánh giá! Hãy là người đầu tiên đánh giá bài viết này.

Trả lời

Giao diện bởi Anders Norén