Inicio / Blockchain / Blockchain: De Cero a Desarrollador / Smart Contracts: Patrones y Seguridad

Smart Contracts: Patrones y Seguridad

CEI, reentrancy, access control, proxy y auditoría.

Intermedio Funciones
🔒 Solo lectura
📖

Estás en modo lectura

Puedes leer toda la lección, pero para marcar progreso, hacer ejercicios y ganar XP necesitas una cuenta Pro.

Desbloquear por $9/mes

Smart Contracts: Patrones y Seguridad

La seguridad es el aspecto más crítico del desarrollo en blockchain. Un bug en un smart contract puede resultar en la pérdida irreversible de millones de dólares. En esta lección aprenderemos los patrones de diseño seguros y las vulnerabilidades más comunes.

Patrón Checks-Effects-Interactions (CEI)

El patrón más importante para prevenir ataques de reentrancy:

contract PatronCEI {
    mapping(address => uint) public balances;

    function retirar(uint cantidad) external {
        // 1. CHECKS: validar condiciones
        require(balances[msg.sender] >= cantidad, "Saldo insuficiente");

        // 2. EFFECTS: actualizar estado ANTES de interactuar
        balances[msg.sender] -= cantidad;

        // 3. INTERACTIONS: interactuar con contratos externos AL FINAL
        (bool sent, ) = msg.sender.call{value: cantidad}("");
        require(sent, "Fallo en transferencia");
    }
}

Reentrancy Attack

El ataque más famoso en la historia de Ethereum (The DAO hack, 2016, $60M robados):

// ❌ CONTRATO VULNERABLE
contract VaultVulnerable {
    mapping(address => uint) public balances;

    function retirar() external {
        uint bal = balances[msg.sender];
        require(bal > 0);

        // ¡PELIGRO! Interacción ANTES de actualizar estado
        (bool sent, ) = msg.sender.call{value: bal}("");
        require(sent);

        // El atacante ya volvió a llamar retirar() antes de llegar aquí
        balances[msg.sender] = 0;
    }
}

// ☠️ CONTRATO ATACANTE
contract Atacante {
    VaultVulnerable public vault;

    receive() external payable {
        // Se ejecuta cuando vault envía ETH
        // Llama retirar() de nuevo antes de que el balance se actualice
        if (address(vault).balance >= 1 ether) {
            vault.retirar();
        }
    }

    function atacar() external payable {
        vault.depositar{value: 1 ether}();
        vault.retirar(); // Inicia el ciclo
    }
}

Solución: ReentrancyGuard

// ✅ PROTEGIDO con mutex
contract VaultSeguro {
    mapping(address => uint) public balances;
    bool private locked;

    modifier noReentrant() {
        require(!locked, "No reentrant");
        locked = true;
        _;
        locked = false;
    }

    function retirar() external noReentrant {
        uint bal = balances[msg.sender];
        require(bal > 0);

        balances[msg.sender] = 0; // CEI: effects antes de interactions

        (bool sent, ) = msg.sender.call{value: bal}("");
        require(sent);
    }
}

Access Control

Ownable Pattern

contract Ownable {
    address public owner;

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    constructor() {
        owner = msg.sender;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "Not owner");
        _;
    }

    function transferOwnership(address newOwner) public onlyOwner {
        require(newOwner != address(0), "Invalid address");
        emit OwnershipTransferred(owner, newOwner);
        owner = newOwner;
    }

    function renounceOwnership() public onlyOwner {
        emit OwnershipTransferred(owner, address(0));
        owner = address(0);
    }
}

Role-Based Access Control

contract AccessControl {
    mapping(bytes32 => mapping(address => bool)) private _roles;

    bytes32 public constant ADMIN_ROLE = keccak256("ADMIN");
    bytes32 public constant MINTER_ROLE = keccak256("MINTER");

    modifier onlyRole(bytes32 role) {
        require(_roles[role][msg.sender], "Acceso denegado");
        _;
    }

    constructor() {
        _roles[ADMIN_ROLE][msg.sender] = true;
    }

    function grantRole(bytes32 role, address account) public onlyRole(ADMIN_ROLE) {
        _roles[role][account] = true;
    }

    function revokeRole(bytes32 role, address account) public onlyRole(ADMIN_ROLE) {
        _roles[role][account] = false;
    }
}

