Як уникнути арифметичних помилок у Solidity

Автор статті

Помилки в арифметиці в Solidity-розумних контрактах виникають, коли обчислення токенів, часток або цін виконуються за неправильною формулою, масштабом десяткових знаків, напрямком округлення або числовими припущеннями, через що користувачі можуть створювати зайві вимоги, знімати більше, ніж внесли, або порушувати облікові інваріанти. Вирішення полягає у явному визначенні одиниць виміру, використанні арифметики з повною точністю і перевірці економічних інваріантів, які має зберігати формула.

Наслідки суттєві. Cetus CLMM втратив $223.0M на Sui 2025-05-22 в інциденті, класифікованому як Arithmetic Error, пов’язаному з обліком токенів і часток. Тож арифметична безпека — це не просто запобігання переповненню Solidity з revert’ом. Це — гарантія того, що кожна викарбувана частка являє собою захищене право на активи.

Що таке арифметичні помилки в Solidity і як працюють атаки?

Арифметичні помилки в Solidity — це дефекти в обчисленнях, що перетворюють депозити, зняття, баланси, ціни, комісії або ліквідність на економічні вимоги. Зловмисник використовує невідповідність між наміреним обліковим моделям і реалізованою формулою, часто вибираючи порядок транзакцій, кількість активів або стан пулу таким чином, що неправильне перетворення йому вигідне.

У сучасному Solidity переповнення та недоповнення арифметичних операцій за замовчуванням викликає revert. Цей захист корисний, але не гарантує, що формула економічно коректна. Обчислення можуть залишатися в межах 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 для активу з іншою десятковою точністю.
  • Карбування часток за фіксованим курсом 1:1 після зміни ціни часток.
  • Ділення перед множенням зі втратами значущої точності.
  • Округлення депозитів або зняттів вгору, що створює кумулятивний витік вартості.
  • Використання 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: ігнорує поточну ціну існуючих часток.
        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-десяткові облікові одиниці, карбує частки за поточним обмінним курсом, використовує OpenZeppelin Math.mulDiv з повною точністю, округлює вниз і встановлює межі підтримуваних десяткових значень.

// 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 Переповнення і втрата точності в a * b / c Більший газ, ніж прості оператори Не перевіряє економічну коректність формули
Округлення депозитів і зняттів вниз Надкарбування і надвиплати через округлення Може створити пилові баланси Необхідна політика відновлення пилу
Мінімальний поріг карбування часток Депозити, що округляються до нуля часток Одне порівняння Не має заважати законним малим користувачам
Обмеженість вхідних даних і десяткових знаків Небезпечні експоненти та екстремальні вхідні дані користувачів Перевірки валідації Межі мають співпадати з підтримуваними активами
Тести інваріантів Перехід станів, що порушують облік пулу Потрібно писати тести Інваріанти мають охоплювати шкідливі послідовності

Використовуйте округлення вниз там, де альтернативою було б надання пулу вартості, яку він не може підтримати. Наприклад, сховище зазвичай округлює видані частки вниз і повернені активи вниз. Якщо продукт потребує округлення на користь користувача, він повинен явно фінансувати відповідну субсидію замість того, щоб непомітно навантажувати інших депозиторів.

Інсайт з безпеки: За методологією 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. В обліку токенів і часток вразливий стан — це часто неправильне співвідношення між резервами, пропозицією, ціною та претензіями користувачів.

DefiLlama зафіксувала 35 інцидентів Arithmetic Error у смарт-контрактах із 2020 по 2026 роки з сумарними втратами $561.0M. Для команд протоколів ця історія підкреслює пріоритетність огляду облікових шляхів нарівні з контролем доступу і обробкою зовнішніх викликів.

Як виявити арифметичні помилки до запуску?

Арифметичні помилки найкраще виявляти, аналізуючи стани транзакцій і тестуючи економічні властивості на багатьох послідовностях, а не перевіряючи поодинокі формули вручну. Статичний аналіз показує ризикові оператори і блоки unchecked, тоді як фаззинг і тести інваріантів встановлюють, чи зберігається облікова модель за депозитів, зняттів, доходу, втрат і незвичних десяткових значень.

