คำเตือนความเสี่ยง: ระวังความเสี่ยงจากการระดมทุนที่ผิดกฎหมายในนาม 'สกุลเงินเสมือน' 'บล็อกเชน' — จากห้าหน่วยงานรวมถึงคณะกรรมการกำกับดูแลการธนาคารและการประกันภัย
ข่าวสาร
ค้นพบ
ค้นหา
เข้าสู่ระบบ
简中
繁中
English
日本語
한국어
ภาษาไทย
Tiếng Việt
BTC
ETH
HTX
SOL
BNB
ดูตลาด
Balancer ถูกแฮ็ก การวิเคราะห์ช่องโหว่
ExVul Security
特邀专栏作者
@exvulsec
2025-11-04 08:19
บทความนี้มีประมาณ 11217 คำ การอ่านทั้งหมดใช้เวลาประมาณ 17 นาที
เมื่อวันที่ 3 พฤศจิกายน 2568 โปรโตคอล Balancer ถูกแฮ็กเกอร์โจมตีบนเครือข่ายสาธารณะหลายแห่ง รวมถึง Arbitrum และ Ethereum ส่งผลให้สูญเสียสินทรัพย์มูลค่า 120 ล้านดอลลาร์สหรัฐ แก่นของการโจมตีเกิดจากช่องโหว่สองประการ ได้แก่ การสูญเสียความแม่นยำและการจัดการค่าคงที่ ปัญหาสำคัญของการโจมตีครั้งนี้อยู่ที่ตรรกะของโปรโตคอลในการจัดการธุรกรรมขนาดเล็ก

คำนำ

เมื่อวันที่ 3 พฤศจิกายน 2568 โปรโตคอล Balancer ถูกโจมตีโดยแฮกเกอร์บนเครือข่ายสาธารณะหลายแห่ง เช่น Arbitrum และ Ethereum ส่งผลให้สูญเสียสินทรัพย์มูลค่า 120 ล้านดอลลาร์สหรัฐฯ แก่นของการโจมตีเกิดจากช่องโหว่สองประการ ได้แก่ การสูญเสียความแม่นยำ (precision loss) และการจัดการที่ไม่แปรเปลี่ยน (invariant manipulation)

ประเด็นสำคัญของการโจมตีครั้งนี้อยู่ที่ตรรกะของโปรโตคอลในการจัดการธุรกรรมขนาดเล็ก เมื่อผู้ใช้ทำการแลกเปลี่ยนมูลค่าเล็กน้อย โปรโตคอลจะเรียกใช้ฟังก์ชัน _upscaleArray ซึ่งใช้ mulDown เพื่อปัดเศษค่าลง หากยอดคงเหลือในธุรกรรมและจำนวนเงินที่ป้อนเข้ามาอยู่ในขอบเขตการปัดเศษที่กำหนด (เช่น ช่วง 8-9 wei) จะเกิดข้อผิดพลาดด้านความแม่นยำสัมพัทธ์อย่างมีนัยสำคัญ

ข้อผิดพลาดด้านความแม่นยำที่แพร่กระจายไปยังการคำนวณค่าคงที่ D ในโปรโตคอลทำให้ค่า D ลดลงอย่างผิดปกติ การเปลี่ยนแปลงของค่า D ส่งผลให้ราคาของ BPT (Balancer Pool Token) ในโปรโตคอล Balancer ลดลงโดยตรง แฮกเกอร์ใช้ประโยชน์จากราคา BPT ที่ตกต่ำนี้เพื่อดำเนินการอาร์บิทราจผ่านเส้นทางธุรกรรมที่ออกแบบไว้ล่วงหน้า ซึ่งท้ายที่สุดแล้วทำให้เกิดการสูญเสียสินทรัพย์จำนวนมหาศาล

ลิงก์ช่องโหว่: https://etherscan.io/tx/0x6ed07db1a9fe5c0794d44cd36081d6a6df103fab868cdd75d581e3bd23bc9742

ลิงก์โอนสินทรัพย์: https://etherscan.io/tx/0xd155207261712c35fa3d472ed1e51bfcd816e616dd4f517fa5959836f5b48569

การวิเคราะห์ทางเทคนิค

จุดเข้าโจมตี

