BARU: Helius mengakuisisi Light Protocol
aritmetika Solana — cara melakukan perhitungan matematika dengan program atau smart contract Solana
Blog/Dasar-Dasar

Aritmetika Solana: Praktik Terbaik untuk Membangun Aplikasi Keuangan

Edukasi DeveloperMike MacCana di XMike MacCana di LinkedIn
Bacaan 9 menit

Terima kasih kepada 0xIchigo dan Lostin yang telah menyunting dan berkontribusi pada artikel ini.

Solana adalah ruang yang kompetitif—baik Anda sedang mengembangkan protokol pinjaman baru, agregator, pasar prediksi, tokenisasi RWA, maupun hal lainnya, Anda mungkin tergoda untuk terburu-buru meluncurkannya ke mainnet.

Namun, penting untuk diingat bahwa karena mengelola nilai, aplikasi blockchain pada dasarnya adalah aplikasi keuangan.

Jika Anda belum pernah membangun aplikasi keuangan—baik di blockchain maupun keuangan tradisional—Anda perlu memahami pentingnya matematika keuangan.

Ada banyak referensi berkualitas tentang topik pemrograman khusus Solana—memeriksa account mana yang harus menandatangani setiap instruction, program yang memiliki account yang Anda gunakan, menghindari serangan pembukaan kembali account, dan sebagainya. Batasan account Anchor memudahkan penerapan banyak pemeriksaan ini, sedangkan Rust memiliki beberapa pengaturan default yang cerdas, seperti mendeteksi overflow dan underflow integer dalam mode Debug.

Namun, pemrograman keuangan yang aman mencakup lebih dari sekadar topik khusus Solana. Satu kesalahan dalam aritmetika token dapat menyebabkan kebocoran, inflasi yang tidak disengaja, dan pengguna yang marah. Di Solana, yang volume transaksinya lebih tinggi daripada blockchain lain, celah dapat dieksploitasi dengan lebih cepat.

Banyak teknik pemrograman keuangan dari keuangan tradisional tidak berkaitan dengan blockchain—sehingga tidak mendapatkan perhatian yang semestinya—tetapi tetap sangat penting untuk melindungi token pengguna Anda.

Dalam artikel ini, secara khusus kita akan membahas:

  • Penggunaan integer dan unit minor
  • Menghindari hilangnya presisi — kalikan lalu bagi
  • Kebijakan pembulatan yang konsisten
  • Penggunaan perhitungan bunga tanpa float

Gunakan Integer dan Unit Minor

Kurangnya presisi adalah kerentanan umum dalam smart contract. Operasi matematika yang tidak presisi dapat menimbulkan kerentanan. Kita perlu menggunakan integer dan unit minor untuk menjalankan operasi matematika yang aman dalam aplikasi Solana.

Mari kita lihat contoh dasarnya.

Dalam keuangan tradisional, setiap interaksi yang pernah Anda lakukan ‘dalam USD’ sebenarnya dilakukan dalam sen. Jika menggunakan GBP, transaksi tersebut dilakukan dalam pence. 

Demikian pula, transaksi SOL Anda harus diproses dalam lamport, sedangkan transaksi USDC harus diproses dalam sepersejuta USDC.

Sen, pence, dan lamport adalah jenis unit minor (juga disebut unit dasar). Alasan untuk melakukan semuanya dalam unit minor sangat sederhana: komputer tidak dapat menangani angka floating point.

Berikut representasi angka satu dalam biner:

Tiga Puluh DuaEnam BelasDelapanEmpatDuaSatu
000001

Berikut representasi angka sembilan dalam biner:

Tiga Puluh DuaEnam BelasDelapanEmpatDuaSatu
001001

Namun, bagaimana Anda merepresentasikan, misalnya, 0,3 USDC? 

Jawaban yang benar adalah: Anda tidak bisa: 

Kode
let answer = 0.1 + 0.2;
msg!("0.1 + 0.2 = {}", answer);

Menghasilkan output berikut:

Program mencatat: "0.1 + 0.2 = 0.30000000000000004"

Sebagai gantinya, perlakukan dolar, GBP, SOL, USDC, dan setiap ‘mata uang’ lainnya sebagai jumlah integer dalam unit minornya.

