يا هلا يا فنان! طالما إنك واصل هنا وبتقرأ الكلام ده، يبقى يا إما مهووس بـ smart contract security، يا إما شغال حالياً على معماريّة بروتوكول DeFi جديد، وركبك بتخبط في بعضها من فكرة إن كل تعبك يطير في الهواء بكرة بسبب ثغرة تافهة.
بص يا صاحبي، أنا دايس في السكة دي من زمان. بدأت من أيام الـ hackathons لما كنا بنقعد نـ build إكسبلويتات على السريع وإحنا بناكل بيتزا وبنشرب مشروبات طاقة، لحد ما وصلت لكرسي الـ CTO في منصة كريبتو كبيرة. وعارف هقولك إيه؟ أغلب الـ hacks الثقيلة والخراب اللي شفته وحققت فيه (وفي جزء منه لحقته وعطلته وأنا بعمل audit)، مش بتحصل عشان التشفير باظ، ولا عشان الـ Solidity compiler اتهبل فجأة. لأ، بتحصل بسبب فجوة قاتلة وفهم سطحي لطريقة تعامل معايير الـ ERC مع بعضها وقت التداخل.
إحنا اتعودنا نعتبر الـ standards دي كأنها قانون وضمان للأمان، بس الحقيقة إن الشيطان دايماً مستخبي في تفاصيل الـ implementation والآثار الجانبية المخفية. تعال نقعد كده ونفصص الأماكن اللي المهندسين بيلبسوا فيها في الحيط، وعشان تعرف تـ design السيستم بتاعك وتنام وأنت حاطط في بطنك بطيخة صيفي.
1. الخطر المستخبي في ERC-20: الكلاسيكيات القاتلة
ممكن تفتكر إن الـ ERC-20 ده اتهرس بحث وفهم، وإيه اللي ممكن يبوظ فيه؟ الحقيقة كل حاجة ممكن تبوظ لو عملت دمج لـ tokens غريبة في البروتوكول بتاعك من غير ما تفرملها بـ validation حديدي.
معضلة غياب القيمة المرتجعة (The No-Return Dilemma)
حسب الـ specification الأساسية، المفروض وظائف الـ transfer والـ transferFrom يرجعوا قيمة bool. بس في أرض الواقع، عندك كمية توكنز قديمة وثقيلة في السوق (تحية خاصة لـ USDT و BNB في بعض عقودهم القديمة) مبيعملوش كده أصلاً، ومبيرجعوش أي حاجة لو العملية نجحت.
لو العقد بتاعك مستني قيمة bool من خلال الـ standard interface العادي:
// أوعى تعمل كده لو شغال مع توكنز عشوائية!
IERC20(token).transferFrom(msg.sender, address(this), amount);فأول ما السيستم يلمس الـ USDT، الـ transaction هتروح في داهية وتعمل (revert)، لأن الـ EVM هتقعد تدور على الـ return value في الـ stack ومش هتلاقي حاجة. أو الأسوأ من كده، لو أنت مش بتشيك على النتيجة أصلاً (يعني كاتب token.transfer(...) حاف بدل require(token.transfer(...)))، ففي توكنز لما بيحصل معاها error بترجع false بدل ما تعمل revert، والعقد بتاعك هيكمل عادي جداً كأن مفيش أي مشكلة. النتيجة؟ اليوزر قدر يطبع لنفسه رصيد ووهمي من الهوا.
الحل: انسى تماماً حاجة اسمها استدعاء مباشر لـ transfer و transferFrom. استخدم مكتبة SafeERC20 من OpenZeppelin ومعاها الـ methods الخاصة بها زي safeTransfer / safeTransferFrom. دي تحت الغطاء بتشيك على الـ low-level return وبتظبط العقود البايظة دي وتتعامل معاها صح.
Weird ERC-20 Tokens: لما المعيار يقلب مقلب
دي جدول-شفرة سريع لتوكنز بتتصرف بطرق برة الصندوق ومختلفة عن اللي مكتوب في الكتب. أي مهندس عقود ذكية ملزم يزرع التفاصيل دي في منطق الـ liquidity pools بتاعته.
| نوع التوكن (Weird ERC-20) | فين المقلب؟ | إيه خطورته على الـ Architecture؟ |
|---|---|---|
| Deflationary / Fee-on-Transfer (مثل STA, PAXG) | بيخصموا عمولة ورسوم فوراً وقت التحويل. | أنت بتبقى فاكر إن العقد استلم 100 توكن، بس اللي دخل الحساب فعلياً 99. الحسبة الداخلية للـ liquidity بتضرب وبيبقى عندك عجز في السيولة. |
| Upgradable Proxies (مثل USDC, USDT) | المنطق والـ logic بتاع التوكن ممكن يتغير في أي لحظة من الـ admins. | حوار الـ Blacklists (القوائم السوداء). لو عنوان العقد بتاعك اتحظر، كل السيولة والـ liquidity اللي جواه هتتحبس للأبد. |
| Rebasing Tokens (مثل AMPL) | الرصيد في المحافظ بيتغير بشكل ديناميكي (الـ supply بيلعب عشان يثبت السعر). | رصيد العقد بتاعك ممكن يقل أو يزيد لوحده فجأة، من غير ما يتنفذ أي استدعاء لوظائف التحويل. |
2. ERC-721 و ERC-1155: فخ الـ onERC721Received والـ Reentrancy
أوه، ده بقا موضوعي "المفضل". ياما منصات NFT-marketplaces وبروتوكولات lending اتصفت وراحت في داهية بسبب وظائف التحويل اللي بيسموها آمنة!
لما بتستدعي safeTransferFrom في الـ ERC-721 أو ERC-1155، عقد التوكن بيروح يشوف هل المستلم ده عبارة عن smart contract ولا لأ. لو طلع عقد، بيقوم مشغل عند المستلم hook اسمه onERC721Received أو onERC1155Received.
ليه اللفة دي؟ عشان يتأكد إن العقد المستلم بيعرف يتعامل مع الـ NFTs والتوكنز ومستحيل تتحبس جواه.
فين الخازوق؟ الـ hook ده بيمسك التحكم والـ control ويسلمه لكود خارجي مش مضمون في نص الـ transaction بتاعتك، وقبل ما تلحق تعمل update للـ internal state بتاع العقد بتاعك!
كود لعملية Minting / لـ Marketplace مكشوف وثغرة واضحة
بص على الكود ده. أنا كتبته مخصوص بالطريقة دي عشان أوريك الغلطة المعمارية الشهيرة - كسر الـ pattern بتاع Checks-Effects-Interactions.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
contract VulnerableNFCLending {
// بنخزن بيانات الرهونات: اليوزر => ID بتاع التوكن => هل الرهن فعال؟
mapping(address => mapping(uint256 => bool)) public hasCollateral;
IERC721 public nftToken;
constructor(address _nft) {
nftToken = IERC721(_nft);
}
// اليوزر عاوز ياخد قرض بضمان الـ NFT
function depositCollateral(uint256 tokenId) external {
// 1. Interactions: بنحول الـ NFT للعقد
// الـ safeTransferFrom بتشغل الـ hook onERC721Received على عقد المستلم؟
// ثانية، المستلم هنا هو إحنا. بس لو العقد بيستدعي safeMint...
// خلينا نعدل الـ context لسيناريو بنرجع فيه الـ NFT أو لما المهاجم يلقط التحكم في النص.
// إعادة صياغة السيناريو: العقد بيرجع الـ NFT لورا (مثلاً عند سحب الرهن)
// أو ده عقد minting بيحول الأول وبعدين بيعمل update للـ state.
}
}تعال أوريك مثال أنظف وأوضح على عقد minting مكشوف، الفكرة هنا هتبان علطول. نفترض عندنا عقد بيسمح بعمل mint لـ NFT واحدة بس لكل شخص ببلاش (Free Mint).
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
contract VulnerableMint is ERC721 {
mapping(address => bool) public hasMinted;
uint256 public currentTokenId;
constructor() ERC721("DangerNFT", "DNFT") {}
function freeMint() external {
// Checks
require(!hasMinted[msg.sender], "Brother, you already have one!");
// Interactions (جوه الـ _safeMint مستخبي استدعاء لعقد خارجي!)
_safeMint(msg.sender, currentTokenId);
currentTokenId++;
// Effects (تحديث الـ state بيحصل متأخر جداً!)
hasMinted[msg.sender] = true;
}
}وده كود المهاجم (Attacker). هو كل اللي بيعمله إنه بيلقط الـ hook ده، ويشوف إن الـ hasMinted في العقد الأساسي لسه قيمتها false، فيقوم مستدعي الـ freeMint تاني وتالت ورابع، ويفضل شغال ويدخل ورا بعضه لحد ما يخلص الـ limit كله أو الـ gas يخلص عليه.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IVulnerableMint {
function freeMint() external;
}
contract Attacker {
IVulnerableMint public target;
uint256 count;
constructor(address _target) {
target = IVulnerableMint(_target);
}
function attack() external {
target.freeMint();
}
// الـ hook المنحوس اللي بيتحكم فيه الـ standard ERC-721
function onERC721Received(
address,
address,
uint256,
bytes calldata
) external returns (bytes4) {
if (count < 5) {
count++;
// دخول متكرر Reentrancy! الـ internal state بتاع الضحية لسه متحدثش
target.freeMint();
}
return this.onERC721Received.selector;
}
}يا الله، ياما شفت اللقطة دي في جلسات الـ audit. المطورين بيبقوا فاكرين: "عادي يا عم، ده مجرد نقل NFT، إحنا مش بنحول ETH حقيقي، إيه اللي ممكن يحصل يعني؟". اللي بيحصل إن عقدك بيتصفى في ثواني.
قاعدة المهندس الذهبية: حدث الـ state أولاً (hasMinted[msg.sender] = true;)، وبعدها بس ابدأ استدعي أي methods للـ mint أو الـ transfer. ودايماً، أرجوك دايماً، ارزع الـ modifier nonReentrant من OpenZeppelin على أي وظيفة بتتعامل مع تحويلات الـ NFT.
نكمل مشوارنا. بعد ما فككنا سالفة الـ Reentrancy اللي تصير عن طريق الهوكس (hooks)، خلنا نحفر في مشكلة أجدد وأكثر خبثاً، وقليل من المطورين يفكرون فيها وقت تصميم الهيكل المعماري (architecture)—لين تقع الفاس بالراس وتطير الفندات.
3. معيار ERC-2612 (Permit): الموافقات الوهمية (Ghost Approvals) وهجمات الـ Front-running
معيار ERC-2612 جاب حل جبار ريح مجتمع الـ Web3، وهو فنكشن الـ permit. هالفنكشن تخلي المستخدم يوقع على رسالة أوفلاين (EIP-712) عشان يعطي صلاحية (allowance) بصرف التوكنات، ويرمي تكلفة الغاز (gas fees) على ريليير (relayer) أو على البروتوكول نفسه. الـ UX هنا طار للسما: بدال ما اليوزر يعلق في ترانزاكشنين غثيثة (approve + transferFrom)، صار يخلص موضوعه بلمسة وحدة وبترانزاكشن واحد.
بس المشكلة وين؟ إن مهندسي العقود الذكية وايد منهم ينسون كيف هالتوقيع يشتغل تحت الكواليس، ويطيحون في أخطاء منطقية قاتلة.
فرنت رانينغ التوقيع (Signature Front-running)
تخيل عندك فنكشن إيداع (deposit) كلاسيكية في سمارت كونترات تستخدم الـ permit:
function depositWithPermit(
uint256 amount,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external {
// أول شيء، ننفذ الـ permit باستخدام التوقيع اللي مرره اليوزر
IERC20Permit(token).permit(msg.sender, address(this), amount, deadline, v, r, s);
// بعدين نسحب التوكنات
IERC20(token).transferFrom(msg.sender, address(this), amount);
// ونوزع الحصص الداخلية أو نقاط المسبح (pool shares)
_mintShares(msg.sender, amount);
}وين الثغرة هنا؟ أي بوت MEV جالس يراقب الميمبول (mempool) العام بيشوف هالترانزاكشن المعلق. البوت ببساطة بيسحب التوقيع الصالح (v, r, s) والبارامترات من الترانزاكشن حقك، ويسوي ترانزاكشن خاص فيه يستدعي فنكشن token.permit(...) مباشرة على عقد التوكن، ويرفع سعر الغاز (Gas Price) أعلى من اليوزر عشان يسوي له فرنت ران (front-run).
ترانزاكشن البوت بيمر أول واحد، والتوقيع بيتفعل بنجاح والـ allowance بيتحط. بعدها مباشرة يوصل ترانزاكشن اليوزر المسكين للشبكة. وبما إن التوقيع قد استُخدم وخلاص، الـ nonce حق اليوزر في عقد التوكن زاد! استدعاء الـ permit داخل فنكشن depositWithPermit بيعطي revert على طول لأن التوقيع صار منتهي الصلاحية وغير صالح.
النتيجة؟ الترانزاكشن حق اليوزر بيفشل، وبيخسر الغاز على الفاضي، والـ UX حقه بيخترب. ولو كان هذا الإيداع حرج وجاي كـ margin top-up عشان يحمي بوزيشن لونغ (long position) من التصفية، البوزيشن حقه بيتصفى (liquidated) بارد مبرد بسبب هالتأخير.
كيف تحمي الهيكل المعماري حقك؟
الحل إنك تحط استدعاء الـ permit داخل بلوك try/catch. لو التوقيع قد انرمى في الشبكة من قبل بوت فرنت رانر، عقد التوكن أصلاً بيكون عنده الـ allowance المطلوب جاهز. عقدك المفروض يطوف خطأ التوقيع المكرر ويكمل طريقه بدون مشاكل عشان ينفذ الـ transferFrom.
try IERC20Permit(token).permit(msg.sender, address(this), amount, deadline, v, r, s) {}
catch {
// إذا صار ريفيرت—فالاحتمال الأكبر إن التوقيع حاشه فرنت ران.
// نتأكد إذا الـ allowance الحالي كافي لإتمام العملية أو لا.
require(IERC20(token).allowance(msg.sender, address(this)) >= amount, "Permit failed and allowance insufficient");
}مشكلة "البيرمت الوهمي" (Phantom Permit)
هالجزئية هي الوجع الحقيقي اللي يعرفونه أهل الصنعة والإنسايدرز، وقليل جداً تلقى حد كاتب عنه. شو بيصير لو البروتوكول حقك (DeFi protocol) يدعم توكنات عشوائية، وجاء يوزر يبي يستدعي depositWithPermit لتوكن أصلاً ما يدعم معيار ERC-2612؟
أكيد بتقول في خاطرك: "عادي، الترانزاكشن بيفشل لأن فنكشن الـ permit مب موجودة هناك أساساً". للاسف، اللعبة مب دايماً جي!
لو عقد التوكن فيه فنكشن عامة مثل fallback() أو receive() وما تسوي ريفيرت (revert) إذا حد استدعى سيليكتور (selector) غير معروف—وهالشيء دارج في بعض عقود البروكسي (proxy) أو التوكنات القديمة مثل نسخ الـ WETH الجديمة—فإن استدعاء الـ permit بيمر وكأن كل شيء تمام وبيرجع (success = true)، بس في الحقيقة ما في أي موافقة أو allowance اعتمدت.
بعدها العقد حقك بيكمل عشان ينفذ الـ transferFrom، وهنا عاد بيفشل الترانزاكشن إلا لو كان فيه approve جديم مغبر في العقد. بس لو دمجت هالثغرة مع منطق برمجية يتأكد من الـ allowance بطرق ثانية، البروتوكول حقك بياكل ريكت (rekt) تاريخي. عشان جي دايماً تأكد إن التوكن المستهدف يدعم واجهة IERC20Permit فعلياً عن طريق معيار ERC-165، أو خلك حاسم واستخدم وايت لست (whitelist) صارمة للتوكنات.
4. معيار ERC-3156 (Flash Loans): خطر تلاعب السيولة داخل نفس الترانزاكشن
القروض السريعة (Flash loans) أداة قوية بشكل بوجلي، بس تكسر كل فرضيات التوقيت اللي تعودوا عليها مهندسي البرمجيات التقليديين. داخل ترانزاكشن واحد ذري (atomic transaction)، يقدر المهاجم يقترض ملايين الدولارات، ويلعب في الحسابات والـ states، ويرجع الفندات في نفس اللحظة.
أكبر خطأ معماري كارثي يصير هنا، هو استخدام فنكشن balanceOf(address(this)) عشان تحسب سعر حصص المسبح (pool shares) أو تقييم سعر الأصل.
// خطأ معماري كارثي تضيع فيه فندات
function getSharePrice() public view returns (uint256) {
// سعر الحصة يعتمد مباشرة على الرصيد الحالي للتوكنات في العقد
return token.balanceOf(address(this)) / totalShares;
}إذا عقدك يسمح بأخذ Flash Loan من نفس هالتوكن، ففي اللحظة اللي يسحب فيها المقترض الفندات، رصيد العقد بينهار ل الصفر تقريباً. وإذا في نفس هالميلي ثانية (داخل الـ callback حق onFlashLoan)، البروتوكول حقك سمح بعمليات ثانية تتنفذ—مثل التصفيات (liquidations) أو حساب المكافآت—فسعر الحصة المحسوب بيكون مضروب ومشوه تماماً.
الهكر يسحب فلاش لون -> رصيد المسبح يصفر -> سعر الحصة يطيح في القاع -> الهكر بمحفظة ثانية يخم الحصص برخص التراب -> يرجع الفلاش لون -> رصيد المسبح يرجع طبيعي -> الهكر يدب الحصص بالسعر الأصلي العالي. وجي يكون المسبح تفضى بالكامل.
القاعدة الذهبية: لا تعتمد أبداً على balanceOf(address(this)) في الحسابات الرياضية أو الاقتصادية الحساسة، إذا كان ه الرصيد ممكن يتلاعب فيه بشكل مؤقت بدون ما تتغير الحالة المنطقية العميقة للسيستم. بدال هذا، استخدم حسابات داخلية (internal accounting) عن طريق state variable مثل uint256 internalReserve، وما يتحدث إلا في حالات الإيداع والسحب الرسمية والمحكومة بالكامل.
زين، خلنا ندخل في الأشياء اللي كـ شخص مسؤول عن أمن البنية التحتية للمنصات، تخليني أتحسس راسي من الخوف وعلامات الاستفهام. بنتكلم عن المعايير (standards) الجديدة والفرش، اللي للحين الـ developers ما طاحوا في حفرها ولا جربوا مشاكلها بشكل كافي.
5. ERC-4337 (Account Abstraction): فخاخ على مستوى الـ Batch Transactions والـ Paymaster
ميزة الـ Account Abstraction قوية وفنانة، ما فيها كلام. الحين قاعدين نتحول من الـ EOAs (المحافظ العادية) إلى الـ Smart Contracts كـ محافظ للمستخدمين. يعني ودّع الـ seed phrases، وصار عندك ميزة الـ social recovery وتقدر تدفع الـ gas بالـ stablecoins عن طريق الـ Paymasters.
لكن من منظور الـ protocol architect اللي يسوي دمج (integration) مع الـ ERC-4337، هنا تفتح لك طاقة جهنم من ثغرات الـ attack vectors الغريبة والدقيقة جداً.
ثغرة التوقيع (Signature) في الـ validateUserOp
في معيار ERC-4337، النقطة الأساسية في توثيق المحفظة (custom validation) هي الـ method اللي اسمها validateUserOp. هذي لازم تتأكد من توقيع المعاملة (transaction signature) وترجع status معين.
// مثال مبسط لمنطق الـ validation في الـ smart wallet
function validateUserOp(
UserOperation calldata userOp,
bytes32 userOpHash,
uint256 missingAccountFunds
) external returns (uint256 validationData) {
// خطأ كارثي: هل قاعدين نثق بالاتصال لو جـا من ANY address؟
// لا، الـ Bundler يستدعي هذا الشيء عن طريق الـ EntryPoint. بس لو نسينا الـ check...
require(msg.sender == entryPoint, "Only EntryPoint can trigger validation");
// الـ validation الخاص بالـ signature
if (_verifySignature(userOp, userOpHash)) {
// نرجع 0 إذا الـ validation تم بنجاح
return 0;
}
// نرجع SIG_VALIDATION_FAILED (عادةً 1) في حال وجود خطأ
return 1;
}لحظة، ركز معاي هنا وشوف اللعبة وين؟ حسب الـ specification مال ERC-4337، إذا الـ signature validation فشل، الـ function المفروض ما تسوي revert أبداً. لازم ترجع قيمة مدمجة خاصة (error constant)، عشان الـ EntryPoint (العقد المايسترو) يفهم إن الـ transaction غير صالحة، وما يخصم الـ gas من المحفظة، بل يكنسل العملية مباشرة على مستوى الـ Bundler.
لو أنت كـ architect، وبسبب العادة، كتبت هناك require(isValid, "Invalid signature");، فـ أنت يبت العيد. لما يتم إرسال مجموعة معاملات دفعة واحدة (batch)، معاملة وحدة بس تفشل وتسوي hard revert بتخرب الـ batch كامل على الـ Bundler. والنتيجة؟ محفظتك أو الـ paymaster مالك بياكل بلوك (ban) من الـ bundlers، ومستخدمينك ما بيقدرون يمررون أي معاملة على الشبكة. منطق الـ validation لازم يكون ذري (atomic) ويمشي بحذافيره مع رياضيات الـ return values الخاصة بـ ERC-4337، مش على الـ patterns الكلاسيكية مالت Solidity.
الهجوم على الـ Paymaster (تصفية الـ Gas)
إذا الـ DeFi protocol مالك شغال كـ Paymaster (يعني مثلاً تدعم الـ gas حق مستخدمينك عشان يتداولون بدون رسوم)، أنت مجبر إنك تعزل مرحلة الـ validation عن العالم الخارجي تماماً وبشكل قاطع.
في عقد الـ Paymaster، عندك method اسمها validatePaymasterUserOp. داخل هذي الـ method، **ممنوع منعاً باتاً** تستخدم أي dynamic state ممكن تتغير بين وقت الـ simulation اللي يسويه الـ bundler ووقت إدراج المعاملة في الـ block. على سبيل المثال، ما تقدر تستدعي الـ price oracles (مثل Chainlink) داخل الـ paymaster validation عشان تحسب كم توكن بتخصم من اليوزر مقابل الـ gas.
**ليش؟** الـ attacker يقدر يرسل transaction، ووقت الـ simulation الـ oracle بيعطي سعر مضبوط (والـ validation بيمشي)، بس قبل ما تدخل المعاملة في الـ block مباشرة، الهكر يتلاعب بسعر الـ oracle نفسه (عن طريق flash loan أو trade سريع). هنا الـ on-chain validation بيبدأ يفشل، بس الـ bundler يكون استلم المعاملة واشتغل عليها وصرف الـ gas خلاص. الرصيد بينسحب من الـ paymaster مالك واليوزر ما بيدفع ولا فلس. بـ هالطريقة، رصيد الـ gas عندك بيصفر في غضون ساعات قليلة.
6. ERC-4626 (Tokenized Vaults): الـ Front-running على أول إيداع (Inflation Attack)
معيار ERC-4626 هو الستاندرد الذهبي حق الـ tokenized vaults (الـ staking، الـ yield pools، والـ lending). ووحد الـ methods مثل deposit، mint، withdraw، و redeem. هذا الشيء عبقري، لأن الحين أي yield aggregator مثل Yearn يقدر يدمج أي pool جديد خلال 5 دقائق بس.
لكن في التصميم الرياضي للمعيار نفسه، فيه قنبلة موقوتة معروفة بـ اسم **Inflation Attack** (هجوم تضخيم الأصول). هذا الهجوم يضرب الـ pools في اللحظة اللي تندمج فيها في الشبكة لأول مرة ويكون رصيدها صفر.
طريقة عمل الهجوم
معادلة حساب كمية الحصص (shares) اللي يحصل عليها المستخدم لما يودع أصول (assets) عادةً تكون جي:
$$\text{shares} = \frac{\text{assets} \times \text{totalShares}}{\text{totalAssets}}$$
إذا الـ pool فاضي (totalShares == 0)، فـ الطبيعي إن الـ shares == assets. يعني النسبة 1 إلى 1.
الحين شوف خفة يد الهكر وين:
- مستخدم نيتة صافية يطرش معاملة
depositبقيمة 1000 USDC لـ pool جديد وفاضي تماماً من نوع ERC-4626. - الهكر يشوف المعاملة معلقة في الـ mempool ويسوي لها front-run (يرفع الـ gas). يودع في الـ pool بس 1 wei من الـ USDC. الـ pool بيميلنت له بالضبط 1 wei من الـ shares. الحين الوضع صار:
totalShares = 1، وtotalAssets = 1. - بعدها، وفي نفس المعاملة (atomic transaction)، الهكر يسوي تحويل مباشر (عن طريق
transferعادي مال ERC20، مش عبر الـdepositfunction) لمبلغ ضخم — مثلاً 10,000 USDC — لـ عنوان عقد الـ pool مباشرة. - شو استوى في رياضيات الـ pool؟ الـ
totalSharesللحين 1، بس الـtotalAssetsالحين قفز لـ 10,001 USDC (بسبب التحويل المباشر رصيد العقد ارتفع، بس بدون ما تندمج shares جديدة). الحين سعر الـ share الواحد صار فلكي. أخيراً، تتنفذ معاملة اليوزر المسكين الـ 1000 USDC. العقد يحسب الـ shares بالمعادلة:
$$\text{shares} = \frac{1000 \times 1}{10\,001} = 0$$
بسبب ميزة التقريب لأسفل (rounding down) في الـ integer division داخل Solidity، المستخدم يحصل على 0 shares بالضبط! لكن الـ 1000 USDC ماله تدخل رصيد الـ pool بكل نجاح.
- الهكر الحين بس يسوي
withdrawلحصته الوحيدة (1 wei shares) ويقش كل اللي في الـ pool: الـ 10,000 USDC ماله، والـ 1 wei الأساسي، والـ 1000 USDC مالت المستخدم اللي طار عليه الحلال.
الحل المعماري (Architectural Solution): تقدر تحمي نفسك من هذا الشيء بطريقتين. الأولى - عند إنشاء الـ pool، تسوي إجبار لـ ميتنق "السيولة الوهمية" (dead shares) وترسلها للـ zero address (تقفل أول 1000 wei من الـ shares هناك، نفس الطريقة المتبعة في Uniswap V2). الثانية - تستخدم مكتبات OpenZeppelin المحدثة، اللي فيها حماية جاهزة عن طريق الـ virtual offsets (الـ virtual assets والـ virtual shares)، وهذي تمنع مقام الكسر (denominator) إنه يتحول لـ صفر أو واحد خلال هالنوع من التلاعبات.
اسمع، خلنا نرفع الليفل شوي. تكلمنا عن التوكنز، الـ NFTs، الـ permits، والـ vaults. بس كيف تربط هالأشياء كلها في أركيتكشر وحدة بدون ما تجيب العيد وتضيع وقت الانتجريشن؟
لما تصمم سيستم ضخم، مثل "Yield Aggregator" أو "Cross-chain Bridge"، بتضطر تتعامل مع كل هالمعايير في نفس الوقت. وهنا تطلع لك "synergistic vulnerabilities"؛ يعني فيتشرز (features) كل وحدة لحالها تكون آمنة، بس لما تجمعهم مع بعض يفتحون ثغرة تخسرك السيستم كامل.
7. مصفوفة المخاطر الأركيتكشورية (Architectural Risk Matrix)
عشان تكون الصورة واضحة قدامك، سويت لك هالجدول. اعتبره "Checklist" لأي "Architecture Review" قادم. احفظه في Notion أو اطبعه عندك وخلّه مرجع.
| معيار ERC | الخطر الخفي الأكبر | كيف تظهر في الكود (Logic) | كيف تعالجها في الديزاين؟ |
|---|---|---|---|
| ERC-20 | عدم وجود Return Value / تحويل غير قياسي | توقف المعاملة (Revert) أو تجاهل الخطأ بصمت | استخدم دائمًا SafeERC20 (OpenZeppelin). |
| ERC-20 (Weird) | Fee-on-Transfer / تغير في الرصيد (Rebase) | عدم تطابق بين سجلاتك الداخلية والرصيد الحقيقي في العقد | احسب الفرق balanceAfter - balanceBefore بدلاً من الوثوق بالـ amount المرسل. |
| ERC-721 / 1155 | السيطرة على التدفق عبر الـ hooks مثل onERC...Received | Reentrancy قبل تحديث الحالة الداخلية (Internal State) | طبق قاعدة Checks-Effects-Interactions بدقة + استخدم nonReentrant. |
| ERC-2612 | الـ Frontrunning للتواقيع في الـ Mempool | تعطيل الخدمة (DoS) للمستخدم الحقيقي | غلف نداءات permit داخل بلوك try/catch. |
| ERC-3156 | تفريغ السيولة مؤقتًا (Flash Loan) | التلاعب بأسعار الـ Spot المعتمدة على balanceOf | استخدم متغيرات احتياطي داخلية (internal reserves) بدل الاعتماد على الرصيد المباشر. |
| ERC-4337 | الـ revert القوي أثناء التحقق الجماعي | حظر العقد أو المحفظة من قبل الـ Bundlers | رجع "Magic Error Constants" بدل ما تكسر المعاملة بـ require. |
| ERC-4626 | Inflation Attack (هجوم الإيداع الأول) | تقريب الحصص (Shares) للصفر وسرقة أموال المودع الأول | اعمل Mint لـ "حصص ميتة" للـ address(0) عند التهيئة أو استخدم Virtual Offsets. |
8. خواطر وقواعد ذهبية لأركيتكشر آمنة
شوف، بعد 3 سنوات كـ CTO، تعلمت شغلة واحدة: أكثر كود آمن هو الكود اللي ما كتبته. كل ما زاد تعقيد الأركيتكشر عندك، زادت الروابط الخفية، وزادت فرصة أن واحد "عبقري" في هاكاثون يلقى ثغرة ما خطرت على بالك حتى وأنت تشرب قهوتك الصباحية.
لو لازم أعطيك 3 قواعد بس تحمي مشروعك من العناوين الكارثية في "Rekt News"، فهي كالتالي:
- لا تثق أبدًا بالعقود الخارجية. حتى لو كان التوكن هو الأكثر شهرة في العالم. بكرة الأدمنز يغيرون البروكسي (Proxy)، يضيفون "Blacklist"، ويوقف سيستمك بالكامل. اكتب الكود دائمًا على أساس أن التوكن الخارجي هو أخطر وأكثر كيان غير متوقع في الشبكة.
- الـ State أولًا، ثم التحويلات. ماراح أمل من تكرار هالنقطة. هذي أساسيات تدرسها في أول يوم، بس الناس مصرّة تغلط وتحول التوكنز قبل ما تعدل الأرقام في الـ Mappings. أول شيء عدل الصلاحيات أو الأرصدة داخليًا، ثبتها في البلوكشين، وآخر خطوة سوي الـ
transfer،safeMint، أو الـcall. - افصل الرياضيات عن الرصيد الخارجي. رصيد عقدك في الـ EVM هو شيء علني وسهل التلاعب فيه. أي أحد يقدر يرسل لك ملايين الدولارات عبر Flash Loan أو يسوي "Selfdestruct" لعقد ويرمي Ether في عنوانك غصب. لو منطق توزيع المكافآت أو سعر الحصة يعتمد على اللي موجود في عقدك، فأنت خسران خسران. حساباتك الداخلية لازم تكون معزولة تمامًا مثل كبينة الطيار.
هذا باختصار، مرينا على أهم نقاط الألم في المعايير اللي ممكن تضيع مجهود أقوى فريق ديف، لو ما كان الأركيتكت ذكي وعرف يركب الـ "patch" أو الحل المناسب في مكانه الصح.