PancakeSwap Infinity là phiên bản AMM thế hệ mới của PancakeSwap, được thiết kế theo kiến trúc mô-đun hóa, giúp giao dịch rẻ hơn, linh hoạt hơn và cho phép các nhà phát triển mở rộng chức năng của DEX mà không cần thay đổi giao thức cốt lõi. PancakeSwap Infinity có kiến trúc kiểu “Uniswap V4 + Hooks” nhưng mở rộng thêm nhiều loại Pool và tối ưu gas hơn. Do cũng khá giống Uniswap V4 nên tôi tạm gọi là PancakeSwap V4 cho ngắn gọn.
Vì có sự khác biệt khá nhiều đặc biệt là Interface so với Uniswap V4 nên tôi phải thực hiện tìm hiểu riêng để hỗ trợ. Nếu bạn tò mò về Uniswap V4 có thể xem thêm bài viết: Hướng dẫn viết smart contract thực hiện tính toán và hoán đổi trực tiếp trên pool của Uniswap V4
Mục lục
Tìm hiểu về PancakeSwap Infinity
Chi tiết về PancakeSwap Infinity, hãy chịu khó đọc tài liệu của PancakeSwap. Tôi không đi sâu vào chi tiết lý thuyết mà chỉ quan tâm đến sự khác biệt với Uniswap V4 ở điểm nào và nó tác động tới code như thế nào mà thôi.
PancakeSwap Infinity về cơ bản giống Uniswap V4 ở một số điểm:
- Kiến trúc Singleton
- Kế toán nhanh (Flash Accounting)
- Hỗ trợ Hooks
Có điểm khác biệt quan trọng so với Uniswap V4 là PancakeSwap Infinity hỗ trợ nhiều loại Pool khác nhau:
- CLAMM: Concentrated Liquidity giống V3
- LBAMM: Liquidity Book tương tự Trader Joe
- StableSwap: Tối ưu cho StableCoin
Do sự khác biệt này mà các contract của PancakeSwap Infinity khác so với UniswapV4. PancakeSwap Infinity có các contract sau:
- Vault: Là hợp đồng trung tâm dùng để lưu trữ token thực tế của toàn bộ hệ thống. Uniswap V4 không có hợp đồng này.
- Nó hoạt động như một lớp kế toán, suy trì sổ cái ghi chép các token đã được gửi hoặc còn nợ, tạo điều kiện thuận lợi cho quá trình thanh toán an toàn và hiệu quả sau mỗi giao dịch.
- Một trong những cơ chế quan trọng trong Vault là cơ chế khóa . Người gọi cần lấy được khóa từ kho tiền trước khi thực hiện bất kỳ thao tác nào (hoán đổi, thanh khoản hoặc quyên góp) với người quản lý nhóm.
- Điểm khác với Uniswap V4:
- Uniswap V4 sử dụng PoolManager để chứa tài sản và cả logic
- Pancake Infinity sử dụng Vault để chứa tài sản, còn logic để bên PoolManager.
- PoolManager: Bao gồm các hợp đồng chứa logic của các loại AMM khác nhau. Uniswap V4 chỉ có 1 hợp đồng này và nó chứa cả tài sản và logic, trong khi của Pancake Infinity thì nó chỉ chứa logic.
- Có 2 loại AMM được hỗ trợ tương ứng với 2 PoolManager khác nhau:
- CLPoolManager: Sử dụng cho các pool dạng CLAMM
- BinPoolManager: Sử dụng cho các pool dạng LBAMM
- Điểm khác so với Uniswap V4:
- Với Uniswap V4, PoolManager chứa cả tài sản lẫn logic và có StateView để lấy thông tin các pool
- Với Pancake Infinity, Vault chứa tài sản, PoolManager chứa logic và đồng thời sử dụng để lấy thông tin pool. Trên Pancake Infinity, không có StateView.
- Có 2 loại AMM được hỗ trợ tương ứng với 2 PoolManager khác nhau:
Viết Smart Contract thực hiện swap trên pool CLAMM của PancakeSwap Infinity
Luồng thực hiện swap trên Pool của PancakeSwap Infinity
Để thực hiện swap trong Pancake Infinity, chúng ta không tương tác trực tiếp với Pool mà phải tương tác cả với Vault và với PoolManager. Để swap được chúng ta phải biết:
- Vault: Địa chỉ Vault
- clPoolManager: Địa chỉ của PoolManager cho các pool dạng CLAMM
- PoolKey: Thông tin của Pool gồm (token0, token1, hooks, poolManager, fee, parameters).
parameters là kiểu bytes32 chứa các thông tin thêm như:- tickSpacing cho CL
- binStep cho Bin
- SwapParams: Thông số cho swap bao gồm (zeroForOne, amountSpecified, sqrtPriceLimitX96)
Luồng đầy đủ quá trình swap trên Pool V4 như sau:
- B1: Gọi hàm vault.lock(…) để khởi tạo một phiên giao dịch. Khi hàm lock() được gọi, Vault sẽ gọi tới lockAcquired(…) của contract để chúng ta cài đặt các nghiệp vụ trong đó.
- B2: Trong lockAcquired(…), gọi hàm clPoolManager.swap(…), hàm này sẽ trả về BalanceDelta chứa thông tin swap => Từ dữ liệu BalanceDelta chúng ta sẽ có được amount0 và amount1, kết hợp với zeroForOne ta sẽ xác định được amountIn và amountOut.
- B3: Thực hiện tất toán giao dịch (Thanh toán nợ)
Chúng ta cần thanh toán đủ tokenIn vào PoolManager và rút tokenOut về ví. Quá trình này gồm 2 bước nhỏ (Bạn thực hiện bước nào trước cũng được):- B4.1: Rút tokenOut về ví bằng cách gọi hàm:
poolManager.take(tokenOut, sender, amountOut) - B4.2: Thanh toán đủ tokenIn cho PoolManager
- Nếu tokenIn là token thường: Chúng ta sẽ gọi các hàm sau để hoàn thành công việc
poolManager.sync(tokenIn)
IERC20(tokenIn).transferFrom(sender, poolManager, amountIn)
poolManager.settle() - Nếu tokenIn là ETH: Chúng ta sẽ gọi các hàm sau để hoàn thành công việc, thêm phần hoàn trả ETH nếu còn dư
poolManager.sync(tokenIn)
poolManager.settle{value: amountIn}()
refundETH()
- Nếu tokenIn là token thường: Chúng ta sẽ gọi các hàm sau để hoàn thành công việc
- B4.1: Rút tokenOut về ví bằng cách gọi hàm:
Mã nguồn thực hiện swap trên Pool của PancakeSwap V4
Bên dưới đây là mã nguồn thực hiện swap. Để cho code thực sự ngắn gọn và dễ đọc, tôi đã cố gắng tối giản chỉ lấy các interface thật sự cần thiết và đóng gọi lại trong 1 tệp để bạn có thể dễ dàng triển khai trên nhiều công cụ khác nhau. Nếu bạn muốn xem chi tiết các interface, bạn có thể xem tại PancakeV4-Core.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
type Currency is address;
type BalanceDelta is int256;
type BeforeSwapDelta is int256;
type PoolId is bytes32;
struct SwapParams {
bool zeroForOne;
int256 amountSpecified; // The desired input amount if negative (exactIn), or the desired output amount if positive (exactOut)
uint160 sqrtPriceLimitX96;
}
interface IERC20 {
function transfer(address recipient, uint256 amount) external returns (bool);
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
}
interface IHooks {
}
interface IVault {
function lock(bytes calldata data) external returns (bytes memory);
function take(Currency currency, address to, uint256 amount) external;
function sync(Currency token0) external;
function settle() external payable returns (uint256 paid);
function settleFor(address recipient) external payable returns (uint256 paid);
}
interface ICLPoolManager {
function swap(PoolKey memory key, SwapParams memory params, bytes calldata hookData) external returns (BalanceDelta delta);
}
struct PoolKey {
Currency currency0;
Currency currency1;
IHooks hooks;
ICLPoolManager poolManager;
uint24 fee;
bytes32 parameters;
}
// // Swap Pool on PancakeSwap Infinity
contract PancakeV4Swap {
uint160 internal constant MIN_SQRT_RATIO = 4295128739;
uint160 internal constant MAX_SQRT_RATIO = 1461446703485210103287273052203988822378723970342;
constructor() {
}
struct CallbackData {
address sender;
IVault vault;
PoolKey poolKey;
bool zeroForOne;
int256 amountSpecified;
}
receive() external payable {}
fallback() external {}
function swapExactInput(address vault, PoolKey calldata poolKey, bool zeroForOne, uint128 amountIn) external payable {
swap(vault, poolKey, zeroForOne, amountIn, true);
}
function swapExactOutput(address vault, PoolKey calldata poolKey, bool zeroForOne, uint128 amountOut) external payable {
swap(vault, poolKey, zeroForOne, amountOut, false);
}
function swap(address vault, PoolKey calldata poolKey, bool zeroForOne, uint128 amount, bool exactInput) internal {
IVault iVault = IVault(vault);
CallbackData memory data = CallbackData({
sender: msg.sender,
vault: iVault,
poolKey: poolKey,
zeroForOne: zeroForOne,
amountSpecified: (exactInput?-int256(uint256(amount)):int256(uint256(amount)))
});
iVault.lock(abi.encode(data));
}
function lockAcquired(bytes calldata rawData) external returns (bytes memory) {
// Do swap
CallbackData memory data = abi.decode(rawData, (CallbackData));
require(msg.sender == address(data.vault), "Not vault");
SwapParams memory swapParams = SwapParams({
zeroForOne: data.zeroForOne,
amountSpecified: data.amountSpecified,
sqrtPriceLimitX96: (data.zeroForOne ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1)
});
BalanceDelta delta = ICLPoolManager(data.poolKey.poolManager).swap(data.poolKey, swapParams, new bytes(0));
(int128 amount0, int128 amount1) = parseAmounts(delta);
if (data.zeroForOne) {
// Receive currency1
uint256 takeAmount = uint256(uint128(amount1));
data.vault.take(data.poolKey.currency1, data.sender, takeAmount);
// Pay currency0
uint256 settleAmount = uint256(int256(-amount0));
data.vault.sync(data.poolKey.currency0);
if (Currency.unwrap(data.poolKey.currency0)==address(0)) {
data.vault.settle{value: settleAmount}();
refundETH(data.sender);
} else {
safeTransferFrom(Currency.unwrap(data.poolKey.currency0), data.sender, address(data.vault), settleAmount);
data.vault.settle();
}
} else {
// Receive currency0
uint256 takeAmount = uint256(uint128(amount0));
data.vault.take(data.poolKey.currency0, data.sender, takeAmount);
// Pay currency1
uint256 settleAmount = uint256(uint128(-amount1));
data.vault.sync(data.poolKey.currency1);
if (Currency.unwrap(data.poolKey.currency1)==address(0)) {
data.vault.settle{value: settleAmount}();
refundETH(data.sender);
} else {
safeTransferFrom(Currency.unwrap(data.poolKey.currency1), data.sender, address(data.vault), settleAmount);
data.vault.settle();
}
}
return abi.encode(delta);
}
function refundETH(address recipient) internal {
uint256 balance = address(this).balance;
if (balance > 0) {
safeTransferETH(recipient, balance);
}
}
function parseAmounts(BalanceDelta balanceDelta) internal pure returns (int128 amount0, int128 amount1) {
assembly ("memory-safe") {
amount0 := sar(128, balanceDelta)
amount1 := signextend(15, balanceDelta)
}
}
function safeTransferFrom(address token, address from, address to, uint value) internal {
// bytes4(keccak256(bytes('transferFrom(address,address,uint)')));
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, value));
require(success && (data.length == 0 || abi.decode(data, (bool))), 'Token tranfer failed');
}
function safeTransferETH(address to, uint value) internal {
(bool success, ) = payable(to).call{value: value}(new bytes(0));
require(success, 'ETH transfer failed');
}
}
Về cơ bản khá giống với Pool của Uniswap V4, chỉ thay đổi interface mà thôi.
Triển khai và test thử
Tôi đã triển khai trên BSC Testnet tại địa chỉ: 0x108809C65ac006880ae42bC2dE12F9471a3d26C7
Bây giờ chúng ta sẽ test thử trên pool BNB/CC1222: 0x2169ba2ebc172b8130f7970d0018ff633364a6bd1da451613afa592f3d1c6c04.
Chi tiết địa chỉ các contract xem mục Pancake V4 trên BSC Testnet.
Đầu tiên chúng ta cần phải có thông tin PoolKey. Để lấy được thông tin PoolKey thì từ poolId 0x1927686e9757bb312fc499e480536d466c788dcdc86a1b62c82643157f05b603, ta gọi hàm poolIdtoPoolKey() của contract PoolManager 0x36A12c70c9Cf64f24E89ee132BF93Df2DCD199d4 để được thông tin:
{
currency0 (address) : 0x0000000000000000000000000000000000000000
currency1 (address) : 0x0A3ae7423FaBd83765ecCa42200E887535F76b79
hooks (address) : 0x0000000000000000000000000000000000000000
poolManager (address) : 0x36A12c70c9Cf64f24E89ee132BF93Df2DCD199d4
fee (uint24) : 2500
parameters (bytes32) : 0x00000000000000000000000000000000000000000000000000000000003c0000
}

