How Diamond Upgrades Work
Nick Mudge
Posted on May 14, 2021
To understand diamond upgrades, let's first understand how function calls work in contracts created by Solidity.
How Function Calls Work
In order for a transaction to execute a function on a smart contract there needs to be a way for a transaction to specify to the contract what function to execute. This is done by a transaction supplying a four-byte value that identifies for a contract what function to execute.
The four-byte value is called a function selector. It consists of the first four bytes of a hash of a string of a function signature.
A function signature is a string that consists of a function name and the types of its arguments. Let's look at an example. Here's a function signature: "transfer(address,uint256)"
Here we get the first four bytes of a hash of that function signature:
bytes functionSelector = bytes4(keccak256("transfer(address,uint256)"));
The function selector is0xa9059cbb
. So if the first four bytes of transaction data is that value and the transaction is sent to a contract, then it tells that contract to execute the "transfer(address,uint256)"
function.
How Diamond Upgrades Work
Diamonds store function selectors and contract addresses of contracts that have the functions that the function selectors identify. The contracts that diamonds use are called facets.
A diamond has a mapping that maps function selectors to facet addresses.
Adding functions to a diamond means adding function selectors to a diamond and adding the facet addresses of the facets that have the functions that the function selectors identify.
Replacing functions means associating existing function selectors in a diamond with different facet addresses.
Removing functions means removing function selectors and their facet addresses from the diamond.
The diamondCut
function is used to upgrade diamonds. It has a _diamondCut
argument that consists of an array of function selectors and facet addresses and an action which specifies whether to add, replace or remove them. Here's an example of a value for the _diamondCut
argument:
[
{
facetAddress: '0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82',
action: 0,
functionSelectors: [
'0x4cd5d1f7',
'0x24a6665e',
'0x4113e5ca',
'0x49aa1f27',
'0x24d86f00',
'0x2e0bcb43',
'0xbec10cde',
'0xa3ea00f1'
]
},
{
facetAddress: '0x549549085493058324890438543949485a499A1b',
action: 1,
functionSelectors: [
'0x45943954',
'0xa4349495',
'0x9684834e'
]
}
]
In the example above the action value of 0 means adding new functions. The action value of 1 means replacing the functions by replacing the facet address for them.
In Javascript scripts the FacetCutAction object can used to more explicitly show the action. Here is its definition:
const FacetCutAction = { Add: 0, Replace: 1, Remove: 2 }
Here is an example of a Javascript script that creates the _diamondCut
argument:
const FacetCutAction = { Add: 0, Replace: 1, Remove: 2 }
const newFuncs = [
getSelector('function setSleeves(tuple(uint256 sleeveId, uint256 wearableId)[] calldata _sleeves) external'),
getSelector('function updateSvg(string calldata _svg, tuple(bytes32 svgType, uint256[] ids, uint256[] sizes)[] calldata _typesAndIdsAndSizes) external')
]
const existingFuncs = getSelectors(facet).filter(selector => !newFuncs.includes(selector))
const _diamondCut = [
{
facetAddress: facet.address,
action: FacetCutAction.Add,
functionSelectors: newFuncs
},
{
facetAddress: facet.address,
action: FacetCutAction.Replace,
functionSelectors: existingFuncs
}
]
console.log(_diamondCut)
The AavegotchiDiamond has many upgrade scripts. Check them out for more code examples.
You may want to initialize state after an upgrade. That is what the second and third arguments of the diamondCut
function are for. The _init
argument holds the contract address of a function to call to initialize the state of the diamond. The _calldata
argument holds a function call to send to the contract at _init
. The function call is executed with delegatecall so that the diamond's state is affected and can be initialized.
Executing an initialization function by using the _init
and _calldata
arguments when making an upgrade means that the upgrade and state initialization are done in the same transaction. This is good because it prevents the diamond from getting into a bad or inconsistent state, which could possibly happen if an upgrade and state initialization are done in separate transactions.
Posted on May 14, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.