+5

Khốn vãi lò. Phần 2: x*y=k đã đi đâu? Sao tôi không thấy nó trong code.

Ủa sao nói UniswapV3 vẫn là constant-product mà có thấy dòng nào trong code đảm bảo xy=kx * y= k đâu?

2. Suy luận từ V2 sang V3

2.1. Nhắc lại một chút về V2

Ai cũng biết, công thức toán khi swap của Uniswap là constant-product, nghĩa là: xy=kx*y=k. Giá trị k trước khi swap và sau khi swap phải được giữ nguyên, k chỉ thay đổi khi có hành động thêm hoặc rút thanh khoản.

Dễ hiểu hơn, ban đầu Pool đang có reserves là xx token0 và yy token1. Alice muốn swap một lượng x0x_0 token0 lấy một lượng y0y_0 token1. Thì x0x_0y0y_0 phải thoả mãn:

(x+x0)(yy0)=xy(x + x_0) * (y - y_0) = x * y

Ví dụ: ban đầu x=100x = 100, y=48y = 48. Alice muốn swap 20 token0 lấy token1 thì lượng token1 mà Alice nhận được là:

y0=484800100+20=8y_0 = 48 - \frac{4800}{100 + 20} = 8

Và, thanh khoản của Pool được tính theo công thức:

L=xyL=\sqrt{x*y}

Mắc đi hát karaoke quá mà ở quê không ai rủ đi.

Tại sao công thức tính L lại là L=xyL = \sqrt{x * y} ?

Suy luận một chút, tính chất đầu tiên của L cần phải được thoả mãn là: LL phải được giữ nguyên giá trị trước khi swap và sau khi swap. Nếu lấy công thức L=x+yL = x + y, tính chất đó sẽ không thể thoả mãn, mà ta đã có xy=kx* y = k (hằng số), do đó L sẽ là một công thức gì đó liên quan đến xyx * y, dạng L=f(xy)L = f(x * y).

Tính chất thứ hai mà LL cần phải được thoả mãn là tính tỷ lệ, nghĩa là khi một liquidity provider thêm thanh khoản vào Pool, lượng liquidity được ghi nhận cho provider đó phải có tỷ lệ tương xứng với liquidity của tổng Pool dưa trên số lượng token0 và token1 mà provider đó đã thêm vào.

Ví dụ, Pool đang có reserves là xx token0 và yy token1, có tổng liquidity là LtotalL_{total}, Alice thêm x0x_0 token0 và y0y_0 token1 vào Pool, liquidity được ghi nhận cho Alice L0L_0, phải thoả mãn:

L0Ltotal=x0x\frac{L_0}{L_{total}}=\frac{x_0}{x} hoặc L0Ltotal=y0y\frac{L_0}{L_{total}} = \frac{y_0}{y}

Nếu chúng ta sử dụng công thức L=xyL = x * y, công thức trên không thể thoả mãn, hãy cho các con số vào để dễ hình dung hơn.

x=300,y=600=>Ltotal=300600=180000x = 300, y = 600 => L_{total} = 300 * 600 = 180000

x0=100,y0=200=>L0=100200=20000x_0 = 100, y_0 = 200 => L_0 = 100 * 200 = 20000

=>L0Ltotal=20000180000=19=> \frac{L_0}{L_{total}} = \frac{20000}{180000} = \frac{1}{9}

=>x0x=100300=13=> \frac{x_0}{x} = \frac{100}{300} = \frac{1}{3}yy=200600=13\frac{y}{y} = \frac{200}{600} = \frac{1}{3}

Rõ ràng là công thức L=xyL= x * y không thể thoả mãn tính tỷ lệ.

Nếu L=xyL = \sqrt{x * y} thì sao? Hãy thử với các con số ở trên:

x=300,y=600=>Ltotal=300600=180000x = 300, y = 600 => L_{total} = \sqrt{300 * 600} = \sqrt{180000}

x0=100,y0=200=>L0=100200=20000x_0 = 100, y_0 = 200 => L_0 = \sqrt{100 * 200} = \sqrt{20000}

L0Ltotal=20000180000=13\frac{L_0}{L_{total}} = \frac{\sqrt{20000}}{\sqrt{180000}} = \frac{1}{3}

Như vậy, công thức L=xyL= \sqrt{x*y} thoả mãn tính tỷ lệ.

Oke, tiếp theo, tại sao lại hoặc giữa x0x\frac{x_0}{x}, y0y\frac{y_0}{y}. Hãy nhìn vào code một tí, hàm mint() của UniswapV2Pair.sol

image.png

Nếu là provider đầu tiên thêm thanh khoản thì LL sẽ được tính theo công thức L=xyL =\sqrt{x * y}, dòng 119-120.

