بعد إطلاق Uniswap v4 على الشبكة الرئيسية، أصبحت آلية Hook واحدة من أبرز الابتكارات في مجال التمويل اللامركزي. يستخدم منصة إصدار الميمكويين على سلسلة Base، Flaunch، Hook لتحقيق سعر مسبق ثابت وآلية تسوية تلقائية عند الإطلاق؛ كما يستخدم بروتوكول السيولة Bunni v2 Hook لبناء نموذج سيولة قابلة للبرمجة ونموذج إعادة رهن؛ كما شهدت عملات مثل SATO وuPEG (Unipeg) وSlonks، التي تدور حول مفاهيم Hook، ارتفاعات تصل إلى عشرات المرات خلال فترة قصيرة هذا العام.
في الجانب الآخر من ازدهار نظام Hook، تزداد الهجمات المستهدفة لعيوب تنفيذ Hook بشكل ملحوظ. ستتناول هذه المقالة آلية Hook في Uniswap v4، وتحلل تدريجيًا طبقة الاستدعاء الأساسية، لمساعدة مشاريع فهم الثغرات المحتملة فيها.
أمان Uniswap v4 Hook
1. مقدمة
أبرز تغيير في البنية بين Uniswap v4 وv3 هو إدخال آلية الـ Hook: التي تسمح للمطورين بربط عقود مخصصة بأحداث دورة حياة حوض السيولة، وحقن أي منطق في نقاط مثل التبادل، وإضافة أو إزالة السيولة، والتهيئة.
التعديلات الرئيسية في الإصدار v4 هي كالتالي:
نمط Singleton: يتم إدارة حالة جميع الحقول بواسطة عقد PoolManager واحد مركزي، بدلاً من نشر عقد منفصل لكل حقل.
- المحاسبة الفورية: يتم تسجيل التغييرات في الأرصدة الوسيطة أثناء عملية التداول فقط في التخزين المؤقت، ويتم التسوية الموحدة فقط عند انتهاء التداول.
- آلية Hook: يمكن ربط كل حوض بعقد Hook، وسيقوم PoolManager بدعوة العقد في النقاط الرئيسية (beforeInitialize، beforeSwap، afterAddLiquidity، إلخ)
- لا يمكن تغيير Hook: بمجرد إكمال تهيئة الحوض، يُثبَّت عنوان Hook المرتبط بشكل دائم (لا يمكن تعديل عنوان Hook المرتبط بالحوض، لكن ما إذا كان عقد Hook نفسه قابلًا للترقية يعتمد على طريقة تنفيذه)

في فترة v3، كان على المطورين الاعتماد فقط على بروتوكول Uniswap نفسه؛ أما في فترة v4، فإن أمان كل حوض يعتمد على الـ Hook المرتبط به. لقد حوّلت الـ Hooks AMM من عنصر مالي ثابت إلى بنية تحتية مالية قابلة للبرمجة، لكن نموذج الأمان تحول من "مستوى البروتوكول" إلى "مستوى الحوض".
2. هيكل Hook
2.1 PoolManager ونموذج unlock/callback
العقد الأساسي في الإصدار v4 هو PoolManager ككائن فريد. أي عملية تغيير حالة الصندوق (التبادل، إضافة أو طرح السيولة) يجب أن تبدأ أولاً باستدعاء PoolManager.unlock() للحصول على إذن استدعاء واحد، ثم إكمال الإجراءات المحددة داخل unlockCallback(). في نهاية العملية، يتحقق PoolManager من توازن الدفتر المحاسبي:

عندما يكون NonzeroDeltaCount != 0، يتم التراجع مباشرة؛ هذا هو القيود الأساسية لمحاسبة الفلاش v4. يمكن لأي Hook أن يخلق عدم توازن مؤقت في الحسابات أثناء التنفيذ، لكنه يجب أن يُسوي الحسابات بنفسه قبل انتهاء المعاملة، وإلا ستُراجع المعاملة بالكامل.
يتم تحديد كل حوض بواسطة هيكل PoolKey، والذي يحتوي على حقل hooks:

