Lanjut ke konten utama
Change page

Keamanan kontrak pintar

Terakhir diedit: @roifnaufal21(opens in a new tab), 22 Mei 2024

Kontrak pintar sangat fleksibel dan mampu mengontrol nilai dan data dalam jumlah besar, sambil menjalankan logika permanen yang berdasarkan kode yang disebarkan pada Rantai Blok. Hal ini telah menciptakan ekosistem dinamis yang terdiri dari aplikasi tanpa kepercayaan dan terdesentralisasi yang memberikan banyak keuntungan dibandingkan sistem legasi. Hal tersebut juga membuka peluang bagi penyerang yang mencari keuntungan dengan mengeksploitasi kerentanan pada kontrak pintar.

Rantai Blok publik, seperti Ethereum, makin memperumit masalah pengamanan kontrak pintar. Kode kontrak yang disebarkan biasanya tidak dapat diubah untuk menambal kekurangan pada keamanannya, sementara aset yang dicuri dari kontrak pintar sulit sekali untuk dilacak dan kebanyakan tidak dapat dipulihkan karena sifat permanennya.

Meskipun ada perbedaan angka, diperkirakan bahwa nilai total yang dicuri atau hilang akibat cacat keamanan di kontrak pintar dapat mencapai lebih dari $1 miliar. Hal ini termasuk insiden yang menjadi perhatian publik, seperti peretasan DAO(opens in a new tab) (3,6 juta ETH dicuri, yang saat ini bernilai lebih dari $1 miliar dolar), Peretasan dompet parity multi-sig(opens in a new tab) ($30 juta hilang karena peretas), dan Masalah dompet beku parity(opens in a new tab) (lebih dari $300 juta nilai ETH terkunci selamanya).

Berbagai masalah yang disebutkan di atas mengharuskan pengembang untuk meningkatkan upaya dalam membuat kontrak pintar yang aman, kuat, dan tangguh. Keamanan kontrak pintar adalah masalah yang serius, dan harus dipelajari dengan baik oleh setiap pengembang. Panduan ini akan membahas pertimbangan keamanan bagi pengembang Ethereum dan menyelidiki sumber daya untuk peningkatan keamanan kontrak pintar.

Prasyarat

Pastikan Anda memahami dasar-dasar pengembangan kontrak pintar sebelum mempelajari masalah keamanan.

Pedoman untuk membuat kontrak pintar Ethereum yang aman

1. Merancang kontrol akses yang tepat

Di dalam kontrak pintar, fungsi yang ditandai sebagai public atau external dapat dipanggil oleh setiap akun milik eksternal atau externally owned account (EOA) atau akun kontrak. Menetapkan visibilitas publik untuk fungsi dibutuhkan jika Anda ingin orang lain dapat berinteraksi dengan kontrak Anda. Akan tetapi, fungsi yang ditandai sebagai private hanya dapat dipanggil oleh fungsi di dalam kontrak pintar, dan tidak bisa dipanggil oleh akun eksternal. Memberikan akses ke fungsi kontrak kepada setiap peserta di jaringan dapat menimbulkan masalah, terutama jika mengakibatkan setiap orang dapat melakukan operasi yang sensitif (misalnya, mencetak token baru).

Untuk mencegah penggunaan fungsi kontrak pintar yang tidak sah, perlu diterapkan kontrol akses yang aman. Mekanisme kontrol akses membatasi kemampuan penggunaan beberapa fungsi tertentu pada kontrak pintar untuk entitas yang disetujui, seperti akun yang bertanggung jawab untuk mengelola kontrak. Pola Ownable dan kontrol berbasis peran adalah dua pola yang bermanfaat untuk menerapkan kontrol akses di kontrak pintar:

Pola Ownable atau dapat dimiliki

Dalam pola Ownable atau dapat dimiliki, satu alamat ditetapkan sebagai "pemilik" kontrak selama proses pembuatan kontrak. Fungsi yang dilindungi diberikan pengubah OnlyOwner, yang memastikan agar kontrak melakukan autentikasi terhadap identitas alamat pemanggil sebelum menjalankan fungsi tersebut. Panggilan ke fungsi terlindung dari alamat lain, selain pemilik kontrak, akan selalu dikembalikan guna mencegah akses yang tidak diinginkan.

Kontrol akses berbasis peran

Mendaftarkan satu alamat tunggal sebagai Owner di kontrak pintar menimbulkan risiko sentralisasi dan berpotensi menjadi titik kegagalan tunggal. Jika kunci akun pemilik berhasil dikuasai, penyerang dapat menyerang kontrak yang dimilikinya. Inilah alasan penggunaan pola kontrol akses yang berbasis peran dengan beberapa akun administratif dapat menjadi pilihan yang lebih baik.

Dalam kontrol akses berbasis peran, akses ke fungsi yang sensitif didistribusikan di antara sekelompok peserta yang tepercaya. Misalnya, satu akun mungkin bertanggung jawab untuk pencetakan token, sementara akun yang lain melakukan peningkatan atau menjeda kontrak. Desentralisasi kontrol akses dengan cara ini menghilangkan titik kegagalan tunggal dan mengurangi asumsi kepercayaan bagi pengguna.

Menggunakan dompet multi-tanda tangan

Pendekatan lain untuk menerapkan kontrol akses yang aman adalah dengan menggunakan akun multi-tanda tangan untuk mengelola kontrak. Tidak seperti EOA biasa, akun multi-tanda tangan dimiliki oleh beberapa entitas dan membutuhkan tanda tangan dari beberapa akun dengan jumlah minimum—misalkan 3 dari 5—untuk menjalankan transaksi.

Menggunakan multisig untuk kontrol akses menimbulkan lapisan keamanan tambahan karena tindakan pada kontrak target memerlukan persetujuan dari banyak pihak. Hal ini terutama sangat berguna apabila dibutuhkan penggunaan pola Ownable atau dapat dimiliki, karena pola ini mempersulit penyerang atau orang jahat di internal dalam memanipulasi fungsi kontrak yang sensitif untuk tujuan jahat.

2. Gunakan pernyataan require(), assert(), dan revert() untuk melindungi operasi kontrak

Sebagaimana disebutkan, fungsi publik dalam kontrak pintar Anda dapat dipanggil oleh siapa saja setelah disebarkan di Rantai Blok. Karena Anda tidak dapat mengetahui sebelumnya tentang cara akun eksternal berinteraksi dengan kontrak, hal yang ideal adalah menerapkan perlindungan internal terhadap operasi yang bermasalah sebelum penyebaran. Anda dapat menerapkan perilaku yang tepat di kontrak pintar dengan menggunakan pernyataan require(), assert(), dan revert() untuk memicu pengecualian dan mengembalikan perubahan keadaan apabila eksekusi gagal memenuhi persyaratan tertentu.

require(): require didefinisikan pada awal fungsi dan memastikan kondisi pradefinisi terpenuhi sebelum fungsi yang dipanggil dijalankan. Pernyataan require dapat digunakan untuk memvalidasi input pengguna, memeriksa variabel keadaan, atau melakukan autentikasi identitas akun pemanggil sebelum melanjutkan dengan fungsi.

assert(): assert() digunakan untuk mendeteksi kesalahan internal dan memeriksa pelanggaran "invarian" dalam kode Anda. Invarian adalah penegasan logis tentang keadaan kontrak yang harus tetap benar untuk semua eksekusi fungsi. Contoh invarian adalah total pasokan atau saldo maksimum dari kontrak token. Penggunaan assert() memastikan bahwa kontrak Anda tidak pernah mencapai keadaan rentan, dan jika keadaan tersebut terjadi, semua perubahan pada variabel keadaan akan digulung balik ke keadaan sebelumnya.

revert(): revert() dapat digunakan dalam pernyataan if-else yang memicu pengecualian apabila kondisi yang dibutuhkan tidak terpenuhi. Contoh kontrak di bawah ini menggunakan revert() untuk menjaga eksekusi fungsi:

1pragma solidity ^0.8.4;
2
3contract VendingMachine {
4 address owner;
5 error Unauthorized();
6 function buy(uint amount) public payable {
7 if (amount > msg.value / 2 ether)
8 revert("Not enough Ether provided.");
9 // Perform the purchase.
10 }
11 function withdraw() public {
12 if (msg.sender != owner)
13 revert Unauthorized();
14
15 payable(msg.sender).transfer(address(this).balance);
16 }
17}
Tampilkan semua

3. Menguji kontrak pintar dan memverifikasi ketepatan kode

Sifat permanen pada kode yang dijalankan di Mesin Virtual Ethereum menyebabkan kontrak pintar membutuhkan tingkat penilaian kualitas yang lebih tinggi selama fase pengembangan. Pengujian kontrak secara ekstensif dan pemantauannya untuk menghindari hasil yang tidak diharapkan akan sangat meningkatkan keamanan dan melindungi pengguna Anda dalam jangka panjang.

Metode yang umum adalah menulis uji unit yang kecil dengan menggunakan data palsu yang biasanya diterima oleh kontrak dari pengguna. Pengujian unit berguna untuk menguji fungsionalitas fungsi tertentu dan memastikan kontrak pintar berfungsi sesuai harapan.

Sayangnya, pengujian unit hampir tidak efektif untuk meningkatkan keamanan kontrak pintar apabila digunakan secara terisolasi. Pengujian unit dapat membuktikan berjalan baiknya suatu fungsi dengan data palsu, tetapi efektivitas uji unit ini hanya seefektif uji yang ditulis. Hal ini menyulitkan pendeteksian kasus khusus yang tidak diuji dan kerentanan yang dapat merusak keamanan kontrak pintar Anda.

Cara yang lebih baik adalah menggabungkan pengujian unit dengan pengujian berbasis properti yang dilakukan dengan menggunakan analisis statis dan dinamis. Analisis statis mengandalkan representasi tingkat rendah, seperti grafik aliran kontrol(opens in a new tab) dan pohon sintaksis abstrak(opens in a new tab), untuk menganalisis keadaan program yang dapat dicapai dan jalur eksekusi. Sementara itu, teknik analisis dinamis, seperti fuzzing, menjalankan kode kontrak dengan nilai input acak untuk mendeteksi operasi yang melanggar properti keamanan.

Verifikasi formal adalah teknik lain untuk memverifikasi properti keamanan di kontrak pintar. Berbeda dengan pengujian rutin, verifikasi formal dapat membuktikan dengan pasti tidak adanya kesalahan di kontrak pintar. Hal ini dicapai dengan membuat spesifikasi formal yang mencakup properti keamanan yang diinginkan dan membuktikan bahwa model formal dari kontrak mematuhi spesifikasi ini.

4. Meminta peninjauan kode Anda secara independen

Setelah menguji kontrak Anda, ada baiknya meminta seseorang untuk memeriksa kode sumbernya untuk mendeteksi setiap masalah keamanan. Pengujian tidak akan mengungkap setiap kekurangan di kontrak pintar, tetapi mendapatkan tinjauan independen akan meningkatkan kemungkinan penemuan kerentanan.

Audit