Contohnya, untuk menjumlahkan 0,1 dan 0,2 menggunakan USDC:

Kode
let answer_ints: u128 = 100000 + 200000;
msg!("100000 + 200000 = {}", answer_ints);

Menghasilkan output berikut:

Program mencatat: "100000 + 200000 = 300000"

Penggunaan unit minor tidak menyebabkan kehilangan nilai.

Developer harus menggunakan jumlah tempat desimal suatu token sambil mengingat bahwa tidak semua token memiliki jumlah desimal yang sama.

Menggunakan integer untuk jumlah token mungkin terasa jelas, tetapi ingat bahwa Anda harus menggunakan integer di mana pun, bukan hanya untuk jumlah token.

Persentase

Persentase harus dinyatakan dalam bilangan bulat. Kebanyakan orang menggunakan basis point (terkadang disebut ‘bip’), yang direpresentasikan sebagai bps.

Contohnya, 4,74% adalah 474 bps.

Bunga Majemuk

Bunga majemuk tidak boleh dihitung menggunakan e (bilangan Euler), yang merepresentasikan batas bunga majemuk ketika frekuensi pemajemukan mendekati tak terhingga.

Meskipun penggunaan e memungkinkan ‘pemajemukan kontinu’, yang pada dasarnya menghasilkan garis mulus untuk bunga majemuk, e sendiri direpresentasikan sebagai float di Rust.

Anda akan mendapatkan kesalahan pembulatan sekaligus hasil yang berbeda saat menggunakan e dibandingkan mekanisme lain.

Kita akan mendemonstrasikan keduanya nanti dalam artikel ini.

Overflow dan Underflow

Rust menyimpan integer sebagai variabel berukuran tetap. Artinya, bergantung pada apakah variabel tersebut bertanda atau tidak bertanda, variabel hanya dapat menggunakan sejumlah ruang tertentu dalam memori.

Contohnya, tipe u8 dapat menampung nilai apa pun dari 0 hingga 255. Namun, jika kita menyimpan nilai di luar rentang tersebut, akan terjadi overflow atau underflow.

Apa itu overflow?

Overflow terjadi ketika nilai melebihi kapasitas maksimum yang dapat disimpan oleh tipe variabel, sehingga nilainya kembali ke nilai minimum.

Contohnya, jika kita mencoba menyimpan 256 dalam u8, nilainya akan kembali ke 0. Nilai 257 akan kembali ke 1, 258 ke 2, 511 ke 255, dan 512 kembali ke 0.

Apa itu underflow?

Underflow terjadi ketika nilai berada di bawah nilai minimum yang dimungkinkan, lalu kembali ke nilai maksimum. Untuk u8, -1 akan kembali ke 255, -2 ke 254, dan seterusnya. Singkatnya, underflow mirip dengan overflow, tetapi perilakunya terjadi ke arah yang berlawanan.

Rust memang memiliki sejumlah pemeriksaan yang akan membuat program mengalami panic saat runtime jika terjadi overflow atau underflow, tetapi pemeriksaan ini tidak disertakan saat Anda melakukan kompilasi dalam mode release. Selain itu, toolchain yang menjadi bagian penting dari lingkungan pengembangan Solana mengompilasi program Solana dalam mode release secara default. 

Untuk mempelajari lebih lanjut tentang overflow dan underflow serta cara mengatasinya, baca panduan keamanan program Solana kami.

Kalikan, lalu Bagi

Membagi sebelum mengalikan sering kali terasa alami—mari gunakan contoh membangun pasar prediksi. Ketika pengguna bertaruh pada hasil yang menang, kita harus menghitung pembayaran kemenangan. Pemenang di pasar prediksi dibayar berdasarkan porsi taruhan mereka pada hasil yang menang. Secara intuitif, perhitungannya sangat mudah dipahami sebagai:

kumpulan kemenangan ÷ total taruhan pada hasil yang menang × jumlah taruhan 

Ini terasa sangat alami.

Pertama, kita membagi kumpulan kemenangan menjadi bagian-bagian kecil yang masing-masing mewakili 1 ‘porsi’ kemenangan, lalu menghitung jumlah porsi yang harus diberikan kepada setiap orang.

