استلام وإرسال الأموال (Ether) برمجياً: فهم الدوال payable و fallback
استلام وإرسال الأموال (Ether) برمجياً: فهم الدوال payable و fallback
التعامل مع الأموال داخل عقود Ethereum ليس مجرد استقبال رصيد وإرساله، بل هو جزء حساس من منطق التطبيق نفسه. فعند بناء Smart Contracts تتعامل مباشرة مع قيمة مالية حقيقية أو تجريبية، لذلك يجب فهم كيفية استقبال Ether وتوجيهه ومعالجة الحالات غير المتوقعة بدقة.
هذا الموضوع يبني عملياً على مفاهيم شرحت سابقاً في مدخل إلى Web3: ما هو البلوكتشين ولماذا يغير شكل الإنترنت والأنظمة المالية؟، كما يعتمد على فهم الدوال في الدوال (Functions) في Solidity: من يمكنه قراءة وتعديل بيانات العقد؟ وآليات التكاليف في التكاليف (Gas Fees): كيف يحسب البلوكتشين تكلفة تنفيذ الأكواد؟.
في هذا المقال سنفكك سلوك الكلمات المفتاحية payable، والدالتين receive() وfallback()، ثم ننتقل إلى إرسال الأموال برمجياً بطريقة أكثر أماناً باستخدام call.
ما معنى payable داخل Solidity؟
الكلمة payable تعني أن الدالة أو العنوان يمكنه استقبال Ether. إذا حاولت إرسال قيمة مالية إلى دالة غير موسومة بهذه الكلمة، فستفشل المعاملة تلقائياً ويحدث revert.
عملياً، عند استدعاء أي دالة مع قيمة في الحقل msg.value، يجب أن تكون تلك الدالة payable. وهذه من أساسيات تصميم تدفق الأموال داخل العقد الذكي.
مثال بسيط على دالة تستقبل الأموال
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract SimpleVault {
address public owner;
constructor() {
owner = msg.sender;
}
function deposit() external payable {
// يتم استقبال Ether وإضافته لرصيد العقد تلقائياً
}
function getBalance() external view returns (uint256) {
return address(this).balance;
}
}
في المثال السابق، الدالة deposit() لا تحتوي منطقاً داخلياً معقداً، لكن مجرد تعريفها كـ external payable يجعلها قادرة على استقبال الأموال. أما رصيد العقد فيمكن قراءته عبر address(this).balance.
الفرق بين receive() و fallback()
في الإصدارات الحديثة من Solidity تم الفصل بين استقبال الأموال المباشر وبين التعامل مع الاستدعاءات غير المطابقة لتواقيع الدوال. لذلك لدينا وظيفتان خاصتان:
receive(): تُستدعى عند إرسالEtherمباشرة بدون بياناتcalldata.fallback(): تُستدعى عندما لا يجد العقد دالة مطابقة للاستدعاء، أو عندما تصل بيانات غير معروفة، ويمكن أيضاً جعلهاpayableلاستقبال الأموال.
متى يعمل كل منهما؟
- إذا أرسلت أموالاً فقط إلى العقد بدون أي بيانات، فالأولوية تكون إلى
receive(). - إذا لم تكن
receive()موجودة، وكانتfallback()معرفة كـpayable، فسيتم تنفيذها. - إذا أرسلت استدعاءً إلى دالة غير موجودة، فسيتم تشغيل
fallback().
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract EtherReceiver {
event Received(address indexed sender, uint256 amount, string functionType);
receive() external payable {
emit Received(msg.sender, msg.value, "receive");
}
fallback() external payable {
emit Received(msg.sender, msg.value, "fallback");
}
function getBalance() external view returns (uint256) {
return address(this).balance;
}
}
استخدام الأحداث (Events): كيف يخبر العقد الذكي واجهة الموقع (React) بأن شيئاً ما قد حدث؟ هنا مهم جداً، لأنك تستطيع تتبع هل وصلت الأموال عبر receive() أم عبر fallback().
إرسال Ether من العقد إلى عنوان آخر
استقبال الأموال نصف الصورة فقط. النصف الآخر هو إرسالها بشكل موثوق. في الماضي كان المطورون يستخدمون transfer وsend، لكن النهج الأحدث والأكثر مرونة هو استخدام call.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract SmartWallet {
address public owner;
event Deposited(address indexed from, uint256 amount);
event Withdrawn(address indexed to, uint256 amount);
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
constructor() {
owner = msg.sender;
}
receive() external payable {
emit Deposited(msg.sender, msg.value);
}
function deposit() external payable {
emit Deposited(msg.sender, msg.value);
}
function withdraw(address payable to, uint256 amount) external onlyOwner {
require(address(this).balance >= amount, "Insufficient balance");
(bool success, ) = to.call{value: amount}("");
require(success, "Transfer failed");
emit Withdrawn(to, amount);
}
function contractBalance() external view returns (uint256) {
return address(this).balance;
}
}
لاحظ هنا أن العنوان المستلم تم تعريفه كنوع address payable لأننا نريد إرسال أموال إليه. كما أن الدالة withdraw() محمية بمعدل onlyOwner، وهي فكرة مرتبطة مباشرة بما شرحناه في المعدلات (Modifiers): حماية الدوال برمجياً.
إرسال الأموال عبر
callأكثر مرونة منtransfer، لكنه يتطلب فحص قيمةsuccessدائماً. تجاهل نتيجة الإرسال قد يسبب أخطاء مالية أو منطقية داخل العقد.
مخاطر أمنية مرتبطة باستقبال وإرسال الأموال
أي عقد يتعامل مع الرصيد يجب أن يُراجع بعقلية هجومية. ليس كل خطر ناتجاً عن اختراق معقد؛ أحياناً مجرد ترتيب خاطئ لسطور الكود يؤدي إلى ثغرة. لهذا من المهم إتقان التحقق من الشروط باستخدام التعامل مع الأخطاء وإرجاع الأموال: استخدام require, assert, revert.
أهم المخاطر الشائعة
- السماح لأي مستخدم بسحب الرصيد لغياب التحقق من الصلاحيات.
- تنفيذ استدعاء خارجي قبل تحديث الحالة الداخلية، ما قد يفتح الباب لهجوم
Reentrancy. - الاعتماد على
fallback()كمنطق أساسي معقد، ما يصعّب المراجعة والاختبار. - عدم تسجيل العمليات عبر
events، فيصعب تتبع التدفقات المالية.
قاعدة أمنية مهمة: اتبع نمط
Checks-Effects-Interactions. تحقق أولاً من الشروط، ثم حدّث الحالة الداخلية، وبعدها فقط نفّذ الاستدعاء الخارجي الذي ينقل الأموال.
كيف تختبر هذه الدوال عملياً؟
يمكنك تجربة هذه السيناريوهات بسهولة عبر محرر Remix IDE: كتابة ونشر أول عقد ذكي أو عبر Hardhat. وإذا لم تكن أعددت البيئة بعد، فارجع إلى إعداد بيئة التطوير: تثبيت محفظة MetaMask والاتصال بشبكات الاختبار ثم إلى الحصول على عملات تجريبية مجانية (Faucet).
سيناريو اختبار مقترح
- انشر عقداً يحتوي على
receive()وfallback(). - أرسل إلى العقد قيمة بدون بيانات، ثم راقب الحدث المسجل.
- نفّذ استدعاءً باسم دالة غير موجودة مع إرسال قيمة، ثم تأكد من تشغيل
fallback(). - اختبر السحب إلى عنوان آخر وتحقق من تغير الرصيد قبل وبعد العملية.
نصائح لتقليل استهلاك الغاز وتحسين التصميم
معالجة الأموال تؤثر مباشرة على تكلفة التشغيل، خصوصاً إذا كانت الدوال تُستدعى بكثرة داخل DApps. لذلك يجب كتابة منطق واضح ومختصر، وعدم تحميل الدوال الخاصة باستقبال الأموال أعمالاً ثقيلة غير ضرورية.
لتحسين استهلاك
Gas، اجعلreceive()وfallback()بسيطتين قدر الإمكان. لا تضف حلقاتloopsأو تحديثات تخزينية كثيرة إلا عند الضرورة القصوى.
ومن الأفضل أيضاً الفصل بين منطق الإيداع ومنطق المحاسبة المعقدة. استقبل الأموال في دالة واضحة، ثم عالج التسجيل أو التوزيع في دوال منفصلة ومدروسة. هذا يسهل التدقيق الأمني ويحسن قابلية الصيانة.
الخلاصة
فهم payable هو الأساس لأي عقد ذكي يتعامل مع القيمة المالية. أما receive() وfallback() فهما خط الدفاع والسلوك الافتراضي عند وصول الأموال أو الاستدعاءات غير المتوقعة. وعند الإرسال، يبقى استخدام call مع التحقق الأمني الصارم هو الخيار العملي في كثير من السيناريوهات الحديثة.
إذا أتقنت هذه المفاهيم، فستصبح قادراً على بناء محافظ ذكية، أنظمة إيداع، عقود اشتراكات، وبوابات دفع داخل تطبيقات Web3 بشكل أكثر أماناً واحترافية.
12 comments