ERC-8109: Diamonds, Simplified

At the moment, yes, Blockscout requires the Diamond proxy to expose facetAddresses in order to be recognized. However, as ERC-8109 sees broader adoption, we may update the integration to rely on the functionFacetPairs getter instead, which would remove this requirement.

1 Like

@vbaranov Thank you. It would be best if Blockscout supported both facetAddresses and functionFacetPairs because ERC2535 diamonds are not required to have the functionFacetPairs function, only ERC-8109 diamonds are required to have it.

There is a bit of a chicken or the egg problem here. Some developers may only want to adopt ERC-8109 when there is enough block explorer support. But if block explorers only add support for ERC-8109 when there is enough adoption …, you see the problem? I would really appreciate it if you could add support for the functionFacetPairs to smooth adoption.

@radek The ERC-8109 standard has been updated to use delegateCalldata. Thanks again.

I think the events can be simplified into one. The current design has 3:

  • DiamondFunctionAdded(selector, facet)
  • DiamondFunctionReplaced(selector, oldFacet, newFacet)
  • DiamondFunctionRemoved(selector, oldFacet)

I think one method would be better:

  • SetDiamondFacet(selector, facet)

This would mean that configuration functions would not have to SLOAD the previous value. Since these are logs, reading the prior value can already be done by loading prior logs.

1 Like

Just sharing here an interesting thing about Diamond contracts. I used this code as an example of how you can trick people using upgradable Diamonds. This was my submission to the underhanded solidity competition.

Let’s see if you can figure out this hack!

// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0;
pragma experimental ABIEncoderV2; // Required for Diamond Standard

// Latest published version now is 1.3.5
import "https://github.com/mudgen/diamond-1/blob/1.3.5/contracts/Diamond.sol";
import "https://github.com/mudgen/diamond-1/blob/1.3.5/contracts/facets/DiamondCutFacet.sol";

library ManagerDataA {
    bytes32 constant DIAMOND_STORAGE_POSITION = keccak256("org.soliditylang.underhanded.submission224.storage");

    struct DiamondStorage {
        uint256 proposedUpgradeTime;
        bool hasAnybodyVetoed;
        
        // diamondCut() call parameters:
        IDiamondCut.FacetCut[] readyDiamondCut;
        address readyInit;
        bytes readyCalldata;
    }

    function diamondStorage() internal pure returns (DiamondStorage storage ds) {
        bytes32 position = DIAMOND_STORAGE_POSITION;
        assembly {
            ds.slot := position
        }
    }
}

contract ManagerFacet1 is DiamondCutFacet {
    function proposeUpgrade(IDiamondCut.FacetCut[] calldata proposedDiamondCut, address proposedInit, bytes calldata ProposedCalldata) public {
        assert(msg.sender == LibDiamond.contractOwner());
        ManagerDataA.diamondStorage().proposedUpgradeTime = block.timestamp;
        ManagerDataA.diamondStorage().hasAnybodyVetoed = false;
        delete ManagerDataA.diamondStorage().readyDiamondCut;
        
        for (uint256 facetIndex; facetIndex < proposedDiamondCut.length; facetIndex++) {
            ManagerDataA.diamondStorage().readyDiamondCut.push(
                FacetCut(
                    proposedDiamondCut[facetIndex].facetAddress,
                    proposedDiamondCut[facetIndex].action,
                    proposedDiamondCut[facetIndex].functionSelectors
                )
            );
        }
        
        ManagerDataA.diamondStorage().readyInit = proposedInit;
        ManagerDataA.diamondStorage().readyCalldata = ProposedCalldata;
    }

    // Anybody can veto the upgrade, that will stop the owner from upgrading
    function vetoUpgrade() public {
        ManagerDataA.diamondStorage().hasAnybodyVetoed = true;
    }

    // Give owner full permission to upgrade, re-implemented in v2
    function isUpgradeConsented() public returns(bool) {
        return true;
    }
    
    function performUpgrade() public {
        assert(isUpgradeConsented());
        assert(block.timestamp > ManagerDataA.diamondStorage().proposedUpgradeTime + 60*60*24*30);

        // These lines copy-pasted from
        // https://github.com/mudgen/diamond-1/blob/1.3.5/contracts/facets/DiamondCutFacet.sol#L26-L36
        // with variables renamed to ready* as above
        uint256 selectorCount = LibDiamond.diamondStorage().selectors.length;
        for (uint256 facetIndex; facetIndex < ManagerDataA.diamondStorage().readyDiamondCut.length; facetIndex++) {
            selectorCount = LibDiamond.addReplaceRemoveFacetSelectors(
                selectorCount,
                ManagerDataA.diamondStorage().readyDiamondCut[facetIndex].facetAddress,
                ManagerDataA.diamondStorage().readyDiamondCut[facetIndex].action,
                ManagerDataA.diamondStorage().readyDiamondCut[facetIndex].functionSelectors
            );
        }
        emit DiamondCut(ManagerDataA.diamondStorage().readyDiamondCut, ManagerDataA.diamondStorage().readyInit, ManagerDataA.diamondStorage().readyCalldata);
        LibDiamond.initializeDiamondCut(ManagerDataA.diamondStorage().readyInit, ManagerDataA.diamondStorage().readyCalldata);
        // end copy-paste

        delete ManagerDataA.diamondStorage().proposedUpgradeTime;
        delete ManagerDataA.diamondStorage().hasAnybodyVetoed;
        delete ManagerDataA.diamondStorage().readyDiamondCut;
        delete ManagerDataA.diamondStorage().readyInit;
        delete ManagerDataA.diamondStorage().readyCalldata;
    }
    
    function proposeUpgradeSEL() public pure returns(bytes4) {return this.proposeUpgrade.selector;}
    function vetoUpgradeSEL() public pure returns(bytes4) {return this.vetoUpgrade.selector;}
    function isUpgradeConsentedSEL() public pure returns(bytes4) {return this.isUpgradeConsented.selector;}
    function performUpgradeSEL() public pure returns(bytes4) {return this.performUpgrade.selector;}
}

