التفاعل بين العقود الذكية: كيف تجعل عقداً يستدعي دالة من عقد آخر؟

دقائق القراءة: 6

التفاعل بين العقود الذكية: كيف تجعل عقداً يستدعي دالة من عقد آخر؟

في الأنظمة اللامركزية الحديثة، نادراً ما يعمل عقد ذكي واحد بمعزل عن غيره. أغلب تطبيقات Web3 الفعلية تعتمد على مجموعة عقود مترابطة: عقد للتخزين، وآخر للصلاحيات، وثالث لإدارة المدفوعات أو الرموز. لهذا السبب، فإن فهم آلية استدعاء دالة من عقد آخر يعتبر خطوة متقدمة بعد إتقان الدوال (Functions) في Solidity: من يمكنه قراءة وتعديل بيانات العقد؟ وفهم بنية محرر Remix IDE: كتابة ونشر أول عقد ذكي (Smart Contract) على المتصفح مباشرة.

هذا التفاعل يتم داخل بيئة EVM عبر رسائل داخلية بين العقود. وعندما يستدعي عقد دالة من عقد آخر، فإنك لا تنقل فقط التحكم البرمجي، بل تدخل أيضاً في تفاصيل تتعلق بـ Gas Fees، وانتشار الأخطاء، وصلاحيات msg.sender، وأمن الاستدعاءات الخارجية.

لماذا نحتاج إلى تفاعل بين عقدين ذكيين؟

التصميم الاحترافي للعقود الذكية يتجنب تضخيم عقد واحد بكل المسؤوليات. تقسيم المنطق إلى عدة عقود يجعل الصيانة أسهل، وإعادة الاستخدام أفضل، والاختبار أكثر وضوحاً. هذه الفكرة ترتبط أيضاً بما تعلمته في الوراثة (Inheritance): بناء عقود ذكية متقدمة بالاعتماد على أكواد عقود سابقة، لكن التفاعل بين العقود يختلف عن الوراثة لأنه يحدث بين عقود منشورة أو مستقلة منطقياً.

  • عقد رئيسي يستدعي عقد تخزين بيانات.
  • عقد مدفوعات يتواصل مع عقد عضويات أو صلاحيات.
  • عقد DAO يستدعي عقد تصويت.
  • عقد متجر يستدعي عقد رمز مميز من نوع ERC20 لتحصيل المدفوعات.

الفكرة الأساسية: عنوان العقد + واجهته البرمجية

لكي يستدعي عقد دالة موجودة في عقد آخر، يحتاج إلى أمرين أساسيين:

  1. عنوان العقد الهدف contract address.
  2. تعريف يصف الدالة المطلوب استدعاؤها، غالباً عبر interface.

الواجهة لا تحتوي على منطق تنفيذي، بل تصف شكل الدالة: اسمها، مدخلاتها، ونوع القيمة المعادة. إذا سبق لك قراءة أساسيات لغة Solidity: أنواع البيانات والمتغيرات (State Variables)، ففكر في الواجهة كاتفاق برمجي يخبر المترجم كيف يتحدث مع العقد الآخر.

مثال عملي: عقد يستدعي دالة تحديث في عقد آخر

في المثال التالي، لدينا عقد أول باسم Counter يحتفظ بعدّاد، ثم عقد ثانٍ باسم CounterCaller يستدعي دالة الزيادة فيه.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Counter {
    uint256 public count;

    event CounterIncremented(uint256 newValue, address calledBy);

    function increment() external {
        count += 1;
        emit CounterIncremented(count, msg.sender);
    }

    function getCount() external view returns (uint256) {
        return count;
    }
}

interface ICounter {
    function increment() external;
    function getCount() external view returns (uint256);
}

contract CounterCaller {
    address public counterAddress;

    constructor(address _counterAddress) {
        counterAddress = _counterAddress;
    }

    function callIncrement() external {
        ICounter(counterAddress).increment();
    }

    function readCounter() external view returns (uint256) {
        return ICounter(counterAddress).getCount();
    }
}

كيف يعمل هذا المثال؟

العقد CounterCaller لا يرث من العقد الآخر، بل يحتفظ بعنوانه في المتغير counterAddress. وعند تنفيذ الدالة callIncrement، يتم تحويل العنوان إلى واجهة ICounter ثم استدعاء الدالة المطلوبة مباشرة.

لاحظ أن قيمة msg.sender داخل عقد Counter لن تكون المستخدم النهائي، بل عنوان عقد CounterCaller. وهذه نقطة محورية عند بناء أنظمة الصلاحيات والمعدلات، خاصة إذا كنت تعتمد على المعدلات (Modifiers): حماية الدوال برمجياً.

استدعاء دوال تقرأ البيانات مقابل دوال تعدل الحالة

عندما يستدعي عقد دالة view من عقد آخر، فالهدف غالباً قراءة البيانات دون تعديل حالة البلوكتشين. أما عند استدعاء دالة تغير حالة العقد الهدف، فستتحمل المعاملة تكلفة تنفيذ فعلية. لذلك من المهم استيعاب الفرق الذي جرى شرحه في أنواع الدوال : فهم view و pure لتوفير رسوم الـ Gas.

عند تصميم التفاعل بين العقود، اجعل الدوال القرائية view قدر الإمكان، وافصلها عن الدوال التي تعدل الحالة. هذا يقلل استهلاك gas ويحسن وضوح البنية البرمجية.

كيف تتعامل مع القيم المعادة والأخطاء؟

يمكن للعقد المستدعي استقبال قيمة معادة من العقد الهدف كما في readCounter. لكن إذا فشل الاستدعاء داخل العقد الآخر، فسوف ترتد المعاملة كلها عادة، ما لم تستخدم استدعاءات منخفضة المستوى مثل call.

لذلك يجب أن تكون مدركاً لكيفية انتشار الأخطاء بين العقود، وأن تستخدم أدوات الحماية مثل require وrevert بحذر، كما تم شرحه في التعامل مع الأخطاء وإرجاع الأموال: استخدام require, assert, revert.

الاستدعاء منخفض المستوى باستخدام call

أحياناً لا تريد تعريف واجهة كاملة، أو تحتاج مرونة في تمرير البيانات. هنا يُستخدم call، لكنه أكثر خطورة وأقل أماناً من الاستدعاء عبر الواجهة.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract LowLevelCaller {
    function executeIncrement(address target) external returns (bool, bytes memory) {
        (bool success, bytes memory data) = target.call(
            abi.encodeWithSignature("increment()")
        );

        require(success, "External call failed");
        return (success, data);
    }
}

في هذا الأسلوب، تتم كتابة اسم الدالة نصياً داخل abi.encodeWithSignature. أي خطأ إملائي أو اختلاف في التوقيع سيؤدي إلى فشل أو سلوك غير متوقع. لهذا السبب، يبقى استخدام interface هو الخيار المفضل في أغلب التطبيقات.

لا تستخدم call إلا عند وجود سبب تقني واضح. الاستدعاءات منخفضة المستوى تزيد سطح الهجوم، وتصعّب المراجعة الأمنية Security Auditing، وقد تفتح المجال لثغرات مثل reentrancy إذا كان هناك تحويل أموال أو منطق خارجي معقد.

ماذا عن إرسال Ether أثناء الاستدعاء؟

إذا كانت الدالة في العقد الهدف من نوع payable، يمكن للعقد المستدعي تمرير قيمة مالية معها. هذا يدخلنا في نطاق أعمق يتعلق بـ استلام وإرسال الأموال (Ether) برمجياً: فهم الدوال payable و fallback. عندها يجب الانتباه إلى ترتيب التنفيذ، وتحديث الحالة أولاً قبل إجراء الاستدعاء الخارجي متى أمكن.

أفضل ممارسات الأمان وتحسين الأداء

من منظور Gas Optimization، خزّن عنوان العقد الخارجي إذا كان ثابتاً نسبياً، وتجنب إنشاء مراجع متكررة أو استدعاءات متعددة لنفس الدالة في نفس المعاملة دون حاجة.

اختبار هذا النوع من العقود عملياً

يمكنك تجربة هذا السيناريو بسهولة عبر Remix أو بيئة Hardhat. ابدأ بنشر العقد الأول، انسخ عنوانه، ثم مرره إلى منشئ العقد الثاني. بعد ذلك نفّذ الدالة من العقد المستدعي وراقب تغيّر الحالة في العقد الهدف.

إذا كنت تعمل على شبكة اختبار، فاحرص أولاً على استكمال إعداد بيئة التطوير: تثبيت محفظة MetaMask والاتصال بشبكات الاختبار (Testnets) والحصول على عملات تجريبية مجانية (Faucet) للبدء في نشر واختبار العقود الذكية حتى تتمكن من تنفيذ المعاملات ودفع رسوم الاختبار.

الخلاصة

التفاعل بين العقود الذكية هو العمود الفقري للتطبيقات اللامركزية المركبة. الفكرة الجوهرية بسيطة: عنوان عقد + واجهة صحيحة + فهم دقيق لسلوك الاستدعاء الخارجي. لكن التنفيذ الاحترافي يتطلب أيضاً وعياً بتغير msg.sender، وانتقال الأخطاء، وكلفة gas، والمخاطر الأمنية المرتبطة بالاستدعاءات الخارجية.

كلما أتقنت هذا المفهوم، أصبحت قادراً على بناء أنظمة DApps أكثر احترافية، سواء كنت تبني بروتوكولاً مالياً، أو منصة عضويات، أو طبقة خدمات تتكامل مع عدة عقود مستقلة ضمن منظومة مدخل إلى Web3: ما هو البلوكتشين ولماذا يغير شكل الإنترنت والأنظمة المالية؟.

8 comments

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *