
Phân tích quy trình làm sạch dữ liệu Uniswap V3
Tuyển chọn TechFlowTuyển chọn TechFlow

Phân tích quy trình làm sạch dữ liệu Uniswap V3
Chúng tôi đã tính toán giá trị ròng và tỷ suất sinh lời của người dùng trên Uniswap từ góc độ địa chỉ người dùng.
Tác giả: Zelos
Giới thiệu
Kỳ trước, chúng tôi đã thống kê giá trị ròng và tỷ suất sinh lời của người dùng trên Uniswap dựa theo địa chỉ. Lần này, mục tiêu vẫn như vậy, nhưng sẽ bổ sung thêm tiền mặt mà các địa chỉ này nắm giữ để tính toán tổng giá trị ròng và tỷ suất sinh lời.
Mẫu thống kê lần này gồm hai pool:
-
Pool USDC-WETH (phí: 0.05%) trên Polygon, địa chỉ pool: 0x45dda9cb7c25131df268515131f647d726f50608[1], cũng là pool được sử dụng trong phân tích kỳ trước
-
Pool USDC-ETH (phí: 0.05%) trên Ethereum, địa chỉ pool: 0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640[2], do pool này chứa token gốc nên gây một số khó khăn trong xử lý dữ liệu
Dữ liệu cuối cùng thu được là dữ liệu theo giờ, lưu ý: mỗi dòng dữ liệu đại diện cho giá trị tại thời điểm cuối cùng của giờ đó.
Quy trình tổng thể
-
Lấy dữ liệu từ Uniswap
-
Lấy dữ liệu tiền mặt của người dùng
-
Tính chuỗi giá, tức là giá ETH.
-
Lấy phí giao dịch thu được mỗi phút tại từng tick
-
Lấy danh sách tất cả các position trong chu kỳ thống kê
-
Xác định mối quan hệ giữa địa chỉ và position
-
Tính tỷ suất sinh lời cho từng position
-
Dựa vào mối quan hệ giữa position và địa chỉ, tính tỷ suất sinh lời khi làm LP cho từng địa chỉ người dùng
-
Gộp tiền mặt và LP của người dùng, tính tỷ suất sinh lời tổng thể

1. Lấy dữ liệu Uniswap
Trước đây, để cung cấp nguồn dữ liệu cho Demeter, chúng tôi đã phát triển công cụ demeter-fetch. Công cụ này có thể lấy log từ pool Uniswap qua nhiều kênh khác nhau và phân tích thành các định dạng khác nhau. Các nguồn dữ liệu hỗ trợ bao gồm:
-
RPC Ethereum: Giao diện RPC chuẩn từ client eth. Hiệu quả lấy dữ liệu thấp, cần mở nhiều luồng.
-
Google BigQuery: Tải dữ liệu từ bộ dữ liệu BigQuery. Dù cập nhật mỗi ngày một lần, nhưng ưu điểm là dễ dùng và giá rẻ.
-
Trueblocks chifra: Dịch vụ Chifra có thể crawl giao dịch trên blockchain và tổ chức lại, giúp dễ dàng xuất thông tin giao dịch, số dư... Tuy nhiên điều này yêu cầu tự xây dựng nút và dịch vụ.
Các định dạng đầu ra bao gồm:
-
minute: Lấy mẫu lại dữ liệu giao dịch swap Uniswap thành dữ liệu mỗi phút. Dùng để backtest
-
tick: Ghi lại mọi giao dịch trong Pool, bao gồm swap và thao tác thanh khoản
Lần này, chúng tôi chủ yếu lấy dữ liệu tick để thống kê thông tin position, bao gồm khối lượng vốn / lợi nhuận mỗi phút / vòng đời / chủ sở hữu...
Dữ liệu này được lấy từ event log của pool, ví dụ mint, burn, collect, swap. Tuy nhiên log của pool không chứa token id, khiến ta không thể xác định thao tác nào liên quan đến position nào.
Thực tế, quyền lợi LP Uniswap được quản lý qua NFT, và người quản lý NFT token này là hợp đồng proxy. Token id chỉ tồn tại trong event log của proxy. Do đó, để có đầy đủ thông tin LP position, cần lấy event log từ proxy rồi kết hợp với event log của pool.
Ví dụ với giao dịch [3], ta cần chú ý hai log có chỉ số 227 và 229. Chúng lần lượt là mint từ hợp đồng pool và IncreaseLiquidity từ hợp đồng proxy. Giá trị amount (tức liquidity), amount0 và amount1 của hai log này giống nhau, có thể dùng làm cơ sở liên kết. Bằng cách ghép nối hai log này, ta có thể biết khoảng tick, liquidity, token id và giá trị tương ứng của hai token cho hành động LP này.


Đối với người dùng cao cấp, đặc biệt là một số quỹ, họ chọn bỏ qua proxy và thao tác trực tiếp với hợp đồng pool. Trong trường hợp này, position sẽ không có token id. Khi đó, chúng tôi sẽ tạo ID cho LP position theo định dạng address-LowerTick-UpperTick.
Với burn và collect, cũng có thể dùng cách này để tìm ID position tương ứng cho event của pool. Tuy nhiên có một vấn đề nhỏ: đôi khi giá trị hai event không hoàn toàn khớp, có sai lệch nhỏ. Ví dụ như giao dịch này

Giá trị amount0 và amount1 có chút chênh lệch nhỏ. Dù hiếm gặp nhưng hiện tượng này khá phổ biến. Vì vậy, khi ghép nối burn và collect, chúng tôi để một biên dung sai nhất định.
Vấn đề tiếp theo cần xử lý là ai khởi tạo giao dịch này. Với việc rút thanh khoản, chúng tôi dùng receipt trong sự kiện collect làm chủ sở hữu position. Còn với mint, chỉ có thể lấy sender từ event mint của pool (xem hình minh họa event mint).
Nếu người dùng thao tác trực tiếp với pool, sender chính là nhà cung cấp LP. Nhưng nếu người dùng thường, thao tác qua proxy, sender sẽ là địa chỉ proxy vì tiền thực sự được chuyển từ proxy sang pool. May mắn là proxy sẽ phát sinh NFT token, và NFT này chắc chắn được chuyển cho nhà cung cấp LP. Do đó, bằng cách kiểm tra sự kiện transfer từ hợp đồng proxy (tức hợp đồng NFT), ta có thể xác định được nhà cung cấp LP tương ứng với mint.
Ngoài ra, nếu NFT được chuyển nhượng, chủ sở hữu position sẽ thay đổi. Chúng tôi đã thống kê và thấy trường hợp này rất ít. Để đơn giản hóa, chúng tôi không xét việc chuyển NFT sau khi mint.
2. Lấy tiền mặt nắm giữ bởi địa chỉ
Mục tiêu giai đoạn này là lấy số lượng token mà một địa chỉ nắm giữ tại từng thời điểm trong chu kỳ thống kê. Để đạt được điều này, cần lấy hai loại dữ liệu:
-
Số dư ban đầu của địa chỉ
-
Lịch sử chuyển khoản của địa chỉ trong chu kỳ thống kê.
Sử dụng lịch sử chuyển khoản để cộng trừ vào số dư, ta có thể suy luận ra số dư tại từng thời điểm.
Số dư ban đầu có thể truy vấn qua giao diện RPC. Khi dùng node archive, có thể đặt tham số chiều cao khối để lấy số dư tại bất kỳ thời điểm nào. Cả token gốc và ERC20 đều có thể lấy theo cách này.
Lấy lịch sử chuyển khoản ERC20 khá dễ dàng, có thể dùng bất kỳ kênh nào (Big Query, RPC, chifra).
Còn lịch sử chuyển khoản ETH cần lấy qua giao dịch và trace. Giao dịch thì ổn, nhưng truy vấn và xử lý trace tốn rất nhiều tài nguyên. May mắn là chifra cung cấp chức năng xuất lịch sử số dư ETH. Nó ghi lại một bản ghi khi số dư thay đổi, dù chỉ ghi thay đổi về số lượng chứ không ghi đối tượng chuyển, nhưng cũng đủ đáp ứng yêu cầu. Đây là phương pháp hiệu quả về chi phí.
3. Lấy giá
Uniswap là một sàn giao dịch, khi có giao dịch đổi token xảy ra sẽ tạo ra một sự kiện swap, ta có thể lấy giá token từ trường sqrtPriceX96. Từ trường liquidity có thể lấy tổng thanh khoản tại thời điểm đó.
Do các pool của chúng tôi đều có stablecoin, việc lấy giá so với USD rất dễ dàng. Tuy nhiên giá này không hoàn toàn chính xác. Thứ nhất, nó bị ảnh hưởng bởi tần suất giao dịch; nếu không có giao dịch swap, giá sẽ bị trễ. Thứ hai, khi stablecoin rời mốc, giá này sẽ lệch so với giá thực tế. Nhưng nhìn chung, giá này đủ chính xác cho các nghiên cứu thị trường.
Cuối cùng, lấy mẫu lại giá token để có danh sách giá theo phút.
Ngoài ra, do trường liquidity trong event cũng chứa tổng thanh khoản hiện tại của pool, chúng tôi cũng đưa luôn vào. Cuối cùng tạo thành bảng như sau:

