איך להימנע מטעויות אריתמטיות בחוזי Solidity

המאמר נכתב על ידי

טעויות חישוביות בחוזים חכמים ב-Solidity מתרחשות כאשר חישובי טוקנים, מניות או מחירים משתמשים בנוסחה שגויה, בקנה מידה עשרוני לא נכון, בכיוון עיגול שגוי או בהנחה מספרית מוטעית, מה שמאפשר למשתמשים ליצור תביעות מיותרות, למשוך יותר ערך מהפקדתם או לגרום לכישלון של אי-השוויונות החשבונאיים. הפתרון הוא להגדיר יחידות במפורש, להשתמש באריתמטיקה מדויקת ומוגבלת, ולבדוק את האי-השוויונות הכלכליים שהנוסחה חייבת לשמר.

ההשלכות חומריות. Cetus CLMM איבדה 223.0 מיליון דולר ב-Sui ב-2025-05-22 באירוע המסווג כ Arithmetic Error שכולל טעויות בחשבונאות טוקנים ומניות. בטיחות אריתמטית אינה רק על מניעת revert בגלל overflow ב-Solidity, אלא על וידוא שכל מניה שנמכרת מייצגת תביעה מוצדקת על נכסים.

מהן טעויות אריתמטיות ב-Solidity וכיצד פועלות התקפות?

טעויות אריתמטיות ב-Solidity הן פגמים בחישובים שמתרגמים הפקדות, משיכות, יתרות, מחירים, עמלות או נזילות לתביעות כלכליות. תוקף מנצל את הפער בין מודל החשבונאות הרצוי והנוסחה המיושמת, לעיתים על ידי בחירת סדר העסקאות, כמויות נכסים, או מצבי הבריכה שמביאים להמרה שגויה לטובתו.

ב-Solidity מודרנית, overflow ו-underflow גורמים ל-revert כברירת מחדל. ההגנה הזו מועילה, אך אינה מוכיחה שנוסחה נכונה מבחינה כלכלית. חישוב יכול להישאר בתוך גבולות uint256 ועדיין ליצור יותר מדי מניות מחסן, לגבות מעט מדי ביטחונות, או לעגל משיכה לטובת המשתמש.

התקפת מניות-יתר טיפוסית עוברת את השלבים הבאים:

  1. למחסן יש נכסים ומניות קיימות, כך שמחיר מניה אינו יחידה.
  2. המחסן מקבל תשואה, עמלות, תרומות או הגדלת יתרה בעקבות ריבייס.
  3. מחיר המניה עולה כי סך הנכסים גדל וסך המניות לא.
  4. התוקף מפקיד נכסים באמצעות נוסחה שמניחה שמניה אחת שווה תמיד נכס אחד.
  5. המחסן מנפיק יותר מניות מה שההפקדה צריכה לרכוש במחיר הנוכחי.
  6. התוקף מחלץ את המניות כדי לקבל חלק מופרז מסך הנכסים המאוגדים.

האי-השוויון המרכזי פשוט: לאחר הפקדה, החלק בבעלות של המשתמש החדש צריך להתאים לערך שתרם. אם הפקדה השווה ל-1% מנכסי הבריכה מפיקה יותר מ-1% מהמניות הקיימות, בעלי מניות קיימים מודללים.

אילו דפוסי Solidity גורמים לטעויות בחשבון טוקנים ומניות?

טעויות בחשבון טוקנים ומניות נובעות לרוב מערבוב יחידות, קיצוץ ערכים ביניים או טיפול במחיר מניה שמשתנה כקבוע. הפונקציות בסיכון הגבוה הן deposit, mint, withdraw, redeem, הגבלות הלוואה, הסדרת עמלות, חישובי נזילות והפצת תגמולים.

דפוס מסוכן הוא יצירת מניות ישירות מיחידות נכס שהופקדו:

shares = assets * 1e18 / assetScale;

המרה זו מתאימה רק להפקדה הראשונה, כאשר מניה מוגדרת בכוונה כיחידת נכס מנורמלת אחת. ברגע שהמחסן מרוויח תשואה או סופג הפסד, ההפקדות חייבות להשתמש בשער החליפין הנוכחי:

shares = assets * totalShares / totalAssets;

גם הביטוי הזה דורש טיפול זהיר. כפל תחילה עלול לגרום ל-overflow אם נמצא בתוך unchecked, וחלוקה תחילה עלולה לאבד דיוק. נירמול עשרוני מהווה מקור נוסף לטעויות. נכסי USDC לדוגמה משתמשים בפחות ספרות עשרוניות מטוקן מניה ב-18 ספרות, ופרוטוקול חייב להבחין בעקביות בין יחידות טוקן גולמיות ליחידות חשבונאיות מנורמלות.