Patrón Proxy (Upgradeable Contracts)

Los smart contracts son inmutables por defecto. El patrón Proxy permite "actualizarlos":

                    Usuario
                      │
                      ▼
               ┌──────────────┐
               │    Proxy      │ ← Storage vive aquí
               │ (delegatecall)│
               └──────┬───────┘
                      │
                      ▼
               ┌──────────────┐
               │ Implementation│ ← Lógica vive aquí
               │   (v1 → v2)   │
               └──────────────┘

delegatecall: ejecuta el código de Implementation
              pero usa el storage del Proxy
contract Proxy {
    address public implementation;
    address public admin;

    constructor(address _implementation) {
        implementation = _implementation;
        admin = msg.sender;
    }

    function upgrade(address _newImpl) external {
        require(msg.sender == admin);
        implementation = _newImpl;
    }

    fallback() external payable {
        address impl = implementation;
        assembly {
            calldatacopy(0, 0, calldatasize())
            let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch result
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }
}

Overflow / Underflow

Desde Solidity 0.8+, los overflows revierten automáticamente:

contract OverflowDemo {
    // En Solidity 0.8+: revierte automáticamente
    function sumar() public pure returns (uint8) {
        uint8 x = 255;
        return x + 1; // ¡Revierte! (overflow)
    }

    // Si necesitas wrapping behavior:
    function sumarUnchecked() public pure returns (uint8) {
        uint8 x = 255;
        unchecked {
            return x + 1; // Retorna 0 (wrapping)
        }
    }
}

Otros Ataques Comunes

Front-running

Escenario:
1. Alice envía tx para comprar token a buen precio
2. Bot ve la tx en el mempool
3. Bot envía la misma tx con gas más alto → se ejecuta primero
4. El precio sube → Alice compra más caro

Mitigación:
- Commit-reveal schemes
- Usar flashbots (private mempool)
- Slippage protection

Denial of Service

// ❌ VULNERABLE: un fallo en un envío bloquea todos
contract VulnerableDoS {
    address[] public participantes;

    function distribuir() public {
        for (uint i = 0; i < participantes.length; i++) {
            // Si un participante es un contrato que revierte, bloquea todo
            payable(participantes[i]).transfer(1 ether);
        }
    }
}

// ✅ SEGURO: patrón Pull over Push
contract SeguroDoS {
    mapping(address => uint) public pendiente;

    function distribuir() public {
        for (uint i = 0; i < participantes.length; i++) {
            pendiente[participantes[i]] += 1 ether;
        }
    }

    function retirar() public {
        uint cantidad = pendiente[msg.sender];
        pendiente[msg.sender] = 0;
        payable(msg.sender).transfer(cantidad);
    }

    address[] public participantes;
}

Auditoría y Mejores Prácticas

Checklist de seguridad:
☐ Usar Checks-Effects-Interactions
☐ Aplicar ReentrancyGuard en funciones que envían ETH
☐ Validar todos los inputs con require
☐ Usar custom errors en lugar de strings largos
☐ Implementar pausability para emergencias
☐ Limitar funciones admin con access control
☐ Usar OpenZeppelin contracts cuando sea posible
☐ Escribir tests exhaustivos (incluir edge cases)
☐ Realizar auditoría antes de deploy a mainnet
☐ Usar herramientas: Slither, Mythril, Echidna

Resumen

La seguridad en smart contracts es no negociable. El patrón CEI previene reentrancy, el control de acceso protege funciones sensibles, y el patrón proxy permite upgrades. Los ataques más comunes (reentrancy, front-running, DoS) tienen mitigaciones probadas. Siempre usar OpenZeppelin, escribir tests exhaustivos y auditar antes de deploy a mainnet.

🔒

Ejercicio práctico disponible

Vault seguro con patrones de seguridad

Desbloquear ejercicios
// Vault seguro con patrones de seguridad
// Desbloquea Pro para acceder a este ejercicio
// y ganar +50 XP al completarlo

function ejemplo() {
    // Tu código aquí...
}

¿Te gustó esta lección?

Con Pro puedes marcar progreso, hacer ejercicios, tomar quizzes, ganar XP y obtener tu constancia.

Ver planes desde $9/mes