يتم حساب PoolId باستخدام keccak256(PoolKey)، لذا فإن عناوين hooks المختلفة تنتج أحواضًا مختلفة. وهذا يعني أيضًا أن PoolManager لا يتحقق مما إذا كان عنوان Hook معين قد استُخدم سابقًا في حوض آخر، ويمكن ربط عقد Hook واحد مع عدة أحواض في نفس الوقت.

2.2 ترميز أذونات Hook في العنوان
إحدى التصاميم غير البديهية في الإصدار v4 هي أن صلاحيات الـ Hook لا تُحدَّد بواسطة متغير داخلي في العقد، بل تُحدَّد بواسطة عنوان نشر عقد الـ Hook.
يقوم PoolManager بفحص البتات الأقل 14 من عنوان Hook لتحديد ما إذا كان يجب استدعاء هذا Hook في نقطة معينة من دورة الحياة:

على سبيل المثال، BEFORE_SWAP_FLAG = 1 << 7. إذا كانت البتة السابعة في عنوان Hook تساوي 1، فسيقوم PoolManager باستدعاء beforeSwap() للـ Hook قبل عملية swap؛ وإلا، فحتى لو قام عقد Hook بتنفيذ beforeSwap()، فلن يتم استدعاؤه أبدًا من قبل PoolManager.
هذا يعني أنه عند نشر Hook، يجب حساب العنوان باستخدام CREATE2 + salt لإنشاء عنوان يتطابق تمامًا مع الصلاحيات المستهدفة. توفر Uniswap أداة HookMiner لهذا الغرض:

عند عدم توافق أوضاع الصلاحيات مع تنفيذ الدوال، تنشأ نوعان من المشكلات:
(1) تم تنفيذ دالة hook، لكن العنوان لم يتم ترميزه لتمثيل حقوق الصلاحية — لن يُستدعى PoolManager هذه الدالة أبدًا، وبالتالي المنطق غير فعال
(2) تم ترميز العنوان ببت صلاحية، لكن الـ hook لم يُنفّذ الدالة المقابلة — قد يحدث إعادة تراجع في PoolManager عند الاستدعاء، مما يؤدي إلى هجوم DOS أو فشل في التحقق من القيمة المعادة، مما يمنع تنفيذ العمليات ذات الصلة.
هذا أيضًا عائق طبيعي لترقية Hook: إذا كان يمكن ترقية Hook عبر وكيل، فعنوان النشر لا يتغير أثناء الترقية، وبالتالي بعد الترقية، لا يمكن سوى تعديل تنفيذ وظائف Hook الموجودة، وليس إضافة أنواع جديدة من Hook. لضمان قدرة التوسع في المستقبل، يجب افتتاح جميع حقوق الاستخدام المحتملة منذ النشر الأولي.
2.3 BaseHook وفخ تحكم في الوصول تم تجاهله على نطاق واسع
العقد المجرد BaseHook المقدم من Periphery لإصدار Uniswap v4، يمكن للمطورين أن يرثوه لتنفيذ Hook مخصص. أحد الأدوار المهمة لـ BaseHook هو توفير مُعدِّل onlyPoolManager للدالة unlockCallback():

لكن — هناك فخ تصميمي سهل التغاضي عنه — حيث قام الإصدار الأولي من BaseHook بإضافة onlyPoolManager فقط لـ unlockCallback، ولم يُضف أي حماية لدوال التفعيل الأخرى (beforeSwap، afterSwap، beforeAddLiquidity، إلخ). يجب على مطوري الـ Hook إضافة التحكم في الوصول لهذه الدوال بشكل صريح.
3. قراءة شفرة دورة حياة Hook
على سبيل المثال، مع تبديل إدخال دقيق، يُحلل أدناه سلسلة المكالمات الكاملة من بدء المستخدم للصفقة حتى التسوية.
3.1 تهيئة الحوض وربط الـ Hook
يمكن لأي شخص استدعاء PoolManager.initialize() لإنشاء حوض جديد:

يتحقق isValidHookAddress فقط من توافق خانة الصلاحيات للعنوان وحقل الرسوم، ولا يتحقق مما إذا كان Hook مستخدمًا بالفعل في حوض آخر، ولا يتحقق مما إذا كان Hook "يرغب" في قبول هذا PoolKey. إذا لم يتم تصميم Hook لإضافة قائمة بيضاء أو منطق ربط حوض واحد داخل beforeInitialize، فيمكن لأي شخص إنشاء حوض جديد يستخدم نفس Hook ولكن بزوج رموز عشوائي وتفعيل جميع ردود الفعل اللاحقة للـ Hook.
3.2 beforeSwap و BeforeSwapDelta
مُدخل عملية التبديل هو PoolManager.swap()، والذي يُنفّذ دالة Hooks.beforeSwap() قبل تنفيذ منطق التبديل الأساسي:

قيمة إرجاع beforeSwap هي ثلاثية (bytes4, BeforeSwapDelta, uint24):
- bytes4: يجب أن يساوي IHooks.beforeSwap.selector، وإلا فإن PoolManager سيُعيد العملية مباشرة
- BeforeSwapDelta: التغيير في الـ delta للرمز المحدد وغير المحدد خلال هذه العملية Swap
- uint24: قيمة تغطية معدل الرسوم LP الديناميكي (تنطبق فقط عند تفعيل الرسوم الديناميكية للحوض)
BeforeSwapDelta هو اسم مستعار لـ int256، حيث أن الـ 128 بت العليا تمثل delta للرمز المحدد (أي الرمز الذي حددته المستخدم)، والـ 128 بت السفلى تمثل delta للرمز غير المحدد:

يجب ملاحظة أن معنى BeforeSwapDelta هو أن الـ Hook الذي يفرض رسومًا يجب أن يُرجع قيمة موجبة، بينما الـ Hook الذي يُعيد الرموز المميزة يجب أن يُرجع قيمة سالبة. من السهل على المطورين أن يقلبوا الإشارة؛ كما أن العلاقة بين المحدد وغير المحدد تعتمد على params.zeroForOne وسالبية أو موجبية amountSpecified، وأي خطأ طفيف في الكتابة قد يؤدي إلى تبديل الرموز المميزة.
سيقوم PoolManager بجمع specifiedDelta المُعاد من beforeSwap مباشرةً إلى amountToSwap:

تحتوي هذه السطر على دلالة رئيسية: يمكن للـ Hook أن يحتجز مبلغ Swap. عندما يكون hookDeltaSpecified يساوي -params.amountSpecified، يصبح amountToSwap صفرًا مباشرة، مما يعني أن الـ Hook يتولى بالكامل عملية Swap هذه — وهذا ما يُعرف بـ Async Hook أو Custom Curve Hook.
يُعد Async Hook أحد أنماط التصميم الأعلى خطورة في الإصدار v4: فهو يحل محل منطق التبديل الأصلي لـ Uniswap بمنطق Hook الخاص به. إذا كان هناك ثغرة في Hook أو كان Hook خبيثًا بحد ذاته، فلن تخضع أموال المستخدمين لقيود منطق التسعير الأصلي لـ Uniswap، بل ستعتمد بشكل رئيسي على صحة تنفيذ Hook نفسه.
3.3 تسويات دلتا و NonzeroDeltaCount
لا يتم تشغيل التحويل فورًا من قبل delta المُعاد من beforeSwap و afterSwap، بل يتم تسجيله في دفتر الحسابات الداخلي لـ PoolManager:

عندما يتحول مجموع delta لرمز إلى قيمة غير صفرية من الصفر، يزداد NonzeroDeltaCount؛ وعند العودة إلى الصفر، ينقص. كما هو مذكور في 2.1، إذا كان NonzeroDeltaCount ≠ 0 عند انتهاء unlock()، يتم إلغاء整个 المعاملة.
يوازن Hook دلتا من خلال عمليتين: settle() (تحويل الأموال إلى PoolManager) وtake() (سحب الأموال من PoolManager):

