This repository contains examples of performing upgrades to Managed and Dynamic smart accounts.
Both of these accounts are upgradeable and written in the dynamic contract pattern. Read more about how these accounts work in this deep dive post.
account-upgrade-examples
|
|-- src: "extension contracts used for upgrades to smart accounts"
|-- test: "tests illustrating an upgrade method and account functionality pre/post upgrade."
|-- scripts: "scripts you can run that perform the upgrades showcased in tests"
Clone this repoitory
git clone https://github.com/thirdweb-example/smart-account-upgrades.git
Install dependencies
yarn install
forge install
Deploy an extension contract to use for upgrades
npx thirdweb deploy --contract-name {name}
Perform an upgrade on a managed / dynamic account factory
ts-node scripts/{upgrade-script-name}.ts
- Contract:
src/NFTAllowlist.sol
- Test:
test/AccountUpgradeNFTAllowlist.t.sol
- Upgrade script:
scripts/accountUpgradeNFTAllowlist.ts
This extension (NFTAllowlist
) allows the account admin to configure an allowlist of NFTs that the account is allowed to receive.
On transferring an NFT to an account (smart contract) via the safeTransferFrom
method, the NFT contracts calls the onERC721Received
/ onERC1155Received
methods. This extension overrides these methods to check if the caller is included in the mentioned allowlist.
Example use case: Allow accounts created on your app to only receive and own only in-app / approved NFTs.
Upgrade steps:
-
Disable
onERC721Received
,onERC1155Received
andonERC1155BatchReceived
functions on theAccountExtension
default extension by callingManagedAccountFactory.disableFunctionInExtension
.This is to prevent conflicts when adding the
NFTAllowlist
extension where we define the updated NFT callback functions we want active in the smart account. -
Add
NFTAllowlist
as an extension to the smart account by callingManagedAccount.addExtension
.
The Managed and Dynamic variety of smart accounts are upgradeable. Writing upgrades for these smart accounts comes down to understanding:
- How upgrades work for dynamic contracts.
- The difference in the upgrade-setup for managed and dynamic smart accounts.
- Writing extension smart contracts that contain the logic to add to the account.
- Using the dynamic contracts API to perform your the upgrade.
The job of a proxy contract is to forward any calls it receives to the implementation contract via delegateCall. As a shorthand — a proxy contract stores state, and always asks an implementation contract how to mutate its state (upon receiving a call).
The dynamic contract pattern introduces a Router
smart contract.
This router contract is a proxy, but instead of always delegateCall-ing the same implementation contract, a router delegateCalls particular implementation contracts (a.k.a “Extensions”) for the particular function calls it receives:
A router stores a map from function selectors → to the implementation contract where the given function is implemented. “Upgrading a contract” now simply means updating what implementation contract a given function, or functions are mapped to.
Dynamic accounts:
The DynamicAccount
account smart contract is written in the dynamic contract pattern and inherits the router contract mentioned previously. This means that for each individual DynamicAccount
account created via a DynamicAccountFactory
-- the admin of a given account decides what upgrades to make to their own individual account.
Managed Accounts:
Like the dynamic accounts, the ManagedAccount
account contract is also written in the dynamic contract pattern.
The main difference between these two types of account contracts is that each individual dynamic account stores its own map of function selectors → to extension contracts, whereas all managed account contracts listen into the same map stored by their parent ManagedAccountFactory factory contracts.
This is why managed accounts are called “managed”. An admin of the managed account factory contracts is responsible for managing the capabilities of the factory’s children managed accounts.
When an admin of a managed account factory updates the function selector → extension map in the factory contract (through), this upgrade is instantly applied to all of the factory’s children account contracts.
For boilerplate code of an extension smart contract, run the following in your contracts project:
thirdweb create --extension
An Extension
contract is written like any other smart contract, except that its state must be defined using a struct
within a library
and at a well defined storage location. This storage technique is known as storage structs.
Example: NFTAllowlistStorage
defines the storage layout for the NFTAllowlist
contract.
// SPDX-License-Identifier: Apache 2.0
pragma solidity ^0.8.0;
/// @author thirdweb
library NFTAllowlistStorage {
/// @custom:storage-location erc7201:nft.allowlist.storage
/// @dev keccak256(abi.encode(uint256(keccak256("nft.allowlist.storage")) - 1)) & ~bytes32(uint256(0xff))
bytes32 internal constant NFT_ALLOWLIST_STORAGE_POSITION = keccak256(abi.encode(uint256(keccak256("nft.allowlist.storage")) - 1)) & ~bytes32(uint256(0xff));
struct Data {
mapping(address => bool) allowlisted;
}
function data() internal pure returns (Data storage s) {
bytes32 loc = NFT_ALLOWLIST_STORAGE_POSITION;
assembly {
s.slot := loc
}
}
}
Each Extension
of a router must occupy a unique, unused storage location. This is important to ensure that state updates defined in one Extension
doesn't conflict with the state updates defined in another Extension
, leading to corrupted state.
Find an in-depth explanation of extensions in this post.
The ManagedAccountFactory
and DynamicAccount
contracts implement the ExtensionManager API. This API exposes the following methdods for performing upgrades:
- addExtension: add a new extension to the account. All calls to functions specified in this extension will be routed to the implementation provided along with this extension.
- replaceExtension: replace an existing extension of the account. This replaces all of the extension's data stored on the contract with the provided input -- this includes all functions and the implementation associated with the extension.
- removeExtension: remove an existing extension of the account. This deletes the extension namespace and all extension data from the contract.
- disableFunctionInExtension: deletes the map of a specific function to the given extension's implementation.
- enableFunctionInExtension: maps a specific extension to the given extension's implementation.
All examples in this repo use a combination of these functions to perform an upgrade.