Smart Contract Design Patterns
Goals
- Minimal Upgradability
- Maximal Composability
Upgradability is how a protocol can change overtime and what guarantees it makes for the future.
Composability is how easy it is to build on top of the protocol.
Upgradability
- Unconstrained Upgradability
- Arbitrary Changes
- Any rules enforced by the contract can be overwritten by an upgrade
- eg: Proxies
- Constrained Upgradability
- Limited Changes
- The system can encode rules on what parts of the protocol can be upgraded and what must remain constants
- eg: modules, configurable parameters
Proxies
- Ralph makes a call to the proxy contract.
- The proxy contract delegate calls the implementation
- All the storage is saved and read from the proxy contract. But the logic is handled on the implementation contract.
- The implementation of the proxy can be upgraded. This let’s us fix bugs or add new features to the smart contract. But it also let’s the owner upgrade to a malicious implementation.
- Using this pattern defeats the immutability of a smart contract.
- Simple Proxy System
contract SimpleProxy {
address internal implementation;
function fallback() external {
implementation.delegatecall(msg.data);
}
}
contract SomeImplementation {
address internal implementation;
uint256 public a;
uint256 public b;
function setVars(uint256 _a, uint256 _b) external {
a = _a;
b = _b;
}
}
- The
SimpleProxy
contract has a only one special functionfallback
.
- A
fallback
is a special function. When a function called by the user does not exist on the contract, it calls thefallback
function.
- Here the fallback function
delegatecall
the implementation contract and passes on the transaction data to the implementation.
- Since the Proxy contract makes a delegatecall to the implementation, the implementation contract is going to update the storage of the proxy contract. In this case, the values
a
andb
are stored in theProxy
contract.
- This enables us to save the continuity of the state between upgrades.
Proxies - Storage Layout
- Storage in a smart contract is held in slots. Each slot holds 32 bytes of data.
- These slots start from 0 → 
- Because of the delegatecall, the proxy contract and the implementation contracts are going to share these slots.
- The proxy only has one variable, the
implentation
address. It stores this inslot 0
since it’s the first variables.
- If the implementation has the first variable as
a
instead of theimplementation
, it’ll rewrite the value atslot 0
of the proxy contract and the proxy won’t be able to delegate call the implementation.
Proxies - EIP1967
contract Proxy {
bytes32 internal constant IMPLEMENTATION_SLOT = bytes32(
uint256(keccak256("eip1967.proxy.implementation")) - 1);
function fallback() external {
address implementation;
assembly {
implementation := sload(IMPLEMENTATION_SLOT)
}
implementation.delegatecall(msg.data);
}
}
contract SomeImplementation {
uint256 public a;
uint256 public b;
function setVars(uint256 _a, uint256 _b) external {
a = _a;
b = _b;
}
}
- The main difference here is that instead of storing the implementation address in slot 0, we store it in a random slot.
- There are still ways to make mistakes here. If you deploy a new implementation with the values of
a
andb
swapped, then the slots for those will change as well. So the next time you access the value ofa
, it’ll return the value ofb
and vice versa.
- DO NOT DELETE OLD VARIABLES.
- DO NOT SWAP OR TRANSPOSE VARIABLES
- YOU CAN ADD NEW VARIABLES. BUT IT HAS TO BE IN A SLOT AFTER THE OLD VARIABLES
Proxies - Upgrade Logic
- You can either have the upgrade logic on the implementation or on the proxy itself.
- Mistakes with Upgrade on Implementation - You could upgrade your implementation and accidentally modify the upgrade logic and totally lose control of your proxy.
Proxies - Diamond Pattern
- In a diamond pattern, you have one proxy and multiple different implementations called facets.
- The proxy maps certain functions for each contract function
- You can get around the contract size limit using this pattern
- Drawbacks - The storage is shared between all the implementation. One implementation might overwrite the other implementation storage variable.
- Upgrade to this is the Diamond Storage Pattern.
Problems with Proxies
- Supports unconstrained upgradability
- Can make composability difficult due to no guaranteed behaviour in the future
- Can be difficult to reason
Authorization
- How do we manage who can do what?
- Role Based Authorization
contract Roles {
address owner;
mapping (address => bool) coolGuys;
modifier onlyOwner() {
require(msg.sender == owner );
_;
}
modifier onlyCoolGuys() {
require (coolGuys [msg. sender]);
_;
}
}
- Role based authorization is very simple.
- In the above example you have an address that’s the owner and a mapping that allows only the addresses in the mapping to call certain function
- DS-Auth
contract Auth { IAuthority authority; modifier auth() { require(authority. isAuthorized(msg.sender, msg.sig)); _; } }
- With DS-Auth you have one address called authority. - Used by MakerDAO
- The authority determines if a given address has access to a certain function.
- You can have more nuance and and complicated authorization like can bob call mint? Can alice call borrow before a given block
- It can even be upgraded.
- Unchecked Receive
- In this you have an owner. An owner can set another address to be the owner
- The owner might accidentally set the wrong
contract YoloSetOwner {
address owner;
modifier onlyOwner () {
require(msg. sender == owner);
_;
}
function setOwner(address newOwner) external onlyOwner {
owner = newOwner;
}
}
- Checked Receive
- The owner sets a new owner and the new owner has to claim the ownership.
- Compound Governance uses this pattern
contract CheckedReceive {
address owner;
address pendingOwner;
function setOwner(address newOwner) external {
pendingOwner = newOwner;
}
function claimOwnership() external {
require(msg.sender == pendingOwner);
owner = pendingOwner;
pendingOwner = address(0);
}
}
Modularity
- Modules can interact with each other to call functions
- They can even have complex authorization patterns that let’s certain modules do certain stuffs
- Models can be swapped for new ones or even be proxy
- This helps us achieve constrained upgradability where some modules can be immutable and others can be proxies.
- DS-Auth can be very useful here
Modularity - Simple Composition
- In this example pattern, the
VerbsDAO NFT
contract can have a certain constrains that are immutable like only one mint per day, minimum price etc and theAuctionModule
can have upgradable logic.
Modularity - Invoker
- The Vault has only one
invoke
function that takes an arbitrary function call and calls the specific contract
- The vault can have certain constraints
Modularity - DelegateCall
- Almost unconstrained upgradability
- Here the
Governance
contract is immutable. But theProposalPayload
is mutable and can be upgraded.
Modularity - Zora v3
- Bart approves the
TransferHelper
module to spend his NFT.TransferHelper
is an immutable, non-upgradable contract.
- Bart then approves the
AskModule
in theModuleManager
. TheAskModule
is modular. But theModuleManager
is immutable.
- Bart lists the NFT for sale in the
AskModule
that has arbitrary rules on how to sell NFT
- When Ned purchases the NFT from the
AskModule
, theAskModule
requests the transfer of Bart’s NFT from theTransferHelper
module and theTransferHelper
module checks with theModuleManager
to see if Bart has approved theAskModule
.
- If approved, the
TransferHelper
transfers the NFT from Bart to Ned.
- Why this is good?
- Modules are extremely constrained in their behaviour
- Users have full control over what modules are to be trusted
- New modules can be added with new functionality
- Owner/DAO can remove a module from the
ModuleManager
if there is a bug in any modules.
Composability
- A good way to implement composability is by handling all accounting tasks through
tokenization
. eg: uniswap LP tokens, compound cToken
- When you provide liquidity on Uniswap, you get LP tokens that handles the accounting of your provided liquidity. This let’s other protocol build on top of these LP tokens.
Diamond Storage Proxy Pattern
Resources
https://www.youtube.com/watch?v=da52yRwWi1E&t=1750s