Melakukan uji coba audit kontrak pintar adalah satu cara untuk melakukan tinjauan kode secara independen. Pihak auditor berperan penting dalam memastikan bahwa kontrak pintar aman dan bebas dari cacat kualitas serta kesalahan desain.

Meskipun demikian, jangan menganggap audit sebagai solusi ajaib untuk semua masalah. Audit kontrak pintar tidak akan menemukan setiap bug dan sebagian besar audit dirancang untuk memberikan babak tinjauan tambahan, yang dapat membantu mendeteksi masalah yang terlewatkan oleh pengembang selama pengembangan awal dan pengujian. Sebaiknya Anda juga mengikuti praktik terbaik dalam bekerja sama dengan auditor(opens in a new tab), seperti mendokumentasikan kode dengan benar dan menambahkan komentar dalam kode, guna memaksimalkan manfaat audit kontrak pintar.

Hadiah bounty bug

Menyiapkan program hadiah bounty bug adalah cara lain untuk melaksanakan tinjauan kode secara eksternal. Hadiah bounty bug adalah imbalan finansial yang diberikan kepada para individu (biasanya peretas topi putih) yang menemukan kerentanan dalam aplikasi.

Apabila digunakan dengan tepat, hadiah bounty bug dapat memberikan insentif kepada komunitas peretas untuk memeriksa kode Anda guna menemukan kekurangan yang kritis. Contoh yang nyata adalah "infinite money bug" yang memungkinkan penyerang menciptakan Ether dalam jumlah tak terhingga pada Optimism(opens in a new tab), yaitu protokol Lapisan ke-2 yang berjalan di Ethereum. Untungnya, seorang peretas topi putih menemukan kekurangan tersebut(opens in a new tab) dan memberi tahu timnya, sekaligus memperoleh imbalan besar dalam proses tersebut(opens in a new tab).

Strategi yang bermanfaat adalah menetapkan pembayaran program hadiah bounty bug secara proporsional dengan jumlah uang yang mengalami risiko. Disebut sebagai "penskalaan hadiah bounty bug(opens in a new tab)", cara ini memberikan insentif finansial bagi individu untuk mengungkapkan kerentanan secara bertanggung jawab dan bukan malah mengeksploitasinya.

5. Patuhi praktik terbaik selama pengembangan kontrak pintar

Keberadaan audit dan hadiah bounty bug bukan alasan untuk menghindari tanggung jawab penulisan kode program yang berkualitas tinggi. Keamanan kontrak pintar yang baik dimulai dengan mengikuti proses desain dan pengembangan yang tepat:

  • Simpan semua kode dalam sistem kontrol versi, seperti git

  • Atur semua modifikasi kode melalui permintaan penarikan

  • Pastikan permintaan penarikan memiliki setidaknya satu peninjau independen. Jika Anda bekerja solo di suatu proyek, pertimbangkan untuk mencari pengembang lain dan bertukar tinjauan kode

  • Gunakan lingkungan pengembangan untuk pengujian, kompilasi, dan penyebaran kontrak pintar

  • Jalankan kode Anda melalui alat analisis kode dasar, seperti Mythril dan Slither. Idealnya, hal ini harus dilakukan sebelum digabungkannya setiap permintaan penarikan, lalu bandingkan perbedaannya pada output

  • Pastikan kode Anda dikompilasi tanpa kesalahan, dan alat kompilasi Solidity tidak mengeluarkan peringatan

  • Dokumentasikan kode Anda dengan baik (menggunakan NatSpec(opens in a new tab)) dan jelaskan detail tentang arsitektur kontrak dengan bahasa yang mudah dipahami. Hal ini akan memudahkan orang lain yang akan mengaudit dan meninjau kode Anda.

6. Terapkan rencana pemulihan bencana yang andal

Merancang kontrol akses yang aman, menerapkan pengubah fungsi, dan saran-saran lainnya dapat meningkatkan keamanan kontrak pintar, tetapi hal-hal tersebut tidak dapat menghilangkan kemungkinan eksploit yang jahat. Membuat kontrak pintar yang aman membutuhkan "persiapan kegagalan" dan rencana cadangan perlu dimiliki untuk merespons serangan secara efektif. Rencana pemulihan bencana yang tepat akan menggabungkan beberapa atau semua komponen berikut ini:

Peningkatan kontrak

Meskipun kontrak pintar Ethereum secara default bersifat permanen, tetapi dimungkinkan untuk melakukan perubahan terbatas dengan menggunakan pola peningkatan. Peningkatan kontrak diperlukan dalam kasus ketika cacat kritis menyebabkan kontrak lama menjadi tidak dapat digunakan dan pilihan paling layak adalah menyebarkan logika baru.

Mekanisme peningkatan kontrak berfungsi dengan cara berbeda, tetapi "pola proksi" adalah salah satu cara yang lebih populer untuk meningkatkan kontrak pintar. Pola proksi memisahkan keadaan dan logika aplikasi menjadi dua kontrak. Kontrak pertama (disebut 'kontrak proksi') menyimpan variabel keadaan (misalnya, saldo pengguna), sedangkan kontrak kedua (yang disebut 'kontrak logika') menyimpan kode untuk menjalankan fungsi kontrak.

Akun berinteraksi dengan kontrak proksi, yang mengirimkan semua panggilan fungsi ke kontrak logika dengan menggunakan panggilan tingkat rendah delegatecall()(opens in a new tab). Berbeda dengan pemanggilan message biasa, delegatecall() memastikan bahwa kode yang berjalan pada alamat kontrak logika akan dijalankan dalam konteks kontrak yang memanggil. Hal ini berarti bahwa kontrak logika akan selalu menulis ke penyimpanan kontrak proksi (bukan ke penyimpanannya sendiri) dan nilai asli dari msg.sender dan msg.value akan dipertahankan.

Mendelegasikan panggilan ke kontrak logika membutuhkan penyimpanan alamatnya di penyimpanan kontrak proksi. Oleh sebab itu, peningkatan logika kontrak hanyalah masalah penyebaran kontrak logika yang lain dan menyimpan alamat baru di kontrak proksi. Karena panggilan berikutnya ke kontrak proksi secara otomatis dirutekan ke kontrak logika yang baru, Anda akan "meningkatkan" kontrak tanpa benar-benar mengubah kode.

Selengkapnya tentang peningkatan kontrak.

Penghentian darurat

Seperti yang telah disebutkan, audit dan pengujian yang ekstensif tidak akan dapat menemukan semua bug dalam kontrak pintar. Jika kerentanan muncul dalam kode Anda setelah penyebaran, upaya menambalnya tidak mungkin dilakukan karena Anda tidak dapat mengubah kode yang berjalan di akun kontrak. Selain itu, mekanisme peningkatan (misalnya, pola proksi) dapat memakan waktu untuk diterapkan (sering kali membutuhkan persetujuan dari berbagai pihak), yang hanya memberikan lebih banyak waktu bagi penyerang untuk menimbulkan kerusakan lain.

Opsi nuklir adalah menerapkan fungsi "penghentian darurat" yang akan memblokir panggilan ke fungsi-fungsi yang rentan dalam kontrak. Penghentian darurat biasanya terdiri dari beberapa komponen berikut ini:

  1. Variabel Boolean global yang menunjukkan apakah kontrak pintar dalam keadaan berhenti atau tidak. Variabel ini ditetapkan sebagai false saat penyiapan kontrak, tetapi akan kembali ke true setelah kontrak dihentikan.

  2. Fungsi yang mereferensikan variabel Boolean ketika dijalankan. Fungsi-fungsi tersebut dapat diakses ketika kontrak pintar tidak dihentikan, dan menjadi tidak bisa diakses ketika fitur penghentian darurat dipicu.

  3. Entitas yang memiliki akses ke fungsi penghentian darurat, yang menetapkan variabel Boolean ke nilai true. Untuk mencegah tindakan jahat, panggilan ke fungsi ini dapat dibatasi hanya untuk alamat tepercaya (misalnya, pemilik kontrak).

Setelah kontrak mengaktifkan penghentian darurat, fungsi tertentu tidak akan dapat dipanggil. Hal ini dicapai dengan memasukkan beberapa fungsi tertentu ke dalam pengubah yang mengacu ke variabel global. Berikut contoh(opens in a new tab) yang menggambarkan pelaksanaan pola ini dalam kontrak:

1// This code has not been professionally audited and makes no promises about safety or correctness. Use at your own risk.
2
3contract EmergencyStop {
4
5 bool isStopped = false;
6
7 modifier stoppedInEmergency {
8 require(!isStopped);
9 _;
10 }
11
12 modifier onlyWhenStopped {
13 require(isStopped);
14 _;
15 }
16
17 modifier onlyAuthorized {
18 // Check for authorization of msg.sender here
19 _;
20 }
21
22 function stopContract() public onlyAuthorized {
23 isStopped = true;
24 }
25
26 function resumeContract() public onlyAuthorized {
27 isStopped = false;
28 }
29
30 function deposit() public payable stoppedInEmergency {
31 // Deposit logic happening here
32 }
33
34 function emergencyWithdraw() public onlyWhenStopped {
35 // Emergency withdraw happening here
36 }
37}
Tampilkan semua
Salin

Contoh ini menunjukkan fitur dasar penghentian darurat:

  • isStopped adalah Boolean yang dinilai menjadi false saat awal dan menjadi true ketika kontrak memasuki mode darurat.

  • Pengubah fungsi onlyWhenStopped dan stoppedInEmergency memeriksa variabel isStopped. stoppedInEmergency digunakan untuk mengontrol fungsi-fungsi yang seharusnya tidak dapat diakses ketika kontrak mengalami kerentanan (misalnya, deposit()). Panggilan ke berbagai fungsi ini akan langsung dibatalkan.

onlyWhenStopped digunakan untuk fungsi-fungsi yang seharusnya dapat dipanggil selama keadaan darurat (misalnya, emergencyWithdraw()). Fungsi-fungsi tersebut dapat membantu mengatasi situasi sehingga dikecualikan dari daftar "fungsi terbatas".

Penggunaan fungsionalitas penghentian darurat memberikan tindakan sementara yang efektif untuk menangani kerentanan serius pada kontrak pintar Anda. Namun, fungsi ini membutuhkan kepercayaan lebih besar dari pengguna terhadap pengembang agar tidak mengaktifkan fungsi ini untuk kepentingannya sendiri. Untuk tujuan ini, solusi yang mungkin adalah desentralisasi kontrol penghentian darurat dengan cara memberlakukan mekanisme voting di dalam rantai, timelock, atau persetujuan dari dompet multisig.

Pemantauan aksi

Aksi(opens in a new tab) memungkinkan Anda melacak panggilan ke fungsi kontrak pintar dan memantau perubahan pada variabel keadaan. Idealnya, kontrak pintar Anda diprogram untuk mengeluarkan aksi setiap kali ada pihak yang melakukan tindakan yang kritis dari segi keamanan (misalnya, menarik dana).

Pembuatan log untuk aksi dan pemantauannya di luar rantai memberikan wawasan tentang operasi kontrak dan membantu mempercepat pengungkapan tindakan jahat. Hal ini berarti tim Anda dapat merespons lebih cepat terhadap peretasan dan melakukan tindakan untuk mengurangi dampaknya terhadap pengguna, seperti menjeda fungsi atau melakukan peningkatan.

