Autenticación
📚 Autenticación y Autorizaciones en Stellar Soroban
🎯 Parte Teórica: ¿Qué son las autorizaciones? Las autorizaciones son el conjunto de mecanismos y patrones que garantizan que solo quien tiene derecho pueda ejecutar acciones concretas en un contrato inteligente. En Soroban esto se traduce principalmente a comprobar quién firmó la transacción y comparar esa identidad con lo que el contrato espera (owner, admin, etc.).
🤔 Analogía: imagina una cerradura inteligente que solo abre con la llave correcta. require_auth()
es la comprobación de llave; el almacenamiento del contrato (env.storage()
) contiene la información sobre quién tiene la llave (admin, owner, roles).
🔍 ¿Por qué necesitamos autorizaciones? (detallado)
🛡️ Prevención de ataques: bloquea la ejecución de funciones críticas por parte de actores no autorizados (ej.: transferir fondos, cambiar parámetros).
🎛️ Control de acceso fino: separa responsabilidades (admin vs. usuario) y reduce la superficie de riesgo.
💰 Protección de activos: evita que fondos o estados sensibles sean alterados por quien no debe.
🏢 Gestión de roles: posibilita auditorías, rotación de llaves y políticas como “solo admin puede pausar contrato”.
🧪 Mejor testeo y trazabilidad: con autorizaciones claras puedes simular y probar casos de abuso (attack vectors) y asegurar que mecanismos fallan de forma controlada.
🌟 ¿Qué ofrece Soroban para autorizaciones? (explicación técnica)
Address::require_auth()
: solicita al entorno (host) que verifique que la dirección proporcionada autorizó la transacción. Si la firma/ autorización no está presente, la ejecución aborta. Esto se usa para garantizar que el titular deAddress
realmente firmó.Storage para roles:
env.storage().instance().set/get(...)
típico para guardar direcciones que representan roles (admin, oracle, treasury).Logs (
log!
): registrar quién hizo qué ayuda en auditoría y debugging en testnet.Patrones de diseño: Owner pattern, Role-based Access Control (RBAC), Two-step transfer (pending_admin → accept), multisig delegados vía contratos.
Nota práctica:
require_auth
verifica la autorización del Address pasado; si tu interfaz front-end intenta pasar otraAddress
sin su firma,require_auth()
fallará. Esto protege contra “param tampering”.
🔧 Herramientas y conceptos clave (más allá de la sintaxis)
require_auth()
— qué hace y efectosPide al host que valide que el signature set de la transacción incluye autorización para esa
Address
.Si falla, la ejecución termina en host error: el estado no cambia.
Úsalo siempre antes de ejecutar acciones irreversibles (transferencias, cambios de roles, mint/burn).
Storage:
instance()
vspersistent()
(uso práctico)instance()
es útil para datos del contrato en el contexto de su instancia (clave por Symbol). En muchos ejemplos se usa para roles.persistent()
se usa para esquemas de almacenamiento más genéricos (aunque ambos son persistentes entre ejecuciones). Mantén consistencia en tu elección.
Preferir
Result<..., ContractError>
vspanic!()
Result
permite devolver errores legibles y manejables por callers.panic!()
aborta la transacción abruptamente (útil para invariantes que no deberían romperse, pero menos amigable para testing/UX).
Diseños de seguridad
Minimiza el poder del admin: reduce funciones sensibles que solo admin puede ejecutar.
Two-step admin transfer: evita pérdida accidental de control.
Auditoría & logging: crucial durante pruebas.
Gas & storage: cada dirección que guardes consume espacio — planifica cambios de roles y rotaciones para minimizar writes innecesarios.
📋 Estrategias y patrones prácticos (con por qué y cuándo usarlas)
Funciones públicas vs privadas
Públicas: lecturas o endpoints no sensibles. Riesgo: nadie con malos fines puede cambiar estado.
Privadas/autenticadas: toda escritura importante debe exigir
require_auth
.
Owner/Admin pattern
Guardar
admin
en storage y validar en funciones críticas. Sencillo y efectivo.
Two-step role transfer (mejor práctica)
set_pending_admin(new)
(solo admin) →accept_admin()
(firma new). Evita transferencias accidentales.
Multisig o permisos compuestos
Para operaciones críticas, delegar autorización a un contrato multisig o sistema de aprobación en varias firmas.
Principio de menor privilegio
Da lo mínimo necesario a cada rol. Si solo necesita pausar, crea un rol “pauser” en vez de usar al admin para todo.
💡 Ejemplos Prácticos — Código + explicaciones detalladas (línea a línea)
🌱 Ejemplo 1 — Función pública (sin autorización)
#![no_std]
use soroban_sdk::{contract, contractimpl, log, Env, Symbol};
// 📦 Contrato simple sin autenticación
#[contract]
pub struct PublicContract;
#[contractimpl]
impl PublicContract {
pub fn hello(env: Env) -> Symbol {
// 📢 Cualquiera puede ejecutar esta función
log!(&env, "Función pública ejecutada sin autorización.");
Symbol::new(&env, "COUNTER")
}
}
Explicación profunda del ejemplo 1
#![no_std]
: estándar para contratos Soroban (sin std).use ...
: importa macros y tipos.#[contract] pub struct PublicContract;
declara el contrato.pub fn hello(env: Env) -> Symbol
¿Por qué devuelve
Symbol
?:Symbol
es un tipo barato y pequeño (clave textual) ideal para mensajes o claves. Aquí ilustramos una función pública que devuelve un identificador estático.log!(&env, "...")
: escribe un mensaje que es útil en testnet para debugging. No confíes en logs para seguridad, son solo para observabilidad.
Cuándo usar este patrón: endpoints informativos (version, status, lectura de métricas públicas).
Riesgos: nunca lances operaciones que cambien estado/tokenes desde una función pública. Si necesitas una función pública que cambie estado, añade validaciones fuertes.
🔑 Ejemplo 2 — Autenticación básica con require_auth
require_auth
#![no_std]
use soroban_sdk::{contract, contractimpl, Env, Symbol, log, Address};
#[contract]
pub struct AuthContract;
#[contractimpl]
impl AuthContract {
pub fn secure_action(env: Env, user: Address) -> Symbol {
// 🔐 Usuario debe firmar la transacción
user.require_auth();
log!(&env, "Acción segura ejecutada por un usuario autorizado.");
Symbol::new(&env, "ok")
}
}
Explicación profunda del ejemplo 2 (punto por punto)
pub fn secure_action(env: Env, user: Address) -> Symbol
user: Address
: el contrato recibe la dirección que debe autorizar la operación.user.require_auth()
: instruye al host a verificar que la transacción incluye la autorización deuser
.Efecto: si el remitente de la transacción no posee la firma de
user
, la llamada falla y no hay cambios de estado.Por qué es seguro: aun si un atacante intenta pasar otra dirección como
user
, no podrá firmar por esa dirección sin la llave privada.
Casos de uso típicos:
Cambios realizados por el propietario de una cuenta (ej.: reclamar fondos, actualizar perfil).
Acciones donde el contrato necesita confirmar que la persona indicada dio su consentimiento explícito.
Puntos a cuidar:
Inyección de
user
maliciosa: si tu front-end pasa la dirección de otro usuario pero ese otro no firmó,require_auth
fallará — es correcto.Confusión caller vs. user: el
caller
puede ser quien invoca (p. ej. una cuenta distinta o un relayer). La verificación real es la firma deluser
. Si esperas quecaller
sea el que firma, usacaller.require_auth()
o no pasesuser
como parámetro.Relayers / meta-transactions: si usas relayers, estructura tu contrato para que verifique firmas o pruebas off-chain y no confíes solo en
require_auth
sin el patrón correcto.
👑 Ejemplo 3 — Contrato con Roles (Admin + Usuario)
#![no_std]
use soroban_sdk::{contract, contractimpl, log, Env, Symbol, Address};
#[contract]
pub struct RoleBasedContract;
#[contractimpl]
impl RoleBasedContract {
// 🏗️ Inicializar contrato con un administrador
pub fn initialize(env: Env, admin: Address) -> Symbol {
admin.require_auth();
env.storage().instance().set(&Symbol::new(&env, "admin"), &admin);
log!(&env, "Contrato inicializado con administrador.");
Symbol::new(&env, "INITIALIZED")
}
// 👤 Acción para usuarios normales
pub fn user_action(env: Env, user: Address) -> Symbol {
user.require_auth();
log!(&env, "Acción de usuario ejecutada.");
Symbol::new(&env, "USER_OK")
}
// 👑 Acción solo para administradores
pub fn admin_action(env: Env, caller: Address) -> Symbol {
caller.require_auth();
let admin: Address = env.storage()
.instance()
.get(&Symbol::new(&env, "admin"))
.unwrap();
if caller != admin {
panic!("Solo el administrador puede ejecutar esta función.");
}
log!(&env, "Acción administrativa ejecutada por el admin.");
Symbol::new(&env, "ADMIN_OK")
}
// 🔄 Transferir rol de administrador
pub fn transfer_admin(env: Env, current_admin: Address, new_admin: Address) -> Symbol {
current_admin.require_auth();
let stored_admin: Address = env.storage()
.instance()
.get(&Symbol::new(&env, "admin"))
.unwrap();
if current_admin != stored_admin {
panic!("Solo el administrador actual puede transferir el rol.");
}
env.storage().instance().set(&Symbol::new(&env, "admin"), &new_admin);
log!(&env, "Administrador transferido exitosamente.");
Symbol::new(&env, "ADMIN_TRANSFERRED")
}
}
Explicación profunda del ejemplo 3 (punto por punto)
initialize
admin.require_auth()
garantiza que quien firma la transacción es la direcciónadmin
indicada. Esto evita que alguien inicialice el contrato con un admin inexistente sin su firma.env.storage().instance().set(&Symbol::new(&env, "admin"), &admin);
guarda la dirección del admin bajo la clave"admin"
.Mejora recomendada: proteger
initialize
para que solo pueda ejecutarse una vez (ej.: comprobarget("admin")
y devolver error si ya existe).
user_action
Similar a
secure_action
del ejemplo 2: patrón para acciones que un usuario hace sobre su propia cuenta.
admin_action
caller.require_auth()
exige que la persona que presenta la transacción sea quien dice ser.let admin: Address = ...get(...).unwrap();
riesgo:unwrap()
harápanic!()
si no hay admin guardado. Si el contrato no fue inicializado, esto aborta la transacción.Recomendación: manejar ese caso con
Result
y un error tipoNotInitialized
para devolver un mensaje claro.
if caller != admin { panic!(...) }
compara direcciones y aborta si no coincide.Alternativa segura: retornar
Err(ContractError::Unauthorized)
para manejo más limpio en llamadas compositoras.
transfer_admin
Patrón sencillo: el admin actual firma y reemplaza el admin con
new_admin
.Problemas potenciales: si
new_admin
es equivocada, se pierde control.Mejor patrón (recomendado): two-step transfer:
set_pending_admin(env, new_admin)
(firma del admin actual).accept_admin(env)
(firma delnew_admin
para aceptar).
Esto evita transferencias accidentales a direcciones que no controlas.
❗ Errores comunes y cómo evitarlos
Confiar en parámetros sin
require_auth
: nunca confíes en que el parámetrouser
enviado por la UI está autorizado. Usarequire_auth
.Usar
unwrap()
en production: causa abortos no controlados. PreferirResult
.Permitir re-initialization: añade guardas para que
initialize
solo corra una vez.No contemplar relayers: si usas relayers, implementa patrones de meta-transaction o firmados off-chain.
Dar demasiado poder al admin: separa roles y reduce privilegios.
🎉 Conclusión (resumen práctico)
require_auth()
es la herramienta central para comprobar identidad en Soroban.Guarda roles en storage y siempre verifica firma + almacenamiento antes de acciones sensibles.
Prefiere
Result
y errores explícitos frente apanic!()
para mejorar UX y testeo.Implementa patrones seguros (two-step transfer, multisig, timelocks) para producción.
Documenta, prueba y audita tus funciones de autorización — son la primera línea de defensa contra exploits.
Last updated
Was this helpful?