Saltar al contenido principal

Implementación Diamond (EIP-2535)

El patrón Diamond se utiliza en ISBE para modularizar la lógica de los contratos, facilitar su extensión y evitar las limitaciones de tamaño de un contrato monolítico. Aun así, para entender bien cómo se construye una facet, lo más útil no es empezar por la introspección o por los selectores, sino por un contrato normal y ver cómo se reorganiza para adaptarlo al formato Diamond.

En esta guía usamos como ejemplo HashTimestamp, un módulo sencillo cuyo comportamiento es fácil de seguir. Primero partimos de una implementación monolítica y, después, mostramos cómo se separa en interfaz, lógica interna, contrato externo y facet.

1. Partir de un contrato normal

Antes de aplicar el patrón Diamond, HashTimestamp puede imaginarse como un contrato Solidity normal, con toda la lógica en un único archivo: almacenamiento, validaciones y funciones públicas.

Ejemplo de contrato monolítico

pragma solidity ^0.8.28;

contract HashTimestampStandalone {
event HashTimestamped(
bytes32 indexed hash,
address indexed sender,
uint256 timestamp
);

error HashAlreadyExists(bytes32 hash);

mapping(bytes32 => uint256) private hashTimestamps;

function timestampHash(bytes32 _hash) external {
require(hashTimestamps[_hash] == 0, HashAlreadyExists(_hash));

uint256 timestamp = block.timestamp;
hashTimestamps[_hash] = timestamp;

emit HashTimestamped(_hash, msg.sender, timestamp);
}

function exists(bytes32 _hash) external view returns (bool) {
return hashTimestamps[_hash] != 0;
}

function getTimestamp(bytes32 _hash) external view returns (uint256) {
return hashTimestamps[_hash];
}
}

Este contrato ya resuelve completamente el caso de uso:

  • registra un hash
  • evita duplicados
  • permite consultar si existe
  • devuelve el timestamp asociado

La adaptación a Diamond no cambia este comportamiento. Lo que cambia es la forma de organizar el código.

2. Separar la interfaz pública

El primer paso al adaptar un contrato al formato Diamond es extraer su interfaz pública. Esto permite desacoplar la definición del módulo de su implementación concreta.

Ejemplo de interfaz

pragma solidity ^0.8.28;

/// @title Interface Hash Timestamp
/// @notice Interface for a contract that timestamps hashes
interface IHashTimestamp {

/// @notice Emitted when a hash is timestamped
/// @param hash The hash that was timestamped
/// @param sender The address that submitted the hash to timestamp
/// @param timestamp The block timestamp when the hash was recorded
event HashTimestamped(
bytes32 indexed hash,
address indexed sender,
uint256 timestamp
);

error HashAlreadyExists(bytes32 hash);

/// @notice Timestamps a given hash
/// @param _hash The hash to be timestamped
function timestampHash(bytes32 _hash) external;

/// @notice Checks whether a hash has been timestamped
/// @param _hash The hash to check
/// @return exists_ True if the hash has been recorded, false in other case
function exists(bytes32 _hash) external view returns (bool exists_);

/// @notice Returns the timestamp when a hash was recorded
/// @param _hash The hash to query
/// @return timestamp_ The timestamp when the hash was recorded
function getTimestamp(
bytes32 _hash
) external view returns (uint256 timestamp_);
}

Separar la interfaz no es algo exclusivo de Diamond, pero en esta arquitectura resulta especialmente útil porque deja clara la API del módulo desde el principio.

3. Almacenamiento No Estructurado (Diamond Storage)

Una vez separada la interfaz, el siguiente paso es aislar el almacenamiento y la lógica interna del módulo. En Diamond esto es obligatorio, porque varias facets comparten el mismo contexto de almacenamiento y no pueden depender del layout secuencial estándar de Solidity.

Paso 1: mover el almacenamiento a una estructura propia

struct HashTimestampStorage {
mapping(bytes32 => uint256) hashTimestamps;
}

Paso 2: definir una función de acceso al slot

La posición del slot se calcula a partir de un identificador único del módulo. Así se evita que distintas facets escriban sobre la misma zona de almacenamiento.

function _hashTimestampStorage()
internal
pure
returns (HashTimestampStorage storage storage_)
{
bytes32 position = _HASH_TIMESTAMP_STORAGE_POSITION;
assembly {
storage_.slot := position
}
}

Paso 3: mover la lógica interna a un contrato Internal

Todo lo que en el contrato monolítico no forma parte directamente de la API pública se mueve a una capa interna: validaciones, lectura y escritura de estado y funciones auxiliares.

pragma solidity ^0.8.28;

import {_HASH_TIMESTAMP_STORAGE_POSITION} from '../constants/storagePositions.sol';
import {IHashTimestamp} from './IHashTimestamp.sol';
import {DidDocumentDetailedInternal} from '../identity/didregistry/DidDocumentDetailedInternal.sol';

