UUPS is simpler to implement and maintain. The core contracts are slightly modified ERC-20 and ERC-1155 tokens so moving them to diamond facets seems to be an overkill.
diamond proxy standard requires unique function selectors (i.e. you can't add multiple ERC-20 token contracts to the diamond because they have equal function selectors)
Proxy diagram
Core contracts
The following table represents the core contracts:
When a user calls some method on the Diamond contract then this call uses delegatecall() to a corresponding diamond facet which stores function selector for the call's msg.sig. Each facet uses a corresponding library which implements a business logic and stores any state variable in a unique storage slot (i.e. each library stores state variables in a unique storage slot).
There is also a special Modifiers contract which is a base contract for most of the facets. It contains a special state variable which stores an AppStorage struct that is shared across all of the facets. The AppStorage struct occupies slot 0 in all of the facets.
The following table represents the diamond facet contracts:
[!WARNING]
In order to maintain UUPS and Diamond upgradeability all new state variables must be added to the end of a contract or a storage struct (storage struct example).
The following code refactors must be avoided:
Rule
UUPS related
Diamond related
New state variable introduced in the beginning or in the middle of a contract or a storage struct
+
+
Order of state variables changed
+
+
Type of state variable changed
+
+
Existing state variable removed from the middle of a contract or a storage struct
+
+
Existing state variable removed from the end of a contract or a storage struct
+
+
Inheritance order changed (ex: contract MyContract is A,B != "contract MyContract is B,A)
+
-
New state variable added to a base (i.e. derived) contract (unless storage gaps are used properly)
+
-
Inner struct is added to a storage struct (unless we 100% sure that we will never add new state variables to the inner struct)[^1]
-
+
[^1]: Do not put structs directly in another struct unless you don't plan on ever adding more state variables to the inner structs. You won't be able to add new state variables to inner structs in upgrades without overwriting the storage slot of variables declared after the struct. The solution is to add new state variables to the structs that are stored in mappings, rather than putting structs directly in structs, as the storage slot for variables in mappings is calculated differently and is not continuous in storage.
Other contracts
Some of the contracts are neither part of the core contracts no part of the facets for different reasons:
Smart Contracts Architecture
Overview
All of the Ubiquity contracts are upgradeable and divided in 2 parts:
Core contracts use the UUPS proxy standard while diamond facet contracts use the diamond proxy standard.
We decided to keep the core contracts aside from the diamond facets because:
Proxy diagram
Core contracts
Diamond facet contracts
When a user calls some method on the Diamond contract then this call uses
delegatecall()
to a corresponding diamond facet which stores function selector for the call'smsg.sig
. Each facet uses a corresponding library which implements a business logic and stores any state variable in a unique storage slot (i.e. each library stores state variables in a unique storage slot).There is also a special Modifiers contract which is a base contract for most of the facets. It contains a special state variable which stores an AppStorage struct that is shared across all of the facets. The AppStorage struct occupies
slot 0
in all of the facets.Upgradeability
contract MyContract is A,B != "contract MyContract is B,A
)[^1]: Do not put structs directly in another struct unless you don't plan on ever adding more state variables to the inner structs. You won't be able to add new state variables to inner structs in upgrades without overwriting the storage slot of variables declared after the struct. The solution is to add new state variables to the structs that are stored in mappings, rather than putting structs directly in structs, as the storage slot for variables in mappings is calculated differently and is not continuous in storage.
Other contracts