/* ******************************************************************************************** */

contract ScholarshipFacet1 {
    // This is the well-known address of Mary Princeton, a deserving scholar
    address payable constant MARY_PRINCETON = 0x829bD824b016326a401D083B33D093a93333a830;
    
    // Only the scholar may take funds
    function takeScholarship() public {
        assert(msg.sender == MARY_PRINCETON);
        MARY_PRINCETON.transfer(address(this).balance);
    }
    
    function takeScholarshipSEL() public pure returns(bytes4) {return this.takeScholarship.selector;}
}

/* ******************************************************************************************** */

contract ManagerFacet2 is DiamondCutFacet{
    // Give owner full permission to upgrade, re-implemented in v2
    function isUpgradeConsented() public returns(bool) {
        return ManagerDataA.diamondStorage().hasAnybodyVetoed == false;
    }
    
    function isUpgradeConsentedSEL() public pure returns(bytes4) {return this.isUpgradeConsented.selector;}
}

1 Like

Indeed. We are considering to extend configurations with blueprints as we are preparing the diamond facets carving ecosystem across chains.

The case is - there are atomically specialised facets deployed (think like one facet per external call, i.e. a few tens of facets even for ERC20 diamond). Now the upgrade calldata would become relatively huge for any new diamond setup with repetitive configuration (think ERC20).

We can introduce blueprints, i.e. contracts to copy from params of facets and selectors.

ala

function upgradeDiamond(
  address[] blueprintsForAddingFunctions,
  ...
)

that would extcodecopy function selectors and facet addresses from each provided blueprint address.

@mudgen WDYT?

1 Like

@kavsky Can you please elaborate more / provide git URL on related scripts if available?

Something is happening in the Polygon ecosystem now :thinking: last month Solana jump on network… now polygon getting billions and billions money. Ii think guilty is one of the old validator.. he come and revive the money like magician’s :rofl::rofl:. For Christmas he wanted to donate a huge amount of money… but companies started compete who will give more. Contribution is failed :person_facepalming:.. but lucky old guy got 1% in OI intelligence coin :hugs:. What’s You people think.. will he get another 1% in OI for his contribution tonight? And which ecosystem would you like to invest next? Love you off one last living creator off this world Shakacro :face_blowing_a_kiss:

I noticed error FunctionNotFound(bytes4) is in the example. Can it be a MUST standard for the proxy?

1 Like

I made a PR for your consideration.

Yes, good idea!

Adding the necessary number of characters here so the website will let me reply.

@wjmelements This is under consideration.

Even with the SetDiamondFacet(selector, facet) event, an SLOAD of the previous value still needs to be done.

Because adding, replacing, removing are different operations and an SLOAD of the previous value is needed to determine which operation is being done, and if that is already known, to verify if the operation can be done.

Besides maintaining a mapping of selector -> facet address, an ERC-8109 diamond also typically maintains an array of selectors. This is necessary to implement the functionFacetPairs() introspection function.

If someone adds a function to a diamond that already exists in the diamond, then the function selector gets added to the selectors array twice which corrupts the data returned by functionFacetPairs(). This is why an SLOADis needed to see if the function already exists in the diamond, so the diamond can revert, or instead do a replacement.

The reference implementation can be seen here: https://eips.ethereum.org/EIPS/eip-8109#reference-implementation

I have started work on an assembly implementation of this ERC.

I previously suggested making this function optional. I have a local WIP for that change too. SetDiamondFacet is still a good simplification regardless, reducing gas and codesize.

1 Like

The standard is at a crossroads because I found a significant change that can be made in the architecture that improves things significantly.

Deployment and upgrade of diamonds has had a couple problems:

  1. High gas costs.
  2. Function selector management complexity
    Today, deploying or upgrading a diamond requires assembling function selectors off-chain. Existing tools like Hardhat and Foundry don’t support this natively, so developers must rely on custom scripts or third-party libraries to handle diamond “plumbing.”

The change I propose below reduces gas costs and eliminates selector management complexity.

  • Diamonds become cheaper to deploy
  • Function selectors no longer need to be gathered off-chain
  • Standard deployment tools can be used without special support

But it also solves another problem:

ERC-2535 has the introspection functions facetAddresses(), which returns the facet addresses used by a diamond, and facets() which returns the facet addresses and function selectors used by a diamond. The problem with these functions is their implementations have been complex, feeding the idea that diamonds are complex to implement or understand. Earlier we solved this problem in ERC-8109 by removing these functions and adding the functionFacetPairs() function which is far simpler to implement.

Well, the change I propose, just by accident, happens to significantly simplify the implementations of facetAddresses() facets() so we can use them again in ERC-8109. With the change, efficient implementations of these functions becomes simple and easy to understand. We can change ERC-8109 to use the original introspection functions defined by ERC-2535 and functionFacetPairs is no longer needed so we can remove it from ERC-8109.

Having ERC-8109 use the same introspection functions as ERC-2535 will probably mean that Etherscan, Blockscout and Louper will support ERC-8109 diamonds automatically.

Architecture Change

Each facet MUST implement a pure introspection function:

function functionSelectors() external pure returns (bytes4 memory selectors)

This allows a diamond to discover all selectors directly from the facet at deployment or upgrade time.

When deploying or upgrading a diamond, an array of facet addresses is required. The diamond queries each facet’s functionSelectors() function on-chain to determine which selectors to add, replace, or remove.

Selector gathering is no longer an off-chain responsibility.

This also means that ERC-8109 diamonds are facet based, not function based.

Example

Here is an example implementation of this function in DiamondInspectFacet.sol

function functionSelectors() external pure returns (bytes4[] memory selectors) {
    selectors = new bytes4[](4);
    selectors[0] = DiamondInspectFacet.facetAddress.selector;
    selectors[1] = DiamondInspectFacet.facetFunctionSelectors.selector;
    selectors[2] = DiamondInspectFacet.facetAddresses.selector;
    selectors[3] = DiamondInspectFacet.facets.selector;
}

Reduced Deployment Gas

Current diamond implementations store every selector in on-chain storage in a selectors array for introspection.

  • 1 selector = 4 bytes
  • 8 selectors fit in 1 storage slot
  • 1 slot costs ~20,000 gas

Example:
8 facets × 10 selectors = 80 selectors
→ 10 storage slots
→ ~200,000 gas

With the new model, only one selector per facet needs to be stored.

8 facets → 8 selectors → 1 slot → ~20,000 gas

~10× reduction in selector storage cost

In addition to this, since ERC-8109 is now facet-based, it can use simpler, more gas efficient facet-based events to record the addition/replacement/removal of facets.

Reducing Complexity of Introspection Functions

As mentioned above, with the change in architecture, only the first selector of a facet needs to be stored in the selectors array. This is because the introspection functions can use the first selector of a facet to get a facet’s address and then, if needed, can call functionSelectors() on the facet to get the rest of the function selectors provided by the facet.

Here is how the facetAddresses() function is implemented:

    /**
     * @notice Gets the facet addresses used by the diamond.
     * @dev If no facets are registered return empty array.
     * @return allFacets The facet addresses.
     */
    function facetAddresses() external view returns (address[] memory allFacets) {
        DiamondStorage storage s = getStorage();
        bytes4[] memory selectors = s.selectors;
        uint256 facetCount = selectors.length;
        allFacets = new address[](facetCount);
        for (uint256 selectorIndex; selectorIndex < facetCount; selectorIndex++) {
            bytes4 selector = selectors[selectorIndex];
            address facet = s.facetAndPosition[selector].facet;
            allFacets[selectorIndex] = facet;
        }
    }

Here is how the facets() function is implemented:

struct Facet {
    address facet;
    bytes4[] functionSelectors;
}

/**
 * @notice Returns the facet address and function selectors of all facets
 *         in the diamond.
 * @return facetsAndSelectors An array of Facet structs containing each
 *                            facet address and its function selectors.
 */
function facets() external view returns (Facet[] memory facetsAndSelectors) {
        DiamondStorage storage s = getStorage();
        bytes4[] memory selectors = s.selectors;
        uint256 facetCount = selectors.length;
        facetsAndSelectors = new Facet[](facetCount);
        for (uint256 selectorIndex; selectorIndex < facetCount; selectorIndex++) {
            bytes4 selector = selectors[selectorIndex];
            address facet = s.facetAndPosition[selector].facet;
            bytes4[] memory facetSelectors = IFacet(facet).functionSelectors();
            facetsAndSelectors[selectorIndex].facet = facet;
            facetsAndSelectors[selectorIndex].functionSelectors = facetSelectors;
        }
    }

