Understanding EIP-2535 Diamonds

The Diamonds pattern allows developers to create a single contract that can point to multiple facets, where each facet contains specific functionalities. This modular approach not only helps in managing complexity but also significantly reduces gas costs associated with contract upgrades.

Key Concepts

  • Diamond: The main contract that delegates calls to various facets.
  • Facet: A contract that contains specific functionality and can be added or removed from the diamond.
  • Selector: A unique identifier for each function, derived from the function's signature.

Setting Up the Diamond Structure

To implement the Diamonds pattern, we will create a simple example with a diamond contract that manages a simple storage functionality. The diamond will have two facets: one for setting a value and another for getting the value.

Step 1: Create the Diamond Storage

First, we need a storage structure to hold our state variables. This will be used by the diamond to manage shared state across facets.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

library DiamondStorage {
    struct AppStorage {
        uint256 value;
    }

    function diamondStorage() internal pure returns (AppStorage storage ds) {
        assembly {
            ds.slot := 0
        }
    }
}

Step 2: Create the Facets

Next, we will create two facets: one for setting the value and another for getting the value.

SetFacet.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./DiamondStorage.sol";

contract SetFacet {
    function setValue(uint256 _value) external {
        DiamondStorage.AppStorage storage ds = DiamondStorage.diamondStorage();
        ds.value = _value;
    }
}

GetFacet.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./DiamondStorage.sol";

contract GetFacet {
    function getValue() external view returns (uint256) {
        DiamondStorage.AppStorage storage ds = DiamondStorage.diamondStorage();
        return ds.value;
    }
}

Step 3: Create the Diamond Contract

Now, we will create the main diamond contract that will manage the facets.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./SetFacet.sol";
import "./GetFacet.sol";

contract Diamond {
    // Mapping of function selectors to facet addresses
    mapping(bytes4 => address) public facets;

    constructor(address _setFacet, address _getFacet) {
        // Register facets
        facets[SetFacet.setValue.selector] = _setFacet;
        facets[GetFacet.getValue.selector] = _getFacet;
    }

    fallback() external {
        address facet = facets[msg.sig];
        require(facet != address(0), "Function not found");
        assembly {
            // Delegate the call to the facet
            calldatacopy(0, 0, calldatasize())
            let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)
            // Retrieve the return data
            returndatacopy(0, 0, returndatasize())
            switch result
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }
}

Deploying the Diamond

To deploy the diamond, you will need to compile and deploy the SetFacet, GetFacet, and Diamond contracts. After deploying, you can interact with the diamond contract to set and get values.

Managing Facets

One of the powerful features of the Diamonds pattern is the ability to add or remove facets. To do this, you will need a function in the diamond contract that allows for the registration of new facets.

function addFacet(address _facet, bytes4[] memory _selectors) external {
    for (uint256 i = 0; i < _selectors.length; i++) {
        facets[_selectors[i]] = _facet;
    }
}

Best Practices

  1. Use Libraries: Utilize libraries for shared storage and utility functions to keep your code DRY (Don't Repeat Yourself).
  2. Maintain Function Selectors: Keep track of your function selectors to avoid conflicts when adding new facets.
  3. Gas Optimization: Consider gas costs when designing your facets and diamond contract structure.

Conclusion

The EIP-2535 Diamonds pattern provides a flexible and efficient way to manage complex smart contracts on Ethereum. By breaking down functionalities into facets, developers can create modular contracts that are easier to maintain and upgrade. This approach not only enhances code readability but also optimizes gas usage, making it a valuable technique for advanced Solidity developers.

Learn more with useful resources: