Penulis: Jiu Jiu
Sunting: 77
Latar belakang
Pada 4 September 2026, platform pinjaman terdesentralisasi terkenal, Notional Finance, mengalami serangan, dengan kerugian sekitar $1,73 juta. Berikut adalah analisis rinci dari tim keamanan SlowMist mengenai insiden serangan ini:
Pengetahuan dasar
Di Notional Finance V1, fCash dapat dipahami sebagai klaim kas dengan tanggal jatuh tempo. Cash receiver menerima pembayaran pada tanggal jatuh tempo, sedangkan cash payer membayar pada tanggal jatuh tempo. Protokol mencatat kedua posisi ini dalam Portfolio, lalu menilai kesehatan akun untuk menentukan apakah akun tersebut dapat membuka posisi melalui pemeriksaan collateral bebas.
safeTransferFrom dari ERC1155Trade bertindak sebagai fungsi pencatatan aset di sini. Ketika jenis aset yang dimasukkan milik cash receiver, kontrak akan memanggil Portfolios.mintfCashPair, sekaligus mencatat kewajiban untuk payer dan hak tagih setara untuk receiver. Panggilan ini tampak seperti transfer ERC1155, tetapi hasilnya adalah penciptaan pasangan fCash.
Setelah posisi baru ditulis ke Portfolio, perhitungan dan pemeriksaan aset jaminan bebas diperlukan. Kontrak RiskFramework terlebih dahulu akan menjumlahkan kewajiban fCash payer menjadi int256 bertanda, lalu mengonversi saldo masing-masing mata uang ke ETH untuk dijumlahkan. Angka negatif mewakili kas yang harus dibayar, sedangkan angka positif mewakili kas atau klaim yang dimiliki akun. Hasil perhitungan ini menentukan apakah transaksi diizinkan untuk dilakukan.
Setelah posisi berakhir, Portfolio akan memanggil fungsi portfolioSettleCash dari Escrow untuk mengubah fCash yang jatuh tempo menjadi saldo kas. Akun kemudian menarik aset DAI atau USDC yang sesuai dari Escrow melalui fungsi penarikan.
Penyebab mendasar
Vulnerabilitas inti dalam serangan ini terletak pada ExchangeRate._convertToETH yang digunakan dalam kontrak implementasi Escrow, di mana satu baris kode secara langsung mengonversi nilai absolut bilangan bulat bertanda menjadi uint128.

Penyerang terlebih dahulu membuka dua posisi utang dengan payer yang sama, dengan jumlah masing-masing 1 dan uint128.max. Jumlah kedua utang tersebut tepat sama dengan 2^128. Karena pembayaran oleh payer dalam perhitungan risiko dicatat sebagai angka negatif, maka parameter kunci yang diberikan ke convertBalancesToETH di Escrow adalah -2^128.
Namun, versi Solidity yang digunakan dalam kontrak ini adalah 0.6.x, sehingga nilai absolut 2^128 yang dipaksa dikonversi menjadi uint128 akan mengalami overflow dan menjadi 0, sehingga kewajiban ini tidak masuk ke dalam hasil yang dihitung dalam ETH.
Terakhir, dalam fungsi mintfCashPair, hanya memerlukan bahwa jaminan bebas pembayar lebih besar dari atau sama dengan nol, sehingga setelah terjadi overflow menjadi 0, akun tersebut masih dapat melewati pemeriksaan ini dan tetap dapat membuka posisi meskipun tidak memiliki aset yang cukup untuk menutupi kewajibannya.
Analisis langkah-langkah serangan
1. Dalam transaksi pendahuluan (0xe1589a19…d60a), penyerang pertama-tama membuat beberapa kontrak bantuan dan memanggil fungsi setApprovalForAll dari kontrak ERC1155Trade untuk memberikan otorisasi kepada kontrak bantuan; selain itu, penyerang juga sebelumnya memeriksa jatuh tempo dua CashMarket dan menghitung nilai tiga parameter AssetId yang diperlukan untuk operasi selanjutnya.

2. Selanjutnya, penyerang memanggil fungsi safeTransferFrom dari kontrak ERC1155Trade untuk mencetak satu unit fCash pair, dengan cashGroupId aset obligasi sebesar 2 dan timestamp jatuh tempo 1788480000 (4 September 2026 pukul 08:00). Fungsi ini memanggil fungsi internal _upsertAsset untuk memperbarui kewajiban dan pendapatan yang diharapkan masing-masing untuk alamat from (kontrak penyerang) dan alamat to (kontrak penerima 1).

