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 de Address 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 otra Address 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 efectos

    • Pide 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() vs persistent() (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> vs panic!()

    • 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)

  1. 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.

  2. Owner/Admin pattern

    • Guardar admin en storage y validar en funciones críticas. Sencillo y efectivo.

  3. Two-step role transfer (mejor práctica)

    • set_pending_admin(new) (solo admin) → accept_admin() (firma new). Evita transferencias accidentales.

  4. Multisig o permisos compuestos

    • Para operaciones críticas, delegar autorización a un contrato multisig o sistema de aprobación en varias firmas.

  5. 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

#![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 de user.

      • 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 del user. Si esperas que caller sea el que firma, usa caller.require_auth() o no pases user 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ón admin 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.: comprobar get("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 tipo NotInitialized 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:

        1. set_pending_admin(env, new_admin) (firma del admin actual).

        2. accept_admin(env) (firma del new_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ámetro user enviado por la UI está autorizado. Usa require_auth.

  • Usar unwrap() en production: causa abortos no controlados. Preferir Result.

  • 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 a panic!() 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?