تعرفون، أيامما كنت أقضي الليل كله في الهاكاثونات... عذراً، أي ليالي وليلة، كنا غالباً ننام ثلاث ساعات تحت طاولات السيرفرات، بدا لي وقتها أن أي أداة تحليل ثابت يمكنها رصد الثغرات البسيطة في العقود الذكية بكل سهولة. ما الصعب في ذلك أصلاً؟ هجمات إعادة الدخول (Reentrancy) هي الكلاسيكية التي كتب عنها الجميع. نمط Checks-Effects-Interactions، ومعدلات مختلفة... ثم يأتي إلينا مبرمج مبتدئ في الفريق قبل بضعة أشهر ليقول بكل فخر: «اسمعوا، ChatGPT كتب لي عقد ستكينغ (Staking) مثالي، راجعت بنفسي وهو آمن تماماً!»
كادت قهوتي تسقط على لوحة المفاتيح الميكانيكية من الفزع.
دعونا نكون صرحاء: نماذج اللغات الكبيرة (LLMs) تكتب لغة Solidity بشكل جميل جداً. الصياغة براقة، الاستيراد مرتب، وOpenZeppelin مربوطة حسب أحدث صيحات البرمجة. لكن عندما يتعلق الأمر بالمنطق الدقيق لتوزيع الحالات، يبدأ الذكاء الاصطناعي بكل ثقة عمياء وصلبة بتوليد ثغرات تكلف المشاريع ملايين الدولارات أثناء عمليات التدقيق الأمني (Audits). دعونا نفكك ثلاث حالات غير واضحة يورط فيها ChatGPT نفسه تماماً، بينما يؤكد لك أن كودك اجتاز تدقيق CertiK بنجاح تام.
الحالة الأولى: الستكينغ غير المتزامن مع مكافآت البلوكات واستدعاء خارجي مخفي
يا جماعة، عندما تطلب من الذكاء الاصطناعي كتابة عقد يوزع التوكنات بناءً على فترة البقاء في المسبح (Pool)، ماذا يفعل النموذج؟ يضع ببساطة معدل nonReentrant في كل مكان يرى فيه تحويل أموال للمستخدم، لكنه ينسى تفصيلاً معممارياً بالغ الأهمية.
ألقِ نظرة على هذا المقتطف الذي أخرجته من مراجعة كود حقيقية بعد تلك الحادثة مع المبتدئ:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
interface IERC20Mintable {
function mint(address to, uint256 amount) external;
}
contract SecureStakingVault {
IERC20Mintable public rewardToken;
mapping(address => uint256) public balances;
mapping(address => uint256) public lastUpdateBlock;
uint256 public rewardRatePerBlock = 10 * 10**18;
bool private locked;
// لقد أمّناه يا رجل! ماذا تريد أيضاً أيها الشكاك؟
modifier noReentry() {
require(!locked, "LOCKED");
locked = true;
_;
locked = false;
}
constructor(address _token) {
rewardToken = IERC20Mintable(_token);
}
function stake() external payable {
require(msg.value > 0, "Zero stake");
balances[msg.sender] += msg.value;
lastUpdateBlock[msg.sender] = block.number;
}
// تحفة ChatGPT التي تبدو آمنة تماماً
function harvestAndUnstake(uint256 _amount) external noReentry {
require(balances[msg.sender] >= _amount, "Insufficient balance");
uint256 blocksPassed = block.number - lastUpdateBlock[msg.sender];
uint256 reward = blocksPassed * rewardRatePerBlock * _amount / balances[msg.sender];
// تنبيه: يتم تحديث الحالة قبل عملية الـ mint الخارجية
balances[msg.sender] -= _amount;
lastUpdateBlock[msg.sender] = block.number;
// إصدار المكافآت يستدعي مُنشئ التوكن أو خطافاً (Hook)، وهنا تقع المفاجأة!
rewardToken.mint(msg.sender, reward);
(bool success, ) = msg.sender.call{value: _amount}("");
require(success, "Transfer failed");
}
}أين تكمن الخديعة هنا؟ يعتقد النموذج جازماً أنه بما أن متغيرات الرصيد تم تصفيرها قبل استدعاء rewardToken.mint()، فالـهجوم مستحيل. لكن rewardToken هو توكن ERC-20 مخصص يحتوي عقد السك الخاص به على خطاف _beforeTokenTransfer أو دالة رد نداء مشابهة لـ ERC-777 (إذا كان التوكن معقداً أو يعتمد على بروكسي). في لحظة استدعاء الـ mint، تنتقل السيطرة إلى كود خارجي قبل أن يصل الإيثريوم إلى محفظة المستخدم، ولكن في منتصف جلسة نشطة تكون فيها حالة المسبح قد تعدلت بشكل أحادي الجانب، بينما فسد حساب المكافآت لبقية المستخدمين. هذه ثغرة إعادة دخول بين العقود (Cross-Contract Reentrancy) من الجيل الجديد والتي تتجاهلها الكتب التعليمية كلياً.
الحالة الثانية: الاختراق عبر الوظائف المشتركة عبر أوراكل السيولة
يكمن خبث الاستغلالات الحديثة في أن هجمات إعادة الدخول غالباً لا تقع داخل دالة واحدة، بل بين نقطتي دخول مختلفتين تماماً تتشاركان نفس خريطة التعيين (Mapping). يعشق الذكاء الاصطناعي تقسيم المنطق إلى وحدات "نظيفة"، متجاهلاً تماماً ترتيب تعديل الثوابت العامة للنظام.
- معيار الثغرة: تقييم أداة التحليل الثابت
- الوضع الفعلي: المعدلات (Modifiers)
- موجودة (nonReentrant): عديمة الفائدة ضد الاستدعاءات المشتركة بين الوظائف
- ترتيب العمليات: تم احترام Checks-Effects محلياً
- تم تدمير التوازن العام للبروتوكول: رد فعل ChatGPT
- «الكود محمي تماماً ضد إعادة الدخول»: يتجاهل التلاعب المتقاطع بالحالة
الحالة الثالثة: العقود ذات المدد المتغيرة وخزائن ERC-4626
يعشق ChatGPT تماماً قوالب OpenZeppelin القياسية الخاصة بخزائن العائد (ERC-4626). يأخذ أمثلتها الجاهزة، يضيف إليها رياضيات مخصصة لحساب الحصص ويخرج لك كوداً يتم تجميعه (Compiles) من المحاولة الأولى. لكن المشكلة أن دالة تحويل الأسهم إلى أصول (convertToAssets) في ظل ظروف استدعاء معينة عبر عقد وسيط تسمح بتشغيل عملية إرجاع تحكم متعددة المستويات أثناء حساب السيولة الافتراضية.
لقد أمضيت نصف الليل قبل ثلاثة أسابيع وأنا أتقصى خطأً في شبكة الاختبار (Testnet) عندما قام بوت محاكاة بسحب السيولة كاملة من المسبح وفقاً لهذه التشكيلة بالضبط. وكان الذكاء الاصطناعي يكتب لي بكل برود: «استخدم مقياس المضاعفات للحصول على دقة أعلى». والنتيجة في النهاية؟ ثغرة إعادة دخول تقليدية للقراءة فقط (Read-Only Reentrancy)، حيث يقوم نظام خارجي بقراءة الحالة الوسيطة وغير المثبتة للخزينة في لحظة تنفيذ الإيداع.
نواصل غوصنا في مسرح اللامعقول هذا الذي تولده لنا نماذج اللغات عندما نطلب منها كتابة "عقد ذكي بسيط وموثوق".
هل تعرف ما هو أكثر شيء مضحك في عمل مدقق الأمان السابق؟ هو رؤية المطورين وهم يثقون بشكل أعمى في تعليقات ChatGPT بنفسه. يكتب الذكاء الاصطناعي في الأعلى سطرًا مثل // Safe against reentrancy attacks thanks to Checks-Effects-Interactions — وفقط هكذا، يتوقف التفكير النقدي لدى الشخص تمامًا. يصبح بصره غائمًا، ويدخل دماغه في وضع الاسترخاء، ثم يحدث انفجار كبير في الـ mainnet.
دعونا نستعرض مثالين قويين آخرين يصر الذكاء الاصطناعي على اعتبارهما المعيار الذهبي للأمان، بينما يتم اختراقهما في الواقع بسهولة تامة.
الحالة رقم 4: التحكيم متعدد الخطوات مع الاسترداد عبر قروض الفلاش (Flash Loans)
هذه هي المشكلة الكلاسيكية التي تُعطى غالبًا في الهاكاثون أو تُركن للمطورين الجدد: كتابة عقد يأخذ عملات مشفرة عبر فلاش, ويمررها عبر DEX, ويجمع الأرباح, ويرتجع أصل الدين ويسجل العمولات في مسب السيولة. ماذا يفعل ChatGPT؟ يقوم بترتيب شروط require للرصيد بدقة في بداية ونهاية المعاملة، معتقدًا بإخلاص أنه إذا تطابق الـ delta، فإن النظام منيع تمامًا.
لكن الشيطان، كالعادة، يكمن في تفاصيل استدعاء دالة الـ callback الواردة في uniswapV2Call (أو واجهة مشابهة لبروتوكولات أخرى).
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
interface IUniswapV2Pair {
function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external;
}
contract AIArbitrageBotHolder {
address private immutable owner;
bool private unlocked = true;
// يعتقد الذكاء الاصطناعي أن هذا الحاجز يكفي لحماية المنطق بأكمله
modifier lock() {
require(unlocked, "LOCKED");
unlocked = false;
_;
unlocked = true;
}
constructor() {
owner = msg.sender;
}
// تلك الطريقة "الآمنة" بحسب الشبكة العصبية
function executeFlashLoan(address _pair, uint256 _amountA, bytes calldata _data) external lock {
// طلب السيولة من المسب
IUniswapV2Pair(_pair).swap(_amountA, 0, address(this), _data);
}
// دالة الـ Callback حيث يعيد المسب التحكم
function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external {
// خطأ فادح: غالباً ما يتجاهل الذكاء الاصطناعي التحقق من msg.sender لكي يبدو الكود "نظيفاً"
// وفوالا، يمكن لأي عقد غريب الدخول هنا وبدء حلقة إعادة الدخول (Reentrancy loop)
uint256 fee = amount0 * 3 / 997 + 1;
uint256 repayment = amount0 + fee;
// منطق التحكيم المخصص الذي ألفه ChatGPT
_executeInternalArbitrage(amount0);
// سداد الدين للمسب
// (وهنا ينسون أن حالة المسابح الخارجية لم تتم تسويتها نهائياً بعد)
bool success = IERC20(msg.sender).transfer(msg.sender, repayment);
require(success, "Repayment failed");
}
function _internalArbitrage(uint256 amount) internal {
// دالة وهمية لمنطق التبادل المعقد
}
}هل ترون الثغرة؟ تضع الشبكة العصبية معدل lock على نقطة الدخول الخارجية executeFlashLoan، لكنها تنسى تمامًا أن دالة الاستدعاء العكسي uniswapV2Call يجب عزلها بشكل منفصل أو التحقق بصرامة من العنوان المستدعي (msg.sender). يمكن للمهاجم استدعاء الـ callback هذا مباشرة من عقده الخاص أثناء تنفيذ الحسابات الوسيطة، عندما لا تكون أرصدة عقد الـ wrapper متسقة بعد، وسحب الضمان بالكامل حتى آخر wei. ولن يحذرك ChatGPT من هذا أبدًا؛ بل سيكتفي بالابتسام برموز تعبيرية في الدردشة ليقول لك: "الكود مُحسَّن وفقاً لمعيار ERC".
الحالة رقم 5: عقود السك (Minting) الجماعية لـ ERC-721 المُحسَّنة مع التخزين المؤقت (Caching)
تتطلب سوق ال NFTs الحديثة غازًا رخيصًا. يفهم الذكاء الاصطناعي هذا تمامًا ويبدأ في "تحسين" الكود من خلال إدخال هياكل تعيين مالك مخصصة وعدادات تخزين مؤقت مباشرة في ذاكرة العقد.
- مقياس التدقيق: حكم ChatGPT — الحالة الحقيقية للثغرة
- استهلاك الغاز: منخفض للغاية (Gas Optimized) — يتم تحقيقه عن طريق كسر عزل الحالة (State Isolation)
- نمط التخزين: استخدام هياكل محلية (structs) في الذاكرة — عرضة للتلاعب عبر خطافات onERC721Received
- الاستقرار: يجتاز اختبارات Hardhat القياسية — يتعطل عندكدس استدعاءات إعادة الدخول العميقة
عندما يرسل العقد NFT عبر safeMint، يتطلب المعيار استدعاء دالة تأكيد في عقد الذكي المستلم. إذا كنت تقوم بسك جماعي (لنفترض 10 توكنات دفعة واحدة) وتحديث عداد الرصيد الإجمالي بعد إرسال كل توكن (أو في حلقة داخل استدعاء خارجي)، فإن المهاجم يختطف السيطرة في التكرار الثالث، عندما تكون مصفوفة التوكنات الداخلية ممتلئة نصف امتلاء فقط، بينما لا يزال عداد المعاملات يعتقد أن العملية بدأت للتو. النتيجة النهائية: تصادم في المؤشرات وإصدار لا نهائي للأصول من العدم.
ما الذي ينبغي عليّ، عليك، وعلى الصناعة بأكملها فعله حيال ذلك؟
اسمعوا، أنا أكتب الكود بنفسي كل يوم، لكن الثقة العمياء في النماذج التوليدية في عالم العملات المشفرة تشبه لعب الروليت الروسية مع مسظر ممتلئ بالكامل بالرصاص ما عدا واحدة. إذا كنت تستخدم الذكاء الاصطناعي لتوليد البروتوكولات، فتذكر ثلاث قواعد صارمة:
- لا تثق أبدًا بالمعدلات (Modifiers) القادمة من القوالب. إذا وضع الذكاء الاصطناعي
nonReentrant، فهذا يعني فقط أنه يعرف الكلمة، لكنه لا يفهم السياق غير المتزامن لسلسلة الكتل (Blockchain) الخاصة بك. - تحقق دائمًا من
msg.senderفي دوال الـ callback. يجب حماية أي واجهات مثلuniswapV2CallأوonERC721ReceivedأوtokensReceivedبشكل أقوى وأشد من بوابة مخبأ عسكري. - اكتب اختبارات الفاز (Fuzz tests) الخاصة بك على Foundry. لن تجد أي اختبارات وحدة ثابتة (Unit tests) ثغرات إعادة الدخول متعددة الدوال بكفاءة مثل بضعة ملايين من عمليات التشغيل العشوائية القائمة على الثوابت (Invariants).
بينما أصصد في الحديث عن هذه الثغرات، هناك فكرة واحدة لا تبارح ذهننا: في الواقع، نحن من قمنا بإرخاء السوق بأنفسنا. لقد اعتدنا على تفويض المهام الروتينية لتوليد الكود، ناسیين أن البلوكشين لا يغفر أخطاء ترجمة المنطق. في تطوير الويب التقليدي، يمكنك التراجع عن الcommit، أو إطلاق patch، أو الاعتذار للمستخدمين عن downtime لمدة خمس دقائق. أما في بيئة EVM، فسيبقى الbug الخاص بك إلى الأبد في سجل غير قابل للتعديل (immutable ledger) كمعلم تذكاري للإهمال البشري وانتصار العمى الخوارزمي.
دعونا ننهي بقية الموضوع ونحلل متجهاً غير بديهي آخر يولده ChatGPT باتساق مرعب.
الحالة رقم 6: تجميع السيولة (Liquidity Aggregators) والحصص الافتراضية في تجمعات العملات المستقرة
هذه أحدث صيحة بين مطوري DeFi — وهي إنشاء تجمعات تبادل مخصصة (custom pools) مع حساب ديناميكي لأوزان الرموز بناءً على منحنيات المنتج الثابت (constant product curves). تعشق الشبكات العصبية هذه الرياضيات لأن هناك تيرابايتات من كود المصدر المفتوح لـ Curve و Uniswap v2 ملقاة على الإنترنت. يأخذ النموذج المعادلة، ويكيفها حسب متطلباتك، ويضيف دالة addLiquidity ويعلن بفخر أن المنتج جاهز للـ deploy.
لكن المشكلة أن النموذج ليس لديه أي فكرة عن كيفية عمل الحساب غير المتزامن (asynchronous calculation) للأرصدة الافتراضية عند استدعاء رموز خارجية ذات رسوم متغيرة (مثل USDT أو رموز deflationary المخصصة ذات ضريبة التحويل).
ألقِ نظرة على النمط الذي يعتبره الذكاء الاصطناعي آمناً تماماً:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
interface ITokenWithFee {
function transferFrom(address sender, address recipient, amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
}
contract AIWarpPool {
mapping(address => uint256) public userShares;
uint256 public totalShares;
// يعتقد الذكاء الاصطناعي أننا إذا حسبنا الرصيد أولاً ثم كتبناه في الـ mapping، فنحن في أمان
function deposit(address token, uint256 amount) external {
uint256 balanceBefore = ITokenWithFee(token).balanceOf(address(this));
// استدعاء خارجي لتحويل الرمز
bool success = ITokenWithFee(token).transferFrom(msg.sender, address(this), amount);
require(success, "Transfer failed");
uint256 balanceAfter = ITokenWithFee(token).balanceOf(address(this));
uint256 realReceived = balanceAfter - balanceBefore;
// إصدار الحصص بناءً على الأموال الواردة فعلياً
uint256 shares = totalShares == 0 ? realReceived : (realReceived * totalShares) / balanceBefore;
userShares[msg.sender] += shares;
totalShares += shares;
}
function withdraw(uint256 shares) external {
require(userShares[msg.sender] >= shares, "Not enough shares");
uint256 amountToReturn = (shares * ITokenWithFee(msg.sender).balanceOf(address(this))) / totalShares;
// نقوم بتقليل الـ state أولاً...
userShares[msg.sender] -= shares;
totalShares -= shares;
// ...ثم نُسلم الأموال. نمط Checks-Effects-Interactions الكلاسيكي!
// ولكن انتظر لحظة...
(bool success, ) = msg.sender.call{value: 0}(""); // استدعاء خارجي مشروط لتحويل الرمز
require(success, "Withdraw failed");
}
}هل رأيت الفخ؟ التزم الشبكة العصبية بصدق بترتيب Checks-Effects-Interactions داخل دالة withdraw. لكنها غفلت تماماً عن حقيقة أن حساب amountToReturn مرتبط بـ balanceOf(address(this)) الحالي وقت الاستدعاء. إذا قام مهاجم بإنشاء عقد غلاف (wrapper contract) يبادر بإعادة الدخول (reentrancy) إلى دالة withdraw عند استلام التحكم (أو عبر callback الرمز) قبل أن يتغير الـ totalShares العالمي في تجمع مرتبطة آخر أو عبر استدعاء أوراكل عابر للعقود، فهو يحسب النسب بناءً على قيمة سيولة قديمة.
القائمة النهائية للتحقق مما كتبه لك الذكاء الاصطناعي
كفى تكراراً لنفس الأخطاء على أمل أن تكون الشبكة العصبية أذكى منك في هندسة الأنظمة الموزعة. قبل إرسال الكود المُولد إلى الـ production أو حتى إلى testnet بأموال حقيقية، راجع هذه القائمة:
- اعزل أي دالة callback كما لو كان يجلس بداخلها هاكر ومعه exploit جاهز.
- افحص كل استدعاء خارجي لمعرفة ما إذا كان بإمكانه إعادة التحكم إلى العقد في لحظة لم تكتمل فيها الثوابت العالمية (global invariants) بعد.
- انسه الإيمان الأعمى بالـ modifiers — فهي تحمي فقط من إعادة الدخول المباشر إلى نفس الدالة، لكنها عاجزة تماماً أمام السلاسل المعقدة العابرة للعقود (cross-contract).