> ## 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.

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

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

وهذا المقال تعليمي تقني فقط، ولا يتضمن أي نصيحة استثمارية أو مالية، ولا يروج لأي عملة أو منصة، وكل التجارب فيه تعمل على شبكة تطوير محلية بحسابات اختبار ليس لها قيمة، ولا يقول المقال إن استخدام العقود الذكية لحفظ أموال العملاء مسموح أو ممنوع في أي بلد، فهذا سؤال نظامي يجاب عنه من مصادره الرسمية. وقد شرحنا الحسابات والغاز وآلة 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/)، وسوف نعتمد عليه هنا دون أن نكرره.

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

- ما هو العقد الذكي فعلاً، ومن أين جاءت الفكرة، وما الذي ليس عقداً ذكياً.
- دورة حياة العقد: من الكود المصدري إلى الواجهة ABI والكود الثنائي، ثم معاملة النشر والعنوان، ثم القراءة والمعاملة والأحداث.
- تصميم عقد ضمان Escrow لمنصة العمل الحر كآلة حالات، وكتابته بلغة Solidity، وما وجده المدقق الآلي في نسخته الأولى.
- تجربة كاملة على شبكة محلية: النشر، والتمويل، والأخطاء المخصصة Custom Errors، والتسليم والصرف، والاسترداد بعد الموعد بتحريك ساعة الشبكة.
- ما لا يستطيع العقد أن يعرفه: مشكلة الأوراكل Oracle والحكم Arbiter.
- هل العقد لا يتغير فعلاً؟ الترقية بالوكيل Proxy بتجربة حقيقية، وحادثة مكتبة Parity.
- خريطة المخاطر، وقائمة قرار للفريق، وشبكات الاختبار العامة.

## ما هو العقد الذكي؟ وما الذي ليس عقداً ذكياً؟

الفكرة أقدم من Ethereum بعشرين سنة تقريباً، ففي سنة 1994 كتب Nick Szabo [مقالته عن العقود الذكية](https://www.fon.hum.uva.nl/rob/Courses/InformationInSpeech/CDROM/Literature/LOTwinterschool2006/szabo.best.vwh.net/smart.contracts.html?ref=arabroot.io)، وعرف العقد الذكي بأنه بروتوكول معاملات محوسب ينفذ شروط العقد، وكان مثاله الأشهر آلة البيع: تضع العملة وتختار المنتج فتعطيك الآلة ما اخترته، دون بائع يقرر هل يعطيك أم لا، لأن القاعدة مبنية داخل الآلة نفسها. وهذا هو التشبيه الذي يعتبره [توثيق العقود الذكية](https://ethereum.org/en/developers/docs/smart-contracts/?ref=arabroot.io) في ethereum.org أفضل تشبيه للعقد الذكي، ويعرفه بأنه برنامج يعمل على شبكة Ethereum، أي مجموعة من الدوال والبيانات تعيش على عنوان معين، ويتعامل معها المستخدمون بالمعاملات.

ولكن الاسم نفسه يضلل كثيراً من الناس، لذلك قبل أن نكتب سطراً من الكود يجب أن نتفق على ثلاثة أشياء ليست هي العقد الذكي:

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

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

## دورة حياة العقد: من الكود إلى العنوان

قبل أن نصمم عقد الضمان نحتاج أن نفهم الطريق الذي يمر به أي عقد، لأن كل مرحلة فيه لها أخطاؤها الخاصة:

1. **الكود المصدري**: يكتب في الغالب بلغة [Solidity](https://docs.soliditylang.org/en/v0.8.36/?ref=arabroot.io)، وهي لغة تشبه JavaScript في الشكل ولكنها مصممة لآلة EVM، وفيها أنواع ثابتة Static Types وأرقام صحيحة حتى 256 بت. وهناك لغة أخرى أقل انتشاراً اسمها Vyper تشبه Python.
2. **الترجمة Compilation**: المترجم solc يخرج شيئين: الواجهة ABI، أي وصف أسماء الدوال ووسائطها وأحداثها بصيغة JSON يحتاجه تطبيقك ليتكلم مع العقد، والكود الثنائي Bytecode الذي تنفذه الشبكة.
3. **النشر Deployment**: معاملة خاصة ليس لها مستقبل، بياناتها كود الإنشاء Creation Code، فينفذ منشئ العقد Constructor مرة واحدة فقط، ثم يخزن على السلسلة كود التشغيل Runtime Code وحده، ويصبح للعقد عنوان مثل أي حساب.
4. **التعامل Interaction**: إما قراءة Call تنفذها عقدة واحدة دون معاملة ولا رسوم، وإما معاملة Transaction موقعة تغير الحالة وتدفع الغاز، وإما متابعة الأحداث Events التي يطلقها العقد ويقرؤها نظامك.

والصورة التالية تجمع هذه المراحل بالأرقام التي سوف تظهر عندما نبني عقد الضمان وننشره:

![مخطط بأربع مراحل من اليمين إلى اليسار: الكود المصدري Escrow.sol بلغة Solidity، ثم مخرجات المترجم solc وهي الواجهة ABI مع محددات الدوال fund بقيمة b60d4288 وrelease بقيمة 86d1a69f، والكود الثنائي بكود إنشاء 4557 بايت وكود تشغيل 3805 بايت، ثم معاملة النشر مع وسائط المنشئ المستقل و2 ETH و7 أيام، ثم العقد على السلسلة بالعنوان 0x5FbDB231 مع ملاحظة أن إصلاح أي خطأ يحتاج نشر عقد جديد، وفي الأسفل ثلاث طرق للتعامل مع العقد: القراءة call دون رسوم، والمعاملة send التي تغير الحالة أو ترفض بخطأ، والأحداث Events التي يقرؤها النظام](https://arabroot.io/content/images/2026/10/smart-contracts-01-lifecycle.webp)

الكود المصدري لا يخزن على السلسلة، وإنما كود التشغيل وحده، ويبقى على عنوانه كما هو

ولاحظ أن الشبكة تضع حداً لحجم كود التشغيل قدره 24576 بايت، أي 24 كيلوبايت، كما في [EIP-170](https://eips.ethereum.org/EIPS/eip-170?ref=arabroot.io)، وحداً لكود الإنشاء ضعف ذلك منذ [EIP-3860](https://eips.ethereum.org/EIPS/eip-3860?ref=arabroot.io)، والسبب أن كل عقدة يجب أن تحمل الكود وتحلله قبل تنفيذه، وعقد الضمان الذي سوف نكتبه أصغر بكثير من هذا الحد.

## تصميم عقد الضمان Escrow كآلة حالات

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

- **أنشئ Created**: نشر العميل العقد وحدد فيه عنوان المستقل والمبلغ والموعد النهائي بالأيام، ولا يوجد فيه مال بعد.
- **ممول Funded**: أرسل العميل المبلغ المحدد بالضبط، ولا أحد غيره يستطيع أن يمول.
- **سلم Delivered**: أعلن المستقل أنه سلم العمل، ومعه نص يدل على التسليم مثل رابط وهاش الملف.
- **صرف Released**: وافق العميل، فأرسل العقد المبلغ كاملاً إلى المستقل.
- **أعيد Refunded**: مر الموعد النهائي ولم يسلم المستقل، فاسترد العميل ماله.

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

### الكود: عقد Escrow بلغة Solidity

أنشئ مجلداً باسم `lab/escrow`، وضع فيه ملف الإعداد `foundry.toml` التالي، وفيه نثبت إصدار المترجم حتى يعطي نفس الكود الثنائي في كل مرة:

```toml
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
solc = "0.8.36"
```

ثم العقد نفسه في الملف `src/Escrow.sol`، وهذه نسخته الأولى كما كتبناها:

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

/// Escrow for one freelance job: the client funds it, the freelancer delivers,
/// the client releases the money, or takes it back after the deadline.
/// A teaching example for a local devnet, not audited.
contract Escrow {
    enum State { Created, Funded, Delivered, Released, Refunded }

    address public immutable client;
    address public immutable freelancer;
    uint256 public immutable amount;
    uint256 public immutable deadline;
    State public state;

    event Funded(uint256 amount);
    event Delivered(string proof);
    event Released(address indexed to, uint256 amount);
    event Refunded(address indexed to, uint256 amount);

    error NotClient();
    error NotFreelancer();
    error WrongState(State current);
    error WrongAmount(uint256 sent, uint256 expected);
    error TooEarly(uint256 nowTime, uint256 deadline);

    constructor(address _freelancer, uint256 _amount, uint256 _days) {
        client = msg.sender;
        freelancer = _freelancer;
        amount = _amount;
        deadline = block.timestamp + _days * 1 days;
    }

    function fund() external payable {
        if (msg.sender != client) revert NotClient();
        if (state != State.Created) revert WrongState(state);
        if (msg.value != amount) revert WrongAmount(msg.value, amount);
        state = State.Funded;
        emit Funded(msg.value);
    }

    function deliver(string calldata proof) external {
        if (msg.sender != freelancer) revert NotFreelancer();
        if (state != State.Funded) revert WrongState(state);
        state = State.Delivered;
        emit Delivered(proof);
    }

    function release() external {
        if (msg.sender != client) revert NotClient();
        if (state != State.Delivered) revert WrongState(state);
        state = State.Released;
        (bool ok, ) = freelancer.call{value: amount}("");
        require(ok, "transfer failed");
        emit Released(freelancer, amount);
    }

    function refund() external {
        if (msg.sender != client) revert NotClient();
        if (state != State.Funded) revert WrongState(state);
        if (block.timestamp < deadline) revert TooEarly(block.timestamp, deadline);
        state = State.Refunded;
        (bool ok, ) = client.call{value: amount}("");
        require(ok, "transfer failed");
        emit Refunded(client, amount);
    }
}
```

في الكود أعلاه لاحظ التالي:

- المتغيرات الأربعة الأولى `immutable`، أي أنها تحدد مرة واحدة في المنشئ ثم تكتب داخل كود التشغيل نفسه بدل التخزين، فقراءتها أرخص بكثير من قراءة خانة تخزين، ولا توجد أي دالة تستطيع أن تغير المستقل أو المبلغ بعد النشر. أما `state` فهو المتغير الوحيد في التخزين لأنه الوحيد الذي يتغير.
- كل دالة تبدأ بالتحقق من شيئين: من المرسل `msg.sender`، وما الحالة الحالية، ولا تغير أي شيء قبل أن ينجح التحققان، وهذا هو جوهر آلة الحالات.
- الأخطاء مكتوبة كأخطاء مخصصة Custom Errors بالكلمة `error` كما يشرح [توثيق Solidity](https://docs.soliditylang.org/en/latest/contracts.html?ref=arabroot.io#custom-errors)، بدلاً من نصوص طويلة في `require`، والسبب أنها أرخص في الغاز وتحمل معها القيم التي سببت الخطأ، كما سوف نرى في المخرجات.
- في `release` و`refund` نغير الحالة أولاً ثم نرسل المال، وليس العكس، وهذا الترتيب مقصود لأن المستقبل قد يكون عقداً يستدعي `release` مرة ثانية أثناء استلامه المال، فإذا كانت الحالة ما زالت `Delivered` صرف العقد مرتين. وهذا النمط اسمه «تحقق ثم عدل ثم تواصل» Checks-Effects-Interactions، والثغرة التي يمنعها اسمها إعادة الدخول Reentrancy، وهي التي أسقطت The DAO كما ذكرنا في [مقال البلوكتشين للمبتدئين](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/)، وسوف نجربها بالتفصيل في مقال أمن العقود الذكية.
- الإرسال بالطريقة `call{value: amount}("")` ثم التحقق من `ok`، لأن الدالتين القديمتين `transfer` و`send` تمرران كمية غاز ثابتة صغيرة قد لا تكفي إذا كان المستقبل محفظة ذكية.

### ماذا وجد المدقق الآلي في النسخة الأولى؟

نشغل Anvil كما فعلنا في المقال السابق، ونجعل مجلد `lab` كله مرئياً داخل ال Container، ثم نبني المشروع:

```bash
docker run -d --name lab-sc -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/escrow lab-sc forge build
```

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

```text
Compiling 1 files with Solc 0.8.36
Solc 0.8.36 finished in 45.51ms
Compiler run successful!
warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check
   ╭▸ src/Escrow.sol:27:17
   │
27 │     constructor(address _freelancer, uint256 _amount, uint256 _days) {
   │                 ━━━━━━━━━━━━━━━━━━━
   │
   ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check

warning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on
   ╭▸ src/Escrow.sol:55:9
   │
55 │         emit Released(freelancer, amount);
   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   │
   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events

warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators
   ╭▸ src/Escrow.sol:61:13
   │
61 │         if (block.timestamp < deadline) revert TooEarly(block.timestamp, deadline);
   │             ━━━━━━━━━━━━━━━━━━━━━━━━━━
   │
   ╰ help: https://getfoundry.sh/forge/linting/block-timestamp

warning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on
   ╭▸ src/Escrow.sol:65:9
   │
65 │         emit Refunded(client, amount);
   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   │
   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events
```

الترجمة نجحت، ولكن أداة `forge` شغلت بعدها مدققاً آلياً Linter، كما يشرح [توثيق التدقيق في Foundry](https://getfoundry.sh/forge/linting?ref=arabroot.io)، فوجد ثلاثة أنواع من الملاحظات، وكل واحدة منها تستحق أن نفكر فيها بدل أن نسكتها:

- **missing-zero-check**: لو نشر العميل العقد وأخطأ فوضع عنوان المستقل صفراً، لأصبح العقد لا يستطيع أن يصرف لأحد، لأن لا أحد يملك مفتاح العنوان صفر فيسلم العمل، ولبقي المال حتى الموعد ثم يعود. والإصلاح سطر واحد في المنشئ يرفض العنوان صفر. وهذه ملاحظة صحيحة.
- **reentrancy-events**: أطلقنا الحدث بعد إرسال المال، فإذا كان المستقبل عقداً يعيد الدخول فقد تظهر الأحداث في السجل بترتيب مختلف عن ترتيب الحالات الحقيقي، ونظام المنصة الذي يقرأ الأحداث سوف يبني صورة خاطئة. والإصلاح أن نطلق الحدث قبل الإرسال مع تغيير الحالة. وهذه ملاحظة صحيحة أيضاً، وقد فاتتنا رغم أننا رتبنا تغيير الحالة بعناية.
- **block-timestamp**: يحذر المدقق من أن المدقق Validator الذي يقترح الكتلة يستطيع أن يتلاعب بالوقت. وهذا كان صحيحاً في أيام إثبات العمل عندما كان من يقوم بالتعدين يحدد الوقت ضمن هامش، أما في Ethereum اليوم فالوقت مربوط بالخانة Slot نفسها، فمواصفات الإجماع في [ترقية Bellatrix](https://github.com/ethereum/consensus-specs/blob/master/specs/bellatrix/beacon-chain.md?ref=arabroot.io) تشترط أن يساوي وقت الكتلة وقت بداية خانتها بالضبط. وحتى لو تلاعب أحد بثوان، فموعدنا سبعة أيام، لذلك نقرر أن نبقي المقارنة ونسكت التحذير في هذا السطر فقط مع تعليق يشرح السبب.

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

```solidity
    error ZeroAddress();

    constructor(address _freelancer, uint256 _amount, uint256 _days) {
        if (_freelancer == address(0)) revert ZeroAddress();
        client = msg.sender;
        freelancer = _freelancer;
        amount = _amount;
        deadline = block.timestamp + _days * 1 days;
    }

    function release() external {
        if (msg.sender != client) revert NotClient();
        if (state != State.Delivered) revert WrongState(state);
        state = State.Released;
        emit Released(freelancer, amount);
        (bool ok, ) = freelancer.call{value: amount}("");
        require(ok, "transfer failed");
    }

    function refund() external {
        if (msg.sender != client) revert NotClient();
        if (state != State.Funded) revert WrongState(state);
        // forge-lint: disable-next-line(block-timestamp)
        if (block.timestamp < deadline) revert TooEarly(block.timestamp, deadline);
        state = State.Refunded;
        emit Refunded(client, amount);
        (bool ok, ) = client.call{value: amount}("");
        require(ok, "transfer failed");
    }
```

وعند البناء مرة أخرى بعد حذف المجلدين `out` و`cache` يكون المخرج كما يلي، دون أي ملاحظة:

```text
Compiling 1 files with Solc 0.8.36
Solc 0.8.36 finished in 53.47ms
Compiler run successful!
```

وتستطيع أن ترى الواجهة التي سوف يستخدمها تطبيق المنصة بالأمر `forge inspect Escrow methodIdentifiers`، وفيها محدد كل دالة، مثل `fund()` بالمحدد `b60d4288` و`release()` بالمحدد `86d1a69f`، وبالأمر `forge inspect Escrow errors` محددات الأخطاء، مثل `NotClient()` بالمحدد `20dbc874`، وهذه هي الأرقام التي سوف تراها في رسائل الخطأ بعد قليل. وقد قسنا حجم الكود الثنائي من نفس الأمر: كود الإنشاء 4557 بايت، وكود التشغيل 3805 بايت.

## التجربة: مشروع واحد من التمويل إلى الصرف

الآن نمثل مشروعاً كاملاً بحسابين من حسابات Anvil: الحساب 0 هو العميل، والحساب 1 هو المستقل. والسكربت التالي ينشر العقد بمبلغ 2 ETH وموعد سبعة أيام، ثم يجرب الأخطاء التي يمكن أن يرتكبها كل طرف، ثم يكمل المشروع إلى الصرف، واحفظه باسم `lab/03-escrow.sh`:

```bash
# Accounts from Anvil: 0 = client (the company's customer), 1 = freelancer
CLIENT_PK=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
FREE_PK=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
FREE=0x70997970C51812dc3A010C7d01b50e0d17dc79C8
cd /lab/escrow
echo "== 1. deploy: freelancer, 2 ETH, 7 days"
forge create src/Escrow.sol:Escrow --broadcast --private-key $CLIENT_PK \
  --constructor-args $FREE 2ether 7 | grep -E 'Deployed to|Transaction hash'
E=0x5FbDB2315678afecb367f032d93F642f64180aa3
echo "state: $(cast call $E 'state()(uint8)')  amount: $(cast call $E 'amount()(uint256)')  deadline: $(cast call $E 'deadline()(uint256)')"
echo "== 2. the freelancer tries to fund it"
cast send $E "fund()" --value 2ether --private-key $FREE_PK 2>&1 | tail -1
echo "== 3. the client sends the wrong amount"
cast send $E "fund()" --value 1ether --private-key $CLIENT_PK 2>&1 | tail -1
echo "== 4. the client funds 2 ETH"
cast send $E "fund()" --value 2ether --private-key $CLIENT_PK | grep -E '^(status|gasUsed) '
echo "contract balance: $(cast balance $E --ether)  state: $(cast call $E 'state()(uint8)')"
echo "== 5. the client wants the money back before the deadline"
cast send $E "refund()" --private-key $CLIENT_PK 2>&1 | tail -1
echo "== 6. the freelancer delivers"
cast send $E "deliver(string)" "https://example.com/jobs/17/delivery.zip sha256:9f2c" --private-key $FREE_PK | grep -E '^(status|gasUsed) '
echo "== 7. the client releases the payment"
B=$(cast balance $FREE)
TX=$(cast send $E "release()" --private-key $CLIENT_PK --json | grep -o '"transactionHash":"[^"]*"' | cut -d'"' -f4)
echo "freelancer got (wei): $(( $(cast balance $FREE) - B ))  contract balance: $(cast balance $E)  state: $(cast call $E 'state()(uint8)')"
echo "== 8. the events"
cast logs --from-block 0 --address $E | grep -E 'blockNumber|data:|^\s+0x'
echo "Released topic: $(cast sig-event 'Released(address,uint256)')"
D=$(cast logs --from-block 3 --to-block 3 --address $E | grep 'data:' | awk '{print $2}')
echo "Delivered proof: $(cast decode-abi --input 'x(string)' $D)"
echo "== 9. release again?"
cast send $E "release()" --private-key $CLIENT_PK 2>&1 | tail -1
```

ثم شغله:

```bash
docker exec lab-sc bash /lab/03-escrow.sh
```

والمخرج سوف يكون كما يلي، وقيم الوقت تختلف في كل تشغيل لأنها من ساعة جهازك:

```text
== 1. deploy: freelancer, 2 ETH, 7 days
Deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Transaction hash: 0xaa6c1ad68fa83c3de3c25df6ff91b40770bba8c99b3fab468a570702e48af41f
state: 0  amount: 2000000000000000000 [2e18]  deadline: 1793752520 [1.793e9]
== 2. the freelancer tries to fund it
Error: Failed to estimate gas: server returned an error response: error code 3: execution reverted: custom error 0x20dbc874, data: "0x20dbc874": NotClient
== 3. the client sends the wrong amount
Error: Failed to estimate gas: server returned an error response: error code 3: execution reverted: custom error 0x312b0033: 0000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000001bc16d674ec80000, data: "0x312b00330000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000001bc16d674ec80000": WrongAmount(1000000000000000000 [1e18], 2000000000000000000 [2e18])
== 4. the client funds 2 ETH
gasUsed              44885
status               1 (success)
contract balance: 2.000000000000000000  state: 1
== 5. the client wants the money back before the deadline
Error: Failed to estimate gas: server returned an error response: error code 3: execution reverted: custom error 0xe58369d1: 000000000000000000000000000000000000000000000000000000006ae1434c000000000000000000000000000000000000000000000000000000006aea7dc8, data: "0xe58369d1000000000000000000000000000000000000000000000000000000006ae1434c000000000000000000000000000000000000000000000000000000006aea7dc8": TooEarly(1793147724 [1.793e9], 1793752520 [1.793e9])
== 6. the freelancer delivers
gasUsed              30190
status               1 (success)
== 7. the client releases the payment
freelancer got (wei): 2000000000000000000  contract balance: 0  state: 3
== 8. the events
  blockNumber: 2
  data: 0x0000000000000000000000000000000000000000000000001bc16d674ec80000
  	0xc4c14883ae9fd8e26d5d59e3485ed29fd126d781d7e498a4ca5c54c8268e4936
  blockNumber: 3
  data: 0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003468747470733a2f2f6578616d706c652e636f6d2f6a6f62732f31372f64656c69766572792e7a6970207368613235363a39663263000000000000000000000000
  	0xa62e400ec2f4d8d483acfb4767b3abdf8e7d2487f601a9be001066386c158f14
  blockNumber: 4
  data: 0x0000000000000000000000000000000000000000000000001bc16d674ec80000
  	0xb21fb52d5749b80f3182f8c6992236b5e5576681880914484d7f4c9b062e619e
  	0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8
Released topic: 0xb21fb52d5749b80f3182f8c6992236b5e5576681880914484d7f4c9b062e619e
Delivered proof: "https://example.com/jobs/17/delivery.zip sha256:9f2c"
== 9. release again?
Error: Failed to estimate gas: server returned an error response: error code 3: execution reverted: custom error 0xe37d4aeb: , data: "0xe37d4aeb0000000000000000000000000000000000000000000000000000000000000003": WrongState(3)
```

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

- العقد نشر على العنوان `0x5FbDB231…`، وهو أول عنوان للحساب 0 كما حسبناه في المقال السابق، والحالة `0` أي `Created`، والموعد النهائي رقم بالثواني منذ سنة 1970، أي وقت كتلة النشر مضافاً إليه 604800 ثانية.
- عندما حاول المستقل أن يمول العقد رفضته العقدة قبل الإرسال برسالة فيها المحدد `0x20dbc874`، وقد ترجمه `cast` إلى `NotClient` لأنه يعرف واجهة العقد من مجلد المشروع، أما تطبيق المنصة فيجب أن يحمل الواجهة ABI ليترجم الأخطاء بنفسه، وإلا رأى المستخدم رقماً لا معنى له.
- في الخطأ `WrongAmount` حمل الخطأ نفسه القيمتين: المرسل 1 ETH والمطلوب 2 ETH، وهذا ما يجعل الأخطاء المخصصة أفضل من نص ثابت، فتستطيع الواجهة أن تقول للعميل بالضبط ما الخطأ.
- بعد التمويل الصحيح أصبح في العقد نفسه 2 ETH، وليس في حساب المنصة ولا في حساب المستقل، والحالة `1` أي `Funded`.
- حاول العميل أن يسترد ماله قبل الموعد فرفض العقد بالخطأ `TooEarly` ومعه الوقت الحالي والموعد. وهذه القاعدة هي التي تحمي المستقل: بعد أن يمول العميل لا يستطيع أن يسحب المال وهو يعمل.
- بعد التسليم والموافقة وصل إلى المستقل 2 ETH بالضبط، أي 2 × 10 أس 18 Wei، ورصيد العقد صفر، والحالة `3` أي `Released`. ولاحظ أن المستقل لم يدفع شيئاً من مبلغه للغاز، لأن العميل هو الذي أرسل معاملة الصرف ودفع رسومها.
- الأحداث الثلاثة في الكتل 2 و3 و4، والسطر الذي يبدأ بمسافة هو الموضوع Topic، وأول موضوع في كل حدث هاش توقيعه، كما تأكدنا بالأمر `cast sig-event` لحدث `Released`، وفي هذا الحدث موضوع ثان هو عنوان المستقل لأنه `indexed`. أما نص التسليم فقد خزن في بيانات الحدث، وفككناه إلى النص الأصلي، وكلفنا ذلك أقل بكثير من تخزينه في متغير، لأن الحدث لا يدخل حالة العقد.
- محاولة الصرف مرة ثانية رفضت بالخطأ `WrongState(3)`، لأن الحالة لم تعد `Delivered`، وهذا هو الحاجز الذي يمنع الدفع المزدوج.

والصورة التالية تبين آلة الحالات كما عملت في التجربة، مع الأخطاء التي رأيناها:

![آلة حالات من خمسة صناديق: أنشئ Created بالرقم 0، ثم ممول Funded بالرقم 1 عبر fund من العميل فقط بمبلغ 2 ETH بالضبط، ثم سلم العمل Delivered بالرقم 2 عبر deliver من المستقل فقط، ثم صرف للمستقل Released بالرقم 3 عبر release من العميل فقط، ومن ممول سهم إلى أعيد للعميل Refunded بالرقم 4 عبر refund من العميل بعد الموعد، وصندوق أحمر بالأخطاء التي رفضها العقد في التجربة NotClient وWrongAmount وTooEarly وWrongState، وصندوق يشرح أن العقد لا يعرف هل العمل جيد ولا توجد فيه حالة نزاع](https://arabroot.io/content/images/2026/10/smart-contracts-02-escrow-states.webp)

كل دالة تتحقق من المرسل والحالة أولاً، ولذلك رفضت المحاولات الخاطئة الأربع قبل أن تصل إلى أي كتلة

### ماذا لو لم يسلم المستقل أبداً؟ نحرك ساعة الشبكة

المسار الثاني في آلة الحالات هو الاسترداد بعد الموعد، ولا نريد أن ننتظر سبعة أيام لنجربه، وشبكة التطوير تسمح لنا أن نحرك الوقت بالطريقة `evm_increaseTime` ثم ننشئ كتلة بالطريقة `evm_mine`. واحفظ السكربت التالي باسم `lab/04-refund.sh`:

```bash
CLIENT_PK=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
CLIENT=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
FREE=0x70997970C51812dc3A010C7d01b50e0d17dc79C8
cd /lab/escrow
echo "== a second job: deploy and fund, the freelancer never delivers"
E2=$(forge create src/Escrow.sol:Escrow --broadcast --private-key $CLIENT_PK --constructor-args $FREE 1ether 7 | grep 'Deployed to' | awk '{print $3}')
echo "escrow 2: $E2"
cast send $E2 "fund()" --value 1ether --private-key $CLIENT_PK | grep -E '^status'
echo "now: $(cast block latest --field timestamp)  deadline: $(cast call $E2 'deadline()(uint256)')"
echo "== move the devnet clock 8 days ahead"
cast rpc evm_increaseTime 691200 > /dev/null; cast rpc evm_mine > /dev/null
echo "now: $(cast block latest --field timestamp)"
B=$(cast balance $CLIENT)
R=$(cast send $E2 "refund()" --private-key $CLIENT_PK --json)
echo "$R" | grep -o '"status":"[^"]*"'
G=$(echo "$R" | grep -o '"gasUsed":"[^"]*"' | cut -d'"' -f4); P=$(echo "$R" | grep -o '"effectiveGasPrice":"[^"]*"' | cut -d'"' -f4)
echo "client got back (wei): $(( $(cast balance $CLIENT) - B ))  fee paid (wei): $(( G * P ))  state: $(cast call $E2 'state()(uint8)')"
```

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

```text
== a second job: deploy and fund, the freelancer never delivers
escrow 2: 0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9
status               1 (success)
now: 1793148313  deadline: 1793753111 [1.793e9]
== move the devnet clock 8 days ahead
now: 1793839513
"status":"0x1"
client got back (wei): 999985891325285509  fee paid (wei): 14108674714491  state: 4
```

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

- العقد الثاني على عنوان جديد تماماً، فكل مشروع عقد مستقل بماله وحالته، وهذا يعني أن خطأً في مشروع لا يمس مال مشروع آخر، ولكنه يعني أيضاً أن كل مشروع يدفع ثمن نشر عقد كامل.
- بعد تحريك الساعة 691200 ثانية، أي ثمانية أيام، أصبح الوقت بعد الموعد، فنجح الاسترداد وأصبحت الحالة `4` أي `Refunded`.
- الرصيد زاد بأقل من 1 ETH بقليل، والفرق هو رسوم معاملة الاسترداد نفسها: 999985891325285509 + 14108674714491 = 1000000000000000000 بالضبط. فالعميل استرد ماله كاملاً ودفع ثمن المعاملة التي طلب بها الاسترداد.

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

### كم يكلف مشروع واحد بالغاز؟

قبل أن يتحمس الفريق للفكرة يجب أن يعرف ثمنها، والسكربت التالي يقرأ الغاز المستهلك في كل كتلة من كتل التجربتين، لأن كل كتلة على Anvil فيها معاملة واحدة، واحفظه باسم `lab/06-costs.sh`:

```bash
# gas used by each step of the first job (blocks 1 to 4) and the refund of the second job (block 8)
for n in 1 2 3 4 8; do
  printf "block %s  gasUsed %d\n" $n $(cast block $n --field gasUsed)
done
```

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

```text
block 1  gasUsed 878463
block 2  gasUsed 44885
block 3  gasUsed 30190
block 4  gasUsed 37778
block 8  gasUsed 35281
```

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

- نشر العقد وحده كلف 878463 وحدة غاز، أي قرابة عشرين ضعف أي خطوة أخرى، والسبب أن الشبكة تحفظ 3805 بايت من الكود إلى الأبد، ولكل بايت منها ثمن. وفي تصميم «عقد لكل مشروع» يدفع كل مشروع هذا الثمن من جديد.
- التمويل 44885، وأغلبه كتابة الحالة من صفر إلى قيمة كما شرحنا في المقال السابق، والتسليم 30190 مع أن نص التسليم أكثر من 50 حرفاً، لأنه ذهب إلى حدث وليس إلى التخزين، والصرف 37778 والاسترداد 35281 لأنهما يرسلان مالاً ويعدلان الحالة.
- المشروع الكامل من النشر إلى الصرف كلف قرابة 991 ألف وحدة غاز، وثمنها بالعملة يساوي هذا الرقم ضرب الرسوم الأساسية والإكرامية وقت كل معاملة، وهي أرقام تتغير من كتلة لأخرى كما رأينا في تجربة EIP-1559، فلا يوجد ثمن ثابت تستطيع المنصة أن تعلنه لعملائها.

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

## ما لا يستطيع العقد أن يعرفه: الأوراكل Oracle

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

والحلول المعروفة كلها تضيف طرفاً يثق به العقد:

- **حكم Arbiter**: عنوان ثالث يحدد في المنشئ، ويستطيع في حالة النزاع أن يقرر الصرف أو الاسترداد، وقد يكون المنصة نفسها أو جهة تحكيم يتفق عليها الطرفان. والعقد هنا ينفذ قرار الحكم بدقة، ولكنه لا يعرف هل القرار عادل.
- **أوراكل بيانات**: خدمة تنشر أسعاراً أو أحداثاً، مثل سعر صرف أو نتيجة فحص، ويقرؤها العقد. وأمان العقد يصبح بقدر أمان هذه الخدمة وطريقة جمعها للبيانات.
- **توقيع الطرفين**: لا يصرف العقد في حالة النزاع إلا بتوقيعين من ثلاثة، العميل والمستقل والحكم، وهو تطبيق لفكرة المحفظة متعددة التوقيع Multisig.

والصورة التالية تضع الحدود بين ما يعرفه العقد وما يحتاج أن يخبره به أحد:

![مخطط من جزأين: على اليمين بالأخضر ما يعرفه العقد يقيناً داخل السلسلة، msg.sender وmsg.value وblock.timestamp ورصيده وحالة العقود الأخرى، ولذلك ينفذ بنفسه من يمول وكم ومن يصرف ومتى يسترد العميل، وعلى اليسار بالبرتقالي ما لا يعرفه إلا إذا أخبره أحد، هل سلم المستقل العمل وهل هو جيد، ومن هو العميل باسمه وهل اجتاز التحقق KYC، وكم يساوي المبلغ بالريال، وهل وصلت الشحنة، والحل حكم Arbiter يوقع قراره أو خدمة أوراكل Oracle، وفي الأسفل قاعدة أن العقد ينفذ بدقة ما يقوله من يخبره بالمعلومة صحيحاً كان أو خاطئاً](https://arabroot.io/content/images/2026/10/smart-contracts-04-onchain-offchain.webp)

الثقة لا تختفي مع العقد الذكي، وإنما تنتقل إلى من يكتب المعلومة الخارجية على السلسلة

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

## هل العقد لا يتغير فعلاً؟

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

- **الانتقال إلى عقد جديد Contract Migration**: تنشر عقداً جديداً وتنقل إليه الأرصدة والبيانات، وتطلب من كل من يستخدم العنوان القديم أن ينتقل. وهي آمنة وبسيطة ولكنها بطيئة ومكلفة.
- **فصل البيانات عن المنطق Data Separation**: عقد للبيانات لا يتغير، وعقد للمنطق يستبدل.
- **الوكيل Proxy**: عقد ثابت يحمل العنوان والبيانات، ويحول كل استدعاء بالتعليمة `delegatecall` إلى عقد تنفيذ Implementation يمكن استبداله، وهي الطريقة الأشهر اليوم.
- **الماسة Diamond**: وكيل يوجه كل دالة إلى عقد تنفيذ مختلف، ويستخدم أيضاً لتجاوز حد 24 كيلوبايت.

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

### تجربة: نغير الرسوم من 5% إلى 50% دون أن يتغير العنوان

لنفرض أن المنصة وضعت حساب رسومها في عقد مستقل يستدعيه عقد الضمان، وجعلته قابلاً للترقية خلف وكيل من مكتبة [OpenZeppelin Contracts](https://docs.openzeppelin.com/contracts/5.x/api/proxy?ref=arabroot.io)، وهي مكتبة عقود مراجعة برخصة MIT. والنسخة الأولى من منطق الرسوم تأخذ 5%، والثانية 50%، واحفظهما في مجلد جديد `lab/proxy` في الملف `src/Fees.sol`:

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

/// The platform fee logic, version 1: 5% of each job goes to the treasury.
contract FeesV1 {
    address public treasury;

    function initialize(address t) external {
        require(treasury == address(0), "already initialized");
        treasury = t;
    }

    function feeBps() external pure returns (uint256) { return 500; }
    function version() external pure returns (string memory) { return "v1"; }
}

/// Version 2: same storage layout, but whoever holds the admin key decided the fee is now 50%.
contract FeesV2 {
    address public treasury;

    function feeBps() external pure returns (uint256) { return 5000; }
    function version() external pure returns (string memory) { return "v2"; }
}
```

ومع المشروع ملف `foundry.toml` فيه إعادة تسمية المسار Remapping حتى يجد المترجم المكتبة:

```toml
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
solc = "0.8.36"
remappings = ["@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/"]
```

ثم نحمل المكتبة بإصدار محدد، والخيار `--no-git` يحملها دون أن يحتاج المجلد أن يكون مستودع Git:

```bash
docker exec -w /lab/proxy lab-sc forge install --no-git OpenZeppelin/openzeppelin-contracts@v5.7.0
```

وفي أول محاولة نشرنا الوكيل دون بيانات تهيئة، أي بالقيمة `0x` مكان الوسيط الثالث، فرفضت العقدة النشر بالخطأ `custom error 0xc28a273c`، وعندما بحثنا عن هذا المحدد في كود المكتبة وجدنا أنه `ERC1967ProxyUninitialized()`، أي أن الإصدار 5.7.0 من المكتبة يرفض وكيلاً لا يهيئ نفسه عند النشر، والسبب الحادثة التي سوف نرويها بعد قليل. لذلك أضفنا الدالة `initialize` إلى النسخة الأولى، ونمرر استدعاءها في النشر نفسه. والسكربت التالي ينشر النسختين والوكيل، ثم يقرأ من الوكيل، ثم يحاول شخص غريب أن يرقيه، ثم يرقيه المالك، واحفظه باسم `lab/05-proxy.sh`:

```bash
OWNER_PK=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
OWNER=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
OTHER_PK=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
IMPL_SLOT=0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
ADMIN_SLOT=0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103
cd /lab/proxy
dep() { forge create --broadcast --private-key $OWNER_PK "$@" | grep 'Deployed to' | awk '{print $3}'; }
V1=$(dep src/Fees.sol:FeesV1); V2=$(dep src/Fees.sol:FeesV2)
P=$(dep lib/openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol:TransparentUpgradeableProxy --constructor-args $V1 $OWNER $(cast calldata "initialize(address)" $OWNER))
echo "V1: $V1"; echo "V2: $V2"; echo "proxy: $P"
ADMIN=0x$(cast storage $P $ADMIN_SLOT | cut -c27-)
echo "ProxyAdmin (from the EIP-1967 admin slot): $ADMIN  owner: $(cast call $ADMIN 'owner()(address)')"
echo "== through the proxy"
echo "implementation slot: $(cast storage $P $IMPL_SLOT)"
echo "version: $(cast call $P 'version()(string)')  feeBps: $(cast call $P 'feeBps()(uint256)')  treasury: $(cast call $P 'treasury()(address)')"
echo "== someone else tries to upgrade"
cast send $ADMIN "upgradeAndCall(address,address,bytes)" $P $V2 0x --private-key $OTHER_PK 2>&1 | tail -1
echo "== the owner upgrades"
cast send $ADMIN "upgradeAndCall(address,address,bytes)" $P $V2 0x --private-key $OWNER_PK | grep -E '^(status|gasUsed) '
echo "implementation slot: $(cast storage $P $IMPL_SLOT)"
echo "same address $P  version: $(cast call $P 'version()(string)')  feeBps: $(cast call $P 'feeBps()(uint256)')  treasury: $(cast call $P 'treasury()(address)')"
echo "the implementation contract itself still says: $(cast call $V1 'feeBps()(uint256)')"
echo "== what if a stranger calls initialize() on the implementation contract itself?"
cast send $V1 "initialize(address)" 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 --private-key $OTHER_PK | grep -E '^status'
echo "V1 treasury now: $(cast call $V1 'treasury()(address)')   proxy treasury: $(cast call $P 'treasury()(address)')"
```

ثم شغله على نفس الشبكة بعد سكربتي الضمان:

```bash
docker exec lab-sc bash /lab/05-proxy.sh
```

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

```text
V1: 0x0165878A594ca255338adfa4d48449f69242Eb8F
V2: 0xa513E6E4b8f2a923D98304ec87F64353C4D5C853
proxy: 0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6
ProxyAdmin (from the EIP-1967 admin slot): 0x94099942864ea81ccf197e9d71ac53310b1468d8  owner: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
== through the proxy
implementation slot: 0x0000000000000000000000000165878a594ca255338adfa4d48449f69242eb8f
version: "v1"  feeBps: 500  treasury: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
== someone else tries to upgrade
Error: Failed to estimate gas: server returned an error response: error code 3: execution reverted: custom error 0x118cdaa7: 00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8, data: "0x118cdaa700000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8": OwnableUnauthorizedAccount(0x70997970C51812dc3A010C7d01b50e0d17dc79C8)
== the owner upgrades
gasUsed              39524
status               1 (success)
implementation slot: 0x000000000000000000000000a513e6e4b8f2a923d98304ec87f64353c4d5c853
same address 0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6  version: "v2"  feeBps: 5000  treasury: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
the implementation contract itself still says: 500
== what if a stranger calls initialize() on the implementation contract itself?
status               1 (success)
V1 treasury now: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8   proxy treasury: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
```

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

- الوكيل يحفظ عنوان عقد التنفيذ في خانة تخزين ثابتة رقمها مشتق من هاش نص معروف، وهي الخانة `0x3608…2bbc` الموصوفة في [EIP-1967](https://eips.ethereum.org/EIPS/eip-1967?ref=arabroot.io)، واختير هذا الرقم حتى لا يتصادم مع متغيرات عقد التنفيذ نفسه، وبنفس الطريقة تحفظ خانة أخرى عنوان المسؤول. لذلك يستطيع أي شخص، ومنهم العملاء، أن يقرأ هاتين الخانتين ويعرف ما المنطق الذي يعمل الآن ومن يملك تغييره.
- الوكيل في الإصدار الخامس من المكتبة أنشأ عقد إدارة اسمه `ProxyAdmin` عند النشر، ومالكه الحساب الذي حددناه. والغريب الذي حاول الترقية رفض بالخطأ `OwnableUnauthorizedAccount` ومعه عنوانه.
- المالك رقى الوكيل بمعاملة واحدة كلفت 39524 وحدة غاز، فتغير محتوى خانة التنفيذ من `0x0165…` إلى `0xa513…`، وأصبحت الرسوم 5000 نقطة أساس، أي 50%، على نفس العنوان `0x2279…`. وتطبيق المنصة وعقود الضمان التي تستدعي هذا العنوان لم تلاحظ أي تغيير في العنوان، ولكنها تأخذ الآن عشرة أضعاف الرسوم.
- المتغير `treasury` بقي كما هو بعد الترقية، لأنه محفوظ في تخزين الوكيل وليس في عقد التنفيذ، والتعليمة `delegatecall` تنفذ كود التنفيذ على تخزين الوكيل. ولهذا السبب يجب أن تحافظ النسخة الثانية على ترتيب المتغيرات نفسه، فلو وضعنا متغيراً جديداً قبل `treasury` لقرأت النسخة الثانية الخانة 0 على أنها شيء آخر.
- عقد التنفيذ الأول نفسه ما زال يقول 500، لأن له تخزينه الخاص الذي لم يهيأ أبداً، وفي التجربة الأخيرة استطاع شخص غريب أن يستدعي `initialize` عليه مباشرة ويضع عنوانه هو، فنجح. وهذا لم يضر الوكيل هنا، فما زال `treasury` في الوكيل كما هو، ولكن هذا النمط بالضبط هو ما أدى إلى واحدة من أشهر حوادث Ethereum.

والصورة التالية تجمع التجربة:

![مخطط: العميل أو تطبيق المنصة يستدعي feeBps على نفس العنوان، فيصل إلى الوكيل Proxy بالعنوان 0x2279B7A0 الذي يحمل الحالة treasury وخانة EIP-1967 لعنوان التنفيذ تغيرت من 0x0165 إلى 0xa513، ومنه سهم delegatecall إلى التنفيذ الأول FeesV1 الذي يعيد 500 أي 5%، وسهم متقطع أحمر إلى التنفيذ الثاني FeesV2 الذي يعيد 5000 أي 50%، وتحته ProxyAdmin ومالكه 0xf39F حيث نجحت upgradeAndCall من المالك بغاز 39524 ورفضت من غيره، وصندوق يشرح أن العنوان والحالة والرصيد بقيت كما هي والرسوم تغيرت بقرار مفتاح واحد، وصندوق أخضر بطرق تخفيف الخطر: محفظة متعددة التوقيع ومهلة زمنية وإعلان الترقية أو التخلي عنها](https://arabroot.io/content/images/2026/10/smart-contracts-03-proxy.webp)

العقد القابل للترقية لا يلغي الثقة، وإنما ينقلها إلى من يملك مفتاح الإدارة

📌

في 6 نوفمبر 2017 فتح مستخدم في مستودع عميل Parity على GitHub بلاغاً بعنوان [«anyone can kill your contract»](https://github.com/openethereum/parity-ethereum/issues/6995?ref=arabroot.io)، وكتب فيه أنه قتل العقد بالخطأ. والعقد كان مكتبة مشتركة تعتمد عليها محافظ Parity متعددة التوقيع، تستدعيها بالتعليمة `delegatecall` كما يستدعي الوكيل عقد التنفيذ في تجربتنا، ولم تهيأ المكتبة نفسها أبداً، فاستدعى المستخدم دالة التهيئة عليها مباشرة وأصبح مالكها، كما يشرح أحد المعلقين في نفس البلاغ أن أمر القتل جاء من مالك مسجل، ثم استدعى دالة الحذف فيها. ولأن المحافظ كانت تعتمد على كود المكتبة، توقفت كلها عن العمل وتجمدت أموالها، وأعلن فريق Parity بعد ثلاثة أيام في نفس البلاغ أنه أزال المكتبة من سجله. والدرس هو بالضبط ما رأيناه في آخر سطرين من التجربة: عقد التنفيذ الذي لا يهيأ يملكه أول من يستدعي دالة التهيئة عليه. ولذلك توصي OpenZeppelin في [توثيق العقود القابلة للترقية](https://docs.openzeppelin.com/contracts/5.x/upgradeable?ref=arabroot.io) بتعطيل التهيئة في عقد التنفيذ نفسه، ولذلك أيضاً رفضت المكتبة في تجربتنا وكيلاً بلا تهيئة. ومنذ ترقية Dencun في مارس 2024 قيد [EIP-6780](https://eips.ethereum.org/EIPS/eip-6780?ref=arabroot.io) تعليمة الحذف SELFDESTRUCT فلم تعد تحذف كود العقد إلا إذا استدعيت في نفس المعاملة التي أنشأته، فلا يتكرر الحذف نفسه اليوم، أما خطأ التهيئة فما زال ممكناً كما رأيت.

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

## خريطة المخاطر

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

![ست بطاقات مخاطر: خطأ في الكود لا يصلح بعد النشر مثل The DAO في 2016 ويخففه الاختبار العشوائي والمراجعة والمكتبات المراجعة، ومفتاح الإدارة الذي غير الرسوم من 5% إلى 50% ويخففه Multisig وTimelock، والتهيئة والمكتبات المشتركة مثل Parity في نوفمبر 2017 ويخففها تعطيل التهيئة، والمعلومة الخارجية Oracle ويخففها تعدد المصادر، وأن كل شيء علني ويخففه عدم وضع بيانات شخصية على السلسلة، والمال العالق والمفتاح الضائع ويخففه طريق خروج لكل حالة ونمط السحب، وفي الأسفل المخاطر النظامية](https://arabroot.io/content/images/2026/10/smart-contracts-05-risks.webp)

كل خطر له طريقة معروفة لتخفيفه، ولكن لا يوجد عقد بلا مخاطر

- **الخطأ الذي لا يصلح**: الكود منشور أمام الجميع، ومن يجد الخطأ أولاً يستطيع أن يستغله، ولا يوجد زر إيقاف إلا إذا بنيته بنفسك وقبلت ثمنه في الثقة. وهذا سبب أن العقود التي تحمل قيمة حقيقية تمر باختبارات أكثر بكثير من تطبيقات الويب، وسوف نخصص لها مقال أدوات التطوير.
- **مفتاح الإدارة**: كل صلاحية في العقد، مثل الترقية أو الإيقاف أو تغيير الرسوم، تجعل من يملك المفتاح نقطة ثقة ونقطة هجوم معاً، فإذا سرق المفتاح سرق العقد.
- **التهيئة والمكتبات**: كما في حادثة Parity، وكما في تجربتنا مع FeesV1.
- **المعلومة الخارجية**: العقد ينفذ ما يقوله الأوراكل، فإذا استطاع أحد أن يحرك السعر أو يؤثر على المصدر أثر على العقد، وسوف نرى أمثلة حقيقية على ذلك في مقال أمن العقود الذكية.
- **العلنية**: كل مبلغ وعنوان ونص تسليم يراه أي شخص إلى الأبد، والمعاملة نفسها تظهر في الطابور قبل أن تنفذ، فيستطيع أحد أن يرى ما تنوي فعله ويسبقك إليه. لذلك لا تضع بيانات شخصية على السلسلة أصلاً، وفي المملكة تخضع البيانات الشخصية [لنظام حماية البيانات الشخصية](https://dgp.sdaia.gov.sa/wps/portal/pdp/?ref=arabroot.io)، ونص التسليم في تجربتنا رابط وهاش فقط وليس اسماً أو مستنداً.
- **المال العالق**: دالة تفشل دائماً، أو حالة لا مخرج منها مثل `Delivered` في عقدنا، أو طرف فقد مفتاحه، كلها تعني مالاً لا يخرج أبداً.

وفوق كل ذلك المخاطر النظامية، فالعقد الذكي لا يعفي المنصة من الأنظمة التي تحكم حفظ أموال العملاء والتحقق من هوياتهم في بلدها، وقد شرحنا التحقق من هوية العميل في [مقال معرفة العميل KYC](https://arabroot.io/articles/%D9%85%D8%B9%D8%B1%D9%81%D8%A9-%D8%A7%D9%84%D8%B9%D9%85%D9%8A%D9%84-kyc-%D9%88%D9%81%D8%AD%D8%B5-%D8%A7%D9%84%D8%B9%D9%82%D9%88%D8%A8%D8%A7%D8%AA-%D8%A8%D8%A7%D9%84%D8%A7%D8%B3%D9%85%D8%A7%D8%A1-%D8%A7%D9%84%D8%B9%D8%B1%D8%A8%D9%8A%D8%A9/)، وموقف الجهات الرسمية في المملكة من العملات الرقمية موضوع مقال مستقل في هذا المسار.

## من الشبكة المحلية إلى شبكة اختبار عامة

كل ما فعلناه كان على Anvil، وهي شبكة على جهازك وحدك. والخطوة التالية في أي مشروع حقيقي أن تنشر العقد على شبكة اختبار عامة Testnet، حيث تعمل عقد حقيقية بنفس قواعد الشبكة الرئيسية، ويستطيع زملاؤك أن يجربوا التطبيق معك. وكما يقول [توثيق الشبكات](https://ethereum.org/en/developers/docs/networks/?ref=arabroot.io) فإن Sepolia هي شبكة الاختبار الموصى بها لتطوير التطبيقات، وHoodi لمن يختبر تشغيل المدققين والترقيات، أما Holesky فقد أوقفت في سبتمبر 2025\. والعملة على شبكات الاختبار تحصل عليها مجاناً من صفحات توزيع تسمى Faucets، وليس لها قيمة حقيقية في الأصل، مع أن التوثيق نفسه ينبه إلى أن أسواقاً ظهرت لبعضها عندما ندرت.

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

## هل تضع منطق الضمان في عقد ذكي؟ جدول قرار

بعد التجارب جلس الفريق ليجيب عن السؤال الأصلي، والجدول التالي يلخص ما وصلوا إليه لكل جزء من خدمة الضمان:

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

وقبل أن ينشر أي فريق عقداً يحمل قيمة حقيقية، يجب أن يجيب بنعم عن كل سؤال في القائمة التالية:

1. رسمنا آلة الحالات كاملة، ولكل حالة طريق خروج، ولا توجد حالة يعلق فيها المال إلى الأبد.
2. كل دالة تتحقق من المرسل والحالة قبل أي تغيير، وتغير الحالة وتطلق الحدث قبل إرسال أي مال.
3. شغلنا المدقق الآلي، وأصلحنا ملاحظاته أو كتبنا سبب تجاهل كل واحدة في الكود نفسه.
4. جربنا كل خطأ متوقع من كل طرف، وكل مسار بالوقت، على شبكة محلية ثم على شبكة اختبار عامة.
5. نعرف كل صلاحية إدارية في العقد، ومن يملكها، وكيف يحمي مفتاحها، وهل يعرف المستخدمون بوجودها.
6. كل معلومة خارجية يعتمد عليها العقد لها مصدر معروف، ونعرف ماذا يحدث إذا كذب هذا المصدر.
7. لا توجد بيانات شخصية على السلسلة، والأحداث تكفي نظامنا ليعرف حالة كل مشروع.
8. راجعنا الجانب النظامي من مصادره الرسمية، وراجع العقد طرف مستقل قبل وضع أي مال حقيقي فيه.

## تدريب عملي

1. أضف إلى عقد الضمان حكماً Arbiter يحدد في المنشئ، ودالة `resolve(bool payFreelancer)` لا يستدعيها إلا الحكم وفي الحالة `Delivered` فقط، ثم جرب الحالة التي علق فيها المال في هذا المقال وتأكد أنها أصبحت تخرج.
2. أضف إلى `FeesV1` منشئاً Constructor يجعل `treasury` عنواناً غير صفري في عقد التنفيذ نفسه، ثم أعد آخر خطوة في `05-proxy.sh` وتأكد أن الغريب لم يعد يستطيع تهيئته، وفكر لماذا لا يؤثر هذا المنشئ على تخزين الوكيل.

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

احذف ال Container، ومجلد المكتبة الذي حملناه لأنه قرابة 15 ميجابايت، ومجلدات البناء:

```bash
docker rm -f lab-sc
rm -rf lab/proxy/lib lab/proxy/out lab/proxy/cache lab/escrow/out lab/escrow/cache
```

## الخلاصة

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

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

وفي المقال التالي من هذه المرحلة سوف نأخذ نفس عقد الضمان إلى بيئة تطوير كاملة، ونكتب له اختبارات حقيقية وعشوائية وثوابت Invariants مع Foundry، ونقارن ذلك بما تقدمه Hardhat.

[![](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/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/smart-contract-security-cover-cover.webp)دليلأمن العقود الذكية: أشهر الثغرات وحوادثها الحقيقيةنكتب عقوداً معطوبة ونهاجمها على شبكة محلية لنرى كل صنف ثغرة يعمل، ثم نضع النسخة المصححة بجانبه، من جهة الدفاع.](https://arabroot.io/articles/%D8%A7%D9%85%D9%86-%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%A7%D8%B4%D9%87%D8%B1-%D8%A7%D9%84%D8%AB%D8%BA%D8%B1%D8%A7%D8%AA-%D9%88%D8%AD%D9%88%D8%A7%D8%AF%D8%AB%D9%87%D8%A7-%D8%A7%D9%84%D8%AD%D9%82%D9%8A%D9%82%D9%8A%D8%A9/)[![](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/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/kyc-aml-cover-cover.webp)دليلمعرفة العميل KYC وفحص العقوبات بالأسماء العربيةفحصنا عشرين ألف عميل مولد مقابل قائمة مجلس الأمن، ورأينا كم مدرجاً تفوته المطابقة التامة وكم بريئاً تضيفه كل درجة في العتبة.](https://arabroot.io/articles/%D9%85%D8%B9%D8%B1%D9%81%D8%A9-%D8%A7%D9%84%D8%B9%D9%85%D9%8A%D9%84-kyc-%D9%88%D9%81%D8%AD%D8%B5-%D8%A7%D9%84%D8%B9%D9%82%D9%88%D8%A8%D8%A7%D8%AA-%D8%A8%D8%A7%D9%84%D8%A7%D8%B3%D9%85%D8%A7%D8%A1-%D8%A7%D9%84%D8%B9%D8%B1%D8%A8%D9%8A%D8%A9/)

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

- أكتوبر 2026: كتابة المقال، وتشغيل التجارب على Docker Engine 29.8.0 مع Foundry v1.8.6 (anvil وcast وforge مع المدقق الآلي) وSolc 0.8.36، ومكتبة OpenZeppelin Contracts v5.7.0، على شبكة Anvil محلية.