UniswapV3 Phần 5: Mở ra một chân trời mới khi tìm hiểu cách tính fee của UniswapV3

0 0 0

Người đăng: Trịnh Tân

Theo Viblo Asia

image.png

5. Fee

Đúng là như một chân trời mới được mở ra khi tìm hiểu về cách tính fee của UniswapV3. Lúc đầu mình cũng không quan tâm đến việc tính fee của UniswapV3 lắm, vì cơ bản cũng hiểu được sơ sơ về các tính toán và flow xung quang việc Swap và Liquidity thì coi như hiểu được core của nó rồi. Nhưng hôm nay mình có hứng nên ngồi xem lại để viết nốt về đoạn tính fee thì OMG luôn á, biết vậy viết sớm hơn.

5.1 Các biến liên quan đế việc tính fee.

Các biến global

image.png

Ở trong UniswapV3, có 2 biến là feeGrowthGlobal0X128feeGrowthGlobal1X129 để ghi lại lượng fee trên 1 liquidity unit đã kiếm được từ tất cả lệnh swap trong suốt vòng đời của Pool, cho token0token1 , 2 biến này luôn tăng, chúng ta có thể nhìn vào dòng code sau để thấy điều đó.

image.png

Đây là cách mà Pool ghi lại fee thu được trong hàm swap().

Lưu ý: Mặc dù feeGrowthGlobal0X128feeGrowthGlobal1X128 luôn tăng sau mỗi swap, nhưng chúng không phản ánh số fee thực tế mà mỗi đơn vị liquidity trong Pool nhận được. Đây là chỉ số toàn cục, được dùng làm mốc để tính toán fee mà một người dùng cụ thể có thể nhận, tùy thuộc vào:

  • họ cung cấp thanh khoản ở range nào,
  • và thời điểm nào.

Các biến riêng của tick

image.png

Hãy chú ý vào phần chú thích của 2 biến đó: fee growth per unit of liquidity on the _other_ side of this tick (relative to the current tick) only has relative meaning, not absolute — the value depends on when the tick is initialized.

feeGrowthOutside{0,1}X128 thì không tăng đều và không mang ý nghĩa tuyệt đối. Mỗi khi giá cross qua tick, giá trị này được cập nhật bằng phép trừ:

feeGrowthOutside = feeGrowthGlobalNow - feeGrowthOutside

Điều này giúp nó thể hiện fee ở phía bên kia của tick so với vị trí giá hiện tại, và bị "đảo chiều" mỗi lần tick bị cross. Vì vậy, giá trị feeGrowthOutside có thể tăng hoặc giảm. image.png

5.2 Từ initialize đến việc tính fee

Khi một tick được initialize, các biến như feeGrowthOutside{0,1} sẽ được gán giá trị từ feeGrowthGlobal{0,1} tại thời điểm đó. image.png

Uniswap quy ước rằng: mọi fee tích lũy trước thời điểm tick được khởi tạo đều nằm ở phía bên dưới tick (tức là fee phát sinh khi giá nằm ngoài range).

Do đó, feeGrowthOutside lúc initialize đóng vai trò là mốc tham chiếu để sau này tính toán fee đúng cho từng khoảng giá.

Trong bài viết này, mình tập trung vào trường hợp điển hình: cả tickLower và tickUpper đều đã được initialize trước thời điểm giá nằm trên cả hai tick (tức là tickLower, tickUpper < tickCurrent). Trường hợp này đơn giản nhưng đủ để hiểu được fee trong range được tính thế nào từ feeGrowthGlobalfeeGrowthOutside. Với các tình huống khác (ví dụ như giá đang nằm trong range khi tick được initialize), bạn hoàn toàn có thể suy luận từ nguyên lý trên để áp dụng tương tự.

Từ giờ, để đơn giản chúng ta chỉ cần xét việc tính fee của token0 cho ngắn gọn, vì với token1 cũng tương tự.

Bước 1: Tại thời điểm feeGrowthGlobal = 12, Alice lần đầu tiên thêm thanh khoản vào một price range nằm hoàn toàn bên dưới currentTick. Khi đó, cả lowerTick và upperTick đều được initialize với feeGrowthOutside = 12.

Đây là minh hoạ cho các vùng feeGrowthOutside của lowerTickupperTick, ở đây chúng ta ko cần xét đến vùng của feeGrowthGlobal

