Ethereum: de historia a labs
Artículo 22 de 48
46%
completado al llegar aquí
Sistemas distribuidosNota de investigación13 min de lectura
Access control: owner, roles, multisig y timelocks
Autorización en contratos Ethereum: Ownable y Ownable2Step, roles con AccessControl, multisig con umbral y TimelockController para separar operación diaria de cambios de política.
Ruta Ethereum: 0/48
Prerequisitos recomendados
Si alguno de estos conceptos todavía está medio nebuloso, empieza por aquí. Te ahorra tropiezos y un par de cejas fruncidas.
Proxies y upgrades: cambiar el motor con el auto andando
Cómo delegatecall separa código y storage, qué fijan ERC-1967 y ERC-1822, y por qué layout, inicializadores y admin deciden si un upgrade es seguro.
Blockchain17 min de lectura→Cuentas, claves y direcciones sin humo
Cómo una clave secp256k1 se convierte en dirección con Keccak-256, qué valida el checksum EIP-55 y qué cambia EIP-7702 para quien custodia claves.
Blockchain21 min de lectura→
En una API REST, la autorización suele vivir en un middleware que consulta un servicio de identidad. Si la política cambia, alguien despliega una configuración nueva y el equipo de operaciones puede revertirla. En un contrato Ethereum, la autorización es código público que compara direcciones, y la "configuración" es storage que solo ciertas direcciones pueden cambiar. No hay consola de administración por encima: quien controla esas direcciones controla el contrato.
Este capítulo recorre las herramientas habituales, de la más simple a la más gobernada: un owner, roles con permisos acotados, un multisig que exige varias firmas y un timelock que obliga a esperar antes de ejecutar. La idea central es separar dos caminos: la operación diaria, rápida y limitada, y el cambio de política, lento y visible.
Brújula de lectura
Actor, evidencia y estado
Actor
Titular de un rolUna EOA, un multisig o un timelock cuya dirección pasa el chequeo de msg.sender.
Evidencia
Eventos RoleGranted y CallScheduledCada cambio de permisos y cada operación programada queda en los logs.
Estado
Mapa de roles en storageQuién tiene qué rol y quién administra cada rol.
Preguntas guía
- Si mañana se filtra una clave privada de tu equipo, ¿qué puede hacer con ella y durante cuánto tiempo?
- ¿Qué diferencia hay entre tener un rol y poder otorgarlo a otros?
- ¿Por qué
msg.sendery notx.originpara autorizar? - ¿Qué gana un usuario cuando el admin es un timelock y no una cuenta común?
- ¿Cómo listarías hoy todas las direcciones con poder sobre tu sistema?
Modelo mínimo: owner, onlyOwner y transferencias en dos pasos
El patrón más simple es un owner: una dirección que puede ejecutar tareas administrativas. Ownable de OpenZeppelin fija el owner inicial en el constructor y agrega el modificador onlyOwner, transferOwnership y renounceOwnership. Renunciar deja todas las funciones onlyOwner sin nadie que pueda llamarlas, para siempre. [1]
El error operativo más común es transferir el owner a una dirección equivocada, por ejemplo una que nadie controla. Ownable2Step lo evita: el owner actual propone y el nuevo owner debe llamar acceptOwnership para que el cambio tenga efecto. [2] Es el mismo principio que un cambio de DNS con verificación: el receptor demuestra que existe antes de recibir el control.
Hay un detalle del lenguaje que vale fijar ahora. La autorización se hace con msg.sender, la dirección que llamó directamente a la función. tx.origin es la cuenta que firmó la transacción original. La documentación de Solidity dice "nunca uses tx.origin para autorización": si el owner es engañado para llamar a un contrato malicioso, ese contrato puede llamar a tu wallet y tx.origin seguirá siendo el owner. [4]
Grafo de permisos: roles, admins y mínimo privilegio
Un owner único mezcla tareas con riesgos muy distintos: pagar una factura, cambiar un límite, actualizar la lógica. El control de acceso basado en roles separa esas tareas. En AccessControl, cada rol es un identificador bytes32, normalmente keccak256("NOMBRE_ROLE"), y el modificador onlyRole revierte si quien llama no lo tiene. OpenZeppelin lo presenta como aplicación del principio de mínimo privilegio. [1]
La parte que más se pasa por alto es el admin de cada rol. Tener un rol no permite otorgarlo; eso lo decide el rol admin asociado. Por defecto, el admin de todos los roles es DEFAULT_ADMIN_ROLE, que además es admin de sí mismo. Quien lo tiene puede darse cualquier rol. OpenZeppelin advierte ese riesgo y ofrece AccessControlDefaultAdminRules, que limita el admin a una sola cuenta y exige una transferencia en dos pasos con demora. [1]
Por eso conviene dibujar los permisos como un grafo, no como una lista de modificadores. Cada arista dice "esta dirección puede llamar esta función" o "este rol puede otorgar este otro". La pregunta de auditoría es: ¿qué nodo, si se compromete, alcanza a todos los demás? AccessControl básico no enumera miembros en cadena; para reconstruir el grafo se procesan los eventos RoleGranted y RoleRevoked, o se usa la extensión AccessControlEnumerable. [1]
Este tesoro separa un rol operativo, PAYER_ROLE, de la política, maxPayment, que solo cambia el admin. Compila con Solidity 0.8.26 y OpenZeppelin Contracts 5.4; es ilustrativo:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
contract Treasury is AccessControl {
bytes32 public constant PAYER_ROLE = keccak256("PAYER_ROLE");
uint256 public maxPayment;
event Paid(address indexed to, uint256 amount);
constructor(address admin, uint256 initialMax) {
_grantRole(DEFAULT_ADMIN_ROLE, admin); // admin: idealmente un timelock
maxPayment = initialMax;
}
receive() external payable {}
// Operación diaria: rol acotado, monto acotado.
function pay(address payable to, uint256 amount) external onlyRole(PAYER_ROLE) {
require(amount <= maxPayment, "over limit");
(bool ok, ) = to.call{value: amount}("");
require(ok, "transfer failed");
emit Paid(to, amount);
}
// Cambio de política: solo el admin.
function setMaxPayment(uint256 newMax) external onlyRole(DEFAULT_ADMIN_ROLE) {
maxPayment = newMax;
}
}Multisig: varias firmas para una sola dirección
Un rol crítico en manos de una EOA, una cuenta controlada por una sola clave privada, tiene un único punto de falla. Un multisig es un contrato que actúa como cuenta y solo ejecuta una transacción si la aprueban al menos k de sus n dueños. En Safe, un multisig de código abierto para Ethereum, el contrato guarda la lista de owners y un threshold, que debe ser mayor que cero y no superar la cantidad de owners. [5]
OpenZeppelin recuerda que un contrato puede ser owner de otro: el owner de tu tesoro puede ser un Safe con un umbral de 2 de 3, por ejemplo. [1] Para un equipo de custodia, el umbral es una decisión de diseño con dos riesgos opuestos: un umbral bajo facilita el robo con pocas claves, y uno alto facilita el bloqueo si se pierden claves. Ninguno de los dos se arregla después sin la aprobación del propio multisig.
Un multisig reduce el riesgo de una clave robada, pero no el de una decisión mala tomada rápido. Si los firmantes aprueban algo dañino, o son engañados para firmar un payload que no entienden, el contrato lo ejecuta en la misma transacción. Ahí entra el timelock.
Estado de una operación en el timelock: programar, esperar, ejecutar
TimelockController es un contrato que se convierte en owner o admin de otros y obliga a que cada operación pase por tres estados: programada, lista y ejecutada. Quien tiene PROPOSER_ROLE llama schedule con el destino, el calldata y una demora; quien tiene EXECUTOR_ROLE llama execute cuando la demora mínima (minDelay) pasó; quien tiene CANCELLER_ROLE puede cancelar mientras tanto. [3]
El objetivo, en palabras de OpenZeppelin, es dar a los usuarios tiempo para revisar una operación de mantenimiento y salir del sistema si lo consideran conveniente. Ni Ownable ni AccessControl protegen contra un administrador que ataca su propio sistema; la demora sí da una ventana. [1]
Dos caminos de autorización: política lenta, operación acotada
Cómo leer esta figura
Diagrama en dos filas separadas por una línea punteada. Fila superior, camino lento para cambiar la política: un multisig de k de n firmantes llama schedule en un TimelockController con minDelay; el timelock llama execute sobre el Treasury, donde tiene DEFAULT_ADMIN_ROLE. Debajo del timelock: espera mayor o igual a minDelay, ventana para revisar o salir. Debajo del Treasury: grantRole, setMaxPayment, upgrades. Fila inferior, camino rápido para operar dentro de la política: una clave operativa con PAYER_ROLE llama pay(to, amount) sin demora y con límite, donde amount debe ser menor o igual a maxPayment. Nota: si la clave se filtra, el daño queda acotado por maxPayment hasta que el admin revoque.
El constructor recibe minDelay, la lista de proposers (que también reciben el rol de canceller), la lista de executors y un admin opcional. La documentación recomienda pasar address(0) como admin, o renunciarlo después de la configuración, para que el timelock se administre a sí mismo y hasta cambiar minDelay deba pasar por la demora. [3] El test siguiente, ejecutado con Foundry 1.5.1, comprueba que ejecutar antes de tiempo revierte y que después de dos días funciona:
// Fragmento del test: el timelock es DEFAULT_ADMIN_ROLE del Treasury.
function test_PolicyChangeWaitsForDelay() public {
bytes memory data = abi.encodeCall(treasury.setMaxPayment, (50 ether));
vm.prank(multisig);
timelock.schedule(address(treasury), 0, data, bytes32(0), bytes32(0), 2 days);
vm.prank(multisig);
vm.expectRevert(); // todavía no está lista
timelock.execute(address(treasury), 0, data, bytes32(0), bytes32(0));
vm.warp(block.timestamp + 2 days);
vm.prank(multisig);
timelock.execute(address(treasury), 0, data, bytes32(0), bytes32(0));
assertEq(treasury.maxPayment(), 50 ether);
}Una advertencia de la misma documentación: si el timelock se administra a sí mismo y se pierden las cuentas con rol de proposer o executor, el timelock y todo lo que controla quedan bloqueados para siempre. [1]
Riesgo operacional: separar operar de cambiar las reglas
Con estas piezas se puede escribir una política concreta. La operación diaria la hace una clave caliente con un rol estrecho y límites en el propio contrato. Los cambios de política, los otorgamientos de roles y los upgrades pasan por un multisig que propone a un timelock. Si la clave operativa se filtra, el daño máximo está acotado por el contrato hasta que el admin revoque el rol.
Quién debería tener cada poder
Para contratos con muchas funciones restringidas, OpenZeppelin ofrece AccessManager, que centraliza en un solo contrato qué rol puede llamar cada función de cada contrato, con demoras de ejecución por cuenta y la posibilidad de cerrar un contrato entero durante un incidente. [1] Cualquiera sea la herramienta, el entregable de una revisión es el mismo: una tabla de direcciones con poder, qué pueden hacer, con qué demora y quién las vigila.
Conexiones dentro de la serie
- Antes: Cuentas, claves y direcciones sin humo distingue EOAs de cuentas con código, la base para entender multisigs.
- Antes: Proxies y upgrades: cambiar el motor con el auto andando muestra el poder más peligroso que este capítulo enseña a gobernar.
- Antes: Reentrancy: la llamada externa que muerde de vuelta, la otra gran familia de fallas en contratos.
- Después: Approvals, permit y phishing de firmas lleva la pregunta "quién puede mover qué" a los tokens de los usuarios.
- Más adelante: ERC-4337: UserOperation, bundlers y EntryPoint generaliza la idea de cuentas con reglas de autorización programables.
Bibliografía académica
- [1]
OpenZeppelin, "Access Control," OpenZeppelin Contracts 5.x Documentation.
Ownable, Ownable2Step, AccessControl, admin de roles, DEFAULT_ADMIN_ROLE, enumeración de roles, TimelockController y AccessManager.
- [2]
OpenZeppelin, "Access Control," OpenZeppelin Contracts 5.x API Reference.
API de Ownable, Ownable2Step (pendingOwner, acceptOwnership), AccessControl y AccessControlDefaultAdminRules.
- [3]
OpenZeppelin, "Governance," OpenZeppelin Contracts 5.x API Reference.
TimelockController: constructor(minDelay, proposers, executors, admin), schedule, execute, cancel, getMinDelay, updateDelay y estados de operación.
- [4]
Solidity Team, "Security Considerations: tx.origin," Solidity Documentation.
Regla: nunca usar tx.origin para autorización, con ejemplo de wallet atacable mediante un contrato intermedio.
- [5]
S. George and R. Meissner, "OwnerManager.sol," Safe Smart Account source code, GitHub repository safe-global/safe-smart-account.
Gestión de owners y threshold; validaciones de que el umbral sea mayor que cero y no supere el número de owners.
Rutas vecinas
Si este tema te abrió hambre, sigue por aquí.
Series y labs que comparten área o dependen de esta ruta. Pocas opciones, para no convertir la curiosidad en menú infinito.
Series relacionadas
HotStuff: de cero a estado del arte
Ruta progresiva para entender consenso BFT moderno: quórums, sincronía parcial, locks, taxonomía one/two/three-chain, QC/highQC, multipipeline, view-change, responsiveness, AggQC, Fast-HotStuff, sync y frontera DAG-BFT.
Substrate: del runtime al consenso propio
Serie de 20 capítulos, de neófito a experto, sobre Substrate (Polkadot SDK): runtime en Wasm, FRAME, extrinsics, estado, pesos, actualizaciones sin fork, Aura, BABE, GRANDPA, sincronización, parachains, XCM, Sassafras, BEEFY y cómo conectar un consenso BFT propio a un nodo.
Ruta práctica: activos digitales en la operación
Nueve módulos, de menor a mayor dependencia, para decidir cómo usar custodia, wallets, llaves, firmas multifirma y MPC, AML/KYT y stablecoins con Fireblocks, BitGo, Kaleido, Circle y Tether.
Labs para convertir lectura en práctica
Prueba de trabajo y reajuste de dificultad
Laboratorio determinista para medir el costo de la prueba de trabajo y ver cómo el reajuste por altura de bloque estabiliza el tiempo entre bloques.
Consensus benchmark
Banco reproducible para comparar consenso BFT y documentar los trade-offs de HotStuff-2.
Ethereum local: Anvil, wallet y primera transacción
Laboratorio base para levantar una red local, crear cuentas, firmar transacciones y observar receipts sin gastar ETH real.
RPC y Cast: leer Ethereum con lupa
Ejercicios con JSON-RPC y Cast para consultar bloques, nonces, gas, receipts, logs y errores reproducibles.
Sigue leyendo
Ideas conectadas con este artículo
Una ruta corta para continuar sin abrir veinte pestañas y perder el hilo.
ERC-20: simple por fuera, tramposo por dentro
Token fungible, balances, transfers, approvals, allowances y riesgos comunes del estándar ERC-20.
Proxies y upgrades: cambiar el motor con el auto andando
Delegatecall, transparent proxies, UUPS, storage collisions y gobernanza de upgrades.
Testing serio: unit, fork, fuzz e invariantes
Cómo probar contratos con unit tests, fork tests, fuzzing e invariants antes de confiar en un deploy.
Reentrancy: la llamada externa que muerde de vuelta
Cómo funciona reentrancy, por qué The DAO sigue siendo lección viva y cómo defender con CEI, pull payments y guards.