diff --git a/README.md b/README.md index ce74964..15567e9 100644 --- a/README.md +++ b/README.md @@ -1,59 +1,116 @@ -# Dynamic Contracts Standard +# ERC-7504: Dynamic Contracts standard. -### Architectural pattern for writing dynamic smart contracts in Solidity +**Architectural pattern for writing client-friendly one-to-many proxy contracts (aka 'dynamic contracts') in Solidity.** -This repository provides core interfaces and preset implementations that: +This repository implements ERC-7504: Dynamic Contracts [[DRAFT](https://ethereum-magicians.org/t/erc-7504-dynamic-contracts/15551)]. This repository provides core interfaces and preset implementations that: -- Provide guardrails for writing dynamic contracts that can have functionality added, updated or removed over time -- Enables scaling up contracts by eliminating the restriction of contract size limit altogether +- Provide guardrails for writing dynamic contracts that can have functionality added, updated or removed over time. +- Enables scaling up contracts by eliminating the restriction of contract size limit altogether. -> This architecture builds upon the diamond pattern ([EIP-2535](https://eips.ethereum.org/EIPS/eip-2535)). We've taken inspiration from it, and boiled it down to its leanest, simplest form. +> ⚠️ **ERC-7504** [DRAFT] is now published and open for feedback! You can read the EIP and provide your feedback at its [ethereum-magicians discussion link](https://ethereum-magicians.org/t/erc-7504-dynamic-contracts/15551). -## Installation +# Installation -#### Forge +### Forge projects: ```bash forge install https://github.com/thirdweb-dev/dynamic-contracts ``` -#### Hardhat +### Hardhat / JS based projects: ```bash npm install @thirdweb-dev/dynamic-contracts ``` -## Core concepts +```shell +src +| +|-- core +| |- Router: "Minmal abstract contract implementation of EIP-7504 Router." +| |- RouterPayable: "A Router with `receive` as a fixed function." +| +|-- presets +| |-- ExtensionManager: "Defined storage layout and API for managing a router's extensions." +| |-- DefaultExtensionSet: "A static store of a set of extensions, initialized on deployment." +| |-- BaseRouter: "A Router with an ExtensionManager." +| |-- BaseRouterWithDefaults: "A BaseRouter initialized with extensions on deployment." +| +|-- interface: "Interfaces for core and preset contracts." +|-- example: "Example dynamic contracts built with presets." +|-- lib: "Storage layouts and helper libraries." +``` -- A `Router` contract can route function calls to any number of destination contracts -- We call these destination contracts `Extensions`. -- `Extensions` can be added/updated/removed at any time, according to a predefined set of rules. +# Running locally -![router-pattern](/docs/img/router-diagram.png) +This repository is a forge project. ([forge handbook](https://book.getfoundry.sh/)) -## Getting started +**Clone the repository:** -### 1. `Router` - the entrypoint contract +```bash +git clone https://github.com/thirdweb-dev/dynamic-contracts.git +``` -The simplest way to write a `Router` contract is to extend the preset [`BaseRouter`](/src/presets/BaseRouter.sol) available in this repository. +**Install dependencies:** -```solidity -import "lib/dynamic-contracts/src/presets/BaseRouter.sol"; +```bash +forge install +``` + +**Compile contracts:** + +```bash +forge build +``` + +**Run tests:** + +```bash +forge test +``` + +**Generate documentation** + +```bash +forge doc --serve --port 4000 ``` -The `BaseRouter` contract comes with an API to add/update/remove extensions from the contract. It is an abstract contract, and expects its consumer to implement the `_canSetExtension(...)` function, which specifies the conditions under which `Extensions` can be added, updated or removed. The rest of the implementation is generic and usable for all purposes. +# Core concepts + +An “upgradeable smart contract” is actually two kinds of smart contracts considered together as one system: + +1. **Proxy** smart contract: The smart contract whose state/storage we’re concerned with. +2. **Implementation** smart contract: A stateless smart contract that defines the logic for how the proxy smart contract’s state can be mutated. + +![A proxy contract that forwards all calls to a single implementation contract](https://ipfs.io/ipfs/QmdzTiw5YuaMa1rjBtoyDuGHHRLdi9Afmh2Tu9Rjj1XuoA/proxy-with-single-impl.png) + +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). + +ERC-7504 introduces a `Router` smart contract. + +![A router contract that forwards calls to one of many implementation contracts based on the incoming calldata](https://ipfs.io/ipfs/Qmasd6DHrqMnkhifoapWAeWSs8eEJoFbzKJUpeEBacPAM7/router-many-impls.png) + +Instead of always delegateCall-ing the same implementation contract, a `Router` delegateCalls a particular implementation contract (i.e. “Extension”) for the particular function call 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. + +![Upgrading a contract means updating what implementation a given function, or functions are mapped to](https://ipfs.io/ipfs/QmUWk4VrFsAQ8gSMvTKwPXptJiMjZdihzUNhRXky7VmgGz/router-upgrades.png) + +# Getting started + +The simplest way to write a `Router` contract is to extend the preset [`BaseRouter`](/src/presets/BaseRouter.sol) available in this repository. ```solidity -function _canSetExtension(Extension memory _extension) internal view virtual returns (bool); +import "lib/dynamic-contracts/src/presets/BaseRouter.sol"; ``` -Here's a very simple example that allows only the original contract deployer to add/update/remove `Extensions`. +The `BaseRouter` contract comes with an API to add/replace/remove extensions from the contract. It is an abstract contract, and expects its consumer to implement the `_isAuthorizedCallToUpgrade` function, which specifies the conditions under which `Extensions` can be added, replaced or removed. The rest of the implementation is generic and usable for all purposes. ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import "lib/dynamic-contracts/src/presets/BaseRouter.sol"; +import "@thirdweb-dev/dynamic-contracts/src/presets/BaseRouter.sol"; /// Example usage of `BaseRouter`, for demonstration only @@ -61,216 +118,592 @@ contract SimpleRouter is BaseRouter { address public deployer; - constructor(Extension[] memory _extensions) BaseRouter(_extensions) { + constructor() { deployer = msg.sender; } - /// @dev Returns whether extensions can be set in the given execution context. - function _canSetExtension(Extension memory _extension) internal view virtual override returns (bool) { + /// @dev Returns whether all relevant permission checks are met before any upgrade. + function isAuthorizedCallToUpgrade() internal view virtual override returns (bool) { return msg.sender == deployer; } } ``` -#### Choosing a permission model: - -The main decision as a `Router` contract author is to decide the permission model to add/update/remove extensions. This repository offers some presets for a few possible permission models: +## Choosing a permission model -- #### [`RouterUpgradeable`](/src/presets/example/RouterUpgradeable.sol) +The main decision as a `Router` contract author is to decide the permission model to add/replace/remove extensions. This repository offers some examples of a few possible permission models: -This a is a preset that **allows the contract owner to add / upgrade / remove extensions**. The contract owner can be changed. This is a very basic permission model, but enough for some use cases. You can expand on this and use a permission based model instead for example. +- [**RouterImmutable**](https://github.com/thirdweb-dev/dynamic-contracts/blob/main/src/example/RouterImmutable.sol) -- #### [`RouterImmutable`](/src/presets/example/RouterImmutable.sol) + This is a preset you can use to create static contracts that cannot be updated or get new functionality. This still allows you to create modular contracts that go beyond the contract size limit, but guarantees that the original functionality cannot be altered. With this model, you would pass all the Extensions for this contract at construction time, and guarantee that the functionality is immutable. -This is a preset you can use to **create static contracts that cannot be updated or get new functionality**. This still allows you to create modular contracts that go beyond the contract size limit, but guarantees that the original functionality cannot be altered. With this model, you would pass all the `Extensions` for this contract at construction time, and guarantee that the functionality is immutable. +- [**RouterUpgradeable**](https://github.com/thirdweb-dev/dynamic-contracts/blob/main/src/example/RouterUpgradeable.sol) -Other permissions models might include an explicit list of extensions that can be added or removed for example. The implementation is up to the Router author. + This a is a preset that allows the contract owner to add / replace / remove extensions. The contract owner can be changed. This is a very basic permission model, but enough for some use cases. You can expand on this and use a permission based model instead for example. -- #### [`RouterRegistryConstrained`](/src/presets/example/RouterRegistryConstrained.sol) +- [**RouterRegistryContrained**](https://github.com/thirdweb-dev/dynamic-contracts/blob/main/src/example/RouterRegistryConstrained.sol) -This is a preset that **allows the owner to change extensions if they are defined on a given registry contract**. This is meant to demonstrate how a protocol ecosystem could constrain extensions to known, audited contracts, for instance. The registry and router upgrade models are of course too basic for production as written. + This is a preset that allows the owner to change extensions if they are defined on a given registry contract. This is meant to demonstrate how a protocol ecosystem could constrain extensions to known, audited contracts, for instance. The registry and router upgrade models are of course too basic for production as written. -### 2. `Extensions` - implementing routeable contracts +## Writing extension smart contracts -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](https://mirror.xyz/horsefacts.eth/EPB4o-eyDl0N8gu0gEz1uw7BTITheaZUqIAOEK1m-jE). This is important to ensure that state defined in an `Extension` doesn't conflict with the state of another `Extension` of the same `Router` at the same storage location. +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](https://mirror.xyz/horsefacts.eth/EPB4o-eyDl0N8gu0gEz1uw7BTITheaZUqIAOEK1m-jE). -Here's an example of a simple contract written as an `Extension` contract: +**Example:** `ExtensionManagerStorage` defines the storage layout for the `ExtensionManager` contract. ```solidity +// SPDX-License-Identifier: MIT +// @author: thirdweb (https://github.com/thirdweb-dev/dynamic-contracts) + +pragma solidity ^0.8.0; + +import "./StringSet.sol"; +import "../interface/IExtension.sol"; + +library ExtensionManagerStorage { -/// library defining the data structure of our contract -library NumberStorage { - /// specify the storage location, needs to be unique - bytes32 public constant NUMBER_STORAGE_POSITION = keccak256("number.storage"); + /// @custom:storage-location erc7201:extension.manager.storage + bytes32 public constant EXTENSION_MANAGER_STORAGE_POSITION = keccak256(abi.encode(uint256(keccak256("extension.manager.storage")) - 1)); - /// the state data struct struct Data { - uint256 number; + /// @dev Set of names of all extensions stored. + StringSet.Set extensionNames; + /// @dev Mapping from extension name => `Extension` i.e. extension metadata and functions. + mapping(string => IExtension.Extension) extensions; + /// @dev Mapping from function selector => metadata of the extension the function belongs to. + mapping(bytes4 => IExtension.ExtensionMetadata) extensionMetadata; } - /// state accessor, always use this to access the state data - function numberStorage() internal pure returns (Data storage numberData) { - bytes32 position = NUMBER_STORAGE_POSITION; + function data() internal pure returns (Data storage data_) { + bytes32 position = EXTENSION_MANAGER_STORAGE_POSITION; assembly { - numberData.slot := position + data_.slot := position } } } +``` -/// implementation of our contract's logic, notice the lack of local state -/// state is always accessed via the storage library defined above -contract Number { +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. - function setNumber(uint256 _newNumber) external { - NumberStorage.Data storage data = NumberStorage.numberStorage(); - data.number = _newNumber; - } +## Extensions: logical grouping of functionality - function getNumber() external view returns (uint256) { - NumberStorage.Data storage data = NumberStorage.numberStorage(); - return data.number; - } +By itself, the core `Router` contract does not specify _how to store or fetch_ appropriate implementation addresses for incoming function calls. + +While the Router pattern allows to point to a different contract for each function, in practice functions are usually groupped by functionality related to a shared state (a read and a set function for example). + +To make the pattern more practical, we created a generic `BaseRouter` contract that makes it easy to have logical group of functions plugged in and out of it, each group of functions being implemented in a separate implementation contract. We refer to each such implementation contract as an **_extension_**. + +`BaseRouter` maintains a `function_signature` → `implementation` mapping, and provides an API for updating that mapping. By updating the values stored in this map, functionality can be added to, removed from or updated in the smart contract. + +![Upgrading a contract means updating what implementation a given function, or functions are mapped to](https://ipfs.io/ipfs/QmUWk4VrFsAQ8gSMvTKwPXptJiMjZdihzUNhRXky7VmgGz/router-upgrades.png) + +## Deploying a Router + +Deploying a contract in the router pattern looks a little different from deploying a regular contract. + +1. Deploy all your `Extension` contracts first. You only need to do this once per `Extension`. Deployed `Extensions` can be re-used by many different `Router` contracts. + +2. Deploy your `Router` contract that implements `BaseRouter`. +3. Add extensions to youe router via the API available in `BaseRouter`. (Alternatively, you can use `BaseRouterDefaults` which can be initialized with a set of extensions on deployment.) + +### `Extensions` - Grouping logical functionality together + +By itself, the core `Router` contract does not specify _how to store or fetch_ appropriate implementation addresses for incoming function calls. + +While the Router pattern allows to point to a different contract for each function, in practice functions are usually groupped by functionality related to a shared state (a read and a set function for example). + +To make the pattern more practical, we created a generic `BaseRouter` contract that makes it easy to have logical group of functions plugged in and out of it, each group of functions being implemented in a separate implementation contract. We refer to each such implementation contract as an **_extension_**. + +`BaseRouter` maintains a `function_signature` → `implementation` mapping, and provides an API for updating that mapping. By updating the values stored in this map, functionality can be added to, removed from or updated in the smart contract. + +## Extension to Extension communication + +When splitting logic between multiple extensions in a `Router`, one might want to access data from one `Extension` to another. + +A simple way to do this is by casting the current contract address as the `Extension` (ideally its interface) we're trying to call. This works from both a `Router` or any of its extensions. + +Here's an example of accessing a `IPermission` extension from another one: + +```solidity +modifier onlyAdmin(address _asset) { + /// we access our IPermission extension by casting our own address + IPermissions(address(this)).hasAdminRole(msg.sender); } ``` -To compare, here is the same contract written in a regular way: +Note that if we don't have an `IPermission` extension added to our `Router`, this method will revert. + +## Upgrading Extensions + +Just like any upgradeable contract, there are limitations on how the data structure of the updated contract is modified. While the logic of a function can be updated safely, changing the data structure of a contract requires careful consideration. + +A good rule of thumb to follow is: + +- It is safe to append new fields to an existing data structure +- It is _not_ safe to update the type or order of existing structs; deprecate and add new ones instead. + +Refer to [this article](https://mirror.xyz/horsefacts.eth/EPB4o-eyDl0N8gu0gEz1uw7BTITheaZUqIAOEK1m-jE) for more information. + +# API reference + +You can generate and view the full API reference for all contracts, interfaces and libraries in the repository by running the repository locally and running: + +```bash +forge doc --serve --port 4000 +``` + +## Router ```solidity -contract Number { +import "@thirdweb-dev/dynamic-contracts/src/core/Router.sol"; +``` - uint256 private number; +The `Router` smart contract implements the ERC-7504 [`Router` interface](https://github.com/thirdweb-dev/dynamic-contracts/blob/main/src/interface/IRouter.sol). - function setNumber(uint256 _newNumber) external { - number = _newNumber; - } +For any given function call made to the Router contract that reaches the fallback function, the contract performs a delegateCall on the address returned by `getImplementationForFunction(msg.sig)`. - function getNumber() external view returns (uint256) { - return number; - } +This is an abstract contract that expects you to override and implement the following functions: + +- `getImplementationForFunction` + ```solidity + function getImplementationForFunction(bytes4 _functionSelector) public view virtual returns (address implementation); + ``` + +### fallback + +delegateCalls the appropriate implementation address for the given incoming function call. + +_The implementation address to delegateCall MUST be retrieved from calling `getImplementationForFunction` with the +incoming call's function selector._ + +```solidity +fallback() external payable virtual; +``` + +#### Revert conditions: + +- `getImplementationForFunction(msg.sig) == address(0)` + +### \_delegate + +_delegateCalls an `implementation` smart contract._ + +```solidity +function _delegate(address implementation) internal virtual; +``` + +### getImplementationForFunction + +Returns the implementation address to delegateCall for the given function selector. + +```solidity +function getImplementationForFunction(bytes4 _functionSelector) public view virtual returns (address implementation); +``` + +**Parameters** + +| Name | Type | Description | +| ------------------- | -------- | ------------------------------------------------------------ | +| `_functionSelector` | `bytes4` | The function selector to get the implementation address for. | + +**Returns** + +| Name | Type | Description | +| ---------------- | --------- | --------------------------------------------------------------------------- | +| `implementation` | `address` | The implementation address to delegateCall for the given function selector. | + +## ExtensionManager + +```solidity +import "@thirdweb-dev/dynamic-contracts/src/presets/ExtensionManager.sol"; +``` + +The `ExtensionManager` contract provides a defined storage layout and API for managing and fetching a router's extensions. This contract implements the ERC-7504 [`RouterState` interface](https://github.com/thirdweb-dev/dynamic-contracts/blob/main/src/interface/IRouterState.sol). + +The contract's storage layout is defined in `src/lib/ExtensionManagerStorage`: + +```solidity +struct Data { + StringSet.Set extensionNames; + mapping(string => IExtension.Extension) extensions; + mapping(bytes4 => IExtension.ExtensionMetadata) extensionMetadata; } ``` -The main difference is how the state is defined. While an `Extension` written this way requires a bit more boilerplate to setup, it is a one time cost that ensures full modularity when using multiple `Extension` contracts with a single `Router`. +The following are some helpful **invariant properties** of `ExtensionManager`: -### 3. Deploying a `Router` +- Each extension has a non-empty, unique name which is stored in `extensionNames`. +- Each extension's metadata specifies a _non_-zero-address implementation. +- A function `fn` has a non-empty metadata i.e. `extensionMetadata[fn]` value _if and only if_ it is a part of some extension `Ext` such that: -Deploying a contract in the router pattern looks a little different from deploying a regular contract. + - `extensionNames` contains `Ext.metadata.name` + - `extensions[Ext.metadata.name].functions` includes `fn`. -1. Deploy all your `Extension` contracts first. You only need to do this once per `Extension`. Deployed `Extensions` can be re-used by many different `Router` contracts. +This contract is meant to be used along with a Router contract, where an upgrade to the Router means updating the storage of `ExtensionManager`. For example, the preset contract `BaseRouter` inherits `Router` and `ExtensionManager` and overrides the `getImplementationForFunction` function as follows: -2. Deploy your `Router` contract that implements `BaseRouter`. +```solidity +function getImplementationForFunction(bytes4 _functionSelector) public view virtual override returns (address) { + return getMetadataForFunction(_functionSelector).implementation; + } +``` + +This contract is an abstract contract that expects you to override and implement the following functions: + +- `isAuthorizedCallToUpgrade` + ```solidity + function isAuthorizedCallToUpgrade() internal view virtual returns (bool); + ``` + +### onlyAuthorizedCall + +Checks that a call to any external function is authorized. + +```solidity +modifier onlyAuthorizedCall(); +``` -3. Optionally, you pass your default `Extensions` in the constructor of your `BaseRouter` at deploy time. This is a convenient way to bootstrap an `Router` with a set of default `Extension` in one transaction. +#### Revert conditions: -### 4. Adding, removing or upgrading `Extensions` post deployment +- `!_isAuthorizedCallToUpgrade()` -The preset `BaseRouter` comes with an API to add/update/remove `Extensions` at any time after deployment: +### getAllExtensions -- `addExtension()`: function to add completely new `Extension` to your `Router`. -- `updateExtension()`: function to update the address, metadata, or functions of an existing `Extension` in your `Router`. -- `removeExtension()`: remove an existing `Extension` from your `Router`. +Returns all extensions of the Router. -The permission to modify `Extensions` is encoded in your `Router` and can have different conditions. +```solidity +function getAllExtensions() external view virtual override returns (Extension[] memory allExtensions); +``` -With this pattern, your contract is now dynamically updeatable, with granular control. +**Returns** -- Add entire new functionality to your contract post deployment -- Remove functionality when it's not longer needed -- Deploy security and bug fixes for a single function of your contract +| Name | Type | Description | +| --------------- | ------------- | --------------------------- | +| `allExtensions` | `Extension[]` | An array of all extensions. | ---- +### getMetadataForFunction -## Going deeper - background and technical details +Returns the extension metadata for a given function. -In the standard proxy pattern for smart contracts, a proxy smart contract calls a _logic contract_ using `delegateCall`. This allows proxies to keep a persistent state (storage and balance) while the code is delegated to the logic contract. ([EIP-1967](https://eips.ethereum.org/EIPS/eip-1967)) +```solidity +function getMetadataForFunction(bytes4 functionSelector) public view virtual returns (ExtensionMetadata memory); +``` -The pattern aims to solve for the following two limitations of this standard proxy pattern: +**Parameters** -1. The proxy contract points to a single smart contract as its _logic contract_, at a time. -2. The _logic contract_ is subject to the smart contract size limit of ~24kb ([EIP-170](https://eips.ethereum.org/EIPS/eip-170)). This prevents a single smart contract from having all of the features one may want it to have. +| Name | Type | Description | +| ------------------ | -------- | -------------------------------------------------------- | +| `functionSelector` | `bytes4` | The function selector to get the extension metadata for. | -> **Note:** The diamond pattern ([EIP-2535](https://eips.ethereum.org/EIPS/eip-2535)) anticipates these same problems and more. We've taken inspiration from it, and boiled it down to its leanest, simplest form. +**Returns** -The router pattern eliminates these limitations performing a lookup for the implementation smart contract address associated with every incoming function call, and make a `delegateCall` to that particular implementation. +| Name | Type | Description | +| -------- | ------------------- | ----------------------------------------------------- | +| `` | `ExtensionMetadata` | metadata The extension metadata for a given function. | -This is different from the standard proxy pattern, where the proxy stores a single implementation smart contract address, and calls via `delegateCall` this same implementation for every incoming function call. +### getExtension -**Standard proxy pattern** +Returns the extension metadata and functions for a given extension. ```solidity -contract StandardProxy { +function getExtension(string memory extensionName) public view virtual returns (Extension memory); +``` - address public constant implementation = 0xabc...; +**Parameters** - fallback() external payable virtual { - _delegateCall(implementation); - } -} +| Name | Type | Description | +| --------------- | -------- | ---------------------------------------------------------------- | +| `extensionName` | `string` | The name of the extension to get the metadata and functions for. | + +**Returns** + +| Name | Type | Description | +| -------- | ----------- | ----------------------------------------------------------- | +| `` | `Extension` | The extension metadata and functions for a given extension. | + +### addExtension + +Add a new extension to the router. + +```solidity +function addExtension(Extension memory _extension) external onlyAuthorizedCall; ``` -**Router pattern** +**Parameters** + +| Name | Type | Description | +| ------------ | ----------- | --------------------- | +| `_extension` | `Extension` | The extension to add. | + +#### Revert conditions: + +- Extension name is empty. +- Extension name is already used. +- Extension implementation is zero address. +- Selector and signature mismatch for some function in the extension. +- Some function in the extension is already a part of another extension. + +### replaceExtension + +Fully replace an existing extension of the router. + +_The extension with name `extension.name` is the extension being replaced._ ```solidity -abstract contract Router { +function replaceExtension(Extension memory _extension) external onlyAuthorizedCall; +``` - fallback() external payable virtual { - address implementation = getImplementationForFunction(msg.sig); - _delegateCall(implementation); - } +**Parameters** - function getImplementationForFunction(bytes4 _functionSelector) public view virtual returns (address); -} +| Name | Type | Description | +| ------------ | ----------- | -------------------------------------- | +| `_extension` | `Extension` | The extension to replace or overwrite. | + +#### Revert conditions: + +- Extension being replaced does not exist. +- Provided extension's implementation is zero address. +- Selector and signature mismatch for some function in the provided extension. +- Some function in the provided extension is already a part of another extension. + +### removeExtension + +Remove an existing extension from the router. + +```solidity +function removeExtension(string memory _extensionName) external onlyAuthorizedCall; ``` -This setup in the `Router` contract allows for different functions of the smart contract to be implemented in different logic contracts. +**Parameters** -### `Extensions` - Grouping logical functionality together +| Name | Type | Description | +| ---------------- | -------- | ------------------------------------ | +| `_extensionName` | `string` | The name of the extension to remove. | -By itself, the core `Router` contract does not specify _how to store or fetch_ appropriate implementation addresses for incoming function calls. +#### Revert conditions: -While the Router pattern allows to point to a different contract for each function, in practice functions are usually groupped by functionality related to a shared state (a read and a set function for example). +- Extension being removed does not exist. -To make the pattern more practical, we created a generic `BaseRouter` contract that makes it easy to have logical group of functions plugged in and out of it, each group of functions being implemented in a separate implementation contract. We refer to each such implementation contract as an **_extension_**. +### enableFunctionInExtension -`BaseRouter` maintains a `function_signature` → `implementation` mapping, and provides an API for updating that mapping. By updating the values stored in this map, functionality can be added to, removed from or updated in the smart contract. +Enables a single function in an existing extension. + +_Makes the given function callable on the router._ -![updating-extensions](/docs/img/update-diagram.png) +```solidity +function enableFunctionInExtension(string memory _extensionName, ExtensionFunction memory _function) + external + onlyAuthorizedCall; +``` + +**Parameters** -### `Extension` to `Extension` communication +| Name | Type | Description | +| ---------------- | ------------------- | --------------------------------------------------------- | +| `_extensionName` | `string` | The name of the extension to which `extFunction` belongs. | +| `_function` | `ExtensionFunction` | The function to enable. | -When splitting logic between multiple `Extensions` in a `Router`, one might want to access data from one `Extension` to another. +#### Revert conditions: -A simple way to do this is by casting the current contract address as the `Extension` (ideally its interface) we're trying to call. This works from both a `Router` or any of its `Extensions`. +- Provided extension does not exist. +- Selector and signature mismatch for some function in the provided extension. +- Provided function is already a part of another extension. -Here's an example of accessing a IPermission `Extension` from another one: +### disableFunctionInExtension + +Disables a single function in an Extension. ```solidity -/// in MyExtension.sol -modifier onlyAdmin(address _asset) { - /// we access our IPermission extension by casting our own address - IPermissions(address(this)).hasAdminRole(msg.sender); -} +function disableFunctionInExtension(string memory _extensionName, bytes4 _functionSelector) + external + onlyAuthorizedCall; ``` -Note that if we don't have a IPermission `Extension` added to our `Router`, this method will revert. +**Parameters** -### Upgrading `Extensions` +| Name | Type | Description | +| ------------------- | -------- | ------------------------------------------------------------------------------ | +| `_extensionName` | `string` | The name of the extension to which the function of `functionSelector` belongs. | +| `_functionSelector` | `bytes4` | The function to disable. | -Just like any upgradeable contract, there are limitations on how the data structure of the updated contract is modified. While the logic of a function can be updated safely, changing the data structure of a contract requires careful consideration. +#### Revert conditions: -A good rule of thumb to follow is: +- Provided extension does not exist. +- Provided function is not part of provided extension. -- It is safe to append new fields to an existing data structure -- It is _not_ safe to update the type or order of existing structs, deprecate and add new ones instead +### \_getExtension -Refer to [this article](https://mirror.xyz/horsefacts.eth/EPB4o-eyDl0N8gu0gEz1uw7BTITheaZUqIAOEK1m-jE) for more information. +_Returns the Extension for a given name._ + +```solidity +function _getExtension(string memory _extensionName) internal view returns (Extension memory); +``` + +### \_setMetadataForExtension + +_Sets the ExtensionMetadata for a given extension._ + +```solidity +function _setMetadataForExtension(string memory _extensionName, ExtensionMetadata memory _metadata) internal; +``` + +### \_deleteMetadataForExtension + +_Deletes the ExtensionMetadata for a given extension._ + +```solidity +function _deleteMetadataForExtension(string memory _extensionName) internal; +``` + +### \_setMetadataForFunction + +_Sets the ExtensionMetadata for a given function._ + +```solidity +function _setMetadataForFunction(bytes4 _functionSelector, ExtensionMetadata memory _metadata) internal; +``` + +### \_deleteMetadataForFunction + +_Deletes the ExtensionMetadata for a given function._ + +```solidity +function _deleteMetadataForFunction(bytes4 _functionSelector) internal; +``` + +### \_enableFunctionInExtension + +_Enables a function in an Extension._ + +```solidity +function _enableFunctionInExtension(string memory _extensionName, ExtensionFunction memory _extFunction) + internal + virtual; +``` + +### \_disableFunctionInExtension + +Note: `bytes4(0)` is the function selector for the `receive` function. +So, we maintain a special fn selector-signature mismatch check for the `receive` function. + +_Disables a given function in an Extension._ + +```solidity +function _disableFunctionInExtension(string memory _extensionName, bytes4 _functionSelector) internal; +``` + +### \_removeAllFunctionsFromExtension + +_Removes all functions from an Extension._ + +```solidity +function _removeAllFunctionsFromExtension(string memory _extensionName) internal; +``` + +### \_canAddExtension + +_Returns whether a new extension can be added in the given execution context._ + +```solidity +function _canAddExtension(Extension memory _extension) internal virtual returns (bool); +``` + +### \_canReplaceExtension + +_Returns whether an extension can be replaced in the given execution context._ + +```solidity +function _canReplaceExtension(Extension memory _extension) internal virtual returns (bool); +``` + +### \_canRemoveExtension + +_Returns whether an extension can be removed in the given execution context._ + +```solidity +function _canRemoveExtension(string memory _extensionName) internal virtual returns (bool); +``` + +### \_canEnableFunctionInExtension + +_Returns whether a function can be enabled in an extension in the given execution context._ + +```solidity +function _canEnableFunctionInExtension(string memory _extensionName, ExtensionFunction memory) + internal + view + virtual + returns (bool); +``` + +### \_canDisableFunctionInExtension + +_Returns whether a function can be disabled in an extension in the given execution context._ + +```solidity +function _canDisableFunctionInExtension(string memory _extensionName, bytes4 _functionSelector) + internal + view + virtual + returns (bool); +``` + +### \_extensionManagerStorage + +_Returns the ExtensionManager storage._ + +```solidity +function _extensionManagerStorage() internal pure returns (ExtensionManagerStorage.Data storage data); +``` + +### isAuthorizedCallToUpgrade + +_To override; returns whether all relevant permission and other checks are met before any upgrade._ + +```solidity +function isAuthorizedCallToUpgrade() internal view virtual returns (bool); +``` + +## BaseRouter + +```solidity +import "@thirdweb-dev/dynamic-contracts/src/presets/BaseRouter" +``` + +`BaseRouter` inherits `Router` and `ExtensionManager`. It overrides the `Router.getImplementationForFunction` function to use the extensions stored in the `ExtensionManager` contract's storage system. + +This contract is an abstract contract that expects you to override and implement the following functions: + +- `isAuthorizedCallToUpgrade` + ```solidity + function isAuthorizedCallToUpgrade() internal view virtual returns (bool); + ``` + +### getImplementationForFunction + +Returns the implementation address to delegateCall for the given function selector. + +```solidity +function getImplementationForFunction(bytes4 _functionSelector) public view virtual override returns (address); +``` + +**Parameters** + +| Name | Type | Description | +| ------------------- | -------- | ------------------------------------------------------------ | +| `_functionSelector` | `bytes4` | The function selector to get the implementation address for. | + +**Returns** + +| Name | Type | Description | +| -------- | --------- | ------------------------------------------------------------------------------------------ | +| `` | `address` | implementation The implementation address to delegateCall for the given function selector. | -## Feedback +# Feedback -The best, most open way to give feedback/suggestions for the router pattern is to open a github issue. +The best, most open way to give feedback/suggestions for the router pattern is to open a github issue, or comment in the ERC-7504 [ethereum-magicians discussion](https://ethereum-magicians.org/t/erc-7504-dynamic-contracts/15551). Additionally, since [thirdweb](https://thirdweb.com/) will be maintaining this repository, you can reach out to us at support@thirdweb.com or join our [discord](https://discord.gg/thirdweb). -## Authors +# Authors - [thirdweb](https://github.com/thirdweb-dev) diff --git a/docs/img/proxy-diagram.png b/docs/img/proxy-diagram.png deleted file mode 100644 index 71cc624..0000000 Binary files a/docs/img/proxy-diagram.png and /dev/null differ diff --git a/docs/img/router-diagram.png b/docs/img/router-diagram.png deleted file mode 100644 index 9053c3b..0000000 Binary files a/docs/img/router-diagram.png and /dev/null differ diff --git a/docs/img/update-diagram.png b/docs/img/update-diagram.png deleted file mode 100644 index 3dd1781..0000000 Binary files a/docs/img/update-diagram.png and /dev/null differ diff --git a/src/core/Router.sol b/src/core/Router.sol index 71bbfae..171639b 100644 --- a/src/core/Router.sol +++ b/src/core/Router.sol @@ -1,14 +1,20 @@ // SPDX-License-Identifier: MIT -// @author: thirdweb (https://github.com/thirdweb-dev/dynamic-contracts) - pragma solidity ^0.8.0; import "../interface/IRouter.sol"; +/// @title ERC-7504 Dynamic Contracts: Router. +/// @author thirdweb (https://github.com/thirdweb-dev/dynamic-contracts) +/// @notice Routes an incoming call to an appropriate implementation address. + abstract contract Router is IRouter { + /** + * @notice delegateCalls the appropriate implementation address for the given incoming function call. + * @dev The implementation address to delegateCall MUST be retrieved from calling `getImplementationForFunction` with the + * incoming call's function selector. + */ fallback() external payable virtual { - /// @dev delegate calls the appropriate implementation smart contract for a given function. address implementation = getImplementationForFunction(msg.sig); require(implementation != address(0), "Router: function does not exist."); _delegate(implementation); @@ -40,6 +46,10 @@ abstract contract Router is IRouter { } } - /// @dev Returns the implementation contract address for a given function signature. - function getImplementationForFunction(bytes4 _functionSelector) public view virtual returns (address); + /** + * @notice Returns the implementation address to delegateCall for the given function selector. + * @param _functionSelector The function selector to get the implementation address for. + * @return implementation The implementation address to delegateCall for the given function selector. + */ + function getImplementationForFunction(bytes4 _functionSelector) public view virtual returns (address implementation); } \ No newline at end of file diff --git a/src/core/RouterPayable.sol b/src/core/RouterPayable.sol index edbd41e..2913eda 100644 --- a/src/core/RouterPayable.sol +++ b/src/core/RouterPayable.sol @@ -1,10 +1,14 @@ // SPDX-License-Identifier: MIT -// @author: thirdweb (https://github.com/thirdweb-dev/dynamic-contracts) - pragma solidity ^0.8.0; import "./Router.sol"; +/// @title IRouterPayable. +/// @author thirdweb (https://github.com/thirdweb-dev/dynamic-contracts) +/// @notice A Router with `receive` as a fixed function. + abstract contract RouterPayable is Router { + + /// @notice Lets a contract receive native tokens. receive() external payable virtual {} } \ No newline at end of file diff --git a/src/example/RouterImmutable.sol b/src/example/RouterImmutable.sol index 62aea18..771643c 100644 --- a/src/example/RouterImmutable.sol +++ b/src/example/RouterImmutable.sol @@ -17,7 +17,7 @@ contract RouterImmutable is BaseRouterWithDefaults { Overrides //////////////////////////////////////////////////////////////*/ - /// @dev Returns whether a function can be disabled in an extension in the given execution context. + /// @dev Returns whether all relevant permission and other checks are met before any upgrade. function isAuthorizedCallToUpgrade() internal view virtual override returns (bool) { return false; } diff --git a/src/example/RouterRegistryConstrained.sol b/src/example/RouterRegistryConstrained.sol index 2479d80..846eafa 100644 --- a/src/example/RouterRegistryConstrained.sol +++ b/src/example/RouterRegistryConstrained.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.0; -import "../presets/BaseRouterWithDefaults.sol"; +import "../presets/BaseRouter.sol"; /** * This smart contract is an EXAMPLE, and is not meant for use in production. @@ -26,18 +26,18 @@ contract ExtensionRegistry { /** * This smart contract is an EXAMPLE, and is not meant for use in production. */ -contract RouterRegistryConstrained is BaseRouterWithDefaults { +contract RouterRegistryConstrained is BaseRouter { address public admin; ExtensionRegistry public registry; - // @dev Cannot initialize with extensions before registry is set, so we pass empty array to base constructor. - constructor(address _registry) BaseRouterWithDefaults(new Extension[](0)) { + /// @dev Cannot initialize with extensions before registry is set, so we pass empty array to base constructor. + constructor(address _registry) { admin = msg.sender; registry = ExtensionRegistry(_registry); } - // @dev Sets the admin address. + /// @dev Sets the admin address. function setAdmin(address _admin) external { require(msg.sender == admin, "RouterUpgradeable: Only admin can set a new admin"); admin = _admin; @@ -47,7 +47,7 @@ contract RouterRegistryConstrained is BaseRouterWithDefaults { Overrides //////////////////////////////////////////////////////////////*/ - /// @dev Returns whether a function can be disabled in an extension in the given execution context. + /// @dev Returns whether all relevant permission and other checks are met before any upgrade. function isAuthorizedCallToUpgrade() internal view virtual override returns (bool) { return msg.sender == admin; } diff --git a/src/interface/IExtension.sol b/src/interface/IExtension.sol index f2b9cf4..cb3dca5 100644 --- a/src/interface/IExtension.sol +++ b/src/interface/IExtension.sol @@ -1,15 +1,18 @@ // SPDX-License-Identifier: MIT -// @author: thirdweb (https://github.com/thirdweb-dev/dynamic-contracts) - pragma solidity ^0.8.0; + +/// @title IExtension +/// @author thirdweb (https://github.com/thirdweb-dev/dynamic-contracts) +/// @notice Provides an `Extension` abstraction for a router's implementation contracts. + interface IExtension { /*/////////////////////////////////////////////////////////////// Structs //////////////////////////////////////////////////////////////*/ /** - * @notice A extension's metadata. + * @notice An interface to describe an extension's metadata. * * @param name The unique name of the extension. * @param metadataURI The URI where the metadata for the extension lives. @@ -22,7 +25,7 @@ interface IExtension { } /** - * @notice An interface to describe a extension's function. + * @notice An interface to describe an extension's function. * * @param functionSelector The 4 byte selector of the function. * @param functionSignature Function signature as a string. E.g. "transfer(address,address,uint256)" diff --git a/src/interface/IExtensionManager.sol b/src/interface/IExtensionManager.sol index b5398aa..9ed6b17 100644 --- a/src/interface/IExtensionManager.sol +++ b/src/interface/IExtensionManager.sol @@ -1,56 +1,70 @@ // SPDX-License-Identifier: MIT - pragma solidity ^0.8.0; import "./IExtension.sol"; +/// @title IExtensionManager +/// @author thirdweb (https://github.com/thirdweb-dev/dynamic-contracts) +/// @notice Defined storage and API for managing a router's extensions. + interface IExtensionManager is IExtension { + /*/////////////////////////////////////////////////////////////// + Events + //////////////////////////////////////////////////////////////*/ + + /// @dev Emitted when a extension is added. + event ExtensionAdded(string indexed name, address indexed implementation, Extension extension); + + /// @dev Emitted when a extension is replaced. + event ExtensionReplaced(string indexed name, address indexed implementation, Extension extension); + + /// @dev Emitted when a extension is removed. + event ExtensionRemoved(string indexed name, Extension extension); + + /// @dev Emitted when a function is enabled i.e. made callable. + event FunctionEnabled(string indexed name, bytes4 indexed functionSelector, ExtensionFunction extFunction, ExtensionMetadata extMetadata); + + /// @dev Emitted when a function is disabled i.e. made un-callable. + event FunctionDisabled(string indexed name, bytes4 indexed functionSelector, ExtensionMetadata extMetadata); + /*/////////////////////////////////////////////////////////////// External functions //////////////////////////////////////////////////////////////*/ /** * @notice Add a new extension to the router. + * @param extension The extension to add. */ function addExtension(Extension memory extension) external; /** * @notice Fully replace an existing extension of the router. + * @dev The extension with name `extension.name` is the extension being replaced. + * @param extension The extension to replace or overwrite. */ function replaceExtension(Extension memory extension) external; /** * @notice Remove an existing extension from the router. + * @param extensionName The name of the extension to remove. */ function removeExtension(string memory extensionName) external; /** * @notice Enables a single function in an existing extension. + * @dev Makes the given function callable on the router. + * + * @param extensionName The name of the extension to which `extFunction` belongs. + * @param extFunction The function to enable. */ function enableFunctionInExtension(string memory extensionName, ExtensionFunction memory extFunction) external; /** * @notice Disables a single function in an Extension. + * + * @param extensionName The name of the extension to which the function of `functionSelector` belongs. + * @param functionSelector The function to disable. */ function disableFunctionInExtension(string memory extensionName, bytes4 functionSelector) external; - - /*/////////////////////////////////////////////////////////////// - Events - //////////////////////////////////////////////////////////////*/ - - /// @dev Emitted when a extension is added. - event ExtensionAdded(string indexed name, address indexed implementation, Extension extension); - - /// @dev Emitted when a extension is added. - event ExtensionReplaced(string indexed name, address indexed implementation, Extension extension); - - /// @dev Emitted when a extension is added. - event ExtensionRemoved(string indexed name, Extension extension); - - /// @dev Emitted when a function is updated. - event FunctionAdded(string indexed name, bytes4 indexed functionSelector, ExtensionFunction extFunction, ExtensionMetadata extMetadata); - - /// @dev Emitted when a function is removed. - event FunctionRemoved(string indexed name, bytes4 indexed functionSelector, ExtensionMetadata extMetadata); } \ No newline at end of file diff --git a/src/interface/IRouter.sol b/src/interface/IRouter.sol index 5cf86fc..7db14ec 100644 --- a/src/interface/IRouter.sol +++ b/src/interface/IRouter.sol @@ -1,12 +1,12 @@ // SPDX-License-Identifier: MIT - pragma solidity ^0.8.0; -/** - * @title ERC-7504 Dynamic Contracts. - * @dev Fallback function delegateCalls `getImplementationForFunction(msg.sig)` for a given incoming call. - * NOTE: The ERC-165 identifier for this interface is 0xce0b6013. - */ +/// @title ERC-7504 Dynamic Contracts: IRouter. +/// @author thirdweb (https://github.com/thirdweb-dev/dynamic-contracts) +/// @notice Routes an incoming call to an appropriate implementation address. +/// @dev Fallback function delegateCalls `getImplementationForFunction(msg.sig)` for a given incoming call. +/// NOTE: The ERC-165 identifier for this interface is 0xce0b6013. + interface IRouter { /** @@ -16,6 +16,14 @@ interface IRouter { */ fallback() external payable; - /// @dev Returns the implementation address to delegateCall for the given function selector. - function getImplementationForFunction(bytes4 _functionSelector) external view returns (address); + /*/////////////////////////////////////////////////////////////// + View Functions + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Returns the implementation address to delegateCall for the given function selector. + * @param _functionSelector The function selector to get the implementation address for. + * @return implementation The implementation address to delegateCall for the given function selector. + */ + function getImplementationForFunction(bytes4 _functionSelector) external view returns (address implementation); } \ No newline at end of file diff --git a/src/interface/IRouterPayable.sol b/src/interface/IRouterPayable.sol index c437a8a..82ca0eb 100644 --- a/src/interface/IRouterPayable.sol +++ b/src/interface/IRouterPayable.sol @@ -1,11 +1,13 @@ // SPDX-License-Identifier: MIT -// @author: thirdweb (https://github.com/thirdweb-dev/dynamic-contracts) - pragma solidity ^0.8.0; import "./IRouter.sol"; -/// @dev See {IRouter}. +/// @title IRouterPayable. +/// @author thirdweb (https://github.com/thirdweb-dev/dynamic-contracts) +/// @notice A Router with `receive` as a fixed function. + interface IRouterPayable is IRouter { + /// @notice Lets a contract receive native tokens. receive() external payable; } \ No newline at end of file diff --git a/src/interface/IRouterState.sol b/src/interface/IRouterState.sol index d1ccf15..c0b5fec 100644 --- a/src/interface/IRouterState.sol +++ b/src/interface/IRouterState.sol @@ -1,19 +1,21 @@ // SPDX-License-Identifier: MIT - pragma solidity ^0.8.0; import "./IExtension.sol"; -/** - * @title ERC-7504 Dynamic Contracts. - * NOTE: The ERC-165 identifier for this interface is 0x4a00cc48. - */ +/// @title ERC-7504 Dynamic Contracts: IRouterState. +/// @author thirdweb (https://github.com/thirdweb-dev/dynamic-contracts) +/// @notice Defines an API to expose a router's extensions. + interface IRouterState is IExtension { /*/////////////////////////////////////////////////////////////// View Functions //////////////////////////////////////////////////////////////*/ - /// @dev Returns all extensions of the Router. + /** + * @notice Returns all extensions of the Router. + * @return allExtensions An array of all extensions. + */ function getAllExtensions() external view returns (Extension[] memory allExtensions); } \ No newline at end of file diff --git a/src/interface/IRouterStateGetters.sol b/src/interface/IRouterStateGetters.sol index 977c689..5593a23 100644 --- a/src/interface/IRouterStateGetters.sol +++ b/src/interface/IRouterStateGetters.sol @@ -1,18 +1,29 @@ // SPDX-License-Identifier: MIT -// @author: thirdweb (https://github.com/thirdweb-dev/dynamic-contracts) - pragma solidity ^0.8.0; import "./IExtension.sol"; +/// @title IRouterStateGetters. +/// @author thirdweb (https://github.com/thirdweb-dev/dynamic-contracts) +/// @notice Helper view functions to inspect a router's state. + interface IRouterStateGetters is IExtension { + /*/////////////////////////////////////////////////////////////// View functions //////////////////////////////////////////////////////////////*/ - /// @dev Returns the extension metadata for a given function. - function getMetadataForFunction(bytes4 functionSelector) external view returns (ExtensionMetadata memory); + /** + * @notice Returns the extension metadata for a given function. + * @param functionSelector The function selector to get the extension metadata for. + * @return metadata The extension metadata for a given function. + */ + function getMetadataForFunction(bytes4 functionSelector) external view returns (ExtensionMetadata memory metadata); - /// @dev Returns the extension metadata and functions for a given extension. + /** + * @notice Returns the extension metadata and functions for a given extension. + * @param extensionName The name of the extension to get the metadata and functions for. + * @return extension The extension metadata and functions for a given extension. + */ function getExtension(string memory extensionName) external view returns (Extension memory); } \ No newline at end of file diff --git a/src/lib/ExtensionManagerStorage.sol b/src/lib/ExtensionManagerStorage.sol index bfd4c88..e9a46fa 100644 --- a/src/lib/ExtensionManagerStorage.sol +++ b/src/lib/ExtensionManagerStorage.sol @@ -1,18 +1,20 @@ // SPDX-License-Identifier: MIT -// @author: thirdweb (https://github.com/thirdweb-dev/dynamic-contracts) - pragma solidity ^0.8.0; import "./StringSet.sol"; import "../interface/IExtension.sol"; +/// @title IExtensionManagerStorage +/// @author thirdweb (https://github.com/thirdweb-dev/dynamic-contracts) +/// @notice Defined storage for managing a router's extensions. + library ExtensionManagerStorage { /// @custom:storage-location erc7201:extension.manager.storage bytes32 public constant EXTENSION_MANAGER_STORAGE_POSITION = keccak256(abi.encode(uint256(keccak256("extension.manager.storage")) - 1)); struct Data { - /// @dev Set of names of all extensions stored. + /// @dev Set of names of all extensions of the router. StringSet.Set extensionNames; /// @dev Mapping from extension name => `Extension` i.e. extension metadata and functions. mapping(string => IExtension.Extension) extensions; @@ -20,6 +22,7 @@ library ExtensionManagerStorage { mapping(bytes4 => IExtension.ExtensionMetadata) extensionMetadata; } + /// @dev Returns access to the extension manager's storage. function data() internal pure returns (Data storage data_) { bytes32 position = EXTENSION_MANAGER_STORAGE_POSITION; assembly { diff --git a/src/lib/StringSet.sol b/src/lib/StringSet.sol index 28811eb..950db3b 100644 --- a/src/lib/StringSet.sol +++ b/src/lib/StringSet.sol @@ -1,6 +1,4 @@ -// SPDX-License-Identifier: Apache 2.0 -// @author: thirdweb (https://github.com/thirdweb-dev/dynamic-contracts) - +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; library StringSet { diff --git a/src/presets/BaseRouter.sol b/src/presets/BaseRouter.sol index bffefdb..f88b86f 100644 --- a/src/presets/BaseRouter.sol +++ b/src/presets/BaseRouter.sol @@ -1,14 +1,20 @@ // SPDX-License-Identifier: MIT -// @author: thirdweb (https://github.com/thirdweb-dev/dynamic-contracts) - pragma solidity ^0.8.0; import "../core/Router.sol"; import "./ExtensionManager.sol"; +/// @title BaseRouter +/// @author thirdweb (https://github.com/thirdweb-dev/dynamic-contracts) +/// @notice A preset Router + ExtensionManager. + abstract contract BaseRouter is Router, ExtensionManager { - /// @dev Returns the implementation contract address for a given function signature. + /** + * @notice Returns the implementation address to delegateCall for the given function selector. + * @param _functionSelector The function selector to get the implementation address for. + * @return implementation The implementation address to delegateCall for the given function selector. + */ function getImplementationForFunction(bytes4 _functionSelector) public view virtual override returns (address) { return getMetadataForFunction(_functionSelector).implementation; } diff --git a/src/presets/BaseRouterWithDefaults.sol b/src/presets/BaseRouterWithDefaults.sol index 3c3818e..7af3b2b 100644 --- a/src/presets/BaseRouterWithDefaults.sol +++ b/src/presets/BaseRouterWithDefaults.sol @@ -1,18 +1,22 @@ // SPDX-License-Identifier: MIT -// @author: thirdweb (https://github.com/thirdweb-dev/dynamic-contracts) - pragma solidity ^0.8.0; import "../core/Router.sol"; import "./ExtensionManager.sol"; import "./DefaultExtensionSet.sol"; +/// @title BaseRouterWithDefaults +/// @author thirdweb (https://github.com/thirdweb-dev/dynamic-contracts) +/// @notice A preset Router + ExtensionManager that can be initialized with a set of default extensions on deployment. + abstract contract BaseRouterWithDefaults is Router, ExtensionManager { using StringSet for StringSet.Set; + /// @notice The address where the router's default extension set is stored. address public immutable defaultExtensions; - + + /// @notice Initialize the Router with a set of default extensions. constructor(Extension[] memory _extensions) { defaultExtensions = address(new DefaultExtensionSet(_extensions)); } @@ -22,7 +26,10 @@ abstract contract BaseRouterWithDefaults is Router, ExtensionManager { //////////////////////////////////////////////////////////////*/ - /// @notice Returns all extensions of the Router. + /** + * @notice Returns all extensions of the Router. + * @return allExtensions An array of all extensions. + */ function getAllExtensions() external view override returns (Extension[] memory allExtensions) { Extension[] memory defaults = IRouterState(defaultExtensions).getAllExtensions(); @@ -55,7 +62,11 @@ abstract contract BaseRouterWithDefaults is Router, ExtensionManager { } } - /// @notice Returns the extension metadata for a given function. + /** + * @notice Returns the extension metadata for a given function. + * @param _functionSelector The function selector to get the extension metadata for. + * @return metadata The extension metadata for a given function. + */ function getMetadataForFunction(bytes4 _functionSelector) public view override returns (ExtensionMetadata memory) { ExtensionMetadata memory defaultMetadata = IRouterStateGetters(defaultExtensions).getMetadataForFunction(_functionSelector); ExtensionMetadata memory nonDefaultMetadata = _extensionManagerStorage().extensionMetadata[_functionSelector]; @@ -63,7 +74,11 @@ abstract contract BaseRouterWithDefaults is Router, ExtensionManager { return nonDefaultMetadata.implementation != address(0) ? nonDefaultMetadata : defaultMetadata; } - /// @notice Returns the extension metadata and functions for a given extension. + /** + * @notice Returns the extension metadata and functions for a given extension. + * @param extensionName The name of the extension to get the metadata and functions for. + * @return extension The extension metadata and functions for a given extension. + */ function getExtension(string memory extensionName) public view override returns (Extension memory) { Extension memory defaultExt = IRouterStateGetters(defaultExtensions).getExtension(extensionName); Extension memory nonDefaultExt = _extensionManagerStorage().extensions[extensionName]; @@ -102,7 +117,7 @@ abstract contract BaseRouterWithDefaults is Router, ExtensionManager { Overriden internal functions //////////////////////////////////////////////////////////////*/ - /// @dev Enables a function in an Extension. + /// @dev Enables a function in an Extension i.e. makes the function callable function _enableFunctionInExtension(string memory _extensionName, ExtensionFunction memory _extFunction) internal virtual override { // Ensure that the function is not already implemented as part of a default extension different from diff --git a/src/presets/DefaultExtensionSet.sol b/src/presets/DefaultExtensionSet.sol index dbe9ddc..9745aad 100644 --- a/src/presets/DefaultExtensionSet.sol +++ b/src/presets/DefaultExtensionSet.sol @@ -1,6 +1,4 @@ // SPDX-License-Identifier: MIT -// @author: thirdweb (https://github.com/thirdweb-dev/dynamic-contracts) - pragma solidity ^0.8.0; import "../interface/IRouterState.sol"; @@ -8,6 +6,10 @@ import "../interface/IRouterStateGetters.sol"; import "../lib/ExtensionManagerStorage.sol"; import "../lib/StringSet.sol"; +/// @title DefaultExtensionSet +/// @author thirdweb (https://github.com/thirdweb-dev/dynamic-contracts) +/// @notice A static router initialized with a set of extensions on deployment. Serves as a default extension set for Routers. + contract DefaultExtensionSet is IRouterState, IRouterStateGetters { using StringSet for StringSet.Set; @@ -16,6 +18,7 @@ contract DefaultExtensionSet is IRouterState, IRouterStateGetters { Constructor //////////////////////////////////////////////////////////////*/ + /// @notice Initializes the DefaultExtensionSet with a set of extensions. Serves as a default extension set for Routers. constructor(Extension[] memory _extensions) { uint256 len = _extensions.length; for (uint256 i = 0; i < len; i += 1) { @@ -27,7 +30,10 @@ contract DefaultExtensionSet is IRouterState, IRouterStateGetters { View functions //////////////////////////////////////////////////////////////*/ - /// @notice Returns all extensions of the Router. + /** + * @notice Returns all extensions of the Router. + * @return allExtensions An array of all extensions. + */ function getAllExtensions() external view override returns (Extension[] memory allExtensions) { string[] memory names = _extensionManagerStorage().extensionNames.values(); @@ -40,12 +46,20 @@ contract DefaultExtensionSet is IRouterState, IRouterStateGetters { } } - /// @dev Returns the extension metadata for a given function. + /** + * @notice Returns the extension metadata for a given function. + * @param functionSelector The function selector to get the extension metadata for. + * @return metadata The extension metadata for a given function. + */ function getMetadataForFunction(bytes4 functionSelector) public view returns (ExtensionMetadata memory) { return _extensionManagerStorage().extensionMetadata[functionSelector]; } - /// @dev Returns the extension metadata and functions for a given extension. + /** + * @notice Returns the extension metadata and functions for a given extension. + * @param extensionName The name of the extension to get the metadata and functions for. + * @return extension The extension metadata and functions for a given extension. + */ function getExtension(string memory extensionName) public view returns (Extension memory) { return _getExtension(extensionName); } diff --git a/src/presets/ExtensionManager.sol b/src/presets/ExtensionManager.sol index f9ba2a7..7cdac99 100644 --- a/src/presets/ExtensionManager.sol +++ b/src/presets/ExtensionManager.sol @@ -1,6 +1,4 @@ // SPDX-License-Identifier: MIT -// @author: thirdweb (https://github.com/thirdweb-dev/dynamic-contracts) - pragma solidity ^0.8.0; import "../interface/IExtensionManager.sol"; @@ -8,6 +6,10 @@ import "../interface/IRouterState.sol"; import "../interface/IRouterStateGetters.sol"; import "../lib/ExtensionManagerStorage.sol"; +/// @title ExtensionManager +/// @author thirdweb (https://github.com/thirdweb-dev/dynamic-contracts) +/// @notice Defined storage and API for managing a router's extensions. + abstract contract ExtensionManager is IExtensionManager, IRouterState, IRouterStateGetters { using StringSet for StringSet.Set; @@ -16,6 +18,7 @@ abstract contract ExtensionManager is IExtensionManager, IRouterState, IRouterSt Modifier //////////////////////////////////////////////////////////////*/ + /// @notice Checks that a call to any external function is authorized. modifier onlyAuthorizedCall() { require(isAuthorizedCallToUpgrade(), "ExtensionManager: unauthorized."); _; @@ -25,7 +28,10 @@ abstract contract ExtensionManager is IExtensionManager, IRouterState, IRouterSt View functions //////////////////////////////////////////////////////////////*/ - /// @notice Returns all extensions of the Router. + /** + * @notice Returns all extensions of the Router. + * @return allExtensions An array of all extensions. + */ function getAllExtensions() external view virtual override returns (Extension[] memory allExtensions) { string[] memory names = _extensionManagerStorage().extensionNames.values(); @@ -38,12 +44,20 @@ abstract contract ExtensionManager is IExtensionManager, IRouterState, IRouterSt } } - /// @dev Returns the extension metadata for a given function. + /** + * @notice Returns the extension metadata for a given function. + * @param functionSelector The function selector to get the extension metadata for. + * @return metadata The extension metadata for a given function. + */ function getMetadataForFunction(bytes4 functionSelector) public view virtual returns (ExtensionMetadata memory) { return _extensionManagerStorage().extensionMetadata[functionSelector]; } - /// @dev Returns the extension metadata and functions for a given extension. + /** + * @notice Returns the extension metadata and functions for a given extension. + * @param extensionName The name of the extension to get the metadata and functions for. + * @return extension The extension metadata and functions for a given extension. + */ function getExtension(string memory extensionName) public view virtual returns (Extension memory) { return _getExtension(extensionName); } @@ -54,6 +68,7 @@ abstract contract ExtensionManager is IExtensionManager, IRouterState, IRouterSt /** * @notice Add a new extension to the router. + * @param _extension The extension to add. */ function addExtension(Extension memory _extension) external onlyAuthorizedCall { // Check: extension namespace must not already exist. @@ -78,6 +93,8 @@ abstract contract ExtensionManager is IExtensionManager, IRouterState, IRouterSt /** * @notice Fully replace an existing extension of the router. + * @dev The extension with name `extension.name` is the extension being replaced. + * @param _extension The extension to replace or overwrite. */ function replaceExtension(Extension memory _extension) external onlyAuthorizedCall { // Check: extension namespace must already exist. @@ -102,6 +119,7 @@ abstract contract ExtensionManager is IExtensionManager, IRouterState, IRouterSt /** * @notice Remove an existing extension from the router. + * @param _extensionName The name of the extension to remove. */ function removeExtension(string memory _extensionName) external onlyAuthorizedCall { // Check: extension namespace must already exist. @@ -120,6 +138,10 @@ abstract contract ExtensionManager is IExtensionManager, IRouterState, IRouterSt /** * @notice Enables a single function in an existing extension. + * @dev Makes the given function callable on the router. + * + * @param _extensionName The name of the extension to which `extFunction` belongs. + * @param _function The function to enable. */ function enableFunctionInExtension(string memory _extensionName, ExtensionFunction memory _function) external onlyAuthorizedCall { // Check: extension namespace must already exist. @@ -132,11 +154,14 @@ abstract contract ExtensionManager is IExtensionManager, IRouterState, IRouterSt // 2. Store: metadata for function. _setMetadataForFunction(_function.functionSelector, metadata); - emit FunctionAdded(_extensionName, _function.functionSelector, _function, metadata); + emit FunctionEnabled(_extensionName, _function.functionSelector, _function, metadata); } /** * @notice Disables a single function in an Extension. + * + * @param _extensionName The name of the extension to which the function of `functionSelector` belongs. + * @param _functionSelector The function to disable. */ function disableFunctionInExtension(string memory _extensionName, bytes4 _functionSelector) external onlyAuthorizedCall { // Check: extension namespace must already exist. @@ -150,7 +175,7 @@ abstract contract ExtensionManager is IExtensionManager, IRouterState, IRouterSt // 2. Delete: metadata for function. _deleteMetadataForFunction(_functionSelector); - emit FunctionRemoved(_extensionName, _functionSelector, extMetadata); + emit FunctionDisabled(_extensionName, _functionSelector, extMetadata); } /*/////////////////////////////////////////////////////////////// diff --git a/test/utils/MockContracts.sol b/test/utils/MockContracts.sol index 9dddd2b..182c12b 100644 --- a/test/utils/MockContracts.sol +++ b/test/utils/MockContracts.sol @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + library NumberStorage { /// @custom:storage-location erc7201:number.storage