/// @title HashTimestampInternal
/// @notice Internal logic for hash timestamp
/// @dev Meant to be used only by contracts extending HashTimestamp
abstract contract HashTimestampInternal is DidDocumentDetailedInternal {

/// @notice Struct storing timestamped hashes
struct HashTimestampStorage {
mapping(bytes32 => uint256) hashTimestamps;
}

/// @notice Modifier to validate that provided hash
/// @param _hash The hash to check
modifier onlyNonExistentHash(bytes32 _hash) {
_checkHash(_hash);
_;
}

function _timestampHash(bytes32 _hash) internal virtual {
uint256 timestamp = _blockTimestamp();
_hashTimestampStorage().hashTimestamps[_hash] = timestamp;
emit IHashTimestamp.HashTimestamped(_hash, msg.sender, timestamp);
}

function _exists(bytes32 _hash) internal view virtual returns (bool) {
return _getTimestamp(_hash) != 0;
}

function _getTimestamp(
bytes32 _hash
) internal view virtual returns (uint256) {
return _hashTimestampStorage().hashTimestamps[_hash];
}

function _checkHash(bytes32 _hash) internal view virtual {
require(!_exists(_hash), IHashTimestamp.HashAlreadyExists(_hash));
}

/// @notice Returns the storage slot for hash timestamp
/// @dev Uses inline assembly to return storage struct at predefined slot
/// @return storage_ The hash timestamp storage struct
function _hashTimestampStorage()
internal
pure
returns (HashTimestampStorage storage storage_)
{
bytes32 position = _HASH_TIMESTAMP_STORAGE_POSITION;
// slither-disable-start assembly
// solhint-disable-next-line no-inline-assembly
assembly {
storage_.slot := position
}
// slither-disable-end assembly
}
}

¿Por qué almacenamiento no estructurado?

En el patrón Diamond, todas las facets se ejecutan sobre el mismo almacenamiento. Si cada módulo usara variables de estado normales (slot 0, slot 1, etc.), acabarían produciéndose colisiones entre facetas. Por eso cada módulo debe encapsular su estado en una estructura propia y ubicarla en un slot fijo derivado de un identificador único.

4. Separación de Lógica Externa / Interna

Una vez extraída la lógica interna, el contrato principal del módulo se simplifica y queda como una capa externa que solo expone funciones públicas y delega en la parte interna.

Contrato externo del módulo

pragma solidity ^0.8.28;

import {IHashTimestamp} from './IHashTimestamp.sol';
import {HashTimestampInternal} from './HashTimestampInternal.sol';
import {_HASH_TIMESTAMP_ROLE} from '../constants/roles.sol';

/// @title HashTimestamp
/// @notice Implements timestamp for hashes
/// @dev Inherits from IHashTimestamp and HashTimestampInternal, providing external timestamp hashes functions
abstract contract HashTimestamp is IHashTimestamp, HashTimestampInternal {

function timestampHash(
bytes32 _hash
)
external
override
onlyNonExistentHash(_hash)
whenNotPaused
onlyRole(_HASH_TIMESTAMP_ROLE)
{
_timestampHash(_hash);
}

function exists(bytes32 _hash) external view override returns (bool) {
return _exists(_hash);
}

function getTimestamp(
bytes32 _hash
) external view override returns (uint256) {
return _getTimestamp(_hash);
}

function _implementedInterfaces()
internal
pure
virtual
override
returns (bytes4[] memory interfaces_)
{
uint256 interfacesLength = 1;
interfaces_ = new bytes4[](interfacesLength);
interfaces_[--interfacesLength] = type(IHashTimestamp).interfaceId;
}
}

En este punto, la lógica original ya ha quedado separada en capas:

  • Interfaz: define la API del módulo.
  • Contrato interno: contiene almacenamiento y lógica auxiliar.
  • Contrato externo: expone las funciones públicas y delega en la parte interna.

5. Adaptación final a Facet Diamond

El último paso consiste en añadir la capa específica de Diamond. Aquí no se redefine la lógica funcional del módulo, sino que se declara la información que el Diamond necesita para descubrirlo y enrutarlo correctamente.

Toda faceta debe implementar la interfaz IEIP2535Introspection para que el Diamond pueda descubrir sus funciones e identificadores.

Ejemplo de Facet

pragma solidity ^0.8.28;

import {_HASH_TIMESTAMP_RESOLVER_KEY} from '../constants/resolverKeys.sol';
import {HashTimestamp} from './HashTimestamp.sol';
import {IEIP2535Introspection} from '../proxies/eip2535/interfaces/IEIP2535Introspection.sol';

/// @title HashTimestampFacet
/// @notice Implements timestamp for hashes facet
/// @dev Inherits from HashTimestamp, providing external timestamp hashes functions
contract HashTimestampFacet is HashTimestamp, IEIP2535Introspection {

function interfacesIntrospection()
external
pure
returns (bytes4[] memory interfaces_)
{
return _implementedInterfaces();
}

function businessIdIntrospection()
external
pure
override
returns (bytes32 businessId_)
{
businessId_ = _HASH_TIMESTAMP_RESOLVER_KEY;
}

function selectorsIntrospection()
external
pure
override
returns (bytes4[] memory selectors_)
{
uint256 selectorsLength = 3;
selectors_ = new bytes4[](selectorsLength);
selectors_[--selectorsLength] = this.timestampHash.selector;
selectors_[--selectorsLength] = this.exists.selector;
selectors_[--selectorsLength] = this.getTimestamp.selector;
}
}

La parte específica del Diamond aparece aquí:

  • interfacesIntrospection(): devuelve los interfaceId soportados por la facet.
  • businessIdIntrospection(): identifica la lógica de negocio del módulo.
  • selectorsIntrospection(): declara los selectores públicos que el Diamond debe registrar.

6. Arquitectura y Proxies No Soportados

aviso

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.

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