גורמים שכיחים כוללים:

  • הקצאת קבוע 1e18 לנכס עם דיוק עשרוני שונה.
  • יצירת מניות בשער קבוע אחד-לאחד לאחר שינוי מחיר המניה.
  • חלוקה לפני כפל שאיבדה דיוק משמעותי.
  • עיגול הפקדות או משיכות כלפי מעלה שגורם לדליפה ערכית מצטברת.
  • שימוש ב-unchecked סביב ערכים הניתנים לשליטה על ידי משתמש או גורמי קנה מידה.
  • הנחה שמאזני טוקנים לא משתנים מחוץ לזרמי הפקדה ומשיכה רגילים.
  • שכחה שטוקנים עם עמלת העברה, ריבייס ותשואה יכולים להפוך את החשבונאות למוטעית.

כיצד לתקן טעויות אריתמטיות בחישובי מניות ב-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: מתעלם ממחיר המניות הנוכחי.
        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: תמחור מניות יחסית ליחס הבריכה לפני ההפקדה.
        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 overflow ואובדן דיוק ב- a * b / c יותר גז מפעולות פשוטות לא מאמתת את הנוסחה הכלכלית
עיגול כלפי מטה בהפקדות ומשיכות הפקדה ומחזור יתר בגלל עיגול למעלה יכול ליצור יתרות אבק אבק דורש מדיניות התאוששות
סף מינימלי להנפקת מניות הפקדות שעולות ל-0 מניות עקב עיגול השוואה אחת אסור לחסום משתמשים קטנים לגיטימיים באופן בלתי צפוי
קלטים ודיוקים עשרוניים מוגבלים חישוב מעריכי בלתי בטוח וקלטי משתמש קיצוניים בדיקות אימות הגבולות חייבים להתאים לנכסים הנתמכים
בדיקות אי-השוויון מעברי מצב שמפרים חשבונאות הבריכה מאמץ כתיבת בדיקות אי-השוויונים חייבים לכסות רצפים עוינים

השתמש בעיגול כלפי מטה כאשר האלטרנטיבה היא להקצות ערך שהבריכה לא תוכל לתמוך בו. לדוגמה, מחסן בדרך כלל מעגל את מניות ההפקדה ואת הערך שנמשך כלפי מטה. אם מוצר זקוק לעיגול לטובת המשתמש, עליו לממן במפורש את הסובסידיה שנוצרת במקום לכפות אותה באילף על מפקידים אחרים.

תובנת אבטחה: תחת מתודת Soken, סקירת אריתמטיקה מתחילה ביחידות וחוקי שימור, לא באופרוטורים. כפל שנבדק עדיין עלול להיות ניתן לניצול אם התוצאה מייצגת נכס שגוי, מניה שגויה, מחיר שגוי או תביעת בעלות שגויה.

אילו מקרים אמיתיים מדגימים את השפעת טעויות אריתמטיות?

אירועי Arithmetic Error מראים שחשבונאות טוקנים ומניות היא תכונת אבטחה ברמת הפרוטוקול, לא רק בעיית איכות קוד מקומית. הלוגיקה שנפגעה יכולה להיות במלאי 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. ב־token ו-share accounting, התנאי הפגיע הוא לרוב יחס שגוי בין רזרבות, היצע, מחיר ותביעות משתמש.

DefiLlama מתעד 35 אירועי Arithmetic Error בחוזים חכמים בין 2020 ל-2026, עם הפסדים משולבים של 561.0 מיליון דולר. עבור צוותי פרוטוקול, ההיסטוריה הזו מחזקת את הצורך בבדיקת מסלולי חשבונאות בעדיפות שווה לניהול שליטה בגישה וטיפול בקריאות חיצוניות.

כיצד מזהים טעויות אריתמטיות לפני פריסה?

טעויות אריתמטיות מזוהים היטב על ידי סקירת מעברי מצב ובדיקת נכסים כלכליים לאורך רצפים רבים, לא על ידי בדיקת נוסחאות מבודדות ידנית. ניתוח סטטי יכול לזהות אופרטורים מסוכנים ובלוקים unchecked, בעוד fuzzing ובדיקות אי-השוויון מבהירים אם מודל החשבונאות מחזיק תחת הפקדות, משיכות, תשואה, הפסד וערכים עשרוניים חריגים.

התמקד ברשימת ביקורת זו:

  1. פרט כל משתנה נכס, מניה, מחיר, ריבית ועמלה עם יחידתו ועשרוניותיו.
  2. וודא שכל הפקדה משתמשת בשער החליפין לפני ההפקדה.
  3. וודא שכל משיכה משתמשת בשער עקבי עם מניות הנשרפות.
  4. חפש שימוש ב-*, /, unchecked וקבועים קשיחים בקווי חשבונאות.
  5. בדוק נכסים עם דיוקים עשרוניים שונים והפקדות קרובות לגודל המינימום.
  6. אשר שעיגול אינו מאפשר ליצור מניות לא נתמכות או לשחרר נכסים לא נתמכים.
  7. דגם שינויים ביתרות מריבייס, תרומות, עמלות ותשואה לפני כל פעולה של משתמש.
  8. ודא שסך תביעות המשתמשים לא יכול לעלות על ערכי הנכסים הזמינים למחזור.

Slither יכול לדגל דפוסי אריתמטיקה מסוכנים, בלוקים לא מבוקרים ופעולות דיוק חשודות, אך אינו יכול לקבוע אם נוסחת מחיר מניה תקינה כלכלית. Foundry fuzzing יעיל יותר בשילוב עם אמירות כגון כיסוי כל התביעות ש-totalAssets ושהמפקיד לא יכול להרוויח ממחזור הפקדה-משיכה מיידי ללא תשואה לגיטימית.

בדיקות אי-השוויון צריכות לכלול רצפי משתמשים מרובים: הפקדה, דיווח תשואה, משיכה, משיכה חלקית, תרומה והפקדות קטנות חוזרות. נהלי סקירת Soken משווים גם את המודל המתמטי המתוכנן עם כל עדכון מצב, מכיוון שנוסחה נכונה שמיושמת לפני שינוי מצב במקום לאחריו עלולה עדיין לגרום למחיר שגוי של מניות.

בטיחות אריתמטית היא המשמעת של שמירת האמת הכלכלית בכל המרה. מחסן יכול להשתמש באריתמטיקה של Solidity 0.8 עם בדיקות ועדיין להיות פגיע אם הוא מממש תביעות במחיר שגוי, מניח עשרוניות שגויות או מעגל ערך החוצה.

התחל בכתיבת יחידות ואי-השוויון לכל משתנה חשבונאי, ואז קודד אי-השוויונות האלו בבדיקות Foundry לפני הוספת סוגי נכסים או נתיבי תמחור חדשים. למחקר מעמיק יותר של דפוסי אבטחה ב-Solidity, בקר ב-Soken Hub.

המאמר נכתב על ידי

שאלות נפוצות

מהן טעויות אריתמטיות בחוזי Solidity?

טעויות אריתמטיות ב-Solidity הן ליקויים בחישובים של הפקדות, משיכות, יתרות, מחירים, עמלות, נזילות, טוקנים או מניות. הן עלולות לגרום לתביעות עודפות, משיכות יתר או כישלונות באינבריאנטים חשבונאיים בגלל נוסחאות, סקלות עשרוניות, כיווני עיגול או הנחות מספריות שגויות.

כיצד טעויות אריתמטיות מאפשרות למשתמשים למשוך יותר מדי?

טעויות אריתמטיות ב-Solidity יכולות לאפשר למשתמשים למנט טענות עודפות או למשוך יותר ערך מה שהפקידו כאשר חישובי טוקן, מניה או מחיר משתמשים בנוסחה, בקנה מידה עשרוני, בכיוון עיגול או בהנחה מספרית שגויים. החישוב יוצר טענה כלכלית שהנכסים אינם תומכים בה.

כיצד למנוע טעויות אריתמטיות ב-Solidity?

מניעת טעויות אריתמטיות ב-Solidity דורשת הגדרה מפורשת של יחידות בכל חישוב, שימוש במתמטיקה מדויקת עם גבולות, ובדיקת אינבריאנטים כלכליים שהנוסחא חייבת לשמור. אלה מתמודדים עם נוסחאות שגויות, סקלות עשרוניות, כיווני עיגול והנחות שגויות שעשויות לעוות טענות טוקנים, מניות או מחירים.

האם פניות overflow מספיקות לעצור טעויות אריתמטיות ב-Solidity?

פניות overflow בלבד אינן חוסמות טעויות אריתמטיות ב-Solidity. כדי להבטיח בטיחות אריתמטית נדרשים נוסחאות נכונות, יחידות ברורות, סקלות עשרוניות מתאימות, כיווני עיגול נכונים והנחות מספריות נכונות. חישוב עלול להתחמק מ-failure overflow ועדיין ליצור טענה בלתי מוצדקת למניה או משיכה.

מה מלמד ההפסד של Cetus CLMM על טעויות אריתמטיות?

ההפסד של Cetus CLMM בסכום של 223 מיליון דולר ב-Sui תואר כ'טעות אריתמטית' שקשורה לנהל טוקנים ומניות. המקרה מדגים כי בטיחות אריתמטית לא מתמקדת רק במניעת overflow ב-Solidity, אלא בכל מניה שצריך לייצג טענה מוצדקת על נכסים.

צ׳אט