Solidity akıllı kontratlarında aritmetik hatalar, token, pay veya fiyat hesaplamalarının yanlış formül, ondalık ölçek, yuvarlama yönü veya sayısal varsayım kullanması sonucu oluşur; bu da kullanıcıların fazla talepte bulunmasına, yatırdıklarından daha fazla değer çekmesine veya muhasebe değişmezlerinin bozulmasına olanak tanır. Çözüm, birimleri açıkça tanımlamak, tam hassasiyetli sınırlandırılmış aritmetik kullanmak ve formülün koruması gereken ekonomik değişmezleri test etmektir.
Sonuçlar önemlidir. Cetus CLMM, 2025-05-22 tarihinde Sui üzerinde, token ve pay muhasebesini içeren bir Aritmetik Hata olarak sınıflandırılan bir olayda 223.0 milyon dolar kaybetmiştir. Bu nedenle aritmetik güvenliği yalnızca Solidity taşma (overflow) revertlerini önlemekle ilgili değildir. Her basılan payın varlıklar üzerinde savunulabilir bir talebi temsil etmesini sağlamaktır.
Solidity’deki aritmetik hatalar nedir ve saldırılar nasıl işler?
Solidity’deki aritmetik hatalar, yatırma, çekme, bakiye, fiyat, ücret veya likidite gibi ekonomik talepleri temsil eden hesaplamalardaki kusurlardır. Bir saldırgan, amaçlanan muhasebe modeli ile uygulanan formül arasındaki uyumsuzluktan faydalanır; genellikle işlem sırasını, varlık miktarlarını veya havuz durumlarını seçerek yanlış bir dönüşümün kendisine avantaj sağlamasını sağlar.
Modern Solidity’de, aritmetik taşma ve alt sınır ihlali varsayılan olarak revert olur. Bu koruma faydalıdır ancak bir formülün ekonomik olarak doğru olduğunu kanıtlamaz. Bir hesaplama uint256 sınırları içinde kalarak fazla sayıda kasa payı basabilir, çok az teminat talep edebilir veya çekmeyi kullanıcının lehine yuvarlayabilir.
Tipik bir fazla pay saldırısı şu adımları izler:
- Bir kasada mevcut varlıklar ve paylar vardır, bu yüzden her pay birim fiyatı birden farklıdır.
- Kasa getiri, ücret, bağış veya yeniden dengeleme sonucu bakiye artışı alır.
- Toplam varlık artarken toplam pay değişmediği için pay fiyatı yükselir.
- Saldırgan, her payın her zaman bir varlığa eşit olduğu varsayımıyla varlık yatırır.
- Kasa, mevcut fiyata göre yatırma miktarının alması gereken paydan daha fazlasını basar.
- Saldırgan, bu payları havuz varlıklarının orantısız bir kısmı karşılığında geri çeker.
Temel değişmez şudur: Bir yatırmadan sonra, yeni kullanıcının sahiplik oranı yatırdığı değere karşılık gelmelidir. Eğer %1 değerdeki bir yatırma, dolaşımdaki payların %1’inden fazla pay yaratıyorsa, mevcut sahipler seyrelir.
Hangi Solidity desenleri token ve pay-muhasebesi hatalarına yol açar?
Token ve pay muhasebesi hataları genellikle birimlerin karıştırılması, ara değerlerin kırpılması veya değişen pay fiyatının sabit varsayılmasıyla ortaya çıkar. En yüksek riskli fonksiyonlar deposit, mint, withdraw, redeem, borç limitleri, ücret çözümü, likidite hesaplamaları ve ödül dağıtımıdır.
Tehlikeli bir desen, yatırılan varlık birimlerinden doğrudan pay basmaktır:
shares = assets * 1e18 / assetScale;
Bu dönüşüm sadece ilk yatırma için uygun olabilir; o zaman bir pay bilinçli olarak normalleştirilmiş bir varlık birimine eşit tanımlanır. Bir kasa getiri kazanıp zarar ederse, yatırmalar güncel döviz kuru üzerinden hesaplanmalıdır:
shares = assets * totalShares / totalAssets;
Bu ifade bile dikkatle işlenmelidir. Önce çarpma, unchecked içinde yer alırsa taşmaya neden olabilir; önce bölme ise hassasiyet kaybettirir. Ondalık normalizasyonu ek hata kaynağıdır. USDC gibi varlıklar genellikle 18 ondalık pay tokeninden daha az ondalık kullanır; bir protokol ham token birimleri ile normalleştirilmiş muhasebe birimlerini tutarlı biçimde ayırt etmelidir.
Yaygın nedenler şunlardır:
- Farklı ondalık hassasiyetli bir varlık için
1e18sabitini doğrudan kodlamak. - Pay fiyatı değiştikten sonra sabit bire-bir oranla pay basmak.
- Önce bölüp sonra çarparak anlamlı hassasiyet kaybetmek.
- Yatırmaları veya çekimleri yukarı yuvarlayarak kümülatif değer sızıntısı yaratmak.
- Kullanıcı kontrollü değerler veya ölçek faktörleri etrafında
uncheckedkullanmak. - Token bakiyelerinin normal yatırma ve çekme akışlarının dışında değişemeyeceğini varsaymak.
- Ücret transferi, yeniden dengeleme ve getiri taşıyan tokenların önbelleğe alınmış muhasebeyi geçersiz kılabileceğini unutmak.
Solidity pay hesaplamalarındaki aritmetik hatalar nasıl düzeltilir?
Aritmetik hataları düzeltmek için varlık birimleri bir kez normalleştirilmeli, paylar güncel havuz oranına göre hesaplanmalı, tam hassasiyetle çarpma ve bölme yapılmalı ve yuvarlama mevcut sahiplerden yeni yatırana değer aktarımı yapmayacak şekilde seçilmelidir. Kontrat, varlık bakiyesi ve pay arzını bağlı muhasebe durumu olarak ele almalıdır.
Aşağıdaki savunmasız kasa, ondalıklı konfigüre edilebilir bir token kabul eder. reportYield ile varlık arttığında bile sabit oranla normalleştirilmiş pay basar. Yatırımcı, getiri biriktiğinde çok fazla pay alabilir, ardından bu payları daha büyük havuz karşılığında geri çekebilir.
// 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;
// ZAYIFLIK: Mevcut pay fiyatını yok sayıyor.
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");
}
}
Düzeltilmiş versiyon, ham varlık birimlerini 18 ondalıklı muhasebe birimlerine dönüştürür ve payları güncel döviz kurundan basar. OpenZeppelin’in tam hassasiyetli Math.mulDiv fonksiyonunu kullanır, yatırma ve çekimleri aşağı yuvarlar ve desteklenen ondalık değerleri sınırlar.
// 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);
// DÜZELTME: Payları yatırma öncesi havuz oranına göre değerlendir.
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");
}
}
Protokolünüz yatırma, çekme, teminat veya likiditeyi özel aritmetikle fiyatlandırıyorsa, bir akıllı kontrat denetimi ve güvenlik değerlendirmesi birim dönüşümlerini ve ekonomik değişmezleri işlem yaşam döngüsü boyunca izleyebilir.
Geliştiriciler aritmetik hataları nasıl durdurabilir?
Geliştiriciler, formülleri uygulamadan önce muhasebe birimlerini belirleyerek, muhafazakar yuvarlama uygulayarak ve her dönüşümü bir güvenlik sınırı olarak ele alarak aritmetik hataların önüne geçer. Bir formül, her girişin neyi temsil ettiğini, ondalık hassasiyetini, izin verilen aralığını ve yuvarlamadan kimin faydalandığını belirtmelidir.
| Yöntem | Önlediği şey | Gaz veya karmaşıklık maliyeti | Sınırlama |
|---|---|---|---|
| Açık birim normalizasyonu | Ham token birimlerinin 18 ondalıklı pay birimleriyle karışması | Ek çarpma veya bölme | Belgelendirilmiş ondalık politikası gerekir |
Math.mulDiv |
a * b / c hesaplamasında taşma ve hassasiyet kaybı |
Basit işleçlerden daha fazla gaz | Ekonomik formül doğrulamıyor |
| Yatırma ve çekimleri aşağı yuvarlama | Yuvarlamadan dolayı fazla basım ve fazla geri çekim | Toz bakiye oluşabilir | Toz bakiyesi için kurtarma politikası gerekir |
| Minimum pay basım eşiği | Sıfıra yuvarlanan yatırımlar | Bir karşılaştırma | Meşru küçük kullanıcıları engellememeli |
| Sınırlandırılmış giriş ve ondalıklar | Güvensiz üs alma ve aşırı kullanıcı girdileri | Doğrulama kontrolleri | Sınırlar desteklenen varlıklarla uyumlu olmalı |
| Değişmez testleri | Havuz muhasebesini ihlal eden durum değişiklikleri | Test yazma çabası | Değişmezler saldırgan dizileri kapsamlı ele almalı |
Alternatifin havuzun destekleyemeyeceği değer üreteceği yerde aşağı yuvarlamayı kullanın. Örneğin, bir kasa yatırılan payları ve çekilen varlıkları genellikle aşağı yuvarlar. Bir ürün kullanıcı lehine yuvarlama yapacaksa, buna bağlı sübvansiyonu açıkça fonlamalı, başka yatıranlara gizlice yüklememelidir.
Güvenlik içgörüsü: Soken metodolojisinde aritmetik inceleme işleçlerle değil, birimler ve korunum kanunlarıyla başlar. Kontrol edilen bir çarpma, sonuç yanlış varlık, pay, fiyat veya sahiplik talebini temsil ederse yine de istismar edilebilir.
Aritmetik hataların etkisini gösteren gerçek olaylar nelerdir?
Aritmetik Hata olayları, token ve pay muhasebesinin lokal kod kalitesi sorunundan ziyade protokol çapında bir güvenlik özelliği olduğunu ortaya koyar. Etkilenen mantık AMM rezervlerinde, borç verme ödüllerinde, kasa paylarında, likidite hesaplamalarında veya varlık dönüşüm rutinlerinde olabilir.
| Protokol | Tarih | Kaybedilen Tutar | Zincir |
|---|---|---|---|
| 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 |
Bu olaylar ortak kritik bir özelliğe sahiptir: On-chain değer dağıtımını veya değerlemesini yöneten bir hesaplama söz konusudur. Hata mutlaka kontrolsüz bir uint256 toplama değildir. Token ve pay muhasebesinde savunmasız koşul genellikle rezervler, arz, fiyat ve kullanıcı talepleri arasındaki hatalı ilişkidir.
DefiLlama 2020–2026 arasında 35 akıllı kontrat Aritmetik Hata olayını ve toplam 561.0 milyon dolarlık kaybı kaydetmiştir. Protokol ekipleri için bu geçmiş, muhasebe yollarını erişim kontrolü ve harici çağrı işlemiyle aynı öncelikte incelemeyi destekler.
Dağıtımdan önce aritmetik hatalar nasıl tespit edilir?
Aritmetik hatalar, izole formülleri manuel kontrol etmektense durum geçişlerini gözden geçirmek ve ekonomik özellikleri çoklu dizilerde test etmekle daha iyi tespit edilir. Statik analiz riskli işleçleri ve unchecked bloklarını tanımlayabilir; fuzzing ve değişmez testleri yatırma, çekme, getiri, zarar ve sıra dışı ondalık değerler altında muhasebe modelinin geçerliliğini ortaya koyar.
Odaklanmış inceleme kontrol listesi şudur:
- Her varlık, pay, fiyat, oran ve ücret değişkenini birimi ve ondalıklarıyla listeleyin.
- Her yatırmanın yatırma öncesi döviz kurunu kullandığından emin olun.
- Her çekmenin yakılan paylarla tutarlı oranda fiyatlandığını doğrulayın.
- Muhasebe yollarında ham
*,/,uncheckedve sabit ölçek değerleri arayın. - Farklı ondalık hassasiyetlerde varlıklar ve minimum boyut yakınında yatırmalar test edin.
- Yuvarlamanın desteklenmeyen pay basmamasını veya varlık salmamasını teyit edin.
- Kullanıcı işlemi öncesi yeniden dengeleme, bağış, ücret ve getiriyle bakiye değişikliklerini modelleyin.
- Toplam kullanıcı taleplerinin geri çekilebilir varlıkları aşamadığını kontrol edin.
Slither tehlikeli aritmetik desenler, unchecked bloklar ve şüpheli hassasiyet işlemlerini işaretleyebilir ama pay-fiyat formülünün ekonomik sağlamlığını belirleyemez. Foundry fuzzing, totalAssets‘in tüm geri alınan talepleri kapsaması ve yatıran kişinin meşru getiri olmadan hemen yatırma-çekme ile kar edememesi gibi doğrulamalarla birlikte daha etkilidir.
Değişmez testleri şu çoklu kullanıcı dizilerini üretmelidir: yatırma, getiri raporu, çekme, kısmi çekme, bağış ve tekrarlayan küçük yatırmalar. Soken inceleme uygulaması, amaçlanan matematik modeli her durum güncellemesiyle karşılaştırır; çünkü doğru formül durum değişikliğinden önce uygulanmazsa payları yine yanlış fiyatlayabilir.
Aritmetik güvenliği, her dönüşümde ekonomik gerçeği koruma disiplinidir. Solidity 0.8 kontrollü aritmetiği kullanan bir kasa, hatalı fiyatla talep basarsa, yanlış ondalıkları varsayarsa veya değeri dışa yuvarlarsa hala savunmasızdır.
Her muhasebe değişkeni için birim ve değişmezi yazmakla başlayın, ardından yeni varlık türleri veya fiyatlama yolları eklemeden önce bu değişmezleri Foundry testlerinde kodlayın. Solidity güvenlik desenleri hakkında derinlemesine araştırmalar için Soken Hub ziyaret edin.