Implementación Diamond (EIP-2535)
El patrón Diamond se utiliza en ISBE para lograr modularidad, extensibilidad y superar las limitaciones de tamaño de contrato (24KB). Esta guía detalla cómo implementar facetas y gestionar el almacenamiento de forma segura.
1. Interfaz de Faceta (IEIP2535Introspection)
Toda faceta debe implementar la interfaz IEIP2535Introspection para que el Diamond pueda descubrir sus funciones e identificadores.
Ejemplo de Introspection
/**
* @notice Expone la lista de interfaceId implementados por la faceta.
* El diamond usa esta información para responder a supportsInterface.
*/
function interfacesIntrospection() external pure returns (bytes4[] memory interfaces_) {
return _implementedInterfaces();
}
// Implementacion interna recomendada (ej. en Common.sol)
function _implementedInterfaces()
internal pure virtual
returns (bytes4[] memory interfaces_)
{
uint256 interfacesLength = 1;
interfaces_ = new bytes4[](interfacesLength);
interfaces_[--interfacesLength] = type(IAccessControl).interfaceId;
}
/**
* @notice Define un identificador único para la lógica de negocio de la faceta.
* Permite al diamond resolver la faceta correcta durante la gestión.
*/
function businessIdIntrospection()
external pure
returns (bytes32 businessId_)
{
// keccak256('isbe.contracts.erc20.resolver.key');
businessId_ = 0x2428f215905ecd05cc26794e218b9fad455e6ae2ca828b2f1c1903e8770265ad;
}
/**
* @notice Especifica todos los selectores de funciones que esta faceta expone.
* Cualquier funcion no listada aqui resultara en FunctionNotFound.
*/
function selectorsIntrospection()
external pure
returns (bytes4[] memory selectors_)
{
uint256 selectorsLength = 12;
selectors_ = new bytes4[](selectorsLength);
selectors_[--selectorsLength] = this.initializeErc20.selector;
selectors_[--selectorsLength] = this.transfer.selector;
selectors_[--selectorsLength] = this.transferFrom.selector;
selectors_[--selectorsLength] = this.approve.selector;
selectors_[--selectorsLength] = this.allowance.selector;
selectors_[--selectorsLength] = this.balanceOf.selector;
selectors_[--selectorsLength] = this.totalSupply.selector;
selectors_[--selectorsLength] = this.decimals.selector;
selectors_[--selectorsLength] = this.name.selector;
selectors_[--selectorsLength] = this.symbol.selector;
selectors_[--selectorsLength] = this.mint.selector;
selectors_[--selectorsLength] = this.burn.selector;
}
2. Almacenamiento No Estructurado (Diamond Storage)
Para evitar colisiones de almacenamiento entre facetas que comparten el mismo contrato Diamond, es obligatorio utilizar almacenamiento no estructurado.
Paso 1: Definir la estructura de almacenamiento
struct ERC20Storage {
mapping(address account => uint256) balances;
mapping(address account => mapping(address spender => uint256)) allowances;
uint256 totalSupply;
uint8 decimals;
string name;
string symbol;
}
Paso 2: Definir la función de acceso al slot
La posición del slot se calcula como el hash keccak256 de un identificador único del namespace. Esto garantiza que dos facetas nunca colisionen en el mismo slot de almacenamiento.
function _erc20Storage() private pure returns (ERC20Storage storage storage_) {
// keccak256('isbe.contracts.erc20.storage');
bytes32 position =
0xd93ac5c223af8b55b10aca6a04761f021176cb4baf866e7484f3c8d7325c3a93;
assembly {
storage_.slot := position
}
}
Paso 3: Usar la función de acceso para todas las interacciones de estado
function _totalSupply() internal view returns (uint256) {
return _erc20Storage().totalSupply;
}
function _balanceOf(address account) internal view returns (uint256) {
return _erc20Storage().balances[account];
}
function _transfer(address from, address to, uint256 amount) internal {
ERC20Storage storage s = _erc20Storage();
require(s.balances[from] >= amount, "ERC20: saldo insuficiente");
s.balances[from] -= amount;
s.balances[to] += amount;
}
¿Por qué almacenamiento no estructurado? En el Patrón Diamond, todas las facetas se ejecutan en el mismo contexto de almacenamiento. Si cada faceta usase el layout secuencial estándar (slot 0, slot 1, etc.), las variables de distintas facetas colisionarían, causando corrupción de estado. Este patrón elimina el riesgo asignando cada struct a un slot derivado de un hash único.
3. Separación de Lógica Externa / Interna
Es una buena práctica dividir la responsabilidad en dos contratos:
- Contrato Externo (Facet): Contiene las funciones expuestas. Delega en el contrato interno.
- Contrato Interno (Library): Gestiona el almacenamiento y la lógica principal. Implementa el almacenamiento no estructurado.
4. Arquitectura y Proxies No Soportados
Proxies Prohibidos en ISBE Los siguientes tipos de proxy no pueden ser desplegados en ISBE:
- Proxy Transparente: Impide la gobernanza directa y el pausado centralizado por parte de ISBE.
- Proxy UUPS: La lógica de actualización puede ser manipulada de forma incompatible con el modelo de control de la red.
- Proxy Beacon: Incompatible con el modelo de gestión modular de múltiples lógicas de negocio.
5. Recomendación de Composición
Se recomienda usar composición en lugar de comunicación entre facetas. Si la lógica de una faceta requiere datos de otra, el patrón recomendado es acceder directamente al almacenamiento compartido (Diamond Storage) en lugar de realizar llamadas inter-faceta via delegatecall, reduciendo así el consumo de gas y la complejidad técnica.