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

بقلم

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

العواقب مادية. فقد خسر Cetus CLMM مبلغ 223.0 مليون دولار على شبكة Sui في 2025-05-22 في حادثة مصنفة كخطأ حسابي يتعلق بالمحاسبة على الرموز والحصص. لذا فإن أمان العمليات الحسابية لا يقتصر فقط على منع الأخطاء الناتجة عن تجاوز حدود Solidity، بل يتعلق بضمان أن كل حصة يتم سكها تمثل حقًا قابلًا للدفاع عليه في الأصول.

ما هي أخطاء الحساب في Solidity وكيف تتم الهجمات؟

أخطاء الحساب في Solidity هي عيوب في الحسابات التي تحوّل الودائع، السحوبات، الأرصدة، الأسعار، الرسوم، أو السيولة إلى مطالبات اقتصادية. يستغل المهاجم عدم التطابق بين نموذج المحاسبة المقصود والصيغة المُنفَّذة، غالبًا عن طريق اختيار ترتيب المعاملات، أو كميات الأصول، أو حالات البركة التي تجعل التحويل الخاطئ في صالحه.

في Solidity الحديثة، يحدث الرجوع التلقائي عند تجاوز أو نقصان القيم الحسابية. هذا الحماية مفيدة لكنها لا تثبت صحة صيغة حسابية من الناحية الاقتصادية. إذ يمكن للحساب أن يبقى ضمن حدود uint256 بينما يُصدر العديد من حصص الخزنة، أو يفرض رهونات أقل من اللازم، أو يقرب السحب لصالح المستخدم.

هجوم حصص مفرط نموذجي يتبع هذا التسلسل:

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

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

ما أنماط Solidity التي تسبب أخطاء في محاسبة الرموز والحصص؟

أخطاء المحاسبة على الرموز والحصص عادة ما تنشأ من خلط الوحدات، أو قطع القيم الوسيطة، أو اعتبار سعر الحصة المتغير كقيمة ثابتة. الوظائف الأكثر عرضة للخطر تشمل deposit، mint، withdraw، redeem، حدود الاقتراض، تسوية الرسوم، حسابات السيولة، وتوزيع المكافآت.

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

shares = assets * 1e18 / assetScale;

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

shares = assets * totalShares / totalAssets;

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

الأسباب الشائعة تشمل:

  • ترميز ثابت لـ 1e18 لأصل يملك دقة عشرية مختلفة.
  • سك الحصص بمعدل ثابت واحد لواحد بعد تغير سعر الحصة.
  • القسمة قبل الضرب وفقدان الدقة المهمة بصمت.
  • تقريب الودائع أو السحوبات لأعلى، مما يخلق تسريبات تراكمية في القيمة.
  • استخدام unchecked حول قيم يتحكم فيها المستخدم أو عوامل التدريج.
  • افتراض أن أرصدة الرموز لا يمكن تغييرها خارج تدفقات الودائع والسحب المعتادة.
  • نسيان أن الرموز المفروضة عليها رسوم النقل، أو المعاد توازنها، أو المُنتجة للعائد، يمكن أن تبطل الحسابات المخزنة.

كيف تصلح أخطاء الحساب في حساب الحصص في Solidity؟

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

الخزنة الضعيفة أدناه تقبل رمزًا بأي عدد عشري محدد. تُصدر حصصًا معادلة بدقة ثابتة حتى بعد reportYield التي تزيد الأصول. يمكن للمودع الحصول على حصص مفرطة بعد تراكم العائد ثم استردادها مقابل تجمع أكبر.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface IERC20 {
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
    function transfer(address to, uint256 amount) external returns (bool);
}

contract VulnerableShareVault {
    IERC20 public immutable asset;
    uint8 public immutable assetDecimals;
    uint256 public totalAssets;
    uint256 public totalShares;
    mapping(address => uint256) public shares;

    constructor(address asset_, uint8 decimals_) {
        asset = IERC20(asset_);
        assetDecimals = decimals_;
    }

    function deposit(uint256 assets) external {
        require(asset.transferFrom(msg.sender, address(this), assets), "transfer failed");

        uint256 scale = 10 ** assetDecimals;
        // VULNERABLE: ignores the current price of existing shares.
        uint256 minted = (assets * 1e18) / scale;

        totalAssets += assets;
        totalShares += minted;
        shares[msg.sender] += minted;
    }

    function reportYield(uint256 assets) external {
        require(asset.transferFrom(msg.sender, address(this), assets), "transfer failed");
        totalAssets += assets;
    }

    function withdraw(uint256 shareAmount) external {
        uint256 assets = (shareAmount * totalAssets) / totalShares;
        shares[msg.sender] -= shareAmount;
        totalShares -= shareAmount;
        totalAssets -= assets;
        require(asset.transfer(msg.sender, assets), "transfer failed");
    }
}

النسخة المعدلة تحول وحدات الأصول الخام إلى وحدات محاسبة ذات 18 رقمًا عشريًا وتصدر الحصص بسعر الصرف الحالي. تستخدم Math.mulDiv الدقيقة بالكامل من OpenZeppelin، وتقرب كلاً من الودائع والسحوبات إلى الأسفل، وتحدد حدود القيم العشرية المدعومة.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

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

interface IERC20 {
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
    function transfer(address to, uint256 amount) external returns (bool);
}

contract FixedShareVault {
    IERC20 public immutable asset;
    uint256 public immutable assetScale;
    uint256 public totalAssets;
    uint256 public totalShares;
    mapping(address => uint256) public shares;

    constructor(address asset_, uint8 decimals_) {
        require(decimals_ <= 18, "unsupported decimals");
        asset = IERC20(asset_);
        assetScale = 10 ** decimals_;
    }

    function deposit(uint256 assets) external {
        require(assets > 0, "zero assets");
        require(asset.transferFrom(msg.sender, address(this), assets), "transfer failed");

        uint256 normalizedAssets = Math.mulDiv(assets, 1e18, assetScale);
        uint256 normalizedPool = Math.mulDiv(totalAssets, 1e18, assetScale);

        // FIX: price shares against the pre-deposit pool ratio.
        uint256 minted = totalShares == 0
            ? normalizedAssets
            : Math.mulDiv(normalizedAssets, totalShares, normalizedPool);

        require(minted > 0, "deposit too small");
        totalAssets += assets;
        totalShares += minted;
        shares[msg.sender] += minted;
    }

    function withdraw(uint256 shareAmount) external {
        uint256 assets = Math.mulDiv(shareAmount, totalAssets, totalShares);
        shares[msg.sender] -= shareAmount;
        totalShares -= shareAmount;
        totalAssets -= assets;
        require(asset.transfer(msg.sender, assets), "transfer failed");
    }
}

إذا كان بروتوكولك يُسعر الودائع، السحوبات، الضمانات، أو السيولة من خلال حسابات مخصصة، يمكن لخدمة تدقيق العقود الذكية والتقييم الأمني تتبع تحويلات الوحدات والثوابت الاقتصادية خلال دورة المعاملة كاملة.

كيف يمكن للمطورين التوقف عن ارتكاب أخطاء حسابية؟

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

التقنية ما يمنعه تكلفة الغاز أو التعقيد القيد
تطبيع الوحدة الصريح خلط وحدات الرموز الأولية بوحدات الحصة ذات 18 رقمًا عشريًا ضرب أو قسمة إضافية يتطلب سياسة عشرية موثقة
Math.mulDiv تجاوز الدقة أو الفقد أثناء a * b / c استهلاك غاز أكثر من العمليات البسيطة لا يتحقق من صحة الصيغة الاقتصادية
تقريب الودائع والسحوبات للأسفل الاحتفاظ بالحصص المفرطة أو الاسترداد المفرط الناتج عن التقريب قد يخلق أرصدة غبارية (Dust) الغبار يحتاج إلى سياسة استرداد
حد ترتيب سك الحصة الأدنى الودائع التي تقرب إلى حصص صفرية مقارنة واحدة يجب ألا يمنع المستخدمين الصغار المشروعين بلا سبب
تحديد حدود المدخلات والأرقام العشرية القوة المفرطة أو مدخلات المستخدم الشاذة عمليات التحقق الحدود يجب أن تتطابق مع الأصول المدعومة
اختبارات الثوابت التحولات التي تنتهك محاسبة التجمع جهد كتابة الاختبارات يجب أن تشمل الثوابت تسلسلات عدائية

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

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

