Architecture
The primary goal of the current architecture is to give flexibility in order to accommodate the project's potential for development over time. Having stated that, the EIP-2535: Diamonds, Multi-Facet Proxy served as an inspiration for the system architecture. Although the Diamond standard achieves the primary objective, it is complicated and has features that we will not use. However, we have implemented a storage-module design with the modifications and advantages listed below:
Simplified version of the Diamond standard.
Instead of facets, we have modules. It is pretty much the same functionality, but with a more familiar terminology.
It allows the admins to easily upgrade the protocol and add/update/remove functionality.
Decreases the chance of reaching the 24kb contract limit size.
Mitigates storage collision during upgrades.
Any call or transaction is forwarded to the appropriate module, which contains the implementation code, by the entry point contract DIMORegistry
acting as a proxy. Despite the fact that state variables can be defined in modules, they are not really preserved in the module storage. The DIMORegistry
contract contains all of the state, and the modules contain all of the logic.
The DIMORegistry
contract manages the system's modules, including their additions, deletions, updates, and tracking. To add, remove, or update a module, you'll need the implementation address as well as all of the function signatures. The main contract associates each signature ever registered in the system with its corresponding implementation, mapping the implementation address to a hash made up of all function signatures of that module.
In order to forward the calls to the correct module, the fallback
function gets a corresponding implementation based on the msg.sig
of the call.
Storage
Storage collision is one of the key problems with upgradable proxy arrangements. You must be cautious when upgrading after specifying the storage design in the proxy to prevent overwriting current state variables and losing data.
By using an unstructured storage proxy, the Diamond Standard and the DIMO identity architecture both avoid the storage collision. Each module that uses state variables has separate storage of its own. This can be achieved by creating a struct containing its corresponding state variables, that is kept in a slot defined by a hash based on the name of the module. The code sample below demonstrates how the storage of the modules is defined as libraries in order to minimise the contract size.
The variables inside the struct adhere to the conventional proxy's collision rule. Instead of the entire system, it just applies to the variables within the struct. Nevertheless, by constructing tiny, independent archives that are randomly assigned to certain locations in the storage, we are able to reduce the likelihood of collisions.
Node Structure
DIMO's primary functionality is organised as a tree with parents and children. Each node is a representation of a real-world object, such as a manufacturer, a vehicle, or an aftermarket device. Every time a new entity is introduced to the DIMO system, they are minted as NFTs in tree structure to ensure their uniqueness. A new node can be added to the tree structure in one of two ways:
Root
The new node does not have a parent and it is a starting point which all other nodes will be derived from. Ex: Manufacturer
Child
The node must be associated with a parent node that also represents a real world relationship. Ex: A Vehicle and an Aftermarket Device must be under their respective Manufacturer
The struct
that represents each node is in the following format:
parentNode: this is the link to the immediate parent node that builds the tree structure.
info: a mapping to store any
attribute-value
associate to the node. It is worth reminding that only whitelisted attributes are allowed to be in the mapping.
Sharing between modules
This article shows different ways to share functionality between facets in the Diamond Standard. Since DIMO is inspired by the same standard, we also use similar approaches to sharing functionality.
Storing shared state inside an existing storage struct.
Organizing and refactoring the modules so they don't need cross-module calls.
Writing libraries/contracts with internal function calls.
Last updated