برمجة مجمعات السيولة (Liquidity Pools): كيف يتم التبادل بدون وسيط مركزي؟
برمجة مجمعات السيولة (Liquidity Pools): كيف يتم التبادل بدون وسيط مركزي؟
عند الحديث عن التمويل اللامركزي، فإن مجمعات السيولة تمثل أحد أهم الابتكارات التي سمحت بتنفيذ التبادل دون دفتر أوامر مركزي ودون جهة تتحكم في مطابقة المشترين والبائعين. وإذا كنت قد قرأت مدخل إلى Web3: ما هو البلوكتشين ولماذا يغير شكل الإنترنت والأنظمة المالية؟ ثم انتقلت إلى مقدمة في التمويل اللامركزي (DeFi): كيف تعمل منصات التبادل مثل Uniswap برمجياً؟ فهذه المقالة ستأخذك إلى المستوى البرمجي الأعمق داخل منطق Liquidity Pools.
الفكرة الجوهرية بسيطة ظاهرياً: بدلاً من أن ينتظر المتداول وجود طرف مقابل، يقوم العقد الذكي بالاحتفاظ باحتياطي من أصلين رقميين، ثم يحسب السعر آلياً وفق معادلة رياضية. لكن برمجياً، هذا يتطلب فهماً دقيقاً لآلية التسعير، توزيع الرسوم، إدارة أرصدة المزوّدين، والتحقق من أمان الدوال التي تنقل رموز ERC-20.
ما هو مجمع السيولة برمجياً؟
مجمع السيولة هو عقد ذكي يحتفظ برصيدين من توكنين، مثل TokenA وTokenB. عندما يضيف المستخدم سيولة، فإنه يودع الأصلين بنسبة معينة. وعندما ينفذ متداول عملية تبديل، يدخل أحد الأصلين إلى المجمع ويخرج الأصل الآخر بعد خصم الرسوم.
هذا النموذج يعتمد على Automated Market Maker أو AMM، وهو بديل برمجي لدفتر الأوامر التقليدي. بدلاً من البحث عن سعر من أوامر السوق، يتم تحديد السعر من الاحتياطيات داخل العقد.
المعادلة الأساسية: لماذا يعتمد السعر على الاحتياطي؟
أشهر نموذج مستخدم في منصات مثل Uniswap V2 هو معادلة المنتج الثابت: x * y = k. هنا يمثل x احتياطي الأصل الأول، وy احتياطي الأصل الثاني، بينما يبقى k ثابتاً تقريباً بعد كل عملية، مع مراعاة الرسوم.
إذا زاد أحد الاحتياطيات بسبب دخول متداول، يجب أن ينخفض الاحتياطي الآخر للمحافظة على التوازن الرياضي. بهذه الطريقة يتغير السعر ذاتياً بحسب العرض والطلب داخل المجمع، وليس عبر مدير سوق مركزي.
أي عملية تبديل كبيرة مقارنة بحجم الاحتياطي تسبب
Slippageمرتفعاً. لهذا فإن عمق السيولة ليس مجرد رقم تسويقي، بل عامل حاسم في جودة السعر واستقرار التبادل.
المكونات البرمجية لعقد مجمع سيولة بسيط
قبل كتابة الكود، نحتاج إلى عدة مفاهيم درستها سابقاً مثل أساسيات لغة Solidity: أنواع البيانات والمتغيرات (State Variables)، والقواميس (Mappings): أسرع طريقة لربط عناوين المحافظ بأرصدتها (Key-Value)، والتعامل مع الأخطاء وإرجاع الأموال: استخدام require, assert, revert.
عقد مجمع السيولة يحتاج عادة إلى:
- عنواني التوكنين بصيغة
IERC20. - متغيري احتياطي مثل
reserve0وreserve1. - تتبع حصص مزودي السيولة عبر
mappingأو عبر توكن حصة منفصل. - دوال لإضافة السيولة، سحبها، وتنفيذ
swap. - أحداث
eventsلتحديث الواجهة الأمامية، كما شرحنا في الأحداث (Events): كيف يخبر العقد الذكي واجهة الموقع (React) بأن شيئاً ما قد حدث؟.
نموذج عقد ذكي مبسط لمجمع سيولة
الكود التالي ليس بديلاً عن بروتوكول إنتاجي كامل، لكنه يوضح الهيكل المنطقي لمجمع بسيط يدعم أصلين ورسماً ثابتاً على التبديل. كما أنه يوظف واجهات التوكنات، وهي فكرة مرتبطة مباشرة بمقال الواجهات (Interfaces): التحدث مع عقود ذكية لا تملك كودها المصدري (مثل منصات التبادل).
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IERC20 {
function transferFrom(address from, address to, uint256 value) external returns (bool);
function transfer(address to, uint256 value) external returns (bool);
function balanceOf(address account) external view returns (uint256);
}
contract SimpleLiquidityPool {
IERC20 public token0;
IERC20 public token1;
uint256 public reserve0;
uint256 public reserve1;
uint256 public totalShares;
mapping(address => uint256) public shares;
event LiquidityAdded(address indexed provider, uint256 amount0, uint256 amount1, uint256 mintedShares);
event LiquidityRemoved(address indexed provider, uint256 amount0, uint256 amount1, uint256 burnedShares);
event Swapped(address indexed user, address tokenIn, uint256 amountIn, address tokenOut, uint256 amountOut);
constructor(address _token0, address _token1) {
token0 = IERC20(_token0);
token1 = IERC20(_token1);
}
function addLiquidity(uint256 amount0, uint256 amount1) external returns (uint256 mintedShares) {
require(amount0 > 0 && amount1 > 0, "Invalid amounts");
token0.transferFrom(msg.sender, address(this), amount0);
token1.transferFrom(msg.sender, address(this), amount1);
if (totalShares == 0) {
mintedShares = sqrt(amount0 * amount1);
} else {
uint256 share0 = (amount0 * totalShares) / reserve0;
uint256 share1 = (amount1 * totalShares) / reserve1;
mintedShares = share0 < share1 ? share0 : share1;
}
require(mintedShares > 0, "Zero shares");
shares[msg.sender] += mintedShares;
totalShares += mintedShares;
reserve0 += amount0;
reserve1 += amount1;
emit LiquidityAdded(msg.sender, amount0, amount1, mintedShares);
}
function removeLiquidity(uint256 shareAmount) external returns (uint256 amount0, uint256 amount1) {
require(shareAmount > 0, "Invalid share");
require(shares[msg.sender] >= shareAmount, "Not enough shares");
amount0 = (shareAmount * reserve0) / totalShares;
amount1 = (shareAmount * reserve1) / totalShares;
shares[msg.sender] -= shareAmount;
totalShares -= shareAmount;
reserve0 -= amount0;
reserve1 -= amount1;
token0.transfer(msg.sender, amount0);
token1.transfer(msg.sender, amount1);
emit LiquidityRemoved(msg.sender, amount0, amount1, shareAmount);
}
function swapToken0ForToken1(uint256 amountIn) external returns (uint256 amountOut) {
require(amountIn > 0, "Invalid input");
token0.transferFrom(msg.sender, address(this), amountIn);
uint256 amountInWithFee = amountIn * 997;
uint256 numerator = amountInWithFee * reserve1;
uint256 denominator = (reserve0 * 1000) + amountInWithFee;
amountOut = numerator / denominator;
require(amountOut > 0 && amountOut < reserve1, "Invalid output");
reserve0 += amountIn;
reserve1 -= amountOut;
token1.transfer(msg.sender, amountOut);
emit Swapped(msg.sender, address(token0), amountIn, address(token1), amountOut);
}
function sqrt(uint256 y) internal pure returns (uint256 z) {
if (y > 3) {
z = y;
uint256 x = y / 2 + 1;
while (x < z) {
z = x;
x = (y / x + x) / 2;
}
} else if (y != 0) {
z = 1;
}
}
}
كيف تعمل الدوال الأساسية؟
1) دالة إضافة السيولة
عند أول مزود سيولة، لا يوجد سعر سابق، لذلك تُحسب الحصص من الجذر التربيعي لحاصل ضرب الكميتين. بعد ذلك، يجب على أي مزود جديد أن يضيف الأصلين بما يتناسب مع الاحتياطي الحالي، وإلا سيحصل على عدد حصص أقل من المتوقع.
2) دالة السحب
السحب لا يعتمد على المبلغ الأصلي المودع، بل على نسبة الحصة الحالية من إجمالي الحصص. هذا يعني أن مزود السيولة يستفيد من الرسوم المتراكمة داخل الاحتياطيات، لكنه أيضاً يتعرض لتغير السعر بين الأصلين.
3) دالة التبديل
دالة swapToken0ForToken1 تطبق رسماً نسبته 0.3% عبر ضرب الإدخال في 997/1000. ثم تستخدم المعادلة لاستخراج ناتج التبديل دون كسر ثابت المنتج.
المخاطر التقنية التي يجب فهمها
برمجة مجمع سيولة ليست مجرد نقل توكنات بين محافظ. هناك مخاطر حقيقية مثل Impermanent Loss، والتلاعب السعري، وهجمات العقود الخبيثة التي تستغل ترتيب العمليات أو إعادة الدخول.
إذا كانت دالة السحب أو التبديل تستدعي توكنات خارجية قبل تحديث الحالة الداخلية، فقد تفتح الباب لهجمات إعادة الدخول. راجع أمن العقود الذكية (1): ثغرة إعادة الدخول (Reentrancy Attack) الشهيرة وكيفية استغلالها ثم طبّق الحماية الموضحة في أمن العقود الذكية (2): الحماية من ثغرة Reentrancy باستخدام ReentrancyGuard.
من المهم أيضاً الانتباه إلى أن بعض توكنات ERC-20 لا تعيد bool بشكل منضبط، ولهذا يفضّل استخدام مكتبة OpenZeppelin لكتابة عقود ذكية آمنة ومختبرة مسبقاً مع أدوات مثل SafeERC20.
تحسين استهلاك الغاز في مجمعات السيولة
بما أن مجمعات السيولة تتلقى عدداً كبيراً من العمليات، فإن أي تحسين صغير في الغاز ينعكس مباشرة على تجربة المستخدم وكلفة البروتوكول. لهذا يجب ضبط بنية التخزين والدوال بعناية، خاصة إذا كنت تعمل ضمن بيئة الانتقال إلى بيئة العمل الاحترافية: تثبيت إطار عمل Hardhat باستخدام Node.js وتبني اختبارات قياس الأداء.
لتقليل
Gas Fees، خزّن القيم المستخدمة كثيراً في متغيرات محلية داخل الدالة، وقلّل عدد عمليات الكتابة إلىstorage. ولمراجعة الأساس النظري ارجع إلى التكاليف (Gas Fees): كيف يحسب البلوكتشين تكلفة تنفيذ الأكواد؟ وإدارة الذاكرة بذكاء: الفرق الحاسم بين Storage, Memory, و Calldata.
كيف نختبر المجمع ونربطه بالواجهة؟
بعد كتابة العقد، يجب اختباره محلياً عبر Hardhat للتأكد من صحة الحسابات في حالات الإيداع والسحب والتبديل، خصوصاً في الحواف مثل الإدخالات الصغيرة أو السحب الكامل. ويمكنك الرجوع إلى اختبار العقود الذكية محلياً: كتابة اختبارات الوحدة (Unit Tests) باستخدام Chai & Mocha.
أما على مستوى الواجهة، فعادة يتم استخدام Ethers.js لقراءة الاحتياطيات، تنفيذ الموافقات approve، ثم إرسال معاملة swap. وهنا يفيدك الربط مع هندسة الويب اللامركزي (Web3.js & Ethers.js): كيف نربط الواجهات بالعقود الذكية؟ وكتابة البيانات وإرسال المعاملات (Transactions) من واجهة الويب إلى العقد الذكي.
الخلاصة
مجمعات السيولة ليست مجرد فكرة مالية، بل هي بنية حسابية وعقدية دقيقة تسمح للتبادل بأن يحدث دون وسيط مركزي، عبر احتياطيات محفوظة داخل عقد ذكي ومعادلة تسعير واضحة. نجاح هذا النموذج يعتمد على فهم التوازن بين الرياضيات، أمان التنفيذ، وتجربة المستخدم.
وعندما تتقن برمجة Liquidity Pools، فأنت لا تتعلم فقط كيف يعمل التبديل اللامركزي، بل تدخل فعلياً إلى قلب تطبيقات DeFi الحديثة، حيث تصبح الرياضيات والعقود الذكية بديلاً برمجياً لوساطة كانت حكراً على المنصات المركزية.
3 comments