Diamond Implementation (EIP-2535)
The Diamond pattern is used in ISBE to achieve modularity, extensibility, and to overcome contract size limitations (24KB). This guide details how to implement facets and manage storage securely.
1. Facet Interface (IEIP2535Introspection)
Every facet must implement the IEIP2535Introspection interface so the Diamond can discover its functions and identifiers.
Introspection Example
/**
* @notice Exposes the list of interfaceId implemented by the facet.
* The diamond uses this information to respond to supportsInterface.
*/
function interfacesIntrospection() external pure returns (bytes4[] memory interfaces_) {
return _implementedInterfaces();
}
// Recommended internal implementation (e.g., in Common.sol)
function _implementedInterfaces()
internal pure virtual
returns (bytes4[] memory interfaces_)
{
uint256 interfacesLength = 1;
interfaces_ = new bytes4[](interfacesLength);
interfaces_[--interfacesLength] = type(IAccessControl).interfaceId;
}
/**
* @notice Defines a unique identifier for the facet's business logic.
* Allows the diamond to resolve the correct facet during management.
*/
function businessIdIntrospection()
external pure
returns (bytes32 businessId_)
{
// keccak256('isbe.contracts.erc20.resolver.key');
businessId_ = 0x2428f215905ecd05cc26794e218b9fad455e6ae2ca828b2f1c1903e8770265ad;
}
/**
* @notice Specifies all function selectors that this facet exposes.
* Any function not listed here will result in 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. Unstructured Storage (Diamond Storage)
To avoid storage collisions between facets sharing the same Diamond contract, it is mandatory to use unstructured storage.
Step 1: Define the storage structure
struct ERC20Storage {
mapping(address account => uint256) balances;
mapping(address account => mapping(address spender => uint256)) allowances;
uint256 totalSupply;
uint8 decimals;
string name;
string symbol;
}
Step 2: Define the slot access function
The slot position is calculated as the keccak256 hash of a unique namespace identifier. This ensures that two facets never collide in the same storage slot.
function _erc20Storage() private pure returns (ERC20Storage storage storage_) {
// keccak256('isbe.contracts.erc20.storage');
bytes32 position =
0xd93ac5c223af8b55b10aca6a04761f021176cb4baf866e7484f3c8d7325c3a93;
assembly {
storage_.slot := position
}
}
Step 3: Use the access function for all state interactions
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: insufficient balance");
s.balances[from] -= amount;
s.balances[to] += amount;
}
Why unstructured storage? In the Diamond Pattern, all facets execute in the same storage context. If each facet used the standard sequential layout (slot 0, slot 1, etc.), variables from different facets would collide, causing state corruption. This pattern eliminates the risk by assigning each struct to a slot derived from a unique hash.
3. External / Internal Logic Separation
It is a best practice to split responsibility into two contracts:
- External Contract (Facet): Contains the exposed functions. Delegates to the internal contract.
- Internal Contract (Library): Manages storage and main logic. Implements unstructured storage.
4. Architecture and Unsupported Proxies
Forbidden Proxies in ISBE The following proxy types cannot be deployed on ISBE:
- Transparent Proxy: Prevents direct governance and centralized pausing by ISBE.
- UUPS Proxy: Update logic can be manipulated in a way that is incompatible with the network's control model.
- Beacon Proxy: Incompatible with the modular management model for multiple business logics.
5. Composition Recommendation
It is recommended to use composition instead of communication between facets. If a facet's logic requires data from another, the recommended pattern is to directly access the shared storage (Diamond Storage) instead of making inter-facet calls via delegatecall, thus reducing gas consumption and technical complexity.