Bây giờ chúng ta gọi hàm swapExactInput(…) với các tham số như hình dưới:

Giao dịch thực hiện thành công: 0x66d42368520afa503da1cefaa8b55c5e50203769ee3160bf91cfbf56755f63c6
Chúng ta đã hoán đổi 0.01 BNB để lấy 176,589.951660602802251475 CC1222.

Tiếp theo chúng ta gọi hàm swapExactOutput(…) với các tham số như hình dưới:

Giao dịch thực hiện thành công: 0xc0c00dfa304a6c0d9e33d86da6b2eb99ac8ac1aa223a16af4dbdded006e53993
Theo như giao dịch chúng ta đã hoán đổi 0.005870949361603315 BNB để đổi lấy 100000 CC1222, và lượng dư ra 0.004129050638396685 ETH được hoàn trả lại:

Bạn cũng có thể thực hiện swap từ CC1222 sang BNB, nhưng trước khi thực hiện swap chúng ta phải thực hiện cấp quyền thủ công cho địa chỉ contract PancakeV4Swap được quyền sử dụng CC1222 từ ví của bạn.
Mình là lập trình viên, mặc dù cũng đã khá lớn tuổi nhưng vẫn thích Lập trình. Gần đây mình tập trung tìm hiểu nhiều hơn về Lĩnh vực Blockchain. Với kiến thức tìm hiểu được, mình muốn viết ra để lưu lại cũng như để chia sẻ cho những người quan tâm. Mong mọi người góp ý và có thể cùng mình chia sẻ nhiều kiến thức hơn cho cộng đồng.







Trả lời