4. Thống kê phí giao dịch
Phí giao dịch là nguồn thu chính của position. Mỗi khi có người dùng thực hiện swap trên pool, các position có tick nằm trong khoảng lower và upper sẽ nhận được một phần phí (số tiền phụ thuộc vào tỷ lệ thanh khoản, mức phí của pool và khoảng tick).
Để thống kê thu nhập phí của người dùng, chúng tôi ghi lại mỗi phút tại tick nào đã xảy ra bao nhiêu giá trị swap. Sau đó tính thu nhập phí cho phút đó:

Cuối cùng tạo thành bảng như sau

Phương pháp này không xét trường hợp khi swap, thanh khoản tại tick hiện tại bị cạn kiệt. Tuy nhiên do mục tiêu thống kê là LP, dùng khoảng tick để tính, sai số này được giảm bớt phần nào.
5. Lấy danh sách position
Để lấy danh sách position, trước tiên cần xác định định danh position.
-
Đối với LP đầu tư qua Proxy, mỗi position sẽ có một NFT, tức có một token id, dùng làm ID position.
-
Đối với LP đầu tư trực tiếp vào pool, chúng tôi sẽ tạo ID theo định dạng
address_LowerTick_UpperTick. Như vậy, mọi position đều có định danh riêng.
Thông qua định danh này, ta có thể tổng hợp mọi thao tác của LP thành danh sách mô tả toàn bộ vòng đời position. Ví dụ:

Tuy nhiên cần lưu ý, đối tượng thống kê lần này là năm 2023, không phải từ khi pool được tạo. Do đó, với một số position, chúng tôi không thể lấy thao tác trước ngày 1/1/2023. Ta cần suy luận lượng liquidity tại thời điểm bắt đầu thống kê. Chúng tôi dùng cách kinh tế sau:
-
Cộng tổng liquidity từ mint và burn, được số L
-
Nếu L > 0, tức mint > burn, coi như đã có sẵn thanh khoản trước thống kê, lúc đó sẽ bổ sung một thao tác mint tại thời điểm bắt đầu (0:0:0 ngày 1/1/2023).
-
Nếu L < 0, coi như đến cuối thống kê vẫn còn nắm giữ liquidity.
Cách này tránh tải dữ liệu trước 2023, tiết kiệm chi phí. Nhưng gặp phải vấn đề thanh khoản chìm: Nếu LP không thực hiện thao tác nào trong năm, ta không thể tìm thấy LP đó. Tuy nhiên vấn đề này không nghiêm trọng. Vì chu kỳ thống kê là một năm, ta giả định người dùng thường sẽ điều chỉnh LP trong thời gian này. Do giá ETH thay đổi lớn trong một năm, và người dùng có nhiều lý do để điều chỉnh LP như giá vượt khỏi khoảng tick, chuyển vốn sang DeFi khác... Do đó, một người dùng hoạt động sẽ luôn điều chỉnh LP theo giá. Còn những người để vốn nằm yên trong pool, không điều chỉnh, ta coi là người dùng không hoạt động, nằm ngoài phạm vi thống kê.
Một tình huống phiền phức hơn là position đã mint liquidity trước 2023, rồi thực hiện thêm mint/burn trong chu kỳ, đến cuối thống kê vẫn chưa burn hết thanh khoản. Khi đó ta chỉ thống kê được một phần thanh khoản. Tình huống này khiến thanh khoản chìm ảnh hưởng đến ước tính phí, dẫn đến tỷ suất sinh lời bất thường. Nguyên nhân cụ thể sẽ bàn sau.
Trong thống kê cuối cùng, Polygon có 73.278 position, Ethereum có 21.210 position, mỗi chuỗi có không quá 10 position có tỷ suất bất thường, chứng tỏ giả định này đáng tin cậy.
6. Xác định mối quan hệ giữa địa chỉ và position
Do mục tiêu cuối cùng là lợi nhuận của địa chỉ, ta cần xác định mối quan hệ giữa địa chỉ và position. Qua liên kết này, có thể biết hành vi đầu tư cụ thể của người dùng.
Ở bước 1, chúng tôi đã làm việc để tìm người dùng liên quan đến thao tác vốn (mint/collect). Do đó, chỉ cần tìm sender của mint và receipt của collect, ta có thể xác định mối quan hệ giữa position và địa chỉ.
7. Tính giá trị ròng và tỷ suất sinh lời của position
Ở bước này, ta tính giá trị ròng của từng position, sau đó tính tỷ suất sinh lời từ giá trị ròng.
Giá trị ròng
Giá trị ròng của position gồm hai phần: một là liquidity của LP, tương đương vốn làm thị trường. Sau khi người dùng nạp vốn vào position, lượng liquidity không thay đổi, nhưng giá trị ròng dao động theo giá. Phần thứ hai là thu nhập phí, độc lập với liquidity, lưu riêng trong hai trường fee0 và fee1. Giá trị phí tăng dần theo thời gian.
Do đó, tại bất kỳ phút nào, kết hợp liquidity với giá phút đó sẽ có giá trị ròng phần vốn. Việc tính phí cần dùng bảng phí đã tính ở bước 4.
Trước tiên lấy liquidity của position chia cho tổng liquidity của pool hiện tại làm tỷ lệ chia. Sau đó cộng phí tại mọi tick nằm trong khoảng tick của position để có thu nhập phí phút đó.
Biểu diễn bằng công thức:

Cuối cùng cộng fee0 và fee1 để có giá trị phí. Cộng với giá trị ròng của thanh khoản để có tổng giá trị ròng.
Khi tính giá trị ròng, ta chia vòng đời position theo các giao dịch mint/burn/collect.
-
Khi có giao dịch mint, tăng thanh khoản
-
Khi có giao dịch burn, giảm thanh khoản. Đồng thời quy đổi giá trị thanh khoản vào trường phí (hợp đồng pool cũng xử lý như vậy)
-
Khi có giao dịch collect, kích hoạt tính toán, phạm vi tính toán từ lần collect trước đến thời điểm hiện tại. Ta tính giá trị ròng và thu nhập phí mỗi phút, tạo thành danh sách.
Cuối cùng, tổng hợp danh sách giá trị ròng từ mọi lần collect. Sau đó lấy mẫu lại và thống kê khác để có kết quả cuối cùng.

Ngoài ra, để nâng cao độ chính xác, chúng tôi thực hiện hai tối ưu.
Thứ nhất, với giờ có giao dịch (mint/burn/collect), thống kê theo phút; với giờ không có giao dịch, thống kê theo giờ. Cuối cùng lấy mẫu lại thành dữ liệu theo giờ.
Thứ hai, trong sự kiện collect, ta có thể lấy tổng giá trị thanh khoản + phí. Do đó, có thể so sánh giá trị collect thực tế với giá trị lý thuyết, lấy chênh lệch phí (thực tế chênh lệch còn bao gồm cả vốn LP, nhưng sai số vốn rất nhỏ, gần như bằng 0). Ta sẽ bổ sung chênh lệch phí vào từng dòng để nâng cao độ chính xác ước tính phí (tức các trường fee_modify0 và fee_modify1 trong bảng trên).
Lưu ý:
-
Khi bổ sung, cần căn cứ vào thanh khoản tại giờ đó để phân bổ phí theo trọng số, nếu không sẽ dẫn đến phí giờ đó bị cao bất thường.
-
Do dữ liệu thống kê là cả năm 2023, không phải dữ liệu đầy đủ, nên tồn tại hiện tượng thanh khoản chìm như đề cập ở mục 5. Điều này khiến phí thực tế cao hơn nhiều so với lý thuyết, dẫn đến tỷ suất sinh lời bất thường.
Do mỗi dòng là dữ liệu tại thời điểm cuối cùng của giờ đó, với position đã đóng hoàn toàn, giá trị ròng sẽ là 0. Trong trường hợp này, giá trị ròng tại thời điểm đóng sẽ bị mất. Để lưu giữ giá trị này, ở cuối file, tạo thêm một dòng thời gian 00:00:00 ngày 1/1/2038, lưu trữ dữ liệu giá trị ròng tại thời điểm đóng position, phục vụ nhu cầu thống kê của các dự án khác.
Tỷ suất sinh lời
Thông thường, tính tỷ suất sinh lời bằng giá trị ròng cuối chia cho giá trị ròng đầu. Nhưng ở đây không áp dụng được. Lý do như sau:
-
Tỷ suất sinh lời ở đây cần chi tiết đến từng phút,
-
Do position có thể có dòng tiền vào/ra giữa chừng, việc chia đơn thuần giá trị đầu/cuối không phản ánh đúng lợi nhuận.
Với vấn đề 1, ta có thể chia giá trị ròng từng phút để có tỷ suất mỗi phút, sau đó nhân dồn các tỷ suất phút để có tỷ suất tổng.

Nhưng thuật toán này có vấn đề nghiêm trọng: nếu một dữ liệu tỷ suất phút bị sai, sẽ dẫn đến sai lệch lớn tỷ suất tổng. Khi đó, quá trình thống kê như đi trên dây, không được phép sai sót. Tuy nhiên mặt tốt là mọi lỗi thống kê sẽ lộ rõ.
Với vấn đề 2, nếu có dòng tiền vào/ra trong phút đó, việc chia trực tiếp vẫn cho ra tỷ suất vô lý. Do đó cần tinh chỉnh thuật toán tỷ suất từng phút.
Thử nghiệm đầu tiên của chúng tôi là chia nhỏ chi tiết sự thay đổi giá trị ròng, sau đó loại bỏ phần thay đổi vốn. Chúng tôi chia sự thay đổi giá trị ròng thành ba phần: 1 là thay đổi vốn do giá, 2 là phí tích lũy trong phút, 3 là dòng tiền vào/ra. Rõ ràng phần 3 cần loại khỏi thống kê. Với mục đích này, chúng tôi đưa ra phương pháp tính sau:
-
Chỉ định phút hiện tại là n, phút trước là n-1
-
Giả định mọi giao dịch chuyển khoản trong phút hiện tại đều xảy ra tại giây n:0.000. Như vậy trong thời gian còn lại, giá trị ròng LP không đổi, tức giá trị tại giây n:0.001 bằng giá trị tại giây n:59.999.
-
Việc tích lũy phí xảy ra vào cuối phút, tức giây n:59.999.
-
Giá và phí tại cuối phút trước (n-1:59.999) chính là giá và phí tại đầu phút hiện tại (n:0.000).
Dựa trên các giả định này, tỷ suất mỗi phút bằng giá trị cuối cùng của thanh khoản/giá/phí chia cho giá trị cuối cùng của thanh khoản/giá đầu/phí đầu, biểu diễn bằng công thức như sau, trong đó f là hàm quy đổi liquidity thành giá trị ròng.

