ERC-2535
ERC-2535¶
Overview¶
ERC-2535 (Diamond Standard, also known as Diamonds, Multi-Facet Proxy) is Ethereum's diamond standard, defining a modular smart contract system that can be infinitely upgraded and extended after deployment, breaking through Ethereum's 24KB contract size limit.
The diamond standard is like a multi-faceted diamond, where each facet represents a functional module, and all facets together form a complete contract system. This design enables large and complex DeFi protocols to organize and manage unlimited functionality under a single address.
Core Concepts¶
What Is a Diamond A Diamond is a proxy contract with the following characteristics: - Single contract address - Unlimited functional extensibility - Modular code organization - Flexible upgrade mechanism
What Is a Facet A Facet is a contract that implements specific functionality: - Similar to an implementation contract or library - Each Facet contains a set of related functions - Multiple Facets can compose complete business logic - Can be independently upgraded and replaced
Differences from Traditional Proxy Patterns
| Feature | Traditional Proxy | Diamond Standard |
|---|---|---|
| Number of implementation contracts | 1 | Unlimited |
| Contract size limit | 24KB | Unlimited |
| Upgrade granularity | Entire contract | Individual functions |
| Code organization | Monolithic | Modular |
Core Components¶
Diamond Contract The main contract acting as a proxy:
contract Diamond {
// Store function selector to facet address mapping
mapping(bytes4 => address) selectorToFacet;
// Fallback function: routes all calls
fallback() external payable {
address facet = selectorToFacet[msg.sig];
require(facet != address(0), "Function does not exist");
// Delegatecall to the corresponding facet
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
}
DiamondCut The core interface for upgrade functionality:
interface IDiamondCut {
enum FacetCutAction { Add, Replace, Remove }
struct FacetCut {
address facetAddress; // Facet contract address
FacetCutAction action; // Operation type
bytes4[] functionSelectors;// Function selector list
}
function diamondCut(
FacetCut[] calldata _diamondCut,
address _init,
bytes calldata _calldata
) external;
event DiamondCut(FacetCut[] _diamondCut, address _init, bytes _calldata);
}
DiamondLoupe The diamond inspection interface for querying Diamond structure:
interface IDiamondLoupe {
struct Facet {
address facetAddress;
bytes4[] functionSelectors;
}
// Return all facet addresses
function facets() external view returns (Facet[] memory);
// Return the facet for a given function selector
function facetAddress(bytes4 _functionSelector) external view returns (address);
// Return all function selectors
function facetFunctionSelectors(address _facet) external view returns (bytes4[] memory);
// Return all facet addresses
function facetAddresses() external view returns (address[] memory);
}
Storage Patterns¶
Diamond Storage The recommended storage pattern to avoid storage collisions:
// Define storage structure
struct DiamondStorage {
mapping(address => uint256) balances;
uint256 totalSupply;
// ...other state variables
}
// Use a fixed storage position
bytes32 constant DIAMOND_STORAGE_POSITION = keccak256("diamond.standard.diamond.storage");
function diamondStorage() internal pure returns (DiamondStorage storage ds) {
bytes32 position = DIAMOND_STORAGE_POSITION;
assembly {
ds.slot := position
}
}
// Usage in a Facet
contract TokenFacet {
function transfer(address to, uint256 amount) external {
DiamondStorage storage ds = diamondStorage();
require(ds.balances[msg.sender] >= amount, "Insufficient balance");
ds.balances[msg.sender] -= amount;
ds.balances[to] += amount;
}
}
AppStorage Another storage pattern where all state variables are in a single struct:
struct AppStorage {
uint256 totalSupply;
mapping(address => uint256) balances;
mapping(address => mapping(address => uint256)) allowances;
// ...all state variables
}
// In the first storage position of all Facets
contract BaseFacet {
AppStorage internal s;
}
How It Works¶
Function Call Flow
-
User calls a function
-
Fallback function intercepts
-
Find the corresponding Facet
-
Delegatecall execution
-
Return result
Upgrade Operations¶
Add New Functionality
// Deploy new Facet
GovernanceFacet governanceFacet = new GovernanceFacet();
// Prepare DiamondCut
IDiamondCut.FacetCut[] memory cut = new IDiamondCut.FacetCut[](1);
cut[0] = IDiamondCut.FacetCut({
facetAddress: address(governanceFacet),
action: IDiamondCut.FacetCutAction.Add,
functionSelectors: [
GovernanceFacet.propose.selector,
GovernanceFacet.vote.selector,
GovernanceFacet.execute.selector
]
});
// Execute upgrade
diamond.diamondCut(cut, address(0), "");
Replace Existing Functionality
// Deploy new version of Facet
TokenFacetV2 newTokenFacet = new TokenFacetV2();
cut[0] = IDiamondCut.FacetCut({
facetAddress: address(newTokenFacet),
action: IDiamondCut.FacetCutAction.Replace,
functionSelectors: [TokenFacet.transfer.selector]
});
diamond.diamondCut(cut, address(0), "");
Remove Functionality
cut[0] = IDiamondCut.FacetCut({
facetAddress: address(0),
action: IDiamondCut.FacetCutAction.Remove,
functionSelectors: [OldFeature.deprecatedFunction.selector]
});
diamond.diamondCut(cut, address(0), "");
Initialization Data
// Deploy initialization contract
InitContract init = new InitContract();
// Call initialization during upgrade
diamond.diamondCut(
cut,
address(init),
abi.encodeWithSelector(InitContract.init.selector, constructorArgs)
);
Key Advantages¶
Breaking Contract Size Limits - Ethereum contract size limit: 24KB - A Diamond can have unlimited Facets - Each Facet can be up to 24KB - Total functionality size: Unlimited
Fine-Grained Upgrades - Only upgrade the functions that need modification - Does not affect other functionality - Reduces upgrade risk - Saves Gas fees
Single Address - Users and integrators only need to remember one address - Simplifies frontend integration - Facilitates brand recognition - Simplifies authorization management
Modular Architecture - Clear code organization - Independent functional modules - Facilitates team collaboration - Easy to maintain and test
Transparent Upgrade History - Each upgrade triggers a DiamondCut event - Complete upgrade logs - Traceable change history - Improves transparency
Use Cases¶
Large **DeFi Protocols** Complex protocols like Aave, Compound: - Lending functionality - Governance functionality - Liquidation functionality - Reward distribution - Each module can be an independent Facet
Game Contracts Blockchain games require extensive functionality: - Character management - Item system - Battle logic - Trading marketplace - Each system upgrades independently
DAO Infrastructure Contracts for decentralized organizations: - Proposal system - Voting mechanism - Treasury management - Member management - Modular extension
*NFT* Platforms** Complex NFT protocols: - Minting logic - Market trading - Royalty distribution - Metadata management - Flexible extension
Notable Implementations¶
Aavegotchi A GameFi project using the Diamond standard: - Modular game logic - Continuous feature iteration - Maintains a single contract address
Louper Diamond explorer and development tool: - Visualize Diamond structure - View all Facets - Track upgrade history
Development Tools¶
diamondABI Automatically generates the complete ABI for a Diamond:
const diamond = await ethers.getContractAt('DiamondFullABI', diamondAddress)
// Can call functions from all Facets
hardhat-deploy Supports Diamond deployment and upgrades:
louper.dev Online Diamond explorer: - Enter a Diamond address - View all Facets and functions - Track upgrade history
Security Considerations¶
Upgrade Permission Management
// Using AccessControl
modifier onlyOwner() {
require(msg.sender == owner, "Not authorized");
_;
}
function diamondCut(...) external onlyOwner {
// Upgrade logic
}
Storage Collisions - Use the Diamond Storage pattern - Each data structure uses a unique storage position - Avoid storage overwrites between Facets
Function Selector Collisions - Ensure different Facets do not have identical function signatures - DiamondCut should detect collisions - Use tools for automated checking
Initialization Safety
bool private _initialized;
function init() external {
require(!_initialized, "Already initialized");
_initialized = true;
// Initialization logic
}
Best Practices¶
Module Division - Divide Facets by functional domain - Each Facet has a single responsibility - Avoid overly large Facets - Keep code clean
Upgrade Strategy - Use timelocks to delay upgrades - Multi-sig controls upgrade permissions - Provide emergency pause mechanisms - Thoroughly test upgrade processes
Event Logging
event DiamondCut(FacetCut[] _diamondCut, address _init, bytes _calldata);
// Log all important operations
emit FunctionAdded(selector, facetAddress);
Documentation Maintenance - Document the functionality of each Facet - Describe upgrade history - Provide integration guides - Maintain ABI documentation
Limitations¶
Complexity - Steep learning curve - More complex development and debugging - Requires specialized knowledge
*Gas* Cost** - Delegatecall has additional overhead - Function lookup has a cost - Slightly more expensive than direct calls
Ecosystem Support - Relatively few tool support - Limited audit experience - Fewer community resources
Audit Challenges - Need to audit the entire Diamond system - Upgrades may introduce new vulnerabilities - Continuous security monitoring required
Future Outlook¶
The Diamond standard is becoming the preferred architecture for large and complex protocols: - More DeFi protocols adopting it - Tool ecosystem continuously improving - Best practices gradually forming - Audit standardization
Related Links¶
- ERC-2535 Official Specification
- Diamond Standard Explained (QuickNode)
- Nick Mudge's Diamond Repository
- Louper Diamond Explorer
- Awesome Diamonds Resource List