1. Introduction
Solidity is one of the most widely used programming languages in the Web3 space, especially on Ethereum and Ethereum Virtual Machine (EVM) compatible chains. As smart contract development has evolved, developers have encountered several challenges, particularly when trying to build complex, upgradeable, and gas-efficient contracts.
One major limitation is Solidity’s 24KB maximum contract size, which restricts the amount of logic that can be deployed in a single contract. This has led developers to explore different upgradeability patterns and modular architectures.
To address these limitations, Nick Mudge proposed the Diamond Standard (EIP-2535). This standard provides a framework for building modular, upgradeable smart contracts in a structured way. By organizing contract logic into „facets“ and using a single entry point, developers can scale their applications beyond Solidity’s size limit while keeping the code maintainable and gas-efficient.
2. The Problems with Traditional Solidity Design
Before understanding the Diamond Standard, it’s important to identify the key problems it solves:
- Contract size limit: Solidity contracts cannot exceed 24KB in size when deployed, which restricts the amount of logic that can be included.
- Tight coupling: Logic is often bundled together, making it hard to isolate and upgrade specific parts.
- Upgrade limitations: Traditional upgrade patterns (e.g., proxies) can be limiting and error-prone.
- Gas inefficiency: Repetitive or redundant logic in monolithic contracts can increase gas usage.
These issues make it difficult to scale smart contract systems and maintain clean architecture.
3. What is the Diamond Standard (EIP-2535)?
The Diamond Standard is a smart contract architecture pattern that enables modularity and upgradeability. It allows you to break down your smart contract logic into separate modules called facets, while maintaining a single contract interface (the diamond).
Think of it as a smart contract plugin system:
- The Diamond acts as a router.
- The Facets contain modular business logic.
- A fallback function dynamically delegates calls to the correct facet.
This setup enables an application to have unlimited functionality, even beyond the 24KB contract limit.
Comparison with Other Patterns
Pattern | Modular | Upgradeable | Bypasses 24KB Limit |
---|---|---|---|
Proxy | No | Yes | Yes |
Beacon Proxy | Partial | Yes | Yes |
Diamond | Yes | Yes | Yes |
4. Core Building Blocks (Conceptual Foundation)
4.1 Storage Slot and Variable Packing
Solidity stores variables in 32-byte (256-bit) storage slots. To save gas, Solidity will try to pack variables tightly into the same slot if possible.
For example:
uint64 a = 1; // 8 bytes
bool b = true; // 1 byte
uint64 c = 2; // 8 bytes
All three variables can fit in a single 32-byte slot:
-
uint64
= 8 bytes -
bool
= 1 byte (packed to 1 byte)
Variable Packing Tip:
Always order small data types together to reduce unused space.
4.2 Function Call vs Delegate Call
- Call: Executes logic in the callee contract and modifies its own state.
- Delegatecall: Executes logic in the callee contract but modifies the caller contract’s state.
(bool success, ) = otherContract.call(abi.encodeWithSignature("doSomething()"));
This modifies otherContract
’s state.
(bool success, ) = otherContract.delegatecall(abi.encodeWithSignature("doSomething()"));
This modifies the caller’s state — useful for the diamond pattern where all state is kept in one place.
4.3 Function Signature & Selector
-
Function Signature: A string like
"deposit(uint256)"
- Selector: First 4 bytes of the keccak256 hash of the signature.
bytes4 selector = bytes4(keccak256("deposit(uint256)"));
Used for routing function calls in the fallback function.
5. Anatomy of the Diamond Standard
5.1 The Diamond (Entry Point)
This is the main contract. It uses a fallback()
function to dynamically route calls to the right facet using delegatecall
.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {DepositFacet} from "./DepositFacet.sol";
import {WithdrawFacet} from "./WithdrawFacet.sol";
contract Diamond {
mapping(bytes4 => address) public selectorToFacet;
constructor() {
selectorToFacet[bytes4(keccak256("deposit()"))] = address(new DepositFacet());
selectorToFacet[bytes4(keccak256("getMyBalance()"))] = address(new DepositFacet());
selectorToFacet[bytes4(keccak256("withdraw(uint256)"))] = address(new WithdrawFacet());
}
fallback() external payable {
address facet = selectorToFacet[msg.sig];
require(facet != address(0), "Function not found");
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()) }
}
}
receive() external payable {}
}
5.2 Facets
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import { LibAppStorage } from "./DiamondStorage.sol";
contract DepositFacet {
function deposit() external payable {
LibAppStorage.AppStorage storage s = LibAppStorage.getStorage();
s.balances[msg.sender] += msg.value;
s.totalBalance += msg.value;
}
function getMyBalance() external view returns (uint256) {
LibAppStorage.AppStorage storage s = LibAppStorage.getStorage();
return s.balances[msg.sender];
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import { LibAppStorage } from "./DiamondStorage.sol";
contract WithdrawFacet {
function withdraw(uint256 amount) external {
LibAppStorage.AppStorage storage s = LibAppStorage.getStorage();
require(s.balances[msg.sender] >= amount, "Insufficient balance");
s.balances[msg.sender] -= amount;
s.totalBalance -= amount;
payable(msg.sender).transfer(amount);
}
}
5.3 Diamond Storage
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
library LibAppStorage {
struct AppStorage {
uint256 totalBalance;
mapping(address => uint256) balances;
address owner;
}
bytes32 constant DIAMOND_STORAGE_POSITION = keccak256("diamond.storage.article");
function getStorage() internal pure returns (AppStorage storage s) {
bytes32 position = DIAMOND_STORAGE_POSITION;
assembly {
s.slot := position
}
}
}
6. Putting It All Together (Code Example)
Assume we have 4 files:
-
Diamond.sol
(entry point) DepositFacet.sol
WithdrawFacet.sol
DiamondStorage.sol
Deployment Steps in Remix:
- Deploy
DepositFacet
andWithdrawFacet
to get their addresses. - Deploy
Diamond.sol
, mapping selectors in the constructor. - Use the Diamond contract address in Remix’s „At Address“ to interact with it.
7. Testing & Interaction (in Remix)
- Deploy
DepositFacet
andWithdrawFacet
first to get their addresses. - Then deploy
Diamond.sol
, passing in those addresses during setup. - Use
At Address
in Remix to interact withDiamond
. - The fallback function ensures any calls to known selectors are routed to the correct facet.
8. Real-World Use Cases
- Aavegotchi uses the Diamond Standard to power its modular gaming contracts.
- Any dApp that needs to scale beyond 24KB or separate logic by domain.
Use cases include:
- DeFi protocols
- DAOs
- On-chain games
- Cross-chain bridge contracts
9. Pros and Cons
✅ Pros
- Scalable and modular
- Upgradeable
- Efficient storage organization
- Encourages separation of concerns
❌ Cons
- Complexity in setup
- Requires good understanding of low-level calls and storage
- Less tooling support than proxies
10. Conclusion
The Diamond Standard (EIP-2535) solves critical challenges in Solidity contract development: modularization, upgradability, and size limitations. By routing calls through a central diamond and organizing logic into facets, developers can write more maintainable and scalable smart contracts.
If you’re building a serious dApp with complex logic or expect your contract to grow in the future, the Diamond pattern is worth considering.
If you found this article helpful, don’t forget to leave a like and drop a comment it really helps!
🔗 Stay connected: