A collection of upgradeable contracts compatible with diamond storage.
To clarify: these contracts DO NOT require EIP-2535 Upgradeable Diamond Standard to be used. They are simply compatible. In fact, these contracts can also be used without any upgradeability at all. They can be used in almost exactly the same way as OpenZeppelin's upgradeable contracts, but include some beneficial properties.
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"
│ └── Multicallable.sol - "Batch transaction calls"
└── 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"
Install with Foundry
forge install 0xPhaze/UDS
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(address) internal override onlyOwner {}
}
The example uses OwnableUDS and Initializable.
import {ERC1967Proxy} from "UDS/proxy/ERC1967Proxy.sol";
bytes memory initCall = abi.encodeCall(Implementation.init, (param1, param2));
address proxyAddress = new ERC1967Proxy(implementationAddress, initCall);
import {UUPSUpgrade} from "UDS/proxy/UUPSUpgrade.sol";
UUPSUpgrade(deployedProxy).upgradeToAndCall(implementationAddress, initCall);
A full example using Foundry and Solidity Scripting
can be found here deploy and here upgrade
(note that these examples require you to install project dependencies by running forge install
).
A more advanced example of upgrading and maintaining proxies (including safety checks for storage layout changes) can be found here: upgrade-scripts.
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)
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.
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.
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.