Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,6 @@ jobs:
run: |
forge --version

- name: Run Forge fmt
run: |
forge fmt --check
id: fmt

- name: Run Forge build
run: |
forge build --sizes
Expand Down
42 changes: 20 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,30 +12,28 @@ forge test
```

## Storage layout of `SemVerProxy`
`SemVerProxy` reserves storage slots **0 to 99** for implementation contracts by declaring a fixed-size array that occupies these slots:
```solidity
uint256[100] private __gap; // Reserves slots 0-99 for implementations
```
So, the storage layout of a proxy and an arbitrary implementation contract can be represented as follows:
`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:
```text
Proxy storage layout:
┌─────────────┐
│ Slot 0-99 │ ← `__gap` placeholder (reserved for implementations)
├─────────────┤
│ Slot 100 │ ← `_latestVersion`
│ Slot 101 │ ← Other state variables
│ ... │
└─────────────┘
┌───────────────┐
│ Slot 0-1000 │ ← empty
├───────────────┤
│ Slot 1000 │ ← `_latestVersion`
│ Slot 1001 │ ← `_releases` mapping
│ Slot 1002 │ ← `_subscribedClients` mapping
│ ... │
└───────────────┘

Implementation storage example:
┌─────────────┐
│ Slot 0 │ ← Implementation's first state variable
│ Slot 1 │ ← Implementation's second state variable
│ ... │
│ Slot 99 │ ← Last available slot for implementation
├─────────────┤
| Slot 100 | ← ⚠️ This will collide with proxy's storage
└─────────────┘
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
└───────────────
```

