> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# أمن العقود الذكية: أشهر الثغرات وحوادثها الحقيقية
- URL: https://arabroot.io/articles/امن-العقود-الذكية-اشهر-الثغرات-وحوادثها-الحقيقية/
- Published: 2026-10-28T06:30:30.000Z
- Updated: 2026-10-28T07:37:40.000Z
- Description: نكتب عقوداً معطوبة ونهاجمها على شبكة محلية لنرى كل صنف ثغرة يعمل، ثم نضع النسخة المصححة بجانبه، من جهة الدفاع.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, البلوكتشين والعملات الرقمية

مرت على فريق منصة العمل الحر أربعة مقالات: فهموا كيف يعمل Ethereum، وكتبوا عقد الضمان، واختبروه بالثوابت، وتعلموا الرموز. وفي كل مقال مر بهم خطأ كان سيكلف مالاً لو نشر: أحداث أطلقت بعد إرسال المال، ومفتاح إدارة يغير القواعد، وعقد تنفيذ لم يهيأ ملكه أول من استدعاه، ورموز علقت إلى الأبد. وكل واحد من هذه الأخطاء ليس حادثاً فردياً، وإنما نسخة من صنف معروف من الثغرات، له اسم وحوادث موثقة بمئات الملايين، وله إصلاح معروف أيضاً. ولذلك قرر الفريق قبل أن يقترب من أي مال حقيقي أن يدرس هذه الأصناف واحداً واحداً، لا ليتعلم كيف يهاجم، وإنما ليعرف كيف يدافع: ما الخطأ، وكيف يبدو في الكود، وما الذي حدث فعلاً عندما أهمله آخرون، وكيف يصلح.

وهذا المقال تعليمي تقني وموجه للدفاع فقط. كل مثال معطوب فيه عقد صغير جداً نكتبه ونشغله على شبكة تطوير محلية بحسابات اختبار ليس لها قيمة، وبجانبه النسخة المصححة مباشرة، والمهاجم فيه عقد اختبار يهاجم عقدنا نحن على جهازنا، والهدف أن ترى الثغرة تعمل لتفهم لماذا يجب أن تمنعها. وهو المقال الخامس في مرحلة Ethereum، ويبني على [مقال العقود الذكية](https://arabroot.io/articles/%D8%A7%D9%84%D8%B9%D9%82%D9%88%D8%AF-%D8%A7%D9%84%D8%B0%D9%83%D9%8A%D8%A9-smart-contracts-%D9%85%D8%A7-%D9%87%D9%8A-%D9%88%D9%85%D8%A7-%D9%85%D8%AE%D8%A7%D8%B7%D8%B1%D9%87%D8%A7/) و[مقال أدوات التطوير](https://arabroot.io/articles/%D8%AA%D8%B7%D9%88%D9%8A%D8%B1-%D8%A7%D9%84%D8%B9%D9%82%D9%88%D8%AF-%D8%A7%D9%84%D8%B0%D9%83%D9%8A%D8%A9-%D8%A8-solidity-%D9%85%D8%B9-foundry-%D9%88-hardhat/).

وسوف نناقش في هذا المقال ما يلي:

- كيف نرتب أصناف الثغرات، ولماذا نتعلم الهجوم من جهة الدفاع.
- إعادة الدخول Reentrancy: أشهر صنف، بتجربة كاملة يفرغ فيها المهاجم خزنة، ثم النسخة الآمنة والحارس، وحادثة The DAO.
- التحكم في الصلاحيات Access Control: دالة نسيت فحص المالك، وإصلاحها، وحادثة جسر Ronin.
- التلاعب بالأوراكل Price Oracle Manipulation: مقرض يقرأ سعراً لحظياً يسهل تحريكه، وحادثة Mango Markets.
- تجاوز سعة الأعداد الصحيحة Integer Overflow، والاستدعاء غير المفحوص Unchecked Call، وأصناف أخرى.
- التحليل الآلي مع المدقق المدمج وأداة Slither، ودفاع الطبقات الخمس، وقائمة تحقق للفريق.

## لماذا نتعلم الهجوم من جهة الدفاع؟

قد يبدو غريباً أن يكتب الفريق عقوداً معطوبة ويهاجمها، ولكن هذا هو بالضبط ما تفعله المراجعة الأمنية الجادة، لأنك لا تستطيع أن تمنع ثغرة لا تفهم كيف تعمل. والفرق بين هذا وبين تعليم الهجوم أن كل ما سوف نهاجمه هو عقودنا نحن على شبكتنا المحلية، بمبالغ وهمية، وأن كل مثال معطوب يأتي بجانبه الإصلاح مباشرة، فالهدف هو السطر الذي يجب أن تكتبه، لا السطر الذي يجب أن تستغله. وهذه هي طريقة [توثيق الأمان في ethereum.org](https://ethereum.org/en/developers/docs/smart-contracts/security/?ref=arabroot.io) نفسه، وطريقة قوائم الثغرات المعروفة.

ولتنظيم الأصناف نستخدم قائمة [OWASP Smart Contract Top 10](https://owasp.org/www-project-smart-contract-top-10/?ref=arabroot.io) لسنة 2025، وهي ترتب أشهر عشرة أصناف، أولها التحكم في الصلاحيات، ثم التلاعب بالأوراكل، ثم أخطاء المنطق، ثم نقص التحقق من المدخلات، ثم إعادة الدخول، ثم الاستدعاء غير المفحوص، ثم هجمات القروض السريعة، ثم تجاوز سعة الأعداد، ثم العشوائية غير الآمنة، ثم حجب الخدمة. وسوف نأخذ أربعة منها بتجارب كاملة، ونمر على الباقي بإيجاز، لأن الأربعة تغطي الأنماط الأساسية: استدعاء خارجي في وقت خاطئ، وفحص غائب، ومعلومة يثق بها العقد ويمكن تحريكها، وحساب خاطئ.

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

نجهز مشروع Foundry في `lab/sec` كما في المقال السابق، بملف إعداد يربط مكتبة OpenZeppelin، ونشغل Anvil، ونحمل المكتبتين:

```bash
docker run -d --name lab-sec -p 127.0.0.1:8545:8545 -v "$PWD/lab:/lab" -w /lab \
  ghcr.io/foundry-rs/foundry:v1.8.6 "anvil --host 0.0.0.0"
docker exec -w /lab/sec lab-sec forge install --no-git OpenZeppelin/openzeppelin-contracts@v5.7.0
docker exec -w /lab/sec lab-sec forge install --no-git foundry-rs/forge-std@v1.17.0
```

## الصنف الأول: إعادة الدخول Reentrancy

هذا الصنف هو الذي أسقط The DAO سنة 2016، وما زال من أكثر الثغرات تكراراً. وفكرته أن عقدك يستدعي عقداً خارجياً، ثم يكمل عمله بعد عودة الاستدعاء، ولكن العقد الخارجي يستطيع أثناء الاستدعاء أن يعيد الدخول إلى عقدك قبل أن يكمل، فيجد الحالة كما كانت قبل أن يحدثها. ولنرى ذلك نكتب خزنة بسيطة، مثل صندوق إكراميات مشترك يودع فيه كل مستقل ويسحب رصيده لاحقاً، في الملف `src/Vault.sol`، وفيه النسختان المعطوبة والآمنة معاً:

```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.36;

/// A shared tip jar: each freelancer deposits, and withdraws their own balance later.
/// VulnerableVault sends the ether BEFORE zeroing the balance: the reentrancy class (The DAO, 2016).
contract VulnerableVault {
    mapping(address => uint256) public balance;

    function deposit() external payable {
        balance[msg.sender] += msg.value;
    }

    function withdraw() external {
        uint256 amount = balance[msg.sender];
        require(amount > 0, "nothing to withdraw");
        (bool ok, ) = msg.sender.call{value: amount}(""); // external call first
        require(ok, "send failed");
        balance[msg.sender] = 0;                           // state updated after: too late
    }
}

/// The fix: checks, then effects (zero the balance), then the interaction. Same logic, safe order.
contract SafeVault {
    mapping(address => uint256) public balance;

    function deposit() external payable {
        balance[msg.sender] += msg.value;
    }

    function withdraw() external {
        uint256 amount = balance[msg.sender];
        require(amount > 0, "nothing to withdraw");
        balance[msg.sender] = 0;                            // effects first
        (bool ok, ) = msg.sender.call{value: amount}("");   // interaction last
        require(ok, "send failed");
    }
}

/// The attacker contract: deposits 1 ether, then re-enters withdraw() from its receive().
contract Attacker {
    address public target;
    uint256 public hits;

    constructor(address _target) { target = _target; }

    function attack() external payable {
        (bool ok1, ) = target.call{value: 1 ether}(abi.encodeWithSignature("deposit()"));
        require(ok1, "deposit failed");
        (bool ok2, ) = target.call(abi.encodeWithSignature("withdraw()"));
        require(ok2, "withdraw failed");
    }

    receive() external payable {
        hits++;
        if (target.balance >= 1 ether) {
            target.call(abi.encodeWithSignature("withdraw()"));
        }
    }
}
```

الفرق بين النسختين سطران بترتيب معكوس. في المعطوبة نرسل المال أولاً بالأمر `msg.sender.call{value: amount}("")`، ثم نصفر الرصيد. وعندما يكون المستقبل عقد المهاجم، فإن إرسال المال يشغل دالة `receive` فيه، وهذه الدالة تعيد استدعاء `withdraw` قبل أن يصل السطر الذي يصفر الرصيد، فيجد الرصيد كما هو، فيسحب مرة أخرى، وهكذا. والنسخة الآمنة تصفر الرصيد قبل الإرسال، فعند إعادة الدخول يجد الرصيد صفراً فيرفض. وهذا النمط اسمه «تحقق ثم عدل ثم تواصل» Checks-Effects-Interactions، وقد ذكرناه في مقال العقود الذكية، والآن نراه يعمل.

نكتب اختباراً يلعب فيه عقد المهاجم دوره، في `test/Reentrancy.t.sol`، ويودع مستقلان أمينان خمسة ETH لكل منهما، ثم يهاجم:

```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.36;

import {Test, console2} from "forge-std/Test.sol";
import {VulnerableVault, SafeVault, Attacker} from "../src/Vault.sol";

contract ReentrancyTest is Test {
    address alice = makeAddr("alice");
    address bob = makeAddr("bob");
    address evil = makeAddr("evil");

    function _seed(address vault) internal {
        // two honest freelancers each deposit 5 ether
        vm.deal(alice, 5 ether); vm.prank(alice);
        (bool a,) = vault.call{value: 5 ether}(abi.encodeWithSignature("deposit()")); require(a);
        vm.deal(bob, 5 ether); vm.prank(bob);
        (bool b,) = vault.call{value: 5 ether}(abi.encodeWithSignature("deposit()")); require(b);
    }

    function test_Vulnerable_IsDrained() public {
        VulnerableVault vault = new VulnerableVault();
        _seed(address(vault));
        Attacker attacker = new Attacker(address(vault));
        vm.deal(evil, 1 ether);
        vm.prank(evil);
        attacker.attack{value: 1 ether}();
        console2.log("attacker deposited 1, vault had 11, attacker withdrew (wei):", address(attacker).balance);
        console2.log("re-entries:", attacker.hits());
        console2.log("vault left (wei):", address(vault).balance);
        assertGt(address(attacker).balance, 1 ether); // took more than it put in
    }

    function test_Safe_Resists() public {
        SafeVault vault = new SafeVault();
        _seed(address(vault));
        Attacker attacker = new Attacker(address(vault));
        vm.deal(evil, 1 ether);
        vm.prank(evil);
        attacker.attack{value: 1 ether}();
        // the re-entrant withdraw finds a zero balance and reverts; the attacker only gets its own 1 back
        console2.log("re-entries:", attacker.hits());
        console2.log("attacker balance (wei):", address(attacker).balance);
        console2.log("vault left (wei):", address(vault).balance);
        assertEq(address(attacker).balance, 1 ether);
        assertEq(address(vault).balance, 10 ether); // the two honest deposits are untouched
    }
}
```

ثم نشغله:

```bash
docker exec -w /lab/sec lab-sec forge test --match-contract Reentrancy -vv
```

والمخرج سوف يكون كما يلي:

```text
Ran 2 tests for test/Reentrancy.t.sol:ReentrancyTest
[PASS] test_Safe_Resists() (gas: 218778)
Logs:
  re-entries: 1
  attacker balance (wei): 1000000000000000000
  vault left (wei): 10000000000000000000

[PASS] test_Vulnerable_IsDrained() (gas: 302909)
Logs:
  attacker deposited 1, vault had 11, attacker withdrew (wei): 11000000000000000000
  re-entries: 11
  vault left (wei): 0

Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 1.62ms
```

في المخرج أعلاه لاحظ التالي:

- في النسخة المعطوبة أودع المهاجم 1 ETH فقط، ثم خرج برصيد 11 ETH، أي كل ما في الخزنة: ودائعه هو وودائع المستقلين. وعداد إعادة الدخول `hits` يساوي 11، أي أن دالة `receive` في عقده استدعت `withdraw` إحدى عشرة مرة في معاملة واحدة قبل أن تفرغ الخزنة. والخزنة بقي فيها صفر.
- في النسخة الآمنة، بنفس المهاجم بالضبط، خرج المهاجم برصيد 1 ETH فقط، أي وديعته هو، وبقي في الخزنة 10 ETH، أي ودائع المستقلين كاملة. وعداد إعادة الدخول 1، أي أن `receive` حاول الدخول مرة واحدة فوجد رصيده صفراً فرفض العقد السحب، فتوقف.
- الاختباران نجحا، لأن اختبار النسخة المعطوبة يتوقع أن يسرق المهاجم، واختبار النسخة الآمنة يتوقع أنه لا يستطيع. وهذا هو المعنى العملي لاختبار الأمان: أن تكتب الهجوم كاختبار، وتتأكد أن الإصلاح يوقفه.

والصورة التالية تضع النسختين جنباً إلى جنب مع الأرقام:

![مقارنة بين نسختين من دالة withdraw: على اليمين الخزنة المعطوبة ترسل المال بالأمر call أولاً في السطر 1 ثم تصفر الرصيد في السطر 2، مع ملاحظة أن المهاجم أودع 1 وسحب 11 وأعاد الدخول 11 مرة والخزنة صارت 0، وعلى اليسار الخزنة الآمنة تصفر الرصيد في السطر 1 ثم ترسل في السطر 2 مع ملاحظة أن المهاجم استرد 1 فقط وودائع المستخدمين 10 لم تمس، وفي الأسفل صندوق أزرق عن حادثة The DAO في 17 يونيو 2016 والرد بالتفرع الصلب عند الكتلة 1920000 وإنشاء Ethereum Classic، وأن إعادة الدخول البند الخامس في قائمة OWASP لسنة 2025](https://arabroot.io/content/images/2026/10/smart-contract-security-01-reentrancy.webp)

نفس المنطق بترتيبين، والفرق بينهما سرقة الخزنة كلها أو حمايتها

والصورة التالية تفصل خطوات الهجوم في المعاملة الواحدة:

![مخطط تسلسلي بين عقد المهاجم والخزنة المعطوبة: الخطوة 1 deposit مع 1 ETH فيصبح رصيد المهاجم 1، الخطوة 2 withdraw، الخطوة 3 الخزنة ترسل 1 ETH فتشغل receive في المهاجم ولاحظ أن الرصيد لم يصفر بعد، الخطوة 4 withdraw مرة أخرى والرصيد ما زال 1، ثم حلقة تتكرر فيها الخطوتان 3 و4 إحدى عشرة مرة حتى تفرغ الخزنة، الخطوة 5 receive يتوقف عندما يصبح رصيد الخزنة أقل من 1 ETH، وفي الأسفل أن كل الدخولات في معاملة واحدة لا تملك أي عقدة أن توقفها والخزنة تخرج 11 ETH مقابل إيداع 1](https://arabroot.io/content/images/2026/10/smart-contract-security-02-attack-flow.webp)

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

📌

في 17 يونيو 2016 كتب Vitalik Buterin في [بيان مؤسسة Ethereum](https://blog.ethereum.org/2016/06/17/critical-update-re-dao-vulnerability?ref=arabroot.io) أن مهاجماً استغل ثغرة الاستدعاء المتكرر Recursive Calling في دالة `split` في عقد The DAO، وهي نفس الثغرة التي رأيناها: استدعاء خارجي يسمح بإعادة الدخول قبل تحديث الحالة، فجمع المهاجم الأموال مراراً في نفس العملية. وكان الرد الذي اختاره المجتمع تفرعاً صلباً Hard Fork عند الكتلة 1,920,000 أعاد الأموال، واعترض جزء من المجتمع فاستمر على السلسلة القديمة التي صارت Ethereum Classic، كما رويناها في [مقال البلوكتشين للمبتدئين](https://arabroot.io/articles/%D8%A7%D9%84%D8%A8%D9%84%D9%88%D9%83%D8%AA%D8%B4%D9%8A%D9%86-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86-%D9%83%D9%8A%D9%81-%D9%8A%D8%B9%D9%85%D9%84-%D9%88%D9%85%D8%AA%D9%89-%D9%84%D8%A7-%D8%AA%D8%AD%D8%AA%D8%A7%D8%AC%D9%87/). والبيان نفسه قال إن الثغرة تخص عقد The DAO وحده وأن Ethereum سليمة، وهذا هو الدرس: المشكلة ليست في الشبكة، وإنما في سطرين بترتيب خاطئ.

وقد يتساءل البعض: لماذا يسمح Ethereum بإعادة الدخول أصلاً، أليس من الأفضل أن تمنع الشبكة عقداً من استدعاء عقد آخر يعيد استدعاءه؟ والإجابة أن القدرة على استدعاء العقود بعضها بعضاً هي أقوى ما في Ethereum، وهي التي تجعل العقود كقطع تركيب يبني بعضها على بعض Composability، كما ذكرنا في مقال العقود الذكية. فالشبكة لا تستطيع أن تمنع إعادة الدخول دون أن تمنع هذه القدرة، ولذلك تركت الأمر لمن يكتب العقد: أنت من يقرر متى تحدث حالتك، وقبل أي استدعاء خارجي. والمشكلة أن الترتيب الطبيعي الذي يكتبه المطور بلا تفكير، أي «احسب ثم أرسل ثم سجل»، هو بالضبط الترتيب الخطر، لأنه يؤجل التسجيل إلى ما بعد الإرسال. ولهذا السبب ما زال هذا الصنف يتكرر بعد عشر سنوات من The DAO رغم أنه أشهر ثغرة في المجال: لأنه يأتي من عادة برمجية سليمة في كل مكان آخر، ولا يصبح خطأ إلا هنا، حيث الاستدعاء الخارجي يعطي الطرف الآخر التحكم في منتصف دالتك.

ولاحظ تفصيلاً مهماً في عقد المهاجم: دالة `receive` فيه هي التي تنفذ عندما يصله المال، وهي التي تعيد الدخول. والمحفظة العادية لشخص حقيقي ليس فيها كود، فلا تعيد الدخول، ولهذا لا يظهر الخطر إلا عندما يكون المستقبل عقداً كتبه أحد لهذا الغرض. وهذا يخدع كثيراً من المطورين: يختبرون عقدهم بحسابات عادية فيعمل كل شيء، ولا تظهر الثغرة إلا عندما يأتي مهاجم بعقد مصمم. ولذلك لا يكفي أن تختبر المسار السعيد بحسابات أمينة، بل يجب أن تكتب المهاجم نفسه كما فعلنا، أو تستخدم الاختبار العشوائي والثوابت التي رأيناها في مقال أدوات التطوير، فالثابت «المال يخرج من الخزنة بقدر ما دخل» كان سيفشل مع العقد المعطوب في أول تسلسل يصل إليه.

### إذا لم تكف إعادة الترتيب: الحارس

إعادة ترتيب السطرين تكفي أغلب الحالات، ولكن بعض العقود تستدعي عقوداً خارجية في أماكن متعددة، أو يصعب فيها ضمان الترتيب في كل دالة، أو توجد فيها إعادة دخول بين دالتين مختلفتين. ولهذه الحالات توجد طبقة ثانية: قفل يمنع أي دخول آخر إلى العقد أثناء تنفيذ الأول. ومكتبة OpenZeppelin توفره جاهزاً باسم `ReentrancyGuard`، وهذا عقد يستخدمه في `src/GuardedVault.sol`:

```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.36;

import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

/// Same vault, protected by a lock that refuses a second entry while the first is still running.
contract GuardedVault is ReentrancyGuard {
    mapping(address => uint256) public balance;

    function deposit() external payable {
        balance[msg.sender] += msg.value;
    }

    function withdraw() external nonReentrant {
        uint256 amount = balance[msg.sender];
        require(amount > 0, "nothing to withdraw");
        balance[msg.sender] = 0;
        (bool ok, ) = msg.sender.call{value: amount}("");
        require(ok, "send failed");
    }
}
```

الكلمة `nonReentrant` على الدالة تجعلها ترفع قفلاً عند الدخول وتنزله عند الخروج، وأي استدعاء يصل والقفل مرفوع يفشل. وجربنا نفس المهاجم عليه فخرج برصيده هو فقط، كما في النسخة الآمنة. والقاعدة: اجعل الترتيب الصحيح هو الأساس دائماً، وأضف الحارس طبقة ثانية في الدوال التي تحرك مالاً وتستدعي عقوداً خارجية، فالحارس وحده مع ترتيب خاطئ قد لا يكفي ضد إعادة الدخول بين دالتين.

## الصنف الثاني: التحكم في الصلاحيات Access Control

هذا هو أول صنف في قائمة OWASP، وأبسطه: دالة حساسة تنسى أن تتحقق من هوية من يستدعيها. ورأينا نسخة منه في مقال العقود الذكية مع عقد Parity الذي لم يهيأ فملكه أول من استدعاه. والمثال التالي أوضح: خزينة منصة فيها دالة تسحب كل الرصيد، ونسيت فحص المالك، في `src/AccessControl.sol`:

```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.36;

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

/// A platform treasury. The "withdraw everything" function forgot its access check (access control class).
contract VulnerableTreasury {
    address public owner;
    constructor() payable { owner = msg.sender; }

    // BUG: no "only owner" check. Anyone can sweep the balance.
    function withdrawAll(address to) external {
        (bool ok, ) = to.call{value: address(this).balance}("");
        require(ok, "send failed");
    }
}

/// The fix: the same function, guarded by Ownable from a reviewed library.
contract SafeTreasury is Ownable {
    constructor() payable Ownable(msg.sender) {}

    function withdrawAll(address to) external onlyOwner {
        (bool ok, ) = to.call{value: address(this).balance}("");
        require(ok, "send failed");
    }
}
```

العقد المعطوب يحفظ المالك في المنشئ، ولكن الدالة `withdrawAll` لا تستعمله، فأي شخص يستطيع أن يستدعيها ويسحب الرصيد إلى عنوانه. والعقد الآمن يرث `Ownable` من المكتبة، ويضع الكلمة `onlyOwner` على الدالة، فترفض أي مستدع غير المالك. واختبرنا الحالتين في `test/AccessAndOracle.t.sol`، ونشغل جزء الصلاحيات:

```bash
docker exec -w /lab/sec lab-sec forge test --match-contract AccessControlTest -vv
```

والمخرج سوف يكون كما يلي:

```text
Ran 2 tests for test/AccessAndOracle.t.sol:AccessControlTest
[PASS] test_Safe_OnlyOwner() (gas: 35631)
[PASS] test_Vulnerable_AnyoneSweeps() (gas: 48151)
Logs:
  stranger balance (wei): 10000000000000000000
```

في النسخة المعطوبة سحب غريب لا علاقة له بالمنصة كل الرصيد، 10 ETH، إلى عنوانه، ونجح الاختبار الذي يتوقع ذلك. وفي النسخة الآمنة رفض العقد الغريب بالخطأ `OwnableUnauthorizedAccount`، وبقي الرصيد كاملاً. والفرق سطر واحد: الكلمة `onlyOwner`. وهذا الصنف يشمل أكثر من نسيان الفحص: صلاحية تعطى لعنوان خاطئ، أو دالة تهيئة يستطيع أي شخص استدعاؤها، أو مالك واحد يملك كل شيء دون مهلة ولا توزيع، وكلها رأينا أمثلة عليها في مقال العقود الذكية.

وقد يبدو هذا الصنف سخيفاً، فمن ينسى فحص المالك على دالة تسحب كل المال؟ ولكنه في الواقع أول صنف في قائمة OWASP لأنه الأكثر حدوثاً، والسبب أن الصلاحيات في العقود الحقيقية ليست مالكاً واحداً ودالة واحدة، وإنما شبكة من الأدوار: من يصدر الرموز، ومن يوقف العقد، ومن يغير الإعدادات، ومن يرقي العقد، وكل دور على عدة دوال، وكل دالة يجب أن تحمل الفحص الصحيح. ويكفي أن تنسى الفحص على دالة واحدة من عشرات، أو أن تعطي دوراً لعنوان خطأ عند النشر، أو أن تضيف دالة جديدة في ترقية وتنسى حراستها، ليفتح الباب. ولهذا لا يعتمد الفريق على الانتباه، وإنما يستخدم نظام أدوار جاهزاً مثل `AccessControl` من OpenZeppelin، ويكتب اختباراً لكل دالة حساسة يتأكد أنها ترفض كل من ليس له الدور، فالاختبار يمسك ما ينساه الانتباه.

وهناك وجه آخر لهذا الصنف يخطئ فيه الفريق في الاتجاه المعاكس: أن تحرس الدالة جيداً، ولكن تجمع كل الصلاحيات في مفتاح واحد يملكه شخص واحد. فالعقد هنا آمن ضد الغرباء، ولكنه ليس آمناً ضد صاحب المفتاح نفسه، سواءً أساء استخدامه أو سرق منه. وهذا بالضبط ما رأيناه في مقال العقود الذكية عندما غيرنا الرسوم من 5% إلى 50% بمعاملة واحدة من المالك. ولذلك حراسة الصلاحية الكاملة تتطلب شيئين: أن ترفض الدالة الغرباء، وأن يكون المفتاح نفسه موزعاً، أي محفظة متعددة التوقيع تتطلب عدة أشخاص لأي إجراء خطير، مع مهلة زمنية تعطي المستخدمين وقتاً للخروج قبل أن ينفذ أي تغيير.

📌

في مارس 2022 أفرغ مهاجم جسر شبكة Ronin، وهي شبكة مرتبطة بلعبة Axie Infinity، بعد أن تحكم بخمسة من أصل تسعة مفاتيح تدقيق يحتاجها الجسر لإقرار السحب، فأصبح يملك الأغلبية التي تسمح بسحب الأموال. وهذا في جوهره فشل في التحكم في الصلاحيات على مستوى المفاتيح وليس على مستوى سطر في العقد، فالصلاحية الكافية اجتمعت في يد واحدة. وفي 14 أبريل 2022 أضافت وزارة الخزانة الأمريكية في [إجراء مكتب OFAC](https://ofac.treasury.gov/recent-actions/20220414?ref=arabroot.io) عنوان Ethereum للمهاجم إلى قائمة العقوبات ونسبته إلى مجموعة Lazarus. والدرس الذي يهم الفريق: حراسة الصلاحية لا تنتهي عند الكود، فمن يملك مفاتيح الإدارة، وكم مفتاحاً يلزم لأي إجراء خطير، وأين تحفظ هذه المفاتيح، أسئلة أمنية بقدر أهمية أي سطر `onlyOwner`، ولذلك أوصينا في مقال العقود الذكية بمحفظة متعددة التوقيع ومهلة زمنية.

## الصنف الثالث: التلاعب بالأوراكل Price Oracle Manipulation

في مقال الرموز قلنا إن العقد لا يرى العالم الخارجي، ويحتاج أوراكل يكتب له المعلومة. والصنف الثاني في قائمة OWASP هو أن يثق العقد بمصدر سعر يمكن تحريكه. والمثال الأشهر مقرض يقرأ السعر من مجمع Pool صغير يعمل بقاعدة حاصل الضرب الثابت x · y = k، وهذه القاعدة تجعل السعر اللحظي يتحرك بشدة مع أي صفقة كبيرة. نكتب المجمع والمقرض في `src/Oracle.sol`:

```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.36;

interface IPool { function spotPrice() external view returns (uint256); }

/// A tiny constant-product pool (x * y = k) whose instantaneous price is easy to move with a big trade.
contract MiniPool is IPool {
    uint256 public reserveToken; // the project token
    uint256 public reserveUsd;   // a stable unit

    constructor(uint256 t, uint256 u) { reserveToken = t; reserveUsd = u; }

    /// price of 1 token in USD units, scaled by 1e18
    function spotPrice() external view returns (uint256) {
        return reserveUsd * 1e18 / reserveToken;
    }

    /// buy tokens with USD: moves the price up (fewer tokens left, more USD in)
    function buy(uint256 usdIn) external returns (uint256 tokensOut) {
        uint256 k = reserveToken * reserveUsd;
        uint256 newUsd = reserveUsd + usdIn;
        uint256 newToken = k / newUsd;
        tokensOut = reserveToken - newToken;
        reserveToken = newToken;
        reserveUsd = newUsd;
    }
}

/// A lender that reads the pool's SPOT price to value collateral. This is the oracle-manipulation class.
contract VulnerableLender {
    IPool public pool;
    constructor(IPool _pool) { pool = _pool; }

    /// how much USD you may borrow against `tokenCollateral` tokens, at the current spot price
    function borrowable(uint256 tokenCollateral) external view returns (uint256) {
        return tokenCollateral * pool.spotPrice() / 1e18;
    }
}
```

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

```bash
docker exec -w /lab/sec lab-sec forge test --match-contract OracleTest -vv
```

والمخرج سوف يكون كما يلي:

```text
Ran 1 test for test/AccessAndOracle.t.sol:OracleTest
[PASS] test_SpotPriceIsManipulable() (gas: 96134)
Logs:
  price before (1e18): 1000000000000000000
  borrowable on 100k tokens before: 100000
  price after (1e18): 25000000000000000000
  borrowable on 100k tokens after: 2500000
```

قبل الهجوم كان سعر الرمز 1.0، والاقتراض على 100 ألف رمز 100 ألف وحدة. وبعد أن اشترى المهاجم في معاملة واحدة أصبح السعر 25.0، أي خمسة وعشرين ضعفاً، فأصبح نفس الضمان يقترض 2.5 مليون. والإصلاح ألا يعتمد العقد على سعر لحظة واحدة من مجمع واحد يمكن تحريكه: يستخدم سعراً متوسطاً على مدى زمني TWAP يصعب تحريكه في معاملة واحدة، أو عدة مصادر مستقلة، أو مغذي أسعار مصمم لهذا مثل Chainlink. والقاعدة الأعم: كل معلومة يبني عليها العقد قراراً مالياً، اسأل كم يكلف تزويرها، كما قلنا في مقال العقود الذكية إن أمان العقد بقدر أمان مصدره.

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

![مخطط من جزأين: على اليمين التحكم في الصلاحيات، دالة withdrawAll بلا فحص مالك يسحب بها غريب كل الرصيد، والإصلاح onlyOwner من Ownable، وحادثة جسر Ronin في 2022 حيث تحكم المهاجم بخمسة من تسعة مفاتيح تدقيق ونسبت وزارة الخزانة OFAC عنوانه إلى مجموعة Lazarus، وعلى اليسار التلاعب بالأوراكل، مقرض يقرأ السعر اللحظي من مجمع صغير، السعر قبل 1.00 والاقتراض 100000 ثم بعد شراء بأربعة ملايين أصبح السعر 25.00 والاقتراض 2500000، والإصلاح سعر متوسط زمني TWAP أو عدة مصادر أو Chainlink، وحادثة Mango Markets في أكتوبر 2022 حيث قفز سعر MNGO 13 ضعفاً في 30 دقيقة](https://arabroot.io/content/images/2026/10/smart-contract-security-03-access-oracle.webp)

الفحص الغائب يسلم المفتاح لأي أحد، والسعر اللحظي يسلمه لمن يملك رأس مال يحركه

📌

في 11 أكتوبر 2022 نفذ شخص هذا الهجوم بالضبط على منصة Mango Markets. وكما جاء في [بلاغ هيئة CFTC الأمريكية](https://www.cftc.gov/PressRoom/PressReleases/8647-23?ref=arabroot.io)، فتح المهاجم حسابين وأخذ مركزاً كبيراً في مشتق مربوط بسعر رمز المنصة MNGO، ثم اشترى كميات كبيرة من MNGO على المنصات التي يقرأ منها الأوراكل سعره، فقفز السعر الذي يبلغه الأوراكل أكثر من ثلاثة عشر ضعفاً خلال ثلاثين دقيقة، فاستخدم قيمة مركزه المنتفخة ضماناً وسحب أكثر من 110 ملايين دولار من المنصة. والدرس أن السعر اللحظي من مصدر رقيق هو بالضبط ما تلاعب به، وأن القروض السريعة Flash Loans، وهي رأس مال ضخم يستعار داخل المعاملة الواحدة، تجعل هذا الهجوم ممكناً حتى لمن لا يملك المال أصلاً، ولذلك هي صنف مستقل في قائمة OWASP.

## الصنف الرابع: تجاوز السعة والاستدعاء غير المفحوص

نجمع هنا صنفين يرتبطان بالحساب والاستدعاء. الأول تجاوز سعة الأعداد الصحيحة Integer Overflow: الأرقام في آلة EVM بطول ثابت 256 بت، فإذا تجاوزت العملية هذا الطول التفت القيمة، مثل عداد يعود إلى الصفر بعد أكبر رقم. وقبل إصدار Solidity 0.8 كان هذا يحدث بصمت، أما منذ 0.8 فالمترجم يتحقق من كل عملية ويوقف المعاملة عند التجاوز. نكتب دالة دفع بالدفعات في `src/Overflow.sol`، بنسخة عادية محمية وأخرى تعيد السلوك القديم في كتلة `unchecked`:

```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.36;

/// The batchOverflow class (BEC token, CVE-2018-10299, April 2018): amount * count overflowed
/// a 256-bit integer on old compilers, minting value from nothing.
contract BatchPay {
    mapping(address => uint256) public balance;
    constructor() { balance[msg.sender] = 1000; }

    /// Solidity 0.8 checks arithmetic by default: this reverts on overflow instead of wrapping.
    function batchTransfer(address[] calldata to, uint256 amount) external {
        uint256 total = amount * to.length;        // checked: reverts if it overflows
        require(balance[msg.sender] >= total, "insufficient");
        balance[msg.sender] -= total;
        for (uint256 i = 0; i < to.length; i++) balance[to[i]] += amount;
    }

    /// The old, pre-0.8 behaviour, reproduced on purpose inside an unchecked block.
    function batchTransferUnchecked(address[] calldata to, uint256 amount) external {
        unchecked {
            uint256 total = amount * to.length;    // wraps around silently, as before 0.8
            require(balance[msg.sender] >= total, "insufficient");
            balance[msg.sender] -= total;
            for (uint256 i = 0; i < to.length; i++) balance[to[i]] += amount;
        }
    }
}
```

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

```bash
docker exec -w /lab/sec lab-sec forge test --match-contract OverflowTest -vv
```

والمخرج سوف يكون كما يلي:

```text
Ran 2 tests for test/Overflow.t.sol:OverflowTest
[PASS] test_ModernCompilerReverts() (gas: 28855)
[PASS] test_UncheckedWraps_MintsFromNothing() (gas: 104585)
Logs:
  sender still has: 1000
  addr(1) now has (as 1e76): 5
```

في الدالة العادية يوقف المترجم المعاملة عند التجاوز بما يسمى Panic برمز `0x11`، فلا يحدث شيء. وفي الدالة التي أعادت السلوك القديم داخل `unchecked` التف حاصل الضرب إلى صفر، فمر فحص الرصيد، وبقي رصيد المرسل 1000 لم ينقص، بينما أصبح رصيد أول مستقبل رقماً هائلاً، قرابة خمسة في عشرة أس ستة وسبعين. فالمهاجم أنشأ ثروة من لا شيء دون أن يدفع شيئاً. والإصلاح أنك تستخدم Solidity 0.8 أو أحدث ولا تضع العمليات في `unchecked` إلا إذا أثبت أنها لا تتجاوز، فالحماية اليوم تأتي من المترجم نفسه.

📌

هذا بالضبط ما حدث لرمز Beauty Ecosystem Coin في أبريل 2018، وسجل في قاعدة الثغرات الأمريكية باسم [CVE-2018-10299](https://nvd.nist.gov/vuln/detail/CVE-2018-10299?ref=arabroot.io)، وتصفه بأن تجاوزاً في دالة `batchTransfer` سمح لمهاجمين بزيادة أصولهم الرقمية دون إذن بتمرير مستقبلين مع قيمة كبيرة، وعرف باسم batchOverflow. والعقد كان مكتوباً بمترجم قديم لا يتحقق من الحساب، فأنشأ المهاجم كميات هائلة من الرمز من لا شيء. وهذا سبب أن تثبيت إصدار مترجم حديث، كما نفعل في كل تجاربنا، ليس تفصيلاً شكلياً، وإنما حماية من صنف كامل من الثغرات.

والصنف الثاني هو الاستدعاء غير المفحوص Unchecked Call. فالدالتان القديمتان `send` و`transfer` لإرسال المال تمرران كمية غاز ثابتة صغيرة هي 2300 وحدة، وهذا يكفي محفظة عادية ولكن قد لا يكفي عقداً يحتاج عملاً عند الاستلام، فيفشل الإرسال. والمشكلة أن `send` تعيد قيمة تقول هل نجحت، فإذا لم يفحصها العقد ظن أنه دفع وأكمل. ولهذا استخدمنا في كل عقودنا الأمر `call` مع فحص القيمة العائدة، كما يوصي [توثيق Solidity للاعتبارات الأمنية](https://docs.soliditylang.org/en/v0.8.36/security-considerations.html?ref=arabroot.io).

📌

في فبراير 2016 وصفت لعبة King of the Ether في [تقريرها بعد الحادثة](https://www.kingoftheether.com/postmortem.html?ref=arabroot.io) أنها دفعت التعويضات بالأمر `send` الذي يمرر 2300 غاز فقط، فلم يكف ذلك محافظ Mist القائمة على عقود، فعاد المال إلى عقد اللعبة، والعقد لم يفحص نتيجة الإرسال فظن أنه دفع وأكمل، فلم تصل بعض مدفوعات الملوك واللاعبين. والدرس في التقرير نفسه: افحص دائماً القيمة العائدة من `send` و`call` وتصرف بناءً عليها، والأفضل أن يسحب المستفيد ماله بنفسه بنمط السحب Pull بدل أن يرسل العقد لكثيرين في حلقة، لأن مستقبلاً واحداً معطلاً قد يوقف الجميع.

![مخطط من جزأين وشريط: على اليمين تجاوز سعة الأعداد، دالة batchTransfer حيث amount ضرب count في كتلة unchecked يلتف إلى صفر فيمر فحص الرصيد وينشأ مبلغ هائل من لا شيء والمرسل ما زال يملك 1000 والمستلم 5e76، والإصلاح أن Solidity 0.8 يتحقق افتراضياً فيرفض بـ panic 0x11، وحادثة رمز BEC في أبريل 2018 batchOverflow والثغرة CVE-2018-10299، وعلى اليسار الاستدعاء بلا فحص حيث send وtransfer تمرران 2300 غاز فقط فيفشل الإرسال لعقد يحتاج أكثر والعقد يظن أنه دفع، والإصلاح استخدام call وفحص القيمة العائدة ونمط السحب، وحادثة King of the Ether في فبراير 2016، وفي الأسفل أصناف أخرى من قائمة OWASP لسنة 2025: القروض السريعة والعشوائية غير الآمنة وحجب الخدمة](https://arabroot.io/content/images/2026/10/smart-contract-security-04-overflow-dos.webp)

صنفان في الحساب والاستدعاء، وكلاهما يصلحه مترجم حديث ونمط سحب وفحص القيمة العائدة

## أصناف أخرى يجب أن تعرفها

بقيت أصناف في قائمة OWASP مررنا على بعضها في المقالات السابقة، ونجمعها هنا:

- **القروض السريعة Flash Loans**: استعارة رأس مال ضخم يرد في نفس المعاملة، وهي ليست ثغرة بذاتها، ولكنها تضخم غيرها، فبها نفذ مهاجم Mango Markets تحريك السعر دون أن يملك المال أصلاً.
- **أخطاء المنطق Logic Errors**: الكود يعمل كما كتب، ولكن ما كتب خطأ، مثل أن يصرف العقد قبل التسليم، وهذه لا يجدها مدقق آلي لأنها في فهم المنطق، وتحتاج اختبار ثوابت ومراجعة بشرية كما رأينا في مقال أدوات التطوير.
- **نقص التحقق من المدخلات**: مثل عنوان صفري بلا فحص، وهو ما نبهنا إليه المدقق الآلي في عقد الضمان.
- **العشوائية غير الآمنة Insecure Randomness**: استخدام `block.timestamp` أو هاش الكتلة كمصدر عشوائية في قرعة أو توزيع، وكلاهما يستطيع مقترح الكتلة أن يؤثر فيه ضمن هامش، فلا يصلح لأي قيمة حقيقية.
- **حجب الخدمة Denial of Service**: حلقة على بيانات يتحكم فيها المستخدم تكبر حتى تتجاوز حد الغاز فتجمد الدالة، وقد رأينا هذا في مقال كيف يعمل Ethereum، والحل نمط السحب وتقسيم العمل.

ونمط السحب Pull يستحق وقفة لأنه يظهر في أكثر من صنف: إعادة الدخول، والاستدعاء غير المفحوص، وحجب الخدمة. والفكرة أن العقد لا يرسل المال لأحد، وإنما يسجل أن لكل مستفيد مبلغاً، ويأتي كل مستفيد ليسحب مبلغه بنفسه في معاملة منفصلة. وبهذا يحل ثلاث مشكلات معاً: إعادة الدخول تصبح أصعب لأن كل سحب معزول، والاستدعاء الفاشل لمستقبل واحد لا يؤثر على غيره لأن كل واحد يسحب وحده، وحجب الخدمة يختفي لأنه لا توجد حلقة ترسل للجميع فتتجمد بواحد معطل. والثمن أن المستفيد يدفع غاز سحبه بنفسه، ويجب أن يتذكر أن يسحب، ولكن هذا ثمن صغير مقابل أن العقد لا يعلق على مستقبل واحد. ولهذا نصح به تقرير King of the Ether نفسه، وهو من أقدم الدروس في هذا المجال وما زال صحيحاً.

## لماذا الأمان هنا أغلى من أي مكان آخر؟

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

وهذه الفروق الثلاثة تقلب معادلة الحافز رأساً على عقب: في النظام العادي، مهاجم يجد ثغرة قد يبيعها أو يبتزك، وأنت تملك وقتاً وخيارات. أما هنا فمن يجد الثغرة يأخذ المال فوراً دون وسيط، فالحافز على الهجوم مباشر وضخم، وهذا يفسر لماذا تمر العقود التي تحمل قيمة كبيرة بمراجعة أمنية مستقلة Audit من شركات متخصصة، تقرأ الكود سطراً سطراً وتحاول كسره، وتدفع مقابلها مبالغ كبيرة قبل النشر، ولماذا تشغل برامج مكافأة اكتشاف الثغرات Bug Bounty التي تدفع لمن يبلغ عن ثغرة بدل أن يستغلها، فتحول حافز الباحث من الهجوم إلى الإبلاغ. والقاعدة التي خرج بها الفريق: تكلفة الأمان قبل النشر، مهما بدت كبيرة، أرخص دائماً من تكلفة ثغرة واحدة بعده، لأن الأولى تدفعها مرة وأنت تختار، والثانية تدفعها كاملة دفعة واحدة وأنت لا تملك شيئاً.

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

## التحليل الآلي: المدقق المدمج وSlither

الأدوات التي تقرأ الكود وتجد أنماط الثغرات دون تشغيله تسمى التحليل الساكن Static Analysis، وهي أرخص طبقة دفاع لأنها تعمل في ثوان بعد كل بناء. وقد رأينا المدقق المدمج في `forge` ينبهنا في المقالات السابقة. وهناك أدوات أعمق متخصصة في العقود، أشهرها [Slither](https://github.com/crytic/slither?ref=arabroot.io) من شركة Trail of Bits، وهي مفتوحة المصدر برخصة AGPL-3.0، وتأتي جاهزة في صورة `eth-security-toolbox`. نشغلها على خزنتنا، مع تمرير مسار المكتبة:

```bash
docker run --rm -v "$PWD/lab:/lab" -w /lab/sec ghcr.io/trailofbits/eth-security-toolbox:nightly \
  slither src/Vault.sol \
  --solc-remaps "@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/" \
  --detect reentrancy-eth
```

والمخرج سوف يكون كما يلي:

```text
Reentrancy in VulnerableVault.withdraw() (src/Vault.sol#13-19):
	External calls:
	- (ok,None) = msg.sender.call{value: amount}() (src/Vault.sol#16)
	State variables written after the call(s):
	- balance[msg.sender] = 0 (src/Vault.sol#18)
	VulnerableVault.balance (src/Vault.sol#7) can be used in cross function reentrancies:
	- VulnerableVault.balance (src/Vault.sol#7)
	- VulnerableVault.deposit() (src/Vault.sol#9-11)
	- VulnerableVault.withdraw() (src/Vault.sol#13-19)
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#reentrancy-vulnerabilities-1
INFO:Slither:src/Vault.sol analyzed (3 contracts with 1 detectors), 1 result(s) found
```

أشارت Slither بدقة إلى الدالة `withdraw` في العقد المعطوب، وقالت بالضبط ما المشكلة: استدعاء خارجي في السطر 16، ثم متغير حالة يكتب بعده في السطر 18، وهذا هو تعريف إعادة الدخول. ولاحظ أنها لم تبلغ عن النسخة الآمنة `SafeVault` في نفس الملف، لأن ترتيبها صحيح، وكل ذلك دون أن نشغل العقد أصلاً. والأداة تجد أكثر من إعادة الدخول: عندما شغلناها بلا تحديد صنف وجدت تسع ملاحظات، منها المتغير الذي يجب أن يكون ثابتاً، والاستدعاء منخفض المستوى بلا فحص. ولكن هذه الأدوات تجد الأنماط المعروفة فقط، ولا تفهم منطق عملك: لن تعرف أن العميل يجب ألا يصرف قبل التسليم، لأن هذا قرار عملك أنت.

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

وقاعدة عملية في التعامل مع مخرجات الأدوات: لا تتجاهل ملاحظة ولا تصلحها عمياء، وإنما صنف كل واحدة إلى ثلاث: خطر حقيقي تصلحه، أو إيجابية كاذبة تكتب بجانبها في الكود سبب تجاهلها بتعليق يراه المراجع، أو ملاحظة في كود اختبار أو مثال لا ينشر فلا تهم. ففي تجاربنا أشار المدقق إلى `arbitrary-send-eth` في عقد المهاجم، وهي ملاحظة صحيحة في مكانها، فعقد المهاجم فعلاً يرسل إلى عنوان حر، ولكنها لا تهمنا لأنه عقد هجوم للاختبار لا ينشر. وأخطر ما يحدث للفريق أن يعتاد تجاهل الملاحظات لكثرتها، فيتجاهل يوماً ملاحظة حقيقية، ولذلك كلما قل عدد الإيجابيات الكاذبة التي يراها الفريق، زاد انتباهه للحقيقية، وهذا سبب أن تضبط الأدوات لتتجاهل ما قررت تجاهله بدل أن تتركه يظهر في كل تشغيل.

## الدفاع في طبقات

الخلاصة التي خرج بها فريق المنصة أن الأمان ليس أداة ولا سطراً، وإنما طبقات كل واحدة تمسك ما تفلته التي قبلها، لأن كل طبقة وحدها تخطئ:

![مخطط بخمس طبقات دفاع من اليمين إلى اليسار: الأنماط الآمنة تحقق ثم عدل ثم تواصل ونمط السحب ومكتبات مراجعة مثل OpenZeppelin وReentrancyGuard تمنع الخطأ قبل كتابته، ثم اختبار يهاجم بـ forge test عشوائياً وثوابت أثبت في التجربة السحب 11 ETH مقابل 1، ثم تحليل آلي بالمدقق المدمج وSlither أشار بدقة إلى reentrancy-eth دون تشغيل العقد، ثم مراجعة بشرية Audit ومكافأة اكتشاف الثغرات لأن الأدوات لا تفهم منطق العمل، ثم تشغيل محدود بمهلة وإيقاف وسقف مبالغ، وفي الأسفل أنه لا توجد طبقة تكفي وحدها وأن تبدأ بسقف صغير](https://arabroot.io/content/images/2026/10/smart-contract-security-05-defense.webp)

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

1. **أنماط آمنة ومكتبات مراجعة**: ترتيب تحقق ثم عدل ثم تواصل، ونمط السحب، ومكتبات مثل OpenZeppelin بدل الكتابة من الصفر، وهذه تمنع الخطأ قبل كتابته.
2. **اختبار يهاجم**: اكتب الهجوم اختباراً كما فعلنا، ومعه الاختبار العشوائي والثوابت التي رأيناها في مقال أدوات التطوير.
3. **تحليل آلي**: المدقق المدمج في كل بناء، وأداة مثل Slither في التكامل المستمر، وكلاهما رخيص وسريع.
4. **مراجعة بشرية**: مراجعة مستقلة Audit من فريق متخصص، ومكافأة اكتشاف الثغرات Bug Bounty، لأن الأدوات لا تفهم منطق عملك.
5. **تشغيل محدود**: مهلة زمنية على الصلاحيات، وإمكانية إيقاف في الطوارئ، وسقف مبالغ يبدأ صغيراً ويزيد مع الثقة، فالعقد الذي يحمل القليل خطؤه يكلف القليل.

## أخطاء شائعة وكيف تلاحظها

- **استدعاء خارجي قبل تحديث الحالة**: تلاحظه عندما تجد سطر إرسال مال أو استدعاء عقد آخر يسبق تحديث متغير يعتمد عليه، والحل الترتيب الصحيح والحارس.
- **دالة حساسة بلا فحص صلاحية**: تلاحظها بسؤال واحد لكل دالة تحرك مالاً أو تغير إعداداً: من يحق له استدعاؤها، وأين الفحص.
- **قرار مالي على سعر لحظي من مصدر واحد**: تلاحظه عندما يقرأ العقد سعراً من مجمع واحد في نفس المعاملة، والحل متوسط زمني أو عدة مصادر.
- **حساب في `unchecked` بلا إثبات**: تلاحظه في مراجعة كل كتلة `unchecked`، فهي تلغي حماية المترجم.
- **قيمة عائدة من إرسال لا تفحص**: تلاحظها في كل `send` أو `call` لا يتبعها `require`.
- **الاكتفاء بطبقة واحدة**: تلاحظه عندما يقول الفريق إن الكود اجتاز المدقق الآلي فهو آمن، والحل أن تمر كل ثغرة بأكثر من طبقة.

## قائمة تحقق أمنية قبل أي مال حقيقي

| الصنف                 | السؤال الذي تسأله                                      | الإصلاح                                                       |
| --------------------- | ------------------------------------------------------ | ------------------------------------------------------------- |
| إعادة الدخول          | هل أحدث الحالة قبل كل استدعاء خارجي؟                   | تحقق ثم عدل ثم تواصل، مع nonReentrant عند الحاجة              |
| التحكم في الصلاحيات   | من يحق له استدعاء كل دالة حساسة، وأين الفحص؟           | onlyOwner أو أدوار، ومحفظة متعددة التوقيع ومهلة للمفاتيح      |
| التلاعب بالأوراكل     | كم يكلف تحريك كل سعر يعتمد عليه العقد؟                 | متوسط زمني TWAP، عدة مصادر، مغذي أسعار مصمم لذلك              |
| تجاوز السعة           | هل المترجم 0.8 أو أحدث، وهل كل unchecked مبرر؟         | مترجم حديث مثبت، ومراجعة كل كتلة unchecked                    |
| الاستدعاء غير المفحوص | هل أفحص القيمة العائدة من كل إرسال؟                    | call مع require، ونمط السحب بدل الإرسال للجميع                |
| العشوائية وحجب الخدمة | هل أعتمد على قيمة يتحكم فيها أحد، أو حلقة تكبر بلا حد؟ | مصدر عشوائية آمن، ونمط سحب يحد حجم كل معاملة                  |
| كل ما سبق             | هل مرت على أكثر من طبقة قبل أي مال؟                    | أنماط، اختبار يهاجم، تحليل آلي، مراجعة بشرية، سقف يبدأ صغيراً |

## تدريب عملي

1. أضف إلى `VulnerableVault` حارس `nonReentrant` دون أن تغير ترتيب السطرين، ثم أعد تشغيل هجوم إعادة الدخول وتأكد أنه فشل، وفكر لماذا نجح الحارس رغم بقاء الترتيب الخاطئ، ومتى لا يكفي وحده.
2. شغل Slither على كل عقود المجلد دون تحديد صنف، واقرأ الملاحظات التسع، وصنف كل واحدة: صحيحة يجب إصلاحها، أم في عقد المهاجم المقصود، أم ملاحظة لا تنطبق على سياقك، واكتب سبب تصنيفك.

## حذف التجربة

```bash
docker rm -f lab-sec
docker image rm ghcr.io/trailofbits/eth-security-toolbox:nightly
rm -rf lab/sec/lib lab/sec/out lab/sec/cache
```

## الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

- أمن العقود يدرس من جهة الدفاع: كل ثغرة نسخة من صنف معروف له إصلاح معروف، نكتبه ونهاجمه على شبكة محلية لنفهم كيف نمنعه.
- إعادة الدخول استدعاء خارجي قبل تحديث الحالة، وقد فرغ فيها مهاجم خزنتنا بسحب 11 ETH مقابل إيداع 1، وأوقفه ترتيب تحقق ثم عدل ثم تواصل، كما في حادثة The DAO.
- التحكم في الصلاحيات فحص غائب يسلم الدالة لأي أحد، وأصلحه `onlyOwner`، وعلى مستوى المفاتيح أفرغ مهاجم جسر Ronin بتحكمه بخمسة من تسعة مفاتيح.
- التلاعب بالأوراكل ثقة بسعر لحظي يمكن تحريكه، وقد رفع مهاجمنا السعر خمسة وعشرين ضعفاً في معاملة واحدة، كما رفع مهاجم Mango Markets سعر MNGO ثلاثة عشر ضعفاً.
- تجاوز السعة يصلحه مترجم 0.8، والاستدعاء غير المفحوص يصلحه فحص القيمة العائدة ونمط السحب، كما علمت حادثة King of the Ether.
- التحليل الآلي رخيص ويجد الأنماط المعروفة، وقد أشار Slither بدقة إلى السطر الخطأ دون تشغيل العقد، ولكنه لا يفهم منطق عملك.
- الأمان طبقات: أنماط آمنة، واختبار يهاجم، وتحليل آلي، ومراجعة بشرية، وتشغيل محدود بسقف يبدأ صغيراً، ولا تكفي أي طبقة وحدها.

وبهذا نكون قد أكملنا الجانب التقني من Ethereum والعقود الذكية. وفي المرحلة التالية ننتقل إلى التداول والمنصات والحفظ: كيف تعمل منصات التداول، وكيف تشترى العملات وتباع، ومن يحفظ المفاتيح، وما تقوله الجهات الرسمية في المملكة.

[![](https://arabroot.io/content/images/2026/10/smart-contracts-cover-cover.webp)دليلما هي العقود الذكية Smart Contracts وما مخاطرها؟نكتب عقد ضمان لمنصة عمل حر ونجربه على شبكة محلية، لنرى أين يفي العقد الذكي بوعده وأين ينقل الثقة إلى مفتاح أو أوراكل.](https://arabroot.io/articles/%D8%A7%D9%84%D8%B9%D9%82%D9%88%D8%AF-%D8%A7%D9%84%D8%B0%D9%83%D9%8A%D8%A9-smart-contracts-%D9%85%D8%A7-%D9%87%D9%8A-%D9%88%D9%85%D8%A7-%D9%85%D8%AE%D8%A7%D8%B7%D8%B1%D9%87%D8%A7/)[![](https://arabroot.io/content/images/2026/10/solidity-dev-tools-cover-cover.webp)دليلتطوير العقود الذكية بـ Solidity مع Foundry وHardhatنأخذ عقد الضمان إلى بيئة تطوير كاملة باختبارات وحدة وعشوائية وثوابت، ونرى كيف وجدت الثوابت خطأ لم تره ثمانية اختبارات.](https://arabroot.io/articles/%D8%AA%D8%B7%D9%88%D9%8A%D8%B1-%D8%A7%D9%84%D8%B9%D9%82%D9%88%D8%AF-%D8%A7%D9%84%D8%B0%D9%83%D9%8A%D8%A9-%D8%A8-solidity-%D9%85%D8%B9-foundry-%D9%88-hardhat/)[![](https://arabroot.io/content/images/2026/10/tokens-erc20-nft-cover-cover.webp)دليلما هي الرموز Tokens؟ معيار ERC-20 ورموز NFTما الذي تملكه فعلاً عندما تقول المحفظة إن لديك رمزاً، ولماذا يحتاج العقد إذناً، وكيف تعلق الرموز داخل العقود إلى الأبد.](https://arabroot.io/articles/%D8%A7%D9%84%D8%B1%D9%85%D9%88%D8%B2-tokens-%D9%85%D8%B9%D9%8A%D8%A7%D8%B1-erc-20-%D9%88%D8%A7%D9%84%D8%B1%D9%85%D9%88%D8%B2-%D8%BA%D9%8A%D8%B1-%D8%A7%D9%84%D9%82%D8%A7%D8%A8%D9%84%D8%A9-%D9%84%D9%84%D8%A7%D8%B3%D8%AA%D8%A8%D8%AF%D8%A7%D9%84-nft/)[![](https://arabroot.io/content/images/2026/10/ethereum-how-it-works-cover-cover.webp)دليلكيف يعمل Ethereum؟ الحسابات والغاز وآلة EVMماذا يحدث داخل Ethereum عندما ترسل معاملة، ولماذا تدفع الرسوم حتى لو فشلت، ولماذا تختلف تكلفة نفس الدالة من مرة لأخرى.](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D9%8A%D8%B9%D9%85%D9%84-ethereum-%D8%A7%D9%84%D8%AD%D8%B3%D8%A7%D8%A8%D8%A7%D8%AA-%D9%88%D8%A7%D9%84%D8%BA%D8%A7%D8%B2-%D9%88%D8%A7%D9%84%D8%A9-evm/)[![](https://arabroot.io/content/images/2026/10/blockchain-basics-cover-cover.webp)دليلكيف يعمل البلوكتشين Blockchain ومتى لا تحتاجهما المشكلة التي يحلها البلوكتشين أصلاً وكيف يعمل من الداخل، ومتى تكون قاعدة البيانات العادية هي الحل الأصح.](https://arabroot.io/articles/%D8%A7%D9%84%D8%A8%D9%84%D9%88%D9%83%D8%AA%D8%B4%D9%8A%D9%86-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86-%D9%83%D9%8A%D9%81-%D9%8A%D8%B9%D9%85%D9%84-%D9%88%D9%85%D8%AA%D9%89-%D9%84%D8%A7-%D8%AA%D8%AD%D8%AA%D8%A7%D8%AC%D9%87/)[![](https://arabroot.io/content/images/2026/10/api-security-cover-cover.webp)دليلأمن ال API بالأمثلة مع OWASP API Security Top 10أشهر عشرة مخاطر في واجهات API كما ترتبها OWASP، ولكل خطر مثال لكود ضعيف وكيف تصلحه وتختبره.](https://arabroot.io/articles/%D8%A7%D9%85%D9%86-%D9%88%D8%A7%D8%AC%D9%87%D8%A7%D8%AA-api-%D9%85%D8%B9-owasp-api-security-top-10/)

## سجل التحديثات

- أكتوبر 2026: كتابة المقال، وتشغيل التجارب على Docker Engine 29.8.0 مع Foundry v1.8.6 وforge-std v1.17.0 وSolc 0.8.36 ومكتبة OpenZeppelin Contracts v5.7.0، وتحليل ساكن بأداة Slither 0.11.6 من صورة eth-security-toolbox، على شبكة Anvil محلية بعقود اختبار معطوبة بغرض الشرح.