Security Considerations

Cuando se desarrolla software, asegurarse de que funciona según lo previsto suele ser sencillo. Sin embargo, evitar usos no previstos y vulnerabilidades puede ser más difícil.

En el desarrollo de contratos inteligentes, la seguridad es muy importante. Un solo error puede provocar la pérdida de activos valiosos o el funcionamiento incorrecto de determinadas características.

Los Smart contracts se ejecutan en un entorno público en el que cualquiera puede examinar el código e interactuar con él. Cualquier error o vulnerabilidad en el código puede ser explotado por actores maliciosos.

Este capítulo presenta recomendaciones generales para escribir contratos inteligentes seguros. Al incorporar estos conceptos durante el desarrollo, puedes crear contratos inteligentes robustos y confiables. Esto reduce las posibilidades de comportamientos inesperados o vulnerabilidades.

Disclaimer

Este capítulo no proporciona una lista exhaustiva de todos los posibles problemas de seguridad, y no garantiza que sus contratos sean completamente seguros.

Si está desarrollando contratos inteligentes para su uso en producción, es muy recomendable llevar a cabo auditorías externas realizadas por expertos en seguridad.

Mindset

Cairo es un lenguaje altamente seguro inspirado en rust. Está diseñado de forma que obliga a cubrir todos los casos posibles. Los problemas de seguridad en Starknet surgen principalmente de la forma en que se diseñan los flujos de contratos inteligentes, y no tanto del propio lenguaje.

Adoptar una mentalidad de seguridad es el paso inicial para escribir contratos inteligentes seguros. Intenta considerar siempre todos los escenarios posibles al escribir código.

Viewing smart contract as Finite State Machines

Las transacciones en los smart contracts son atómicas, lo que significa que tienen éxito o fracasan sin realizar ningún cambio.

Piense en los smart contracts como máquinas de estados: tienen un conjunto de estados iniciales definidos por las restricciones del constructor, y la función externa representa un conjunto de posibles transiciones de estado. Una transacción no es más que una transición de estado.

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

Estas validaciones pueden incluir:

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

Por ejemplo, podría utilizar la función assert para validar que un usuario tiene fondos suficientes para realizar una transacción de retirada. Si la condición no se cumple, la transacción fallará y el estado del contrato no cambiará.

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

El uso de estas funciones para comprobar condiciones añade restricciones que ayudan a definir claramente los límites de las posibles transiciones de estado para cada función de tu smart contract. Estas comprobaciones garantizan que el comportamiento del contrato se mantenga dentro de los límites esperados.

Recommendations

Checks Effects Interactions Pattern

El patrón Checks Effects Interactions es un patrón de diseño común utilizado para prevenir ataques de reentrada en Ethereum. Aunque la reentrada es más difícil de conseguir en Starknet, se recomienda utilizar este patrón en los smart contracts.

El patrón consiste en seguir un orden específico de operaciones en sus funciones:

  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

El Control de Acceso es el proceso de restringir el acceso a determinadas funciones o recursos. Es un mecanismo de seguridad común utilizado para evitar el acceso no autorizado a información o acciones sensibles. En los contratos inteligentes, algunas funciones pueden a menudo estar restringidas a usuarios o roles específicos.

Puede implementar el patrón de control de acceso para gestionar fácilmente los permisos. Este patrón consiste en definir un conjunto de funciones y asignarlas a usuarios específicos. Cada función puede entonces restringirse a roles específicos.

#[starknet::contract]
mod access_control_contract {
    use starknet::ContractAddress;
    use 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: LegacyMap::<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(Contract::is_owner(self), 'Not owner');
        }

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

        // You can easily combine guards to perform complex checks
        fn only_allowed(self: @ContractState) {
            assert(Contract::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) {
            Contract::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) {
            Contract::only_role_a(@self);
        // ...
        }

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

Static analysis tool

El análisis estático se refiere al proceso de examinar el código sin su ejecución, centrándose en su estructura, sintaxis y propiedades. Consiste en analizar el código fuente para identificar posibles problemas, vulnerabilidades o infracciones de normas específicas.

Mediante la definición de normas, como convenciones de codificación o directrices de seguridad, los desarrolladores pueden utilizar herramientas de análisis estático para comprobar automáticamente el código con respecto a estas normas.

Referencias:

Last change: 2023-11-08, commit: 2e0e51b