Deep EVM #4: أساسيات الأمان — msg.sender والتحكم في الوصول وإعادة الدخول
Engineering Team
نموذج أمان EVM
أمان العقود الذكية يختلف جوهرياً عن أمان البرمجيات التقليدية. في البرمجيات التقليدية، الأخطاء تسبب أعطالاً. في العقود الذكية، الأخطاء تتسبب في خسائر مالية لا رجعة فيها.
ثلاث خصائص تجعل أمان EVM فريداً:
- الثبات — الكود المنشور لا يمكن تصحيحه (إلا عبر أنماط الوكيل)
- الشفافية — البايتكود مرئي للجميع، مما يعني أن المهاجمين يمكنهم دراسة عقدك
- التنفيذ الذري — المعاملات إما تنجح بالكامل أو تفشل بالكامل
msg.sender مقابل tx.origin
- msg.sender — عنوان المستدعي المباشر (عقد أو حساب خارجي)
- tx.origin — الحساب الخارجي الذي بدأ المعاملة (دائماً EOA)
لا تستخدم أبداً tx.origin للتحقق من الصلاحيات:
// خطر! عرضة لهجمات التصيد
function withdraw() external {
require(tx.origin == owner); // المهاجم يستدرج المالك لاستدعاء عقد خبيث
payable(msg.sender).transfer(address(this).balance);
}
// آمن
function withdraw() external {
require(msg.sender == owner);
payable(msg.sender).transfer(address(this).balance);
}
سيناريو الهجوم: ينشر المهاجم عقداً يستدعي withdraw() على عقدك. إذا تفاعل المالك مع عقد المهاجم (عبر معاملة تبدو بريئة)، فإن tx.origin سيكون عنوان المالك، مما يمنح التحقق.
هجمات إعادة الدخول
إعادة الدخول هي أكثر ثغرات العقود الذكية شهرة. تحدث عندما يستدعي عقدك عقداً خارجياً قبل تحديث حالته الخاصة.
// عرضة لإعادة الدخول!
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount);
// 1. إرسال ETH (يستدعي receive() على المستلم)
(bool success,) = msg.sender.call{value: amount}("");
require(success);
// 2. تحديث الرصيد — متأخر جداً!
balances[msg.sender] -= amount;
}
المهاجم ينشر عقداً مع:
receive() external payable {
if (address(victim).balance >= 1 ether) {
victim.withdraw(1 ether); // إعادة الدخول!
}
}
الحل: نمط الفحوصات-التأثيرات-التفاعلات
function withdraw(uint256 amount) external {
// الفحوصات
require(balances[msg.sender] >= amount);
// التأثيرات (تحديث الحالة أولاً)
balances[msg.sender] -= amount;
// التفاعلات (الاستدعاء الخارجي أخيراً)
(bool success,) = msg.sender.call{value: amount}("");
require(success);
}
حارس إعادة الدخول (ReentrancyGuard)
OpenZeppelin توفر حارس إعادة الدخول كخط دفاع إضافي:
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract Vault is ReentrancyGuard {
function withdraw(uint256 amount) external nonReentrant {
// محمي من إعادة الدخول
}
}
مخاطر DELEGATECALL
DELEGATECALL ينفذ كود عقد آخر في سياق تخزين العقد المستدعي. هذا قوي لكنه خطير:
// العقد A يستدعي العقد B عبر delegatecall
// كود B يُنفَّذ لكنه يقرأ/يكتب في تخزين A
(bool success,) = addressB.delegatecall(
abi.encodeWithSignature("setOwner(address)", attacker)
);
// إذا كان B يعدل فتحة 0، فهو يعدل فتحة 0 في A!
القاعدة: لا تستخدم delegatecall أبداً مع عقود غير موثوقة. في أنماط الوكيل، تأكد من أن عقد التنفيذ ليس له سلطة تعديل منطق الوكيل.
أنماط التحكم في الوصول
Ownable (مالك واحد)
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
التحكم القائم على الأدوار
import "@openzeppelin/contracts/access/AccessControl.sol";
contract MyContract is AccessControl {
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN");
bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR");
function adminAction() external onlyRole(ADMIN_ROLE) {
// فقط المسؤولون
}
}
التحقق الرسمي
للعقود عالية القيمة، التدقيق البشري ليس كافياً. التحقق الرسمي يثبت رياضياً أن العقد يتصرف وفق مواصفاته:
- Certora — لغة مواصفات CVL + محقق آلي
- KEVM — دلالات K لـ EVM، يمكنه إثبات خصائص على مستوى البايتكود
- Solidity SMTChecker — محقق مدمج في المترجم
الخلاصة
أمان العقود الذكية يتطلب عقلية دفاعية. افترض أن كل استدعاء خارجي معادٍ. حدّث الحالة قبل التفاعلات الخارجية. لا تثق بـ tx.origin. استخدم أنماط التحكم في الوصول المُختبرة. واحصل دائماً على تدقيق احترافي قبل النشر على الشبكة الرئيسية.