يستخدم Uniswap v4 Hooks لتحقيق سيولة قابلة للبرمجة، لكنه يُعد بيئة مستهدفة للهجمات بسبب عيوب في ترميز الصلاحيات والتحكم في الوصول، ويجب الحذر من مخاطر سوء استخدام Hooks المتشابكة ومنطق المحاسبة.كاتب المقال، المصدر: Beosin
بعد إطلاق 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 عند استدعاء Rückruf، مما يؤدي إلى هجوم 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 اعتراض مبلغ التبديل. عندما يكون hookDeltaSpecified يساوي -params.amountSpecified، يصبح amountToSwap صفرًا مباشرة، مما يعني أن الـ Hook يتولى بالكامل عملية التبديل هذه — وهذا ما يُعرف بـ Async Hook أو Custom Curve Hook.
يُعد Async Hook أحد أنماط التصميم الأكثر خطورة في الإصدار v4: فهو يحل محل منطق التبديل الأصلي لـ Uniswap بمنطق Hook الخاص به. إذا كان هناك ثغرة في Hook أو كان Hook خبيثًا بحد ذاته، فلن تخضع أموال المستخدمين لقيود منطق التسعير الأصلي لـ Uniswap، بل ستعتمد بشكل رئيسي على صحة تنفيذ Hook نفسه.
3.3 تسويات دلتا و NonzeroDeltaCount
لا يتم تشغيل التحويل فورًا من قبل beforeSwap و afterSwap، بل يتم تسجيله في دفتر الحسابات الداخلي لـ PoolManager:
عندما يتحول مجموع delta لرمز إلى قيمة غير صفرية من الصفر، يزداد NonzeroDeltaCount؛ وعند العودة إلى الصفر، ينقص. كما هو مذكور في 2.1، إذا كان NonzeroDeltaCount ≠ 0 عند انتهاء unlock()، يتم إلغاء المعاملة بالكامل.
يوازن Hook فرقه من خلال عمليتين: settle() (تحويل الأموال إلى PoolManager) وtake() (سحب الأموال من PoolManager):
المعنى الأمني الناتج عن هذه الآلية واضح: يجب على الجميع في النهاية إغلاق حساباتهم. لكنها تضمن فقط "حفظ الحسابات"، ولا تضمن "صحتها". إذا عاد Hook في beforeSwap بـ delta مُنشأ بشكل خبيث، فسيقوم PoolManager بتسجيل هذا الـ delta بدقة، طالما تم إغلاقه في النهاية، فإن المعاملة تُعتبر ناجحة — حتى لو كان ذلك يعني أن Hook يمكنه من خلال تزوير حالة الأعمال، جعل النظام يعتقد خطأً أن المهاجم يمتلك بعض حقوق الأصول، بينما لا يستطيع PoolManager التعرف على هذا الخطأ على مستوى الأعمال.
كانت حادثة الأمان السابقة لبروتوكول Cork ناتجة عن ثغرة في Hook، وعلى الرغم من أن النظام كان قد خضع لمراجعة أمنية من أربع شركات مراجعة قبل الهجوم. بعد التحليل اللاحق، اكتشفنا أن:
ثلاثة من أربع مراجعات لا تشمل نطاق عقد CorkHook
المراجعة الوحيدة التي أُجريت على CorkHook كشفت عن بعض مشكلات الكود وقدمت اقتراحات للتحسين، لكنها لم تغطِّ جميع قضايا التحكم في الوصول.
كما اقترح طرف مراجعة آخر في تقريره بوضوح: "سيكون مشروع متابعة مثير هو إثبات الثوابت للوظائف CorkHook التي يتم استدعاؤها من قبل مكونات مختلفة تم التحقق منها ضمن نطاق هذا المشروع". هذا الاقتراح يتمتع بدرجة عالية من الدقة من منظور المراجعة اللاحقة.
هذا يكشف عن ثغرة تدقيق جديدة في عصر v4 Hook: إن الزيادة الهائلة في تعقيد البروتوكول تجعل تحديد النطاق نفسه قرارًا أمنيًا. إن سلاسل التفاعل بين Hook والعقود الأخرى للبروتوكول طويلة جدًا، لدرجة أن تدقيق عقد Hook فقط لا يكفي لاكتشاف المشكلات التوافقية بين العقود؛ وفي المقابل، تدقيق العقود المحيطة مع استبعاد Hook من النطاق سيؤدي إلى تفويت أكبر سطح هجوم في عصر v4.
5. التفكير
عند مقارنة آلية البروتوكول وهجوم Cork جنبًا إلى جنب، يمكن استخلاص النقاط الأساسية لنموذج أمان v4 Hook:
(1) إذا كان دالة رد الاتصال Hook تعتمد على سياق الاستدعاء المقدم من PoolManager، فيجب تحديد صراحةً أنها تُستدعى فقط من قبل PoolManager. لن يقوم BaseHook بتنفيذ هذا الأمر نيابةً عن المطور، وهو أصعب فخ تصميمي يتعارض مع تجارب التدقيق العادية للعقود في الإصدار 4.
(2) العلاقة بين الـ Hook والخزانة غير خاضعة لقيود PoolManager. يجب على المطورين تنفيذ قائمة بيضاء للخزانات أو الربط بخزانة واحدة يدويًا في beforeInitialize.
(3) يجب أن تتطابق صلاحيات عنوان Hook بدقة مع تنفيذ الدالة. يجب أن يتضمن العنوان المحسوب جميع صلاحيات التوسع المحتملة في المستقبل مسبقًا.
(4) مُلْحَق Async / Curve مخصص هو تنفيذ مخصص بالكامل للتبادل. لا يحتوي على أي حماية من مستوى بروتوكول Uniswap، ويجب مراجعته وفقًا لمعايير "عقد مالي ذاتي بالكامل".
(5) حفظ دلتا في المحاسبة لا يعني "الصواب". إن وجود NonzeroDeltaCount == 0 يضمن فقط أن الدفتر ينتهي متوازنًا، لكنه لا يضمن أن محتوى الدفتر لم يُشوَّه بقصد خبيث
(6) لبس نوع الرموز عبر الأسواق هو سطح هجوم جديد في عصر v4. عندما يسمح البروتوكول للمستخدمين بإنشاء أسواق، فإن التحقق الدلالي للرموز أمر ضروري، ولا يمكن الاعتماد فقط على التحقق عبر الواجهة.
كل Hook هو مجال ثقة مستقل، ويعتمد أمان كل حوض على Hook المرتبط به. وبالتالي، فإن تعقيد مراجعة أمان Hook لم يعد "مراجعة كود واحد"، بل "مراجعة بروتوكول فرعي كامل" — وهذا التغيير يعني ترقية منهجية لكل من الفريق المشروع وفريق المراجعة.
References
(1) بروتوكول كورك. "تحليل ما بعد الاستغلال في 28 مايو 2025." 2025-06-04. https://www.cork.tech/blog/post-mortem
(2) OWASP أعلى 10 ثغرات أمان العقود الذكية 2026، SC01: ثغرات التحكم في الوصول. https://scs.owasp.org/sctop10/SC01-AccessControlVulnerabilities/
(3) الورقة البيضاء لـ Uniswap v4. https://app.uniswap.org/whitepaper-v4.pdf
(4) Uniswap v4-core. https://github.com/Uniswap/v4-core
(5) Uniswap v4-periphery. https://github.com/Uniswap/v4-periphery
بيوسين، كإحدى أولى الشركات العالمية المتخصصة في التحقق الرسمي لسلامة البلوكشين، تركز على تقديم خدمات ومنتجات متكاملة تجمع بين "السلامة والامتثال"، ولها فروع في أكثر من 10 دول ومناطق حول العالم، وتشمل خدماتها التدقيق الأمني للشفرات قبل إطلاق المشاريع، ومراقبة ومنع المخاطر الأمنية أثناء التشغيل، واسترداد الأصول المسروقة، ومكافحة غسيل الأموال للعملات الافتراضية (AML)، بالإضافة إلى التقييمات الامتثالية المتوافقة مع متطلبات التنظيم المحلية، كحلول "شاملة" للامتثال والأمان في مجال البلوكشين. نرحب بمراسلتنا من خلال مربع الرسائل في قناتنا.

