TinyDiamond is a minimalistic implementation of the Diamond Pattern (EIP-2535). It provides selector-based routing with access control in a lightweight, gas-efficient package.
Author: Martin Monperrus - mab.xyz
The Diamond Pattern (EIP-2535) uses a mapping to route function calls to different implementation contracts (called facets) based on their 4-byte function selectors. This allows you to:
- Split functionality across multiple specialized contracts
- Maintain a single entry point with a stable address
- Upgrade individual functions independently
- Bypass the 24KB contract size limit
- When a function is called on the proxy, the
fallback()function intercepts it - It extracts the function selector (first 4 bytes of calldata)
- Looks up the target contract address for that selector
- Optionally checks access restrictions
- Delegates the call to the target contract using
delegatecall
TinyDiamond implements the core Diamond principle—the essence, nothing more.
# Using Foundry
forge install
# Run tests
forge testinterface ITinyDiamond {
/**
* @notice Set or update the target contract for a specific selector
* To remove a selector, set the target address to address(0)
* @param selector The function selector (4 bytes)
* @param target The target contract address
*/
function sharpCut(bytes4 selector, address target) external;
/**
* @notice Restrict access to a function to a specific authorized address
* Set to address(0) to remove the restriction
* @param selector The function selector (4 bytes)
* @param target The address allowed to call this selector
*/
function restrict(bytes4 selector, address target) external;
}// Deploy TinyDiamond
TinyDiamond diamond = new TinyDiamond();
// Deploy your facet contracts
MyFacet facet1 = new MyFacet();
AnotherFacet facet2 = new AnotherFacet();
// Map function selectors to facets
diamond.sharpCut(MyFacet.myFunction.selector, address(facet1));
diamond.sharpCut(AnotherFacet.anotherFunction.selector, address(facet2));
// Call functions through the diamond
MyFacet(address(diamond)).myFunction();
AnotherFacet(address(diamond)).anotherFunction();// Deploy new implementation
MyFacetV2 facetV2 = new MyFacetV2();
// Update the selector mapping
diamond.sharpCut(MyFacet.myFunction.selector, address(facetV2));
// Calls now go to the new implementation
MyFacet(address(diamond)).myFunction(); // Uses facetV2// Remove a function by setting target to address(0)
diamond.sharpCut(MyFacet.myFunction.selector, address(0));
// Calls to this function will now revertThe contract deployer is automatically set as admin using the EIP-1967 admin slot. Only the admin can call sharpCut() and restrict().
Restrict specific functions to authorized addresses:
- Admin functions: Create more admin functions than
sharpCutandrestrict - Integration control: Restrict certain functions to authorized contracts
// Restrict a function to a specific user
diamond.restrict(MyFacet.sensitiveFunction.selector, authorizedUser);
// Restrict a function to internal calls
diamond.restrict(MyFacet.sensitiveFunction.selector, address(diamond));
// Remove restriction, now anyone can call again
diamond.restrict(MyFacet.sensitiveFunction.selector, address(0));
Facets have complete control over the diamond's storage via delegatecall. A malicious facet can:
- Modify the admin address
- Change selector mappings and restrictions
- Access and modify all storage
Always audit facets thoroughly before adding them to your diamond.
The admin has complete control over the diamond:
- Can add, update, or remove any function
- Can set or remove restrictions
- Admin address is stored in EIP-1967 slot for tooling compatibility
Protect the admin key with appropriate security measures (multisig, timelock, etc.).
Be aware of potential selector collisions:
- Different function signatures can produce the same 4-byte selector (rare but possible)
- Always verify selectors before adding them
- Use
getTarget()to check current mappings
TinyDiamond implements the core concept of EIP-2535 but omits several features for simplicity:
| Feature | TinyDiamond | Full EIP-2535 |
|---|---|---|
| Selector routing | ✅ | ✅ |
delegatecall to facets |
✅ | ✅ |
| Simple API | ✅ | ❌ |
Batch updates (diamondCut) |
❌ | ✅ |
Introspection (IDiamondLoupe) |
❌ | ✅ |
| Per-selector access control | ✅ | ❌ |
| Gas cost per call | Lower | Higher |
When to Use TinyDiamond:
- You want minimal gas overhead
- You want per-function access control
- You prefer simplicity
- You're prototyping or learning the pattern
When to Use Full EIP-2535:
- You need batch updates with initialization
- You want standardized introspection
- You require full EIP-2535 tooling compatibility
- You're building a complex system with many facets
Possible sophistications that could be added:
- Batch Updates: Support the standard
diamondCut(FacetCut[] calldata _diamondCut, address _init, bytes calldata _calldata)method - Introspection: Implement
IDiamondLoupefor querying facets and functions - Immutability: Support sealing to prevent further modifications
- Gas Limits: Per-selector gas budgets
- Events: Enhanced event emission for better off-chain tracking
- Storage Isolation: Patterns to prevent facet storage collisions
MIT