Використовуйте цей чеклист:

  1. Запишіть кожну змінну активів, часток, цін, ставок і комісій з одиницями та десятковими знаками.
  2. Перевірте, щоб кожен депозит використовував курс перед депозитом.
  3. Перевірте, щоб кожне зняття використовувало курс, сумісний із списаними частками.
  4. Пошукайте сирі *, /, unchecked і жорстко закодовані константи масштабу в облікових шляхах.
  5. Тестуйте активи з різною десятковою точністю і депозити близько до мінімальних розмірів.
  6. Підтвердіть, що округлення не карбує непідтримувані частки і не випускає непідтримувані активи.
  7. Моделюйте зміни балансу від ребейзингу, пожертв, комісій і доходу перед кожною дією користувача.
  8. Переконайтеся, що загальні претензії користувачів не перевищують активи, доступні для викупу.

Slither може сигналізувати про небезпечні арифметичні патерни, unchecked блоки і сумнівні операції з точністю, але не визначить, чи економічно коректна формула ціни часток. Foundry-фаззинг ефективніший у поєднанні з assert’ами, такими як покриття всіма викупленими претензіями totalAssets та неможливість депозитору отримати прибуток із циклу депозит-зняття без законного доходу.

Тести інваріантів мають генерувати мультикористувацькі послідовності: депозит, звіт про дохід, зняття, часткове зняття, пожертва і повторні малі депозити. Практика огляду Soken також порівнює намірену математичну модель із кожним оновленням стану, адже застосування коректної формули перед, а не після зміни стану може все одно некоректно оцінювати частки.

Арифметична безпека — це дисципліна збереження економічної істини на кожному кроці перетворення. Сховище може користуватися перевіреною арифметикою Solidity 0.8 і все одно бути вразливим, якщо карбує претензії за невірною ціною, припускає неправильні десяткові знаки або округлює в сторону втрат.

Починайте з прописування одиниць і інваріантів для кожної облікової змінної, потім кодуйте ці інваріанти у Foundry-тестах перед тим, як додавати нові типи активів або шляхи ціноутворення. Для глибшого дослідження патернів безпеки Solidity відвідайте Soken Hub.

Автор статті

Часті запитання

Що таке арифметичні помилки у Solidity смарт-контрактах?

Арифметичні помилки в Solidity — це дефекти у розрахунках депозитів, зняттів, балансів, цін, комісій, ліквідності, токенів чи часток. Вони можуть призводити до зайвих заявок, надмірних зняттів або порушення облікових інваріантів через помилки у формулах, масштабах десяткових, напрямках округлення або числових припущеннях.

Як арифметичні помилки дозволяють користувачам знімати надто багато?

Через арифметичні помилки в Solidity користувачі можуть створювати зайві претензії або знімати більше, ніж внесли, якщо обчислення токенів, часток чи цін використовує неправильну формулу, масштаб, напрямок округлення чи числові припущення. Розрахунок тоді надає економічну претензію, яку активи не підтримують.

Як запобігти арифметичним помилкам у Solidity?

Запобігають помилкам у Solidity чітким визначенням одиниць кожного обчислення, застосуванням точної обмеженої арифметики та тестуванням економічних інваріантів, які має зберігати формула. Такі контролі виправляють помилки у формулах, масштабах, напрямках округлення та числових припущеннях, що можуть спотворювати претензії на токени, частки чи ціни.

Чи достатньо overflow revert для зупинки арифметичних помилок у Solidity?

Перевірка переповнення (overflow revert) сама по собі не ліквідує арифметичних помилок у Solidity. Безпека арифметики вимагає правильних формул, явного визначення одиниць, коректних масштабів десяткових, правильних напрямків округлення та надійних числових припущень. Формула може уникнути переповнення, але створити необґрунтовані претензії на активи.

Який урок із втрати $223M в Cetus CLMM щодо арифметичних помилок?

Втрати Cetus CLMM у $223M на Sui класифікують як арифметичну помилку, пов’язану з обліком токенів та часток. Цей випадок демонструє, що безпека арифметики не обмежується лише уникненням переповнень Solidity, адже кожна створена частка має бути обґрунтованою претензією на активи.

Чат