جولة تفصيلية في عقد Uniswap-v2
مقدمة
يمكن لـ Uniswap v2 (opens in a new tab) إنشاء سوق تبادل بين أي رمزي ERC-20. في هذه المقالة، سنستعرض الكود المصدري للعقود التي تنفذ هذا البروتوكول ونرى لماذا كُتبت بهذه الطريقة.
ماذا يفعل Uniswap؟
بشكل أساسي، هناك نوعان من المستخدمين: مزودو السيولة والمتداولون.
يقوم مزودو السيولة بتزويد المجمع بالرمزين اللذين يمكن تبادلهما (سنطلق عليهما Token0 و Token1). في المقابل، يحصلون على رمز ثالث يمثل ملكية جزئية للمجمع يُسمى رمز السيولة.
يقوم المتداولون بإرسال نوع واحد من الرموز إلى المجمع واستلام النوع الآخر (على سبيل المثال، إرسال Token0 واستلام Token1) من المجمع الذي يوفره مزودو السيولة. يتم تحديد سعر الصرف بناءً على العدد النسبي لرموز Token0 و Token1 الموجودة في المجمع. بالإضافة إلى ذلك، يأخذ المجمع نسبة مئوية صغيرة كمكافأة لمجمع السيولة.
عندما يرغب مزودو السيولة في استعادة أصولهم، يمكنهم حرق رموز المجمع واستعادة رموزهم، بما في ذلك حصتهم من المكافآت.
انقر هنا للحصول على وصف أكمل (opens in a new tab).
لماذا الإصدار الثاني (v2)؟ ولماذا ليس الإصدار الثالث (v3)؟
يُعد Uniswap v3 (opens in a new tab) ترقية أكثر تعقيدًا بكثير من الإصدار الثاني (v2). من الأسهل تعلم الإصدار الثاني أولاً ثم الانتقال إلى الإصدار الثالث.
العقود الأساسية مقابل العقود الطرفية
ينقسم Uniswap v2 إلى مكونين: أساسي وطرفي. يسمح هذا التقسيم للعقود الأساسية، التي تحتفظ بالأصول وبالتالي يجب أن تكون آمنة، بأن تكون أبسط وأسهل في التدقيق. يمكن بعد ذلك توفير جميع الوظائف الإضافية التي يطلبها المتداولون من خلال العقود الطرفية.
تدفقات البيانات والتحكم
هذا هو تدفق البيانات والتحكم الذي يحدث عند تنفيذ الإجراءات الثلاثة الرئيسية في Uniswap:
- المبادلة بين رموز مختلفة
- إضافة سيولة إلى السوق والحصول على مكافأة برموز سيولة ERC-20 الخاصة بزوج التداول
- حرق رموز سيولة ERC-20 واسترداد رموز ERC-20 التي تسمح منصة تداول الزوج للمتداولين بتبادلها
المبادلة
هذا هو التدفق الأكثر شيوعًا، والذي يستخدمه المتداولون:
المتصل
- تزويد الحساب المحيطي (periphery account) بمخصص (allowance) بالمبلغ المراد مبادلته.
- استدعاء إحدى وظائف المبادلة العديدة للعقد المحيطي (تعتمد الوظيفة على ما إذا كان ETH متضمنًا أم لا، وما إذا كان المتداول يحدد مقدار الرموز المراد إيداعها أو مقدار الرموز المراد استردادها، وما إلى ذلك).
تقبل كل وظيفة مبادلة
path، وهي مصفوفة من منصات التداول التي يجب المرور عبرها.
في العقد المحيطي (UniswapV2Router02.sol)
- تحديد المبالغ التي يجب تداولها في كل منصة تداول على طول المسار.
- التكرار عبر المسار. بالنسبة لكل منصة تداول على طول الطريق، فإنه يرسل رمز الإدخال ثم يستدعي وظيفة
swapالخاصة بمنصة التداول. في معظم الحالات، يكون عنوان الوجهة للرموز هو منصة تداول الزوج التالية في المسار. في منصة التداول النهائية، يكون هو العنوان الذي قدمه المتداول.
في العقد الأساسي (UniswapV2Pair.sol)
- التحقق من عدم تعرض العقد الأساسي للاحتيال وأنه يمكنه الحفاظ على سيولة كافية بعد المبادلة.
- معرفة عدد الرموز الإضافية التي لدينا بالإضافة إلى الاحتياطيات المعروفة. هذا المقدار هو عدد رموز الإدخال التي تلقيناها لتبادلها.
- إرسال رموز الإخراج إلى الوجهة.
- استدعاء
_updateلتحديث مبالغ الاحتياطي
العودة إلى العقد المحيطي (UniswapV2Router02.sol)
- إجراء أي عمليات تنظيف ضرورية (على سبيل المثال، حرق رموز WETH لاسترداد ETH لإرساله إلى المتداول)
إضافة السيولة
المتصل
- تزويد الحساب المحيطي بمخصص بالمبالغ المراد إضافتها إلى مجمع السيولة.
- استدعاء إحدى وظائف
addLiquidityالخاصة بالعقد المحيطي.
في العقد المحيطي (UniswapV2Router02.sol)
- إنشاء منصة تداول زوج جديدة إذا لزم الأمر
- إذا كانت هناك منصة تداول زوج حالية، فاحسب مقدار الرموز المراد إضافتها. من المفترض أن تكون هذه القيمة متطابقة لكلا الرمزين، وبالتالي نفس نسبة الرموز الجديدة إلى الرموز الحالية.
- التحقق مما إذا كانت المبالغ مقبولة (يمكن للمتصلين تحديد حد أدنى للمبلغ الذي يفضلون عدم إضافة سيولة إذا كان أقل منه)
- استدعاء العقد الأساسي.
في العقد الأساسي (UniswapV2Pair.sol)
- السك او انشاء رموز السيولة وإرسالها إلى المتصل
- استدعاء
_updateلتحديث مبالغ الاحتياطي
إزالة السيولة
المتصل
- تزويد الحساب المحيطي بمخصص من رموز السيولة ليتم حرقها مقابل الرموز الأساسية.
- استدعاء إحدى وظائف
removeLiquidityالخاصة بالعقد المحيطي.
في العقد المحيطي (UniswapV2Router02.sol)
- إرسال رموز السيولة إلى منصة تداول الزوج
في العقد الأساسي (UniswapV2Pair.sol)
- إرسال الرموز الأساسية إلى عنوان الوجهة بما يتناسب مع الرموز المحروقة. على سبيل المثال، إذا كان هناك 1000 رمز A في المجمع، و500 رمز B، و90 رمز سيولة، وتلقينا 9 رموز لحرقها، فإننا نحرق 10% من رموز السيولة، لذا نعيد للمستخدم 100 رمز A و50 رمز B.
- حرق رموز السيولة
- استدعاء
_updateلتحديث مبالغ الاحتياطي
العقود الأساسية
هذه هي العقود الآمنة التي تحتفظ بالسيولة.
UniswapV2Pair.sol
ينفذ هذا العقد (opens in a new tab) المجمع الفعلي الذي يتبادل الرموز. إنها الوظيفة الأساسية لـ Uniswap.
1pragma solidity =0.5.16;2
3import './interfaces/IUniswapV2Pair.sol';4import './UniswapV2ERC20.sol';5import './libraries/Math.sol';6import './libraries/UQ112x112.sol';7import './interfaces/IERC20.sol';8import './interfaces/IUniswapV2Factory.sol';9import './interfaces/IUniswapV2Callee.sol';هذه هي جميع الواجهات التي يحتاج العقد إلى معرفتها، إما لأن العقد ينفذها (IUniswapV2Pair و UniswapV2ERC20) أو لأنه يستدعي العقود التي تنفذها.
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 {يرث هذا العقد من UniswapV2ERC20، والذي يوفر وظائف ERC-20 لرموز السيولة.
1 using SafeMath for uint;تُستخدم مكتبة SafeMath (opens in a new tab) لتجنب طفح البيانات (overflows) ونقصانها (underflows). هذا مهم لأنه بخلاف ذلك قد ينتهي بنا الأمر إلى حالة يجب أن تكون فيها القيمة -1، ولكنها بدلاً من ذلك 2^256-1.
1 using UQ112x112 for uint224;تتطلب الكثير من الحسابات في عقد المجمع كسورًا. ومع ذلك، فإن الكسور غير مدعومة من قبل آلة إيثريوم الافتراضية (EVM).
الحل الذي وجدته Uniswap هو استخدام قيم بحجم 224 بت، مع 112 بت للجزء الصحيح، و 112 بت للكسر. لذلك يتم تمثيل 1.0 كـ 2^112، ويتم تمثيل 1.5 كـ 2^112 + 2^111، إلخ.
تتوفر المزيد من التفاصيل حول هذه المكتبة لاحقًا في المستند.
المتغيرات
1 uint public constant MINIMUM_LIQUIDITY = 10**3;لتجنب حالات القسمة على صفر، هناك حد أدنى من رموز السيولة الموجودة دائمًا (ولكنها مملوكة للحساب صفر). هذا الرقم هو MINIMUM_LIQUIDITY، وهو ألف.
1 bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)')));هذا هو محدد ABI لوظيفة التحويل الخاصة بـ ERC-20. يتم استخدامه لتحويل رموز ERC-20 في حسابي الرموز.
1 address public factory;هذا هو عقد المصنع الذي أنشأ هذا المجمع. كل مجمع هو عبارة عن تبادل بين رمزين من نوع ERC-20، والمصنع هو نقطة مركزية تربط جميع هذه المجمعات.
1 address public token0;2 address public token1;هناك عناوين العقود لنوعي رموز ERC-20 التي يمكن تبادلها بواسطة هذا المجمع.
1 uint112 private reserve0; // uses single storage slot, accessible via getReserves // يستخدم خانة تخزين واحدة، يمكن الوصول إليها عبر getReserves2 uint112 private reserve1; // uses single storage slot, accessible via getReserves // يستخدم خانة تخزين واحدة، يمكن الوصول إليها عبر getReservesالاحتياطيات التي يمتلكها المجمع لكل نوع من الرموز. نفترض أن الاثنين يمثلان نفس المقدار من القيمة، وبالتالي فإن كل token0 يساوي reserve1/reserve0 من token1.
1 uint32 private blockTimestampLast; // uses single storage slot, accessible via getReserves // يستخدم خانة تخزين واحدة، يمكن الوصول إليها عبر getReservesالطابع الزمني لآخر بلوك حدث فيه تبادل، ويُستخدم لتتبع أسعار الصرف عبر الزمن.
واحدة من أكبر نفقات الغاز في عقود إيثريوم هي التخزين، والذي يستمر من استدعاء واحد للعقد إلى الاستدعاء التالي. يبلغ طول كل خلية تخزين 256 بت. لذلك يتم تخصيص ثلاثة متغيرات، reserve0 و reserve1 و blockTimestampLast، بطريقة يمكن أن تتضمن فيها قيمة تخزين واحدة الثلاثة معًا (112+112+32=256).
1 uint public price0CumulativeLast;2 uint public price1CumulativeLast;تحتفظ هذه المتغيرات بالتكاليف التراكمية لكل رمز (كل منها بدلالة الآخر). يمكن استخدامها لحساب متوسط سعر الصرف خلال فترة زمنية.
1 uint public kLast; // reserve0 * reserve1, as of immediately after the most recent liquidity event // reserve0 * reserve1، كما هو الحال فوراً بعد أحدث حدث للسيولةالطريقة التي يقرر بها تبادل الزوج سعر الصرف بين token0 و token1 هي الحفاظ على مضاعف الاحتياطيين ثابتًا أثناء التداولات. kLast هي هذه القيمة. تتغير عندما يقوم مزود السيولة بإيداع أو سحب الرموز، وتزداد قليلاً بسبب رسوم السوق البالغة 0.3%.
إليك مثال بسيط. لاحظ أنه من أجل التبسيط، يحتوي الجدول على ثلاثة أرقام فقط بعد الفاصلة العشرية، ونتجاهل رسوم التداول البالغة 0.3% لذا فإن الأرقام ليست دقيقة.
| الحدث | reserve0 | reserve1 | reserve0 * reserve1 | متوسط سعر الصرف (token1 / token0) |
|---|---|---|---|---|
| الإعداد الأولي | 1,000.000 | 1,000.000 | 1,000,000 | |
| المتداول أ يبادل 50 token0 مقابل 47.619 token1 | 1,050.000 | 952.381 | 1,000,000 | 0.952 |
| المتداول ب يبادل 10 token0 مقابل 8.984 token1 | 1,060.000 | 943.396 | 1,000,000 | 0.898 |
| المتداول ج يبادل 40 token0 مقابل 34.305 token1 | 1,100.000 | 909.090 | 1,000,000 | 0.858 |
| المتداول د يبادل 100 token1 مقابل 109.01 token0 | 990.990 | 1,009.090 | 1,000,000 | 0.917 |
| المتداول هـ يبادل 10 token0 مقابل 10.079 token1 | 1,000.990 | 999.010 | 1,000,000 | 1.008 |
مع توفير المتداولين للمزيد من token0، تزداد القيمة النسبية لـ token1، والعكس صحيح، بناءً على العرض والطلب.
القفل (Lock)
1 uint private unlocked = 1;هناك فئة من الثغرات الأمنية التي تعتمد على إساءة استخدام إعادة الدخول (reentrancy) (opens in a new tab). تحتاج Uniswap إلى تحويل رموز ERC-20 عشوائية، مما يعني استدعاء عقود ERC-20 التي قد تحاول إساءة استخدام سوق Uniswap الذي يستدعيها.
من خلال وجود متغير unlocked كجزء من العقد، يمكننا منع استدعاء الوظائف أثناء تشغيلها (ضمن نفس المعاملة).
1 modifier lock() {هذه الوظيفة عبارة عن مُعدِّل (modifier) (opens in a new tab)، وهي وظيفة تلتف حول وظيفة عادية لتغيير سلوكها بطريقة ما.
1 require(unlocked == 1, 'UniswapV2: LOCKED');2 unlocked = 0;إذا كان unlocked يساوي واحدًا، فقم بتعيينه إلى الصفر. إذا كان صفرًا بالفعل، فقم بإرجاع (revert) الاستدعاء، واجعله يفشل.
1 _;في المُعدِّل، _; هو استدعاء الوظيفة الأصلي (مع جميع المعلمات). هنا يعني أن استدعاء الوظيفة يحدث فقط إذا كان unlocked واحدًا عند استدعائه، وأثناء تشغيله تكون قيمة unlocked صفرًا.
1 unlocked = 1;2 }بعد عودة الوظيفة الرئيسية، قم بتحرير القفل.
وظائف متنوعة
1 function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {2 _reserve0 = reserve0;3 _reserve1 = reserve1;4 _blockTimestampLast = blockTimestampLast;5 }توفر هذه الوظيفة للمتصلين الحالة الحالية للتبادل. لاحظ أن وظائف Solidity يمكن أن تُرجع قيمًا متعددة (opens in a new tab).
1 function _safeTransfer(address token, address to, uint value) private {2 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value));تقوم هذه الوظيفة الداخلية بتحويل كمية من رموز ERC20 من التبادل إلى شخص آخر. يحدد SELECTOR أن الوظيفة التي نستدعيها هي transfer(address,uint) (انظر التعريف أعلاه).
لتجنب الاضطرار إلى استيراد واجهة لوظيفة الرمز، نقوم بإنشاء الاستدعاء "يدويًا" باستخدام إحدى وظائف ABI (opens in a new tab).
1 require(success && (data.length == 0 || abi.decode(data, (bool))), 'UniswapV2: TRANSFER_FAILED');2 }هناك طريقتان يمكن من خلالهما لاستدعاء تحويل ERC-20 الإبلاغ عن الفشل:
- التراجع (Revert). إذا تراجع استدعاء لعقد خارجي، فإن القيمة المنطقية المرجعة تكون
false - الانتهاء بشكل طبيعي ولكن مع الإبلاغ عن فشل. في هذه الحالة، يكون للمخزن المؤقت للقيمة المرجعة طول غير صفري، وعند فك تشفيره كقيمة منطقية يكون
false
إذا حدث أي من هذين الشرطين، فقم بالتراجع (revert).
الأحداث (Events)
1 event Mint(address indexed sender, uint amount0, uint amount1);2 event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);يتم إصدار هذين الحدثين عندما يقوم مزود السيولة إما بإيداع السيولة (Mint) أو سحبها (Burn). في كلتا الحالتين، تكون كميات token0 و token1 المودعة أو المسحوبة جزءًا من الحدث، بالإضافة إلى هوية الحساب الذي استدعانا (sender). في حالة السحب، يتضمن الحدث أيضًا الهدف الذي تلقى الرموز (to)، والذي قد لا يكون هو نفسه المرسل.
1 event Swap(2 address indexed sender,3 uint amount0In,4 uint amount1In,5 uint amount0Out,6 uint amount1Out,7 address indexed to8 );يتم إصدار هذا الحدث عندما يقوم المتداول بمبادلة رمز بآخر. مرة أخرى، قد لا يكون المرسل والوجهة متطابقين. يمكن إرسال كل رمز إما إلى التبادل، أو استلامه منه.
1 event Sync(uint112 reserve0, uint112 reserve1);أخيرًا، يتم إصدار Sync في كل مرة يتم فيها إضافة الرموز أو سحبها، بغض النظر عن السبب، لتوفير أحدث معلومات الاحتياطي (وبالتالي سعر الصرف).
وظائف الإعداد
من المفترض أن يتم استدعاء هذه الوظائف مرة واحدة عند إعداد تبادل الزوج الجديد.
1 constructor() public {2 factory = msg.sender;3 }يتأكد المُنشئ (constructor) من أننا سنتتبع عنوان المصنع الذي أنشأ الزوج. هذه المعلومات مطلوبة لـ initialize ولرسوم المصنع (إن وجدت).
1 // called once by the factory at time of deployment // يُستدعى مرة واحدة بواسطة المصنع في وقت النشر2 function initialize(address _token0, address _token1) external {3 require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check // تحقق كافٍ4 token0 = _token0;5 token1 = _token1;6 }تسمح هذه الوظيفة للمصنع (والمصنع فقط) بتحديد رمزي ERC-20 اللذين سيتبادلهما هذا الزوج.
وظائف التحديث الداخلية
_update
1 // update reserves and, on the first call per block, price accumulators // تحديث الاحتياطيات، وفي أول استدعاء لكل كتلة، مجمعات الأسعار2 function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {يتم استدعاء هذه الوظيفة في كل مرة يتم فيها إيداع الرموز أو سحبها.
1 require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');إذا كان إما balance0 أو balance1 (uint256) أعلى من uint112(-1) (=2^112-1) (بحيث يفيض ويعود إلى 0 عند تحويله إلى uint112) ارفض الاستمرار في _update لمنع طفح البيانات. مع رمز عادي يمكن تقسيمه إلى 10^18 وحدة، هذا يعني أن كل تبادل يقتصر على حوالي 5.1*10^15 من كل رمز. حتى الآن لم تكن هذه مشكلة.
1 uint32 blockTimestamp = uint32(block.timestamp % 2**32);2 uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired // الطفح (overflow) مطلوب3 if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {إذا لم يكن الوقت المنقضي صفرًا، فهذا يعني أننا أول معاملة تبادل في هذا البلوك. في هذه الحالة، نحتاج إلى تحديث مجمعات التكلفة.
1 // * never overflows, and + overflow is desired // * لا يطفح أبداً، وطفح + مطلوب2 price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;3 price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;4 }يتم تحديث كل مجمع تكلفة بأحدث تكلفة (احتياطي الرمز الآخر / احتياطي هذا الرمز) مضروبًا في الوقت المنقضي بالثواني. للحصول على متوسط السعر، تقرأ السعر التراكمي في نقطتين زمنيتين وتقسمه على الفارق الزمني بينهما. على سبيل المثال، افترض هذا التسلسل من الأحداث:
| الحدث | reserve0 | reserve1 | الطابع الزمني | سعر الصرف الهامشي (reserve1 / reserve0) | price0CumulativeLast |
|---|---|---|---|---|---|
| الإعداد الأولي | 1,000.000 | 1,000.000 | 5,000 | 1.000 | 0 |
| المتداول أ يودع 50 token0 ويسترد 47.619 token1 | 1,050.000 | 952.381 | 5,020 | 0.907 | 20 |
| المتداول ب يودع 10 token0 ويسترد 8.984 token1 | 1,060.000 | 943.396 | 5,030 | 0.890 | 20+10*0.907 = 29.07 |
| المتداول ج يودع 40 token0 ويسترد 34.305 token1 | 1,100.000 | 909.090 | 5,100 | 0.826 | 29.07+70*0.890 = 91.37 |
| المتداول د يودع 100 token1 ويسترد 109.01 token0 | 990.990 | 1,009.090 | 5,110 | 1.018 | 91.37+10*0.826 = 99.63 |
| المتداول هـ يودع 10 token0 ويسترد 10.079 token1 | 1,000.990 | 999.010 | 5,150 | 0.998 | 99.63+40*1.1018 = 143.702 |
لنفترض أننا نريد حساب متوسط سعر Token0 بين الطوابع الزمنية 5,030 و 5,150. الفرق في قيمة price0Cumulative هو 143.702-29.07=114.632. هذا هو المتوسط عبر دقيقتين (120 ثانية). إذن متوسط السعر هو 114.632/120 = 0.955.
حساب السعر هذا هو السبب في أننا بحاجة إلى معرفة أحجام الاحتياطي القديمة.
1 reserve0 = uint112(balance0);2 reserve1 = uint112(balance1);3 blockTimestampLast = blockTimestamp;4 emit Sync(reserve0, reserve1);5 }أخيرًا، قم بتحديث المتغيرات العامة وإصدار حدث Sync.
_mintFee
1 // if fee is on, mint liquidity equivalent to 1/6th of the growth in sqrt(k) // إذا كانت الرسوم مفعلة، قم بـ السك او انشاء سيولة تعادل 1/6 من النمو في sqrt(k)2 function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {في Uniswap 2.0، يدفع المتداولون رسومًا بنسبة 0.30% لاستخدام السوق. يذهب معظم هذه الرسوم (0.25% من التداول) دائمًا إلى مزودي السيولة. يمكن أن تذهب النسبة المتبقية البالغة 0.05% إما إلى مزودي السيولة أو إلى عنوان يحدده المصنع كرسوم بروتوكول، والتي تدفع لـ Uniswap مقابل جهود التطوير الخاصة بهم.
لتقليل الحسابات (وبالتالي تكاليف الغاز)، يتم حساب هذه الرسوم فقط عند إضافة السيولة أو إزالتها من المجمع، بدلاً من حسابها عند كل معاملة.
1 address feeTo = IUniswapV2Factory(factory).feeTo();2 feeOn = feeTo != address(0);اقرأ وجهة الرسوم الخاصة بالمصنع. إذا كانت صفرًا، فلا توجد رسوم بروتوكول ولا حاجة لحساب تلك الرسوم.
1 uint _kLast = kLast; // gas savings // توفير الغازيقع متغير الحالة kLast في التخزين، لذلك سيكون له قيمة بين الاستدعاءات المختلفة للعقد.
يعد الوصول إلى التخزين أكثر تكلفة بكثير من الوصول إلى الذاكرة المتطايرة التي يتم تحريرها عند انتهاء استدعاء الوظيفة للعقد، لذلك نستخدم متغيرًا داخليًا لتوفير الغاز.
1 if (feeOn) {2 if (_kLast != 0) {يحصل مزودو السيولة على حصتهم ببساطة من خلال ارتفاع قيمة رموز السيولة الخاصة بهم. لكن رسوم البروتوكول تتطلب أن يتم السك او انشاء رموز سيولة جديدة وتقديمها إلى عنوان feeTo.
1 uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));2 uint rootKLast = Math.sqrt(_kLast);3 if (rootK > rootKLast) {إذا كانت هناك سيولة جديدة يمكن تحصيل رسوم بروتوكول عليها. يمكنك رؤية وظيفة الجذر التربيعي لاحقًا في هذه المقالة
1 uint numerator = totalSupply.mul(rootK.sub(rootKLast));2 uint denominator = rootK.mul(5).add(rootKLast);3 uint liquidity = numerator / denominator;تم شرح هذا الحساب المعقد للرسوم في الورقة البيضاء (opens in a new tab) في الصفحة 5. نحن نعلم أنه بين الوقت الذي تم فيه حساب kLast والوقت الحاضر لم تتم إضافة أو إزالة أي سيولة (لأننا نجري هذا الحساب في كل مرة تتم فيها إضافة السيولة أو إزالتها، قبل أن تتغير فعليًا)، لذلك يجب أن يأتي أي تغيير في reserve0 * reserve1 من رسوم المعاملة (بدونها سنحافظ على ثبات reserve0 * reserve1).
1 if (liquidity > 0) _mint(feeTo, liquidity);2 }3 }استخدم وظيفة UniswapV2ERC20._mint لإنشاء رموز السيولة الإضافية فعليًا وتعيينها إلى feeTo.
1 } else if (_kLast != 0) {2 kLast = 0;3 }4 }إذا لم تكن هناك رسوم، فقم بتعيين kLast إلى الصفر (إذا لم يكن كذلك بالفعل). عندما تمت كتابة هذا العقد، كانت هناك ميزة استرداد الغاز (opens in a new tab) التي شجعت العقود على تقليل الحجم الإجمالي لحالة إيثريوم عن طريق تصفير التخزين الذي لا يحتاجون إليه.
يحصل هذا الكود على هذا الاسترداد عندما يكون ذلك ممكنًا.
الوظائف التي يمكن الوصول إليها خارجيًا
لاحظ أنه بينما يمكن لأي معاملة أو عقد استدعاء هذه الوظائف، إلا أنها مصممة ليتم استدعاؤها من العقد المحيطي (periphery contract). إذا قمت باستدعائها مباشرة، فلن تتمكن من خداع تبادل الزوج، ولكنك قد تفقد القيمة من خلال خطأ.
mint
1 // this low-level function should be called from a contract which performs important safety checks // يجب استدعاء هذه الدالة منخفضة المستوى من عقد يُجري فحوصات أمان مهمة2 function mint(address to) external lock returns (uint liquidity) {يتم استدعاء هذه الوظيفة عندما يضيف مزود السيولة سيولة إلى المجمع. حيث يقوم بالسك او انشاء رموز سيولة إضافية كمكافأة. يجب استدعاؤها من عقد محيطي يستدعيها بعد إضافة السيولة في نفس المعاملة (حتى لا يتمكن أي شخص آخر من إرسال معاملة تطالب بالسيولة الجديدة قبل المالك الشرعي).
1 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings // توفير الغازهذه هي الطريقة لقراءة نتائج وظيفة Solidity التي تُرجع قيمًا متعددة. نتجاهل القيم المرجعة الأخيرة، وهي الطابع الزمني للبلوك، لأننا لا نحتاج إليها.
1 uint balance0 = IERC20(token0).balanceOf(address(this));2 uint balance1 = IERC20(token1).balanceOf(address(this));3 uint amount0 = balance0.sub(_reserve0);4 uint amount1 = balance1.sub(_reserve1);احصل على الأرصدة الحالية واعرف مقدار ما تمت إضافته من كل نوع من الرموز.
1 bool feeOn = _mintFee(_reserve0, _reserve1);احسب رسوم البروتوكول التي سيتم تحصيلها، إن وجدت، وقم بالسك او انشاء رموز السيولة وفقًا لذلك. نظرًا لأن المعلمات لـ _mintFee هي قيم الاحتياطي القديمة، يتم حساب الرسوم بدقة بناءً فقط على تغييرات المجمع الناتجة عن الرسوم.
1 uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee // توفير الغاز، يجب تعريفه هنا لأن totalSupply يمكن أن يتحدث في _mintFee2 if (_totalSupply == 0) {3 liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);4 _mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens // قفل أول رموز MINIMUM_LIQUIDITY بشكل دائمإذا كان هذا هو الإيداع الأول، فقم بإنشاء رموز MINIMUM_LIQUIDITY وأرسلها إلى العنوان صفر لقفلها. لا يمكن استردادها أبدًا، مما يعني أن المجمع لن يتم إفراغه بالكامل أبدًا (هذا ينقذنا من القسمة على صفر في بعض الأماكن). قيمة MINIMUM_LIQUIDITY هي ألف، والتي بالنظر إلى أن معظم رموز ERC-20 مقسمة إلى وحدات من 10^-18 من الرمز، كما يتم تقسيم ETH إلى wei، هي 10^-15 من قيمة رمز واحد. ليست تكلفة عالية.
في وقت الإيداع الأول، لا نعرف القيمة النسبية للرمزين، لذلك نقوم ببساطة بضرب المبالغ وأخذ الجذر التربيعي، بافتراض أن الإيداع يوفر لنا قيمة متساوية في كلا الرمزين.
يمكننا الوثوق بهذا لأنه من مصلحة المودع توفير قيمة متساوية، لتجنب فقدان القيمة لصالح المراجحة (arbitrage). لنفترض أن قيمة الرمزين متطابقة، لكن المودع أودع أربعة أضعاف من Token1 مقارنة بـ Token0. يمكن للمتداول استخدام حقيقة أن تبادل الزوج يعتقد أن Token0 أكثر قيمة لاستخراج القيمة منه.
| الحدث | reserve0 | reserve1 | reserve0 * reserve1 | قيمة المجمع (reserve0 + reserve1) |
|---|---|---|---|---|
| الإعداد الأولي | 8 | 32 | 256 | 40 |
| المتداول يودع 8 رموز Token0، ويسترد 16 Token1 | 16 | 16 | 256 | 32 |
كما ترى، ربح المتداول 8 رموز إضافية، والتي تأتي من انخفاض في قيمة المجمع، مما يضر بالمودع الذي يمتلكه.
1 } else {2 liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);مع كل إيداع لاحق، نعرف بالفعل سعر الصرف بين الأصلين، ونتوقع من مزودي السيولة توفير قيمة متساوية في كليهما. إذا لم يفعلوا ذلك، فإننا نمنحهم رموز سيولة بناءً على القيمة الأقل التي قدموها كعقوبة.
سواء كان الإيداع الأولي أو إيداعًا لاحقًا، فإن عدد رموز السيولة التي نقدمها يساوي الجذر التربيعي للتغير في reserve0*reserve1 ولا تتغير قيمة رمز السيولة (ما لم نحصل على إيداع لا يحتوي على قيم متساوية من كلا النوعين، وفي هذه الحالة يتم توزيع "الغرامة"). إليك مثال آخر برمزين لهما نفس القيمة، مع ثلاثة إيداعات جيدة وإيداع واحد سيئ (إيداع نوع واحد فقط من الرموز، لذلك لا ينتج أي رموز سيولة).
| الحدث | reserve0 | reserve1 | reserve0 * reserve1 | قيمة المجمع (reserve0 + reserve1) | رموز السيولة التي تم السك او انشاء لها لهذا الإيداع | إجمالي رموز السيولة | قيمة كل رمز سيولة |
|---|---|---|---|---|---|---|---|
| الإعداد الأولي | 8.000 | 8.000 | 64 | 16.000 | 8 | 8 | 2.000 |
| إيداع أربعة من كل نوع | 12.000 | 12.000 | 144 | 24.000 | 4 | 12 | 2.000 |
| إيداع اثنين من كل نوع | 14.000 | 14.000 | 196 | 28.000 | 2 | 14 | 2.000 |
| إيداع بقيمة غير متساوية | 18.000 | 14.000 | 252 | 32.000 | 0 | 14 | ~2.286 |
| بعد المراجحة | ~15.874 | ~15.874 | 252 | ~31.748 | 0 | 14 | ~2.267 |
1 }2 require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');3 _mint(to, liquidity);استخدم وظيفة UniswapV2ERC20._mint لإنشاء رموز السيولة الإضافية فعليًا وإعطائها للحساب الصحيح.
1
2 _update(balance0, balance1, _reserve0, _reserve1);3 if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date // reserve0 و reserve1 محدثان4 emit Mint(msg.sender, amount0, amount1);5 }قم بتحديث متغيرات الحالة (reserve0 و reserve1، وإذا لزم الأمر kLast) وإصدار الحدث المناسب.
burn
1 // this low-level function should be called from a contract which performs important safety checks // يجب استدعاء هذه الدالة منخفضة المستوى من عقد يُجري فحوصات أمان مهمة2 function burn(address to) external lock returns (uint amount0, uint amount1) {يتم استدعاء هذه الوظيفة عند سحب السيولة والحاجة إلى حرق رموز السيولة المناسبة. يجب أيضًا استدعاؤها من حساب محيطي.
1 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings // توفير الغاز2 address _token0 = token0; // gas savings // توفير الغاز3 address _token1 = token1; // gas savings // توفير الغاز4 uint balance0 = IERC20(_token0).balanceOf(address(this));5 uint balance1 = IERC20(_token1).balanceOf(address(this));6 uint liquidity = balanceOf[address(this)];قام العقد المحيطي بتحويل السيولة المراد حرقها إلى هذا العقد قبل الاستدعاء. بهذه الطريقة نعرف مقدار السيولة التي يجب حرقها، ويمكننا التأكد من حرقها.
1 bool feeOn = _mintFee(_reserve0, _reserve1);2 uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee // توفير الغاز، يجب تعريفه هنا لأن totalSupply يمكن أن يتحدث في _mintFee3 amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution // استخدام الأرصدة يضمن التوزيع النسبي4 amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution // استخدام الأرصدة يضمن التوزيع النسبي5 require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');يتلقى مزود السيولة قيمة متساوية من كلا الرمزين. بهذه الطريقة لا نغير سعر الصرف.
1 _burn(address(this), liquidity);2 _safeTransfer(_token0, to, amount0);3 _safeTransfer(_token1, to, amount1);4 balance0 = IERC20(_token0).balanceOf(address(this));5 balance1 = IERC20(_token1).balanceOf(address(this));6
7 _update(balance0, balance1, _reserve0, _reserve1);8 if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date // reserve0 و reserve1 محدثان9 emit Burn(msg.sender, amount0, amount1, to);10 }11
بقية وظيفة burn هي صورة طبق الأصل لوظيفة mint أعلاه.
swap
1 // this low-level function should be called from a contract which performs important safety checks // يجب استدعاء هذه الدالة منخفضة المستوى من عقد يُجري فحوصات أمان مهمة2 function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {من المفترض أيضًا أن يتم استدعاء هذه الوظيفة من عقد محيطي.
1 require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');2 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings // توفير الغاز3 require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');4
5 uint balance0;6 uint balance1;7 { // scope for _token{0,1}, avoids stack too deep errors // نطاق لـ _token{0,1}، يتجنب أخطاء المكدس العميق جداًيمكن تخزين المتغيرات المحلية إما في الذاكرة أو، إذا لم يكن هناك الكثير منها، مباشرة على المكدس (stack). إذا تمكنا من الحد من العدد بحيث نستخدم المكدس، فسنستخدم غازًا أقل. لمزيد من التفاصيل، راجع الورقة الصفراء، المواصفات الرسمية لإيثريوم (opens in a new tab)، ص 26، المعادلة 298.
1 address _token0 = token0;2 address _token1 = token1;3 require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');4 if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens // نقل الرموز بشكل تفائلي5 if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens // نقل الرموز بشكل تفائليهذا التحويل متفائل، لأننا نقوم بالتحويل قبل أن نتأكد من استيفاء جميع الشروط. هذا مقبول في إيثريوم لأنه إذا لم يتم استيفاء الشروط لاحقًا في الاستدعاء، فإننا نتراجع (revert) عنه وعن أي تغييرات أحدثها.
1 if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);أبلغ المتلقي بالمبادلة إذا طُلب ذلك.
1 balance0 = IERC20(_token0).balanceOf(address(this));2 balance1 = IERC20(_token1).balanceOf(address(this));3 }احصل على الأرصدة الحالية. يرسل لنا العقد المحيطي الرموز قبل استدعائنا للمبادلة. هذا يسهل على العقد التحقق من عدم تعرضه للغش، وهو فحص يجب أن يحدث في العقد الأساسي (لأنه يمكن استدعاؤنا من قبل كيانات أخرى غير عقدنا المحيطي).
1 uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;2 uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;3 require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');4 { // scope for reserve{0,1}Adjusted, avoids stack too deep errors // نطاق لـ reserve{0,1}Adjusted، يتجنب أخطاء المكدس العميق جداً5 uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));6 uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));7 require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');هذا فحص سلامة للتأكد من أننا لا نخسر من المبادلة. لا يوجد أي ظرف يجب أن تقلل فيه المبادلة من reserve0*reserve1. هذا أيضًا هو المكان الذي نضمن فيه إرسال رسوم بنسبة 0.3% على المبادلة؛ قبل فحص سلامة قيمة K، نضرب كلا الرصيدين في 1000 مطروحًا منه المبالغ مضروبة في 3، وهذا يعني أنه يتم خصم 0.3% (3/1000 = 0.003 = 0.3%) من الرصيد قبل مقارنة قيمة K الخاصة به بقيمة K للاحتياطيات الحالية.
1 }2
3 _update(balance0, balance1, _reserve0, _reserve1);4 emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);5 }قم بتحديث reserve0 و reserve1، وإذا لزم الأمر مجمعات الأسعار والطابع الزمني وإصدار حدث.
Sync أو Skim
من الممكن أن تخرج الأرصدة الحقيقية عن المزامنة مع الاحتياطيات التي يعتقد تبادل الزوج أنه يمتلكها.
لا توجد طريقة لسحب الرموز دون موافقة العقد، لكن الإيداعات مسألة مختلفة. يمكن لحساب تحويل الرموز إلى التبادل دون استدعاء إما mint أو swap.
في هذه الحالة هناك حلان:
sync، تحديث الاحتياطيات إلى الأرصدة الحاليةskim، سحب المبلغ الإضافي. لاحظ أنه يُسمح لأي حساب باستدعاءskimلأننا لا نعرف من أودع الرموز. يتم إصدار هذه المعلومات في حدث، ولكن لا يمكن الوصول إلى الأحداث من البلوك تشين.
1 // force balances to match reserves // إجبار الأرصدة على مطابقة الاحتياطيات2 function skim(address to) external lock {3 address _token0 = token0; // gas savings // توفير الغاز4 address _token1 = token1; // gas savings // توفير الغاز5 _safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0));6 _safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1));7 }8
9 // force reserves to match balances // إجبار الاحتياطيات على مطابقة الأرصدة10 function sync() external lock {11 _update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1);12 }13}UniswapV2Factory.sol
يقوم هذا العقد (opens in a new tab) بإنشاء تبادلات الأزواج.
1pragma solidity =0.5.16;2
3import './interfaces/IUniswapV2Factory.sol';4import './UniswapV2Pair.sol';5
6contract UniswapV2Factory is IUniswapV2Factory {7 address public feeTo;8 address public feeToSetter;متغيرات الحالة هذه ضرورية لتنفيذ رسوم البروتوكول (انظر الورقة البيضاء (opens in a new tab)، ص 5).
يجمع عنوان feeTo رموز السيولة لرسوم البروتوكول، و feeToSetter هو العنوان المسموح له بتغيير feeTo إلى عنوان مختلف.
1 mapping(address => mapping(address => address)) public getPair;2 address[] public allPairs;تتتبع هذه المتغيرات الأزواج، وهي التبادلات بين نوعين من الرموز.
الأول، getPair، هو تعيين (mapping) يحدد عقد تبادل الزوج بناءً على رمزي ERC-20 اللذين يتبادلهما. يتم تحديد رموز ERC-20 من خلال عناوين العقود التي تنفذها، لذلك فإن المفاتيح والقيمة كلها عناوين. للحصول على عنوان تبادل الزوج الذي يتيح لك التحويل من tokenA إلى tokenB، يمكنك استخدام getPair[<tokenA address>][<tokenB address>] (أو العكس).
المتغير الثاني، allPairs، هو مصفوفة تتضمن جميع عناوين تبادلات الأزواج التي أنشأها هذا المصنع. في إيثريوم، لا يمكنك التكرار (iterate) عبر محتوى التعيين، أو الحصول على قائمة بجميع المفاتيح، لذلك هذا المتغير هو الطريقة الوحيدة لمعرفة التبادلات التي يديرها هذا المصنع.
ملاحظة: السبب في عدم قدرتك على التكرار عبر جميع مفاتيح التعيين هو أن تخزين بيانات العقد مكلف، لذلك كلما قل استخدامنا له كان ذلك أفضل، وكلما قل عدد مرات تغييره كان ذلك أفضل. يمكنك إنشاء تعيينات تدعم التكرار (opens in a new tab)، لكنها تتطلب تخزينًا إضافيًا لقائمة المفاتيح. في معظم التطبيقات لا تحتاج إلى ذلك.
1 event PairCreated(address indexed token0, address indexed token1, address pair, uint);يتم إصدار هذا الحدث عند إنشاء تبادل زوج جديد. ويتضمن عناوين الرموز، وعنوان تبادل الزوج، والعدد الإجمالي للتبادلات التي يديرها المصنع.
1 constructor(address _feeToSetter) public {2 feeToSetter = _feeToSetter;3 }الشيء الوحيد الذي يفعله المُنشئ هو تحديد feeToSetter. تبدأ المصانع بدون رسوم، ووحده feeSetter يمكنه تغيير ذلك.
1 function allPairsLength() external view returns (uint) {2 return allPairs.length;3 }تُرجع هذه الوظيفة عدد أزواج التبادل.
1 function createPair(address tokenA, address tokenB) external returns (address pair) {هذه هي الوظيفة الرئيسية للمصنع، لإنشاء تبادل زوج بين رمزين من نوع ERC-20. لاحظ أنه يمكن لأي شخص استدعاء هذه الوظيفة. لا تحتاج إلى إذن من Uniswap لإنشاء تبادل زوج جديد.
1 require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');2 (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);نريد أن يكون عنوان التبادل الجديد حتميًا (deterministic)، بحيث يمكن حسابه مسبقًا خارج السلسلة (يمكن أن يكون هذا مفيدًا لـ معاملات الطبقة الثانية). للقيام بذلك، نحتاج إلى ترتيب ثابت لعناوين الرموز، بغض النظر عن الترتيب الذي تلقيناها به، لذلك نقوم بفرزها هنا.
1 require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');2 require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient // فحص واحد كافٍمجمعات السيولة الكبيرة أفضل من الصغيرة، لأن أسعارها أكثر استقرارًا. لا نريد أن يكون لدينا أكثر من مجمع سيولة واحد لكل زوج من الرموز. إذا كان هناك تبادل بالفعل، فلا داعي لإنشاء تبادل آخر لنفس الزوج.
1 bytes memory bytecode = type(UniswapV2Pair).creationCode;لإنشاء عقد جديد، نحتاج إلى الكود الذي ينشئه (كل من وظيفة المُنشئ والكود الذي يكتب في الذاكرة الرمز الثنائي (bytecode) لآلة إيثريوم الافتراضية للعقد الفعلي). عادةً في Solidity نستخدم فقط addr = new <name of contract>(<constructor parameters>) ويتولى المترجم (compiler) كل شيء نيابة عنا، ولكن للحصول على عنوان عقد حتمي نحتاج إلى استخدام رمز التشغيل CREATE2 (opens in a new tab).
عندما تمت كتابة هذا الكود، لم يكن رمز التشغيل هذا مدعومًا بعد بواسطة Solidity، لذلك كان من الضروري الحصول على الكود يدويًا. لم يعد هذا يمثل مشكلة، لأن Solidity تدعم الآن CREATE2 (opens in a new tab).
1 bytes32 salt = keccak256(abi.encodePacked(token0, token1));2 assembly {3 pair := create2(0, add(bytecode, 32), mload(bytecode), salt)4 }عندما لا يكون رمز التشغيل مدعومًا بواسطة Solidity بعد، يمكننا استدعاؤه باستخدام التجميع المضمن (inline assembly) (opens in a new tab).
1 IUniswapV2Pair(pair).initialize(token0, token1);استدعِ وظيفة initialize لإخبار التبادل الجديد بالرمزين اللذين يتبادلهما.
1 getPair[token0][token1] = pair;2 getPair[token1][token0] = pair; // populate mapping in the reverse direction // ملء التعيين (mapping) في الاتجاه العكسي3 allPairs.push(pair);4 emit PairCreated(token0, token1, pair, allPairs.length);5 }احفظ معلومات الزوج الجديد في متغيرات الحالة وأصدر حدثًا لإعلام العالم بتبادل الزوج الجديد.
1 function setFeeTo(address _feeTo) external {2 require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');3 feeTo = _feeTo;4 }5
6 function setFeeToSetter(address _feeToSetter) external {7 require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');8 feeToSetter = _feeToSetter;9 }10}تسمح هاتان الوظيفتان لـ feeSetter بالتحكم في مستلم الرسوم (إن وجد)، وتغيير feeSetter إلى عنوان جديد.
UniswapV2ERC20.sol
ينفذ هذا العقد (opens in a new tab) رمز السيولة ERC-20. إنه مشابه لـ عقد OpenZeppelin ERC-20، لذلك سأشرح فقط الجزء المختلف، وهو وظيفة permit.
تكلف المعاملات على إيثريوم إيثر (ETH)، وهو ما يعادل أموالًا حقيقية. إذا كان لديك رموز ERC-20 ولكن ليس لديك ETH، فلا يمكنك إرسال المعاملات، لذلك لا يمكنك فعل أي شيء بها. أحد الحلول لتجنب هذه المشكلة هو المعاملات الوصفية (meta-transactions) (opens in a new tab). يوقع مالك الرموز على معاملة تسمح لشخص آخر بسحب الرموز خارج السلسلة ويرسلها باستخدام الإنترنت إلى المستلم. يقوم المستلم، الذي يمتلك ETH، بتقديم التصريح (permit) نيابة عن المالك.
1 bytes32 public DOMAIN_SEPARATOR;2 // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");3 bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;هذه التجزئة (هاش) هي المعرف لنوع المعاملة (opens in a new tab). النوع الوحيد الذي ندعمه هنا هو Permit مع هذه المعلمات.
1 mapping(address => uint) public nonces;ليس من الممكن للمستلم تزوير التوقيع الرقمي. ومع ذلك، من السهل إرسال نفس المعاملة مرتين (هذا شكل من أشكال هجوم إعادة الإرسال (replay attack) (opens in a new tab)). لمنع ذلك، نستخدم رقم عشوائي (nonce) (opens in a new tab). إذا لم يكن الرقم العشوائي لـ Permit الجديد أكبر بواحد من آخر رقم تم استخدامه، فإننا نفترض أنه غير صالح.
1 constructor() public {2 uint chainId;3 assembly {4 chainId := chainid5 }هذا هو الكود لاسترداد معرف السلسلة (opens in a new tab). يستخدم لهجة تجميع EVM تسمى Yul (opens in a new tab). لاحظ أنه في الإصدار الحالي من Yul يجب عليك استخدام chainid()، وليس chainid.
1 DOMAIN_SEPARATOR = keccak256(2 abi.encode(3 keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'),4 keccak256(bytes(name)),5 keccak256(bytes('1')),6 chainId,7 address(this)8 )9 );10 }احسب فاصل المجال (domain separator) (opens in a new tab) لـ EIP-712.
1 function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external {هذه هي الوظيفة التي تنفذ الأذونات. تتلقى كمعلمات الحقول ذات الصلة، والقيم العددية الثلاث لـ التوقيع (opens in a new tab) (v و r و s).
1 require(deadline >= block.timestamp, 'UniswapV2: EXPIRED');لا تقبل المعاملات بعد الموعد النهائي.
1 bytes32 digest = keccak256(2 abi.encodePacked(3 '\x19\x01',4 DOMAIN_SEPARATOR,5 keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))6 )7 );abi.encodePacked(...) هي الرسالة التي نتوقع الحصول عليها. نحن نعرف ما يجب أن يكون عليه الرقم العشوائي، لذلك ليست هناك حاجة للحصول عليه كمعلمة.
تتوقع خوارزمية توقيع إيثريوم الحصول على 256 بت للتوقيع، لذلك نستخدم وظيفة التجزئة (هاش) keccak256.
1 address recoveredAddress = ecrecover(digest, v, r, s);من الملخص (digest) والتوقيع يمكننا الحصول على العنوان الذي وقعه باستخدام ecrecover (opens in a new tab).
1 require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE');2 _approve(owner, spender, value);3 }4
إذا كان كل شيء على ما يرام، فتعامل مع هذا على أنه موافقة ERC-20 (opens in a new tab).
العقود المحيطية
العقود المحيطية هي واجهة برمجة التطبيقات (API) لـ Uniswap. وهي متاحة للاستدعاءات الخارجية، سواء من عقود أخرى أو تطبيقات لامركزية. يمكنك استدعاء العقود الأساسية مباشرة، ولكن ذلك أكثر تعقيدًا وقد تفقد القيمة إذا ارتكبت خطأ. تحتوي العقود الأساسية فقط على اختبارات للتأكد من عدم تعرضها للاحتيال، وليس فحوصات سلامة لأي شخص آخر. توجد هذه الفحوصات في العقود المحيطية بحيث يمكن تحديثها حسب الحاجة.
UniswapV2Router01.sol
يحتوي هذا العقد (opens in a new tab) على مشاكل، ولم يعد ينبغي استخدامه (opens in a new tab). لحسن الحظ، العقود المحيطية عديمة الحالة (stateless) ولا تحتفظ بأي أصول، لذلك من السهل إيقاف استخدامها واقتراح استخدام البديل، UniswapV2Router02، بدلاً منها.
UniswapV2Router02.sol
في معظم الحالات، ستستخدم Uniswap من خلال هذا العقد (opens in a new tab). يمكنك معرفة كيفية استخدامه هنا (opens in a new tab).
1pragma solidity =0.6.6;2
3import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol';4import '@uniswap/lib/contracts/libraries/TransferHelper.sol';5
6import './interfaces/IUniswapV2Router02.sol';7import './libraries/UniswapV2Library.sol';8import './libraries/SafeMath.sol';9import './interfaces/IERC20.sol';10import './interfaces/IWETH.sol';معظم هذه العناصر إما واجهناها من قبل، أو أنها واضحة إلى حد ما. الاستثناء الوحيد هو IWETH.sol. يسمح Uniswap v2 بالتبادل لأي زوج من رموز ERC-20، ولكن الإيثر (ETH) نفسه ليس رمز ERC-20. فهو يسبق المعيار ويتم تحويله بآليات فريدة. لتمكين استخدام ETH في العقود التي تنطبق على رموز ERC-20، ابتكر الناس عقد الإيثر المغلف (WETH) (opens in a new tab). ترسل إلى هذا العقد ETH، ويقوم بـ السك او انشاء كمية معادلة من WETH لك. أو يمكنك حرق WETH، واسترداد ETH.
1contract UniswapV2Router02 is IUniswapV2Router02 {2 using SafeMath for uint;3
4 address public immutable override factory;5 address public immutable override WETH;يحتاج الموجه (router) إلى معرفة المصنع (factory) الذي يجب استخدامه، وبالنسبة لـ المعاملات التي تتطلب WETH، ما هو عقد WETH الذي يجب استخدامه. هذه القيم غير قابلة للتغيير (opens in a new tab)، مما يعني أنه لا يمكن تعيينها إلا في دالة البناء (constructor). يمنح هذا المستخدمين الثقة بأنه لن يتمكن أحد من تغييرها للإشارة إلى عقود أقل نزاهة.
1 modifier ensure(uint deadline) {2 require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED');3 _;4 }يتأكد هذا المُعدِّل (modifier) من أن المعاملات المحددة بوقت ("افعل X قبل الوقت Y إن استطعت") لا تحدث بعد انتهاء المهلة الزمنية المحددة لها.
1 constructor(address _factory, address _WETH) public {2 factory = _factory;3 WETH = _WETH;4 }تقوم دالة البناء فقط بتعيين متغيرات الحالة غير القابلة للتغيير.
1 receive() external payable {2 assert(msg.sender == WETH); // only accept ETH via fallback from the WETH contract // قبول ETH فقط عبر الدالة الاحتياطية (fallback) من عقد WETH3 }يتم استدعاء هذه الدالة عندما نسترد الرموز من عقد WETH ونحولها مرة أخرى إلى ETH. عقد WETH الذي نستخدمه هو الوحيد المصرح له بالقيام بذلك.
إضافة السيولة
تضيف هذه الدوال الرموز إلى زوج التبادل، مما يزيد من مجمع السيولة.
1
2 // **** ADD LIQUIDITY **** // **** إضافة سيولة ****3 function _addLiquidity(تُستخدم هذه الدالة لحساب كمية الرموز A و B التي يجب إيداعها في زوج التبادل.
1 address tokenA,2 address tokenB,هذه هي عناوين عقود رموز ERC-20.
1 uint amountADesired,2 uint amountBDesired,هذه هي المبالغ التي يرغب مزود السيولة في إيداعها. وهي أيضًا الحد الأقصى للمبالغ من A و B التي سيتم إيداعها.
1 uint amountAMin,2 uint amountBMinهذه هي الحد الأدنى للمبالغ المقبولة للإيداع. إذا لم يكن من الممكن إتمام المعاملة بهذه المبالغ أو أكثر، فسيتم التراجع عنها. إذا كنت لا تريد هذه الميزة، فما عليك سوى تحديد الصفر.
يحدد مزودو السيولة حدًا أدنى، عادةً، لأنهم يريدون قصر المعاملة على سعر صرف قريب من السعر الحالي. إذا تقلب سعر الصرف كثيرًا، فقد يعني ذلك وجود أخبار تغير القيم الأساسية، ويريدون أن يقرروا يدويًا ما يجب القيام به.
على سبيل المثال، تخيل حالة يكون فيها سعر الصرف واحدًا لواحد ويحدد مزود السيولة هذه القيم:
| المعلمة (Parameter) | القيمة |
|---|---|
| amountADesired | 1000 |
| amountBDesired | 1000 |
| amountAMin | 900 |
| amountBMin | 800 |
طالما بقي سعر الصرف بين 0.9 و 1.25، تتم المعاملة. إذا خرج سعر الصرف عن هذا النطاق، يتم إلغاء المعاملة.
السبب وراء هذا الإجراء الاحترازي هو أن المعاملات ليست فورية، فأنت ترسلها وفي النهاية سيقوم مُدقِّق بتضمينها في بلوك (ما لم يكن سعر الغاز الخاص بك منخفضًا جدًا، وفي هذه الحالة ستحتاج إلى إرسال معاملة أخرى بنفس الرقم العشوائي و سعر الغاز أعلى للكتابة فوقها). لا يمكنك التحكم في ما يحدث خلال الفترة الفاصلة بين الإرسال والتضمين.
1 ) internal virtual returns (uint amountA, uint amountB) {تُرجع الدالة المبالغ التي يجب على مزود السيولة إيداعها للحصول على نسبة مساوية للنسبة الحالية بين الاحتياطيات.
1 // create the pair if it doesn't exist yet // إنشاء الزوج إذا لم يكن موجوداً بعد2 if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {3 IUniswapV2Factory(factory).createPair(tokenA, tokenB);4 }إذا لم يكن هناك تبادل لزوج الرموز هذا بعد، فقم بإنشائه.
1 (uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);احصل على الاحتياطيات الحالية في الزوج.
1 if (reserveA == 0 && reserveB == 0) {2 (amountA, amountB) = (amountADesired, amountBDesired);إذا كانت الاحتياطيات الحالية فارغة، فهذا تبادل زوج جديد. يجب أن تكون المبالغ المراد إيداعها مطابقة تمامًا لتلك التي يريد مزود السيولة تقديمها.
1 } else {2 uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);إذا كنا بحاجة إلى معرفة المبالغ التي ستكون عليها، فإننا نحصل على المبلغ الأمثل باستخدام هذه الدالة (opens in a new tab). نريد نفس النسبة مثل الاحتياطيات الحالية.
1 if (amountBOptimal <= amountBDesired) {2 require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');3 (amountA, amountB) = (amountADesired, amountBOptimal);إذا كان amountBOptimal أصغر من المبلغ الذي يريد مزود السيولة إيداعه، فهذا يعني أن الرمز B أكثر قيمة حاليًا مما يعتقده مودع السيولة، لذلك يلزم مبلغ أصغر.
1 } else {2 uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA);3 assert(amountAOptimal <= amountADesired);4 require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');5 (amountA, amountB) = (amountAOptimal, amountBDesired);إذا كان مبلغ B الأمثل أكبر من مبلغ B المطلوب، فهذا يعني أن رموز B أقل قيمة حاليًا مما يعتقده مودع السيولة، لذلك يلزم مبلغ أعلى. ومع ذلك، فإن المبلغ المطلوب هو الحد الأقصى، لذلك لا يمكننا القيام بذلك. بدلاً من ذلك، نحسب العدد الأمثل من رموز A للمبلغ المطلوب من رموز B.
بجمع كل ذلك معًا نحصل على هذا الرسم البياني. افترض أنك تحاول إيداع ألف من رموز A (الخط الأزرق) وألف من رموز B (الخط الأحمر). المحور السيني (x) هو سعر الصرف، A/B. إذا كان x=1، فهما متساويان في القيمة وتودع ألفًا من كل منهما. إذا كان x=2، فإن A يمثل ضعف قيمة B (تحصل على رمزين من B مقابل كل رمز A) لذلك تودع ألف رمز B، ولكن فقط 500 رمز A. إذا كان x=0.5، فإن الوضع ينعكس، ألف رمز A وخمسمائة رمز B.
يمكنك إيداع السيولة مباشرة في العقد الأساسي (باستخدام UniswapV2Pair::mint (opens in a new tab))، ولكن العقد الأساسي يتحقق فقط من أنه لا يتعرض للاحتيال بنفسه، لذلك فإنك تخاطر بفقدان القيمة إذا تغير سعر الصرف بين الوقت الذي ترسل فيه المعاملة الخاصة بك ووقت تنفيذها. إذا كنت تستخدم العقد المحيطي، فإنه يكتشف المبلغ الذي يجب عليك إيداعه ويودعه على الفور، لذلك لا يتغير سعر الصرف ولا تفقد أي شيء.
1 function addLiquidity(2 address tokenA,3 address tokenB,4 uint amountADesired,5 uint amountBDesired,6 uint amountAMin,7 uint amountBMin,8 address to,9 uint deadlineيمكن استدعاء هذه الدالة بواسطة معاملة لإيداع السيولة. معظم المعلمات هي نفسها كما في _addLiquidity أعلاه، مع استثناءين:
. to هو العنوان الذي يحصل على رموز السيولة الجديدة التي تم السك او انشاء لها لإظهار حصة مزود السيولة في المجمع
. deadline هو حد زمني على المعاملة
1 ) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) {2 (amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);3 address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);نحسب المبالغ التي سيتم إيداعها فعليًا ثم نجد عنوان مجمع السيولة. لتوفير الغاز، لا نقوم بذلك عن طريق سؤال المصنع، ولكن باستخدام دالة المكتبة pairFor (انظر أدناه في المكتبات)
1 TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);2 TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);تحويل المبالغ الصحيحة من الرموز من المستخدم إلى زوج التبادل.
1 liquidity = IUniswapV2Pair(pair).mint(to);2 }في المقابل، امنح العنوان to رموز سيولة لملكية جزئية للمجمع. ترى دالة mint في العقد الأساسي عدد الرموز الإضافية التي يمتلكها (مقارنة بما كان لديه في آخر مرة تغيرت فيها السيولة) وتقوم بـ السك او انشاء السيولة وفقًا لذلك.
1 function addLiquidityETH(2 address token,3 uint amountTokenDesired,عندما يرغب مزود السيولة في توفير السيولة لتبادل زوج رمز/ETH، هناك بعض الاختلافات. يتعامل العقد مع تغليف ETH لمزود السيولة. ليست هناك حاجة لتحديد مقدار ETH الذي يريد المستخدم إيداعه، لأن المستخدم يرسله ببساطة مع المعاملة (المبلغ متاح في msg.value).
1 uint amountTokenMin,2 uint amountETHMin,3 address to,4 uint deadline5 ) external virtual override payable ensure(deadline) returns (uint amountToken, uint amountETH, uint liquidity) {6 (amountToken, amountETH) = _addLiquidity(7 token,8 WETH,9 amountTokenDesired,10 msg.value,11 amountTokenMin,12 amountETHMin13 );14 address pair = UniswapV2Library.pairFor(factory, token, WETH);15 TransferHelper.safeTransferFrom(token, msg.sender, pair, amountToken);16 IWETH(WETH).deposit{value: amountETH}();17 assert(IWETH(WETH).transfer(pair, amountETH));لإيداع ETH، يقوم العقد أولاً بتغليفه إلى WETH ثم ينقل WETH إلى الزوج. لاحظ أن التحويل مغلف بـ assert. هذا يعني أنه إذا فشل التحويل، فإن استدعاء العقد هذا يفشل أيضًا، وبالتالي فإن التغليف لا يحدث حقًا.
1 liquidity = IUniswapV2Pair(pair).mint(to);2 // refund dust eth, if any // استرداد غبار (dust) eth، إن وجد3 if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);4 }لقد أرسل لنا المستخدم بالفعل ETH، لذلك إذا كان هناك أي فائض متبقي (لأن الرمز الآخر أقل قيمة مما اعتقده المستخدم)، فنحن بحاجة إلى إصدار استرداد.
إزالة السيولة
ستقوم هذه الدوال بإزالة السيولة وسداد مزود السيولة.
1 // **** REMOVE LIQUIDITY **** // **** إزالة السيولة ****2 function removeLiquidity(3 address tokenA,4 address tokenB,5 uint liquidity,6 uint amountAMin,7 uint amountBMin,8 address to,9 uint deadline10 ) public virtual override ensure(deadline) returns (uint amountA, uint amountB) {أبسط حالة لإزالة السيولة. هناك حد أدنى من كل رمز يوافق مزود السيولة على قبوله، ويجب أن يحدث ذلك قبل الموعد النهائي.
1 address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);2 IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair // إرسال السيولة إلى الزوج3 (uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);تتعامل دالة burn في العقد الأساسي مع إعادة الرموز إلى المستخدم.
1 (address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB);عندما تُرجع دالة قيمًا متعددة، ولكننا مهتمون ببعضها فقط، فهذه هي الطريقة التي نحصل بها على تلك القيم فقط. إنها أرخص إلى حد ما من حيث الغاز من قراءة قيمة وعدم استخدامها أبدًا.
1 (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);ترجمة المبالغ من الطريقة التي يُرجعها بها العقد الأساسي (الرمز ذو العنوان الأدنى أولاً) إلى الطريقة التي يتوقعها المستخدم (المقابلة لـ tokenA و tokenB).
1 require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');2 require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');3 }لا بأس من إجراء التحويل أولاً ثم التحقق من شرعيته، لأنه إذا لم يكن كذلك، فسنقوم بالتراجع عن جميع تغييرات الحالة.
1 function removeLiquidityETH(2 address token,3 uint liquidity,4 uint amountTokenMin,5 uint amountETHMin,6 address to,7 uint deadline8 ) public virtual override ensure(deadline) returns (uint amountToken, uint amountETH) {9 (amountToken, amountETH) = removeLiquidity(10 token,11 WETH,12 liquidity,13 amountTokenMin,14 amountETHMin,15 address(this),16 deadline17 );18 TransferHelper.safeTransfer(token, to, amountToken);19 IWETH(WETH).withdraw(amountETH);20 TransferHelper.safeTransferETH(to, amountETH);21 }إزالة السيولة لـ ETH هي نفسها تقريبًا، باستثناء أننا نتلقى رموز WETH ثم نستردها مقابل ETH لإعادتها إلى مزود السيولة.
1 function removeLiquidityWithPermit(2 address tokenA,3 address tokenB,4 uint liquidity,5 uint amountAMin,6 uint amountBMin,7 address to,8 uint deadline,9 bool approveMax, uint8 v, bytes32 r, bytes32 s10 ) external virtual override returns (uint amountA, uint amountB) {11 address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);12 uint value = approveMax ? uint(-1) : liquidity;13 IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);14 (amountA, amountB) = removeLiquidity(tokenA, tokenB, liquidity, amountAMin, amountBMin, to, deadline);15 }16
17 function removeLiquidityETHWithPermit(18 address token,19 uint liquidity,20 uint amountTokenMin,21 uint amountETHMin,22 address to,23 uint deadline,24 bool approveMax, uint8 v, bytes32 r, bytes32 s25 ) external virtual override returns (uint amountToken, uint amountETH) {26 address pair = UniswapV2Library.pairFor(factory, token, WETH);27 uint value = approveMax ? uint(-1) : liquidity;28 IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);29 (amountToken, amountETH) = removeLiquidityETH(token, liquidity, amountTokenMin, amountETHMin, to, deadline);30 }تقوم هذه الدوال بترحيل المعاملات الوصفية (meta-transactions) للسماح للمستخدمين الذين ليس لديهم إيثر بالسحب من المجمع، باستخدام آلية التصريح (permit).
1
2 // **** REMOVE LIQUIDITY (supporting fee-on-transfer tokens) **** // **** إزالة السيولة (دعم الرموز ذات الرسوم عند التحويل) ****3 function removeLiquidityETHSupportingFeeOnTransferTokens(4 address token,5 uint liquidity,6 uint amountTokenMin,7 uint amountETHMin,8 address to,9 uint deadline10 ) public virtual override ensure(deadline) returns (uint amountETH) {11 (, amountETH) = removeLiquidity(12 token,13 WETH,14 liquidity,15 amountTokenMin,16 amountETHMin,17 address(this),18 deadline19 );20 TransferHelper.safeTransfer(token, to, IERC20(token).balanceOf(address(this)));21 IWETH(WETH).withdraw(amountETH);22 TransferHelper.safeTransferETH(to, amountETH);23 }24
يمكن استخدام هذه الدالة للرموز التي لها رسوم تحويل أو تخزين. عندما يكون للرمز مثل هذه الرسوم، لا يمكننا الاعتماد على دالة removeLiquidity لإخبارنا بمقدار الرمز الذي نستردّه، لذلك نحتاج إلى السحب أولاً ثم الحصول على الرصيد.
1
2 function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens(3 address token,4 uint liquidity,5 uint amountTokenMin,6 uint amountETHMin,7 address to,8 uint deadline,9 bool approveMax, uint8 v, bytes32 r, bytes32 s10 ) external virtual override returns (uint amountETH) {11 address pair = UniswapV2Library.pairFor(factory, token, WETH);12 uint value = approveMax ? uint(-1) : liquidity;13 IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);14 amountETH = removeLiquidityETHSupportingFeeOnTransferTokens(15 token, liquidity, amountTokenMin, amountETHMin, to, deadline16 );17 }تجمع الدالة النهائية بين رسوم التخزين والمعاملات الوصفية.
التداول
1 // **** SWAP **** // **** تبادل ****2 // requires the initial amount to have already been sent to the first pair // يتطلب أن يكون المبلغ الأولي قد تم إرساله بالفعل إلى الزوج الأول3 function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual {تؤدي هذه الدالة المعالجة الداخلية المطلوبة للدوال المعروضة للمتداولين.
1 for (uint i; i < path.length - 1; i++) {أثناء كتابتي لهذا، يوجد 388,160 رمز ERC-20 (opens in a new tab). إذا كان هناك تبادل زوجي لكل زوج من الرموز، فسيكون هناك أكثر من 150 مليار تبادل زوجي. السلسلة بأكملها، في الوقت الحالي، تحتوي فقط على 0.1% من هذا العدد من الحسابات (opens in a new tab). بدلاً من ذلك، تدعم دوال المبادلة مفهوم المسار. يمكن للمتداول استبدال A بـ B، و B بـ C، و C بـ D، لذلك ليست هناك حاجة لتبادل زوجي مباشر بين A و D.
تميل الأسعار في هذه الأسواق إلى أن تكون متزامنة، لأنه عندما تكون غير متزامنة فإنها تخلق فرصة للمراجحة (arbitrage). تخيل، على سبيل المثال، ثلاثة رموز، A و B و C. هناك ثلاثة تبادلات زوجية، واحد لكل زوج.
- الوضع الأولي
- يبيع المتداول 24.695 رمز A ويحصل على 25.305 رمز B.
- يبيع المتداول 24.695 رمز B مقابل 25.305 رمز C، ويحتفظ بحوالي 0.61 رمز B كربح.
- ثم يبيع المتداول 24.695 رمز C مقابل 25.305 رمز A، ويحتفظ بحوالي 0.61 رمز C كربح. يمتلك المتداول أيضًا 0.61 رمز A إضافي (الـ 25.305 التي ينتهي بها المتداول، ناقص الاستثمار الأصلي البالغ 24.695).
| الخطوة | تبادل A-B | تبادل B-C | تبادل A-C |
|---|---|---|---|
| 1 | A:1000 B:1050 A/B=1.05 | B:1000 C:1050 B/C=1.05 | A:1050 C:1000 C/A=1.05 |
| 2 | A:1024.695 B:1024.695 A/B=1 | B:1000 C:1050 B/C=1.05 | A:1050 C:1000 C/A=1.05 |
| 3 | A:1024.695 B:1024.695 A/B=1 | B:1024.695 C:1024.695 B/C=1 | A:1050 C:1000 C/A=1.05 |
| 4 | A:1024.695 B:1024.695 A/B=1 | B:1024.695 C:1024.695 B/C=1 | A:1024.695 C:1024.695 C/A=1 |
1 (address input, address output) = (path[i], path[i + 1]);2 (address token0,) = UniswapV2Library.sortTokens(input, output);3 uint amountOut = amounts[i + 1];احصل على الزوج الذي نتعامل معه حاليًا، وقم بفرزه (للاستخدام مع الزوج) واحصل على كمية المخرجات المتوقعة.
1 (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));احصل على كميات المخرجات المتوقعة، مرتبة بالطريقة التي يتوقعها تبادل الزوج.
1 address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;هل هذا هو التبادل الأخير؟ إذا كان الأمر كذلك، فأرسل الرموز المستلمة للتداول إلى الوجهة. إذا لم يكن كذلك، فأرسلها إلى تبادل الزوج التالي.
1
2 IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap(3 amount0Out, amount1Out, to, new bytes(0)4 );5 }6 }قم فعليًا باستدعاء تبادل الزوج لـ مبادلة الرموز. لا نحتاج إلى استدعاء عكسي (callback) لإبلاغنا بالتبادل، لذلك لا نرسل أي بايتات في هذا الحقل.
1 function swapExactTokensForTokens(تُستخدم هذه الدالة مباشرة من قبل المتداولين لـ مبادلة رمز بآخر.
1 uint amountIn,2 uint amountOutMin,3 address[] calldata path,تحتوي هذه المعلمة على عناوين عقود ERC-20. كما هو موضح أعلاه، هذه مصفوفة لأنك قد تحتاج إلى المرور عبر عدة تبادلات زوجية للانتقال من الأصل الذي تملكه إلى الأصل الذي تريده.
يمكن تخزين معلمة الدالة في Solidity إما في memory أو calldata. إذا كانت الدالة نقطة دخول للعقد، ويتم استدعاؤها مباشرة من مستخدم (باستخدام معاملة) أو من عقد مختلف، فيمكن أخذ قيمة المعلمة مباشرة من بيانات الاستدعاء (call data). إذا تم استدعاء الدالة داخليًا، مثل _swap أعلاه، فيجب تخزين المعلمات في memory. من منظور العقد المستدعى، تكون calldata للقراءة فقط.
مع الأنواع العددية (scalar types) مثل uint أو address، يتعامل المترجم (compiler) مع اختيار التخزين نيابة عنا، ولكن مع المصفوفات، التي تكون أطول وأكثر تكلفة، نحدد نوع التخزين الذي سيتم استخدامه.
1 address to,2 uint deadline3 ) external virtual override ensure(deadline) returns (uint[] memory amounts) {يتم دائمًا إرجاع القيم المرجعة في الذاكرة (memory).
1 amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);2 require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');احسب المبلغ الذي سيتم شراؤه في كل مبادلة. إذا كانت النتيجة أقل من الحد الأدنى الذي يرغب المتداول في قبوله، فتراجع عن المعاملة.
1 TransferHelper.safeTransferFrom(2 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]3 );4 _swap(amounts, path, to);5 }أخيرًا، قم بتحويل رمز ERC-20 الأولي إلى الحساب لتبادل الزوج الأول واستدعِ _swap. يحدث كل هذا في نفس المعاملة، لذلك يعرف تبادل الزوج أن أي رموز غير متوقعة هي جزء من هذا التحويل.
1 function swapTokensForExactTokens(2 uint amountOut,3 uint amountInMax,4 address[] calldata path,5 address to,6 uint deadline7 ) external virtual override ensure(deadline) returns (uint[] memory amounts) {8 amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);9 require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');10 TransferHelper.safeTransferFrom(11 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]12 );13 _swap(amounts, path, to);14 }تسمح الدالة السابقة، swapTokensForTokens، للمتداول بتحديد عدد دقيق من رموز الإدخال التي يرغب في تقديمها والحد الأدنى لعدد رموز الإخراج التي يرغب في تلقيها في المقابل. تقوم هذه الدالة بـ مبادلة عكسية، فهي تتيح للمتداول تحديد عدد رموز الإخراج التي يريدها، والحد الأقصى لعدد رموز الإدخال التي يرغب في دفعها مقابلها.
في كلتا الحالتين، يجب على المتداول أن يمنح هذا العقد المحيطي أولاً مخصصًا (allowance) للسماح له بتحويلها.
1 function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline)2 external3 virtual4 override5 payable6 ensure(deadline)7 returns (uint[] memory amounts)8 {9 require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');10 amounts = UniswapV2Library.getAmountsOut(factory, msg.value, path);11 require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');12 IWETH(WETH).deposit{value: amounts[0]}();13 assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));14 _swap(amounts, path, to);15 }16
17 function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline)18 external19 virtual20 override21 ensure(deadline)22 returns (uint[] memory amounts)23 {24 require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');25 amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);26 require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');27 TransferHelper.safeTransferFrom(28 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]29 );30 _swap(amounts, path, address(this));31 IWETH(WETH).withdraw(amounts[amounts.length - 1]);32 TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);33 }34
35 function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)36 external37 virtual38 override39 ensure(deadline)40 returns (uint[] memory amounts)41 {42 require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');43 amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);44 require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');45 TransferHelper.safeTransferFrom(46 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]47 );48 _swap(amounts, path, address(this));49 IWETH(WETH).withdraw(amounts[amounts.length - 1]);50 TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);51 }52
53 function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline)54 external55 virtual56 override57 payable58 ensure(deadline)59 returns (uint[] memory amounts)60 {61 require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');62 amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);63 require(amounts[0] <= msg.value, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');64 IWETH(WETH).deposit{value: amounts[0]}();65 assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));66 _swap(amounts, path, to);67 // refund dust eth, if any // استرداد غبار (dust) eth، إن وجد68 if (msg.value > amounts[0]) TransferHelper.safeTransferETH(msg.sender, msg.value - amounts[0]);69 }تتضمن هذه المتغيرات الأربعة جميعها التداول بين ETH والرموز. الاختلاف الوحيد هو أننا إما نتلقى ETH من المتداول ونستخدمه لـ السك او انشاء WETH، أو نتلقى WETH من التبادل الأخير في المسار ونحرقه، ونعيد ETH الناتج إلى المتداول.
1 // **** SWAP (supporting fee-on-transfer tokens) **** // **** تبادل (دعم الرموز ذات الرسوم عند التحويل) ****2 // requires the initial amount to have already been sent to the first pair // يتطلب أن يكون المبلغ الأولي قد تم إرساله بالفعل إلى الزوج الأول3 function _swapSupportingFeeOnTransferTokens(address[] memory path, address _to) internal virtual {هذه هي الدالة الداخلية لـ مبادلة الرموز التي لها رسوم تحويل أو تخزين لحل (هذه المشكلة (opens in a new tab)).
1 for (uint i; i < path.length - 1; i++) {2 (address input, address output) = (path[i], path[i + 1]);3 (address token0,) = UniswapV2Library.sortTokens(input, output);4 IUniswapV2Pair pair = IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output));5 uint amountInput;6 uint amountOutput;7 { // scope to avoid stack too deep errors // نطاق لتجنب أخطاء المكدس العميق جداً8 (uint reserve0, uint reserve1,) = pair.getReserves();9 (uint reserveInput, uint reserveOutput) = input == token0 ? (reserve0, reserve1) : (reserve1, reserve0);10 amountInput = IERC20(input).balanceOf(address(pair)).sub(reserveInput);11 amountOutput = UniswapV2Library.getAmountOut(amountInput, reserveInput, reserveOutput);بسبب رسوم التحويل، لا يمكننا الاعتماد على دالة getAmountsOut لإخبارنا بمقدار ما نحصل عليه من كل تحويل (بالطريقة التي نفعلها قبل استدعاء _swap الأصلي). بدلاً من ذلك، يتعين علينا التحويل أولاً ثم معرفة عدد الرموز التي استرددناها.
ملاحظة: من الناحية النظرية، يمكننا فقط استخدام هذه الدالة بدلاً من _swap، ولكن في حالات معينة (على سبيل المثال، إذا انتهى الأمر بالتراجع عن التحويل لعدم وجود ما يكفي في النهاية لتلبية الحد الأدنى المطلوب) فإن ذلك سيكلف المزيد من الغاز. رموز رسوم التحويل نادرة جدًا، لذلك بينما نحتاج إلى استيعابها، ليست هناك حاجة لجميع عمليات المبادلة لافتراض أنها تمر عبر واحد منها على الأقل.
1 }2 (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOutput) : (amountOutput, uint(0));3 address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;4 pair.swap(amount0Out, amount1Out, to, new bytes(0));5 }6 }7
8 function swapExactTokensForTokensSupportingFeeOnTransferTokens(9 uint amountIn,10 uint amountOutMin,11 address[] calldata path,12 address to,13 uint deadline14 ) external virtual override ensure(deadline) {15 TransferHelper.safeTransferFrom(16 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn17 );18 uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);19 _swapSupportingFeeOnTransferTokens(path, to);20 require(21 IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin,22 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'23 );24 }25
26 function swapExactETHForTokensSupportingFeeOnTransferTokens(27 uint amountOutMin,28 address[] calldata path,29 address to,30 uint deadline31 )32 external33 virtual34 override35 payable36 ensure(deadline)37 {38 require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');39 uint amountIn = msg.value;40 IWETH(WETH).deposit{value: amountIn}();41 assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn));42 uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);43 _swapSupportingFeeOnTransferTokens(path, to);44 require(45 IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin,46 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'47 );48 }49
50 function swapExactTokensForETHSupportingFeeOnTransferTokens(51 uint amountIn,52 uint amountOutMin,53 address[] calldata path,54 address to,55 uint deadline56 )57 external58 virtual59 override60 ensure(deadline)61 {62 require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');63 TransferHelper.safeTransferFrom(64 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn65 );66 _swapSupportingFeeOnTransferTokens(path, address(this));67 uint amountOut = IERC20(WETH).balanceOf(address(this));68 require(amountOut >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');69 IWETH(WETH).withdraw(amountOut);70 TransferHelper.safeTransferETH(to, amountOut);71 }هذه هي نفس المتغيرات المستخدمة للرموز العادية، ولكنها تستدعي _swapSupportingFeeOnTransferTokens بدلاً من ذلك.
1 // **** LIBRARY FUNCTIONS **** // **** دوال المكتبة ****2 function quote(uint amountA, uint reserveA, uint reserveB) public pure virtual override returns (uint amountB) {3 return UniswapV2Library.quote(amountA, reserveA, reserveB);4 }5
6 function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut)7 public8 pure9 virtual10 override11 returns (uint amountOut)12 {13 return UniswapV2Library.getAmountOut(amountIn, reserveIn, reserveOut);14 }15
16 function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut)17 public18 pure19 virtual20 override21 returns (uint amountIn)22 {23 return UniswapV2Library.getAmountIn(amountOut, reserveIn, reserveOut);24 }25
26 function getAmountsOut(uint amountIn, address[] memory path)27 public28 view29 virtual30 override31 returns (uint[] memory amounts)32 {33 return UniswapV2Library.getAmountsOut(factory, amountIn, path);34 }35
36 function getAmountsIn(uint amountOut, address[] memory path)37 public38 view39 virtual40 override41 returns (uint[] memory amounts)42 {43 return UniswapV2Library.getAmountsIn(factory, amountOut, path);44 }45}هذه الدوال هي مجرد وكلاء (proxies) تستدعي دوال UniswapV2Library.
UniswapV2Migrator.sol
تم استخدام هذا العقد لترحيل التبادلات من الإصدار القديم v1 إلى v2. الآن بعد أن تم ترحيلها، لم يعد ذا صلة.
المكتبات
مكتبة SafeMath (opens in a new tab) موثقة جيدًا، لذا لا توجد حاجة لتوثيقها هنا.
الرياضيات
تحتوي هذه المكتبة على بعض الدوال الرياضية التي لا يُحتاج إليها عادةً في كود Solidity، لذا فهي ليست جزءًا من اللغة.
1pragma solidity =0.5.16;2
3// a library for performing various math operations // مكتبة لإجراء عمليات رياضية مختلفة4
5library Math {6 function min(uint x, uint y) internal pure returns (uint z) {7 z = x < y ? x : y;8 }9
10 // babylonian method (https://wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method) // الطريقة البابلية (https://wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method)11 function sqrt(uint y) internal pure returns (uint z) {12 if (y > 3) {13 z = y;14 uint x = y / 2 + 1;ابدأ بـ x كتقدير أعلى من الجذر التربيعي (وهذا هو السبب في أننا بحاجة إلى التعامل مع 1-3 كحالات خاصة).
1 while (x < z) {2 z = x;3 x = (y / x + x) / 2;احصل على تقدير أقرب، وهو متوسط التقدير السابق والرقم الذي نحاول إيجاد جذره التربيعي مقسومًا على التقدير السابق. كرر ذلك حتى لا يكون التقدير الجديد أقل من التقدير الحالي. لمزيد من التفاصيل، انظر هنا (opens in a new tab).
1 }2 } else if (y != 0) {3 z = 1;لا ينبغي أن نحتاج أبدًا إلى الجذر التربيعي للصفر. الجذور التربيعية للواحد، والاثنين، والثلاثة هي تقريبًا واحد (نحن نستخدم أعدادًا صحيحة، لذا نتجاهل الكسر).
1 }2 }3}كسور النقطة الثابتة (UQ112x112)
تتعامل هذه المكتبة مع الكسور، والتي لا تكون عادةً جزءًا من العمليات الحسابية في إيثريوم. وتقوم بذلك عن طريق تشفير الرقم x كـ x*2^112. يتيح لنا هذا استخدام أكواد عمليات الجمع والطرح الأصلية دون تغيير.
1pragma solidity =0.5.16;2
3// a library for handling binary fixed point numbers (https://wikipedia.org/wiki/Q_(number_format)) // مكتبة للتعامل مع أرقام النقطة الثابتة الثنائية (https://wikipedia.org/wiki/Q_(number_format))4
5// range: [0, 2**112 - 1] // النطاق: [0, 2**112 - 1]6// resolution: 1 / 2**112 // الدقة: 1 / 2**1127
8library UQ112x112 {9 uint224 constant Q112 = 2**112;Q112 هو التشفير للرقم واحد.
1 // encode a uint112 as a UQ112x112 // تشفير uint112 كـ UQ112x1122 function encode(uint112 y) internal pure returns (uint224 z) {3 z = uint224(y) * Q112; // never overflows // لا يطفح أبداً4 }نظرًا لأن y هو uint112، فإن أقصى قيمة يمكن أن يصل إليها هي 2^112-1. لا يزال من الممكن تشفير هذا الرقم كـ UQ112x112.
1 // divide a UQ112x112 by a uint112, returning a UQ112x112 // قسمة UQ112x112 على uint112، وإرجاع UQ112x1122 function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) {3 z = x / uint224(y);4 }5}إذا قسمنا قيمتين من نوع UQ112x112، فلن يتم ضرب النتيجة في 2^112. لذا بدلاً من ذلك نأخذ عددًا صحيحًا للمقام. كنا سنحتاج إلى استخدام حيلة مشابهة للقيام بعملية الضرب، لكننا لا نحتاج إلى إجراء عملية ضرب لقيم UQ112x112.
مكتبة UniswapV2Library
تُستخدم هذه المكتبة فقط بواسطة العقود المحيطية
1pragma solidity >=0.5.0;2
3import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol';4
5import "./SafeMath.sol";6
7library UniswapV2Library {8 using SafeMath for uint;9
10 // returns sorted token addresses, used to handle return values from pairs sorted in this order // يُرجع عناوين الرموز مرتبة، ويُستخدم للتعامل مع القيم المرجعة من الأزواج المرتبة بهذا الترتيب11 function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) {12 require(tokenA != tokenB, 'UniswapV2Library: IDENTICAL_ADDRESSES');13 (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);14 require(token0 != address(0), 'UniswapV2Library: ZERO_ADDRESS');15 }قم بفرز الرمزين حسب العنوان، حتى نتمكن من الحصول على عنوان منصة التبادل للزوج الخاص بهما. هذا ضروري لأنه بخلاف ذلك سيكون لدينا احتمالان، أحدهما للمعاملات A,B والآخر للمعاملات B,A، مما يؤدي إلى منصتي تبادل بدلاً من واحدة.
1 // calculates the CREATE2 address for a pair without making any external calls // يحسب عنوان CREATE2 لزوج دون إجراء أي استدعاءات خارجية2 function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) {3 (address token0, address token1) = sortTokens(tokenA, tokenB);4 pair = address(uint(keccak256(abi.encodePacked(5 hex'ff',6 factory,7 keccak256(abi.encodePacked(token0, token1)),8 hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f' // init code hash // التجزئة (هاش) لكود التهيئة9 ))));10 }تحسب هذه الدالة عنوان منصة التبادل للزوج الخاص بالرمزين. يتم إنشاء هذا العقد باستخدام كود العملية CREATE2 (opens in a new tab)، لذا يمكننا حساب العنوان باستخدام نفس الخوارزمية إذا كنا نعرف المعاملات التي يستخدمها. هذا أرخص بكثير من سؤال المصنع (factory)، و
1 // fetches and sorts the reserves for a pair // يجلب ويرتب الاحتياطيات لزوج2 function getReserves(address factory, address tokenA, address tokenB) internal view returns (uint reserveA, uint reserveB) {3 (address token0,) = sortTokens(tokenA, tokenB);4 (uint reserve0, uint reserve1,) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves();5 (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0);6 }تُرجع هذه الدالة احتياطيات الرمزين التي تمتلكها منصة التبادل للزوج. لاحظ أنه يمكنها استقبال الرموز بأي ترتيب، وتقوم بفرزها للاستخدام الداخلي.
1 // given some amount of an asset and pair reserves, returns an equivalent amount of the other asset // بناءً على كمية معينة من أصل واحتياطيات الزوج، يُرجع كمية مكافئة من الأصل الآخر2 function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) {3 require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT');4 require(reserveA > 0 && reserveB > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');5 amountB = amountA.mul(reserveB) / reserveA;6 }تمنحك هذه الدالة كمية الرمز B التي ستحصل عليها مقابل الرمز A إذا لم تكن هناك رسوم متضمنة. يأخذ هذا الحساب في الاعتبار أن التحويل يغير سعر الصرف.
1 // given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset // بناءً على كمية إدخال من أصل واحتياطيات الزوج، يُرجع أقصى كمية إخراج من الأصل الآخر2 function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {تعمل دالة quote أعلاه بشكل رائع إذا لم تكن هناك رسوم لاستخدام منصة التبادل للزوج. ومع ذلك، إذا كانت هناك رسوم تبادل بنسبة 0.3%، فإن المبلغ الذي تحصل عليه فعليًا يكون أقل. تحسب هذه الدالة المبلغ بعد رسوم التبادل.
1
2 require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT');3 require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');4 uint amountInWithFee = amountIn.mul(997);5 uint numerator = amountInWithFee.mul(reserveOut);6 uint denominator = reserveIn.mul(1000).add(amountInWithFee);7 amountOut = numerator / denominator;8 }لا تتعامل Solidity مع الكسور بشكل أصلي، لذا لا يمكننا ببساطة ضرب المبلغ في 0.997. بدلاً من ذلك، نضرب البسط في 997 والمقام في 1000، مما يحقق نفس التأثير.
1 // given an output amount of an asset and pair reserves, returns a required input amount of the other asset // بناءً على كمية إخراج من أصل واحتياطيات الزوج، يُرجع كمية الإدخال المطلوبة من الأصل الآخر2 function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) internal pure returns (uint amountIn) {3 require(amountOut > 0, 'UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT');4 require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');5 uint numerator = reserveIn.mul(amountOut).mul(1000);6 uint denominator = reserveOut.sub(amountOut).mul(997);7 amountIn = (numerator / denominator).add(1);8 }تقوم هذه الدالة بنفس الشيء تقريبًا، لكنها تحصل على كمية المخرجات وتوفر المدخلات.
1
2 // performs chained getAmountOut calculations on any number of pairs // يُجري حسابات getAmountOut المتسلسلة على أي عدد من الأزواج3 function getAmountsOut(address factory, uint amountIn, address[] memory path) internal view returns (uint[] memory amounts) {4 require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');5 amounts = new uint[](path.length);6 amounts[0] = amountIn;7 for (uint i; i < path.length - 1; i++) {8 (uint reserveIn, uint reserveOut) = getReserves(factory, path[i], path[i + 1]);9 amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut);10 }11 }12
13 // performs chained getAmountIn calculations on any number of pairs // يُجري حسابات getAmountIn المتسلسلة على أي عدد من الأزواج14 function getAmountsIn(address factory, uint amountOut, address[] memory path) internal view returns (uint[] memory amounts) {15 require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');16 amounts = new uint[](path.length);17 amounts[amounts.length - 1] = amountOut;18 for (uint i = path.length - 1; i > 0; i--) {19 (uint reserveIn, uint reserveOut) = getReserves(factory, path[i - 1], path[i]);20 amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut);21 }22 }23}تتعامل هاتان الدالتان مع تحديد القيم عندما يكون من الضروري المرور عبر عدة منصات تبادل للأزواج.
مساعد التحويل (Transfer Helper)
تضيف هذه المكتبة (opens in a new tab) فحوصات نجاح حول تحويلات ERC-20 وإيثريوم للتعامل مع التراجع (revert) وإرجاع القيمة false بنفس الطريقة.
1// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later2
3pragma solidity >=0.6.0;4
5// helper methods for interacting with ERC20 tokens and sending ETH that do not consistently return true/false // طرق مساعدة للتفاعل مع رموز ERC20 وإرسال ETH التي لا تُرجع true/false بشكل ثابت6library TransferHelper {7 function safeApprove(8 address token,9 address to,10 uint256 value11 ) internal {12 // bytes4(keccak256(bytes('approve(address,uint256)'))); // bytes4(keccak256(bytes('approve(address,uint256)')));13 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x095ea7b3, to, value));14
يمكننا استدعاء عقد مختلف بإحدى طريقتين:
- استخدام تعريف الواجهة (interface) لإنشاء استدعاء دالة
- استخدام واجهة التطبيق الثنائية (ABI) (opens in a new tab) "يدويًا" لإنشاء الاستدعاء. هذا ما قرر مؤلف الكود القيام به.
1 require(2 success && (data.length == 0 || abi.decode(data, (bool))),3 'TransferHelper::safeApprove: approve failed'4 );5 }من أجل التوافق مع الإصدارات السابقة للرموز التي تم إنشاؤها قبل معيار ERC-20، يمكن أن يفشل استدعاء ERC-20 إما عن طريق التراجع (وفي هذه الحالة تكون success هي false) أو من خلال كونه ناجحًا وإرجاع قيمة false (وفي هذه الحالة توجد بيانات مخرجات، وإذا قمت بفك تشفيرها كقيمة منطقية ستحصل على false).
1
2 function safeTransfer(3 address token,4 address to,5 uint256 value6 ) internal {7 // bytes4(keccak256(bytes('transfer(address,uint256)'))); // bytes4(keccak256(bytes('transfer(address,uint256)')));8 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb, to, value));9 require(10 success && (data.length == 0 || abi.decode(data, (bool))),11 'TransferHelper::safeTransfer: transfer failed'12 );13 }تنفذ هذه الدالة وظيفة التحويل (transfer) الخاصة بـ ERC-20 (opens in a new tab)، والتي تسمح لحساب بإنفاق المخصصات المقدمة من حساب مختلف.
1
2 function safeTransferFrom(3 address token,4 address from,5 address to,6 uint256 value7 ) internal {8 // bytes4(keccak256(bytes('transferFrom(address,address,uint256)'))); // bytes4(keccak256(bytes('transferFrom(address,address,uint256)')));9 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, value));10 require(11 success && (data.length == 0 || abi.decode(data, (bool))),12 'TransferHelper::transferFrom: transferFrom failed'13 );14 }تنفذ هذه الدالة وظيفة transferFrom الخاصة بـ ERC-20 (opens in a new tab)، والتي تسمح لحساب بإنفاق المخصصات المقدمة من حساب مختلف.
1
2 function safeTransferETH(address to, uint256 value) internal {3 (bool success, ) = to.call{value: value}(new bytes(0));4 require(success, 'TransferHelper::safeTransferETH: ETH transfer failed');5 }6}تقوم هذه الدالة بتحويل الإيثر (ether) إلى حساب. يمكن لأي استدعاء لعقد مختلف أن يحاول إرسال الإيثر. نظرًا لأننا لا نحتاج فعليًا إلى استدعاء أي دالة، فإننا لا نرسل أي بيانات مع الاستدعاء.
الخاتمة
هذا مقال طويل يتكون من حوالي 50 صفحة. إذا وصلت إلى هنا، فتهانينا! نأمل أن تكون قد فهمت الآن الاعتبارات المتعلقة بكتابة تطبيق واقعي (على عكس البرامج النموذجية القصيرة) وأن تكون أكثر قدرة على كتابة العقود لحالات الاستخدام الخاصة بك.
الآن اذهب واكتب شيئًا مفيدًا وأبهرنا.
انظر هنا للمزيد من أعمالي (opens in a new tab).
آخر تحديث للصفحة: 3 مارس 2026
