در روز دوشنبه، ساعت ۱۱:۱۶ به وقت تهران، سیستم مانیتورینگ Hypernative اولین نشانههای یک اکسپلویت (سوءاستفاده امنیتی) را که استخرهای پایدار ترکیبی (Composable Stable Pools) در Balancer V2 را هدف قرار داده بود، شناسایی کرد. تحلیلهای بعدی تأیید کرد که این مشکل، استخرهای پایدار ترکیبی را در چندین شبکه از جمله اتریوم، بِیس (Base)، آوالانچ، جنوسیس (Gnosis)، براجین (Berachain)، پالیگان، سونیک (Sonic)، آربیتروم و آپتیمیزم تحت تأثیر قرار داده است. این حادثه محدود به استخرهای پایدار ترکیبی در Balancer V2 و فورکهای آن در زنجیرههای دیگر مانند BEX و Beets بود. Balancer V3 و تمام انواع دیگر استخرها تحت تأثیر قرار نگرفتهاند.
تلاشهای واکنش فوری با هماهنگی مشارکتکنندگان بالانسر، شرکای امنیتی و پاسخدهندگان کلاهسفید آغاز شد که منجر به مهار اکسپلویت و بازیابی یا مسدودسازی بخشی از داراییهای آسیبدیده گردید. یک اتاق جنگ (War Room) هماهنگی لازم برای مهار، اطلاعرسانی و بازیابی در سراسر شبکهها را بر عهده گرفت. استخرهای CSPv6 به حالت بازیابی (Recovery Mode) منتقل شدند، مسدودسازی داراییها با همکاری شرکا اجرا شد و تعامل با کلاهسفیدها تحت چارچوب «پناهگاه امن» SEAL ادامه یافت.
به گزارش میهن بلاکچین، اگرچه ارقام نهایی خسارت همچنان در دست بررسی است، اما این اکسپلویت قابل توجه بود. گزارش کامل تحلیلی پسارویداد (Post-mortem) پس از تکمیل تمام بررسیهای فنی و حقوقی منتشر خواهد شد.
علت اولیه
خزانه (Vault) نسخه ۲ (V2) از دو نوع سوآپ (Swap) ساده و دستهای (Batch) پشتیبانی میکند. سوآپ دستهای اجازه میدهد تا چندین عملیات در یک تراکنش واحد ترکیب شوند که این امر از انتقال توکنهای میانی جلوگیری کرده و باعث صرفهجویی قابل توجهی در کارمزد گس (Gas) میشود.
یک ویژگی کلیدی سوآپ دستهای، تسویه با تأخیر (Deferred Settlement) است که در آن کالکنندگان میتوانند عملاً توکنها را وام آنی (Flashloan) بگیرند تا سوآپها را انجام دهند، به شرطی که همهچیز در انتها تسویه شود. برای استخرهای پایدار ترکیبی، توکنهای رسید تأمین نقدینگی (BPT) مانند توکنهای معمولی رفتار میشوند که این امر اجازه میدهد محدودیت حداقل عرضه استخر دور زده شود و سطح نقدینگی در استخر به مقادیر بسیار پایینی برسد.
اکسپلویت از جهت گِرد کردن (Rounding Direction) در تابع upscale که بر سوآپهای نوع EXACT_OUT در استخرهای پایدار ترکیبی تأثیر میگذاشت، نشأت گرفت. این تابع زمانی که فاکتورهای مقیاسبندی مقادیر غیرصحیح هستند (شرایطی که هنگام لحاظ شدن نرخ توکنها در آن فاکتورهای مقیاسبندی رخ میدهد)، اعداد را به پایین گِرد میکند. مهاجمان توانستند از این رفتار نادرست گِرد کردن در ترکیب با قابلیت batchSwap (سوآپ دستهای) سوءاستفاده کنند تا موجودی استخر را دستکاری کرده و ارزش را از آن خارج سازند. در بسیاری از موارد، وجوه اکسپلویت شده پیش از آنکه در تراکنشهای بعدی برداشت شوند، بهعنوان موجودیهای داخلی (Internal Balances) در داخل Vault باقی مانده بودند.
دامنه تأثیر
- استخرهای Composable Stable v5 که پنجرههای زمانی توقف (Pause windows) آنها منقضی شده بود، عمدتاً تحت تأثیر قرار گرفتند.
- استخرهای Composable Stable v6 بهطور خودکار توسط Hypernative متوقف و محافظت شدند.
- بر اساس بازبینی فعلی و بازبینیهای خارجی، Balancer v3 و انواع استخرهای غیرپایدار v2 تحت تأثیر قرار نگرفتهاند.
خسارت
بالانسر در حالی که تحقیقات همچنان فعال است، اولویت را به کاهش خسارت و بازیابی وجوه داده است. برآوردهای خسارت همچنان با هماهنگی تیمهای امنیتی خارجی و پاسخدهندگان کلاهسفید در حال تطبیق و بررسی در زنجیرهها، انواع استخرها و آدرسهای مختلف است.
آنها یک دفتر کل داخلی یکپارچه برای ردیابی موارد زیر نگهداری میکنند:
جریانهای مالی اکسپلویتکننده، نجاتهای انجامشده توسط کلاهسفیدها، داراییهای مسدود شده، وجوه بازیابیشده و برداشتهای پروتکل/کاربر. این موارد بهطور مداوم در حال راستیآزمایی هستند.
مقادیر نهایی تنها پس از اعتبارسنجی چندجانبه (بررسی ردپای آنچین، تأییدیههای شرکا و تطبیق موجودیهای بلوک به بلوک) منتشر خواهند شد.
هرگونه ارقامی که بهصورت عمومی منتشر میشود، تأیید نشده است و نباید رسمی تلقی شود.
گزارش بازبینی تأیید شده از استخرهای آسیبدیده و داراییهای بازیابیشده، شامل متدولوژی و مراجع تراکنشها، در گزارش نهایی پسارویداد منتشر خواهد شد.
اقدامات کاهشی (Mitigations)
علیرغم گستردگی اکسپلویت، چندین واکنش سریع، مجموع زیانها را کاهش داد. موارد زیر نشاندهنده اقدامات تأیید شده است؛ هر جا مبالغی ذکر شده، بهصورت موقت و برای آگاهی از وضعیت است. تمرکز بالانسر همچنان بر کاهش خسارت و بازیابی با هماهنگی فعال شرکا و پاسخدهندگان کلاهسفید است.
- پناهگاه امن کلاهسفید SEAL (BIP-726، اکتبر ۲۰۲۴): چارچوب قانونی از پیش پذیرفتهشده، امکان مداخله سریع و هماهنگ کلاهسفیدها را در طول حادثه فراهم کرد.
- Hypernative — اتوماسیون توقف اضطراری: در ساعت ۰۸:۰۶ UTC، کنترلهای اضطراری CSPv6 فعال شدند؛ تا ساعت ۰۸:۰۷ UTC، تمام استخرهای CSPv6 در شبکههای آسیبدیده متوقف شدند و از گسترش بیشتر خسارت جلوگیری شد.
- تمام استخرهای قابل توقف CSPv6 متوقف و در حالت بازیابی هستند: این شامل استخرهای با ارزش قفل شده پایین (TVL) که زیر آستانههای گزارشدهی قبلی بودند نیز میشود.
- کارخانه (Factory) CSPv6 غیرفعال شد: امکان ایجاد استخرهای آسیبپذیر جدید تا زمان رفع مشکل وجود ندارد.
- Gauges (مربوط به پاداشها) برای استخرهای آسیبدیده متوقف شدند: انتشار توکن و مشوقها برای حفظ BAL و مشوقهای شرکا متوقف شد.
- امکان خروج LPها (تأمینکنندگان نقدینگی) با توقف CSPv6 فراهم شد: LPهای عمده از جمله Crypto.com (۸۰۰ هزار دلار، cdcETH/wstETH) و Ether.fi (۱.۰۶۱ میلیون دلار، eBTC/wBTC) با خیال راحت نقدینگی خود را خارج کردند.
- بازیابی Stakewise: ۵٬۰۴۱ osETH (معادل ۱۹ میلیون دلار) و ۱۳٬۴۹۵ osGNO (حدود ۱.۷ تا ۲ میلیون دلار) بازپسگیری شد (تقریباً ۷۳.۵٪ از osETH به سرقت رفته) تا بهصورت نسبی (pro-rata) به کاربران آسیبدیده بازگردانده شود.
- اعتبارسنجهای (Validators) براجین — توقف شبکه: عملیات زنجیره برای مهار قرار گرفتن در معرض خطر مربوط به Balancer v2 در BEX متوقف شد؛ اقدامات اضطراری هاردفورک آغاز شد.
- Sonic Labs — مسدودسازی اضطراری: Sonic Labs آدرسهای مشکوک مهاجم را مسدود کرد و مانع انتقال یا تبدیل وجوه مرتبط با Beets (فورک Balancer v2 فعال در Sonic) شد.
- بات MEV در شبکه Base — مشارکت در بازیابی: حدود ۱۵۰ هزار دلار در چندین تراکنش بازیابی شد.
- بازیابی کلاهسفید BitFinding: تیم BitFinding حدود ۶۰۰ هزار دلار از داراییها را در شبکه اصلی رهگیری و بازپسگیری کرد.
- Monerium — مسدودسازی EURe: حدود ۱.۳ میلیون EURe در Vault مسدود شد تا از انتقال بیشتر در پلتفرمهای آسیبدیده جلوگیری شود.
- Gnosis — محدودیتهای بریج (Bridge): با هماهنگی تیم Monerium، کنترلهای موقتی برای محدود کردن فعالیت بریج خروجی از زنجیره Gnosis اعمال شد تا ریسک انتشار بین زنجیرهای کاهش یابد.
- ارتباط SEAL و شرکا با مهاجم (مهاجمان): تعامل مستمر تحت چارچوب SEAL برای پیگیری بازگشت وجوه در حال انجام است.
- مشارکتکنندگان کلاهسفید دیگر درگیر شدند: تیمهای متعددی (از جمله SNP و دیگران) در حال کمک به تریاژ (اولویتبندی)، نجات و مسیرهای بازگشت وجوه هستند.
بالانسر به همکاری با شرکا، محققان، صرافیها و تیمهای کلاهسفید برای بازیابی وجوه ادامه میدهد. گزارش جامع پسارویداد با مجموع مبالغ اعتبارسنجیشده، مراجع تراکنشها و جریانهای بازیابی/توزیع پس از تکمیل اعتبارسنجی و تطبیق توسط شرکا منتشر خواهد شد.
انجام سوآپ و عملیات نقدینگی در استخرهای تأثیرناپذیر کاملاً امن باقی میماند. وکتور اکسپلویت فقط برای انواع خاصی از استخرهای پایدار ترکیبی در Balancer v2 اعمال میشود. Balancer V3 و تمام انواع دیگر استخرهای V2 تحت تأثیر قرار نگرفتهاند و بهطور عادی به کار خود ادامه میدهند.
برای کاربران در استخرهای متوقفشده Composable Stable v6، حالت بازیابی (Recovery Mode) فعال شده است تا امکان برداشت متناسب داراییهای پایه فراهم شود. استخرهای Composable Stable v5 تحت تأثیر قرار گرفتهاند و همچنان تحت بررسی هستند؛ کاربران باید از تعامل با این قراردادها تا زمان صدور تأییدیه رسمی خودداری کنند.
تمام اطلاعات و دستورالعملهای تأیید شده منحصراً از طریق کانالهای رسمی Balancer به اشتراک گذاشته خواهد شد.
بازیابی جاری و گامهای بعدی
بازیابی و تطبیق (Reconciliation) در سراسر شبکههای آسیبدیده با شرکای امنیتی، بازبینان و پاسخدهندگان کلاهسفید ادامه دارد. ردیابی داراییها و بازیابی قانونی بهطور مشترک توسط SEAL و zeroShadow تحت چارچوب پناهگاه امن Balancer هدایت میشود تا مسیری شفاف و قانونی برای بازگشت وجوه تضمین شود.
گزارش نهایی پسارویداد شامل موارد زیر خواهد بود:
- علت ریشهای و توضیح در سطح اینواریانت (invariant).
- ترتیب اقدامات کاهشی و متدولوژی بازیابی.
- نتایج اعتبارسنجی و تطبیق توسط شرکا.
- توصیههایی برای مهاجرت به Balancer V3.
تحلیل فنی اولیه
در تاریخ ۳ نوامبر ۲۰۲۵، پروتکل Balancer توسط هکرها در چندین زنجیره عمومی مانند آربیتروم و اتریوم مورد حمله قرار گرفت که منجر به زیان ۱۲۰ میلیون دلاری داراییها شد. هسته این حمله ناشی از یک آسیبپذیری دوگانهی از دست رفتن دقت (Precision loss) و دستکاری اینواریانت (Invariant manipulation) بود.
مشکل اصلی در این حمله به منطق پروتکل برای مدیریت تراکنشهای کوچک بازمیگردد. زمانی که کاربر یک مبادله کوچک انجام میدهد، پروتکل تابع _upscaleArray را فراخوانی میکند که از mulDown برای گرد کردن مقدار به پایین استفاده میکند. اگر موجودی در تراکنش و مقدار ورودی هر دو در یک مرز گرد کردن خاصی (مانند محدوده ۸ تا ۹ wei) قرار گیرند، خطای دقت نسبی قابل توجهی رخ میدهد.
خطای دقت به محاسبه مقدار اینواریانت (D) در پروتکل انتشار یافت و باعث شد که مقدار D بهطور غیرعادی کاهش یابد. تغییر در مقدار D مستقیماً قیمت BPT (توکن استخر Balancer) را در پروتکل Balancer کاهش داد. هکرها از این قیمت سرکوبشده BPT سوءاستفاده کردند تا از طریق مسیرهای تراکنشی از پیش طراحیشده، آربیتراژ را تکمیل کنند و در نهایت باعث زیان عظیم دارایی شوند.
لینک اکسپلویت آسیبپذیری:
https://etherscan.io/tx/0x6ed07db1a9fe5c0794d44cd36081d6a6df103fab868cdd75d581e3bd23bc9742
لینک انتقال دارایی:
https://etherscan.io/tx/0xd155207261712c35fa3d472ed1e51bfcd816e616dd4f517fa5959836f5b48569
تحلیل فنی
نقطه ورود حمله، قرارداد Balancer: Vault (خزانه) و تابع ورودی متناظر، تابع batchSwap است که در داخل خود onSwap را برای انجام مبادلات توکن فراخوانی میکند.
function onSwap(
SwapRequest memory swapRequest,
uint256[] memory balances,
uint256 indexIn,
uint256 indexOut
) external override onlyVault(swapRequest.poolId) returns (uint256) {
_beforeSwapJoinExit();
_validateIndexes(indexIn, indexOut, _getTotalTokens());
uint256[] memory scalingFactors = _scalingFactors();
return
swapRequest.kind == IVault.SwapKind.GIVEN_IN
? _swapGivenIn(swapRequest, balances, indexIn, indexOut, scalingFactors)
: _swapGivenOut(swapRequest, balances, indexIn, indexOut, scalingFactors);
}
از پارامترها و محدودیتهای تابع، میتوان به چند اطلاعات دست یافت:
- مهاجمان باید این تابع را از طریق Vault فراخوانی کنند؛ آنها نمیتوانند مستقیماً آن را فراخوانی کنند.
- تابع در داخل خود
_scalingFactors()را برای به دست آوردن فاکتورهای مقیاسبندی جهت عملیات مقیاسبندی فراخوانی میکند. - عملیات مقیاسبندی یا در
_swapGivenInیا_swapGivenOutانجام میشود.
تحلیل الگوی حمله
در مدل استخر پایدار Balancer، قیمت BPT یک نقطه مرجع مهم است که تعیین میکند کاربر چه تعداد BPT دریافت میکند و به ازای هر BPT چه مقدار دارایی دریافت میشود.
قیمت BPT = D / totalSupply
که در آن D = اینواریانت (invariant)، برگرفته از مدل StableSwap پلتفرم Curve.
در محاسبه مبادله استخر:
// StableMath._calcOutGivenIn
function _calcOutGivenIn(
uint256 amplificationParameter,
uint256[] memory balances,
uint256 tokenIndexIn,
uint256 tokenIndexOut,
uint256 tokenAmountIn,
uint256 invariant
) internal pure returns (uint256) {
/**********************************************************************************************************
// outGivenIn token x for y - polynomial equation to solve //
// ay = amount out to calculate //
// by = balance token out //
// y = by - ay (finalBalanceOut) //
// D = invariant DD^(n+1) //
// A = amplification coefficient y^2 + ( S + ---------- - D) * y - ------------- = 0 //
// n = number of tokens (A * n^n) A * n^2n * P //
// S = sum of final balances but y //
// P = product of final balances but y //
**************************************************************************************************************/
// Amount out, so we round down overall.
balances[tokenIndexIn] = balances[tokenIndexIn].add(tokenAmountIn);
uint256 finalBalanceOut = _getTokenBalanceGivenInvariantAndAllOtherBalances(
amplificationParameter,
balances,
invariant, // با استفاده از D قدیمی
tokenIndexOut
);
// No need to use checked arithmetic since `tokenAmountIn` was actually added to the same balance right before
// calling `_getTokenBalanceGivenInvariantAndAllOtherBalances` which doesn't alter the balances array.
balances[tokenIndexIn] = balances[tokenIndexIn] - tokenAmountIn;
return balances[tokenIndexOut].sub(finalBalanceOut).sub(1);
}
بخشی که بهعنوان معیار قیمت BPT عمل میکند، مقدار ثابت D است؛ یعنی دستکاری قیمت BPT نیازمند دستکاری D است. بیایید فرآیند محاسبه D را تحلیل کنیم:
// StableMath._calculateInvariant
function _calculateInvariant(uint256 amplificationParameter, uint256[] memory balances)
internal
pure
returns (uint256)
{
/**********************************************************************************************
// invariant //
// D = invariant D^(n+1) //
// A = amplification coefficient A n^n S + D = AD n^n + ----------- //
// S = sum of balances n^n P //
// P = product of balances //
// n = number of tokens //
**********************************************************************************************/
// همیشه به پایین گرد کن، تا با محاسبات Vyper (که همیشه کوتاه میکند) مطابقت داشته باشد.
uint256 sum = 0; // S in the Curve version
uint256 numTokens = balances.length;
for (uint256 i = 0; i < numTokens; i++) {
sum = sum.add(balances[i]); // balances مقدار مقیاسبندی شده است
}
if (sum == 0) {
return 0;
}
uint256 prevInvariant; // Dprev in the Curve version
uint256 invariant = sum; // D in the Curve version
uint256 ampTimesTotal = amplificationParameter * numTokens; // Ann in the Curve version
// محاسبه تکراری D...
// محاسبه D بر دقت balances تأثیر میگذارد
for (uint256 i = 0; i < 255; i++) {
uint256 D_P = invariant;
for (uint256 j = 0; j < numTokens; j++) {
// (D_P * invariant) / (balances[j] * numTokens)
D_P = Math.divDown(Math.mul(D_P, invariant), Math.mul(balances[j], numTokens));
}
prevInvariant = invariant;
invariant = Math.divDown(
Math.mul(
// (ampTimesTotal * sum) / AMP_PRECISION + D_P * numTokens
(Math.divDown(Math.mul(ampTimesTotal, sum), _AMP_PRECISION).add(Math.mul(D_P, numTokens))),
invariant
),
// ((ampTimesTotal - _AMP_PRECISION) * invariant) / _AMP_PRECISION + (numTokens + 1) * D_P
(
Math.divDown(Math.mul((ampTimesTotal - _AMP_PRECISION), invariant), _AMP_PRECISION).add(
Math.mul((numTokens + 1), D_P)
)
)
);
if (invariant > prevInvariant) {
if (invariant - prevInvariant <= 1) {
return invariant;
}
} else if (prevInvariant - invariant <= 1) {
return invariant;
}
}
_revert(Errors.STABLE_INVARIANT_DIDNT_CONVERGE);
}
در کد بالا، محاسبه D به آرایه balances (موجودیهای) مقیاسبندیشده بستگی دارد. این بدان معناست که عملیاتی برای تغییر دقت این موجودیها مورد نیاز است که منجر به خطا در محاسبه D میشود.
علت ریشهای از دست رفتن دقت
// BaseGeneralPool._swapGivenIn
function _swapGivenIn(
SwapRequest memory swapRequest,
uint256[] memory balances,
uint256 indexIn,
uint256 indexOut,
uint256[] memory scalingFactors
) internal virtual returns (uint256) {
// Fees are subtracted before scaling, to reduce the complexity of the rounding direction analysis.
swapRequest.amount = _subtractSwapFeeAmount(swapRequest.amount);
_upscaleArray(balances, scalingFactors); // نکته کلیدی: افزایش مقیاس موجودی
swapRequest.amount = _upscale(swapRequest.amount, scalingFactors[indexIn]);
uint256 amountOut = _onSwapGivenIn(swapRequest, balances, indexIn, indexOut);
// توکنهای amountOut در حال خروج از استخر هستند، بنابراین به پایین گرد میکنیم.
return _downscaleDown(amountOut, scalingFactors[indexOut]);
}
عملیات مقیاسبندی:
// ScalingHelpers.sol
function _upscaleArray(uint256[] memory amounts, uint256[] memory scalingFactors) pure {
uint256 length = amounts.length;
InputHelpers.ensureInputLengthMatch(length, scalingFactors.length);
for (uint256 i = 0; i < length; ++i) {
amounts[i] = FixedPoint.mulDown(amounts[i], scalingFactors[i]); // گرد کردن به پایین
}
}
// FixedPoint.mulDown
function mulDown(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 product = a * b;
_require(a == 0 || product / a == b, Errors.MUL_OVERFLOW);
return product / ONE; // گرد کردن به پایین: کوتاه کردن مستقیم
}
همانطور که در بالا نشان داده شده است، هنگام استفاده از _upscaleArray، اگر موجودی بسیار کوچک باشد (مثلاً ۸-۹ wei)، گرد کردن به پایین mulDown منجر به از دست رفتن قابل توجه دقت میشود.
جزئیات فرآیند حمله
فاز ۱: تنظیم در مرز گرد کردن
- مهاجم: BPT → cbETH
- هدف: تنظیم موجودی cbETH در مرز گرد کردن (مثلاً ختم شدن به ۹).
فرض کنید حالت اولیه:
موجودی cbETH (اصلی): ...00000000009 wei (رقم آخر ۹ است)
فاز ۲: فعالسازی از دست رفتن دقت (آسیبپذیری اصلی)
- مهاجم: wstETH (8 wei) → cbETH
قبل از مقیاسبندی:
موجودی cbETH: ...000000000009 weiورودی wstETH: 8 wei
اجرای _upscaleArray:
// مقیاسبندی cbETH: 9 * 1e18 / 1e18 = 9
// اما اگر مقدار واقعی ۹.۵ باشد، به دلیل گرد کردن به پایین، ۹ میشود.
scaled_cbETH = floor(9.5) = 9
// از دست رفتن دقت: ۰.۵ / ۹.۵ = ۵.۳٪ خطای نسبی
// محاسبه مبادله:
// ورودی (wstETH): 8 wei (مقیاسبندی شده)
// موجودی (cbETH): 9 (نادرست، باید ۹.۵ باشد)
// از آنجا که cbETH کم ارزشگذاری شده، موجودی جدید محاسبهشده نیز کم ارزشگذاری میشود،
// که منجر به خطا در محاسبه D میگردد.
D_original = f(9.5, ...)
D_new = f(9, ...) < D_original
فاز ۳: سود بردن از قیمت سرکوبشده BPT
- مهاجم: دارایی پایه → BPT
در این زمان:
D_new = D_original - ΔDقیمت BPT = D_new / totalSupply < D_original / totalSupply
مهاجمان با داراییهای پایه کمتر، همان تعداد BPT را به دست میآورند.
یا داراییهای پایه یکسان را با BPTهای بیشتری مبادله میکنند.
مهاجم در بالا از Batch Swap (سوآپ دستهای) برای انجام چندین سوآپ در یک تراکنش واحد استفاده کرد:
- مبادله اول: BPT → cbETH (تنظیم موجودی)
- مبادله دوم: wstETH (8) → cbETH (فعالسازی از دست رفتن دقت)
- مبادله سوم: داراییهای پایه → BPT (کسب سود)
این سوآپها همگی در یک تراکنش batchSwap قرار دارند و از وضعیت موجودی یکسانی استفاده میکنند، اما _upscaleArray برای تغییر آرایه balances برای هر سوآپ فراخوانی میشود.
فقدان مکانیزم بازخوانی (Callback)
فرآیند اصلی توسط Vault آغاز میشود، پس چگونه این امر منجر به انباشت از دست رفتن دقت میشود؟ پاسخ در مکانیزم انتقال آرایه balances نهفته است.
// تابع منطقی زمانی که Vault تابع onSwap را فراخوانی میکند: _processGeneralPoolSwapRequest
private
returns (uint256 amountCalculated)
{
// ... (بخشهایی از کد برای خواندن موجودیها)
uint256 tokenAmount = poolBalances.length();
uint256[] memory currentBalances = new uint256[](tokenAmount);
request.lastChangeBlock = 0;
for (uint256 i = 0; i < tokenAmount; i++) {
// ...
bytes32 balance = poolBalances.unchecked_valueAt(i);
currentBalances[i] = balance.total(); // خواندن از حافظه (storage)
request.lastChangeBlock = Math.max(request.lastChangeBlock, balance.lastChangeBlock());
// ... (بخشهایی از کد)
}
// انجام سوآپ
// فراخوانی بازخوانی (callback) درخواست سوآپ و محاسبه موجودیهای جدید برای 'token in' و 'token out' پس از سوآپ
amountCalculated = pool.onSwap(request, currentBalances, indexIn, indexOut);
(uint256 amountIn, uint256 amountOut) = _getAmounts(request.kind, request.amount, amountCalculated);
tokenInBalance = tokenInBalance.increaseCash(amountIn);
tokenOutBalance = tokenOutBalance.decreaseCash(amountOut);
// بهروزرسانی حافظه (storage)
poolBalances.unchecked_setAt(indexIn, tokenInBalance);
poolBalances.unchecked_setAt(indexOut, tokenOutBalance);
}
با تحلیل کد بالا، اگرچه Vault هر بار که onSwap فراخوانی میشود، یک آرایه currentBalances جدید ایجاد میکند، اما در Batch Swap:
- پس از مبادله اول، موجودی بهروزرسانی میشود (اما مقدار بهروز شده ممکن است به دلیل از دست رفتن دقت، نادرست باشد).
- مبادله دوم، محاسبه را بر اساس نتیجه مبادله اول ادامه میدهد.
- از دست رفتن انباشته دقت در نهایت منجر به کاهش قابل توجه مقدار اینواریانت D میشود.
مسائل کلیدی:
// BaseGeneralPool._swapGivenIn
function _swapGivenIn(
SwapRequest memory swapRequest,
uint256[] memory balances,
uint256 indexIn,
uint256 indexOut,
uint256[] memory scalingFactors
) internal virtual returns (uint256) {
// ...
_upscaleArray(balances, scalingFactors); // تغییر آرایه درجا (in place).
// ...
uint256 amountOut = _onSwapGivenIn(swapRequest, balances, indexIn, indexOut);
// ...
}
اگرچه Vault هر بار یک آرایه جدید ارسال میکند، اما:
۱. اگر موجودی بسیار کوچک باشد (۸-۹ wei)، از دست رفتن دقت در طول مقیاسبندی قابل توجه است.
۲. در Batch Swap، سوآپهای بعدی محاسبات را بر اساس موجودی که قبلاً دقت خود را از دست داده ادامه میدهند.
۳. بررسی نشد که آیا تغییر در مقدار اینواریانت D در محدوده معقولی بوده است یا خیر.
سخن پایانی
دلایل حمله به Balancer را میتوان به شرح زیر خلاصه کرد:
۱. تابع مقیاسبندی از گرد کردن به پایین استفاده میکند: _upscaleArray از mulDown برای مقیاسبندی استفاده میکند که هنگام بسیار کوچک بودن موجودی (مانند ۸-۹ wei) باعث از دست رفتن قابل توجه دقت نسبی میشود.
۲. محاسبه مقدار اینواریانت به دقت حساس است: محاسبه مقدار اینواریانت D به آرایه balances مقیاسبندیشده بستگی دارد و از دست رفتن دقت مستقیماً به محاسبه D منتقل میشود و آن را کوچکتر میکند.
۳. فقدان اعتبارسنجی تغییرات در مقادیر اینواریانت: در طول فرآیند مبادله، بررسی نشد که آیا تغییرات در مقدار اینواریانت D در محدوده معقولی قرار دارد یا خیر که این امر به مهاجمان اجازه داد تا بهطور مکرر از خطای دقت برای کاهش قیمت BPT سوءاستفاده کنند.
۴. انباشت از دست رفتن دقت در سوآپ دستهای (Batch Swap): در یک batchSwap واحد، از دست رفتن دقت ناشی از چندین سوآپ انباشته شده و در نهایت به زیانهای مالی هنگفت تبدیل میشود.
این دو مسئله (از دست رفتن دقت و فقدان اعتبارسنجی) در ترکیب با طراحی دقیق شرایط مرزی توسط مهاجم، منجر به این زیان شد.