Làm sao chúng ta biết được nên vẽ vùng như thế nào, sao chúng ta lại biết 2 vùng đó đều nằm xuôi về phía bên trái , mời xem lại chú thích của code: fee growth per unit of liquidity on the _other_ side of this tick (relative to the current tick) only has relative meaning, not absolute — the value depends on when the tick is initialized.

Màu xanh là vùng feeGrowthOutside của lowerTick, màu đỏ là vùng feeGrowthOutside của upperTick. Bằng việc quan sát hình ta có thể suy luận được rằng ở trường hợp mà 2 vùng feeGrowthOutside của 2 tick đều nằm về cùng một hướng so với currentTick như thế này thì feeGrowthInside dễ dàng được suy ra bằng cách lấy hiệu của 2 vùng đó (cái nào trừ cho cái nào thì tuỳ vào trường hợp). Do đó ở đây:

feeGrowthInside = upperTick.feeGrowthOutside - lowerTick.FeeGrowOutside = 12 - 12 = 0.


Điều này đúng bởi vì price range này vừa được add và chưa có lệnh swap nào được thực hiện trong price range này nên chưa thu được fee nào.

Bước 2: Sau khi Alice thêm thanh khoản, currentTick di chuyển vào vùng price range vừa được tạo (từ bên phải qua trái). Trong quá trình này:

  • Trước khi vượt qua upperTick, feeGrowthGlobal đã tích lũy thêm 3 fee cho mỗi liquidity unit → đạt 15.
  • Khi vượt qua upperTick, Uniswap cập nhật feeGrowthOutside tại upperTick bằng feeGrowthGlobal hiện tại (15) trừ đi giá trị feeGrowthOutside được gán lúc tick được initialize (12), ta có: feeGrowthOutside = 15 - 12 = 3.
  • Giả sử price range này đã thu được 2 fee unit cho mỗi liquidity unit khi currentTick di chuyển từ upperTick vào giữa price range -> feeGrowthGlobal += 2 = 17.

Đây là minh hoạ cho các vùng feeGrowthOutside của lowerTickupperTick, và vùng feeGrowthGlobal (màu xanh lá)

Lúc này ta thấy upperTick.feeGrowthOutside đã bị đổi chiều do vị trí của currentTick so với upperTick đã thay đổi (xem lại chú thích của 2 biến trong struct Info). Và lúc này feeGrowthInside có thể tính như sau

feeGrowthInside = feeGrowthGlobal - lowerTick.feeGrowthOutside - upperTick.feeGrowthOutside = 17 - 12 - 3 = 2


Bước 3: Tiếp đó currentTick tiếp tục giảm và di chuyển ra khỏi price range mà Alice đã tạo. Trong quá trình này:

  • Trước khi vượt qua lowerTick, feeGrowthGlobal đã tích lũy thêm 2 fee cho mỗi liquidity unit → đạt 19.
  • Khi vượt qua lowerTick, cần cập nhật feeGrowthOutside tại lowerTick bằng feeGrowthGlobal hiện tại (19) trừ đi giá trị feeGrowthOutside được gán lúc tick được initialize (12), ta có: feeGrowthOutside = 19 - 12 = 7.

Đây là minh hoạ cho các vùng feeGrowthOutside của lowerTick và upperTick

Tương tự như ở bước 1, ở trường hợp này chúng ta có thể không cần quan tâm đến feeGrowthGlobal là bao nhiêu, chỉ cần tính feeGrowthInside như sau:

feeGrowthInside = lowerTick.feeGrowthOutside - upperTick.feeGrowthOutside = 7 - 3 = 4


Bước 4: Giả sử current sau khi thoát ra khỏi price range lại quay trở lại price range sau đó. Trong quá trình này:

  • Toàn bộ quá trình đi ra khỏi price range và quay trở lại feeGrowthGlobal tích luỹ thêm được 5 fee cho mỗi đơn vị liquidity unit -> chạm 24
  • Khi vượt qua lowerTick, cần cập nhật feeGrowthOutside tại lowerTick bằng feeGrowthGlobal hiện tại (24) trừ đi giá trị feeGrowthOutside trước đó (7), ta có: feeGrowthOutside = 24 - 7 = 17.
  • Giả sử từ lúc bắt đầu quay trở lại price range này, feeGrowthGlobal đã thu thêm được 3 fee trên mỗi liquidity unit -> feeGrowthGlobal += 3 = 27.

Đây là minh hoạ cho các vùng feeGrowthOutside của lowerTickupperTick, và vùng của feeGrowthGlobal

