|
1 |
| -# semver-proxy |
| 1 | +# SemVer Proxy |
| 2 | +Proxy contract that allows dispatching calls to multiple implementations, based on [Semantic Versioning](https://semver.org/). 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. |
| 3 | + |
| 4 | +## Quick Start |
| 5 | +```sh |
| 6 | +git clone https://github.com/daoio/semver-proxy |
| 7 | + |
| 8 | +## Install deps (note, oz contracts are already installed locally with --no-git) |
| 9 | +forge install |
| 10 | +## Run tests |
| 11 | +forge test |
| 12 | +``` |
| 13 | + |
| 14 | +## Storage layout of `SemVerProxy` |
| 15 | +`SemVerProxy` reserves storage slots **0 to 99** for implementation contracts by declaring a fixed-size array that occupies these slots: |
| 16 | +```solidity |
| 17 | +uint256[100] private __gap; // Reserves slots 0-99 for implementations |
| 18 | +``` |
| 19 | +So, the storage layout of a proxy and an arbitrary implementation contract can be represented as follows: |
| 20 | +```text |
| 21 | +Proxy storage layout: |
| 22 | +┌─────────────┐ |
| 23 | +│ Slot 0-99 │ ← `__gap` placeholder (reserved for implementations) |
| 24 | +├─────────────┤ |
| 25 | +│ Slot 100 │ ← `_latestVersion` |
| 26 | +│ Slot 101 │ ← Other state variables |
| 27 | +│ ... │ |
| 28 | +└─────────────┘ |
| 29 | +
|
| 30 | +Implementation storage example: |
| 31 | +┌─────────────┐ |
| 32 | +│ Slot 0 │ ← Implementation's first state variable |
| 33 | +│ Slot 1 │ ← Implementation's second state variable |
| 34 | +│ ... │ |
| 35 | +│ Slot 99 │ ← Last available slot for implementation |
| 36 | +├─────────────┤ |
| 37 | +| Slot 100 | ← ⚠️ This will collide with proxy's storage |
| 38 | +└─────────────┘ |
| 39 | +``` |
| 40 | + |
| 41 | +## Releases and Versioning |
| 42 | +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. |
| 43 | + |
| 44 | +- **Latest Release**: Stored in the [ERC-1967 implementation slot](https://eips.ethereum.org/EIPS/eip-1967#logic-contract-address) |
| 45 | + (Updating this overwrites the ERC-1967 slot) |
| 46 | +- **Historical Versions**: Preserved in the `_releases` mapping |
| 47 | + (All versions remain accessible to clients by their `major.minor.patch` identifier) |
| 48 | +- **Initial Version**: `0.1.0` per [SemVer initial development guidelines](https://semver.org/#how-should-i-deal-with-revisions-in-the-0yz-initial-development-phase) |
| 49 | + |
| 50 | +### Version Upgrade |
| 51 | +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): |
| 52 | + |
| 53 | +| Function | Behavior | Example | |
| 54 | +| ------------- | ------------- | -- | |
| 55 | +| `releasePatch` | Increments patch | `1.2.3` → `1.2.4` | |
| 56 | +| `releaseMinor` | Increments minor, resets patch | `1.2.3` → `1.3.0` | |
| 57 | +| `releaseMajor` | Increments major, resets minor and patch | `1.2.3` → `2.0.0` | |
| 58 | + |
| 59 | +## Subscribing to Specific Versions |
| 60 | +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: |
| 61 | +```solidity |
| 62 | +function subscribeToVersion(Version memory version) external; |
| 63 | +
|
| 64 | +// Where {Version} is: |
| 65 | +struct Version { |
| 66 | + uint64 major; |
| 67 | + uint64 minor; |
| 68 | + uint128 patch; |
| 69 | +} |
| 70 | +``` |
| 71 | +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 |
| 72 | +```mermaid |
| 73 | +flowchart TD |
| 74 | + A(["subscribed to 1.0.1"]) --> n4["SemVerProxy"] |
| 75 | + n1(["subscribed to 0.1.0"]) --> n4 |
| 76 | + n2(["not subscribed"]) --> n4 |
| 77 | + n4 --> C["Release v 1.0.1"] & D["Release v 0.1.0"] & n3["Latest Release v 2.0.0"] |
| 78 | + n4@{ shape: diam} |
| 79 | + n3@{ shape: rect} |
| 80 | + linkStyle 0 stroke:#D50000,fill:none |
| 81 | + linkStyle 1 stroke:#2962FF,fill:none |
| 82 | + linkStyle 2 stroke:#00C853,fill:none |
| 83 | + linkStyle 3 stroke:#D50000,fill:none |
| 84 | + linkStyle 4 stroke:#2962FF,fill:none |
| 85 | + linkStyle 5 stroke:#00C853 |
| 86 | +``` |
| 87 | + |
| 88 | +Users can unsubscribe from using specific version and use the latest one by calling: |
| 89 | +```solidity |
| 90 | +function unsubscribeFromVersioning() external; |
| 91 | +``` |
| 92 | + |
| 93 | +## Security Considerations |
| 94 | +- Since `SemVerProxy` only reserves slots from 0 to 99, any 100+ slot of the implementation will collide with proxy. |
| 95 | +- `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 of `SemVerProxy`). |
0 commit comments