The full implementation of DiamondInspectFacet.sol can be seen here: Compose/src/diamond/DiamondInspectFacet.sol at main · Perfect-Abstractions/Compose · GitHub

The implementation of the upgradeDiamond function be seen here: Compose/src/diamond/DiamondUpgradeFacet.sol at main · Perfect-Abstractions/Compose · GitHub

I am planning to update ERC-8109 with this new change. I am interested in your ideas, questions and feedback.

1 Like

I’m opposed to facet-level inspection (functionSelectors).

  1. The diamond might not wish to install every selector in a facet. Facets might implement overlapping selectors. Building the list is now more complicated than before.
  2. This prohibits single-purpose facets by requiring delegates to check selector. I am not the only person who wanted this; @radek also envisioned selector dispatch happening only in the proxy.
1 Like

Thanks for this feedback.

Yes, it is true that function level diamonds are more flexible. But I have also seen, and think, that projects will tend to make their diamond systems facet based anyway.

Yes, there is a loss of flexibility. But in these cases there are solutions. If someone doesn’t want to install every selector in the facet the person can take a copy of the source code of the facet and remove the functions they don’t want and deploy the new facet which contains only the functions they want. The same solution can be done for facets with overlapping selectors. Because facets are stateless it is easy to deploy new versions of them, as long as gas costs are not a problem.

Yes, that is a good point. However, single-purpose facets can still be used by having them check for only one selector: 0xb3f3a3bb which is for function functionSelectors(). This does make single purpose functions a little harder to implement and cost a little more gas.

1 Like

This will actually encourage the projects to be further selfish, i.e. the positive outcomes of single function facets reuse is gone.

You mentioned problems of high gas costs (for configuration) and function selector complexity.

That is exactly the reason I proposed to use blueprints - those might solve both of these issues without the need for having extra introspection function.

2 Likes

Hi @mudgen
I find this new direction interesting. I want to add some thoughts.

On removing functionFacetPairs
I did a quick check and it looks like some block explorers already detect ERC-2535 diamonds and their facets so keeping facetAddresses and facets sounds reasonable.
Just want to know if there’s a proper way to tell if a diamond is ERC-2535 or ERC-8109.
Maybe a pure version/metadata function (or interface marker) in ERC-8109?

About Facet-based upgrades
I think this is a trade-off between flexibility vs simplicity/readability.
Downside (as @wjmelements mentioned):

The diamond might not wish to install every selector in a facet.

I think this also reduces the ability to reuse arbitrary external contracts or pre-deployed ERC-2535 facets. But as you said, this seems negotiable.

Upside:

  • It forces the diamond to stay clean and consistent. Since there are no unused functions, it may improve readability and make the system easier to understand.
  • Each facet can explain itself via functionSelectors, which improves transparency.

Since ERC-8109’s goal is to simplify diamonds, this seems aligned. It may introduce some migration cost for existing projects, but for long-term or new deployments the direction looks good.

About functionSelectors
Under this model, if a facet’s functionSelectors() omits selectors (accidentally or intentionally), is that considered valid behavior, or should it be treated as an implementation bug?

To me, even though a diamond uses multiple facet contracts, it should still behave as one unified contract boundary. So missing selectors feels like a monolith bug - deployable, but functionally incorrect. Is that the intended framing?

1 Like

@radek

In what way would projects be selfish? By not making resuable facets? By using more gas?

Single function facets can be very reusable. A single function facet has a small decrease in runtime gas cost because function selector checking is skipped. Is there any other benefits to single function facets? I think that a single function facet would still benefit if it had to also implement functionSelectors() because it would only have to have a single if condition to check if the first four bytes of calldata equal 0xb3f3a3bb.

I am interested in learning how blueprints would work. Can you talk with me about it on Discord? My handle is mudgen.

1 Like

In some cases I don’t think it will matter if a diamond is ERC-2535 or ERC-8109. But tooling can tell which one it is by looking at the upgrade function. ERC-2535 diamonds uses diamondCut and ERC-8109 uses upgradeDiamond. The events are also different.

The functionSelectors function acts like an export function. It tells diamonds what functions the facet exposes to them. Accidentally omitting a function would probably be a bug. The functionSelectors function selector itself is not returned by functionSelectors. In theory a project could add other metadata type functions to a facet like functionSelectors for their own purposes and not expose them to diamonds.

Yea, that seems good to me. But I don’t think the ERC-8109 standard will require functionSelectors to return all external functions in a facet (except for functionSelectors itself), but it makes sense to do that.

2 Likes