El control de acceso define quién puede llamar a funciones específicas o modificar el comportamiento del contrato. Este artículo explica cómo Cairo implementa el control de acceso utilizando la macro assert.
Un Repaso del Control de Acceso en Solidity
En Solidity, los modifiers son una forma concisa de envolver el comportamiento alrededor de una función. Se utilizan comúnmente para el control de acceso. Considera que el siguiente contrato define un modificador onlyOwner, el cual asegura que solo el propietario del contrato pueda llamar a la función callMe:
// SPDX-License-Identifier: MIT
pragma solidity =0.8.30;
contract SomeContract {
address owner;
constructor() {
owner = msg.sender;
}
// THE `ONLYOWNER` MODIFIER
modifier onlyOwner() {
require(msg.sender == owner, "Not the owner");
_;
}
function callMe() public onlyOwner {
// callMe logic
}
}
Los modificadores te permiten mantener limpia la lógica de tu función principal moviendo las precondiciones a otro lugar, tal como lo hacemos en el modificador onlyOwner de arriba.
Cairo no tiene modificadores — cómo hace el control de acceso Cairo
En Cairo, no existe la palabra clave para modificadores. En su lugar, definimos una función regular para aplicar nuestras comprobaciones, digamos only_owner, y la invocamos dentro de la función call_me.
El código a continuación muestra un ejemplo de cómo podría verse esto:
El constructor asigna la dirección del llamador (get_caller_address() es similar al msg.sender de Solidity) a la variable owner.
#[starknet::contract]
mod SomeContract {
// import the required functions from the starknet core library
use starknet::ContractAddress;
use starknet::get_caller_address;
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[storage]
struct Storage {
owner: ContractAddress,
}
#[constructor]
fn constructor(ref self: ContractState) {
self.owner.write(get_caller_address());
}
#[generate_trait]
impl Internal of InternalTrait {
fn only_owner(self: @ContractState) {
let caller = get_caller_address();
let stored_owner = self.owner.read();
// ENSURES THE CALLER IS THE OWNER OR REVERT
assert(caller == stored_owner, 'Not owner');
}
}
#[abi(embed_v0)]
impl SomeContractImpl of super::ISomeContract<ContractState> {
// CALL_ME FUNCTION
fn call_me(ref self: ContractState) {
self.only_owner();
// callMe logic
}
}
}
Esta versión en Cairo refleja el patrón de Solidity al restringir el acceso a la función call_me. Asegura que solo el propietario pueda llamarla comprobando que la dirección del llamador coincida con la del propietario almacenado en el estado del contrato.
assert(caller == stored_owner, 'Not owner');
La función assert se comporta de manera similar al require de Solidity, el cual detiene la ejecución y revierte la transacción si la condición falla. Para hacerlo aún mejor, Cairo ofrece otra función llamada assert!, que admite mensajes de error formateados, haciéndolo más expresivo.
assert Vs assert!
Aunque la función assert y la macro assert! (el ! distingue a las macros de las funciones) sirven para el mismo propósito, asegurar que una condición sea verdadera, difieren en cómo informan los errores.
assert:
El primer argumento, condition, es una expresión booleana. Si es false, el programa lanza un panic con el mensaje de error fijo entre comillas simples.
assert(condition, 'static error message');
assert!:
- El primer argumento,
condition, es una expresión booleana. Si esfalse, el programa lanza un panic con el segundo argumento. - El segundo argumento es una cadena formateada entre comillas dobles.
assert!(condition, "Formatted error: {}", variable);
Qué significa {} en la Cadena Formateada
En una cadena formateada, {} es un marcador de posición. Cuando el código se ejecuta, el valor de variable se convierte en una cadena y se inserta donde aparece el {}.
Piénsalo como rellenar los espacios en blanco:
let name = "Alice";
println!("Hello, {}", name);
// Prints: Hello, Alice
Podemos tener múltiples marcadores de posición:
println!("x = {}, y = {}", x, y);
El orden importa: cada {} se llena con el argumento correspondiente después de la cadena.
Esto ofrece a los desarrolladores más flexibilidad al depurar o manejar errores. En lugar de una cadena estática, puedes incluir valores en tiempo de ejecución en el mensaje, algo que el require de Solidity no soporta directamente.
El método recomendado es usar
assert!, incluso en producción.
Tipos Soportados en assert!
No todos los tipos pueden ser usados dentro del mensaje de assert!. Solo los tipos que implementan el trait core::fmt::Display pueden utilizarse en el formateo del mensaje de assert!. El trait Display define cómo un tipo se convierte a una representación de cadena al usar el especificador de formato {}. Estos son:
ByteArrayboolNonZero<T>(para cualquierTque por sí mismo implementeDisplay)- Todos los tipos primitivos enteros (
felt252,u8,u16,u32,u64,u128,u256, y las variantes con signo si están presentes) @T(referencia a cualquiera de los tiposDisplayanteriores)
Por ejemplo, un tipo como felt252 funciona bien, pero los structs personalizados o tipos como ContractAddress lanzarán un error porque no implementan el trait Display.
Si intentas hacer:
let caller: ContractAddress = get_caller_address();
// ❌ This will fail to compile
assert!(caller == owner, "Caller was: {}", caller);
Verás un error como:
Trait has no implementation in context: core::fmt::Display::<core::starknet::contract_address::ContractAddress>
Para solucionar esto, puedes convertir la dirección a un felt252 si solo necesitas la representación numérica:
let caller: ContractAddress = get_caller_address();
let caller_felt: felt252 = caller.into();
// ✅ This works, assuming the `owner` variable is of type felt252 too
assert!(caller_felt == owner, "Caller was: {}", caller_felt);
Así que, aunque assert! te proporciona un manejo de errores expresivo, ten en cuenta los requisitos de tipo al formatear tus mensajes.
Ejercicio: Escribe una función en Cairo que reciba dos números, n y d, y devuelva su división. Si d es cero, la función debe revertirse con el mensaje: “n is not divisible by d” (incluyendo los valores reales de n y d en el error). Pista: usa la función assert!. Para resolver el ejercicio safe_divide, clona este repositorio.
Este artículo es parte de una serie de tutoriales sobre Cairo Programming on Starknet