Diamond Implementation (EIP-2535)
The Diamond pattern is used in ISBE to modularize contract logic, make it easier to extend, and avoid the size limitations of a monolithic contract. Even so, to properly understand how a facet is built, it is more useful not to start with introspection or selectors, but with a regular contract and then see how it is reorganized to adapt it to the Diamond format.
In this guide, we use HashTimestamp as an example, a simple module whose behavior is easy to follow. We first start from a monolithic implementation and then show how it is split into an interface, internal logic, external contract, and facet.
1. Starting from a regular contract
Before applying the Diamond pattern, HashTimestamp can be thought of as a regular Solidity contract, with all the logic in a single file: storage, validations, and public functions.
Example of a monolithic contract
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];
}
}
This contract already fully solves the use case:
- it registers a hash
- it prevents duplicates
- it allows checking whether a hash exists
- it returns the associated timestamp
Adapting it to Diamond does not change this behavior. What changes is the way the code is organized.
2. Separating the public interface
The first step when adapting a contract to the Diamond format is to extract its public interface. This makes it possible to decouple the module definition from its concrete implementation.
Interface example
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_);
}
Separating the interface is not exclusive to Diamond, but in this architecture it is especially useful because it makes the module API clear from the beginning.
3. Unstructured Storage (Diamond Storage)
Once the interface has been separated, the next step is to isolate the module storage and internal logic. In Diamond, this is mandatory, because several facets share the same storage context and cannot rely on Solidity’s standard sequential layout.
Step 1: move storage into its own structure
struct HashTimestampStorage {
mapping(bytes32 => uint256) hashTimestamps;
}
Step 2: define a slot access function
The slot position is calculated from a unique module identifier. This prevents different facets from writing to the same storage area.
function _hashTimestampStorage()
internal
pure
returns (HashTimestampStorage storage storage_)
{
bytes32 position = _HASH_TIMESTAMP_STORAGE_POSITION;
assembly {
storage_.slot := position
}
}
Step 3: move internal logic into an Internal contract
Everything in the monolithic contract that is not directly part of the public API is moved into an internal layer: validations, state reads and writes, and helper functions.
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
}
}
Why unstructured storage?
In the Diamond pattern, all facets run against the same storage. If each module used regular state variables (slot 0, slot 1, etc.), collisions between facets would eventually occur. For that reason, each module must encapsulate its state in its own structure and place it in a fixed slot derived from a unique identifier.
4. Separating External / Internal Logic
Once the internal logic has been extracted, the main module contract becomes simpler and remains as an external layer that only exposes public functions and delegates to the internal part.
External module contract
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;
}
}
At this point, the original logic has already been split into layers:
- Interface: defines the module API.
- Internal contract: contains storage and auxiliary logic.
- External contract: exposes the public functions and delegates to the internal part.
5. Final adaptation into a Diamond Facet
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.
Every facet must implement the IEIP2535Introspection interface so that the Diamond can discover its functions and identifiers.
Facet example
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;
}
}
The Diamond-specific part appears here:
interfacesIntrospection(): returns the interface IDs supported by the facet.businessIdIntrospection(): identifies the module business logic.selectorsIntrospection(): declares the public selectors that the Diamond must register.
6. Architecture and Unsupported Proxies
Proxies Forbidden in ISBE The following proxy types cannot be deployed in ISBE:
- Transparent Proxy: prevents direct governance and centralized pausing by ISBE.
- UUPS Proxy: the upgrade logic can be manipulated in ways that are incompatible with the network’s control model.
- Beacon Proxy: incompatible with the modular management model for multiple business logic components.
7. Composition Recommendation
It is recommended to use composition instead of communication between facets. If the logic of one facet requires data from another, the recommended pattern is to access the shared storage directly (Diamond Storage) instead of performing inter-facet calls via delegatecall, thus reducing gas consumption and technical complexity.