ERC-8167: Modular Dispatch Proxies

This is a minimalist standard for modular dispatch proxies. Similar standards include

This is designed to be a minimum standard to allow innovation in this model while providing a shared framework to help wallets and explorers. I hope future standards will find this one extensible and build for compatibility. I am compiling a list of compatible extensions and plan to keep the list below up to date.

Compatible extensions:

Links:

I have started a reference implementation. At this stage I am seeking feedback, especially from those who have experience building these or their tooling.

3 Likes

It might be good to have the default reference implementation not to use slot zero based mapping.

Since if an implementation contract used a mapping in slot zero, you could get a situation where everything works in testing, but has the potential to break in the worst possible way under attack by giving the attacker delegatecall powers.

1 Like

I think the selectors() function should return page-based selectors, this to avoid consuming resources beyond RPC capacity when the array is large.

1 Like

I’m familiar with pagination as a strategy for serving large arrays. I’d like to avoid it if possible to simplify the API.

The selectors query is currently the least modular one and something I hope improves before the final version of this ERC. Approaches like ERC-8153 facilitate modularity by expecting facets to supply their own selectors.

I also considered a bitmap design but that seems prohibitively large for 4byte.

Are you familiar with some other RPC limits besides gas and timeout? There is an EIP to make memory gas linear instead of quadratic and that’s the main problem justifying pagination as far as I know.

32768 selectors fit in 1 MB. If we switched to a packed design, it would increase this capacity eightfold. If memory gas becomes linear, the main gas hurdle would be SLOAD, encouraging EXTCODECOPY-based solutions not currently possible in Solidity.

This is a valid concern, and the ERC already discusses defensive storage patterns. I was planning to demonstrate the first such pattern (ā€œsingle shared proxy storage layoutā€) in the reference implementation, but you have changed my mind. Storage namespaces are more foolproof.

2 Likes

Have you considered using sequential indexes to map to selectors? The idea is that we have a structure uint32 index => bytes4 selector next to bytes4 selector => (address facet, uint32 index). We could add a state variable to track the largest value of the index or use a binary search method. This allows external indexers to build different analysis strategies while maintaining the elegance of the function interface.

1 Like

This is a good idea but I don’t want to prescribe a particular data structure because I want to encompass many possible implementations. An index (and also pagination) would force indexing onto systems that are better without indexes, such as ERC-8153. ERC-8153 does pagination by facet, which is a great design. They would be able to implement selectors() with a combination of facetAddresses() and facetFunctionSelectors(address). If they had to support selector(uint32), it would be a linear operation calling exportSelectors(). For ERC-8153 to optimize selector(uint32), they would have to maintain a separate index just for this method.

1 Like

I have pushed a reference implementation for the ERC. It uses a solady-style namespace storage layout and manages the selectors in a list. It’s not especially performant because my main goal was to minimize assembly. I’ll iterate on it with some documentation on this branch and keep them synced.

Left you a few comments and found a bug.

1 Like

Thanks for you review! I appreciate it.

1 Like

Since the standard is to be common base for such kind of proxies, I am not sure about MUST requirement on implementation(bytes4) - ERC could state MUST requirement on to have some selector listing function, but the naming of such function can be up to the implementation (or extension standard). Similarily for the DelegationSet event.

By that 2535 Diamonds and 8153 Facet Diamonds would become compatible extensions.

I agree that the event should be recommended instead of required, but I think the functions should be mandatory. It is possible I am wrong because I haven’t thought too much about it. I think after some discussion we will agree.

This topic opens an important philosophical discussion about what an interface should contain and what it should not.

First, I’ll describe my notion of compatibility to distinguish it from both inheritance and a more vacuous definition. Compatibility means ā€œable to exist or occur together without conflictā€. What makes ERC 2535 and ERC 8153 compatible with ERC 8167 is not inheritance or universal support, but that the interface is not especially burdensome or contradictory. Duplicative events (e.g. FacetAdded and DelegateSet) are somewhat burdensome for gas but don’t force a particular data structure or algorithm. Duplicative methods (e.g. facetAddress and implementation) aren’t burdensome for runtime gas but do require deploying code that can read your data structure. In contrast, ERC-1538 is incompatible with ERC-8153 because ERC-1538 uses strings to surface ABI while ERC-8153 gets its ABI from its facets but those facets aren’t required to provide such strings.

Second, ERC-8167 is a good common base interface because it does not prescribe a particular data structure, and because it does enforce unnecessary requirements. What MUST means is that if you don’t have it, you aren’t in compliance. It’s important to standardize how block explorers surface the ABI. Just specifying that some combination of methods can perform similar actions is not helpful to consumers of the interface. A similar argument can be made about events, but I recognize the gas and codesize costs of log redundancy. The official standard for these imperatives is:

In particular, they MUST only be used where it is
actually required for interoperation or to limit behavior which has
potential for causing harm (e.g., limiting retransmisssions) For
example, they must not be used to try to impose a particular method
on implementors where the method is not required for
interoperability.

This interoperability standard is the criterion by which I argue the two functions should be REQUIRED. However I concede that the DelegateSet event is less important and can be demoted.

Third, why should things ever be RECOMMENDED or OPTIONAL when they can be removed entirely? They are good for harm reduction and for outlining best practices. I feel this way about the error FunctionNotFound because revert data is usually bubbled up, because error handling is so rare, and because on-chain handling of this particular error is unexpected. If the error handling were an important part of the interface, this error would be required. Similarly, there are circumstances where the DelegateSet event may be unnecessary, for example if there were a redundant and more informative event, such as the ones specified in ERC-8153. This is also why selectors() was recommended: there may be superior but noncompliant ways to surface the ABI.

In conclusion:

  • selectors() meets both the interoperability and non-conflict standards and so should be promoted to REQUIRED. Any modular dispatch proxy that can surface its entire ABI can implement this method at no additional runtime cost.
  • implementation(bytes4) meets both the interoperability and non-conflict standards and so should remain REQUIRED. Any modular dispatch proxy has to be able to calculate this in order to delegate so is existence should not impose any additional runtime cost.
  • DelegateSet(bytes4,address) meets neither the interoperability standard nor the non-conflict standard, but because of both its importance in auditing and the viability of good alternatives, it should be demoted to RECOMMENDED.
  • FunctionNotFound(bytes4) has informational importance, and some error has to be returned anyway, but there may be superior alternatives and backwards compatibility challenges, so it should remain RECOMMENDED.

Your opinion on this is important to me, so let me know if you agree with these changes and the larger framework supporting them.

1 Like

Thinking about that, in fact these proxies can easily have both (8167 and diamonds) implementations coexisting at the same time.
So this ERC does not need to limit itself.
Thus I also consider selectors() and implementation() a key minimum for tools like explorers and UI makers.

Note to:

FunctionNotFound(bytes4) has informational importance, and some error has to be returned anyway, but there may be superior alternatives and backwards compatibility challenges, so it should remain RECOMMENDED.

Agree with recommended, but disagree that the error has to be returned. We can have a fallback like / catch all pointer.

1 Like

This is a good point. I’ll change the revert specification to a recommendation to allow for extensions that could support fallback functions.

Hi @wjmelements

I like the idea of having a minimal base standard for this type of architecture.

In general, I appreciate the flexibility it provides, and I agree that ERC-8110 can be seen as a compatible extension of ERC-8167 from a storage organization perspective.

However, I have a couple of concerns.

1. On removing Diamond terminology

I’m not entirely sure that removing the Diamond terminology is necessary.

In the smart contract context, the term ā€œDiamondā€ has become a widely recognized architectural concept.

When developers hear ā€œDiamondā€, they immediately associate it with a proxy that routes transactions to multiple facets. Even though the proxy and facets are separate units, together they behave as a single unified smart contract from an external perspective.

By contrast, the terms ā€œdispatchā€ and ā€œdelegateā€ are more generic and may not clearly convey this architectural property. In fact, dispatch and delegate patterns can also describe other designs, such as hook-based systems or even monolithic proxies with a single implementation.

It might be worth considering retaining terms such as Diamond and Facet to preserve compatibility and conceptual continuity within the ecosystem.

2. On introspection flexibility

While upgradeability is intentionally flexible in this standard, I believe introspection mechanisms such as selectors() and implementation(bytes4) should also remain flexible.

Depending on the chosen upgrade mechanism or extension framework, there may be more efficient or context-aware ways to implement introspection.

Providing a function-based reference implementation is helpful. However, it may be beneficial to explicitly state that these introspection methods can be implemented differently depending on the extension standard being used, as long as they conform to the ERC-8167 interface.

Actually, they precisely describe the architecture in a way that diamond does not. Diamond terminology has been a hindrance to adoption of this standard, promoting an entirely unhelpful metaphor. The basic architecture is broader than both the original and upcoming specifications called diamond, and I have no intention of hijacking that brand.

That is the intent. I selected this minimal interface to avoid any restrictions on how the methods can be implemented. If you know a way the interface can be made even less restrictive without a substantial reduction in its usefulness, I am interested.

it may be beneficial to explicitly state that these introspection methods can be implemented differently depending on the extension standard being used

I did not do so because I thought it was obvious. Indeed I would not want people to think they have to use the reference implementation. I think the best way to communicate this would be to enumerate some alternative designs. I could describe the facet-based approach of ERC-8153 and an immutable versioning-based approach. What else should go into this list?

1 Like

Ah, the flexibility of the implementation exists - as it is common with other standards like ERC20. Reference implementation is only for the inspiration.

Only the interface (incl. return values) is not flexible - this we discussed ERC-8167: Modular Dispatch Proxies - #11 by wjmelements

There I could imagine different designs - like e.g. having a singleton delegate registry instead of implementation(bytes4) function - but IMO the self-contained introspection is better (as defined by this ERC).

Btw, such implementation(bytes4) function used to be in OZ labs: openzeppelin-labs/upgradeability_with_vtable/contracts/Proxy.sol at master Ā· OpenZeppelin/openzeppelin-labs Ā· GitHub

I do not understand why the whole modular approach did not get more traction in OZ implementations.

2 Likes

The hardest part of building smart contracts is ensuring that nothing bad happens. Upgradability adds a lot of difficulty here, since the new code has to work not only on its own, but also with existing state. This can be done, with effort, and you can make sure that code version 3 works cleanly with the data generated by code version 2.

The one thing you do have going for you is that you can cleanly think about and test exactly what this version of code is vs exactly what the previous version is. You can get a sign off from an auditor on an exact version to upgrade to.

Using a modular proxy adds a lot more ways things could go wrong. A contract might be partially upgraded, an old method might be left in by accident, and it follows different rules. None of these are impossible to work around operationally, but they just add a lot more risk and difficulty.

I don’t think modular proxies need to solve all these - modular proxies are just tools for different problems than an upgradable DeFi protocol.

2 Likes

Apologies if my previous message was unclear.
Let me clarify what I meant.

From my perspective, this type of architecture has three core properties:

  1. A proxy that redirects transactions to multiple implementations.

  2. Each implementation can be inspected and may be upgradeable, either partially (per function) or entirely.

  3. Although the proxy and implementations are technically separate units, the overall system behaves externally as a single unified monolithic smart contract, sharing a single state.

For me, the third property is important because it provides the mental model for understanding the architecture.

For example:

  • Because the system behaves as a unified logical contract, a function selector must resolve to exactly one active implementation at any given time.

  • Because there is a single shared storage context, different implementations must coordinate their storage layout carefully. If incompatible layouts are introduced, storage collisions will occur.

  • More generally, architectural constraints arise from the fact that the system presents itself as one contract externally, even though it is composed of multiple implementation units internally.

My initial thought was that the Diamond terminology is helpful because it implicitly conveys this ā€œoverall system behaves as a unified contract with shared stateā€ concept. However, that is just my personal interpretation.

If the Diamond terminology is intentionally avoided, I think it may still be valuable for ERC-8167 to describe this unified mental model more explicitly in the specification.

1 Like

Thank you for the clarification.

Initially, I was thinking that introspection could also be defined by extensions, similar to how upgrade mechanisms are defined. However, if the core idea of ERC-8167 is to standardize a minimal and consistent introspection interface for interoperability, then I agree that the current design is reasonable.

I think the mechanics of ERC-8167 are more closely tied to the chosen upgrade model than standards like ERC-20.

This is just my perspective. It may still be helpful to explicitly state that while the interface is standardized, the implementation of introspection remains flexible. Extension standards should be encouraged to implement these functions in a way that aligns with their own upgrade model, as long as they conform to the ERC-8167 interface.

Moving this ERC into Review.