Facets & Libraries
Facets & Libraries
Understanding the core architecture: how facets and libraries work together through shared storage to build powerful, composable smart contract systems.
Overview
Compose uses a dual-component architecture where facets provide complete implementations and libraries provide reusable helper functions. Both work with the same shared storage, enabling seamless integration.
Facets are complete implementations. Libraries are helper functions. Both access the same storage.
Architecture Comparison
| Component | Purpose | Use Case |
|---|---|---|
| Facets | Complete, self-contained implementations | Use as-is in your diamond for standard functionality |
| Libraries | Helper functions for custom facets | Import into your custom facets to extend Compose |
Facets: Complete Implementations
Facets are fully functional smart contracts that can be added to your diamond:
Self-Contained
Complete implementations ready to use out of the box.
Plug & Play
Add to your diamond via the diamond cut function.
Verified
Audited, tested, and deployed on multiple chains.
Example: ERC721Facet
// ERC721Facet provides standard NFT functionality
contract ERC721Facet {
function balanceOf(address owner) external view returns (uint256) {
return LibERC721.balanceOf(owner);
}
function ownerOf(uint256 tokenId) external view returns (address) {
return LibERC721.ownerOf(tokenId);
}
function transferFrom(
address from,
address to,
uint256 tokenId
) external {
LibERC721.transferFrom(from, to, tokenId);
}
// ... more standard ERC721 functions
}
You can add ERC721Facet to your diamond and immediately have full ERC721 functionality!
Libraries: Helper Functions
Libraries provide internal functions that your custom facets can use:
Building Blocks
Reusable functions for your custom logic.
Shared Storage
Access the same data as standard facets.
Flexible
Combine with your custom logic however you need.
Example: LibERC721
// LibERC721 provides helper functions for custom facets
library LibERC721 {
function mint(address to, uint256 tokenId) internal {
// Modifies storage that ERC721Facet reads
ERC721Storage storage s = erc721Storage();
s.owners[tokenId] = to;
s.balances[to]++;
// ... emit events, etc.
}
function burn(uint256 tokenId) internal {
// Modifies same storage
ERC721Storage storage s = erc721Storage();
address owner = s.owners[tokenId];
delete s.owners[tokenId];
s.balances[owner]--;
// ... emit events, etc.
}
// ... more helper functions
}
The Magic: Shared Storage
Both facets and libraries access the same storage location in your diamond:
// Your custom facet
import {LibERC721} from "compose/LibERC721.sol";
contract GameNFTFacet {
function mintWithGameLogic(
address player,
uint256 tokenId
) external {
// Your custom game logic
require(
playerHasEnoughPoints(player),
"Not enough points"
);
// Use LibERC721 to mint
// This modifies the SAME storage that
// ERC721Facet uses!
LibERC721.mint(player, tokenId);
// Now ERC721Facet.ownerOf(tokenId)
// returns player!
updatePlayerStats(player);
}
}
You don't need to inherit from anything. You don't need to override functions. Just use the library functions and everything works together!
Visual Diagram
┌─────────────────────────────────────────┐
│ Diamond Contract │
├─────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌───────────────┐ │
│ │ ERC721Facet │ │ GameNFTFacet │ │
│ │ │ │ │ │
│ │ - balanceOf()│ │ - mint with │ │
│ │ - ownerOf() │ │ game logic │ │
│ │ - transfer() │ │ │ │
│ └──────┬───────┘ └───────┬───────┘ │
│ │ │ │
│ │ ┌─────────────────┘ │
│ ▼ ▼ │
│ ┌─────────────┐ │
│ │ LibERC721 │ │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ Storage │ │
│ │ (Shared) │ │
│ └─────────────┘ │
│ │
└─────────────────────────────────────────┘
When to Use Each
Use Facets When...
You want standard, complete functionality with no customization needed.
- Implementing standard token interfaces
- Adding access control
- Using governance systems
Use Libraries When...
You're building custom facets that need to integrate with Compose.
- Creating custom minting logic
- Building game mechanics
- Implementing custom rules
Real-World Example
Let's build a game where players earn NFTs by completing quests:
import {LibERC721} from "compose/LibERC721.sol";
import {LibAccessControl} from "compose/LibAccessControl.sol";
contract QuestRewardsFacet {
bytes32 public constant GAME_MASTER = keccak256("GAME_MASTER");
mapping(uint256 => bool) public questCompleted;
function completeQuest(
address player,
uint256 questId,
uint256 tokenId
) external {
// Use LibAccessControl to check permissions
require(
LibAccessControl.hasRole(GAME_MASTER, msg.sender),
"Not authorized"
);
// Your game logic
require(!questCompleted[questId], "Quest already claimed");
questCompleted[questId] = true;
// Use LibERC721 to mint reward NFT
LibERC721.mint(player, tokenId);
// The standard ERC721Facet.ownerOf(tokenId) now works!
// The player can transfer using ERC721Facet.transferFrom()!
}
}
Your QuestRewardsFacet mints NFTs that work with all standard ERC721 functions. No inheritance, no complexity!
Best Practices
Do: Use Libraries in Custom Facets
Import and use library functions to integrate with Compose functionality.
Do: Add Standard Facets As-Is
Use complete facets when you don't need customization.
Don't: Modify Standard Facets
Create custom facets instead. Keep standard facets unchanged.
Don't: Call Facets from Facets
Use libraries for facet-to-facet communication, not external calls.
Available Facets & Libraries
ERC20
Fungible token standard with mint, burn, and transfer functionality.
ERC721
Non-fungible token (NFT) standard with full metadata support.
ERC1155
Multi-token standard supporting both fungible and non-fungible tokens.
Access Control
Role-based permission system for managing contract access.