## Releases and Versioning
Expand Down Expand Up @@ -91,5 +89,5 @@ function unsubscribeFromVersioning() external;
```

## Security Considerations
- Since `SemVerProxy` only reserves slots from 0 to 99, any 100+ slot of the implementation will collide with proxy.
- 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 of `SemVerProxy`).
2 changes: 2 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@ libs = ["lib"]
runs = 1000
depth = 300

solc_version = "0.8.30"

# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
30 changes: 7 additions & 23 deletions src/SemVerProxy.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
pragma solidity 0.8.30;

import {TransparentUpgradeableProxy, ERC1967Utils} from "openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import {Versioning, Version, EncodedVersion} from "./lib/Versioning.sol";
Expand All @@ -23,31 +23,22 @@ import {Clients, Client} from "./lib/Clients.sol";
* specified {_fallback} will delegate to the latest release.
*
* @dev Latest version is stored in a storage slot, specified in ERC-1967.
*
* @dev Storage variables are stored starting from 1000th slot.
*/
contract SemVerProxy is TransparentUpgradeableProxy {
/**
* @dev Since {store} and {obtainRelease} are methods
* that should be applied to {mapping(EncodedVersion => address)}
* we limit them exactly to this mapping type.
*/
contract SemVerProxy is TransparentUpgradeableProxy layout at 1_000 {
using {
Versioning.store,
Versioning.obtainRelease
} for mapping(EncodedVersion => address);
/**
* @dev Incrementors and {encode} functions are limited
* to be applied to {Version} struct.
*/

using {
Versioning.incMajor,
Versioning.incMinor,
Versioning.incPatch,
Versioning.encode
} for Version;
/**
* @dev Subscription logic is limited to be
* applied to {mapping(Client => address)} type.
*/

using {
Clients.subscribe,
Clients.unsubscribe,
Expand All @@ -60,16 +51,9 @@ contract SemVerProxy is TransparentUpgradeableProxy {
event ClientUnsubscribed(Client indexed client);

/*** * STORAGE * ***/
/**
* @dev Reserve 100 storage slots to be used in implementations.
* @notice Any 99+ slot inside implementation will overwrite
* storage of this proxy,
* Therefore, implementations can only safely use
* slots between 0 and 99.
*/
uint256[100] private __gap;

/// @notice Stores SemVer-like latest version of the implemenation.
/// @dev {_latestVersion} is stored in storage slot [1_000].
Version internal _latestVersion;

/// @notice Stores addresses of every existing version.
Expand Down
12 changes: 7 additions & 5 deletions src/interfaces/ISemVerProxy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,19 @@ pragma solidity ^0.8.28;
import {Version, EncodedVersion} from "../lib/Versioning.sol";

interface ISemVerProxy {
function latestVersion() external view returns(Version memory);
function latestVersion() external view returns (Version memory);
function latestEncoded() external view returns (EncodedVersion);
function latestRelease() external view returns (address);

/*~~~~~~~~~~~~~~~~~~~~~~ CLIENT ACTIONS ~~~~~~~~~~~~~~~~~~~~~~*/

/**
* * CLIENT ACTIONS * **
*/
function subscribeToVersion(Version memory version) external;
function unsubscribeFromVersioning() external;

/*~~~~~~~~~~~~~~~~~~~~~~ ADMIN ACTIONS ~~~~~~~~~~~~~~~~~~~~~~*/

/**
* * ADMIN ACTIONS * **
*/
function releaseMajor(address release, bytes memory data) external;
function releaseMinor(address release, bytes memory data) external;
function releasePatch(address release, bytes memory data) external;
Expand Down
32 changes: 17 additions & 15 deletions test/SemVerProxy.t.sol
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pragma solidity ^0.8.28;
pragma solidity 0.8.30;

import {ProxyAdmin} from "openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import {Versioning, Version, EncodedVersion} from "../src/lib/Versioning.sol";
Expand Down Expand Up @@ -49,7 +49,7 @@ contract SemVerProxyTest is Test {

// Verify that writing to unsafe slots breaks storage.
function test_implementationBreaksStorage() public {
// This contract might write to 100th storage slot.
// This contract might write to 1_000th storage slot.
Breaking breaking = new Breaking();

vm.prank(address(proxyAdmin));
Expand All @@ -60,27 +60,26 @@ contract SemVerProxyTest is Test {
(uint64 major, uint64 minor, uint128 patch) = implementation
.latestVersion_();

// This will succeed, since {Breaking} contract
// will read from a storage slot already occupied
// by the {SemVerProxy}.
// This will succeed, since {Breaking} contract
// will read from a storage slot already occupied
// by the {SemVerProxy}.
_compareVersions(
Version(major, minor, patch),
semVerProxy.latestVersion()
);

implementation.setVersion();
(major, minor, patch) = implementation
.latestVersion_();
implementation.setVersion();
(major, minor, patch) = implementation.latestVersion_();

// This will succeed again, because at this point
// the storage slot {100} has collided between
// {SemVerProxy} and {Breaking} contract, and
// {setVersion} call has ovrewritten {_latestVersion}
// storage variable of {SemVerProxy}.
// This will succeed again, because at this point
// the storage slot {1_000} has collided between
// {SemVerProxy} and {Breaking} contract, and
// {setVersion} call has ovrewritten {_latestVersion}
// storage variable of {SemVerProxy}.
_compareVersions(
Version(major, minor, patch),
semVerProxy.latestVersion()
);
);
}

function testFuzz_subscribe(address caller) public {
Expand Down Expand Up @@ -292,7 +291,10 @@ contract SemVerProxyTest is Test {
assertEq(implementation.x(), release2.ANOTHA_WILL_BE_X());
}

function _compareVersions(Version memory v0, Version memory v1) internal {
function _compareVersions(
Version memory v0,
Version memory v1
) internal pure {
assertEq(
EncodedVersion.unwrap(v0.encode()),
EncodedVersion.unwrap(v1.encode())
Expand Down
4 changes: 2 additions & 2 deletions test/InvariantSemVerProxy.t.sol → test/SemVerProxyInv.t.sol
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pragma solidity ^0.8.28;
pragma solidity 0.8.30;

import {ProxyAdmin} from "openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import {Versioning, Version, EncodedVersion} from "../src/lib/Versioning.sol";
Expand All @@ -7,7 +7,7 @@ import {X, Y, Z} from "./mocks/VersionedContract.sol";
import {SemVerProxy} from "../src/SemVerProxy.sol";
import {Test, Vm} from "forge-std/Test.sol";

contract InvariantSemVerProxyTest is Test {
contract SemVerProxyInv is Test {
using {Versioning.encode} for Version;

address latestRelease;
Expand Down
2 changes: 1 addition & 1 deletion test/libs/Clients.t.sol
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pragma solidity ^0.8.28;
pragma solidity 0.8.30;

import {Client, Clients} from "../../src/lib/Clients.sol";
import {Test} from "forge-std/Test.sol";
Expand Down
2 changes: 1 addition & 1 deletion test/libs/Versioning.t.sol
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pragma solidity ^0.8.28;
pragma solidity 0.8.30;

import {Version, Versioning, EncodedVersion} from "../../src/lib/Versioning.sol";
import {Test} from "forge-std/Test.sol";
Expand Down
5 changes: 2 additions & 3 deletions test/mocks/BreakingContract.sol
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
pragma solidity ^0.8.28;
pragma solidity 0.8.30;

import {Version} from "../../src/lib/Versioning.sol";

contract Breaking {
uint256[100] private __gap;
contract Breaking layout at 1_000 {
Version public latestVersion_;

function setVersion() external {
Expand Down
2 changes: 1 addition & 1 deletion test/mocks/VersionedContract.sol
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pragma solidity ^0.8.28;
pragma solidity 0.8.30;

contract X {
uint256 public constant WILL_BE_X = 228;
Expand Down
Loading