ข้อผิดพลาดทางคณิตศาสตร์ใน Solidity smart contracts เกิดขึ้นเมื่อการคำนวณโทเค็น ส่วนแบ่ง หรือราคาผิดสูตร ใช้มาตราส่วนทศนิยมที่ไม่ถูกต้อง ทิศทางการปัดเศษผิด หรือสมมติฐานเชิงตัวเลขผิด ทำให้ผู้ใช้งานสามารถสร้างสิทธิ์ที่เกินจริง ถอนมูลค่ามากกว่าที่ฝาก หรือทำให้นิยามทางบัญชีล้มเหลว วิธีแก้คือการกำหนดหน่วยอย่างชัดเจน ใช้คณิตศาสตร์ที่มีความละเอียดและขอบเขตเต็มที่ และทดสอบนิยามทางเศรษฐศาสตร์ที่สูตรต้องรักษาไว้
ผลที่ตามมามีความสำคัญมาก Cetus CLMM สูญเสียเงิน $223.0M บน Sui เมื่อวันที่ 2025-05-22 ในเหตุการณ์ที่จัดเป็นข้อผิดพลาดทางคณิตศาสตร์ที่เกี่ยวข้องกับการบัญชีโทเค็นและส่วนแบ่ง ดังนั้น ความปลอดภัยทางคณิตศาสตร์จึงไม่ได้หมายถึงแค่การป้องกันการ revert จาก overflow ใน Solidity เท่านั้น แต่มันหมายถึงการรับรองว่าส่วนแบ่งที่ถูกสร้างขึ้นแต่ละครั้งแทนสิทธิ์ที่น่าเชื่อถือในสินทรัพย์จริง
ข้อผิดพลาดทางคณิตศาสตร์ใน Solidity คืออะไรและการโจมตีเกิดขึ้นอย่างไร?
ข้อผิดพลาดทางคณิตศาสตร์ใน Solidity คือข้อบกพร่องในการคำนวณที่แปลงการฝาก ถอน ยอดเงิน ราคา ค่าธรรมเนียม หรือสภาพคล่องเป็นสิทธิ์ทางเศรษฐกิจ ผู้โจมตีใช้ประโยชน์จากความไม่ตรงกันระหว่างโมเดลบัญชีที่ตั้งใจไว้และสูตรที่นำไปใช้จริง โดยมักเลือกลำดับธุรกรรม จำนวนสินทรัพย์ หรือสถานะพูลที่ทำให้การแปลงผิดสูตรเป็นประโยชน์แก่ตนเอง
ใน Solidity สมัยใหม่ การ overflow และ underflow ของเลขจะ revert โดยค่าเริ่มต้น การป้องกันนี้มีประโยชน์ แต่ไม่ยืนยันว่าสูตรถูกต้องในเชิงเศรษฐศาสตร์ เพราะการคำนวณอาจยังอยู่ในขอบเขต uint256 แต่สร้างส่วนแบ่งใน vault เกินความควรจะเป็น คิดค่าค้ำประกันต่ำเกินไป หรือปัดเศษการถอนให้ได้เปรียบผู้ใช้
รูปแบบการโจมตีส่วนแบ่งเกินตัวที่พบบ่อยมีขั้นตอนดังนี้:
- vault มีสินทรัพย์และส่วนแบ่งอยู่แล้ว ดังนั้นราคาส่วนแบ่งจึงไม่เท่ากับ 1
- vault ได้รับผลตอบแทน ค่าธรรมเนียม เงินบริจาค หรือยอดเงินที่เพิ่ม (rebasing)
- ราคาส่วนแบ่งสูงขึ้นเพราะสินทรัพย์รวมเพิ่ม แต่ส่วนแบ่งรวมไม่เพิ่ม
- ผู้โจมตีฝากสินทรัพย์ผ่านสูตรที่สมมติว่าส่วนแบ่ง 1 เท่ากับสินทรัพย์ 1 หน่วยเสมอ
- vault สร้างส่วนแบ่งมากกว่าที่ควรตามราคาปัจจุบันจากการฝาก
- ผู้โจมตีแลกคืนส่วนแบ่งเหล่านั้นเพื่อรับสินทรัพย์ในสัดส่วนที่เกินจริง
นิยามหลักคือ: หลังการฝาก สัดส่วนการถือครองของผู้ใช้ใหม่ควรสอดคล้องกับมูลค่าที่ฝาก หากการฝากที่มีมูลค่า 1% ของสินทรัพย์พูลสร้างส่วนแบ่งเกินกว่า 1% ของส่วนแบ่งรวม ผู้ถือส่วนแบ่งเดิมจะถูกเจือจาง
รูปแบบ Solidity ใดที่ก่อให้เกิดข้อผิดพลาดในการบัญชีโทเค็นและส่วนแบ่ง?
ข้อผิดพลาดในการบัญชีโทเค็นและส่วนแบ่งมักเกิดจากการผสมหน่วยกัน การตัดทอนค่ากลางทาง หรือการถือว่าราคาส่วนแบ่งคงที่แม้จะแปรผัน ฟังก์ชั่นที่เสี่ยงสูง ได้แก่ deposit, mint, withdraw, redeem, ขีดจำกัดเงินกู้ การตั้งค่าค่าธรรมเนียม การคำนวณสภาพคล่อง และการแจกจ่ายรางวัล
รูปแบบที่อันตรายคือ การสร้างส่วนแบ่งโดยตรงจากหน่วยสินทรัพย์ที่ฝาก:
shares = assets * 1e18 / assetScale;
การแปลงนี้เหมาะสำหรับการฝากครั้งแรกเท่านั้น เมื่อส่วนแบ่งหนึ่งหน่วยถูกนิยามเป็นหนึ่งหน่วยสินทรัพย์ที่ normalized อย่างจงใจ เมื่อ vault มีผลตอบแทนหรือขาดทุน การฝากต้องใช้ราคาปัจจุบัน:
shares = assets * totalShares / totalAssets;
แม้แต่สูตรนี้ก็ต้องระวัง เพราะการคูณก่อนอาจ overflow หากใช้ใน unchecked ขณะที่การหารก่อนอาจสูญเสียความแม่นยำ การ normalized ทศนิยมสร้างข้อผิดพลาดอีก จากตัวอย่างโทเค็นแบบ USDC ที่ใช้ทศนิยมต่ำกว่าโทเค็นส่วนแบ่ง 18 ตำแหน่ง โปรโตคอลต้องแยกหน่วยโทเค็นดิบจากหน่วยบัญชี normalized อย่างสม่ำเสมอ
สาเหตุที่พบบ่อยได้แก่:
- กำหนดค่า
1e18แบบตายตัวสำหรับสินทรัพย์ที่มีจุดทศนิยมต่างกัน - สร้างส่วนแบ่งที่อัตราหนึ่งต่อหนึ่งหลังจากราคาส่วนแบ่งเปลี่ยน
- หารก่อนคูณทำให้สูญเสียความแม่นยำโดยเงียบ
- ปัดเศษการฝากหรือถอนขึ้น ทำให้เกิดการรั่วไหลของมูลค่าสะสม
- ใช้
uncheckedกับค่าที่ควบคุมโดยผู้ใช้หรือปัจจัยสเกล - สมมติว่ายอดโทเค็นไม่เปลี่ยนแปลงนอกการฝากถอนปกติ
- ลืมว่าค่าธรรมเนียมแฝง โทเค็นที่มีการ rebasing หรือ yield สามารถทำให้ข้อมูลบัญชีแคชเสียหาย
วิธีแก้ข้อผิดพลาดทางคณิตศาสตร์ในการคำนวณส่วนแบ่ง Solidity คืออะไร?
เพื่อแก้ข้อผิดพลาดทางคณิตศาสตร์ใน Solidity ให้ normalized หน่วยสินทรัพย์ครั้งเดียว คำนวณส่วนแบ่งตามอัตราส่วนพูลปัจจุบัน ใช้การคูณและหารที่มีความละเอียดเต็มที่ และเลือกวิธีปัดเศษที่ไม่สามารถย้ายมูลค่าจากผู้ถือเดิมไปยังผู้ฝากใหม่ได้ แนวทางต้องถือว่ายอดสินทรัพย์ของสัญญาและปริมาณส่วนแบ่งเป็นสถานะบัญชีที่เชื่อมโยงกัน
ตัวอย่าง vault ที่มีช่องโหว่ด้านล่างนี้ รับโทเค็นที่กำหนดจุดทศนิยมได้ มันสร้างส่วนแบ่ง normalized ที่อัตราคงที่ แม้หลังจาก 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;
// ช่องโหว่: ไม่สนใจราคาส่วนแบ่งปัจจุบัน
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);
// แก้ไข: การตั้งราคาส่วนแบ่งตามอัตราก่อนฝาก
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");
}
}
ถ้าโปรโตคอลของคุณตั้งราคาฝาก ถอน ค้ำประกัน หรือสภาพคล่อง โดยใช้คณิตศาสตร์ที่กำหนดเอง การ ตรวจสอบ smart-contract และประเมินความปลอดภัย จะช่วยติดตามการแปลงหน่วยและนิยามทางเศรษฐกิจตลอดวงจรชีวิตธุรกรรมได้
นักพัฒนาจะหยุดข้อผิดพลาดทางคณิตศาสตร์ได้อย่างไร?
นักพัฒนาหยุดข้อผิดพลาดทางคณิตศาสตร์ได้ด้วยการกำหนดหน่วยบัญชีก่อนลงมือเขียนสูตร ใช้การปัดเศษแบบระมัดระวัง และถือว่าการแปลงแต่ละอย่างเป็นเขตความปลอดภัย สูตรควรกำหนดว่าป้อนข้อมูลแต่ละตัวหมายถึงอะไร ความละเอียดทศนิยมเท่าไร ช่วงอนุญาต และผู้ได้ประโยชน์จากการปัดเศษเป็นใคร
| เทคนิค | สิ่งที่หยุดได้ | ค่าใช้จ่ายค่าแก๊สหรือความซับซ้อน | ข้อจำกัด |
|---|---|---|---|
| การ normalized หน่วยอย่างชัดเจน | การผสมหน่วยโทเค็นดิบกับหน่วยส่วนแบ่ง 18 ตำแหน่ง | การคูณหรือหารเพิ่ม | ต้องมีนโยบายทศนิยมชัดเจน |
Math.mulDiv |
overflow และการสูญเสียความแม่นยำใน a * b / c |
ใช้แก๊สมากกว่าการดำเนินการธรรมดา | ไม่ตรวจสอบความถูกต้องเชิงเศรษฐกิจ |
| ปัดเศษลงทั้งฝากและถอน | การสร้างส่วนแบ่งเกินและถอนเกินจากการปัดเศษ | อาจเกิดยอดเล็กๆ เหลือในบัญชี | ต้องมีนโยบายจัดการยอดเล็ก (dust) |
| กำหนดขั้นต่ำการสร้างส่วนแบ่ง | การฝากที่ปัดเศษแล้วเหลือศูนย์ส่วนแบ่ง | เปรียบเทียบค่าเดียว | ต้องไม่บล็อกผู้ใช้เล็กๆ ที่ถูกต้องโดยไม่ตั้งใจ |
| จำกัดค่าป้อนและทศนิยม | การยกกำลังที่ไม่ปลอดภัยและค่าป้อนผิดปกติ | ตรวจสอบความถูกต้อง | ขอบเขตต้องตรงกับสินทรัพย์ที่รองรับ |
| การทดสอบนิยาม | การเปลี่ยนสถานะที่ละเมิดบัญชีพูล | ความพยายามในการเขียนเทส | ต้องครอบคลุมลำดับการโจมตี |
ใช้การปัดเศษลงในกรณีที่ทางเลือกคือการออกมูลค่าที่พูลไม่สามารถรองรับ เช่น vault โดยทั่วไปจะปัดเศษส่วนแบ่งที่ฝากและสินทรัพย์ที่ถอนลง หากผลิตภัณฑ์ต้องการการปัดเศษที่เป็นประโยชน์ต่อผู้ใช้ ต้องเติมเงินชดเชยอย่างชัดเจน ไม่ใช่เขียนฝังกลบผลกระทบให้ผู้ฝากรายอื่นโดยไม่รู้ตัว
ข้อคิดด้านความปลอดภัย: ตามวิธีการของ Soken การตรวจสอบทางคณิตศาสตร์เริ่มที่หน่วยและกฎการอนุรักษ์ ไม่ใช่ตัวดำเนินการ แม้การคูณที่ตรวจสอบแล้วก็ยังถูกโจมตีได้หากผลลัพธ์แทนสินทรัพย์ ส่วนแบ่ง ราคา หรือสิทธิ์การถือครองผิดประเภท
เหตุการณ์จริงใดแสดงถึงผลกระทบของข้อผิดพลาดทางคณิตศาสตร์?
เหตุการณ์ข้อผิดพลาดทางคณิตศาสตร์แสดงให้เห็นว่าการบัญชีโทเค็นและส่วนแบ่งเป็นสมบัติด้านความปลอดภัยของโปรโตคอลโดยรวม ไม่ใช่แค่ปัญหาคุณภาพโค้ดเฉพาะที่ บางครั้งตรรกะที่ได้รับผลกระทบอยู่ในสำรองของ AMM รางวัลการกู้ยืม ส่วนแบ่ง vault การคำนวณสภาพคล่อง หรือฟังก์ชั่นแปลงสินทรัพย์
| โปรโตคอล | วันที่ | จำนวนเงินที่สูญเสีย | เครือข่าย |
|---|---|---|---|
| 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 บันทึกเหตุการณ์ Arithmetic Error ใน smart-contract จำนวน 35 ครั้งระหว่างปี 2020 ถึง 2026 รวมความเสียหายเกือบ $561.0M สำหรับทีมโปรโตคอล ประวัติเหล่านี้สนับสนุนให้ทบทวนเส้นทางบัญชีเท่าเทียมกับการควบคุมการเข้าถึงและการจัดการการเรียกภายนอก
ทำอย่างไรจึงจะตรวจจับข้อผิดพลาดทางคณิตศาสตร์ก่อนปล่อยใช้งาน?
การตรวจจับข้อผิดพลาดทางคณิตศาสตร์ที่ดีที่สุดคือการทบทวนการเปลี่ยนสถานะและทดสอบสมบัติทางเศรษฐกิจในหลายลำดับ ไม่ใช่ตรวจสอบสูตรเดี่ยวแบบแมนนวล การวิเคราะห์โค้ดแบบ static สามารถจับตัวดำเนินการเสี่ยงและบล็อก unchecked ได้ ขณะที่การทดสอบแบบ fuzzing และการทดสอบนิยามช่วยยืนยันว่าโมเดลบัญชียังคงถูกต้องภายใต้การฝาก ถอน ผลตอบแทน ขาดทุน และค่าทศนิยมที่ผิดปกติ
ใช้รายการตรวจสอบนี้ในการตรวจสอบอย่างละเอียด:
- ระบุสินทรัพย์ ส่วนแบ่ง ราคา อัตรา และค่าธรรมเนียมทุกตัวพร้อมหน่วยและทศนิยม
- ตรวจสอบว่าการฝากทุกครั้งใช้ราคาก่อนฝาก
- ตรวจสอบว่าการถอนทุกครั้งใช้ราคาเหมาะสมกับส่วนแบ่งที่เผา
- ค้นหาการใช้
*,/,uncheckedและค่าคงที่ในเส้นทางบัญชี - ทดสอบสินทรัพย์ที่มีจุดทศนิยมต่างกันและการฝากขนาดใกล้ขั้นต่ำ
- ยืนยันว่าการปัดเศษไม่สร้างส่วนแบ่งเกินหรือปล่อยสินทรัพย์เกิน
- สร้างโมเดลการเปลี่ยนยอดจาก rebasing เงินบริจาค ค่าธรรมเนียม และผลตอบแทนก่อนแต่ละการกระทำของผู้ใช้
- ตรวจสอบให้แน่ใจว่าสิทธิ์ผู้ใช้รวมไม่เกินสินทรัพย์ที่พร้อมให้แลกคืน
Slither สามารถแจ้งเตือนรูปแบบคณิตศาสตร์ผิดปกติ บล็อก unchecked และการดำเนินการที่ผิดปกติด้านความแม่นยำ แต่มันไม่สามารถบอกได้ว่าสูตรราคาส่วนแบ่งถูกต้องทางเศรษฐศาสตร์หรือไม่ Foundry fuzzing จะมีประสิทธิภาพมากขึ้นหากใช้ร่วมกับการตรวจสอบข้อสมมติ เช่น การที่ totalAssets ครอบคลุมทุกสิทธิ์ที่แลกคืน และผู้ฝากไม่สามารถได้กำไรจากการฝาก-ถอนทันทีหากไม่มีผลตอบแทนจริง
การทดสอบนิยามควรสร้างลำดับหลายผู้ใช้ เช่น ฝาก รายงานผลตอบแทน ถอน ถอนบางส่วน เงินบริจาค และฝากเล็กซ้ำๆ การปฏิบัติของ Soken ยังเปรียบเทียบโมเดลคณิตศาสตร์ที่ตั้งใจไว้กับการอัปเดตสถานะแต่ละครั้ง เพราะสูตรที่ถูกต้องถ้านำไปใช้ก่อนการเปลี่ยนสถานะ แต่อาจกำหนดราคาส่วนแบ่งผิดได้
ความปลอดภัยทางคณิตศาสตร์คือวินัยในการรักษาความจริงทางเศรษฐกิจในทุกการแปลง Vault สามารถใช้การคำนวณตรวจสอบใน Solidity 0.8 แล้วแต่ยังช่องโหว่หากสร้างสิทธิ์ที่ราคาผิด สมมติทศนิยมผิด หรือปัดเศษค่ามูลค่าให้ออกไปนอกพูล
เริ่มจากเขียนหน่วยและนิยามสำหรับตัวแปรบัญชีแต่ละตัว จากนั้นรหัสนิยามเหล่านั้นในเทส Foundry ก่อนเพิ่มสินทรัพย์หรือเส้นทางการตั้งราคาใหม่ สำหรับการวิจัยเชิงลึกเพิ่มเติมเกี่ยวกับรูปแบบความปลอดภัยใน Solidity เยี่ยมชม Soken Hub