Proxy contract that allows dispatching calls to multiple implementations, based on Semantic Versioning. Automatically handles incrementation of major.minor.patch
parts of a version during upgrades, but it doesn't enforce proper adherence to Semantic Versioning by developers on a smart-contract level - it's considered to be a responsibility of a smart-contract developer to properly adhere to Semantic Versioning.
git clone https://github.com/daoio/semver-proxy
## Install deps (note, oz contracts are already installed locally with --no-git)
forge install
## Run tests
forge test
SemVerProxy
stores variables in the storage, starting at slot 1000. Therefore, implementation contracts can safely use all storage slots except 1000, 1001, and 1002.
So, the storage layout of the proxy and an arbitrary implementation contract can be represented as follows:
Proxy storage layout:
┌───────────────┐
│ Slot 0-1000 │ ← empty
├───────────────┤
│ Slot 1000 │ ← `_latestVersion`
│ Slot 1001 │ ← `_releases` mapping
│ Slot 1002 │ ← `_subscribedClients` mapping
│ ... │
└───────────────┘
Implementation's storage example:
┌───────────────┐
│ Slot 0 │ ← Implementation's 1st state variable
│ Slot 1 │ ← Implementation's 2nd state variable
│ ... │
│ Slot 999 │ ← Implementation's 998th state variable
├───────────────┤
| Slot 1000 │ ← ⚠️ This will collide with proxy's storage
└───────────────┘
A release refers to a versioned implementation contract stored in the proxy. Unlike standard proxies, SemVerProxy
maintains multiple implementations simultaneously, each accessible through its semantic version.
- Latest Release: Stored in the ERC-1967 implementation slot
(Updating this overwrites the ERC-1967 slot) - Historical Versions: Preserved in the
_releases
mapping
(All versions remain accessible to clients by theirmajor.minor.patch
identifier) - Initial Version:
0.1.0
per SemVer initial development guidelines
Admins can release new version of implementation contract via the following function calls (effectively upgrading proxy to use new implemenation, though clients might still use previous versions of the implementation contract):
Function | Behavior | Example |
---|---|---|
releasePatch |
Increments patch | 1.2.3 → 1.2.4 |
releaseMinor |
Increments minor, resets patch | 1.2.3 → 1.3.0 |
releaseMajor |
Increments major, resets minor and patch | 1.2.3 → 2.0.0 |
By default, all non-admin calls that end up in fallback()
function are dispatched to delegatecall
to the latest release (stored in ERC-1967 slot). But, users can "subscribe" to use the implementation version they need, by calling:
function subscribeToVersion(Version memory version) external;
// Where {Version} is:
struct Version {
uint64 major;
uint64 minor;
uint128 patch;
}
After this action all calls of a subscribed user will be dispatched to the major.minor.patch
version they have specified for subscription. All subscriptions are stored inside SemVerProxy._subscribedClients
mapping
flowchart TD
A(["subscribed to 1.0.1"]) --> n4["SemVerProxy"]
n1(["subscribed to 0.1.0"]) --> n4
n2(["not subscribed"]) --> n4
n4 --> C["Release v 1.0.1"] & D["Release v 0.1.0"] & n3["Latest Release v 2.0.0"]
n4@{ shape: diam}
n3@{ shape: rect}
linkStyle 0 stroke:#D50000,fill:none
linkStyle 1 stroke:#2962FF,fill:none
linkStyle 2 stroke:#00C853,fill:none
linkStyle 3 stroke:#D50000,fill:none
linkStyle 4 stroke:#2962FF,fill:none
linkStyle 5 stroke:#00C853
Users can unsubscribe from using specific version and use the latest one by calling:
function unsubscribeFromVersioning() external;
- Storing variables in implementation's storage at slots 1000, 1001, 1002 will collide with the proxy's storage.
SemVerProxy
has externally accessable function, therefore there's a possibiliy of function selector clash (i.e., if implementation defines functions that have the same signature as external functions ofSemVerProxy
).