Những provider tiếp theo chỉ cần tính theo công thức L0=min(x0xLtotal,y0yLtotal)L_0 = min(\frac{x_0}{x} * L_{total}, \frac{y_0}{y} * L_{total}) (dòng 122, 123) là sẽ tự động thoả mãn 2 tính chất ở trên. Điều này cho phép những provider này không cần thêm thành khoản đúng tỷ lệ x0y0=xy\frac{x_0}{y_0} = \frac{x}{y}, với mục đích có thể làm thay đổi price luôn trong lúc thêm thanh khoản, cho nên L0L_{0} phải là min(x0xLtotal,y0yLtotal)min(\frac{x_0}{x} * L_{total}, \frac{y_0}{y} * L_{total}).

2.2. Chuyển đổi từ V2 sang V3

Mặc dụ V2 ko quy ước về công thức giá, nhưng bây giờ chúng ta hãy quy ước rằng P được xem là giá của token0 trên token1, nghĩa là 1 token0 = P token1. Nghĩa là P=yxP = \frac{y}{x}

Chúng ta có thể biểu diễn tương quan của x,y,Px,y,P như sau (lưu ý, hình này không đúng về mặt toán học, đừng cố chia tỷ lệ độ dài hay diện tích làm gì, nó chỉ minh hoạ ra cho dễ hiểu):

image.png

Chúng ta có thể thấy những điều sau với Pool V2:

  • PP sẽ có khoảng giá trị (price-range) là (0,+)(0, +\infin), nhưng PP sẽ không bao giờ chạm đến 0
  • xx càng tăng, nghĩa là lượng token0 trong pool càng nhiều thì PP càng giảm image.png
  • yy càng tăng, nghĩa là lượng token1 trong pool càng nhiều thì PP càng tăng image.png
  • Bên phải của PP, chỉ chứa toàn token0. Bên trái của PP chỉ chứa toàn token1

Ok, bây giờ hãy qua lại điểm khác nhau về tính năng của V2 và V3 là: thanh khoản khi một provider thêm vào V2, provider không có quyền chọn price-range, LL sẽ được hoà tan hoàn toàn vào price-range (0,+)(0, +\infin). Còn ở V3, provider có quyền chọn price-range cho lượng thanh khoản của mình, khi nào current-price đi vào price-range mà provider đã chọn thì lượng thanh khoản mà anh ấy đã thêm vào mới được phép mang ra sử dụng, và L của mỗi price-range có giá trị khác nhau.

Từ hình mình hoạ của V2, chúng ta có thể mở rộng ra minh hoạ cho V3 như sau: image.png Để ra được hình minh hoạ này thì mình tư duy như sau:

  • Lấy A,B,C,D,E,F,G,H là những tick mà index của chúng đều chia hết cho tick-spacing, chúng không cần phải cách đều nhau.

  • Đầu tiên gom cả PoolV2 thành 1 price-range nhất định, ở đây là D-E. Trong price range này, sẽ có vừa có cả xx token0 và yy token1, giá hiện tại (current-price) đang là ở điểm D1 thuộc D-E và LDE=xyL_{D-E} = \sqrt{x * y}. Và nếu sau khi swap, price không vượt qua khỏi [1.0001D1.0001^D, 1.0001E11.0001^{E-1}, thì lệnh swap đó mới được sử dụng token0 hoặc token1 trong thanh khoản LD,EL_{D,E}

  • Bên trái của current-price vẫn sẽ toàn là token1, bên phải của current-price vẫn sẽ toàn là token0.

  • Và như mình đã nói ở phần trước, liquidity provider có thể thêm thanh khoản vào bất cứ price-range nào họ muốn. Do đó, L của các đoạn sẽ nhấp nhô (không bằng nhau). Khi họ thêm thanh khoản vào price-range lớn hơn current-price-range, họ chỉ được phép deposit token0. Khi họ thêm thanh khoản vào price-range nhỏ hơn current-price-range, họ chỉ được phép deposit token1.

Nhưng có một vấn đề ở đây là, với các đoạn E-F, F-G, G-H chỉ toàn token0 và các đoạn A-B, B-C, C-D chỉ toàn là token1 thì làm sao xác định được L của mỗi đoạn nếu chỉ dựa vào công thức thuần tuý L=xyL = \sqrt{x * y}, chẳng phải tất cả L sẽ là 0 sao?

2.3 Hô biến các công thức

Đầu tiên, hãy bắt đầu phân tích từ current-price-range trước: image.png

Ví dụ, ban đầu Alice tạo một Pool V3, vẫn chưa có price-range nào có thanh khoản. Bây giờ Alice là người thêm thanh khoản đầu tiên cho Pool, Alice muốn thêm một lượng LL thanh khoản vào price-range D-E và sau khi thêm thanh khoản xong, giá P sẽ ở đúng điểm D1D1. Như vậy ta có các tham số đầu vào là:

  • L
  • PD1P_{D1}
  • PDP_DPEP_E là 2 cận trên-dưới về gía tương ứng với price-range mà Alice đã chọn.

Chúng ta cần tìm: lượng Δx\Delta{x} token0 và Δy\Delta{y} token1 sao cho ΔyΔx\frac{\Delta{y}}{\Delta{x}} rơi đúng điểm D1D1

Ok, đầu tiên, chúng ta có các công thức cơ bản sau:

  1. L=xyL = \sqrt{x * y}

  2. P=yx<=>P=yxP = \frac{y}{x} <=> \sqrt{P} = \sqrt{\frac{y}{x}}

Các công thức tương quan giữa ΔP\Delta{P}Δx,Δy\Delta{x},\Delta{y} được suy ra từ công thức 1 và 2 như sau:

  1. Δy=LΔP\Delta{y} = L * \Delta{\sqrt{P}}

  2. Δx=LΔ1P\Delta{x} = L * \Delta{\frac{1}{\sqrt{P}}}

WTF 🤡, clq j z? Tại sao từ 1,2 lại suy ra được 3,4? Chứng minh sao?

Ok chứng minh công thức số 3 trước, đầu tiên mình giả sử trạng thái ban đầu là x0,y0,P0x_0, y_0, P_0, sau một lệnh swap nào đó trạng thái thành x1,y1,P1x_1, y_1, P_1, lưu ý cái giả sử này mình chỉ đang giả sử để chứng minh công thức thôi, đừng thắc mắc sao đang nói về thêm thanh khoản mà lại lôi swap và đây.

Δy=LΔP\Delta{y} = L * \Delta{\sqrt{P}}

<=>y1y0=P1P0<=> y_1 - y_0 = \sqrt{P_1} - \sqrt{P_0}

<=>y1y0=L(y1x1y0x0)<=>y_1-y_0 = L * (\sqrt{\frac{y_1}{x_1}} - \sqrt{\frac{y_0}{x_0}})

Mà như đã nói rất nhiều lần L không thay đổi trong quá trình swap, do đó L=x0y0=x1y1L = \sqrt{x_0y_0} = \sqrt{x_1y_1}. Thay vào L ta có:

<=>y1y0=y1x1y1x1y0x0y0x0<=> y_1 - y_0 = \sqrt{\frac{y_1x_1y_1}{x_1}} - \sqrt{\frac{y_0x_0y_0}{x_0}}

<=>y1y0=y12y02<=> y_1 - y_0 = \sqrt{y_1^2} - \sqrt{y_0^2}

<=>y1y0=y1y0<=> y_1 - y_0 = y_1 - y_0

Chứng minh công thức 4:

Δx=LΔ1P\Delta{x} = L * \Delta{\frac{1}{\sqrt{P}}}

x1x0=L(1P11P0)x_1 - x_0 = L * (\frac{1}{\sqrt{P_1}} - \frac{1}{\sqrt{P_0}})

<=>x1x0=L(1y1x11y0x0)<=> x_1 - x_0 = L * (\frac{1}{\sqrt{\frac{y_1}{x_1}}} - \frac{1}{\sqrt{\frac{y_0}{x_0}}})

<=>x1x0=L(x1y1x0y0)<=> x_1 - x_0 = L * (\sqrt{\frac{x_1}{y_1}}- \sqrt{\frac{x_0}{y_0}})

<=>x1x0=x1x1y1y1x0x0y0y0<=> x_1 - x_0 = \sqrt{\frac{x_1x_1y_1}{y_1}} - \sqrt{\frac{x_0x_0y_0}{y_0}}

<=>x1x0=x12x02<=> x_1 - x_0 = \sqrt{x_1^2} - \sqrt{x_0^2}

<=>x1x0=x1x0<=> x_1 - x_0 = x_1 - x_0

Công thức đã có, bây giờ hãy áp dụng nó vào việc tính lượng Δx\Delta{x} token0 và Δy\Delta{y} token1 của việc Alice thêm thanh khoản ở trên:

Δx=L(1PD11PE)=L(PEPD1PD1PE)\Delta{x} = L * (\frac{1}{\sqrt{P_{D1}}} - \frac{1}{\sqrt{P_{E}}}) = L * (\frac{\sqrt{P_E} - \sqrt{P_{D1}}}{\sqrt{P_{D1}}\sqrt{P_E}})

=>Δx=L(1.0001E1.0001D11.0001D11.0001E)=> \Delta{x} = L * (\frac{\sqrt{1.0001^E} - \sqrt{1.0001^{D1}}}{\sqrt{1.0001^{D1}}\sqrt{1.0001^E}})

Δy=L(PD1PD)\Delta{y} = L * (\sqrt{P_{D1}} - \sqrt{P_{D}})

=>Δy=L(1.0001D11.0001D)=> \Delta{y} = L * (\sqrt{1.0001^{D1}} - \sqrt{1.0001^{D}})

Chắc đang cấn chỗ xác định P0,P1P_0, P_1 cho việc tính Δx\Delta{x} đang hơi ngược với xác định P0,P1P_0, P_1 cho việc tính Δy\Delta{y} đang hơi ngược nhau đúng hông, mường tượng cho dễ thì việc di chuyển giá từ P0P_0 đến P1P_1 sẽ là xu hướng phình to của Δx\Delta{x} hoặc Δy\Delta{y}. Do đó:

  • Đối với tính Δx\Delta{x} thì P0=PEP_0 =P_EP1=PD1P_1= P_{D1}
  • Đối với tinh Δy\Delta{y} thì P0=PDP_0 = P_DP1=PD1P_1 = P_{D1}

Ok, đây là khi Alice là người thêm thanh khoản đầu tiên và thiết lập current-price-range, chúng ta sẽ sử dụng cả 2 công thức (3),(4)(3),(4) để tính lượng Δx,Δy\Delta{x}, \Delta{y} mà Alice phải cho vào Pool. Còn nếu Bob muốn thêm thanh khoản vào price-range lớn hơn current-price-range và Carol muốn thêm thanh khoản vào price-range nhỏ hơn current-price-range thì sao image.png image.png

  • Vì price-range E-F lớn hơn current-price-range nên nó chỉ chứa token0, do đó, chúng ta chỉ cần sử dụng công thức (4)(4) để tính lượng token0 mà Bob phải cho vào:

Δxbob=Lbob(1PE1PF)=Lbob(11.0001E11.0001F)\Delta{x}_{bob} = L_{bob} * (\frac{1}{\sqrt{P_E}} - \frac{1}{\sqrt{P_F}}) = L_{bob} * (\frac{1}{\sqrt{1.0001^E}} - \frac{1}{\sqrt{1.0001^F}})

  • Vì price-range C-D nhỏ hơn current-price-range nên nó chỉ chứa token1, do đó, chúng ta chỉ cần sử dụng công thức (3)(3) để tính lượng token1 mà Carol phải cho vào:

Δycarol=Lcarol(PDPC)=Lcarol(1.0001D1.0001C)\Delta{y}_{carol} = L_{carol} * (\sqrt{P_D} - \sqrt{P_C}) = L_{carol} * (\sqrt{1.0001^D} - \sqrt{1.0001^C})

Đối với các price-range còn lại là A-B, B-C và F-G, G-H cũng sử dụng cách tương tự

2.4 Nhìn vào code.

Thật ra có một luồng thoáng hơn ở chỗ Alice tạo pool, xác định current-price ở trên là không cần thiết Alice phải thêm thanh khoản, Alice có thể chỉ tạo pool và xác định current-price, các hành động thêm thanh thanh khoản về sau sẽ tự xác định price-range đang muốn thêm vào có phải là current-price-range hay không mà sử dụng đồng thời công thức (3),(4)(3), (4) hay chỉ sử dụng một trong hai công thức đó.

  • Tạo Pool, xác định current-price là hàm initialize() của UniswapV3Pool.sol image.png
  • Thêm thanh khoản là mint() của UniswapV3Pool.sol image.png
  • Xác định price-range đang muốn thêm thanh khoản có phải là current-price-range không, hay lớn hơn hay nhỏ hơn và sử dụng công thức tương ứng từ dòng 328 đến 369 của UniswapV3Pool.sol image.png
  • Nếu lớn hơn current-price-range, chỉ sử dụng công thức (4)(4), từ dòng 328 đến 335
  • Nếu là current-price-range, sử dụng cả 2 công thức (3),(4)(3), (4) từ dòng 336 đến 361
  • Nếu nhỏ hơn current-price, chỉ sử dụng công thức (3)(3), từ dòng 362 đến 370
  • Code của công thức (3)(3) nằm trong hàm getAmount1Delta() của file libraries/SqrtPriceMath.sol image.png
  • Code của công thức (4)(4) nằm trong hàm getAmount0Delta() của file libraries/SqrtPriceMath.sol image.png

Xong phần 2, phần 3 mình swap ha!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí