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

# ما هي الرموز Tokens؟ معيار ERC-20 ورموز NFT
- URL: https://arabroot.io/articles/الرموز-tokens-معيار-erc-20-والرموز-غير-القابلة-للاستبدال-nft/
- Published: 2026-10-28T06:29:30.000Z
- Updated: 2026-10-28T13:45:38.000Z
- Description: ما الذي تملكه فعلاً عندما تقول المحفظة إن لديك رمزاً، ولماذا يحتاج العقد إذناً، وكيف تعلق الرموز داخل العقود إلى الأبد.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, البلوكتشين والعملات الرقمية

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

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

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

- ما هو الرمز فعلاً، ولماذا هو جدول أرصدة داخل عقد وليس عملة في الشبكة، ومن أين جاءت المعايير.
- معيار ERC-20 دالة دالة، مع تجربة: ننشر رمزاً تجريبياً بست خانات عشرية، ونرى أين يعيش الرصيد في التخزين، ونقرأ الحدث Transfer.
- الإذن approve ثم السحب transferFrom: لماذا يحتاجهما العقد، وعقد الضمان بعد أن أصبح يقبل رمزاً، والإذن غير المحدود وخطره.
- أربعة افتراضات خاطئة تعلق الرموز داخل العقود، جربنا اثنين منها: رموز أرسلت مباشرة، ورمز يقتطع رسوماً من كل تحويل.
- الرموز غير القابلة للاستبدال NFT ومعيار ERC-721، مع شهادة إنجاز لا تنقل، وأين تعيش صورتها وبياناتها فعلاً.
- معيار ERC-1155، وجدول قرار، وقائمة تحقق قبل أن يقبل عقدك أي رمز.

## ما هو الرمز Token فعلاً؟

في المقال الأول من هذه المرحلة رأينا أن لكل حساب في Ethereum حقلاً اسمه `balance`، وهذا الحقل لعملة الشبكة نفسها ETH فقط. والشبكة تعرف معنى هذا الرصيد: تتحقق منه قبل كل معاملة، وتنقص منه الرسوم، وتنقل منه القيمة `value` في المعاملة. أما أي «عملة» أخرى تراها في محفظتك على Ethereum فالشبكة لا تعرف عنها شيئاً، لأنها ليست في حسابك أصلاً، وإنما سطر في جدول داخل عقد ذكي، والجدول يقول إن هذا العنوان يملك هذا الرقم. وعندما «ترسل» 250 وحدة من رمز إلى شخص آخر فأنت لا ترسل شيئاً، وإنما تستدعي دالة في ذلك العقد تنقص سطرك وتزيد سطره.

وقد يتساءل البعض: إذا كان الرمز مجرد عقد يكتب فيه أي شخص ما يشاء، فكيف تعرف المحافظ والمنصات والعقود الأخرى كيف تتعامل مع آلاف الرموز المختلفة؟ والإجابة هي المعايير Standards. ففي نوفمبر 2015 اقترح Fabian Vogelsteller وVitalik Buterin واجهة موحدة في [EIP-20](https://eips.ethereum.org/EIPS/eip-20?ref=arabroot.io)، وهي مجموعة دوال وأحداث بأسماء وأنواع محددة، فأي عقد يطبقها تستطيع أي محفظة أن تعرض رصيده وتحوله دون أن تعرف عنه شيئاً آخر. واسم ERC-20 هو اسم نفس المقترح عندما يتكلم الناس عن المعيار المطبق، وERC اختصار Ethereum Request for Comments. وبعده جاءت معايير أخرى لأشياء مختلفة، أشهرها ERC-721 للأشياء الفريدة سنة 2018، وERC-1155 الذي يجمع النوعين في نفس السنة.

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

## معيار ERC-20: الدوال والأحداث

المعيار نفسه قصير، وكل دالة فيه لها سبب:

- **`totalSupply()`**: مجموع كل الوحدات الموجودة.
- **`balanceOf(owner)`**: رصيد عنوان معين.
- `**transfer(to, value)**`: ينقل من رصيد المرسل إلى عنوان آخر، ويجب أن يطلق الحدث `Transfer`.
- `**approve(spender, value)**`: يسمح لعنوان آخر، غالباً عقد، أن يسحب من رصيدك حتى مبلغ معين، ويطلق الحدث `Approval`.
- **`allowance(owner, spender)`**: كم بقي من الإذن.
- **`transferFrom(from, to, value)`**: يستخدمها صاحب الإذن لينقل من رصيد غيره، وينقص الإذن بنفس المبلغ.
- **`name()` و`symbol()` و`decimals()`**: للعرض، وكلها اختيارية في المعيار، وهو يقول صراحة إن الواجهات والعقود الأخرى يجب ألا تتوقع وجودها.

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

### تجربة: رمز تجريبي بست خانات عشرية

نكتب رمزاً تجريبياً يشبه في شكله الرموز المربوطة بالدولار، وهي تستخدم عادة خانات عشرية أقل من ETH، ونبنيه على عقد `ERC20` من مكتبة [OpenZeppelin Contracts](https://docs.openzeppelin.com/contracts/5.x/erc20?ref=arabroot.io) بدل أن نكتبه من الصفر، لأن هذه المكتبة مراجعة ومستخدمة في آلاف العقود. أنشئ مشروع Foundry في `lab/tokens` بملف الإعداد التالي:

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

ثم الرمز في `src/TestUSD.sol`:

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

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

/// A test dollar-like token for the local devnet: 6 decimals, minted by its owner. No value anywhere.
contract TestUSD is ERC20, Ownable {
    constructor() ERC20("Test USD", "tUSD") Ownable(msg.sender) {}

    function decimals() public pure override returns (uint8) {
        return 6;
    }

    function mint(address to, uint256 amount) external onlyOwner {
        _mint(to, amount);
    }
}
```

لاحظ أن الرمز كله عشرة أسطر، لأن المكتبة تحمل كل دوال المعيار، وأن الدالة `mint` تنشئ وحدات جديدة من لا شيء، ولا يستطيع استدعاءها إلا مالك العقد بسبب `onlyOwner`. وهذا السطر وحده يعني أن قيمة هذا الرمز، لو كانت له قيمة، تعتمد كلياً على من يملك مفتاح المالك، وهذا ما يجب أن تسأل عنه في أي رمز تقبله: من يستطيع أن ينشئ وحدات جديدة، ومن يستطيع أن يوقف التحويل أو يجمد عنواناً؟ وسوف نعود إلى هذا السؤال في مقال العملات المستقرة.

نشغل Anvil مع مجلد التجربة، ونحمل المكتبة بإصدار ثابت، ونبني:

```bash
docker run -d --name lab-tok -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/tokens lab-tok forge install --no-git OpenZeppelin/openzeppelin-contracts@v5.7.0
docker exec -w /lab/tokens lab-tok forge build
docker exec -w /lab/tokens lab-tok forge inspect TestUSD storageLayout
```

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

```text
╭--------------+-------------------------------------------------+------+--------+-------+-------------------------╮
| Name         | Type                                            | Slot | Offset | Bytes | Contract                |
+==================================================================================================================+
| _balances    | mapping(address => uint256)                     | 0    | 0      | 32    | src/TestUSD.sol:TestUSD |
|--------------+-------------------------------------------------+------+--------+-------+-------------------------|
| _allowances  | mapping(address => mapping(address => uint256)) | 1    | 0      | 32    | src/TestUSD.sol:TestUSD |
|--------------+-------------------------------------------------+------+--------+-------+-------------------------|
| _totalSupply | uint256                                         | 2    | 0      | 32    | src/TestUSD.sol:TestUSD |
|--------------+-------------------------------------------------+------+--------+-------+-------------------------|
| _name        | string                                          | 3    | 0      | 32    | src/TestUSD.sol:TestUSD |
|--------------+-------------------------------------------------+------+--------+-------+-------------------------|
| _symbol      | string                                          | 4    | 0      | 32    | src/TestUSD.sol:TestUSD |
|--------------+-------------------------------------------------+------+--------+-------+-------------------------|
| _owner       | address                                         | 5    | 0      | 20    | src/TestUSD.sol:TestUSD |
╰--------------+-------------------------------------------------+------+--------+-------+-------------------------╯
```

جدول الأرصدة `_balances` في الخانة 0، وجدول الأذونات `_allowances` في الخانة 1، وهو جدول داخل جدول، لأن الإذن علاقة بين مالك ومسموح له. والآن السكربت `lab/01-erc20.sh` الذي ينشر الرمز، وينشئ 1000 وحدة للعميل، ويقرأ رصيده من التخزين مباشرة، ثم يحول للمستقل:

```bash
# 0 = the platform (deploys the test token), 1 = client, 2 = freelancer
PK0=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
PK1=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
CLIENT=0x70997970C51812dc3A010C7d01b50e0d17dc79C8
FREE=0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC
cd /lab/tokens
T=$(forge create --broadcast --private-key $PK0 src/TestUSD.sol:TestUSD | grep 'Deployed to' | awk '{print $3}')
echo "token: $T"
echo "name: $(cast call $T 'name()(string)')  symbol: $(cast call $T 'symbol()(string)')  decimals: $(cast call $T 'decimals()(uint8)')"
echo "== mint 1,000 tUSD to the client (1,000 x 10^6 units)"
cast send $T "mint(address,uint256)" $CLIENT 1000000000 --private-key $PK0 | grep -E '^status'
echo "balanceOf(client): $(cast call $T 'balanceOf(address)(uint256)' $CLIENT)  totalSupply: $(cast call $T 'totalSupply()(uint256)')"
echo "== where the balance lives: mapping at slot 0, key = client"
SLOT=$(cast index address $CLIENT 0); echo "slot: $SLOT"
echo "raw storage: $(cast storage $T $SLOT)  = $(cast to-dec $(cast storage $T $SLOT))"
echo "== the client pays the freelancer 250 tUSD directly"
cast send $T "transfer(address,uint256)" $FREE 250000000 --private-key $PK1 | grep -E '^(status|gasUsed) '
B=$(cast block-number)
cast logs --from-block $B --to-block $B --address $T | grep -E 'data:|^\s+0x'
echo "Transfer(address,address,uint256) = $(cast sig-event 'Transfer(address,address,uint256)')"
echo "client: $(cast call $T 'balanceOf(address)(uint256)' $CLIENT)  freelancer: $(cast call $T 'balanceOf(address)(uint256)' $FREE)"
echo "== the client tries to send more than he has"
cast send $T "transfer(address,uint256)" $FREE 900000000 --private-key $PK1 2>&1 | tail -1
echo "$T" > /lab/token-address.txt
```

ثم شغله:

```bash
docker exec lab-tok bash /lab/01-erc20.sh
```

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

```text
token: 0x5FbDB2315678afecb367f032d93F642f64180aa3
name: "Test USD"  symbol: "tUSD"  decimals: 6
== mint 1,000 tUSD to the client (1,000 x 10^6 units)
status               1 (success)
balanceOf(client): 1000000000 [1e9]  totalSupply: 1000000000 [1e9]
== where the balance lives: mapping at slot 0, key = client
slot: 0x14e04a66bf74771820a7400ff6cf065175b3d7eb25805a5bd1633b161af5d101
raw storage: 0x000000000000000000000000000000000000000000000000000000003b9aca00  = 1000000000
== the client pays the freelancer 250 tUSD directly
gasUsed              52119
status               1 (success)
  data: 0x000000000000000000000000000000000000000000000000000000000ee6b280
  	0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
  	0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8
  	0x0000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc
Transfer(address,address,uint256) = 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
client: 750000000 [7.5e8]  freelancer: 250000000 [2.5e8]
== the client tries to send more than he has
Error: Failed to estimate gas: server returned an error response: error code 3: execution reverted: custom error 0xe450d38c: 00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8000000000000000000000000000000000000000000000000000000002cb417800000000000000000000000000000000000000000000000000000000035a4e900, data: "0xe450d38c00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8000000000000000000000000000000000000000000000000000000002cb417800000000000000000000000000000000000000000000000000000000035a4e900": ERC20InsufficientBalance(0x70997970C51812dc3A010C7d01b50e0d17dc79C8, 750000000 [7.5e8], 900000000 [9e8])
```

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

- رصيد العميل 1000000000، أي مليار، وليس 1000\. والسبب أن الرمز مثل ETH لا يعرف الكسور، فكل الأرصدة أعداد صحيحة، والخانات العشرية `decimals` رقم يقول للمحافظ أين تضع الفاصلة عند العرض فقط. فمع 6 خانات تكون الوحدة الواحدة مليون جزء، و1000 وحدة مليار جزء.
- الرصيد لم نقرأه بالدالة فقط، وإنما من التخزين مباشرة: خانة القيمة في جدول داخل Solidity هي هاش Keccak-256 للمفتاح ملصقاً برقم خانة الجدول، وهذا ما حسبه `cast index address $CLIENT 0`، فوجدنا في الخانة الناتجة القيمة `0x3b9aca00`، أي مليار. فالرصيد فعلاً سطر في تخزين العقد، كما قلنا في التعريف.
- التحويل كلف 52119 وحدة غاز، مقابل 21000 لتحويل ETH، لأنه استدعاء دالة تقرأ خانتين وتكتب خانتين وتطلق حدثاً، وأغلب هذا الثمن كتابة خانة المستقل من صفر إلى قيمة كما شرحنا في مقال كيف يعمل Ethereum.
- الحدث `Transfer` له ثلاثة مواضيع: هاش توقيعه `0xddf252ad…`، وهو نفس الرقم في كل رموز ERC-20 في العالم لأن التوقيع نفسه، ثم المرسل، ثم المستقبل، والمبلغ في البيانات `0x0ee6b280`، أي 250 مليون جزء. ولأن الهاش واحد تستطيع أي أداة أن تبحث عن كل تحويلات أي رمز دون أن تعرف كوده، وهكذا تعمل مستكشفات الكتل Block Explorers.
- محاولة تحويل 900 بينما الرصيد 750 رفضت بالخطأ المخصص `ERC20InsufficientBalance` ومعه العنوان والرصيد والمطلوب، وهذا سلوك المكتبة وليس المعيار، فالمعيار كما قلنا يقول «ينبغي» أن يرفض.

والصورة التالية تضع رصيد ETH ورصيد الرمز جنباً إلى جنب:

![مخطط من جزأين: على اليمين عملة الشبكة ETH كرصيد في حساب الشبكة نفسه مع حقول nonce وbalance وcodeHash وstorageRoot وتحويل بسيط بغاز 21000، وعلى اليسار الرمز Token كجدول داخل العقد TestUSD، فيه mapping _balances في الخانة 0، وخانة رصيد العميل keccak256 للعميل مع 0 تساوي 0x14e04a66 وقيمتها 1000000000 أي 1000 tUSD بست خانات عشرية، وتحويل 250 tUSD بغاز 52119، وفي الأسفل الحدث Transfer بموضوعاته الثلاثة: هاش التوقيع 0xddf252ad والمرسل والمستقبل، والبيانات 250000000، وملاحظة أن الرصيد الذي تراه المحفظة سطر في عقد](https://arabroot.io/content/images/2026/10/tokens-erc20-nft-01-token.webp)

رصيد ETH في حسابك، أما رصيد الرمز فسطر في جدول داخل عقد يحكمه كوده

## الإذن approve ثم السحب transferFrom

الآن نعود إلى عقد الضمان. في المقالات السابقة كان العميل يمول العقد بإرسال ETH مع الاستدعاء نفسه، والقيمة `msg.value` يراها العقد ويتحقق منها. أما مع الرمز فلا يوجد ما يشبه `msg.value`، والسبب أن الرمز ليس جزءاً من المعاملة، وإنما سطر في عقد آخر. فإذا أرسل العميل الرموز إلى عقد الضمان بالدالة `transfer` فإن عقد الرمز يعدل جدوله، ولكن عقد الضمان لا يستدعيه أحد ولا يعرف أن شيئاً حدث، فلا يستطيع أن يغير حالته إلى «ممول».

والحل الذي يقدمه المعيار خطوتان: العميل يعطي عقد الضمان إذناً بالسحب من رصيده حتى مبلغ معين بالدالة `approve`، ثم يستدعي `fund` في عقد الضمان، وعقد الضمان نفسه يستدعي `transferFrom` ليسحب المبلغ، فيعرف أن المال وصل لأنه هو من سحبه، ويغير حالته في نفس المعاملة. وهذا هو عقد الضمان بعد أن أصبح يقبل رمزاً، في `src/TokenEscrow.sol`، وقد حذفنا منه الموعد والحكم حتى نركز على الرمز:

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

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

/// The escrow from the previous articles, paid in an ERC-20 token instead of ETH.
/// fund() pulls the tokens with transferFrom, so the client must approve the escrow first.
contract TokenEscrow {
    using SafeERC20 for IERC20;

    enum State { Created, Funded, Delivered, Released, Refunded }

    IERC20 public immutable token;
    address public immutable client;
    address public immutable freelancer;
    uint256 public immutable amount;
    State public state;

    error NotClient();
    error NotFreelancer();
    error WrongState(State current);
    error ZeroAddress();

    constructor(IERC20 _token, address _freelancer, uint256 _amount) {
        if (_freelancer == address(0) || address(_token) == address(0)) revert ZeroAddress();
        token = _token;
        client = msg.sender;
        freelancer = _freelancer;
        amount = _amount;
    }

    function fund() external {
        if (msg.sender != client) revert NotClient();
        if (state != State.Created) revert WrongState(state);
        state = State.Funded;
        token.safeTransferFrom(msg.sender, address(this), amount);
    }

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

    function release() external {
        if (msg.sender != client) revert NotClient();
        if (state != State.Delivered) revert WrongState(state);
        state = State.Released;
        token.safeTransfer(freelancer, amount);
    }
}
```

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

- الدالة `fund` لم تعد `payable`، فلا يمكن أن يعلق فيها ETH بالخطأ، والمال يأتي من `safeTransferFrom`.
- عنوان الرمز نفسه ثابت في العقد، فعقد الضمان يقبل رمزاً واحداً حدده العميل عند النشر، وهذا قرار تصميم مهم سوف نرى سببه في قسم الافتراضات الخاطئة.
- استخدمنا مكتبة `SafeERC20` بدل استدعاء `transferFrom` مباشرة، والسبب أن بعض الرموز القديمة لا تعيد قيمة أصلاً، وبعضها يعيد `false` بدل أن يفشل، والمكتبة تتعامل مع الحالتين فتفشل المعاملة كلها إذا لم ينجح التحويل.

والسكربت `lab/02-approve.sh` يجرب التمويل دون إذن، ثم بإذن محدد، ثم يكمل المشروع، ثم يجرب الإذن غير المحدود:

```bash
PK1=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
PK2=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a
CLIENT=0x70997970C51812dc3A010C7d01b50e0d17dc79C8
FREE=0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC
T=$(cat /lab/token-address.txt)
cd /lab/tokens
echo "== the client deploys a token escrow for 300 tUSD"
E=$(forge create --broadcast --private-key $PK1 src/TokenEscrow.sol:TokenEscrow --constructor-args $T $FREE 300000000 | grep 'Deployed to' | awk '{print $3}')
echo "escrow: $E"
echo "== fund() without approve"
cast send $E "fund()" --private-key $PK1 2>&1 | tail -1
echo "== approve the escrow for exactly 300 tUSD, then fund"
cast send $T "approve(address,uint256)" $E 300000000 --private-key $PK1 | grep -E '^(status|gasUsed) '
echo "allowance: $(cast call $T 'allowance(address,address)(uint256)' $CLIENT $E)"
cast send $E "fund()" --private-key $PK1 | grep -E '^(status|gasUsed) '
echo "allowance after fund: $(cast call $T 'allowance(address,address)(uint256)' $CLIENT $E)  escrow balance: $(cast call $T 'balanceOf(address)(uint256)' $E)"
echo "== deliver and release"
cast send $E "deliver()" --private-key $PK2 | grep -E '^status'
cast send $E "release()" --private-key $PK1 | grep -E '^status'
echo "freelancer: $(cast call $T 'balanceOf(address)(uint256)' $FREE)  escrow: $(cast call $T 'balanceOf(address)(uint256)' $E)"
echo "== an unlimited approval, the common default in many apps"
cast send $T "approve(address,uint256)" $E $(cast max-uint) --private-key $PK1 | grep -E '^status'
echo "allowance: $(cast call $T 'allowance(address,address)(uint256)' $CLIENT $E)"
echo "== revoke it: approve 0"
cast send $T "approve(address,uint256)" $E 0 --private-key $PK1 | grep -E '^status'
echo "allowance: $(cast call $T 'allowance(address,address)(uint256)' $CLIENT $E)"
echo "$E" > /lab/escrow-address.txt
```

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

```text
== the client deploys a token escrow for 300 tUSD
escrow: 0x71C95911E9a5D330f4D621842EC243EE1343292e
== fund() without approve
Error: Failed to estimate gas: server returned an error response: error code 3: execution reverted: custom error 0xfb8f41b2: 00000000000000000000000071c95911e9a5d330f4d621842ec243ee1343292e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011e1a300, data: "0xfb8f41b200000000000000000000000071c95911e9a5d330f4d621842ec243ee1343292e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011e1a300": ERC20InsufficientAllowance(0x71C95911E9a5D330f4D621842EC243EE1343292e, 0, 300000000 [3e8])
== approve the escrow for exactly 300 tUSD, then fund
gasUsed              46892
status               1 (success)
allowance: 300000000 [3e8]
gasUsed              78150
status               1 (success)
allowance after fund: 0  escrow balance: 300000000 [3e8]
== deliver and release
status               1 (success)
status               1 (success)
freelancer: 550000000 [5.5e8]  escrow: 0
== an unlimited approval, the common default in many apps
status               1 (success)
allowance: 115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77]
== revoke it: approve 0
status               1 (success)
allowance: 0
```

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

- التمويل دون إذن فشل في عقد الرمز وليس في عقد الضمان، والخطأ `ERC20InsufficientAllowance` يحمل ثلاث قيم: المسموح له وهو عقد الضمان، والإذن الحالي صفر، والمطلوب 300 مليون جزء.
- الإذن نفسه معاملة كلفت 46892 وحدة غاز، والتمويل 78150، أي أن الدفع بالرمز يحتاج معاملتين يوقعهما العميل بدل معاملة واحدة مع ETH، وهذا يظهر في تجربة المستخدم: المحفظة تطلب منه التوقيع مرتين.
- بعد التمويل أصبح الإذن صفراً، لأن `transferFrom` طرحت منه المبلغ الذي سحبته، وأصبح في عقد الضمان 300 وحدة. ثم وصل للمستقل 300 فأصبح رصيده 550، مع 250 من التحويل المباشر في التجربة الأولى.
- الإذن غير المحدود هو أكبر رقم يحمله `uint256`، أي 2 أس 256 ناقص 1، وتطلبه تطبيقات كثيرة حتى لا يحتاج المستخدم أن يوقع إذناً جديداً في كل مرة. ومعناه أن العقد يستطيع أن يسحب كل رصيدك من هذا الرمز، الآن وفي المستقبل، دون أن تسألك المحفظة مرة أخرى. وإلغاؤه هو إذن جديد بالقيمة صفر، وهو معاملة تدفع غازها.

والصورة التالية تبين الطريق كاملاً بين الأطراف الثلاثة:

![مخطط تسلسلي بين ثلاثة أطراف: العميل وعقد الرمز TestUSD وعقد الضمان TokenEscrow، أولاً سهم أحمر متقطع لاستدعاء fund دون إذن ينتهي بالخطأ ERC20InsufficientAllowance بإذن صفري ومطلوب 300000000، ثم الخطوة 1 approve من العميل إلى عقد الرمز بغاز 46892 يكتب في جدول الإذن 300، ثم الخطوة 2 fund من العميل إلى عقد الضمان بغاز 78150، ثم الخطوة 3 من عقد الضمان إلى عقد الرمز transferFrom من العميل إلى الضمان فيصبح الإذن 0 ورصيد الضمان 300000000، وفي الأسفل تحذير من الإذن غير المحدود 2 أس 256 ناقص 1 وإلغائه بإذن صفري](https://arabroot.io/content/images/2026/10/tokens-erc20-nft-02-approve.webp)

العقد لا يرى الرموز التي ترسل إليه، لذلك يسحبها بنفسه بإذن مسبق ويعرف أنها وصلت

⚠️

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

### مشكلة تغيير الإذن، والتوقيع بدل المعاملة

في نص المعيار ملاحظة قديمة عن الدالة `approve` نفسها: إذا كان لديك إذن لعقد بمبلغ 100 وأردت تغييره إلى 50، فأرسلت معاملة `approve` بقيمة 50، فإن الطرف المسموح له يستطيع أن يرى معاملتك في الطابور قبل أن تنفذ، فيسحب الـ 100 القديمة بسرعة، ثم بعد أن تنفذ معاملتك يسحب الـ 50 الجديدة أيضاً، فيأخذ 150 بدل 50\. ولذلك يقترح المعيار أن تضبط الإذن أولاً على صفر ثم على القيمة الجديدة، ويقول إن العقد نفسه لا يجب أن يفرض ذلك. وهذا مثال على أن رؤية المعاملات قبل تنفيذها، التي ذكرناها في مخاطر العقود الذكية، تؤثر حتى على أبسط دالة.

وهناك امتداد معروف للمعيار اسمه [EIP-2612](https://eips.ethereum.org/EIPS/eip-2612?ref=arabroot.io) يضيف دالة `permit`: بدل أن يرسل المالك معاملة إذن ويدفع غازها، يوقع رسالة على جهازه بصيغة EIP-712، وأي طرف يستطيع أن يرسل هذا التوقيع إلى العقد فيصبح الإذن نافذاً. وهذا يوفر على المستخدم معاملة، ولكنه يعني أيضاً أن توقيع رسالة في المحفظة، وليس فقط إرسال معاملة، قد يعطي أحداً حق سحب رموزك، والمقترح نفسه يذكر في قسم الأمان أن مشكلة سباق الإذن تنطبق عليه أيضاً. لذلك القاعدة التي يجب أن يتعلمها مستخدمو المنصة: اقرأ ما تطلب المحفظة توقيعه، ولو لم يكن معاملة.

## أربعة افتراضات خاطئة تعلق الرموز داخل العقود

المعيار يحدد شكل الدوال، ولكنه يترك أشياء كثيرة لمن يكتب الرمز، وكل افتراض يبنيه عقدك على سلوك لم يكتبه المعيار هو خطأ ينتظر رمزاً يتصرف بطريقة أخرى. وقد جربنا اثنين من هذه الافتراضات في السكربت `lab/03-pitfalls.sh`. والأول يحتاج رمزاً عادياً، أما الثاني فيحتاج رمزاً يقتطع رسوماً من كل تحويل، وهو سلوك يسمح به المعيار، وهذا عقده في `src/FeeToken.sol`:

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

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

/// A token that burns 1% of every transfer: legal under ERC-20, and a trap for contracts that assume
/// "I asked for X, so I received X".
contract FeeToken is ERC20 {
    constructor() ERC20("Fee Token", "FEE") {
        _mint(msg.sender, 1_000_000 ether);
    }

    function _update(address from, address to, uint256 value) internal override {
        if (from != address(0) && to != address(0)) {
            uint256 fee = value / 100;
            super._update(from, address(0), fee);
            value -= fee;
        }
        super._update(from, to, value);
    }
}
```

والدالة `_update` في مكتبة OpenZeppelin هي النقطة التي يمر بها كل تحويل وإنشاء وحرق، فعندما نعيد كتابتها نغير سلوك كل التحويلات: نحرق 1% بإرسالها إلى العنوان صفر، ونحول الباقي. وهذا هو السكربت:

```bash
PK0=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
PK1=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
PK2=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a
CLIENT=0x70997970C51812dc3A010C7d01b50e0d17dc79C8
FREE=0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC
T=$(cat /lab/token-address.txt); E=$(cat /lab/escrow-address.txt)
cd /lab/tokens
echo "== pitfall 1: the client sends 50 tUSD straight to the finished escrow with transfer()"
cast send $T "transfer(address,uint256)" $E 50000000 --private-key $PK1 | grep -E '^status'
echo "escrow balance: $(cast call $T 'balanceOf(address)(uint256)' $E)  state: $(cast call $E 'state()(uint8)')"
echo "functions the escrow has:"; forge inspect TokenEscrow methodIdentifiers | grep -oE '\| [a-z][A-Za-z]*\(' | tr -d '|( ' | tr '\n' ' '; echo
echo "== pitfall 2: a fee-on-transfer token (1% burned on every transfer)"
F=$(forge create --broadcast --private-key $PK0 src/FeeToken.sol:FeeToken | grep 'Deployed to' | awk '{print $3}')
cast send $F "transfer(address,uint256)" $CLIENT 1000ether --private-key $PK0 | grep -E '^status'
echo "client got: $(cast call $F 'balanceOf(address)(uint256)' $CLIENT --ether 2>/dev/null || cast call $F 'balanceOf(address)(uint256)' $CLIENT)"
E2=$(forge create --broadcast --private-key $PK1 src/TokenEscrow.sol:TokenEscrow --constructor-args $F $FREE 100ether | grep 'Deployed to' | awk '{print $3}')
cast send $F "approve(address,uint256)" $E2 100ether --private-key $PK1 | grep -E '^status'
cast send $E2 "fund()" --private-key $PK1 | grep -E '^status'
echo "escrow expects: $(cast call $E2 'amount()(uint256)')  escrow holds: $(cast call $F 'balanceOf(address)(uint256)' $E2)"
cast send $E2 "deliver()" --private-key $PK2 | grep -E '^status'
echo "== release()"
cast send $E2 "release()" --private-key $PK1 2>&1 | tail -1
echo "state: $(cast call $E2 'state()(uint8)')"
```

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

```text
== pitfall 1: the client sends 50 tUSD straight to the finished escrow with transfer()
status               1 (success)
escrow balance: 50000000 [5e7]  state: 3
functions the escrow has:
amount client deliver freelancer fund release state token
== pitfall 2: a fee-on-transfer token (1% burned on every transfer)
status               1 (success)
client got: 990000000000000000000 [9.9e20]
status               1 (success)
status               1 (success)
escrow expects: 100000000000000000000 [1e20]  escrow holds: 99000000000000000000 [9.9e19]
status               1 (success)
== release()
Error: Failed to estimate gas: server returned an error response: error code 3: execution reverted: custom error 0xe450d38c: 00000000000000000000000005aa229aec102f78ce0e852a812a388f076aa5550000000000000000000000000000000000000000000000055005f0c6144800000000000000000000000000000000000000000000000000055de6a779bbac0000, data: "0xe450d38c00000000000000000000000005aa229aec102f78ce0e852a812a388f076aa5550000000000000000000000000000000000000000000000055005f0c6144800000000000000000000000000000000000000000000000000055de6a779bbac0000": ERC20InsufficientBalance(0x05Aa229Aec102f78CE0E852A812a388F076Aa555, 98000000000000000000 [9.8e19], 99000000000000000000 [9.9e19])
state: 2
```

وفي المخرج قصتان:

- **الرموز المرسلة مباشرة**: أرسل العميل 50 tUSD إلى عقد الضمان المنتهي بالدالة `transfer`، ربما لأنه ظن أن هذه طريقة الدفع لمشروع جديد، فنجح التحويل، لأن عقد الرمز لا يسأل المستقبل هل يريد الرموز أو يعرف كيف يتعامل معها. والآن في عقد الضمان 50 وحدة وحالته 3، أي Released، ودواله الثماني كلها لا تخرج هذه الرموز: لا توجد دالة سحب عامة، و`release` لا تعمل إلا في الحالة 2\. فهذه الرموز عالقة إلى الأبد، ولا يستطيع أحد أن يعيدها، لا المنصة ولا العميل. وهذا الخطأ شائع جداً لأن ERC-20 لا يبلغ المستقبل، بعكس ERC-721 كما سوف نرى.
- **الرمز الذي يقتطع رسوماً**: العميل أرسل 1000 وحدة لنفسه فوصله 990، وهذا أول دليل على سلوك الرمز. ثم أعطى عقد الضمان إذناً بـ 100 ومول، فسحب العقد 100 كما طلب، ولكن الذي وصله 99 فقط. والعقد افترض أن ما طلبه هو ما وصله، فغير حالته إلى ممول كأن كل شيء سليم. وعند الصرف حاول أن يرسل 100 للمستقل، فاقتطع الرمز 1 من رصيد العقد أولاً، فبقي 98، ثم حاول أن ينقل 99 فلم يجد، فرفض بالخطأ `ERC20InsufficientBalance(escrow, 98e18, 99e18)`. والنتيجة أن الحالة بقيت 2، أي Delivered، والمال عالق، لأن `release` سوف تفشل دائماً.

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

![أربع بطاقات: رموز أرسلت مباشرة إلى عقد لا يتوقعها، حيث نجح transfer لخمسين tUSD إلى عقد ضمان حالته Released ولا توجد فيه دالة تخرجها فهي عالقة لأن ERC-20 لا يبلغ المستقبل، ورمز يقتطع رسوماً من كل تحويل حيث توقع العقد 100 واستلم 99 ثم فشل الصرف بالخطأ ERC20InsufficientBalance بقيمة 98 مقابل 99 وبقيت الحالة Delivered، والخانات العشرية حيث 1 tUSD مليون وحدة و1 ETH عشرة أس 18، ورموز لا تتبع المعيار حرفياً ولا تعيد bool والحل SafeERC20، وفي الأسفل قاعدة: اقبل رموزاً تعرف سلوكها وقس الرصيد قبل الاستلام وبعده](https://arabroot.io/content/images/2026/10/tokens-erc20-nft-03-pitfalls.webp)

كل افتراض لم يكتبه المعيار قد يعلق المال في العقد، وقد رأينا اثنين منها يحدثان فعلاً

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

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

## الاسم لا يثبت شيئاً: الإصدار والحرق وهوية الرمز

بقي سؤالان يطرحهما كل من يرى رمزاً في محفظته لأول مرة: من أين جاءت هذه الوحدات أصلاً؟ وكيف أعرف أن هذا الرمز هو الرمز الذي أظنه؟ والإجابة عن السؤالين في تجربة قصيرة، واحفظ السكربت التالي باسم `lab/05-names.sh`، وهو يقرأ حدث الإصدار من الكتلة 2، ثم ينشر من حساب آخر رمزاً بنفس الكود:

```bash
PK3=0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6
T=$(cat /lab/token-address.txt)
cd /lab/tokens
echo "== the mint of 1,000 tUSD, as a Transfer event (block 2)"
cast logs --from-block 2 --to-block 2 --address $T | grep -E 'data:|^\s+0x'
echo "== anyone can deploy a token with the same name and symbol"
FAKE=$(forge create --broadcast --private-key $PK3 src/TestUSD.sol:TestUSD | grep 'Deployed to' | awk '{print $3}')
for a in $T $FAKE; do
  echo "$a  name: $(cast call $a 'name()(string)')  symbol: $(cast call $a 'symbol()(string)')  owner: $(cast call $a 'owner()(address)')"
done
echo "code identical? $( [ "$(cast code $T)" = "$(cast code $FAKE)" ] && echo yes || echo no )"
```

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

```text
== the mint of 1,000 tUSD, as a Transfer event (block 2)
  data: 0x000000000000000000000000000000000000000000000000000000003b9aca00
  	0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
  	0x0000000000000000000000000000000000000000000000000000000000000000
  	0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8
== anyone can deploy a token with the same name and symbol
0x5FbDB2315678afecb367f032d93F642f64180aa3  name: "Test USD"  symbol: "tUSD"  owner: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
0x057ef64E23666F000b34aE31332854aCBd1c8544  name: "Test USD"  symbol: "tUSD"  owner: 0x90F79bf6EB2c4f870365E785982E1f101E93b906
code identical? yes
```

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

- الإصدار Minting ليس حدثاً خاصاً في المعيار، وإنما تحويل من العنوان صفر: الموضوع الثاني أصفار، والثالث العميل، والبيانات مليار جزء. وبنفس الطريقة يكون الحرق Burning تحويلاً إلى العنوان صفر، وهذا ما فعله الرمز الذي يقتطع رسوماً عندما أرسل 1% إلى الصفر. لذلك تستطيع أي أداة أن تحسب تاريخ المعروض الكلي لأي رمز من أحداثه وحدها: كل تحويل من الصفر يزيده، وكل تحويل إلى الصفر ينقصه.
- الحساب 3، وهو ليس المنصة، نشر رمزاً اسمه «Test USD» ورمزه «tUSD»، بنفس الكود بالضبط، فالكود الثنائي متطابق لأن المالك ليس جزءاً من الكود وإنما من التخزين. والفرق الوحيد بين الرمزين العنوان والمالك.

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

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

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

## الرموز غير القابلة للاستبدال NFT

الوحدات في ERC-20 متطابقة، فأي 100 tUSD تساوي أي 100 tUSD أخرى، وهذا معنى قابلة للاستبدال Fungible. أما شهادة الإنجاز التي يريدها فريق المنتج فلا تشبه أي شهادة أخرى: لها رقم مشروع ومستقل وتاريخ. والمعيار الذي يصف هذه الأشياء هو [EIP-721](https://eips.ethereum.org/EIPS/eip-721?ref=arabroot.io) الذي نشر في يناير 2018، ويصف نفسه بأنه واجهة قياسية للرموز غير القابلة للاستبدال Non-Fungible Tokens واختصاراً NFT، ويسميها أيضاً سندات Deeds. والفرق في الجدول نفسه: بدل «عنوان ورصيد» يحفظ العقد «رقم رمز ومالك»، والدالة الأساسية `ownerOf(tokenId)`، والمعيار يقول إن الرمز المسجل للعنوان صفر غير صالح وإن السؤال عنه يجب أن يفشل.

وفي المعيار دالتان للتحويل، والفرق بينهما هو بالضبط الخطأ الذي رأيناه مع ERC-20: الدالة `safeTransferFrom` تتحقق إذا كان المستقبل عقداً، وتستدعي فيه دالة اسمها `onERC721Received`، فإذا لم يرد العقد بقيمة محددة يفشل التحويل، أما `transferFrom` فلا تتحقق، والمعيار يكتب بأحرف كبيرة أن المستدعي مسؤول عن التأكد أن المستقبل يستطيع أن يستقبل الرموز، وإلا قد تضيع إلى الأبد.

### تجربة: شهادة إنجاز لا تنقل

الشهادة يجب أن تبقى مع من استحقها، فلا معنى لأن يبيعها المستقل أو يهديها، لذلك نكتب عقد ERC-721 يرفض أي تحويل بين شخصين، ويسمح فقط بالإصدار، وهذا النوع يسمى أحياناً الرمز المرتبط بصاحبه Soulbound. وهذا العقد في `src/JobCertificate.sol`:

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

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

/// A completion certificate for a finished job: one NFT per job, owned by the freelancer.
/// It cannot be transferred (a certificate belongs to the person who earned it).
contract JobCertificate is ERC721, Ownable {
    string private baseURI;
    uint256 public nextId = 1;

    error Soulbound();

    constructor(string memory base) ERC721("Job Certificate", "JOB") Ownable(msg.sender) {
        baseURI = base;
    }

    function issue(address freelancer) external onlyOwner returns (uint256 id) {
        id = nextId++;
        _safeMint(freelancer, id);
    }

    function setBaseURI(string calldata base) external onlyOwner {
        baseURI = base;
    }

    function _baseURI() internal view override returns (string memory) {
        return baseURI;
    }

    /// Minting (from == 0) is allowed; any transfer between two people is refused.
    function _update(address to, uint256 tokenId, address auth) internal override returns (address) {
        address from = _ownerOf(tokenId);
        if (from != address(0) && to != address(0)) revert Soulbound();
        return super._update(to, tokenId, auth);
    }
}
```

والسكربت `lab/04-nft.sh` يصدر شهادة للمستقل، ويقرأها، ويحاول المستقل أن ينقلها، ثم تغير المنصة رابط البيانات، ثم نجرب ماذا يحدث إذا أصدرت المنصة شهادة لعقد لا يستطيع استقبالها:

```bash
PK0=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
PK2=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a
FREE=0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC
OTHER=0x90F79bf6EB2c4f870365E785982E1f101E93b906
cd /lab/tokens
echo "== the platform deploys the certificate contract"
N=$(forge create --broadcast --private-key $PK0 src/JobCertificate.sol:JobCertificate --constructor-args "https://example.com/certificates/" | grep 'Deployed to' | awk '{print $3}')
echo "certificates: $N"
echo "== issue certificate #1 to the freelancer"
cast send $N "issue(address)" $FREE --private-key $PK0 | grep -E '^(status|gasUsed) '
echo "ownerOf(1): $(cast call $N 'ownerOf(uint256)(address)' 1)  balanceOf(freelancer): $(cast call $N 'balanceOf(address)(uint256)' $FREE)"
echo "tokenURI(1): $(cast call $N 'tokenURI(uint256)(string)' 1)"
echo "ownerOf(2), never issued:"; cast call $N 'ownerOf(uint256)(address)' 2 2>&1 | tail -1
echo "supportsInterface(ERC-721 0x80ac58cd): $(cast call $N 'supportsInterface(bytes4)(bool)' 0x80ac58cd)"
echo "== the freelancer tries to sell / give the certificate away"
cast send $N "transferFrom(address,address,uint256)" $FREE $OTHER 1 --private-key $PK2 2>&1 | tail -1
echo "== the platform moves its web server: the metadata link changes for every certificate"
cast send $N "setBaseURI(string)" "https://new-host.example.net/c/" --private-key $PK0 | grep -E '^status'
echo "tokenURI(1): $(cast call $N 'tokenURI(uint256)(string)' 1)"
echo "== what is stored on-chain for certificate #1? the owner, nothing else about the job"
cast storage $N $(cast index uint256 1 2)
echo "== what if the platform issues a certificate to a contract that cannot hold NFTs (the token escrow)?"
cast send $N "issue(address)" $(cat /lab/escrow-address.txt) --private-key $PK0 2>&1 | tail -1
echo "ERC721NonexistentToken(uint256) = $(cast sig 'ERC721NonexistentToken(uint256)')"
```

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

```text
== the platform deploys the certificate contract
certificates: 0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9
== issue certificate #1 to the freelancer
gasUsed              79651
status               1 (success)
ownerOf(1): 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC  balanceOf(freelancer): 1
tokenURI(1): "https://example.com/certificates/1"
ownerOf(2), never issued:
- server returned an error response: error code 3: execution reverted: custom error 0x7e273289: , data: "0x7e2732890000000000000000000000000000000000000000000000000000000000000002"
supportsInterface(ERC-721 0x80ac58cd): true
== the freelancer tries to sell / give the certificate away
Error: Failed to estimate gas: server returned an error response: error code 3: execution reverted: custom error 0xa4420a95, data: "0xa4420a95": Soulbound
== the platform moves its web server: the metadata link changes for every certificate
status               1 (success)
tokenURI(1): "https://new-host.example.net/c/1"
== what is stored on-chain for certificate #1? the owner, nothing else about the job
0x0000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc
== what if the platform issues a certificate to a contract that cannot hold NFTs (the token escrow)?
Error: Failed to estimate gas: server returned an error response: error code 3: execution reverted: custom error 0x64a0ae92: 00000000000000000000000071c95911e9a5d330f4d621842ec243ee1343292e, data: "0x64a0ae9200000000000000000000000071c95911e9a5d330f4d621842ec243ee1343292e": ERC721InvalidReceiver(0x71C95911E9a5D330f4D621842EC243EE1343292e)
ERC721NonexistentToken(uint256) = 0x7e273289
```

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

- إصدار الشهادة كلف 79651 وحدة غاز، والمالك المستقل، ورصيده 1، فالرصيد في ERC-721 عدد الرموز وليس قيمتها.
- السؤال عن الشهادة رقم 2 التي لم تصدر فشل بالخطأ `0x7e273289`، وقد حسبنا في آخر سطر أنه `ERC721NonexistentToken`، كما يطلب المعيار، فلا يمكن أن تقول الدالة إن صاحبها العنوان صفر.
- `supportsInterface(0x80ac58cd)` أعاد `true`، وهذا الرقم معرف واجهة ERC-721، وبه تعرف المحافظ والمنصات أن هذا العقد رموز غير قابلة للاستبدال دون أن تجرب دواله، كما يصف معيار اكتشاف الواجهات [ERC-165](https://eips.ethereum.org/EIPS/eip-165?ref=arabroot.io).
- محاولة المستقل نقل الشهادة رفضت بالخطأ `Soulbound`، لأن دالتنا `_update` ترفض أي تحويل يكون فيه مرسل ومستقبل غير الصفر.
- الأهم: الرابط `tokenURI` تغير لكل الشهادات بمعاملة واحدة من المنصة، لأنه ليس محفوظاً مع كل شهادة، وإنما يركب من بداية مشتركة ورقم. وعندما قرأنا خانة الشهادة رقم 1 في جدول المالكين في الخانة 2 من التخزين وجدنا عنوان المستقل فقط، ولا شيء عن المشروع ولا الصورة.
- عندما حاولت المنصة إصدار شهادة لعقد الضمان، وهو عقد لا يعرف الدالة `onERC721Received`، فشل الإصدار بالخطأ `ERC721InvalidReceiver`، لأننا استخدمنا `_safeMint`. وهذه هي الحماية التي يفتقدها ERC-20، ولو استخدمنا `_mint` العادية لنجح الإصدار وعلقت الشهادة في عقد لا يستطيع أن يفعل بها شيئاً.

### أين تعيش الصورة والبيانات؟

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

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

![مخطط من جزأين: على اليمين ما على السلسلة داخل العقد JobCertificate، جدول _owners في الخانة 2 وفيه الشهادة 1 للمستقل، والرصيد 1، وbaseURI، وtokenURI يساوي baseURI مع الرقم، مع ملاحظة أن النقل مرفوض بالخطأ Soulbound، وسهم رابط إلى اليسار حيث ما خارج السلسلة على سيرفر المنصة، ملف JSON فيه name وdescription وimage يستطيع صاحب السيرفر تغييره، وتغير رابط كل الشهادات بمعاملة واحدة، وفي الأسفل نتائج التجربة: الإصدار بغاز 79651، وERC721NonexistentToken للشهادة 2، وSoulbound عند النقل، وERC721InvalidReceiver عند الإصدار لعقد الضمان، وsupportsInterface تساوي true](https://arabroot.io/content/images/2026/10/tokens-erc20-nft-04-nft.webp)

الرمز غير القابل للاستبدال يثبت من يملك الرقم، أما ما يمثله الرقم فيعيش غالباً خارج السلسلة

## ERC-1155: النوعان في عقد واحد

إذا أرادت المنصة أكثر من نوع من الشارات، مثل «أول مشروع» و«عشرة مشاريع» و«تقييم ممتاز»، ولكل نوع آلاف النسخ المتطابقة، فإن ERC-721 يحتاج رمزاً منفصلاً لكل نسخة، وERC-20 يحتاج عقداً لكل نوع. والمعيار [EIP-1155](https://eips.ethereum.org/EIPS/eip-1155?ref=arabroot.io) الذي نشر في يونيو 2018 يجمع الحالتين: عقد واحد يدير أي عدد من أنواع الرموز، وكل رقم نوع له رصيد لكل عنوان، فيكون قابلاً للاستبدال إذا كان منه آلاف، وفريداً إذا كان منه واحد فقط. ويذكر المعيار أن تحويل عدة أنواع في دفعة واحدة Batch يقلل الغاز، وفيه مثل ERC-721 استدعاء للمستقبل إذا كان عقداً، فلا تعلق فيه الرموز كما تعلق في ERC-20\. ولكن هذه المرونة لها ثمن في الفهم: الإذن في ERC-1155 ليس بمبلغ محدد كما في ERC-20، وإنما إذن لعقد آخر بأن يتصرف في كل رموزك في هذا العقد بكل أنواعها، فإذا أعطيته لعقد خاطئ فقد أعطيته كل شيء دفعة واحدة. وكذلك في ERC-721 توجد دالتان للإذن، واحدة لرمز واحد، وأخرى اسمها `setApprovalForAll` تعطي الطرف الآخر حق التصرف في كل رموزك من نفس العقد، وهي التي تطلبها عادة منصات البيع، وهي أيضاً ما تطلبه المواقع المزيفة. والقاعدة لفريق المنصة: اختر المعيار الأبسط الذي يكفي حاجتك، فالشهادة الفردية يكفيها ERC-721، والدفع يكفيه ERC-20، ولا تلجأ إلى ERC-1155 إلا إذا كانت لديك فعلاً أنواع كثيرة من الشارات بأعداد كبيرة. والصورة التالية تجمع المعايير الثلاثة:

![جدول يقارن ERC-20 وERC-721 وERC-1155: هل الوحدات متطابقة، نعم قابلة للاستبدال مقابل لكل رمز رقم ومالك مقابل الاثنان معاً، وماذا يحفظ العقد، رصيد لكل عنوان مقابل مالك لكل رقم مقابل رصيد لكل عنوان ونوع، وهل يبلغ المستقبل إذا كان عقداً، لا مقابل نعم في safeTransferFrom مقابل نعم في كل تحويل آمن، والإذن للغير، ومثال من القصة، الدفع بالرمز التجريبي مقابل شهادة إنجاز مقابل شارات بأنواع، وتاريخ المعيار نوفمبر 2015 ويناير 2018 ويونيو 2018، وفي الأسفل أن المعيار يحدد الواجهة فقط](https://arabroot.io/content/images/2026/10/tokens-erc20-nft-05-standards.webp)

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

## كم يكلف الدفع بالرمز مقارنة بعملة الشبكة؟

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

| العملية               | بعملة الشبكة ETH      | بالرمز التجريبي                           |
| --------------------- | --------------------- | ----------------------------------------- |
| تحويل مباشر بين شخصين | 21000                 | 52119                                     |
| تمويل عقد الضمان      | 44885 في معاملة واحدة | 46892 للإذن ثم 78150 للتمويل، في معاملتين |
| إصدار شهادة إنجاز     | لا ينطبق              | 79651                                     |

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

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

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

## جدول قرار للفريق

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

| القرار                   | الخيار التقني                                                        | السبب من التجارب                                                                |
| ------------------------ | -------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| الدفع في عقد الضمان برمز | رمز واحد محدد في المنشئ، بعد قراءة كوده وصلاحيات مالكه               | الرمز الذي يقتطع رسوماً علق المال، والرمز المجهول قد يجمد أو يتغير بالترقية     |
| طريقة التمويل            | إذن بالمبلغ الدقيق ثم fund تسحب بنفسها، مع قياس الرصيد قبل وبعد      | العقد لا يرى الرموز المرسلة إليه، والإذن غير المحدود خطر على العميل             |
| تحويل الرموز في الكود    | مكتبة SafeERC20                                                      | رموز لا تعيد قيمة أو تعيد false بدل الفشل                                       |
| المبالغ في نظام المنصة   | أعداد صحيحة مع decimals لكل رمز                                      | رمزنا بست خانات وETH بثماني عشرة                                                |
| شهادة الإنجاز            | ERC-721 لا ينقل، بإصدار آمن \_safeMint، وهاش ملف الشهادة على السلسلة | الرابط وحده تغير بمعاملة واحدة، والإصدار لعقد لا يستقبل فشل بدل أن تعلق الشهادة |
| شارات بأنواع وأعداد      | ERC-1155                                                             | عقد واحد لكل الأنواع، وتحويل بالدفعات، ولا تعلق الرموز في العقود                |

وقبل أن يقبل أي عقد تكتبه رمزاً لم تكتبه أنت، تحقق من التالي:

1. قرأت كود الرمز المنشور فعلاً على عنوانه، وليس كوداً منشوراً في مستودع يقال إنه نفسه، ويفضل أن يكون الكود المصدري متحققاً منه على مستكشف الكتل.
2. تعرف من يستطيع أن ينشئ وحدات جديدة، ومن يستطيع أن يجمد عنواناً أو يوقف التحويلات، وهل الرمز قابل للترقية ومن يملك مفتاحها.
3. تعرف قيمة `decimals`، وهل يقتطع الرمز رسوماً أو يتغير الرصيد دون تحويل.
4. عقدك يقيس ما وصله فعلاً، ويستخدم `SafeERC20`، ولا يقبل إلا الرموز التي اختبرها.
5. لكل رمز قد يصل إلى عقدك بالخطأ طريق خروج مقيد ومحروس، أو قرار مكتوب أنه سوف يبقى عالقاً.
6. واجهة المنصة تطلب إذناً بالمبلغ المطلوب فقط، وتشرح للمستخدم ما يوقعه، ولا تطلب الإذن غير المحدود.
7. بيانات الرموز غير القابلة للاستبدال لها هاش على السلسلة أو مكان لا يتغير، ولا توجد فيها بيانات شخصية.

## تدريب عملي

1. عدل الدالة `fund` في `TokenEscrow` حتى تقرأ رصيد العقد قبل `safeTransferFrom` وبعده، وترفض التمويل إذا كان الفرق أقل من `amount`، ثم أعد تجربة الرمز الذي يقتطع رسوماً وتأكد أن التمويل نفسه يفشل بدل أن يعلق المال عند الصرف.
2. أضف إلى `JobCertificate` متغيراً يحفظ لكل شهادة هاش ملفها، وتعدل `issue` لتأخذه، ثم جرب أن تغير `baseURI` وتأكد أن الهاش لم يتغير.

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

```bash
docker rm -f lab-tok
rm -rf lab/tokens/lib lab/tokens/out lab/tokens/cache lab/tokens/broadcast lab/token-address.txt lab/escrow-address.txt
```

## الخلاصة

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

- الرمز ليس عملة في الشبكة، وإنما جدول أرصدة داخل عقد، وقد قرأنا رصيد العميل من خانة التخزين مباشرة، وكل ما يعد به الرمز يعتمد على كود عقده ومن يملك صلاحياته.
- معيار ERC-20 يحدد الواجهة فقط، والأرصدة أعداد صحيحة، و`decimals` للعرض، والتحويل كلف 52119 وحدة غاز مقابل 21000 لتحويل ETH.
- العقد لا يرى الرموز المرسلة إليه، لذلك يمول عقد الضمان نفسه بالسحب transferFrom بعد إذن approve، والإذن غير المحدود يعطي غيرك حق سحب رصيدك كله.
- رأينا رموزاً تعلق إلى الأبد في عقد لا يعرف أنها وصلت، ورمزاً يقتطع رسوماً جعل الصرف يفشل دائماً، والحل أن تقبل رموزاً تعرفها وتقيس ما وصلك.
- ERC-721 يحفظ مالكاً لكل رقم، والتحويل الآمن يبلغ المستقبل فلا تعلق الرموز، والصورة والبيانات تعيش غالباً خارج السلسلة ويمكن أن تتغير، كما غيرنا رابط كل الشهادات بمعاملة واحدة.
- ERC-1155 يجمع الأنواع في عقد واحد، ويناسب الشارات بأنواع وأعداد.

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

[![](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/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/stablecoins-cover-cover.webp)دليلالعملات المستقرة Stablecoins: كيف تثبت ومتى تنهار؟ثلاث طرق لتثبيت القيمة عند دولار، نرى التصفية بأرقامها، ونحاكي دوامة الموت التي أسقطت UST.](https://arabroot.io/articles/%D8%A7%D9%84%D8%B9%D9%85%D9%84%D8%A7%D8%AA-%D8%A7%D9%84%D9%85%D8%B3%D8%AA%D9%82%D8%B1%D8%A9-stablecoins-%D9%83%D9%8A%D9%81-%D8%AA%D8%AB%D8%A8%D8%AA-%D9%88%D9%85%D8%AA%D9%89-%D8%AA%D9%86%D9%87%D8%A7%D8%B1/)[![](https://arabroot.io/content/images/2026/10/crypto-scams-cover-cover.webp)دليلالاحتيال في العملات الرقمية: كيف تتعرف عليه وتتجنبه؟أنماط الاحتيال وعلاماتها من تحذيرات FTC، مع تجربتين نرى فيهما محفظة تفرغ وعنواناً يخدع.](https://arabroot.io/articles/%D8%A7%D9%84%D8%A7%D8%AD%D8%AA%D9%8A%D8%A7%D9%84-%D9%81%D9%8A-%D8%A7%D9%84%D8%B9%D9%85%D9%84%D8%A7%D8%AA-%D8%A7%D9%84%D8%B1%D9%82%D9%85%D9%8A%D8%A9-%D9%83%D9%8A%D9%81-%D8%AA%D8%AA%D8%B9%D8%B1%D9%81-%D8%B9%D9%84%D9%8A%D9%87-%D9%88%D8%AA%D8%AA%D8%AC%D9%86%D8%A8%D9%87/)[![](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/)

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

- أكتوبر 2026: كتابة المقال، وتشغيل التجارب على Docker Engine 29.8.0 مع Foundry v1.8.6 وSolc 0.8.36 ومكتبة OpenZeppelin Contracts v5.7.0، على شبكة Anvil محلية برموز تجريبية ليس لها قيمة.