// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; // ╔══════════════════════════════════════════════════════════════════════════════╗ // ║ NubeToken.sol v2 · Token NUBE — Omnichain Fungible Token ║ // ║ ───────────────────────────────────────────────────────────────────────── ║ // ║ ERC-20 multi-chain nativo mediante el estándar LayerZero OFT v2. ║ // ║ Control de acceso granular con roles OpenZeppelin y transferencia de ║ // ║ administrador en dos pasos con retardo (AccessControlDefaultAdminRules). ║ // ║ ║ // ║ Roles: ║ // ║ DEFAULT_ADMIN_ROLE → otorga / revoca roles; transferencia en 2 pasos ║ // ║ MINTER_ROLE → puede llamar mint() ║ // ║ BURNER_ROLE → puede llamar burnFrom() ║ // ║ PAUSER_ROLE → puede pausar / reanudar el contrato ║ // ║ ║ // ║ Titular inicial (constructor): ║ // ║ _admin (Gnosis Safe de la institución) → DEFAULT_ADMIN_ROLE + todos los ║ // ║ roles operativos + owner de Ownable (config. LayerZero). ║ // ║ El Safe arranca 1-de-1 (fundador) y escala a 2-de-3 sin redesplegar. ║ // ║ ║ // ║ Doble autoridad (ambas en el Safe): ║ // ║ · defaultAdmin() → roles del protocolo (mint/burn/pause, activación). ║ // ║ · owner() → configuración LayerZero (setPeer cross-chain). ║ // ║ ║ // ║ Transferencia / traspaso del administrador: ║ // ║ Rotación de firmante (cambia el CEO): se hace DENTRO del Safe ║ // ║ (swapOwner) — el contrato no cambia de admin. ║ // ║ Traspaso total (venta del proyecto): mover ambas autoridades a un ║ // ║ Safe nuevo del comprador: ║ // ║ 1) beginDefaultAdminTransfer(nuevoSafe) ← desde el Safe actual ║ // ║ 2) (transcurre el retardo) ║ // ║ 3) acceptDefaultAdminTransfer() ← desde el Safe nuevo ║ // ║ 4) transferOwnership(nuevoSafe) ← mover Ownable de OFT ║ // ║ El paso en 2 tiempos evita enviar el admin a una dirección errónea ║ // ║ e imposibilita el secuestro instantáneo ante una clave comprometida. ║ // ║ ║ // ║ Activación de protocolos (post-despliegue, desde el Safe): ║ // ║ token.grantRole(MINTER_ROLE, address(NubeLending)) ║ // ║ token.grantRole(BURNER_ROLE, address(NubeLending)) ║ // ║ token.grantRole(MINTER_ROLE, address(NubeLender)) ║ // ║ token.grantRole(BURNER_ROLE, address(NubeLender)) ║ // ║ ← sin transferOwnership del token a los protocolos, sin proxies ║ // ║ ║ // ║ Expansión multi-chain: ║ // ║ Desplegar el mismo contrato en Arbitrum / ETH L1. ║ // ║ Configurar peers entre chains con setPeer(). ║ // ║ Las transferencias cross-chain queman en origen y mintean en destino ║ // ║ mediante mensajería LayerZero — sin bridge externo. ║ // ║ ║ // ║ LayerZero Endpoint BSC Mainnet: ║ // ║ 0x1a44076050125825900e736c501f859c50fE728c (Chain ID LZ: 102) ║ // ║ LayerZero Endpoint BSC Testnet: ║ // ║ 0x6EDCE65403992e310A62460808c4b910D972f10f (Chain ID LZ: 10102) ║ // ╚══════════════════════════════════════════════════════════════════════════════╝ /** * @dev Dependencias: * npm install @layerzerolabs/lz-evm-oapp-v2 * npm install @openzeppelin/contracts (>= 4.9.0, requerido por * AccessControlDefaultAdminRules) * * En Remix IDE, los imports se resuelven automáticamente desde npm. * * Solidity: ^0.8.20 * Red principal: BSC Mainnet (Chain ID: 56) * Expandible a: Arbitrum, ETH Mainnet, Base, Optimism, Polygon (vía OFT) */ import { OFT } from "@layerzerolabs/lz-evm-oapp-v2/contracts/oft/OFT.sol"; import { AccessControlDefaultAdminRules } from "@openzeppelin/contracts/access/AccessControlDefaultAdminRules.sol"; import { Pausable } from "@openzeppelin/contracts/security/Pausable.sol"; /** * @title NubeToken * @author Proyecto NUBE — Universidad Nacional de Avellaneda * @notice Token fungible BEP-20 / ERC-20 nativo del ecosistema NUBE. * Implementa el estándar OFT (Omnichain Fungible Token) de LayerZero, * permitiendo transferencias nativas entre chains sin bridge externo. * El control de acceso es granular mediante roles, y la titularidad del * rol de administrador se transfiere en dos pasos con retardo, pensada * para ser custodiada por un multisig institucional (Gnosis Safe). * * @dev Diferencias respecto a NubeToken v1 (Ownable): * - Sistema de roles en lugar de Ownable: MINTER_ROLE y BURNER_ROLE * pueden ser otorgados a múltiples contratos simultáneamente. * - AccessControlDefaultAdminRules: un único DEFAULT_ADMIN a la vez y * transferencia de ese rol en dos pasos con retardo, cancelable. * - Estándar OFT de LayerZero: transferencias cross-chain nativas. * - Solidity 0.8.20 en lugar de 0.8.4. * - No hay ERC20Snapshot (incompatible con OFT; usar ERC20Votes en v3). * Ver Capítulo 4.6 (gobernanza) y 4.8 (historial de decisiones). */ contract NubeToken is OFT, AccessControlDefaultAdminRules, Pausable { // ── Roles ───────────────────────────────────────────────────────────────── /// @notice Puede emitir nuevos tokens NUBE. /// Otorgado a: NubeLending, NubeLender, admin (estabilización). bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); /// @notice Puede quemar tokens de una cuenta con aprobación previa. /// Otorgado a: NubeLending (repago), NubeLender (retiro), admin. bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE"); /// @notice Puede pausar y reanudar el contrato. bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); // ── Constructor ─────────────────────────────────────────────────────────── /** * @param _lzEndpoint Dirección del endpoint LayerZero en la red de despliegue. * BSC Mainnet: 0x1a44076050125825900e736c501f859c50fE728c * BSC Testnet: 0x6EDCE65403992e310A62460808c4b910D972f10f * @param _admin Wallet administradora: el Gnosis Safe de la institución. * Recibe DEFAULT_ADMIN_ROLE, los roles operativos y el * owner de Ownable (config. LayerZero). * @param _adminDelay Retardo (en segundos) para la transferencia del rol de * administrador en dos pasos. Recomendado en mainnet: * >= 3 días (259200). Ajustable luego con * changeDefaultAdminDelay() (el cambio también respeta * un retardo, para que no pueda anularse de golpe). */ constructor(address _lzEndpoint, address _admin, uint48 _adminDelay) OFT("NUBE", "NUBE", _lzEndpoint, _admin) AccessControlDefaultAdminRules(_adminDelay, _admin) { // AccessControlDefaultAdminRules ya otorgó DEFAULT_ADMIN_ROLE a _admin y // habilitó la transferencia de admin en dos pasos con retardo _adminDelay. // Se otorgan explícitamente los roles operativos al mismo admin: _grantRole(MINTER_ROLE, _admin); _grantRole(BURNER_ROLE, _admin); _grantRole(PAUSER_ROLE, _admin); // OFT fija a _admin como delegate de LayerZero, pero deja el owner de // Ownable en el deployer (msg.sender). Se transfiere el owner a _admin // para que el Safe controle también la configuración cross-chain (setPeer). _transferOwnership(_admin); } // ── Emisión y destrucción ───────────────────────────────────────────────── /** * @notice Emite _amount tokens NUBE a la dirección _to. * @dev Llamado por NubeLending al abrir un préstamo, por NubeLender al * recibir un depósito de USDT, o por el admin para estabilización. * El caller debe tener MINTER_ROLE. */ function mint(address _to, uint256 _amount) external onlyRole(MINTER_ROLE) { _mint(_to, _amount); } /** * @notice Quema _amount tokens NUBE de la cuenta _account. * @dev Requiere que _account haya aprobado al caller mediante ERC-20 approve(). * Llamado por NubeLending al repagar, por NubeLender al retirar, * o por el admin para estabilización (comprar y quemar en mercado). * El caller debe tener BURNER_ROLE. */ function burnFrom(address _account, uint256 _amount) external onlyRole(BURNER_ROLE) { _spendAllowance(_account, msg.sender, _amount); _burn(_account, _amount); } // ── Pausa de emergencia ─────────────────────────────────────────────────── /// @notice Pausa todas las transferencias de tokens. Solo PAUSER_ROLE. function pause() external onlyRole(PAUSER_ROLE) { _pause(); } /// @notice Reanuda el token. Solo PAUSER_ROLE. function unpause() external onlyRole(PAUSER_ROLE) { _unpause(); } // ── Overrides requeridos ────────────────────────────────────────────────── /** * @dev Hook ejecutado antes de cualquier transferencia. * Combina la lógica de Pausable (bloqueo) con la del OFT. */ function _beforeTokenTransfer( address from, address to, uint256 amount ) internal override whenNotPaused { super._beforeTokenTransfer(from, to, amount); } /** * @dev Resuelve conflicto de herencia entre OFT y * AccessControlDefaultAdminRules. Ambos heredan de ERC165; se delega * al padre más específico. */ function supportsInterface(bytes4 interfaceId) public view override(OFT, AccessControlDefaultAdminRules) returns (bool) { return super.supportsInterface(interfaceId); } }