General Recommendations

Ketika mengembangkan perangkat lunak, memastikan bahwa itu berfungsi sesuai yang diinginkan biasanya cukup mudah. Namun, mencegah penggunaan yang tidak disengaja dan kerentanan bisa lebih menantang.

Dalam pengembangan kontrak pintar (smart contract), keamanan sangat penting. Satu kesalahan saja dapat mengakibatkan hilangnya aset berharga atau tidak berfungsinya fitur-fitur tertentu dengan semestinya.

Kontrak pintar dieksekusi di lingkungan publik di mana siapa pun dapat memeriksa kode dan berinteraksi dengannya. Setiap kesalahan atau kerentanan dalam kode dapat dimanfaatkan oleh pihak yang jahat.

Bab ini menyajikan rekomendasi umum untuk menulis kontrak pintar yang aman. Dengan menerapkan konsep-konsep ini selama pengembangan, Anda dapat membuat kontrak pintar yang kokoh dan handal. Hal ini mengurangi kemungkinan terjadinya perilaku yang tidak terduga atau kerentanan.

Disclaimer

Bab ini tidak memberikan daftar lengkap dari semua masalah keamanan yang mungkin terjadi, dan tidak menjamin bahwa kontrak Anda akan benar-benar aman secara keseluruhan.

Jika Anda sedang mengembangkan kontrak pintar untuk penggunaan produksi, sangat disarankan untuk melakukan audit eksternal yang dilakukan oleh para ahli keamanan.

Mindset

Cairo adalah bahasa yang sangat aman terinspirasi dari Rust. Dirancang sedemikian rupa sehingga memaksa Anda untuk menutupi semua kasus yang mungkin. Masalah keamanan pada Starknet kebanyakan muncul dari cara alur kontrak pintar dirancang, bukan begitu banyak dari bahasa itu sendiri.

Menerapkan pola pikir keamanan adalah langkah awal dalam menulis kontrak pintar yang aman. Cobalah untuk selalu mempertimbangkan semua skenario yang mungkin saat menulis kode.

Viewing Smart Contracts as Finite State Machines

Transaksi dalam smart contract bersifat atomik, artinya mereka entah berhasil atau gagal tanpa membuat perubahan apapun.

Pikirkan kontrak pintar sebagai mesin keadaan: mereka memiliki serangkaian keadaan awal yang ditentukan oleh batasan konstruktor, dan fungsi eksternal mewakili serangkaian transisi keadaan yang mungkin. Sebuah transaksi tidak lebih dari sekadar transisi keadaan.

The assert! or panic! macros can be used to validate conditions before performing specific actions. You can learn more about these on the Unrecoverable Errors with panic page.

Validasi tersebut dapat meliputi:

  • Masukan yang diberikan oleh pemanggil
  • Persyaratan eksekusi
  • Invarian (kondisi yang harus selalu benar)
  • Value return dari panggilan fungsi lain

For example, you could use the assert! macro to validate that a user has enough funds to perform a withdraw transaction. If the condition is not met, the transaction will fail and the state of the contract will not change.

    impl Contract of IContract<ContractState> {
        fn withdraw(ref self: ContractState, amount: u256) {
            let current_balance = self.balance.read();

            assert!(self.balance.read() >= amount, "Insufficient funds");

            self.balance.write(current_balance - amount);
        }

Menggunakan fungsi-fungsi ini untuk memeriksa kondisi menambahkan batasan-batasan yang membantu mendefinisikan dengan jelas batas-batas transisi keadaan yang mungkin untuk setiap fungsi dalam kontrak pintar Anda. Pemeriksaan ini memastikan bahwa perilaku kontrak tetap dalam batas-batas yang diharapkan.

Recommendations

Pola Periksa Efek Interaksi

Pola Checks Effects Interactions adalah pola desain umum yang digunakan untuk mencegah serangan reentrancy di Ethereum. Meskipun reentrancy lebih sulit untuk dicapai di Starknet, disarankan tetap menggunakan pola ini dalam kontrak pintar Anda.

Pola ini terdiri dari mengikuti urutan operasi tertentu dalam fungsi-fungsi Anda:

  1. Checks: Memvalidasi semua kondisi dan masukan sebelum melakukan perubahan keadaan apapun.
  2. Effects: Melakukan semua perubahan keadaan.
  3. Interactions: Semua panggilan eksternal ke kontrak lain sebaiknya dilakukan di akhir fungsi.

Access Control

Kontrol akses adalah proses pembatasan akses terhadap fitur atau sumber daya tertentu. Ini adalah mekanisme keamanan umum yang digunakan untuk mencegah akses tidak sah terhadap informasi atau tindakan yang sensitif. Dalam kontrak pintar, beberapa fungsi mungkin sering dibatasi hanya untuk pengguna atau peran tertentu.

Anda dapat mengimplementasikan pola kontrol akses untuk dengan mudah mengelola izin. Pola ini terdiri dari menentukan serangkaian peran dan menugaskan peran tersebut kepada pengguna tertentu. Setiap fungsi kemudian dapat dibatasi hanya untuk peran-peran tertentu.

#[starknet::contract]
mod access_control_contract {
    use core::starknet::storage::{
        StoragePointerReadAccess, StoragePointerWriteAccess, StorageMapReadAccess,
        StorageMapWriteAccess, Map,
    };
    use core::starknet::ContractAddress;
    use core::starknet::get_caller_address;

    trait IContract<TContractState> {
        fn is_owner(self: @TContractState) -> bool;
        fn is_role_a(self: @TContractState) -> bool;
        fn only_owner(self: @TContractState);
        fn only_role_a(self: @TContractState);
        fn only_allowed(self: @TContractState);
        fn set_role_a(ref self: TContractState, _target: ContractAddress, _active: bool);
        fn role_a_action(ref self: ContractState);
        fn allowed_action(ref self: ContractState);
    }

    #[storage]
    struct Storage {
        // Role 'owner': only one address
        owner: ContractAddress,
        // Role 'role_a': a set of addresses
        role_a: Map::<ContractAddress, bool>,
    }

    #[constructor]
    fn constructor(ref self: ContractState) {
        self.owner.write(get_caller_address());
    }

    // Guard functions to check roles

    impl Contract of IContract<ContractState> {
        #[inline(always)]
        fn is_owner(self: @ContractState) -> bool {
            self.owner.read() == get_caller_address()
        }

        #[inline(always)]
        fn is_role_a(self: @ContractState) -> bool {
            self.role_a.read(get_caller_address())
        }

        #[inline(always)]
        fn only_owner(self: @ContractState) {
            assert!(Self::is_owner(self), "Not owner");
        }

        #[inline(always)]
        fn only_role_a(self: @ContractState) {
            assert!(Self::is_role_a(self), "Not role A");
        }

        // You can easily combine guards to perform complex checks
        fn only_allowed(self: @ContractState) {
            assert!(Self::is_owner(self) || Contract::is_role_a(self), "Not allowed");
        }

        // Functions to manage roles

        fn set_role_a(ref self: ContractState, _target: ContractAddress, _active: bool) {
            Self::only_owner(@self);
            self.role_a.write(_target, _active);
        }

        // You can now focus on the business logic of your contract
        // and reduce the complexity of your code by using guard functions

        fn role_a_action(ref self: ContractState) {
            Self::only_role_a(@self);
            // ...
        }

        fn allowed_action(ref self: ContractState) {
            Self::only_allowed(@self);
            // ...
        }
    }
}