Compound là giao thức cho vay crypto được phát triển từ khá sớm (khoản 2017, 2018). Ở thời điểm hiện tại codebase của nó tiếp tục được sử dụng rộng rãi cho những dịch vụ lending khác nhau theo kiểu compound-fork.
Sonne Finance là 1 trong những dịch vụ fork từ Compound bị tấn công mới nhất ngày 2024-5-15, do chịu ảnh hưởng từ lỗi tính toán sai số trong giao thức compound đã từng bị khai thác một vài lần trước đây.
Giao thức Compound
Compound cung cấp nhiều market cho vay với mỗi market được quản lý bởi 1 contract cToken
, hay ở bản fork Sonne Finance thì gọi là soToken
. Mỗi loại đồng tiền thế chấp tương ứng với 1 market và đồng token đại diện soToken
.
- market
DAI
đại diện bởi đồng token và contractsoDAI
. - market
WETH
đại diện bởi đồng token và contractsoWETH
. - market
WBTC
đại diện bởi đồng token và contractsoWBTC
. - ...
Người dùng muốn thế chấp tài sản nào thì nạp tiền vào market đó để mint ra 1 lượng soToken
tương ứng. Sau đó có thể dùng lượng soToken
này để hưởng lãi hoặc vay từ các market khác. Ví dụ người dùng có thể thế chấp WETH
vào market soWETH
sau đó vay DAI
từ market soDAI
.
ExchangeRate của mỗi market là tỉ lệ chuyển đổi số lượng từ đồng tiền thế chấp sang số token đại diện soToken
tương ứng.
Collateral Factor (CF) có giá trị 0-90%
là giá trị đại diện cho khả năng vay của mỗi loại tài sản thế chấp. Ví dụ market soWBTC
có CF 90% có nghĩa là với $100 WBTC thế chấp vào thì có khả năng vay được $90 (từ 1 market nào đó khác). Với loại tài sản thế chấp khác nhiều rủi ro hơn thì khả năng vay được sẽ thấp hơn ví dụ với $100 OP chỉ vay được tối đa $40 tương đương CF 40%.
Tổng quan về vụ hack
Hacker nhắm đến market lending mới tạo soVELO
còn trống (chưa có tài sản thế chấp nạp vào, tổng cung bằng 0), chỉ số Collateral Factor lớn hơn 0%.
Hacker 1 mình thao túng để thiết lập tổng cung soVELO
rất nhỏ trong khi lại đại diện cho 1 lượng tài sản rất lớn để vay tiền từ market khác, sau đó lợi dụng việc tính toán sai số để rút ra lại toàn bộ số thế chấp ban đầu đã nạp vào market soVELO
, thu lợi số tiền đã vay từ các market soToken
khác.
Tạo market mới empty
Hacker thông qua contract TimelockController
(sau khi proposal về tạo market lending mới soVELO
được approve và kết thúc 2 ngày delay...cái này mình chưa check kỹ) thực thi:
- Tạo market lending mới
soVELO
hoàn toàn trống. - Set Collateral Factor 35% cho market
soVELO
.
Mint soVELO và donate thế chấp
-
Hacker sử dụng 1 số token Velo rất nhỏ
400000001
(với decimal 18 tương đương4e-10 VELO
) làm thế chấp để mint ra đúng 2 đơn vị WEIsoVELO
(với decimal 8, 2 WEI tương đương2e-8 soVELO
). Lúc này 2 WEI soVELO chỉ đại diện cho khoản thế chấp4e-10 VELO
gần như vô giá trị (1 VELO có giá $0.124). -
Hacker thực hiện chuyển số token Velo trị trá $4.8M vào contract
soVELO
, lúc này chỉ 2 WEI soVELO đại diện cho số tài sản thế chấp rất lớn là $4.8M. Đây là giao dịch chuyển trực tiếp (còn gọi là donate) mà không mint thêm bất kỳsoVELO
nào, mục đích là thao túng để cho sốsoVELO
rất nhỏ đại diện cho số tài sản thế chấp rất lớn. Với sai số trong cách tínhredeemToken
(tính sốsoVELO
để burn đi khi muốn rút thế chấp) là 1 đơn vị trên tổng số 2 thì sai số lên tới 50% số tài sản thế chấp ($4.8M) là cực kỳ lớn và có thể bị lợi dụng để thu lợi.
Mượn tài sản ở market khác (soWETH)
Hacker thực hiện mượn 265.84 WETH (trị giá $820K) dựa vào khoản thế chấp đang sở hữu: $4.8M * CF(35%)
= $1.68M > $820K. Đây không phải con số borrow ngẫu nhiên mà được tính toán gần với mức tối ưu có thể thu về.
Lúc này hacker có tài sản thế chấp là số token VELO
nạp vào trị giá $4.8M với khả năng vay $1.68M, khoản vay WETH
đang sở hữu trị giá $820K (lúc này vẫn chưa có gì bất thường).
Rút tài sản thế chấp
Hacker thực hiện lệnh rút 99.99% số token VELO thế chấp tương đương burn đi 1.9998... WEI soVELO
(trên tổng số 2 WEI soVELO
), đúng ra khi giao thức kiểm tra điều kiện lệnh này sẽ fail vì hiện tại còn nợ $820K WETH nên không thể rút gần như toàn bộ thế chấp.
Tuy nhiên sai lầm trong giao thức xảy ra ở đây, khi tính toán số token soVELO
burn đi đã bị làm tròn 1.9998... -> 1
nên giao thức khi kiểm tra tưởng lầm hacker chỉ rút ra 1 nửa số thế chấp khoảng $840K
Khả năng vay hiện tại ($1.68M) > Số thế chấp rút ra ($840K) + số nợ hiện tại (WETH $820K)
nên điều kiện rút thế chấp vẫn thỏa mãn và được thực thi.
Hàm getHypotheticalAccountLiquidityInternal
chứa logic tính toán chung khi muốn rút thế chấp hoặc khi muốn mượn thêm token. Trong trường hợp này muốn rút thế chấp (redeem) truyền vào borrowAmount = 0
.
// Comptroller.sol - line 389
/* Otherwise, perform a hypothetical liquidity check to guard against shortfall */
(
Error err,
,
uint256 shortfall
) = getHypotheticalAccountLiquidityInternal(
redeemer,
CToken(cToken),
redeemTokens,
0
);
// Comptroller.sol - line 389
function getHypotheticalAccountLiquidityInternal(
address account, // tài khoản
CToken cTokenModify, // market muốn rút thế chấp hoặc vay thêm
uint256 redeemTokens, // số token muốn rút thế chấp
uint256 borrowAmount // số token muốn vay thêm
) internal view returns (Error, uint256, uint256) {
// Duyệt tất cả các market mà account tham gia
// S1 - tính tổng tài sản thế chấp
// S2 - tính tổng tài sản đang vay + số tài sản muốn vay thêm + số tài sản muốn rút ra
// Nếu S1 > S2 trả về giá trị thặng dư S1 - S2
// Nếu S1 < S2 trả về shortfall S2 - S1 (không thể vay thêm hoặc rút thế chấp)
}
Sau bước này thì hacker đã rút ra gần như toàn bộ số token VELO ban đầu làm thế chấp ($4.8M, để chừa lại 1 ít 100 VELO), cộng với thu lợi 265.84 WETH trị giá $820K. Lúc này khoản vay WETH đã không còn được đảm bảo đủ tài sản thế chấp, hacker thực hiện thanh lý khoản vay.
Pool còn lại 100 VELO - 1 WEI soVELO
, hacker nạp tiếp vào 100 VELO
mint ra 1 WEI soVELO
(tỉ lệ exchange rate hiện tại) để pool trở thành 200 VELO - 2 WEI soVELO
, tiếp tục donate khoản lớn token VELO
và lặp lại thao tác như trên cho đến khi rút hết WETH ra khỏi pool soWETH
(và có thể tiếp tục với market khác).
-
Hacker 1 mình nắm toàn bộ pool
soVELO
nên số thế chấp lớn donate vào có thể rút ra toàn bộ mà không bị thiệt hại gì. -
Số WETH vay ở đây nhỏ hơn và gần với 1 nửa khả năng vay hiện tại ($1.68M / 2) là con số để thu lợi tối ưu.
-
Trong trường hợp thông thường giả sử 1 người dùng bình thường nạp vào khoản thế chấp khoảng 10VELO (10e18 đơn vị WEI - $1.24) thì sẽ mint ra
5e10
đơn vịsoVELO
, với sai số là1 / 5e10
thì hầu như sẽ không thu lợi được gì. Bởi vậy hacker cần nhắm đến những market mới tạo và hoàn toàn trống.
Cách thức thực hiện
Trong phần này chúng ta sẽ tìm hiểu chi tiết về cách thức thực tế hacker đã làm để thực hiện vụ tấn công.
- CToken.sol (
soVELO
,soWETH
,...). - Comptroller.sol quản lý tất cả market, chứa logic chung để check, tính toán...
Vụ tấn công được thực hiện thông qua 2 transaction, chúng ta có thể theo dõi event logs để thấy các bước diễn ra
- Tạo market mới
soVELO
, setup các tham số. - Dùng 0.000000000400000001 (4e-10)
VELO
mint ra đúng2 WEI soVELO
.
// CToken.sol - line 558
// số 1 ở cuối 400000001 ko cần thiết nhưng là con số chính xác hacker đã thực hiện
// initExchangeRate = 2e26
// 2 = 400000001 * e18 / 2e26
uint256 mintTokens = div_(actualMintAmount, exchangeRate);
- Donate
2,552.964259704265837526 VELO
vào marketsoVELO
.
- Set Collateral Factor 350000000000000000 (35%).
- Donate tiếp
35,469,150.965253049864450449 VELO
vào marketsoVELO
, market có35,471,703.929512754530287976 VELO - 2 WEI soVELO
. - Vay
265.842857910985546929 WETH
từ marketsoWETH
. - Rút thế chấp (redeem)
35,471,603.929512754530287976 VELO
khỏi marketsoVELO
, market còn100 VELO - 1 WEI soVELO
.
// CToken.sol - line 640
// redeemAmountIn: 35,471,603.929512754530287976 * e18
redeemTokens = div_(redeemAmountIn, exchangeRate); // redeemTokens 1.9998.. -> 1
// CToken.sol - line 645
// Vượt qua kiểm tra điều kiện rút thế chấp
/* Fail if redeem not allowed */
uint256 allowed = comptroller.redeemAllowed(
address(this),
redeemer,
redeemTokens
);
-
Thanh lý khoản vay WETH khi không còn đủ tài sản đảm bảo.
-
Dùng
100 VELO
mint tiếp1 WEI soVELO
(tỉ lệ hiện tại), market có200 VELO - 2 WEI soVELO
. -
Tiếp tục donate lượng lớn VELO vào market
soVELO
và lặp lại các bước trên để rút WETH khỏi marketsoWETH
.
Ở đây trong thực tế mỗi vòng nạp rút hacker tạo 1 tài khoản khác nhau để thực hiện nạp, vay, rút thế chấp, thanh lý sau đó tự hủy đi, nhưng không đề cập ở trên để đơn giản hóa.
Cách ngăn chặn
-
Market mới set Collateral Factor bằng 0.
-
Mint trước 1 số lượng
soToken
(giả sử $100) trước khi set CF > 0 và đưa market vào hoạt động. -
Check kỹ hơn khi tính
redeemTokens
từredeemAmountIn
.
Link tham khảo
https://medium.com/@SonneFinance/post-mortem-sonne-finance-exploit-12f3daa82b06
https://www.comp.xyz/t/hundred-finance-exploit-and-compound-v2/4266
Invocation Flow on Blocksec