Setelah pencetakan pair selesai, Portfolios akan segera memeriksa jaminan bebas dari kontrak serangan, dan karena jumlah pencetakan pertama terlalu kecil, saat dikonversi menjadi ETH berdasarkan kurs dan presisi saat ini, nilainya dibulatkan menjadi nol, sehingga pemeriksaan pertama dapat lolos.
3. Penyerang kemudian memanggil kembali fungsi safeTransferFrom dari kontrak ERC1155Trade untuk mencetak pasangan fCash dengan jumlah uint128.max, di mana cashGroupId aset obligasi kali ini adalah 2, tetapi timestamp jatuh tempo adalah 1796256000 (3 Desember 2026 pukul 08:00).
Ada satu detail: penyerang mencetak aset obligasi dengan tanggal jatuh tempo berbeda ke dua alamat yang berbeda. Ini karena saat memperbarui kewajiban alamat from, jika aset obligasi sama, maka akan langsung ditambahkan, yang menyebabkan revert saat pemrosesan penjumlahan akibat overflow (kontrak menggunakan library safeMath).

4. Setelah pembaruan kewajiban dan pendapatan yang diharapkan untuk alamat from (kontrak serangan) dan alamat to (kontrak penerima 2), fungsi mintfCashPair memanggil fungsi freeCollateral untuk menghitung posisi jaminan bersih dari alamat from dan memeriksa apakah nilainya harus lebih besar atau sama dengan 0 agar tetap sehat.

Di mana terlebih dahulu akan menjumlahkan total kewajiban dua kali pencetakan dari alamat from di fungsi getRequirement dalam kontrak RiskFramework:

Hasil akhirnya adalah -2^128, kemudian akan memanggil fungsi convertBalancesToETH dari kontrak Escrow untuk mengonversi nilai ini ke dalam penilaian ETH, dan fungsi convertBalancesToETH akan memanggil fungsi _convertToETH dari library ExchangeRate untuk pemrosesan perhitungan:


Dengan menelusuri fungsi _convertToETH, dapat dilihat bahwa fungsi tersebut terlebih dahulu mengambil nilai absolut dari balance, lalu memanggil uint128 untuk mengonversi 256 bit menjadi 128 bit. Nilai total utang dari from di atas adalah -2^128; ketika diambil nilai absolutnya menjadi 2^128, konversi paksa uint128() akan menyebabkan overflow melebihi batas atas dan menghasilkan 0. Ini berarti protokol salah menganggap posisi jaminan bebas dari alamat from sebagai 0, yang dianggap sehat, sehingga melewati pemeriksaan akhir dalam fungsi mintfCashPair.

5. Setelah menerima kontrak 2, posisinya dibagi lagi ke dua kontrak pendukung lainnya, kedua safeTransferFrom ini tetap akan masuk ke fungsi mintfCashPair. Namun, karena pada saat ini payer adalah kontrak penerima dari penerbitan kedua, yang memegang posisi pendapatan yang diharapkan besar dari langkah sebelumnya, perhitungan risiko menganggapnya sebagai piutang positif sehingga lolos pemeriksaan jaminan, sehingga kedua kontrak pendukung tersebut memperoleh receiver fCash yang dapat ditukar menjadi kas setelah jatuh tempo.

6. Penyerang kemudian melakukan transaksi keuntungan resmi kedua, di mana hak tagih dari dua posisi yang dipegang pada transaksi sebelumnya diselesaikan melalui kontrak pendukungnya, meningkatkan saldo tunai aset yang sesuai, lalu memanggil fungsi withdraw dari kontrak Escrow untuk menarik asetnya dan mendapatkan keuntungan.
Summary
Poin kunci dari insiden serangan ini adalah penyerang terlebih dahulu memanfaatkan dua posisi terpisah untuk menjumlahkan utang payer menjadi 2^128, lalu meminta Escrow untuk mengonversi utang tersebut menjadi ETH. Dengan memanfaatkan kerentanan overflow dalam konversi tipe data, hasilnya dipotong menjadi nol, sehingga pemeriksaan risiko melewati proses tersebut.
Tim keamanan SlowMist menyarankan agar proyek melakukan pemeriksaan rentang sebelum melakukan konversi tipe, menolak langsung hasil yang melebihi type(uint128).max. Selain itu, semua batas yang melibatkan jumlah bersimbol, penskalaan presisi, dan nominal aset harus diuji dengan nilai ekstrem, terutama mencakup input seperti uint128.max, uint128.max + 1, dan nilai absolut negatif. Anda dapat merujuk atau menggunakan perpustakaan SafeCast dari OpenZeppelin untuk penanganan konversi.