Namun, jika mengalikan terlebih dahulu, kita akan mendapatkan hasil antara yang lebih besar. Dengan begitu, kita dapat mengurangi dampak kesalahan pembulatan dari pembagian berikutnya. 

kumpulan kemenangan × jumlah taruhan ÷ total taruhan pada hasil yang menang

Contoh Pasar Prediksi

Berikut demonstrasi singkat. Jika perhitungan ini dilakukan oleh manusia, bukan komputer, jawaban yang benar adalah 10,5.

Mari coba membagi terlebih dahulu: 

Kode
let answer: u128 = 7 / 2 * 3;
msg!("7 / 2 * 3 = {}", answer);

Menghasilkan output berikut:

Program mencatat: "7 / 2 * 3 = 9"

Dengan membagi terlebih dahulu, kita akan kehilangan 1,5 unit minor.

Bagaimana jika kita mengalikan terlebih dahulu?

Kode
let answer: u128 = 7 * 3 / 2;
msg!("7 * 3 / 2 = {}", answer);

Menghasilkan output berikut:

Program mencatat: "7 * 3 / 2 = 10"

Saat mengalikan terlebih dahulu, kerugian akibat pembulatan hanya sebesar 0,5 unit minor. 

Konsep ini dapat diperluas menjadi ‘jalankan terlebih dahulu setiap operasi yang meningkatkan orde besaran’. 

Jadi, misalnya, jika Anda menghitung ekspresi yang lebih rumit dengan pangkat atau akar (seperti menghitung risiko), hitung pangkat terlebih dahulu dan akar setelahnya. Alasannya, dalam aritmetika fixed-point, melakukan pembagian terlebih dahulu dapat menyebabkan hilangnya presisi jika hasil baginya dibulatkan ke bawah sebelum dikalikan.

Gunakan Kebijakan Pembulatan yang Konsisten

Kebijakan pembulatan yang tidak konsisten dan menimbulkan selisih kecil pada satu token dapat membuat token seolah-olah hilang atau tercipta begitu saja. Seiring waktu, hal ini dapat menguras likuiditas program atau menyebabkan inflasi token yang tidak disengaja.

Seperti yang dijelaskan Will Thieme dari Orca dalam presentasinya di Breakpoint tentang Menghindari Kendala Umum dalam Program Solana, mendapatkan satu token mungkin tidak terlihat signifikan.

Namun, Solana memiliki transaksi riil (dalam pengertian keuangan tradisional) yang terdiri dari beberapa instruction dengan biaya transaksi rendah. Jadi, sebuah transaksi dapat diisi dengan banyak instruction individual yang mengeksploitasi kesalahan off-by-one, lalu dijalankan dengan biaya yang sangat murah.

Kesalahan pembulatan tidak dapat dihindari, tetapi dapat dikelola dengan kebijakan pembulatan yang konsisten. Tentukan sejak awal apakah Anda akan membulatkan ke atas atau ke bawah dan bagaimana Anda menangani ‘setengah’ (yaitu 0,5)—pembulatan setengah ke atas lebih umum dan diwajibkan dalam beberapa standar keuangan. Hal terpenting adalah menerapkan kebijakan tersebut secara konsisten di seluruh kode Anda.

Hal yang Perlu Diperhatikan dalam Pembulatan Khusus Rust

Developer harus memahami beberapa fungsi khusus Rust yang berpotensi menimbulkan masalah karena operasi pembulatan merupakan penyebab umum hilangnya presisi. Pemilihan metode pembulatan dapat berdampak besar terhadap akurasi dan perilaku program Anda.

Pembulatan vs. Pembulatan ke Bawah

Contohnya, fungsi try_round_u64() membulatkan ke bilangan bulat terdekat. Jika ingin membangun program yang mengonversi agunan menjadi likuiditas, pembulatan ke atas dapat menyebabkan pencetakan token likuiditas melebihi jumlah yang seharusnya berdasarkan agunan yang disediakan. Sebagai gantinya, kita harus menggunakan fungsi try_floor_u64() untuk membulatkan ke bawah ke bilangan bulat terdekat.

Fungsi Aritmetika Saturating

Selain itu, developer sering menggunakan fungsi aritmetika saturating_* (misalnya, saturating_add) untuk membatasi nilai pada nilai maksimum dan minimumnya masing-masing guna mencegah overflow dan underflow. Namun, fungsi-fungsi ini dapat menyebabkan hilangnya presisi secara samar.

