General Recommendations
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 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.
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 Contracts 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.
Think of smart contracts as state machines: they have a set of initial states defined by the constructor constraints, and external functions represent a set of possible state transitions. A transaction is nothing more than a state transition.
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.
Estas validaciones pueden incluir:
- 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);
}
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:
- Checks: Validate all conditions and inputs before performing any state changes.
- Effects: Perform all state changes.
- 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 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);
// ...
}
}
}