ما الحوادث الحقيقية التي توضح تأثير الأخطاء الحسابية؟

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

البروتوكول التاريخ المبلغ المفقود الشبكة
Cetus CLMM 2025-05-22 $223.0M Sui
Compound V2 2021-09-29 $147.0M Ethereum
Uranium Finance 2021-04-28 $57.2M BSC
ValueDefi 2021-05-07 $11.0M BSC
KiiChain 2026-08-22 $9.7M KiiChain
Truebit 2026-01-08 $26.4M Ethereum
Notional V2 2026-09-04 $1.7M Ethereum
USM 2026-08-09 $136K Ethereum

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

يسجل DefiLlama 35 حادثة خطأ حسابي في العقود الذكية من 2020 حتى 2026، بخسائر مجمعة تبلغ 561.0 مليون دولار. بالنسبة لفرق البروتوكولات، يدعم هذا التاريخ مراجعة مسارات المحاسبة بنفس أولوية مراجعة التحكم في الوصول والتعامل مع المكالمات الخارجية.

كيف تكشف أخطاء الحساب قبل النشر؟

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

استخدم قائمة مراجعة مركزة:

  1. أدرج كل متغير من الأصول، الحصص، الأسعار، المعدلات، والرسوم مع وحدته وعدده العشري.
  2. تحقق من أن كل إيداع يستخدم سعر الصرف قبل الإيداع.
  3. تحقق من أن كل سحب يستخدم سعرًا يتوافق مع الحصص المحروقة.
  4. ابحث عن عمليات *، /، unchecked، والثوابت المقياسية المرصودة في مسارات المحاسبة.
  5. اختبر أصول ذات دقة عشرية مختلفة وودائع قرب الحدود الدنيا.
  6. أكد أن التقريب لا يصدر حصصًا غير مدعومة أو يحرر أصولًا غير مدعومة.
  7. نمذج تغيرات الأرصدة من إعادة التوازن، التبرعات، الرسوم، والعائد قبل كل فعل مستخدم.
  8. تحقق من أن إجمالي مطالبات المستخدمين لا يمكن أن يتجاوز الأصول المتاحة للسحب.

يمكن لـ Slither الإشارة إلى أنماط حسابية خطرة، كتل unchecked، والعمليات المشكوك في دقتها، لكنه لا يستطيع تحديد سلامة صيغة سعر الحصة اقتصاديًا. فازينغ Foundry أكثر فعالية عند دمجه مع تحقق من صِدَق مثل تغطية totalAssets لكل المطالبات المستردة وعدم تمكن المودع من الربح عبر دورة إيداع-سحب فورية بدون عائد مشروع.

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

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

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

بقلم

الأسئلة الشائعة

ما هي الأخطاء الحسابية في عقود Solidity الذكية؟

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

كيف يمكن للأخطاء الحسابية السماح للمستخدمين بسحب مبالغ زائدة؟

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

كيف أمنع الأخطاء الحسابية في Solidity؟

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

هل تمنع عمليات إعادة التراجع overflow الأخطاء الحسابية في Solidity؟

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

ماذا تعلمنا خسارة Cetus CLMM عن الأخطاء الحسابية؟

خسر Cetus CLMM مبلغ 223 مليون دولار على شبكة Sui في حادثة صنفت كخطأ حسابي متعلق بحساب الرموز والأسهم، مما يوضح أن الأمان الحسابي لا يقتصر على منع overflow في Solidity، لأن كل سهم مصك يجب أن يمثل مطالبة قابلة للدفاع عن الأصول.

دردشة