Mất đâu đó khoảng 2 năm rưỡi thì mình mới bắt đầu ngấm được UniswapV3, những hình khối đầu tiên về nó mới xuẩt hiện trong trí tưởng tượng của mình, nói chung nó vẫn là x*y=k nhưng mà rất là quằng, chặn trên chặn dưới đủ các kiểu.
Mình sẽ viết lại theo cách mình hiểu và đi từ điểm mà mình bắt đầu hiểu về UniswapV3
Giờ viết mở bài đến đây, nếu có ai upvote thì tối mình viết tiếp...
Trời ơi condix nào mới down vote
1. Quản lý thanh khoản của price-range
1.1 Price-range là gì
Như chúng ta đã biết, V3 ra đời nhằm giải quyết vấn đề "hiệu quả sử dụng vốn" cho những nhà cung cấp thanh khoản (liquidity provider) mà V2 gặp phải. Ở V2, thanh khoản của mọi liquidity provider được trải rộng trong khoảng giá (price-range) (0, ). V3 cố gắng đưa ra sự lựa chọn về price-range cho liquidity provider, cho phép liquidity provider nâng cao hiệu quả sử dụng vốn của mình. Ví dụ: 1 ETH đang có giá là $3500 USDT ở trong Pool, Alice có một lượng 2 ETH và 7000 USDT, muốn cung cấp thanh khoản:
- Nếu đây là Pool V2, đang có reserves 10 ETH và 35.000 USDT, thì khi Alice thêm vào, 2 ETH và 7000 USDT sẽ hoà tan hoàn toàn với reserves của Pool V2 ở mức giá hiện tại là $3500, mặc cho số phận đưa đẩy mà khi rút ra sẽ nhận lại số lượng ETH và USDT tương ứng với số phận.
- Nếu đây là Pool V3, Alice cho rằng giá của ETH sẽ vọt lên $4000-$4500, và Alice hoàn toàn có thể chỉ add ETH vào price-range này để khi giá vượt qua, Alice có thể thu được về nhiều USDT hơn, còn thừa 7000 USDT Alice cũng có thể add vào price-range $2000-$2500 chẳng hạn để chờ ETH rớt giá
Khái niệm Tick:
- Tick của V3 nằm trong đoạn MAX_TICK và MIN_TICK [-887272, 887272]. Mỗi số là 1 tick, tick đại diện cho price (giá của token0 trên token1) theo công thức . Ví dụ price tại tick 1000 là
Khái niệm Tick-spacing:
-
Thông số này sẽ dễ hiểu hơn nếu nó đi kèm với price-range, tuy nhiên nó có 1 ý nghĩa nữa là tick-spacing càng lớn (càng dễ trượt giá) thì fee swap càng cao. Tick-spacing được chọn 1 lần từ lúc tạo Pool, và không thể thay đổi về sau. UniswapV3 đưa ra 3 cặp fee - tick-spacing ban đầu như sau:
- fee = 0.05% tương ứng tick-spacing = 10
- fee = 0.3% tương ứng tick-spacing = 60
- fee = 1% tương ứng tick-spacing = 200
Khái niệm Price-range:
- Price-range được được dánh dấu bằng 2 tick (lower và upper), khoảng cách giữa lower và upper (độ rộng của price-range) phải là bội số của tick-spacing, và kết hợp với điều MIN_TICK, MAX_TICK là 2 số đối nhau nên tương đương index của lower và upper cũng phải là bội số của tick-spacing. Điều này có thể thấy trong dòng 28 của libraries/TickBitmap.sol
Ví dụ, Pool được khởi tạo với tick-spacing là 200, thì độ rộng nhỏ nhất mà một price-range có thể có là 200. Price-range có giá thấp nhất và hẹp nhất mà liquidity provider có thể thêm thanh khoản là [-887200, -887000]. Price-range có giá cao nhất và hẹp nhất mà liquidity provider có thể thêm thanh khoản là [887000, 887200]
1.2 Quản lý thanh khoản qua mỗi price-range
Ví dụ, các tick A, B, C, D, E theo chiều tăng dần từ A đến E, cách nhau một khoảng là bội số của tick-spacing như sau, các điểm không cần thiết phải cách đều nhau thanh khoản chưa được add vào bất cứ đâu
Ở đây, mình chưa cần xét đến Liquidity được tính như thế nào, cứ gọi là L.
Alice thêm 50 Liquidity vào price-range A-B, chúng ta sẽ cập nhật liquidityNet của A và B, liquidityNet được lưu trong struct Info ở file libraries/Tick.sol:
liquidityNet được cập nhật lại như sau:
Do A là lower nên giữ nguyên con số 50. B là upper nên phải đảo dấu thành -50. Code nằm ở dòng 147 trong hàm update() ở file libraries/Tick.sol.
Tiếp theo, Bob thêm 100 Liquidity vào price-range B-D, liquidityNet của B và D được cập nhật lại như sau:
Tiếp theo, Carol thêm 150 Liquidity vào C-E như sau, liquidityNet của C và E được cập nhật như sau:
Như vậy, sau khi cả 3 thêm thanh khoản vào price-range mà họ muốn, các price-range mà họ chọn có thể chồng lấn lên nhau thì có thể biểu diễn Liquidity từ A đến E như sau:
Dù chỉ thêm thanh khoản trực tiếp vào 3 price-range là A-B, B-D, C-E, như do sự chồng lấn price-range mà Bob mà Carol đã chọn nên đã tự sinh thêm một price-range là B-C. Liquidity của các price-range lúc này sẽ là:
- L(A,B) = 50 L
- L(B,C) = 100 L
- L(C,D) = 100 L + 150 L = 250 L
- L(D,E) = 150 L
Đấy là mình nhìn hình minh hoạ mình sẽ tự tính được Liquidity mà một price-range đang nắm giữ. Nhưng nếu contract lưu theo kiểu mapping:
mapping[lower][upper] public liquidity; liquidity[A][B] = 50;
liquidity[B][C] = 100;
liquidity[C][D] = 250;
liquidity[D][E] = 150;
thì khi mà có một người nào đó thêm liquidity vào price-range B-E thì contract sẽ phải cập nhật lại liquidity[B][C], liquidity[C][D], liquidity[D][E]. Đấy chỉ là trong trường hợp chỉ có 3 price-range con trong price-range B,E, trường hợp có rất nhiều price-range con, contract không thể chạy vòng for để cập nhật cho tất cả. Đây là lúc liquidityNet phát huy tác dụng:
Ví dụ, lúc đầu giá hiện tại đang nằm trong price-range A-B, L(A,B) = 50 chính là giá trị của biến liquidity lưu ở dòng 90 trong file UniswapV3Pool.sol
Daniel swap token1 lấy token0, làm cho giá tăng lên, sau khi đã hấp thụ hết token0 ở price-range A-B, giá tiếp tục cross qua B đi vào price-range B-C, contract lúc này cần biết L(B,C) là bao nhiêu để phục vụ tính toán, lúc này contract chỉ cần lấy liquidity hiện tại cộng với liquidityNet của B:
- L(B,C) = L(A,B) + 50 = 100, bằng với con số mà mình đã tính ra từ hình ở trên Code đoạn này nằm ở dòng 709 đến 722 trong file UniswapV3Pool.sol
Nếu gía tiếp tục cross qua C đi vào price-range C-D, contract sẽ tính L(C,D) là
- L(C,D) = 100 + 150 = 250, bằng với con số mà mình đã tính ra từ hình ở trên
Nếu gía tiếp tục cross qua D đi vào price-range D-E, contract sẽ tính L(D,E) là
- L(D,E) = 250 - 100 = 150, bằng với con số mà mình đã tính ra từ hình ở trên
Đến đây, chắc chúng ta sẽ trả lời được câu hỏi tại sao liquidityNet của một tick lại là int chứ không phải uint cũng như phần nào mường tượng ra được cách hoạt động của UniswapV3. Thật sự cách UniswapV3 quản lý liquidity của các price-range quá thông minh, đọc code mà học được rất nhiều....
Mai rảnh mình viết tiếp, chắc là mai viết tiếp đoạn tính toán amountToken0 và amountToken1 lúc thêm thanh khoản vào và rút thanh khoản ra khỏi pool.