Cách này trông rất tốt. Nó hoàn hảo loại bỏ ảnh hưởng của thay đổi thanh khoản, đồng thời thể hiện đúng ảnh hưởng của giá và phí đến giá trị ròng. Đúng như mong đợi. Tuy nhiên, trong thực tế, tại một số dòng sẽ xuất hiện tỷ suất rất lớn. Sau khi điều tra, chúng tôi phát hiện vấn đề xảy ra khi rút thanh khoản. Nhớ lại quy tắc của chúng tôi: mỗi dòng đại diện cho thời điểm cuối phút/giờ. Điều này tạo ra thước đo thống nhất, nhưng cần lưu ý rằng ý nghĩa từng cột là khác nhau:
-
Với cột giá trị ròng là giá trị tức thời, tức giá trị cuối cùng của phút/giờ hiện tại.
-
Còn cột phí là giá trị tích lũy, tức tổng phí tích lũy trong suốt phút/giờ hiện tại.
Do đó, với giờ thực hiện burn thanh khoản
-
Khi LP bị burn và token được chuyển đi, giá trị ròng cuối giờ sẽ là 0
-
Còn với phí, do là tích lũy, tại cuối giờ phí sẽ lớn hơn 0.
Điều này khiến công thức trên suy biến thành:
Hiện tượng này không chỉ xảy ra ở cuối vòng đời position, mà khi burn một phần thanh khoản cũng sẽ làm thay đổi tỷ lệ giữa tăng trưởng phí và giá trị ròng LP.
Để đơn giản, khi có thay đổi giá trị ròng LP, chúng tôi đặt tỷ suất sinh lời là 1. Điều này gây sai số trong tính toán tỷ suất, nhưng với một position đầu tư liên tục bình thường, số giờ có giao dịch so với toàn bộ vòng đời là rất ít, nên ảnh hưởng không lớn.
8. Tính tổng lợi nhuận LP của địa chỉ
Có tỷ suất sinh lời từng position, cộng với mối quan hệ position - địa chỉ, ta có thể biết tỷ suất sinh lời của địa chỉ người dùng tại từng position.
Thuật toán ở đây khá đơn giản: nối các position của địa chỉ trong các thời kỳ khác nhau, với thời kỳ không đầu tư, đặt giá trị ròng bằng 0, tỷ suất sinh lời bằng 1 (do giá trị đầu và cuối đều bằng 0, không thay đổi, nên tỷ suất là 1).
Nếu cùng thời kỳ có nhiều position, tại phần trùng lặp, cộng giá trị ròng lại để có tổng giá trị ròng. Khi gộp tỷ suất sinh lời, chúng tôi gộp theo trọng số giá trị ròng của từng position.
9. Gộp tổng lợi nhuận tiền mặt và LP
Cuối cùng, chỉ cần gộp tiền mặt nắm giữ và đầu tư LP của địa chỉ người dùng là có kết quả cuối cùng.
Việc gộp giá trị ròng đơn giản hơn bước trước (gộp position). Chỉ cần tra khoảng thời gian bên LP, sau đó tìm tiền mặt nắm giữ trong khoảng thời gian tương ứng, rồi tra giá ETH, sẽ có tổng giá trị ròng.
Với tỷ suất sinh lời, chúng tôi cũng dùng phương pháp tính tỷ suất từng phút rồi nhân dồn. Ban đầu, chúng tôi dùng thuật toán tỷ suất sai đã đề cập ở mục 7. Điều này yêu cầu tách phần cố định (bao gồm số lượng cash trong tiền mặt, thanh khoản trong LP) và phần biến đổi (biến động giá, tích lũy phí, dòng tiền vào/ra). So với thống kê position, độ phức tạp cao hơn nhiều, vì với dòng tiền vào/ra Uniswap, chỉ cần theo dõi sự kiện mint và collect. Nhưng với tiền mặt thì rất rắc rối, ta phải phân biệt tiền chuyển vào LP hay chuyển ra ngoài. Nếu chuyển vào LP, phần vốn có thể giữ nguyên; nếu chuyển ra ngoài, phải điều chỉnh số lượng vốn. Điều này đòi hỏi truy vết địa chỉ nhận của giao dịch ERC20 và ETH. Công việc này rất phức tạp. Trước hết, khi mint/collect, địa chỉ nhận có thể là pool hoặc proxy. Phức tạp hơn nữa là chuyển ETH, do ETH là token gốc, một số ghi chép chuyển khoản chỉ có thể tra qua trace. Nhưng dữ liệu trace quá lớn, vượt quá khả năng xử lý của chúng tôi.
Cuối cùng, yếu tố đè bẹp駱驼的最后一根稻草 (câu thành ngữ, tạm dịch: giọt nước tràn ly) là chúng tôi phát hiện giá trị ròng mỗi dòng là giá trị tức thời của giờ đó, còn phí là giá trị tích lũy của giờ đó, về mặt vật lý không thể cộng trực tiếp. Vấn đề này thật sự phát hiện rất muộn.
Do đó, chúng tôi từ bỏ thuật toán này, chuyển sang dùng giá trị ròng phút sau chia cho giá trị ròng phút trước. Cách này đơn giản hơn nhiều. Nhưng cũng có vấn đề: khi có dòng tiền vào/ra, tỷ suất sinh lời vẫn xuất hiện tình trạng vô lý. Như đã thảo luận, việc tách dòng tiền rất khó. Do đó, ở đây chúng tôi hy sinh một chút độ chính xác, đặt tỷ suất sinh lời bằng 1 khi có chuyển tiền.
Vấn đề còn lại là: làm sao nhận diện được giờ nào có dòng tiền vào/ra? Thuật toán ban đầu nghĩ rất đơn giản: dùng số dư token giờ trước và giá hiện tại, suy luận nếu nắm giữ token đó thì giá trị ròng giờ này là bao nhiêu. Sau đó lấy giá trị thực tế trừ đi giá trị suy luận. Khi chênh lệch khác 0, tức là có dòng tiền vào/ra. Biểu diễn bằng công thức:

Tuy nhiên thuật toán này bỏ qua độ phức tạp của LP Uniswap. Số lượng token trong LP thay đổi theo giá, đồng thời giá trị ròng cũng thay đổi. Hơn nữa, cách này không xét đến sự thay đổi phí. Cuối cùng gây ra sai số khoảng 0.1% giữa giá trị suy luận và thực tế.
Để nâng cao độ chính xác, chúng tôi chi tiết cấu thành vốn, tính riêng sự thay đổi giá trị LP, đồng thời tính cả phí vào.

Với cách này, sai số giá trị suy luận có thể kiểm soát dưới 0.001%.
Ngoài ra, chúng tôi giới hạn chữ số thập phân, tránh chia các số quá nhỏ (thường dưới 10^-10). Những con số nhỏ này là lỗi tích lũy từ các phép tính và lấy mẫu lại. Nếu không xử lý mà chia trực tiếp, lỗi sẽ bị khuếch đại, khiến tỷ suất sinh lời méo mó nghiêm trọng.
Các vấn đề khác
Token gốc
Lần thống kê này bổ sung pool USDC-ETH trên Ethereum, trong đó ETH là token gốc, cần xử lý đặc biệt.
ETH không thể dùng trực tiếp trong DeFi, phải chuyển thành WETH. Do đó pool này thực chất là pool USDC-WETH. Với người dùng thao tác trực tiếp pool, việc nạp/rút WETH như pool thông thường.
Còn với người dùng thêm LP qua proxy, cần mang ETH trong trường value của giao dịch, gửi cho hợp đồng proxy. Hợp đồng sẽ đổi ETH thành WETH, sau đó nạp vào pool. Khi collect, USDC có thể chuyển trực tiếp cho người dùng, nhưng ETH không thể chuyển trực tiếp, cần rút từ pool về proxy, sau đó proxy đổi WETH thành ETH và gửi qua chuyển khoản nội bộ cho người dùng. Ví dụ xem giao dịch [4].
Do đó, pool USDC-ETH chỉ khác pool thông thường ở khâu nạp/rút vốn. Điều này chỉ ảnh hưởng đến việc ghép position với địa chỉ. Để giải quyết, chúng tôi lấy toàn bộ dữ liệu chuyển nhượng NFT từ khi pool được tạo, sau đó dùng token id để tìm chủ sở hữu position tương ứng.
Position bị thiếu
Trong thống kê, một số position không xuất hiện trong danh sách cuối. Những position này đều có điểm đặc biệt.
Phần lớn là giao dịch MEV, thuần túy arbitrage, không phải nhà đầu tư bình thường, do đó nằm ngoài phạm vi thống kê. Ngoài ra, thực tế thống kê cũng rất khó, cần dữ liệu cấp độ trace. Ở đây chúng tôi dùng chiến lược lọc đơn giản: từ bắt đầu đến kết thúc dưới một phút. Thực tế, do độ chính xác dữ liệu cao nhất là 1 phút, nếu position tồn tại dưới một phút thì không thể thống kê được.
Khả năng khác là position không có giao dịch collect. Như thấy ở bước 7, việc tính lợi nhuận của chúng tôi được kích hoạt qua collect. Không có thao tác collect, sẽ không tính giá trị ròng và tỷ suất sinh lời trước đó. Trong điều kiện bình thường, người dùng đều chọn thu lợi nhuận hoặc vốn LP kịp thời. Nhưng cũng có thể có người dùng đặc biệt muốn giữ tài sản trong các trường fee0 và fee1 của pool Uniswap. Với những người dùng này, chúng tôi cũng coi là đặc biệt, nằm ngoài phạm vi thống kê.
Chào mừng tham gia cộng đồng chính thức TechFlow
Nhóm Telegram:https://t.me/TechFlowDaily
Tài khoản Twitter chính thức:https://x.com/TechFlowPost
Tài khoản Twitter tiếng Anh:https://x.com/BlockFlow_News