จุดเข้าโจมตีคือ สัญญา Balancer: Vault และฟังก์ชันการเข้าที่สอดคล้องกันคือฟังก์ชัน batchSwap ซึ่งเรียก onSwap ภายในเพื่อดำเนินการสลับโทเค็น

 ฟังก์ชัน onSwap(
 SwapRequest หน่วยความจำ swapRequest
 uint256[] การปรับสมดุลหน่วยความจำ
 ดัชนี uint256 ใน
 ดัชนี uint256 ออก
 ) การแทนที่ภายนอก onlyVault (swapRequest.poolId) ส่งคืน (uint256) {
 _ก่อนSwapJoinExit();

 _validateIndexes(indexIn, indexOut, _getTotalTokens());
 uint256[] หน่วยความจำ scalingFactors = _scalingFactors();

 กลับ
 คำขอ swap.kind == IVault.SwapKind.GIVEN_IN
 ? _swapGivenIn(คำขอแลกเปลี่ยน, ยอดคงเหลือ, ดัชนีเข้า, ดัชนีออก, ปัจจัยการปรับขนาด)
 : _swapGivenOut(swapRequest, ยอดคงเหลือ, indexIn, indexOut, scalingFactors);
 -

จากพารามิเตอร์และข้อจำกัดของฟังก์ชัน เราสามารถรับข้อมูลได้หลายส่วน:

  1. ผู้โจมตีจำเป็นต้องเรียกใช้ฟังก์ชันนี้ผ่าน Vault พวกเขาไม่สามารถเรียกใช้โดยตรงได้
  2. ฟังก์ชันภายในจะเรียก _scalingFactors() เพื่อรับปัจจัยการปรับขนาดสำหรับการดำเนินการปรับขนาด
  3. การดำเนินการปรับขนาดได้รับการจัดการใน _swapGivenIn หรือ _swapGivenOut

การวิเคราะห์รูปแบบการโจมตี

วิธีการคำนวณราคา BPT

ในโมเดลพูลเสถียรของ Balancer ราคาของ BPT ถือเป็นจุดอ้างอิงที่สำคัญ ซึ่งกำหนดว่าผู้ใช้จะได้รับ BPT กี่รายการ และจะได้รับสินทรัพย์กี่รายการต่อ BPT

 ราคา BPT = D / อุปทานรวม

โดยที่ D = ค่าคงที่ จากแบบจำลอง StableSwap ของ Curve

ในการคำนวณการแลกเปลี่ยนพูล:

 // StableMath._calcOut ที่ได้รับ
 ฟังก์ชัน _calcOutGivenIn(
 พารามิเตอร์การขยาย uint256
 uint256[] การปรับสมดุลหน่วยความจำ
 uint256 โทเค็นดัชนีใน
 uint256 โทเค็นดัชนีออก
 uint256 โทเค็นจำนวนใน
 uint256 ไม่แปรเปลี่ยน
 ) ผลตอบแทนภายในที่บริสุทธิ์ (uint256) {
 -
 // outGivenIn โทเค็น x สำหรับ y - สมการพหุนามที่จะแก้ //
 // ay = จำนวนเงินที่ต้องคำนวณ //
 // โดย = ยอดคงเหลือโทเค็นออก //
 // y = โดย - ay (finalBalanceOut) //
 // D = คงที่ DD^(n+1) //
 // A = ค่าสัมประสิทธิ์การขยาย y^2 + ( S + ---------- - D) * y - ------------- = 0 //
 // n = จำนวนโทเค็น (A * n^n) A * n^2n * P //
 // S = ผลรวมของยอดคงเหลือสุดท้าย แต่ y //
 // P = ผลคูณของยอดคงเหลือสุดท้าย แต่ y //
 -

 // จำนวนเงินออกแล้ว เราจึงปัดเศษลงโดยรวม
 balances[tokenIndexIn] = balances[tokenIndexIn].add(tokenAmountIn);

 uint256 finalBalanceOut = _getTokenBalanceGivenInvariantAndAllOtherBalances(
 พารามิเตอร์การขยายสัญญาณ
 ยอดคงเหลือ
 ไม่แปรเปลี่ยน, // ใช้ D เก่า
 โทเค็นอินเด็กซ์เอาท์
 -

 // ไม่จำเป็นต้องใช้เลขคณิตที่ตรวจสอบแล้ว เนื่องจาก `tokenAmountIn` ถูกเพิ่มลงในยอดคงเหลือเดียวกันก่อนหน้านั้นแล้ว
 // เรียก `_getTokenBalanceGivenInvariantAndAllOtherBalances` ซึ่งจะไม่เปลี่ยนแปลงอาร์เรย์ยอดคงเหลือ
 balances[tokenIndexIn] = balances[tokenIndexIn] - tokenAmountIn;

 คืนยอดคงเหลือ[tokenIndexOut].sub(finalBalanceOut).sub(1);
 -

ส่วนที่ใช้เป็น เกณฑ์มาตรฐานสำหรับราคา BPT คือ ค่าคงที่ D กล่าวคือ การควบคุมราคา BPT จำเป็นต้องมีการควบคุม D มาวิเคราะห์กระบวนการคำนวณของ D กัน:

 // StableMath._คำนวณค่าคงที่
 ฟังก์ชัน _calculateInvariant(uint256 amplificationParameter, uint256[] memory balances)
 ภายใน
 บริสุทธิ์
 ผลตอบแทน (uint256)
 -
 -
 // ไม่แปรเปลี่ยน //
 // D = ค่าคงที่ D^(n+1) //
 // A = ค่าสัมประสิทธิ์การขยาย A n^n S + D = AD n^n + ----------- //
 // S = ผลรวมของยอดคงเหลือ n^n P //
 // P = ผลคูณของยอดคงเหลือ //
 // n = จำนวนโทเค็น //
 -

 // ปัดลงเสมอ เพื่อให้ตรงกับเลขคณิตของ Vyper (ซึ่งจะปัดเศษเสมอ)

 uint256 sum = 0; // S ในเวอร์ชัน Curve
 uint256 numTokens = ยอดคงเหลือ.ความยาว;
 สำหรับ (uint256 i = 0; i < numTokens; i++) {
 sum = sum.add(balances[i]); // balances คือค่าที่ปรับขนาดแล้ว}
 ถ้า (ผลรวม == 0) {
 กลับ 0;
 -

 uint256 prevInvariant; // Dprev ในเวอร์ชัน Curve
 uint256 invariant = sum; // D ในเวอร์ชัน Curve
 uint256 ampTimesTotal = amplificationParameter * numTokens; // Ann ในเวอร์ชัน Curve

 // การคำนวณแบบวนซ้ำของ D...
 // การคำนวณ D ส่งผลต่อความแม่นยำของเครื่องชั่งสำหรับ (uint256 i = 0; i < 255; i++) {
 uint256 D_P = ไม่แปรเปลี่ยน;

 สำหรับ (uint256 j = 0; j < numTokens; j++) {
 // (D_P * ไม่แปรเปลี่ยน) / (สมดุล[j] * numTokens)
 D_P = Math.divDown(Math.mul(D_P, ไม่แปรเปลี่ยน), Math.mul(ยอดคงเหลือ[j], numTokens));
 -

 prevInvariant = ไม่แปรเปลี่ยน;

 ไม่แปรเปลี่ยน = Math.divDown(
 คณิตศาสตร์.mul(
 // (ampTimesTotal * ผลรวม) / AMP_PRECISION + D_P * numTokens
 (Math.divDown(Math.mul(ampTimesTotal, sum), _AMP_PRECISION).add(Math.mul(D_P, numTokens))),
 ไม่แปรเปลี่ยน
 -
 // ((ampTimesTotal - _AMP_PRECISION) * ไม่แปรเปลี่ยน) / _AMP_PRECISION + (numTokens + 1) * D_P
 -
 Math.divDown(Math.mul((ampTimesTotal - _AMP_PRECISION), ไม่แปรเปลี่ยน), _AMP_PRECISION).add(
 คณิตศาสตร์.mul((จำนวนโทเค็น + 1), D_P)
 -
 -
 -

 ถ้า (ไม่แปรเปลี่ยน > ตัวแปรก่อนหน้า) {
 ถ้า (ไม่แปรเปลี่ยน - ตัวแปรก่อนหน้า <= 1) {
 กลับค่าคงที่;
 -
 } มิฉะนั้น ถ้า prevInvariant - invariant <= 1 {
 กลับค่าคงที่;
 -
 -

 _revert(ข้อผิดพลาด.STABLE_INVARIANT_DIDNT_CONVERGE);
 -

ในโค้ดด้านบน การคำนวณค่า D ขึ้นอยู่กับอาร์เรย์ของเครื่องชั่งแบบปรับสเกล ซึ่งหมายความว่าจำเป็นต้องมีการดำเนินการเพื่อเปลี่ยนความแม่นยำของเครื่องชั่งเหล่านี้ ซึ่งนำไปสู่ข้อผิดพลาดในการคำนวณค่า D

สาเหตุหลักของการสูญเสียความแม่นยำ

 // BaseGeneralPool._swap ที่ได้รับ
 ฟังก์ชัน _swapGivenIn(
 SwapRequest หน่วยความจำ swapRequest
 uint256[] การปรับสมดุลหน่วยความจำ
 ดัชนี uint256 ใน
 uint256 indexOut,
 uint256[] ปัจจัยการปรับขนาดหน่วยความจำ
 ) คืนค่าเสมือนภายใน (uint256) {
 // ค่าธรรมเนียมจะถูกหักออกก่อนการปรับขนาด เพื่อลดความซับซ้อนของการวิเคราะห์ทิศทางการปัดเศษ
 swapRequest.amount = _subtractSwapFeeAmount(จำนวนเงิน swapRequest);

 _upscaleArray(ยอดคงเหลือ, scalingFactors); // คีย์: อัปสเกลยอดคงเหลือ swapRequest.amount = _upscale(swapRequest.amount, scalingFactors[indexIn]);

 uint256 amountOut = _onSwapGivenIn(swapRequest, balances, indexIn, indexOut);

 // โทเค็น amountOut กำลังออกจาก Pool ดังนั้นเราจึงปัดเศษลง
 ส่งคืน _downscaleDown(amountOut, scalingFactors[indexOut]);
 -

การดำเนินการปรับขนาด:

 // ScalingHelpers.sol
ฟังก์ชัน _upscaleArray(uint256[] จำนวนหน่วยความจำ, uint256[] ปัจจัยการปรับขนาดหน่วยความจำ) บริสุทธิ์ {
 uint256 ความยาว = จำนวน.ความยาว;
 InputHelpers.ensureInputLengthMatch(ความยาว, ปัจจัยการปรับขนาด.ความยาว);

 สำหรับ (uint256 i = 0; i < ความยาว; ++i) {
 amounts[i] = FixedPoint.mulDown(amounts[i], scalingFactors[i]); // ปัดเศษลง}
-

// จุดคงที่.mulDown
 ฟังก์ชัน mulDown(uint256 a, uint256 b) คืนค่าภายในบริสุทธิ์ (uint256) {
 uint256 ผลิตภัณฑ์ = a * b;
 _require(a == 0 || ผลิตภัณฑ์ / a == b, Errors.MUL_OVERFLOW);

 คืนผลิตภัณฑ์ / หนึ่ง; // ปัดเศษลง: ตัดทอนโดยตรง}

ดังที่แสดงไว้ด้านบน เมื่อใช้ _upscaleArray หากยอดคงเหลือมีขนาดเล็กมาก (เช่น 8-9 wei) การปัดเศษลงของ mulDown จะทำให้สูญเสียความแม่นยำอย่างมาก

รายละเอียดกระบวนการโจมตี

เฟส 1: ปรับให้เข้ากับขอบเขตการปัดเศษ

 ผู้โจมตี: BPT → cbETH
วัตถุประสงค์: เพื่อปรับยอดคงเหลือ cbETH ให้เป็นขอบเขตการปัดเศษ (เช่น ลงท้ายด้วย 9)

สมมติว่าสถานะเริ่มต้น:
 ยอดคงเหลือ cbETH (เดิม): ...00000000009 wei (หลักสุดท้ายคือ 9)

ระยะที่ 2: การกระตุ้นการสูญเสียความแม่นยำ (ช่องโหว่หลัก)

 ผู้โจมตี: wstETH (8 wei) → cbETH

ก่อนการปรับขนาด:
 cbETH ยอดคงเหลือ: ...000000000009 wei 
 อินพุต wstETH: 8 wei

ดำเนินการ _upscaleArray:
 // การปรับมาตราส่วน cbETH: 9 * 1e18 / 1e18 = 9
 // แต่ถ้าค่าจริงเป็น 9.5 ก็จะกลายเป็น 9 เนื่องจากการปัดเศษลง
 scaled_cbETH = floor(9.5) = 9
 
 การสูญเสียความแม่นยำ: 0.5 / 9.5 = 5.3% การคำนวณข้อผิดพลาดสัมพัทธ์การแลกเปลี่ยน:
 อินพุต (wstETH): 8 wei (ปรับขนาด)
 ยอดคงเหลือ (cbETH): 9 (ไม่ถูกต้อง ควรเป็น 9.5)
 
 เนื่องจาก cbETH ถูกประเมินค่าต่ำเกินไป ยอดคงเหลือใหม่ที่คำนวณได้ก็จะประเมินค่าต่ำเกินไปเช่นกัน ทำให้เกิดข้อผิดพลาดในการคำนวณ D
 D_ต้นฉบับ = f(9.5, ...)
 D_ใหม่ = f(9, ...) < D_ต้นฉบับ

ระยะที่ 3: ทำกำไรจากราคา BPT ที่ตกต่ำ

 ผู้โจมตี: สินทรัพย์ที่อยู่ข้างใต้ → BPT

ในเวลานี้:
 D_new = D_original - ΔD
 ราคา BPT = D_new / totalSupply < D_original / totalSupply
 
ผู้โจมตีจะได้รับ BPT จำนวนเท่าเดิมโดยมีสินทรัพย์พื้นฐานน้อยลง
หรือแลกเปลี่ยนสินทรัพย์อ้างอิงเดียวกันเป็น BPT เพิ่มเติม

ผู้โจมตีข้างต้นใช้ Batch Swap เพื่อดำเนินการสลับหลายครั้งภายในธุรกรรมเดียว:

  1. การแลกเปลี่ยนครั้งแรก: BPT → cbETH (ปรับยอดคงเหลือ)
  2. การสลับครั้งที่สอง: wstETH (8) → cbETH (ทริกเกอร์การสูญเสียความแม่นยำ)
  3. ตลาดหลักทรัพย์ที่สาม: สินทรัพย์อ้างอิง → BPT (กำไร)

การสลับเหล่านี้จะอยู่ในธุรกรรมการสลับแบบแบตช์เดียวกันและ แบ่งปันสถานะสมดุลเดียวกัน แต่ _upscaleArray จะถูกเรียกเพื่อปรับเปลี่ยนอาร์เรย์สมดุลสำหรับการสลับแต่ละครั้ง

การขาดกลไกการโทรกลับ

กระบวนการหลักเริ่มต้นโดย Vault แล้วสิ่งนี้จะนำไปสู่การสะสมของการสูญเสียความแม่นยำได้อย่างไร คำตอบอยู่ที่ กลไกการส่งผ่านของอาร์เรย์สมดุล

 // ฟังก์ชันลอจิกเมื่อ Vault เรียก onSwap: _processGeneralPoolSwapRequest(IPoolSwapStructs.SwapRequest memory request, IGeneralPool pool)
 ส่วนตัว
 ผลตอบแทน (จำนวนเงิน 256 ที่คำนวณ)
 -
 โทเค็น bytes32 ในยอดคงเหลือ;
 โทเค็น bytes32 ยอดคงเหลือ;

 // เราเข้าถึงดัชนีโทเค็นทั้งสองโดยไม่ต้องตรวจสอบการมีอยู่ เนื่องจากเราจะดำเนินการด้วยตนเองทันทีหลังจากนั้น
 EnumerableMap.IERC20ToBytes32Map พูลเก็บข้อมูล Balances = _generalPoolsBalances[request.poolId];
 uint256 indexIn = poolBalances.unchecked_indexOf(คำขอ tokenIn);
 uint256 indexOut = poolBalances.unchecked_indexOf(คำขอ tokenOut);

 ถ้า (indexIn == 0 || indexOut == 0) {
 // โทเค็นอาจไม่ได้รับการลงทะเบียนเนื่องจากพูลเองไม่ได้ลงทะเบียน เราตรวจสอบสิ่งนี้เพื่อให้
 // เหตุผลในการย้อนกลับที่แม่นยำยิ่งขึ้น
 _ensureRegisteredPool(request.poolId);
 _revert(ข้อผิดพลาด.TOKEN_NOT_REGISTERED);
 -

 // EnumerableMap จัดเก็บดัชนี *บวกหนึ่ง* เพื่อใช้ดัชนีศูนย์เป็นค่าเฝ้าระวัง - เนื่องจากค่าเหล่านี้ถูกต้อง
 เราสามารถยกเลิกสิ่งนี้ได้
 ดัชนีIn -= 1;
 ดัชนีออก -= 1;

 uint256 tokenAmount = poolBalances.length();
 uint256[] หน่วยความจำ currentBalances = new uint256[](tokenAmount);

 คำขอ.lastChangeBlock = 0;
 สำหรับ (uint256 i = 0; i < tokenAmount; i++) {
 // เนื่องจากการวนซ้ำถูกจำกัดด้วย `tokenAmount` และไม่มีโทเค็นใดที่ลงทะเบียนหรือยกเลิกการลงทะเบียนที่นี่ เราจึง
 // ทราบว่า `i` เป็นดัชนีโทเค็นที่ถูกต้องและสามารถใช้ `unchecked_valueAt` เพื่อบันทึกการอ่านที่เก็บข้อมูลได้
 bytes32 balance = poolBalances.unchecked_valueAt(i);

 currentBalances[i] = balance.total(); // อ่านจากที่เก็บข้อมูล request.lastChangeBlock = Math.max(request.lastChangeBlock, balance.lastChangeBlock());

 ถ้า (i == indexIn) {
 tokenInBalance = ยอดคงเหลือ;
 } มิฉะนั้นถ้า (i == indexOut) {
 tokenOutBalance = ยอดคงเหลือ;
 -
 -
 
 // ดำเนินการสลับ // ดำเนินการเรียกกลับคำขอสลับและคำนวณยอดคงเหลือใหม่สำหรับ 'โทเค็นเข้า' และ 'โทเค็นออก' หลังจากการสลับ
 amountCalculated = pool.onSwap(คำขอ, ยอดคงเหลือปัจจุบัน, indexIn, indexOut);
 (uint256 amountIn, uint256 amountOut) = _getAmounts(request.kind, request.amount, amountCalculated);
 tokenInBalance = tokenInBalance.increaseCash(จำนวนเงิน);
 tokenOutBalance = tokenOutBalance.decreaseCash(จำนวนเงินที่ออก);

 // อัปเดตที่เก็บข้อมูล // เนื่องจากไม่มีโทเค็นที่ลงทะเบียนหรือยกเลิกการลงทะเบียนระหว่างนี้หรือเมื่อเราเรียกค้นดัชนีสำหรับ
 // 'โทเค็นเข้า' และ 'โทเค็นออก' เราสามารถใช้ 'unchecked_setAt' เพื่อบันทึกการอ่านที่เก็บข้อมูล
 poolBalances.unchecked_setAt(indexIn, tokenInBalance);
 poolBalances.unchecked_setAt(indexOut, tokenOutBalance);
 -

วิเคราะห์โค้ดด้านบน แม้ว่า Vault จะสร้างอาร์เรย์ currentBalances ใหม่ทุกครั้งที่มีการเรียก onSwap ใน Batch Swap :

  1. หลังจากการแลกเปลี่ยนครั้งแรก ยอดคงเหลือจะได้รับการอัปเดต (แต่ค่าที่อัปเดตอาจไม่ถูกต้องเนื่องจากการสูญเสียความแม่นยำ)
  2. การสลับครั้งที่สองจะดำเนินการคำนวณต่อโดยอิงจากผลลัพธ์ของการสลับครั้งแรก
  3. การสูญเสียความแม่นยำที่สะสมในที่สุดจะนำไปสู่การลดลงอย่างมีนัยสำคัญของค่าคงที่ D

ประเด็นสำคัญ:

 // BaseGeneralPool._swap ที่ได้รับ
 ฟังก์ชัน _swapGivenIn(
 SwapRequest หน่วยความจำ swapRequest
 uint256[] การปรับสมดุลหน่วยความจำ
 ดัชนี uint256 ใน
 uint256 indexOut,
 uint256[] ปัจจัยการปรับขนาดหน่วยความจำ
 ) คืนค่าเสมือนภายใน (uint256) {
 // ค่าธรรมเนียมจะถูกหักออกก่อนการปรับขนาด เพื่อลดความซับซ้อนของการวิเคราะห์ทิศทางการปัดเศษ
 swapRequest.amount = _subtractSwapFeeAmount(จำนวนเงิน swapRequest);

 _upscaleArray(balances, scalingFactors); // แก้ไขอาร์เรย์ในสถานที่ swapRequest.amount = _upscale(swapRequest.amount, scalingFactors[indexIn]);

 uint256 amountOut = _onSwapGivenIn(swapRequest, balances, indexIn, indexOut);

 // โทเค็น amountOut กำลังออกจาก Pool ดังนั้นเราจึงปัดเศษลง
 ส่งคืน _downscaleDown(amountOut, scalingFactors[indexOut]);
 -
// แม้ว่า Vault จะส่งผ่านอาร์เรย์ใหม่ทุกครั้ง แต่:
// 1. หากยอดคงเหลือมีขนาดเล็กมาก (8-9 wei) การสูญเสียความแม่นยำระหว่างการปรับขนาดจะมีนัยสำคัญ // 2. ในการสลับแบบแบตช์ การสลับที่ตามมาจะดำเนินการคำนวณต่อโดยอิงจากยอดคงเหลือที่สูญเสียความแม่นยำไปแล้ว // 3. ไม่มีการตรวจสอบว่าการเปลี่ยนแปลงในค่าคงที่ D อยู่ในช่วงที่เหมาะสมหรือไม่

สรุป

สาเหตุของการโจมตีของ Balancer สามารถสรุปได้ดังนี้:

1. ฟังก์ชันการปรับขนาดใช้การปัดเศษลง : _upscaleArray ใช้ mulDown สำหรับการปรับขนาด ซึ่งจะทำให้เกิดการสูญเสียความแม่นยำที่สัมพันธ์กันอย่างมีนัยสำคัญเมื่อยอดคงเหลือมีขนาดเล็กมาก (เช่น 8-9 wei)

2. การคำนวณค่าคงที่มีความอ่อนไหวต่อความแม่นยำ : การคำนวณค่าคงที่ D ขึ้นอยู่กับอาร์เรย์สมดุลที่ปรับขนาด และการสูญเสียความแม่นยำจะถูกส่งต่อไปยังการคำนวณ D โดยตรง ทำให้ D มีขนาดเล็กลง

3. การขาดการตรวจสอบการเปลี่ยนแปลงในค่าคงที่ : ในระหว่างกระบวนการแลกเปลี่ยน ไม่มีการตรวจสอบว่าการเปลี่ยนแปลงในค่าคงที่ D อยู่ในช่วงที่สมเหตุสมผลหรือไม่ ซึ่งทำให้ผู้โจมตีสามารถใช้ประโยชน์จากการสูญเสียความแม่นยำเพื่อลดราคาของ BPT ได้ซ้ำแล้วซ้ำเล่า

4. การสูญเสียความแม่นยำที่สะสมในการสลับแบบแบตช์ : ในการสลับแบบแบตช์เดียวกัน การสูญเสียความแม่นยำจากการสลับหลายรายการจะสะสมและขยายกลายเป็นการสูญเสียทางการเงินจำนวนมหาศาลในที่สุด

ปัญหาสองประการนี้ ได้แก่ การสูญเสียความแม่นยำและการขาดการตรวจสอบ เมื่อรวมเข้ากับการออกแบบเงื่อนไขขอบเขตอย่างรอบคอบของผู้โจมตี ส่งผลให้เกิดการสูญเสียดังกล่าว

Balancer
ยินดีต้อนรับเข้าร่วมชุมชนทางการของ Odaily
กลุ่มสมาชิก
https://t.me/Odaily_News
กลุ่มสนทนา
https://t.me/Odaily_CryptoPunk
บัญชีทางการ
https://twitter.com/OdailyChina
กลุ่มสนทนา
https://t.me/Odaily_CryptoPunk
สรุปโดย AI
กลับไปด้านบน
  • 核心观点:Balancer因精度损失与不变值操控漏洞遭攻击。
  • 关键要素:
    1. 小额交易缩放函数向下舍入致精度损失。
    2. 不变值D计算依赖缩放后余额,误差放大。
    3. 缺少不变值变化验证与批处理误差累积。
  • 市场影响:引发DeFi协议安全机制与审计标准反思。
  • 时效性标注:中期影响
ดาวน์โหลดแอพ Odaily พลาเน็ตเดลี่
ให้คนบางกลุ่มเข้าใจ Web3.0 ก่อน
IOS
Android