DEV Community

Abbas Tolgay Yılmaz
Abbas Tolgay Yılmaz

Posted on

How we used the ERC-2535 Diamonds at Proof of Peacemaking Protocol

Hey fellow devs! Today I want to share how we implemented EIP-2535 Diamond Pattern in our Proof of Peacemaking (POP) protocol. If you're not familiar with Diamond Pattern, it's like microservices for smart contracts, with its API gateway - but way cooler!

Visit the GitHub repo to see the full implementation.

🤔 Why Diamond Pattern?

Before diving into the implementation, let's talk about why we chose Diamonds:

  1. Complex Functionality Split: Our protocol handles expressions, acknowledgments, and NFTs - that's a lot of functionality to pack into a single contract. Each of these components needs its own storage and logic.

  2. Future Upgrades: We have several planned upgrades that need flexible implementation:

    • Moving gas subsidization logic off-chain
    • Implementing more sophisticated allowlist mechanisms
    • Adding new expression and acknowledgment types
    • Enhancing NFT metadata and rendering
  3. Storage Management: We're dealing with multiple storage-heavy features:

    • Gas subsidization mappings for operators and users
    • Allowlist tracking for different permission levels
    • Expression and acknowledgment content (which might move to IPFS later)
    • NFT metadata and verification data

Each contract in Solidity has 24kb limit. In our case, components will need its own storage and logic and diamond is there to save the day.

  1. Modular Development: We wanted our facets to be reusable (DRY principle FTW!). For example:

    • The gas subsidization logic could be reused in other projects
    • The NFT facet could be integrated into other diamonds
    • Expression and acknowledgment patterns might be useful for other social protocols
  2. Clean Upgrade Path: Unlike proxy patterns, Diamond Pattern gives us:

    • Ability to upgrade specific functionality without touching other parts
    • Clear separation of concerns for each component
    • Easy way to add new features without size limitations
    • No complex proxy delegation logic to manage

💡 Storage Layout: The Fun Part

The most interesting part of our implementation is how we handle storage. Instead of using the traditional AppStorage pattern, we went full Diamond Storage. Here's how:

library LibStorage {
    // Each component gets its own storage namespace
    bytes32 constant EXPRESSION_STORAGE_POSITION = keccak256("pop.v1.expression.storage");
    bytes32 constant ACKNOWLEDGEMENT_STORAGE_POSITION = keccak256("pop.v1.acknowledgement.storage");
    bytes32 constant NFT_METADATA_STORAGE_POSITION = keccak256("pop.v1.nft.metadata.storage");
    bytes32 constant GAS_COST_STORAGE_POSITION = keccak256("pop.v1.gas.cost.storage");

    // More code...
}
Enter fullscreen mode Exit fullscreen mode

See those storage positions? Each one is like a unique apartment in the blockchain for our data. We namespace them with pop.v1 to avoid any roommate disputes (storage collisions) 😉

To learn more about the storage types, check out this blog post. written by @mudgen, the creator of the EIP-2535 Diamonds.

🏗️ Architecture Breakdown

Our diamond has several facets, each with its own storage layout:

1. Expression Facet

struct ExpressionStorage {
    mapping(uint256 => Expression) expressions;
    uint256 expressionCount;
    mapping(uint256 => address[]) expressionAcknowledgers;
}
Enter fullscreen mode Exit fullscreen mode

2. Acknowledgement Facet

struct AcknowledgementStorage {
    // expressionId => acknowledger => Acknowledgement
    mapping(uint256 => mapping(address => Acknowledgement)) acknowledgements;
    uint256 acknowledgementCount;
}
Enter fullscreen mode Exit fullscreen mode

3. NFT Facet

struct POPNFTStorage {
    // Core ERC721 storage
    mapping(uint256 => address) owners;
    mapping(address => uint256) balances;
    mapping(uint256 => string) tokenURIs;
    // ... more fields
}
Enter fullscreen mode Exit fullscreen mode

🧩 Helper Structs vs Storage Structs

Here's a cool pattern we used: We separate our data structures into two categories:

  1. Helper Structs: Just data definitions, no storage position needed
struct Expression {
    address creator;
    MediaContent content;
    uint256 timestamp;
    string ipfsHash;
}
Enter fullscreen mode Exit fullscreen mode
  1. Storage Structs: The actual storage layout with a unique position
struct ExpressionStorage {
    mapping(uint256 => Expression) expressions;
    // ... more fields
}
Enter fullscreen mode Exit fullscreen mode

🔍 Accessing Storage

Each storage struct gets its own getter function:

function expressionStorage() internal pure returns (ExpressionStorage storage es) {
    bytes32 position = EXPRESSION_STORAGE_POSITION;
    assembly {
        es.slot := position
    }
}
Enter fullscreen mode Exit fullscreen mode

In our facets, we access storage like this:

function createExpression(...) external {
    LibStorage.ExpressionStorage storage es = LibStorage.expressionStorage();
    LibStorage.GasCostStorage storage gs = LibStorage.gasCostStorage();
    // Now we can use es and gs!
}
Enter fullscreen mode Exit fullscreen mode

🚀 Benefits We've Seen

  1. Clean Separation: Each component has its own storage namespace
  2. Versioning Ready: Our v1 naming makes future upgrades cleaner
  3. Reusable Facets: Any diamond can use our facets without storage conflicts
  4. Gas Efficient: Direct storage access, no proxy overhead

🎯 Tips for Your Own Implementation

  1. Keep your helper structs separate from storage structs
  2. Use consistent naming for storage positions (we use project.version.component.storage)
  3. Think about future upgrades when designing storage layout
  4. Use descriptive variable names in storage getters (es for expression storage, etc.)

🤓 Final Thoughts

Diamond Pattern might seem complex at first (I definitely scratched my head a few times), but once you get the hang of it, it's like LEGO for smart contracts. Our implementation in POP protocol shows how you can build complex functionality while keeping your code modular and upgradeable.

Remember: With great power comes great responsibility... to write clean, maintainable code! 😎


Still want to check out the full implementation? Visit our GitHub repo!

blockchain #solidity #smartcontracts #ethereum #diamondpattern #web3

Top comments (0)