Anda juga dapat memilih alat pemantauan yang dijual bebas yang secara otomatis mengirimkan peringatan setiap kali seseorang berinteraksi dengan kontrak Anda. Dengan berbagai alat ini, Anda dapat membuat peringatan khusus berdasarkan berbagai pemicu, seperti volume transaksi, frekuensi panggilan fungsi, atau fungsi tertentu yang terlibat. Misalnya, Anda dapat memprogram peringatan yang muncul ketika jumlah penarikan dalam satu transaksi melewati ambang batas tertentu.

7. Merancang sistem tata kelola yang aman

Anda mungkin ingin mendesentralisasi aplikasi Anda dengan menyerahkan kontrol atas kontrak pintar inti ke anggota komunitas. Dalam hal ini, sistem kontrak pintar akan mencakup modul tata kelola—mekanisme yang memungkinkan anggota komunitas menyetujui tindakan administratif melalui sistem tata kelola di dalam rantai. Misalnya, proposal peningkatan kontrak proksi menjadi implementasi baru dapat dipilih oleh para pemegang token.

Tata kelola terdesentralisasi dapat bermanfaat, terutama karena sejalan dengan kepentingan pengembang dan pengguna akhir. Namun demikian, mekanisme tata kelola kontrak pintar dapat menimbulkan risiko baru jika penerapannya tidak tepat. Skenario yang dapat terjadi adalah jika penyerang memperoleh kekuatan suara yang sangat besar (diukur dalam jumlah token yang dipegang) dengan mengambil flash loan (pinjaman kilat) dan berhasil masuk melalui proposal jahat.

Salah satu cara untuk mencegah masalah terkait tata kelola di dalam rantai adalah dengan menggunakan timelock (penguncian waktu)(opens in a new tab). Timelock (penguncian waktu) mencegah kontrak pintar menjalankan tindakan tertentu hingga berlalunya jangka waktu tertentu. Strategi lain termasuk menetapkan "bobot suara" pada setiap token berdasarkan lamanya token tersebut terkunci, atau mengukur kekuatan suara dari alamat pada periode sebelumnya (misalnya, 2-3 blok sebelumnya) sebagai pengganti blok saat ini. Kedua metode tersebut mengurangi kemungkinan terkumpulnya suara dengan cepat untuk mempengaruhi suara di dalam rantai.

Selengkapnya tentang merancang sistem tata kelola yang aman(opens in a new tab) dan berbagai mekanisme pemungutan suara dalam DAO(opens in a new tab).

8. Mengurangi kompleksitas dalam kode menjadi sesedikit mungkin

Pengembang perangkat lunak tradisional akrab dengan prinsip KISS ("usahakan tetap sederhana"), yang menyarankan agar tidak memasukkan kompleksitas yang tidak perlu ke dalam desain perangkat lunak. Hal ini mengikuti pemikiran lama bahwa "sistem kompleks akan gagal dengan cara yang kompleks" dan menjadi lebih rentan terhadap kesalahan yang sangat merugikan.

Menjaga kesederhanaan terutama penting saat menulis kontrak pintar, mengingat bahwa kontrak pintar berpotensi mengontrol jumlah nilai yang besar. Tips untuk mencapai kesederhanaan saat menulis kontrak pintar adalah dengan menggunaan kembali pustaka-pustaka yang sudah ada, seperti Kontrak OpenZeppelin(opens in a new tab), jika memungkinkan. Karena berbagai pustaka ini telah melalui audit dan pengujian yang ekstensif oleh para pengembang, penggunaan pustaka ini mengurangi kemungkinan munculnya bug dibandingkan dengan menulis fungsionalitas baru dari awal.

Saran umum lainnya adalah menulis fungsi yang kecil dan menjaga kontrak tetap modular dengan membagi logika bisnis ke dalam beberapa kontrak. Penulisan kode yang lebih sederhana tidak hanya mengurangi permukaan serangan pada kontrak pintar, tetapi juga mempermudah pemahaman tentang ketepatan sistem secara keseluruhan dan mendeteksi kemungkinan kesalahan desain sejak awal.

9. Mencegah kerentanan umum kontrak pintar

Reentrancy (masuk kembali)

EVM tidak mengizinkan konkurensi, yang berarti dua kontrak yang terlibat dalam pemanggilan message tidak dapat berjalan serentak. Panggilan eksternal akan menjeda eksekusi dan memori kontrak pemanggil hingga panggilan tersebut kembali, pada saat itu eksekusi berlanjut dengan normal. Proses ini dapat dideskripsikan secara resmi sebagai mentransfer alur kontrol(opens in a new tab) ke kontrak lain.

Meski kebanyakan aman, mentransfer alur kontrol ke kontrak yang tidak tepercaya dapat menyebabkan masalah, seperti reentrancy (masuk kembali). Serangan reentrancy (masuk kembali) terjadi ketika kontrak jahat memanggil balik ke kontrak yang rentan sebelum pemanggilan fungsi yang asli selesai. Penjelasan terbaik untuk jenis serangan ini adalah dengan contoh.

Misalkan ada kontrak pintar sederhana ('Korban') yang memungkinkan penyetoran dan penarikan Ether bagi siapa saja:

1// This contract is vulnerable. Do not use in production
2
3contract Victim {
4 mapping (address => uint256) public balances;
5
6 function deposit() external payable {
7 balances[msg.sender] += msg.value;
8 }
9
10 function withdraw() external {
11 uint256 amount = balances[msg.sender];
12 (bool success, ) = msg.sender.call.value(amount)("");
13 require(success);
14 balances[msg.sender] = 0;
15 }
16}
Tampilkan semua
Salin

