General Recommendations

在开发软件时,确保其按预期运行通常比较简单而直接。然而,防止非预期的使用和漏洞可能更具挑战性。

在智能合约的开发中,安全性非常重要。仅仅一个简单的错误就可能导致宝贵资产的损失或某些功能的错误运行。

智能合约在一个公开的环境中执行,任何人都可以检查代码并与之交互。代码中的任何错误或漏洞都可能被恶意行为者利用。

本章介绍了编写安全智能合约的一般建议。在开发过程中,通过融入这些概念,你可以创建健壮可靠的智能合约。这将减少出现意外行为或漏洞的机会。

Disclaimer

本章并未提供所有可能的安全问题的详尽列表,也不能保证您的合约完全安全。

如果您正在开发用于实际生产环境中的智能合约,强烈建议由安全专家对其进行第三方审计。

Mindset

Cairo is a highly safe language inspired by Rust. It is designed in a way that forces you to cover all possible cases. Security issues on Starknet mostly arise from the way smart contract flows are designed, not much from the language itself.

采用安全思维是编写安全智能合约的第一步。在编写代码时,尽量始终考虑所有可能的场景。

Viewing Smart Contracts as Finite State Machines

智能合约中的交易是原子性的,这意味着它们要么成功,要么失败且不发生任何变化。

将智能合约视为状态机:它们有一组由构造函数约束定义的初始状态,而外部函数表示一组可能的状态转换。所谓交易也不过是一个状态转换。

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.

这些验证会包括:

  • Inputs provided by the caller
  • Execution requirements
  • Invariants (conditions that must always be true)
  • Return values from other function calls

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);
        }

使用这些函数来进行条件检查,添加一���有助于清晰地定义智能合约中每个函数可能的状态转换的边界的约束。这些检查确保合约的行为保持在预期范围内。

Recommendations

Checks Effects Interactions Pattern

检查-效果-交互模式是一种常见的设计模式,用于防止以太坊上的重入攻击。尽管在 Starknet 上更难实现重入攻击,但仍建议在智能合约中使用这种模式。

该模式由在函数中按照特定的操作顺序进行操作来实现:

  1. Checks: Validate all conditions and inputs before performing any state changes.
  2. Effects: Perform all state changes.
  3. Interactions: All external calls to other contracts should be made at the end of the function.

Access Control

访问控制是限制对特定功能或资源的访问的过程。它是一种常见的安全机制,用于防止未经授权的敏感信息访问或操作。在智能合约中,某些函数可能经常需要被限制为特定的用户或角色使用。

您可以使用访问控制模式来轻松管理权限。该模式包括定义一组不同权限角色,并给不同用户分配对应的角色。每个函数都可限制为特定的角色才可访问。

#[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);
            // ...
        }
    }
}