Contohnya, jika fungsi Anda mengalikan jumlah transaksi dengan pengali reward yang melebihi nilai maksimum tipe variabel hasilnya, pengguna Anda akan menerima reward lebih sedikit dari yang seharusnya.

Hal ini penting untuk diperhatikan, terutama karena program Anda seharusnya menggunakan aritmetika fixed-point.

Gunakan Perhitungan Bunga Tanpa Float

Teknik umum untuk menghitung bunga majemuk adalah menggunakan bilangan Euler e, tetapi e adalah float! Hindari penggunaan float untuk menghitung bunga. Sebagai gantinya, gunakan aritmetika fixed-point.

Library spl-math milik Solana menyediakan PreciseNumber, yang (seperti yang dapat Anda bayangkan) berfungsi di lingkungan Rust Solana yang lebih terbatas dan dapat merepresentasikan pecahan desimal yang sangat kecil (hingga 12 tempat desimal) sambil mempertahankan presisi yang tepat.

Kode
use spl_math::precise_number::PreciseNumber;

fn calculate_compound_interest(
   principal: u128,
   rate_basis_points: u128,
   time: u128,
   compounds_per_year: u128,
) -> u128 {
   // Formula: result = principal * (1 + rate/compounds_per_year)^(compounds_per_year * time)
   // Where rate_basis_points is expressed in basis points (500 for 5%)

   // Convert principal to PreciseNumber
   let principal = PreciseNumber::new(principal).unwrap();

   // Convert basis points to decimal percentage (divide by 10000)
   let rate = PreciseNumber::new(rate_basis_points)
       .unwrap()
       .checked_div(&PreciseNumber::new(10_000).unwrap())
       .unwrap();

   // Calculate rate/compounds_per_year
   let rate_per_period = rate
       .checked_div(&PreciseNumber::new(compounds_per_year).unwrap())
       .unwrap();

   // Calculate 'base', which is 1 + rate/compounds_per_year
   let one = PreciseNumber::new(1).unwrap();
   let base = rate_per_period.checked_add(&one).unwrap();

   // Calculate 'total_periods', which is compounds_per_year * time
   let total_periods = compounds_per_year.checked_mul(time).unwrap();

   // Calculate compound_factor, which is (1 + rate/compounds_per_year)^(compounds_per_year * time)
   let compound_factor = base.checked_pow(total_periods).unwrap();

   // Calculate result = principal * compound_factor
   principal
       .checked_mul(&compound_factor)
       .unwrap()
       .to_imprecise()
       .unwrap()
}

Anda dapat melihat perbedaannya dengan menjalankan kode tersebut:

  • Program mencatat: "Menggunakan spl-math PreciseNumber"
  • Program mencatat: "Investasi sebesar $1000 dengan bunga 5% selama 5 tahun, dimajemukkan 1 kali per tahun:"
  • Program mencatat: "Jumlah akhir: $1276"

Sebaliknya, saat menggunakan e:

  • Program mencatat: " Menggunakan e (jangan lakukan ini)"
  • Program mencatat: "Investasi sebesar $1000 dengan bunga 5% selama 5 tahun, dimajemukkan 1 kali per tahun:"
  • Program mencatat: "Jumlah akhir: $1284"

Kesimpulan

Matematika token adalah topik yang membosankan, tetapi mengabaikannya dapat memicu kejadian yang tidak Anda inginkan. Aplikasi on-chain Anda harus menangani token dengan presisi dan keamanan yang dituntut pengguna.

Dalam artikel ini, kita membahas penggunaan integer dan unit minor, perkalian, cara menghindari hilangnya presisi, mempertahankan kebijakan pembulatan yang konsisten, serta menggunakan perhitungan bunga tanpa float.

Setelah mempelajari dasar-dasar aritmetika ini, minta pihak lain—khususnya perusahaan audit yang berfokus pada Solana—untuk meninjau kode Anda sebelum meluncurkannya di mainnet.

Referensi Tambahan

Berlangganan Helius

Ikuti perkembangan terbaru dalam pengembangan Solana dan dapatkan pembaruan saat kami memublikasikan postingan