Skip to main content

Vụ hack Sonne Finance

· 9 min read

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à contract soDAI.
  • market WETH đại diện bởi đồng token và contract soWETH.
  • market WBTC đại diện bởi đồng token và contract soWBTC.
  • ...

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.

info

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 đương 4e-10 VELO) làm thế chấp để mint ra đúng 2 đơn vị WEI soVELO (với decimal 8, 2 WEI tương đương 2e-8 soVELO). Lúc này 2 WEI soVELO chỉ đại diện cho khoản thế chấp 4e-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ính redeemToken (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).

Notes
  • 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

Transaction 1

  • Tạo market mới soVELO, setup các tham số.
  • Dùng 0.000000000400000001 (4e-10) VELO mint ra đúng 2 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 market soVELO.

Transaction 2

  • Set Collateral Factor 350000000000000000 (35%).
  • Donate tiếp 35,469,150.965253049864450449 VELO vào market soVELO, market có 35,471,703.929512754530287976 VELO - 2 WEI soVELO.
  • Vay 265.842857910985546929 WETH từ market soWETH.
  • Rút thế chấp (redeem) 35,471,603.929512754530287976 VELO khỏi market soVELO, market còn 100 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ếp 1 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 market soWETH.

note

Ở đâ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.

https://medium.com/@SonneFinance/post-mortem-sonne-finance-exploit-12f3daa82b06

https://blocksec.com/blog/6-hundred-finance-incident-catalyzing-the-wave-of-precision-related-exploits-in-vulnerable-forked-protocols

https://www.comp.xyz/t/hundred-finance-exploit-and-compound-v2/4266

Invocation Flow on Blocksec