/UDS

Upgradeable Contracts using the Diamond Storage pattern

Primary LanguageSolidity

Upgradeable Contracts Using Diamond Storage

A collection of upgradeable contracts compatible with diamond storage.

Contracts

src
├── auth
│   ├── AccessControlUDS.sol - "OpenZeppelin style access-control"
│   ├── EIP712PermitUDS.sol - "EIP712 permit"
│   ├── OwnableUDS.sol - "Owner authorization"
│   ├── PausableUDS.sol - "Make contracts pausable"
│   └── ReentrancyGuardUDS.sol - "Prevent reentrancies"
├── proxy
│   ├── ERC1967Proxy.sol - "ERC1967 proxy implementation"
│   └── UUPSUpgrade.sol - "Minimal UUPS upgradeable contract"
├── utils
│   ├── Initializable.sol - "Allow initializing functions for upgradeable contracts"
│   └── Context.sol - "Allows overrides for meta-transactions"
└── tokens
    ├── ERC20UDS.sol - "Solmate's ERC20"
    ├── ERC1155UDS.sol - "Solmate's ERC1155"
    ├── ERC721UDS.sol - "Solmate's ERC721"
    └── extensions
        ├── ERC20BurnableUDS.sol - "ERC20 burnable"
        └── ERC20RewardUDS.sol - "ERC20 with fixed reward accrual"

Installation

Install with Foundry

forge install 0xPhaze/UDS

Deploying an Upgradeable Contract

Implementation

The implementation contract, needs inherit from UUPSUpgrade and the _authorizeUpgrade function must be overriden (and protected).

Example of an upgradeable ERC721

import {ERC20UDS} from "UDS/tokens/ERC20UDS.sol";
import {OwnableUDS} from "UDS/auth/OwnableUDS.sol";
import {UUPSUpgrade} from "UDS/proxy/UUPSUpgrade.sol";
import {Initializable} from "UDS/utils/Initializable.sol";

contract UpgradeableERC20 is UUPSUpgrade, Initializable, OwnableUDS, ERC20UDS {
    function init() external initializer {
        __Ownable_init();
        __ERC20_init("My Token", "TKN", 18);
        _mint(msg.sender, 1_000_000e18);
    }

    function _authorizeUpgrade() internal override onlyOwner {}
}

The example uses OwnableUDS and Initializable.

Deploying the Proxy Contract

import {ERC1967Proxy} from "UDS/proxy/ERC1967Proxy.sol";

bytes memory initCalldata = abi.encodeWithSelector(init.selector, param1, param2);

address proxyAddress = new ERC1967Proxy(implementationAddress, initCalldata);

Upgrading a Proxy Contract

import {UUPSUpgrade} from "UDS/proxy/UUPSUpgrade.sol";

UUPSUpgrade(deployedProxy).upgradeToAndCall(implementationAddress, initCalldata);

A full example using Foundry and Solidity Scripting can be found here Deploy and here Upgrade.

Layout changes

Although, re-ordering contract storage slots through adding inheritance or changing inheritance order won't cause storage collisions, changes in the internal layout of contract storage still can. The contracts contain the private _layout variable that can act as a storage layout "snapshot" to detect differences using forge inspect {Contract} storagelayout.

To take a snapshot of a storage layout, run

./storage-inspect.sh generate ERC20UDS

To check storage layout compatibility with an existing storage layout snapshot, run

./storage-inspect.sh check ERC20UDS

Note that renaming contracts will trigger a positive find.

Benefits

Benefits over using Openzeppelin's upgradeable contracts:

  • No storage collision through adding/removing inheritance or incorrectly adjusted storage gaps, because of diamond storage
  • Removes possibility of an uninitialized implementation
  • Minimal bloat (simplified dependencies and contracts)

What is Diamond Storage?

Diamond Storage keeps contract storage data in structs at arbitrary locations as opposed to in sequence. This means that storage collisions between contracts don't have to be dealt with. However, the same caveats apply to the diamond storage structs internally when upgrading contracts, though these are easier to deal with. The free function s() returns a reference to the struct's storage location, which is required to read/write any state.

What is a Proxy?

A proxy contract delegates all calls to an implementation contract. This means that it runs the code/logic of the implementation contract is executed in the context of the proxy contract. If storage is read from or written to, it happens in the proxy itself. The implementation is (generally) not intended to be interacted with directly. It only serves as a reference for the proxy on how to execute functions. For the most part, a proxy behaves as though it was the implementation contract itself.

A proxy can be upgradeable and swap out the address pointing to the implementation contract for a new one. This can be thought of as changing the contract's runtime code. The code for running an upgrade is left to be handled by the implementation contract (for UUPS proxies).

Generally, upgradeable contracts can't rely on a constructor for initializing variables. If the implementation contains a constructor, its code is only run once during deployment (in the implementation contract's context and not in the proxy's context). The constructor isn't part of the deployed bytecode / runtime code and generally doesn't affect a proxy (often deployed at a later time).

This is why it is useful to have functions that are internal and/or public secured by the initializer modifier (found in Initializable.sol). These functions are then only callable during a proxy contract's deployment and before any new upgrade has completed. In contrast to OpenZeppelin's initializer, these functions won't ever be callable on the implementation contract and can be run again, allowing "re-initialization" (as long as they are run during an upgrade). Forgetting to run all initializing functions can be dangerous. For example, a contract's upgradeability could be lost, if UUPSUpgrade's _authorizeUpgrade is secured by the onlyOwner modifier, but OwnableUDS' __Ownable__init was never called.

Caveats

These contracts are a work in progress and should not be used in production. Use at your own risk. As mentioned before, there exist some notable and important differences to common implementations. Make sure you are aware of these.