Lúc này ta thấy lower.feeGrowthOutside đã bị đổi chiều một lần nữa. Và lúc này feeGrowthInside có thể tính như sau:

feeGrowthInside = feeGrowthGlobal - lowerTick.feeGrowthOutside - upperTick.feeGrowthOutside = 27 - 17 - 3 = 7


-> Kết quả này chính xác bằng tổng fee ở bước này và fee đã thu được ở bước 3 (trên mỗi liquidity unit). Như vậy feeGrowthInside có tính chất luôn tăng giống như feeGrowthGlobal.

5.3 Triển khai một cách tổng quát hơn

Ở phần trên chúng ta đã hình dung được cách tính feeGrowthInside trong các trường hợp, nhưng bây giờ hãy rút ra một cách tính tổng quát hơn.

Mọi trường hợp về vị trí tương đối của lowerTickupperTick so với currentTick đều có thể mô tả như sau:

Từ hình vẽ thì chúng ta có thể dễ dàng rút ra:

feeGrowthInside = feeGrowthGlobal - feeGrowthAbove - feeGrowthBelow


Quay lại xét các bước ở mục 5.2, ta sẽ thấy mọi trường hợp đều có thể đưa về dạng tổng quát này.


Bước 1

Ở đây, currentTick đều nằm bên phải so với upperTicklowTick, và từ hình vẽ chúng ta dễ dàng thấy được rằng:

feeGrowthAbove = feeGrowthGlobal - upperTick.feeGrowthOutside = 12 - 12 = 0

feeGrowthBelow = lowerTick.feeGrowthOutside = 12

=> feeGrowthInside = feeGrowthGlobal - feeGrowthAbove - feeGrowthBelow = 12 - 12 - 0 = 0


Bằng với kết quả mà chúng ta đã tính ở phần trên

Bước 2

Ở đây, currentTick nằm giữa lowerTickupperTick, và từ hình vẽ chúng ta dễ dàng thấy được rằng:

feeGrowthAbove = upperTick.feeGrowthOutside = 3

feeGrowthBelow = lowerTick.feeGrowthOutside = 12

=> feeGrowthInside = feeGrowthGlobal - feeGrowthAbove - feeGrowthBelow = 17 - 3 - 12 = 2


Bằng với kết quả mà chúng ta đã tính ở phần trên

Bước 3

Ở đây, currentTick đều nằm phía bên trái của lowerTickupperTick, chúng ta không cần phải biết sau khi ra khỏi price range, feeGrowthGlobal đã tăng thêm bao nhiêu vì nó sẽ tự triệt tiêu trong phép tính, nhưng để đơn giản, mình cứ giả sử nó đã tăng thêm 2.5

feeGrowthGlobal += 2.5 = 19 + 2.5 = 21.5

feeGrowthAbove = upperTick.feeGrowthOutside = 3

feeGrowthBelow = feeGrowthGlobal - lowerTick.feeGrowthOutside = 21.5 - 7 = 14.5

=> feeGrowthInside = feeGrowthGloabl - feeGrowthAbove - feeGrowthBelow = 21.5 - 3 - 14.5 = 4


Bằng với kết quả mà chúng ta đã tính ở phần trên

Bước 4

Tương tự như bước 2

feeGrowthAbove = upperTick.feeGrowthOutside = 3

feeGrowthBelow = lowerTick.feeGrowthOutside = 17

=> feeGrowthInside = feeGrowthGlobal - feeGrowthAbove - feeGrowthBelow = 27 - 17 - 3 = 7


Bằng với kết quả mà chúng ta đã tính ở phần trên

Và cách xác định feeGrowthAbovefeeGrowthBelow này là những gì mà UniswapV3 đã thực sự làm trong code của mình, file Tick.sol:

image.png

Và một điều đáng chú ý ở đây là, feeGrowthInside có tính chất giống như feeGrowthGlobal, là một giá trị luôn không giảm.

5.4 Cơ chế snapshot và cách rút fee

Như mình đã nói ở trên, các biến dạng feeGrowth{} đều là giá trị của lượng fee kiếm được trên một liquidity unit, và giá trị feeGrowthInside được trả về từ hàm getFeeGrowthInside() luôn là một giá trị luôn tăng nên UniswapV3 đã sử dụng một cách làm rất quen thuộc là snapshop để ghi lại giá trị của feeGrowthInside tại thời điểm người dùng cuối cùng tương tác (gọi là snapshot), rồi so sánh với giá trị hiện tại khi cập nhật, file Position.sol

image.png

  • feeGrowthInside{} trả về từ getFeeGrowthInside() là cộng dồn toàn bộ fee (per unit liquidity) thu được trong khoảng [lowerTick, upperTick] tính đến hiện tại.
  • feeGrowthInsideLast{} trong struct Info của Position là snapshot cũ mà người dùng đã ghi lại lần cuối cùng.

=> Do đó, fee mà user kiếm được sẽ là:

tokensOwed = (feeGrowthInsideNow - feeGrowthInsideLast) * position_liquidity

Điều này giống với cách mà các hệ thống chia phần thưởng dùng share tokens, ví dụ, Compound, Aave, hoặc các hệ thống dùng accPerShare như:

rewards = (accRewardPerShare - userRewardDebt) * userShares


Đây là code: image.png

Lượng fee mà user kiếm được sẽ được ghi nhận như một khoản nợ mà Pool đang nợ user trong position đó, chứ không chuyển trực tiếp cho user.

Okay, vậy là chúng ta đã hiểu cách mà UniswapV3 ghi nhận fee mà một position kiếm được, nhưng để trigger luồng tính fee và ghi nhận được nợ thì user nên gọi hàm nào?

Đó là hàm burn():

image.png

User có thể gọi hàm này với amount = 0 với mục đích chỉ để trigger việc tính fee, còn nếu user muốn rút liquidiity luôn thì truyền amount > 0. Lượng token0token1 thu về từ việc burn liquidity cũng sẽ được ghi chung và tokensOwed{0/1} cùng với fee kiếm được.

Sau đó user gọi hàm collect để withdraw tokensOwed{0/1} về ví mình

image.png

Bình luận

Bài viết tương tự

- vừa được xem lúc

[Blockchain] Road to Bitcoin

. Chắc mọi người hẳn đã không còn xa lạ gì với anh chàng tỷ phú đã ném vỡ cửa kính ô tô nhà mình cùng với siêu năng lực điều khiển vật giá chỉ bằng lời nói, người đã đẩy định giá Bitcoin trên thị trường vượt ngưỡng 50K dolar/coin với những bài twitter để đời . .

0 0 73

- vừa được xem lúc

Khi Ethereum có chi phí giao dịch quá đắt đỏ - Tương lai cho layer2 ?

Với sự phát triển như vũ bão của Blockchain, ETH dường như đang quá tải và hệ quả là chi phí Gas đã lên đến 1000Gwei, phí để tạo những transaction phức tạp đã xấp xỉ 500$ . Và một giải pháp cứu cánh cho các sản phẩm Defi trên ETH chính là Layer2, và trong nhiệm vụ lần này Matic đang thể hiện khả năn

0 0 100

- vừa được xem lúc

Blockchain với Java - Tại sao không?

Cuộc cách mạng công nghiệp 4.0 ra đời kéo theo nhiều sự thay đổi và xu hướng mới được hình thành. Riêng đối với lĩnh vực CNTT cũng không nằm ngoài vùng ảnh hưởng mạnh mẽ. Chính làn sóng 4.

0 0 112

- vừa được xem lúc

Phân loại và tầm quan trọng của các node trong mạng blockchain

Trước khi đi vào phân loại và nêu rõ được tầm quan trọng của các node trọng mạng blockchain thì mình xin được trích dẫn khái niệm về blockchain từ Wikipedia như sau:. .

0 1 81

- vừa được xem lúc

Code Smart Contract bằng Assembly ?

Introduction. Hồi còn học trong ghế nhà trường bộ môn lập trình tốn nhiều não nhất của mình là code assembly. Nôm na thì bất cứ ngôn ngữ bậc cao nào như C , Go, Java,... được sinh ra để người dễ hiểu và dễ code , tuy nhiên chúng đều sẽ được compiled down xuống assembly một ngôn ngữ bậc thấp để máy h

0 0 71

- vừa được xem lúc

Dextool - Công cụ phân tích Decentralized Exchange tuyệt vời

. Trend Defi mặc dù đã bớt nhiệt nhưng những sản phẩm nổi bật của làn sóng này mang lại thì vẫn rất được người dùng ưa chuộng. Đặc biệt là các nền tảng Decentralized Exchange, tiêu biểu là Uniswap, SushiSwap, 1inch Exchange, FalconSwap,... Nhưng khi đã sử dụng các nền tảng DEx này mà không biết đến

0 0 121