المعنى الأمني الناتج عن هذه الآلية واضح: جميع الأطراف يجب أن تُغلق حساباتها في النهاية. لكنها تضمن فقط "حفظ الحسابات"، ولا تضمن "صحتها". إذا عاد Hook في beforeSwap بـ delta مُنشأ بشكل خبيث، فسيقوم PoolManager بتسجيل هذا الـ delta بدقة، طالما تم إغلاقه في النهاية — حتى لو كان ذلك يعني أن Hook يمكنه من خلال تزوير حالة الأعمال، جعل النظام يعتقد خطأً أن المهاجم يمتلك حقوقًا في أصول معينة، بينما لا يستطيع PoolManager اكتشاف هذا الخطأ على مستوى الأعمال.
كان حادث أمان Cork Protocol السابق ناتجًا عن ثغرة في Hook، رغم أنه خضع لمراجعة من أربع شركات مراجعة قبل الهجوم. بعد التحليل اللاحق، اكتشفنا أن:
ثلاثة من أربع مراجعات لا تشمل نطاق عقد CorkHook
تمت مراجعة CorkHook من قبل جهة تدقيق واحدة فقط، والتي حددت بعض مشكلات الكود وقدمت اقتراحات للتحسين، لكنها لم تغطِّ جميع قضايا التحكم في الوصول.
كما اقترح طرف مراجعة آخر في تقريره بوضوح: "سيكون مشروع متابعة مثير هو إثبات الثوابت لدوال CorkHook التي يتم استدعاؤها من قبل مكونات مختلفة تم التحقق منها ضمن نطاق هذا المشروع". هذا الاقتراح يتمتع بتركيز قوي من منظور المراجعة اللاحقة.
هذا يكشف عن منطقة عمياء جديدة في عصر v4 Hook: إن الزيادة الهائلة في تعقيد البروتوكول تجعل تحديد النطاق نفسه قرارًا أمنيًا. ترتبط سلاسل التفاعل بين Hook والعقود الأخرى للبروتوكول بطول كبير، لذا فإن مراجعة عقد Hook فقط لا تكفي لاكتشاف مشكلات التوافق بين العقود؛ وفي المقابل، مراجعة العقود المحيطة مع استبعاد Hook من النطاق يؤدي إلى تفويت أكبر سطح هجوم في عصر v4.
4. التأمل
عند مقارنة آلية البروتوكول وهجوم Cork جنبًا إلى جنب، يمكن استخلاص النقاط الأساسية لنموذج أمان v4 Hook:
(1) إذا كان دالة رد الاتصال Hook تعتمد على سياق الاستدعاء المقدم من PoolManager، فيجب تحديد صراحةً أنها تُستدعى فقط من قبل PoolManager. لن يقوم BaseHook بتنفيذ هذا الأمر نيابةً عن المطور، وهو أخطر فخ تصميمي يتعارض مع تجارب التدقيق العادية للعقود في الإصدار 4.
(2) العلاقة بين الـ Hook والخزانة غير خاضعة لقيود PoolManager. يجب على المطورين تنفيذ قائمة بيضاء للخزانات أو الربط بخزانة واحدة يدويًا في beforeInitialize.
يجب أن تتطابق صلاحيات عنوان Hook بدقة مع تنفيذ الدالة. يجب أن يتضمن العنوان المحسوب جميع صلاحيات التوسع المحتملة في المستقبل مسبقًا.
(4) مُربّط Async / Curve مخصص هو تنفيذ مخصص بالكامل للتبادل. لا يحتوي على أي حماية من مستوى بروتوكول Uniswap، ويجب مراجعته وفقًا لمعايير "عقد مالي ذاتي بالكامل".
(5) حفظ دلتا في المحاسبة لا يعادل "الصواب". إن وجود NonzeroDeltaCount == 0 يضمن فقط أن الدفتر ينتهي متوازنًا، لكنه لا يضمن أن محتوى الدفتر لم يُManipulated بشكل خبيث
(6) الالتباس في أنواع الرموز عبر الأسواق هو سطح هجوم جديد في عصر v4. عندما يسمح البروتوكول للمستخدمين بإنشاء أسواق، فإن التحقق الدلالي للرموز أمر ضروري، ولا يمكن الاعتماد فقط على فحص الواجهة.
كل Hook هو مجال ثقة مستقل، ويعتمد أمان كل حوض على الـ Hook المرتبط به. وبالتالي، فإن تعقيد مراجعة أمان الـ Hook لم يعد "مراجعة كود واحد"، بل "مراجعة بروتوكول فرعي كامل" — وهذا التغيير يعني ترقية منهجية لكل من الفريق المشروع ومراجعي الأمان.