Kontrak ini mengekspos fungsi withdraw() agar pengguna dapat menarik kembali ETH yang sebelumnya disimpan di dalam kontrak. Saat memproses penarikan dana, kontrak melakukan operasi berikut:

  1. Memeriksa saldo ETH pengguna
  2. Mengirim dana ke alamat yang memanggil
  3. Mengatur ulang saldonya menjadi 0, sehingga mencegah penarikan tambahan dari pengguna

Fungsi withdraw() di kontrak Victim mengikuti pola "periksa-interaksi-efek". Fungsi ini memeriksa apakah kondisi yang diperlukan untuk eksekusi terpenuhi (yaitu, pengguna memiliki saldo ETH positif) dan melakukan interaksi dengan mengirim ETH ke alamat pemanggil, sebelum menerapkan efek transaksi (yaitu, mengurangi saldo pengguna).

Jika withdraw() dipanggil dari akun milik eksternal (EOA), fungsi tersebut akan dijalankan sebagaimana diharapkan: msg.sender.call.value() mengirim ETH ke pemanggil. Namun, jika msg.sender adalah akun kontrak pintar yang memanggil withdraw(), pengiriman dana dengan menggunakan msg.sender.call.value() juga akan memicu berjalannya kode yang disimpan di alamat tersebut.

Bayangkan ini sebagai kode yang disebarkan di akun kontrak:

1 contract Attacker {
2 function beginAttack() external payable {
3 Victim(victim_address).deposit.value(1 ether)();
4 Victim(victim_address).withdraw();
5 }
6
7 function() external payable {
8 if (gasleft() > 40000) {
9 Victim(victim_address).withdraw();
10 }
11 }
12}
Tampilkan semua
Salin

Kontrak ini dirancang untuk melakukan tiga hal:

  1. Menerima setoran dari akun lain (dengan kemungkinan akun EOA penyerang)
  2. Setor 1 ETH ke kontrak Korban
  3. Menarik 1 ETH yang tersimpan di kontrak pintar

Tidak ada yang salah di sini, kecuali bahwa Attacker memiliki fungsi lain yang memanggil kembali fungsi withdraw() di Victim jika gas yang tersisa dari msg.sender.call.value yang masuk lebih dari 40.000. Hal ini memberi Attacker kemampuan untuk masuk kembali ke Victim dan menarik lebih banyak dana sebelum selesainya pemanggilan withdraw yang pertama. Siklusnya terlihat seperti ini:

1- Attacker's EOA calls `Attacker.beginAttack()` with 1 ETH
2- `Attacker.beginAttack()` deposits 1 ETH into `Victim`
3- `Attacker` calls `withdraw() in `Victim`
4- `Victim` checks `Attacker`’s balance (1 ETH)
5- `Victim` sends 1 ETH to `Attacker` (which triggers the default function)
6- `Attacker` calls `Victim.withdraw()` again (note that `Victim` hasn’t reduced `Attacker`’s balance from the first withdrawal)
7- `Victim` checks `Attacker`’s balance (which is still 1 ETH because it hasn’t applied the effects of the first call)
8- `Victim` sends 1 ETH to `Attacker` (which triggers the default function and allows `Attacker` to reenter the `withdraw` function)
9- The process repeats until `Attacker` runs out of gas, at which point `msg.sender.call.value` returns without triggering additional withdrawals
10- `Victim` finally applies the results of the first transaction (and subsequent ones) to its state, so `Attacker`’s balance is set to 0
Tampilkan semua
Salin

Ringkasnya adalah karena saldo pemanggil tidak ditetapkan ke 0 hingga selesainya eksekusi fungsi, pemanggilan berikutnya akan berhasil dan memungkinkan pemanggil menarik saldonya beberapa kali. Jenis serangan seperti ini dapat digunakan untuk menguras dana dari kontrak pintar, seperti yang terjadi dalam serangan peretasan DAO 2016(opens in a new tab). Serangan reentrancy (masuk kembali) masih menjadi masalah kritis bagi kontrak pintar saat ini, seperti yang ditunjukkan oleh daftar publik eksploit reentrancy(opens in a new tab).

Cara mencegah serangan reentrancy (masuk kembali)

Salah satu pendekatan untuk menangani reentrancy (masuk kembali) adalah dengan mengikuti pola periksa-efek-interaksi(opens in a new tab). Pola ini mengurutkan eksekusi fungsi sedemikian rupa sehingga kode yang melakukan pemeriksaan yang diperlukan sebelum melanjutkan eksekusi akan berada di urutan pertama, diikuti oleh kode yang memanipulasi keadaan kontrak, lalu yang terakhir adalah kode yang berinteraksi dengan kontrak atau EOA lain.

Pola periksa-efek-interaksi digunakan dalam versi yang direvisi dari kontrak Victim yang ditunjukkan di bawah ini:

1contract NoLongerAVictim {
2 function withdraw() external {
3 uint256 amount = balances[msg.sender];
4 balances[msg.sender] = 0;
5 (bool success, ) = msg.sender.pemanggilan.value(amount)("");
6 require(success);
7 }
8}
Salin

Kontrak ini melakukan pemeriksaan pada saldo pengguna, menerapkan efek dari fungsi withdraw() (dengan mengatur ulang saldo pengguna menjadi 0), dan melanjutkan dengan melakukan interaksi (mengirim ETH ke alamat pengguna). Hal ini memastikan kontrak memperbarui penyimpanannya sebelum panggilan eksternal, menghilangkan kondisi re-entrancy (masuk kembali) yang memungkinkan serangan pertama. Kontrak Attacker masih bisa memanggil kembali ke NoLongerAVictim, tetapi karena balances[msg.sender] telah ditetapkan menjadi 0, maka penarikan tambahan akan menimbulkan kesalahan.

Pilihan lainnya adalah dengan menggunakan kunci pengecualian bersama (umumnya dideskripsikan sebagai "mutex") yang mengunci sebagian dari keadaan kontrak hingga pemanggilan fungsi selesai. Hal ini diterapkan dengan menggunakan variabel Boolean yang ditetapkan ke true sebelum fungsi dieksekusi dan kembali ke false setelah pemanggilan selesai. Seperti yang terlihat pada contoh di bawah ini, penggunaan mutex melindungi fungsi dari pemanggilan rekursif pada saat pemanggilan asli masih diproses, sehingga secara efektif menghentikan reentrancy (masuk kembali).

1pragma solidity ^0.7.0;
2
3contract MutexPattern {
4 bool locked = false;
5 mapping(address => uint256) public balances;
6
7 modifier noReentrancy() {
8 require(!locked, "Blocked from reentrancy.");
9 locked = true;
10 _;
11 locked = false;
12 }
13 // This function is protected by a mutex, so reentrant calls from within `msg.sender.call` cannot call `withdraw` again.
14 // The `return` statement evaluates to `true` but still evaluates the `locked = false` statement in the modifier
15 function withdraw(uint _amount) public payable noReentrancy returns(bool) {
16 require(balances[msg.sender] >= _amount, "No balance to withdraw.");
17
18 balances[msg.sender] -= _amount;
19 bool (success, ) = msg.sender.call{value: _amount}("");
20 require(success);
21
22 return true;
23 }
24}
Tampilkan semua
Salin

Anda juga dapat menggunakan sistem pembayaran tarik(opens in a new tab) yang mengharuskan pengguna menarik dana dari kontrak pintar, sebagai pengganti sistem "pembayaran dorong" yang mengirim dana ke akun. Hal ini menghilangkan kemungkinan secara tidak sengaja memicu kode pada alamat yang tidak dikenal (dan juga dapat mencegah beberapa serangan denial-of-service atau penolakan layanan).

Underflow dan overflow bilangan bulat

Overflow bilangan bulat terjadi ketika hasil operasi aritmatika berada di luar rentang nilai yang dapat diterima sehingga menyebabkan nilainya "bergulung" ke nilai terendah yang dapat dinyatakan. Misalnya, uint8 hanya dapat menyimpan nilai hingga 2^8-1=255. Operasi aritmatika yang menghasilkan nilai yang lebih besar dari 255 akan mengalami overflow dan mengatur ulang uint ke 0, serupa dengan cara odometer di mobil mengatur ulang ke 0 setelah mencapai jarak tempuh maksimal (999999).

Underflow bilangan bulat terjadi karena alasan yang serupa: hasil operasi aritmatika berada di bawah rentang yang dapat diterima. Misalkan Anda mencoba mengurangi lagi nilai 0 pada data jenis uint8, maka hasilnya akan langsung bergulung ke nilai terbesar yang dapat dinyatakan (255).

Baik overflow maupun underflow bilangan bulat dapat menyebabkan perubahan tak terduga pada variabel keadaan di kontrak dan menyebabkan eksekusi yang tidak direncanakan. Di bawah ini adalah contoh yang menunjukkan cara penyerang mengeksploitasi overflow aritmetika di kontrak pintar untuk melakukan operasi yang tidak sah:

1pragma solidity ^0.7.6;
2
3// This contract is designed to act as a time vault.
4// User can deposit into this contract but cannot withdraw for at least a week.
5// User can also extend the wait time beyond the 1 week waiting period.
6
7/*
81. Deploy TimeLock
92. Deploy Attack with address of TimeLock
103. Call Attack.attack sending 1 ether. You will immediately be able to
11 withdraw your ether.
12
13What happened?
14Attack caused the TimeLock.lockTime to overflow and was able to withdraw
15before the 1 week waiting period.
16*/
17
18contract TimeLock {
19 mapping(address => uint) public balances;
20 mapping(address => uint) public lockTime;
21
22 function deposit() external payable {
23 balances[msg.sender] += msg.value;
24 lockTime[msg.sender] = block.timestamp + 1 weeks;
25 }
26
27 function increaseLockTime(uint _secondsToIncrease) public {
28 lockTime[msg.sender] += _secondsToIncrease;
29 }
30
31 function withdraw() public {
32 require(balances[msg.sender] > 0, "Insufficient funds");
33 require(block.timestamp > lockTime[msg.sender], "Lock time not expired");
34
35 uint amount = balances[msg.sender];
36 balances[msg.sender] = 0;
37
38 (bool sent, ) = msg.sender.call{value: amount}("");
39 require(sent, "Failed to send Ether");
40 }
41}
42
43contract Attack {
44 TimeLock timeLock;
45
46 constructor(TimeLock _timeLock) {
47 timeLock = TimeLock(_timeLock);
48 }
49
50 fallback() external payable {}
51
52 function attack() public payable {
53 timeLock.deposit{value: msg.value}();
54 /*
55 if t = current lock time then we need to find x such that
56 x + t = 2**256 = 0
57 so x = -t
58 2**256 = type(uint).max + 1
59 so x = type(uint).max + 1 - t
60 */
61 timeLock.increaseLockTime(
62 type(uint).max + 1 - timeLock.lockTime(address(this))
63 );
64 timeLock.withdraw();
65 }
66}
Tampilkan semua
Cara mencegah underflow dan overflow bilangan bulat

Sejak versi 0.8.0, pengompilasi Solidity menolak kode yang menghasilkan underflow dan overflow bilangan bulat. Namun, kontrak yang dikompilasi dengan versi pengompilasi yang lebih rendah harus melakukan pemeriksaan pada fungsi yang melibatkan operasi aritmatika atau menggunakan pustaka (misalnya, SafeMath(opens in a new tab)) yang memeriksa adanya underflow/overflow.

Manipulasi Oracle

Oracles mengambil informasi di luar rantai dan mengirimnya ke dalam rantai agar dapat digunakan oleh kontrak pintar. Dengan oracle, Anda dapat mendesain kontrak pintar yang mendukung sistem di luar rantai, seperti pasar modal, sehingga dapat sangat memperluas penggunaannya.

Akan tetapi, jika oracle rusak dan mengirimkan informasi yang salah di dalam rantai, kontrak pintar akan dijalankan berdasarkan input yang salah sehingga dapat menimbulkan masalah. Ini adalah dasar dari "masalah oracle", yang berkaitan dengan tugas untuk memastikan informasi dari oracle rantai blok sudah akurat, mutakhir, dan tepat waktu.

Masalah keamanan yang terkait adalah menggunakan oracle di dalam rantai, seperti pertukaran terdesentralisasi, untuk mendapatkan harga spot untuk aset. Platform pemberian pinjaman di industri finansial terdesentralisasi (DeFi) sering melakukan hal ini guna menentukan nilai jaminan pengguna untuk menentukan jumlah yang dapat dipinjamnya.

Harga DEX sering kali akurat, sebagian besar disebabkan oleh arbitrator yang memulihkan parity di pasar. Akan tetapi, harga tersebut mudah dimanipulasi, terutama jika oracle di dalam rantai menghitung harga aset berdasarkan pola perdagangan historis (seperti yang umum terjadi).

Misalnya, penyerang dapat memompa harga spot aset yang palsu dengan mengambil flash loan (pinjaman kilat) tepat sebelum berinteraksi dengan kontrak pemberian pinjaman Anda. Meminta DEX untuk harga aset akan mengembalikan nilai yang lebih tinggi dari biasanya (karena "pesanan beli" penyerang yang lebih besar dari biasanya sehingga mengubah permintaan aset). Hal ini menyebabkan penyerang dapat meminjam lebih banyak dari yang seharusnya. "Serangan pinjaman kilat" tersebut telah digunakan untuk mengeksploitasi ketergantungan pada oracle harga di antara aplikasi DeFi sehingga menyebabkan protokol kehilangan dana jutaan.

Cara mencegah manipulasi oracle

Persyaratan minimum untuk menghindari manipulasi oracle adalah menggunakan jaringan oracle terdesentralisasi yang meminta informasi dari berbagai sumber untuk menghindari titik kegagalan tunggal. Dalam kebanyakan kasus, oracle terdesentralisasi memiliki insentif ekonomi kripto bawaan untuk mendorong simpul oracle agar melaporkan informasi yang tepat sehingga menjadikannya lebih aman daripada oracle terpusat.

Jika Anda berencana untuk meminta harga aset dari oracle di dalam rantai, pertimbangkan untuk menggunakan oracle yang menerapkan mekanisme harga rata-rata berbobot waktu (TWAP). Sebuah oracle TWAP(opens in a new tab) meminta harga aset pada dua titik waktu yang berbeda (yang dapat Anda ubah) dan menghitung harga spot berdasarkan harga rata-rata yang diperoleh. Memilih periode waktu yang lebih lama melindungi protokol Anda dari manipulasi harga karena order besar yang dieksekusi baru-baru ini tidak dapat memengaruhi harga aset.

Sumber daya keamanan kontrak pintar untuk pengembang

Alat untuk menganalisis kontrak pintar dan memverifikasi kebenaran kode

  • Alat dan pustaka pengujian - Kumpulan alat dan pustaka standar industri untuk melakukan pengujian unit, analisis statis, dan analisis dinamis pada kontrak pintar.

  • Alat verifikasi formal - Alat untuk memverifikasi kebenaran fungsional pada kontrak pintar dan memeriksa invarian.

  • Layanan audit kontrak pintar - Daftar organisasi yang menyediakan layanan audit kontrak pintar untuk proyek pengembangan Ethereum.

  • Platform hadiah bounty bug - Platform untuk mengkoordinasikan hadiah bounty bug dan memberi hadiah atas pengungkapan kerentanan kritis di kontrak pintar secara bertanggung jawab.

  • Pemeriksa Fork(opens in a new tab) - Alat online gratis untuk memeriksa semua informasi yang tersedia mengenai kontrak hasil fork.

  • Pengode ABI(opens in a new tab) - Layanan online gratis untuk mengodekan fungsi kontrak Solidity dan argumen konstruktor Anda.

Alat untuk memantau kontrak pintar

Alat untuk administrasi kontrak pintar dengan aman

Layanan audit kontrak pintar

Platform hadiah bounty bug

Publikasi kerentanan dan eksploitasi kontrak pintar yang diketahui

Tantangan untuk mempelajari keamanan kontrak pintar

Praktik terbaik untuk mengamankan kontrak pintar

Tutorial tentang keamanan kontrak pintar

  • Cara menulis kontrak pintar yang aman

  • Cara menggunakan Slither untuk menemukan bug kontrak pintar

  • Cara menggunakan Manticore untuk menemukan bug kontrak pintar

  • Panduan keamanan kontrak pintar

  • Cara aman mengintegrasikan kontrak token Anda dengan token arbitrer

Apakah artikel ini membantu?