├── .github └── workflows │ └── test.yml ├── .gitignore ├── .gitmodules ├── README.md ├── foundry.toml ├── gasreport.txt ├── package.json ├── src ├── core │ ├── Router.sol │ └── RouterPayable.sol ├── example │ ├── RouterImmutable.sol │ ├── RouterRegistryConstrained.sol │ └── RouterUpgradeable.sol ├── interface │ ├── IExtension.sol │ ├── IExtensionManager.sol │ ├── IRouter.sol │ ├── IRouterPayable.sol │ ├── IRouterState.sol │ └── IRouterStateGetters.sol ├── lib │ ├── BaseRouterStorage.sol │ ├── ExtensionManagerStorage.sol │ └── StringSet.sol └── presets │ ├── BaseRouter.sol │ └── ExtensionManager.sol └── test ├── BaseRouter.t.sol ├── BaseRouterBenchmark.t.sol └── utils ├── MockContracts.sol └── Strings.sol /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: workflow_dispatch 4 | 5 | env: 6 | FOUNDRY_PROFILE: ci 7 | 8 | jobs: 9 | check: 10 | strategy: 11 | fail-fast: true 12 | 13 | name: Foundry project 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | submodules: recursive 19 | 20 | - name: Install Foundry 21 | uses: foundry-rs/foundry-toolchain@v1 22 | with: 23 | version: nightly 24 | 25 | - name: Run Forge build 26 | run: | 27 | forge --version 28 | forge build --sizes 29 | id: build 30 | 31 | - name: Run Forge tests 32 | run: | 33 | forge test -vvv 34 | id: test 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | cache/ 2 | out/ 3 | /node_modules 4 | yarn.lock 5 | .DS_Store -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std.git 4 | [submodule "lib/sstore2"] 5 | path = lib/sstore2 6 | url = https://github.com/0xsequence/sstore2 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ERC-7504: Dynamic Contracts standard. 2 | 3 | **Architectural pattern for writing client-friendly one-to-many proxy contracts (aka 'dynamic contracts') in Solidity.** 4 | 5 | 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: 6 | 7 | - Provide guardrails for writing dynamic contracts that can have functionality added, updated or removed over time. 8 | - Enables scaling up contracts by eliminating the restriction of contract size limit altogether. 9 | 10 | > ⚠️ **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). 11 | 12 | # Installation 13 | 14 | ### Forge projects: 15 | 16 | ```bash 17 | forge install https://github.com/thirdweb-dev/dynamic-contracts 18 | ``` 19 | 20 | ### Hardhat / JS based projects: 21 | 22 | ```bash 23 | npm install @thirdweb-dev/dynamic-contracts 24 | ``` 25 | 26 | ### Project structure 27 | 28 | ```shell 29 | src 30 | | 31 | |-- core 32 | | |- Router: "Minmal abstract contract implementation of EIP-7504 Router." 33 | | |- RouterPayable: "A Router with `receive` as a fixed function." 34 | | 35 | |-- presets 36 | | |-- ExtensionManager: "Defined storage layout and API for managing a router's extensions." 37 | | |-- DefaultExtensionSet: "A static store of a set of extensions, initialized on deployment." 38 | | |-- BaseRouter: "A Router with an ExtensionManager." 39 | | |-- BaseRouterWithDefaults: "A BaseRouter initialized with extensions on deployment." 40 | | 41 | |-- interface: "Interfaces for core and preset contracts." 42 | |-- example: "Example dynamic contracts built with presets." 43 | |-- lib: "Storage layouts and helper libraries." 44 | ``` 45 | 46 | # Running locally 47 | 48 | This repository is a forge project. ([forge handbook](https://book.getfoundry.sh/)) 49 | 50 | **Clone the repository:** 51 | 52 | ```bash 53 | git clone https://github.com/thirdweb-dev/dynamic-contracts.git 54 | ``` 55 | 56 | **Install dependencies:** 57 | 58 | ```bash 59 | forge install 60 | ``` 61 | 62 | **Compile contracts:** 63 | 64 | ```bash 65 | forge build 66 | ``` 67 | 68 | **Run tests:** 69 | 70 | ```bash 71 | forge test 72 | ``` 73 | 74 | **Generate documentation** 75 | 76 | ```bash 77 | forge doc --serve --port 4000 78 | ``` 79 | 80 | # Core concepts 81 | 82 | An “upgradeable smart contract” is actually two kinds of smart contracts considered together as one system: 83 | 84 | 1. **Proxy** smart contract: The smart contract whose state/storage we’re concerned with. 85 | 2. **Implementation** smart contract: A stateless smart contract that defines the logic for how the proxy smart contract’s state can be mutated. 86 | 87 | ![A proxy contract that forwards all calls to a single implementation contract](https://ipfs.io/ipfs/QmdzTiw5YuaMa1rjBtoyDuGHHRLdi9Afmh2Tu9Rjj1XuoA/proxy-with-single-impl.png) 88 | 89 | 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). 90 | 91 | ERC-7504 introduces a `Router` smart contract. 92 | 93 | ![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) 94 | 95 | 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. 96 | 97 | 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. 98 | 99 | ![Upgrading a contract means updating what implementation a given function, or functions are mapped to](https://ipfs.io/ipfs/QmUWk4VrFsAQ8gSMvTKwPXptJiMjZdihzUNhRXky7VmgGz/router-upgrades.png) 100 | 101 | # Getting started 102 | 103 | The simplest way to write a `Router` contract is to extend the preset [`BaseRouter`](/src/presets/BaseRouter.sol) available in this repository. 104 | 105 | ```solidity 106 | import "lib/dynamic-contracts/src/presets/BaseRouter.sol"; 107 | ``` 108 | 109 | 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. 110 | 111 | ```solidity 112 | // SPDX-License-Identifier: MIT 113 | pragma solidity ^0.8.0; 114 | 115 | import "@thirdweb-dev/dynamic-contracts/src/presets/BaseRouter.sol"; 116 | 117 | /// Example usage of `BaseRouter`, for demonstration only 118 | 119 | contract SimpleRouter is BaseRouter { 120 | 121 | address public deployer; 122 | 123 | constructor() { 124 | deployer = msg.sender; 125 | } 126 | 127 | /// @dev Returns whether all relevant permission checks are met before any upgrade. 128 | function _isAuthorizedCallToUpgrade() internal view virtual override returns (bool) { 129 | return msg.sender == deployer; 130 | } 131 | } 132 | ``` 133 | 134 | ## Choosing a permission model 135 | 136 | 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: 137 | 138 | - [**RouterImmutable**](https://github.com/thirdweb-dev/dynamic-contracts/blob/main/src/example/RouterImmutable.sol) 139 | 140 | 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. 141 | 142 | - [**RouterUpgradeable**](https://github.com/thirdweb-dev/dynamic-contracts/blob/main/src/example/RouterUpgradeable.sol) 143 | 144 | 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. 145 | 146 | - [**RouterRegistryContrained**](https://github.com/thirdweb-dev/dynamic-contracts/blob/main/src/example/RouterRegistryConstrained.sol) 147 | 148 | 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. 149 | 150 | ## Writing extension smart contracts 151 | 152 | 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). 153 | 154 | **Example:** `ExtensionManagerStorage` defines the storage layout for the `ExtensionManager` contract. 155 | 156 | ```solidity 157 | // SPDX-License-Identifier: MIT 158 | // @author: thirdweb (https://github.com/thirdweb-dev/dynamic-contracts) 159 | 160 | pragma solidity ^0.8.0; 161 | 162 | import "./StringSet.sol"; 163 | import "../interface/IExtension.sol"; 164 | 165 | library ExtensionManagerStorage { 166 | 167 | /// @custom:storage-location erc7201:extension.manager.storage 168 | bytes32 public constant EXTENSION_MANAGER_STORAGE_POSITION = keccak256(abi.encode(uint256(keccak256("extension.manager.storage")) - 1)); 169 | 170 | struct Data { 171 | /// @dev Set of names of all extensions stored. 172 | StringSet.Set extensionNames; 173 | /// @dev Mapping from extension name => `Extension` i.e. extension metadata and functions. 174 | mapping(string => IExtension.Extension) extensions; 175 | /// @dev Mapping from function selector => metadata of the extension the function belongs to. 176 | mapping(bytes4 => IExtension.ExtensionMetadata) extensionMetadata; 177 | } 178 | 179 | function data() internal pure returns (Data storage data_) { 180 | bytes32 position = EXTENSION_MANAGER_STORAGE_POSITION; 181 | assembly { 182 | data_.slot := position 183 | } 184 | } 185 | } 186 | ``` 187 | 188 | 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. 189 | 190 | ## Extensions: logical grouping of functionality 191 | 192 | By itself, the core `Router` contract does not specify _how to store or fetch_ appropriate implementation addresses for incoming function calls. 193 | 194 | 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). 195 | 196 | 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_**. 197 | 198 | `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. 199 | 200 | ![Upgrading a contract means updating what implementation a given function, or functions are mapped to](https://ipfs.io/ipfs/QmUWk4VrFsAQ8gSMvTKwPXptJiMjZdihzUNhRXky7VmgGz/router-upgrades.png) 201 | 202 | ## Deploying a Router 203 | 204 | Deploying a contract in the router pattern looks a little different from deploying a regular contract. 205 | 206 | 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. 207 | 208 | 2. Deploy your `Router` contract that implements `BaseRouter`. 209 | 3. Add extensions to your router via the API available in `BaseRouter`. (Alternatively, you can use `BaseRouterDefaults` which can be initialized with a set of extensions on deployment.) 210 | 211 | ### `Extensions` - Grouping logical functionality together 212 | 213 | By itself, the core `Router` contract does not specify _how to store or fetch_ appropriate implementation addresses for incoming function calls. 214 | 215 | 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). 216 | 217 | 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_**. 218 | 219 | `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. 220 | 221 | ## Extension to Extension communication 222 | 223 | When splitting logic between multiple extensions in a `Router`, one might want to access data from one `Extension` to another. 224 | 225 | 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. 226 | 227 | Here's an example of accessing a `IPermission` extension from another one: 228 | 229 | ```solidity 230 | modifier onlyAdmin(address _asset) { 231 | /// we access our IPermission extension by casting our own address 232 | IPermissions(address(this)).hasAdminRole(msg.sender); 233 | } 234 | ``` 235 | 236 | Note that if we don't have an `IPermission` extension added to our `Router`, this method will revert. 237 | 238 | ## Upgrading Extensions 239 | 240 | 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. 241 | 242 | A good rule of thumb to follow is: 243 | 244 | - It is safe to append new fields to an existing data structure 245 | - It is _not_ safe to update the type or order of existing structs; deprecate and add new ones instead. 246 | 247 | Refer to [this article](https://mirror.xyz/horsefacts.eth/EPB4o-eyDl0N8gu0gEz1uw7BTITheaZUqIAOEK1m-jE) for more information. 248 | 249 | # API reference 250 | 251 | 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: 252 | 253 | ```bash 254 | forge doc --serve --port 4000 255 | ``` 256 | 257 | ## Router 258 | 259 | ```solidity 260 | import "@thirdweb-dev/dynamic-contracts/src/core/Router.sol"; 261 | ``` 262 | 263 | The `Router` smart contract implements the ERC-7504 [`Router` interface](https://github.com/thirdweb-dev/dynamic-contracts/blob/main/src/interface/IRouter.sol). 264 | 265 | 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)`. 266 | 267 | This is an abstract contract that expects you to override and implement the following functions: 268 | 269 | - `getImplementationForFunction` 270 | ```solidity 271 | function getImplementationForFunction(bytes4 _functionSelector) public view virtual returns (address implementation); 272 | ``` 273 | 274 | ### fallback 275 | 276 | delegateCalls the appropriate implementation address for the given incoming function call. 277 | 278 | _The implementation address to delegateCall MUST be retrieved from calling `getImplementationForFunction` with the 279 | incoming call's function selector._ 280 | 281 | ```solidity 282 | fallback() external payable virtual; 283 | ``` 284 | 285 | #### Revert conditions: 286 | 287 | - `getImplementationForFunction(msg.sig) == address(0)` 288 | 289 | ### \_delegate 290 | 291 | _delegateCalls an `implementation` smart contract._ 292 | 293 | ```solidity 294 | function _delegate(address implementation) internal virtual; 295 | ``` 296 | 297 | ### getImplementationForFunction 298 | 299 | Returns the implementation address to delegateCall for the given function selector. 300 | 301 | ```solidity 302 | function getImplementationForFunction(bytes4 _functionSelector) public view virtual returns (address implementation); 303 | ``` 304 | 305 | **Parameters** 306 | 307 | | Name | Type | Description | 308 | | ------------------- | -------- | ------------------------------------------------------------ | 309 | | `_functionSelector` | `bytes4` | The function selector to get the implementation address for. | 310 | 311 | **Returns** 312 | 313 | | Name | Type | Description | 314 | | ---------------- | --------- | --------------------------------------------------------------------------- | 315 | | `implementation` | `address` | The implementation address to delegateCall for the given function selector. | 316 | 317 | ## ExtensionManager 318 | 319 | ```solidity 320 | import "@thirdweb-dev/dynamic-contracts/src/presets/ExtensionManager.sol"; 321 | ``` 322 | 323 | 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). 324 | 325 | The contract's storage layout is defined in `src/lib/ExtensionManagerStorage`: 326 | 327 | ```solidity 328 | struct Data { 329 | StringSet.Set extensionNames; 330 | mapping(string => IExtension.Extension) extensions; 331 | mapping(bytes4 => IExtension.ExtensionMetadata) extensionMetadata; 332 | } 333 | ``` 334 | 335 | The following are some helpful **invariant properties** of `ExtensionManager`: 336 | 337 | - Each extension has a non-empty, unique name which is stored in `extensionNames`. 338 | - Each extension's metadata specifies a _non_-zero-address implementation. 339 | - 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: 340 | 341 | - `extensionNames` contains `Ext.metadata.name` 342 | - `extensions[Ext.metadata.name].functions` includes `fn`. 343 | 344 | 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: 345 | 346 | ```solidity 347 | function getImplementationForFunction(bytes4 _functionSelector) public view virtual override returns (address) { 348 | return getMetadataForFunction(_functionSelector).implementation; 349 | } 350 | ``` 351 | 352 | This contract is an abstract contract that expects you to override and implement the following functions: 353 | 354 | - `isAuthorizedCallToUpgrade` 355 | ```solidity 356 | function _isAuthorizedCallToUpgrade() internal view virtual returns (bool); 357 | ``` 358 | 359 | ### onlyAuthorizedCall 360 | 361 | Checks that a call to any external function is authorized. 362 | 363 | ```solidity 364 | modifier onlyAuthorizedCall(); 365 | ``` 366 | 367 | #### Revert conditions: 368 | 369 | - `!_isAuthorizedCallToUpgrade()` 370 | 371 | ### getAllExtensions 372 | 373 | Returns all extensions of the Router. 374 | 375 | ```solidity 376 | function getAllExtensions() external view virtual override returns (Extension[] memory allExtensions); 377 | ``` 378 | 379 | **Returns** 380 | 381 | | Name | Type | Description | 382 | | --------------- | ------------- | --------------------------- | 383 | | `allExtensions` | `Extension[]` | An array of all extensions. | 384 | 385 | ### getMetadataForFunction 386 | 387 | Returns the extension metadata for a given function. 388 | 389 | ```solidity 390 | function getMetadataForFunction(bytes4 functionSelector) public view virtual returns (ExtensionMetadata memory); 391 | ``` 392 | 393 | **Parameters** 394 | 395 | | Name | Type | Description | 396 | | ------------------ | -------- | -------------------------------------------------------- | 397 | | `functionSelector` | `bytes4` | The function selector to get the extension metadata for. | 398 | 399 | **Returns** 400 | 401 | | Name | Type | Description | 402 | | -------- | ------------------- | ----------------------------------------------------- | 403 | | `` | `ExtensionMetadata` | metadata The extension metadata for a given function. | 404 | 405 | ### getExtension 406 | 407 | Returns the extension metadata and functions for a given extension. 408 | 409 | ```solidity 410 | function getExtension(string memory extensionName) public view virtual returns (Extension memory); 411 | ``` 412 | 413 | **Parameters** 414 | 415 | | Name | Type | Description | 416 | | --------------- | -------- | ---------------------------------------------------------------- | 417 | | `extensionName` | `string` | The name of the extension to get the metadata and functions for. | 418 | 419 | **Returns** 420 | 421 | | Name | Type | Description | 422 | | -------- | ----------- | ----------------------------------------------------------- | 423 | | `` | `Extension` | The extension metadata and functions for a given extension. | 424 | 425 | ### addExtension 426 | 427 | Add a new extension to the router. 428 | 429 | ```solidity 430 | function addExtension(Extension memory _extension) external onlyAuthorizedCall; 431 | ``` 432 | 433 | **Parameters** 434 | 435 | | Name | Type | Description | 436 | | ------------ | ----------- | --------------------- | 437 | | `_extension` | `Extension` | The extension to add. | 438 | 439 | #### Revert conditions: 440 | 441 | - Extension name is empty. 442 | - Extension name is already used. 443 | - Extension implementation is zero address. 444 | - Selector and signature mismatch for some function in the extension. 445 | - Some function in the extension is already a part of another extension. 446 | 447 | ### replaceExtension 448 | 449 | Fully replace an existing extension of the router. 450 | 451 | _The extension with name `extension.name` is the extension being replaced._ 452 | 453 | ```solidity 454 | function replaceExtension(Extension memory _extension) external onlyAuthorizedCall; 455 | ``` 456 | 457 | **Parameters** 458 | 459 | | Name | Type | Description | 460 | | ------------ | ----------- | -------------------------------------- | 461 | | `_extension` | `Extension` | The extension to replace or overwrite. | 462 | 463 | #### Revert conditions: 464 | 465 | - Extension being replaced does not exist. 466 | - Provided extension's implementation is zero address. 467 | - Selector and signature mismatch for some function in the provided extension. 468 | - Some function in the provided extension is already a part of another extension. 469 | 470 | ### removeExtension 471 | 472 | Remove an existing extension from the router. 473 | 474 | ```solidity 475 | function removeExtension(string memory _extensionName) external onlyAuthorizedCall; 476 | ``` 477 | 478 | **Parameters** 479 | 480 | | Name | Type | Description | 481 | | ---------------- | -------- | ------------------------------------ | 482 | | `_extensionName` | `string` | The name of the extension to remove. | 483 | 484 | #### Revert conditions: 485 | 486 | - Extension being removed does not exist. 487 | 488 | ### enableFunctionInExtension 489 | 490 | Enables a single function in an existing extension. 491 | 492 | _Makes the given function callable on the router._ 493 | 494 | ```solidity 495 | function enableFunctionInExtension(string memory _extensionName, ExtensionFunction memory _function) 496 | external 497 | onlyAuthorizedCall; 498 | ``` 499 | 500 | **Parameters** 501 | 502 | | Name | Type | Description | 503 | | ---------------- | ------------------- | --------------------------------------------------------- | 504 | | `_extensionName` | `string` | The name of the extension to which `extFunction` belongs. | 505 | | `_function` | `ExtensionFunction` | The function to enable. | 506 | 507 | #### Revert conditions: 508 | 509 | - Provided extension does not exist. 510 | - Selector and signature mismatch for some function in the provided extension. 511 | - Provided function is already a part of another extension. 512 | 513 | ### disableFunctionInExtension 514 | 515 | Disables a single function in an Extension. 516 | 517 | ```solidity 518 | function disableFunctionInExtension(string memory _extensionName, bytes4 _functionSelector) 519 | external 520 | onlyAuthorizedCall; 521 | ``` 522 | 523 | **Parameters** 524 | 525 | | Name | Type | Description | 526 | | ------------------- | -------- | ------------------------------------------------------------------------------ | 527 | | `_extensionName` | `string` | The name of the extension to which the function of `functionSelector` belongs. | 528 | | `_functionSelector` | `bytes4` | The function to disable. | 529 | 530 | #### Revert conditions: 531 | 532 | - Provided extension does not exist. 533 | - Provided function is not part of provided extension. 534 | 535 | ### \_getExtension 536 | 537 | _Returns the Extension for a given name._ 538 | 539 | ```solidity 540 | function _getExtension(string memory _extensionName) internal view returns (Extension memory); 541 | ``` 542 | 543 | ### \_setMetadataForExtension 544 | 545 | _Sets the ExtensionMetadata for a given extension._ 546 | 547 | ```solidity 548 | function _setMetadataForExtension(string memory _extensionName, ExtensionMetadata memory _metadata) internal; 549 | ``` 550 | 551 | ### \_deleteMetadataForExtension 552 | 553 | _Deletes the ExtensionMetadata for a given extension._ 554 | 555 | ```solidity 556 | function _deleteMetadataForExtension(string memory _extensionName) internal; 557 | ``` 558 | 559 | ### \_setMetadataForFunction 560 | 561 | _Sets the ExtensionMetadata for a given function._ 562 | 563 | ```solidity 564 | function _setMetadataForFunction(bytes4 _functionSelector, ExtensionMetadata memory _metadata) internal; 565 | ``` 566 | 567 | ### \_deleteMetadataForFunction 568 | 569 | _Deletes the ExtensionMetadata for a given function._ 570 | 571 | ```solidity 572 | function _deleteMetadataForFunction(bytes4 _functionSelector) internal; 573 | ``` 574 | 575 | ### \_enableFunctionInExtension 576 | 577 | _Enables a function in an Extension._ 578 | 579 | ```solidity 580 | function _enableFunctionInExtension(string memory _extensionName, ExtensionFunction memory _extFunction) 581 | internal 582 | virtual; 583 | ``` 584 | 585 | ### \_disableFunctionInExtension 586 | 587 | Note: `bytes4(0)` is the function selector for the `receive` function. 588 | So, we maintain a special fn selector-signature mismatch check for the `receive` function. 589 | 590 | _Disables a given function in an Extension._ 591 | 592 | ```solidity 593 | function _disableFunctionInExtension(string memory _extensionName, bytes4 _functionSelector) internal; 594 | ``` 595 | 596 | ### \_removeAllFunctionsFromExtension 597 | 598 | _Removes all functions from an Extension._ 599 | 600 | ```solidity 601 | function _removeAllFunctionsFromExtension(string memory _extensionName) internal; 602 | ``` 603 | 604 | ### \_canAddExtension 605 | 606 | _Returns whether a new extension can be added in the given execution context._ 607 | 608 | ```solidity 609 | function _canAddExtension(Extension memory _extension) internal virtual returns (bool); 610 | ``` 611 | 612 | ### \_canReplaceExtension 613 | 614 | _Returns whether an extension can be replaced in the given execution context._ 615 | 616 | ```solidity 617 | function _canReplaceExtension(Extension memory _extension) internal virtual returns (bool); 618 | ``` 619 | 620 | ### \_canRemoveExtension 621 | 622 | _Returns whether an extension can be removed in the given execution context._ 623 | 624 | ```solidity 625 | function _canRemoveExtension(string memory _extensionName) internal virtual returns (bool); 626 | ``` 627 | 628 | ### \_canEnableFunctionInExtension 629 | 630 | _Returns whether a function can be enabled in an extension in the given execution context._ 631 | 632 | ```solidity 633 | function _canEnableFunctionInExtension(string memory _extensionName, ExtensionFunction memory) 634 | internal 635 | view 636 | virtual 637 | returns (bool); 638 | ``` 639 | 640 | ### \_canDisableFunctionInExtension 641 | 642 | _Returns whether a function can be disabled in an extension in the given execution context._ 643 | 644 | ```solidity 645 | function _canDisableFunctionInExtension(string memory _extensionName, bytes4 _functionSelector) 646 | internal 647 | view 648 | virtual 649 | returns (bool); 650 | ``` 651 | 652 | ### \_extensionManagerStorage 653 | 654 | _Returns the ExtensionManager storage._ 655 | 656 | ```solidity 657 | function _extensionManagerStorage() internal pure returns (ExtensionManagerStorage.Data storage data); 658 | ``` 659 | 660 | ### isAuthorizedCallToUpgrade 661 | 662 | _To override; returns whether all relevant permission and other checks are met before any upgrade._ 663 | 664 | ```solidity 665 | function _isAuthorizedCallToUpgrade() internal view virtual returns (bool); 666 | ``` 667 | 668 | ## BaseRouter 669 | 670 | ```solidity 671 | import "@thirdweb-dev/dynamic-contracts/src/presets/BaseRouter" 672 | ``` 673 | 674 | `BaseRouter` inherits `Router` and `ExtensionManager`. It overrides the `Router.getImplementationForFunction` function to use the extensions stored in the `ExtensionManager` contract's storage system. 675 | 676 | This contract is an abstract contract that expects you to override and implement the following functions: 677 | 678 | - `isAuthorizedCallToUpgrade` 679 | ```solidity 680 | function _isAuthorizedCallToUpgrade() internal view virtual returns (bool); 681 | ``` 682 | 683 | ### getImplementationForFunction 684 | 685 | Returns the implementation address to delegateCall for the given function selector. 686 | 687 | ```solidity 688 | function getImplementationForFunction(bytes4 _functionSelector) public view virtual override returns (address); 689 | ``` 690 | 691 | **Parameters** 692 | 693 | | Name | Type | Description | 694 | | ------------------- | -------- | ------------------------------------------------------------ | 695 | | `_functionSelector` | `bytes4` | The function selector to get the implementation address for. | 696 | 697 | **Returns** 698 | 699 | | Name | Type | Description | 700 | | -------- | --------- | ------------------------------------------------------------------------------------------ | 701 | | `` | `address` | implementation The implementation address to delegateCall for the given function selector. | 702 | 703 | # Feedback 704 | 705 | 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). 706 | 707 | 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). 708 | 709 | # Authors 710 | 711 | - [thirdweb](https://github.com/thirdweb-dev) 712 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | gas_reports = [ 3 | "BaseRouterBenchmark" 4 | ] 5 | libs = ['lib'] 6 | out = 'out' 7 | remappings = [] 8 | src = 'src' 9 | test= 'test' 10 | 11 | # See more config options https://github.com/foundry-rs/foundry/tree/master/config 12 | -------------------------------------------------------------------------------- /gasreport.txt: -------------------------------------------------------------------------------- 1 | No files changed, compilation skipped 2 | 3 | Running 15 tests for test/BaseRouterBenchmark.t.sol:BaseRouterBenchmarkTest 4 | [PASS] test_benchmark_addExtension() (gas: 510831) 5 | [PASS] test_benchmark_deployBaseRouter() (gas: 3352794) 6 | [PASS] test_benchmark_deployBaseRouter_multipleExtensions() (gas: 7274980) 7 | [PASS] test_benchmark_disableFunctionInExtension() (gas: 14256) 8 | [PASS] test_benchmark_disableFunctionInExtension_defaultExtension() (gas: 52944) 9 | [PASS] test_benchmark_enableFunctionInExtension() (gas: 129822) 10 | [PASS] test_benchmark_enableFunctionInExtension_defaultExtension() (gas: 18822) 11 | [PASS] test_benchmark_initializeBaseRouter_multipleExtensions() (gas: 17016974) 12 | [PASS] test_benchmark_initializeBaseRouter_singleExtension() (gas: 4696233) 13 | [PASS] test_benchmark_removeExtension() (gas: 297441) 14 | [PASS] test_benchmark_removeExtension_defautlExtension() (gas: 98128) 15 | [PASS] test_benchmark_replaceExtension() (gas: 297424) 16 | [PASS] test_benchmark_replaceExtension_defaultExtension() (gas: 266335) 17 | [PASS] test_benchmark_upgradeBuggyFunction() (gas: 394149) 18 | [PASS] test_benchmark_upgradeBuggyFunction_defaultExtension() (gas: 231368) 19 | Test result: ok. 15 passed; 0 failed; 0 skipped; finished in 5.04ms 20 | 21 | 22 | Ran 1 test suites: 15 tests passed, 0 failed, 0 skipped (15 total tests) 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@thirdweb-dev/dynamic-contracts", 3 | "description": "Architectural pattern for writing dynamic smart contracts in Solidity", 4 | "version": "1.2.5", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/thirdweb-dev/dynamic-contracts.git" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/thirdweb-dev/dynamic-contracts/issues" 12 | }, 13 | "author": "thirdweb engineering ", 14 | "homepage": "https://thirdweb.com", 15 | "license": "MIT", 16 | "publishConfig": { 17 | "access": "public" 18 | }, 19 | "files": [ 20 | "**/*.sol" 21 | ], 22 | "dependencies": {}, 23 | "scripts": { 24 | "gas": "forge test --mc Benchmark --gas-report > gasreport.txt" 25 | }, 26 | "engines": { 27 | "node": ">=18.0.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/core/Router.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "../interface/IRouter.sol"; 5 | 6 | /// @title ERC-7504 Dynamic Contracts: Router. 7 | /// @author thirdweb (https://github.com/thirdweb-dev/dynamic-contracts) 8 | /// @notice Routes an incoming call to an appropriate implementation address. 9 | 10 | abstract contract Router is IRouter { 11 | 12 | /** 13 | * @notice delegateCalls the appropriate implementation address for the given incoming function call. 14 | * @dev The implementation address to delegateCall MUST be retrieved from calling `getImplementationForFunction` with the 15 | * incoming call's function selector. 16 | */ 17 | fallback() external payable virtual { 18 | if(msg.data.length == 0) return; 19 | 20 | address implementation = getImplementationForFunction(msg.sig); 21 | require(implementation != address(0), "Router: function does not exist."); 22 | _delegate(implementation); 23 | } 24 | 25 | /// @dev delegateCalls an `implementation` smart contract. 26 | function _delegate(address implementation) internal virtual { 27 | assembly { 28 | // Copy msg.data. We take full control of memory in this inline assembly 29 | // block because it will not return to Solidity code. We overwrite the 30 | // Solidity scratch pad at memory position 0. 31 | calldatacopy(0, 0, calldatasize()) 32 | 33 | // Call the implementation. 34 | // out and outsize are 0 because we don't know the size yet. 35 | let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0) 36 | 37 | // Copy the returned data. 38 | returndatacopy(0, 0, returndatasize()) 39 | 40 | switch result 41 | // delegatecall returns 0 on error. 42 | case 0 { 43 | revert(0, returndatasize()) 44 | } 45 | default { 46 | return(0, returndatasize()) 47 | } 48 | } 49 | } 50 | 51 | /** 52 | * @notice Returns the implementation address to delegateCall for the given function selector. 53 | * @param _functionSelector The function selector to get the implementation address for. 54 | * @return implementation The implementation address to delegateCall for the given function selector. 55 | */ 56 | function getImplementationForFunction(bytes4 _functionSelector) public view virtual returns (address implementation); 57 | } -------------------------------------------------------------------------------- /src/core/RouterPayable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "./Router.sol"; 5 | 6 | /// @title IRouterPayable. 7 | /// @author thirdweb (https://github.com/thirdweb-dev/dynamic-contracts) 8 | /// @notice A Router with `receive` as a fixed function. 9 | 10 | abstract contract RouterPayable is Router { 11 | 12 | /// @notice Lets a contract receive native tokens. 13 | receive() external payable virtual {} 14 | } -------------------------------------------------------------------------------- /src/example/RouterImmutable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // @author: thirdweb (https://github.com/thirdweb-dev/dynamic-contracts) 3 | 4 | pragma solidity ^0.8.0; 5 | 6 | import "../presets/BaseRouter.sol"; 7 | 8 | /** 9 | * This smart contract is an EXAMPLE, and is not meant for use in production. 10 | */ 11 | 12 | abstract contract MyRouter is BaseRouter { 13 | 14 | constructor(Extension[] memory _extensions) BaseRouter(_extensions) { 15 | // Initialize the router with a set of default extensions. 16 | __BaseRouter_init(); 17 | } 18 | } 19 | 20 | contract RouterImmutable is MyRouter { 21 | 22 | constructor(Extension[] memory _extensions) MyRouter(_extensions) {} 23 | 24 | /*/////////////////////////////////////////////////////////////// 25 | Overrides 26 | //////////////////////////////////////////////////////////////*/ 27 | 28 | /// @dev Returns whether all relevant permission and other checks are met before any upgrade. 29 | function _isAuthorizedCallToUpgrade() internal view virtual override returns (bool) { 30 | return false; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/example/RouterRegistryConstrained.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // @author: thirdweb (https://github.com/thirdweb-dev/dynamic-contracts) 3 | 4 | pragma solidity ^0.8.0; 5 | 6 | import "../presets/BaseRouter.sol"; 7 | 8 | /** 9 | * This smart contract is an EXAMPLE, and is not meant for use in production. 10 | */ 11 | contract ExtensionRegistry { 12 | 13 | address public immutable admin; 14 | mapping (address => bool) public isRegistered; 15 | 16 | constructor() { 17 | admin = msg.sender; 18 | } 19 | 20 | function setExtensionRegistered(address _extension, bool _isRegistered) external { 21 | require(msg.sender == admin, "ExtensionRegistry: Only admin can alter extension registry"); 22 | isRegistered[_extension] = _isRegistered; 23 | } 24 | } 25 | 26 | /** 27 | * This smart contract is an EXAMPLE, and is not meant for use in production. 28 | */ 29 | contract RouterRegistryConstrained is BaseRouter { 30 | 31 | address public admin; 32 | ExtensionRegistry public registry; 33 | 34 | /// @dev Cannot initialize with extensions before registry is set, so we pass empty array to base constructor. 35 | constructor(address _registry) BaseRouter(new Extension[](0)) { 36 | admin = msg.sender; 37 | registry = ExtensionRegistry(_registry); 38 | } 39 | 40 | /// @dev Sets the admin address. 41 | function setAdmin(address _admin) external { 42 | require(msg.sender == admin, "RouterUpgradeable: Only admin can set a new admin"); 43 | admin = _admin; 44 | } 45 | 46 | /*/////////////////////////////////////////////////////////////// 47 | Overrides 48 | //////////////////////////////////////////////////////////////*/ 49 | 50 | /// @dev Returns whether all relevant permission and other checks are met before any upgrade. 51 | function _isAuthorizedCallToUpgrade() internal view virtual override returns (bool) { 52 | return msg.sender == admin; 53 | } 54 | 55 | /// @dev Returns whether a new extension can be added in the given execution context. 56 | function _canAddExtension(Extension memory _extension) internal virtual override returns (bool) { 57 | return super._canAddExtension(_extension) && registry.isRegistered(_extension.metadata.implementation); 58 | } 59 | 60 | /// @dev Returns whether an extension can be replaced in the given execution context. 61 | function _canReplaceExtension(Extension memory _extension) internal virtual override returns (bool) { 62 | return super._canReplaceExtension(_extension) && registry.isRegistered(_extension.metadata.implementation); 63 | } 64 | } -------------------------------------------------------------------------------- /src/example/RouterUpgradeable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // @author: thirdweb (https://github.com/thirdweb-dev/dynamic-contracts) 3 | 4 | pragma solidity ^0.8.0; 5 | 6 | import "../presets/BaseRouter.sol"; 7 | /** 8 | * This smart contract is an EXAMPLE, and is not meant for use in production. 9 | */ 10 | contract RouterUpgradeable is BaseRouter { 11 | 12 | address public admin; 13 | 14 | constructor() BaseRouter(new Extension[](0)) { 15 | admin = msg.sender; 16 | } 17 | 18 | // @dev Sets the admin address. 19 | function setAdmin(address _admin) external { 20 | require(msg.sender == admin, "RouterUpgradeable: Only admin can set a new admin"); 21 | admin = _admin; 22 | } 23 | 24 | /*/////////////////////////////////////////////////////////////// 25 | Overrides 26 | //////////////////////////////////////////////////////////////*/ 27 | 28 | /// @dev Returns whether a function can be disabled in an extension in the given execution context. 29 | function _isAuthorizedCallToUpgrade() internal view virtual override returns (bool) { 30 | return msg.sender == admin; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/interface/IExtension.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | 5 | /// @title IExtension 6 | /// @author thirdweb (https://github.com/thirdweb-dev/dynamic-contracts) 7 | /// @notice Provides an `Extension` abstraction for a router's implementation contracts. 8 | 9 | interface IExtension { 10 | /*/////////////////////////////////////////////////////////////// 11 | Structs 12 | //////////////////////////////////////////////////////////////*/ 13 | 14 | /** 15 | * @notice An interface to describe an extension's metadata. 16 | * 17 | * @param name The unique name of the extension. 18 | * @param metadataURI The URI where the metadata for the extension lives. 19 | * @param implementation The implementation smart contract address of the extension. 20 | */ 21 | struct ExtensionMetadata { 22 | string name; 23 | string metadataURI; 24 | address implementation; 25 | } 26 | 27 | /** 28 | * @notice An interface to describe an extension's function. 29 | * 30 | * @param functionSelector The 4 byte selector of the function. 31 | * @param functionSignature Function signature as a string. E.g. "transfer(address,address,uint256)" 32 | */ 33 | struct ExtensionFunction { 34 | bytes4 functionSelector; 35 | string functionSignature; 36 | } 37 | 38 | /** 39 | * @notice An interface to describe an extension. 40 | * 41 | * @param metadata The extension's metadata; it's name, metadata URI and implementation contract address. 42 | * @param functions The functions that belong to the extension. 43 | */ 44 | struct Extension { 45 | ExtensionMetadata metadata; 46 | ExtensionFunction[] functions; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/interface/IExtensionManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "./IExtension.sol"; 5 | 6 | /// @title IExtensionManager 7 | /// @author thirdweb (https://github.com/thirdweb-dev/dynamic-contracts) 8 | /// @notice Defined storage and API for managing a router's extensions. 9 | 10 | interface IExtensionManager is IExtension { 11 | 12 | /*/////////////////////////////////////////////////////////////// 13 | Events 14 | //////////////////////////////////////////////////////////////*/ 15 | 16 | /// @dev Emitted when a extension is added. 17 | event ExtensionAdded(string indexed name, address indexed implementation, Extension extension); 18 | 19 | /// @dev Emitted when a extension is replaced. 20 | event ExtensionReplaced(string indexed name, address indexed implementation, Extension extension); 21 | 22 | /// @dev Emitted when a extension is removed. 23 | event ExtensionRemoved(string indexed name, Extension extension); 24 | 25 | /// @dev Emitted when a function is enabled i.e. made callable. 26 | event FunctionEnabled(string indexed name, bytes4 indexed functionSelector, ExtensionFunction extFunction, ExtensionMetadata extMetadata); 27 | 28 | /// @dev Emitted when a function is disabled i.e. made un-callable. 29 | event FunctionDisabled(string indexed name, bytes4 indexed functionSelector, ExtensionMetadata extMetadata); 30 | 31 | /*/////////////////////////////////////////////////////////////// 32 | External functions 33 | //////////////////////////////////////////////////////////////*/ 34 | 35 | /** 36 | * @notice Add a new extension to the router. 37 | * @param extension The extension to add. 38 | */ 39 | function addExtension(Extension memory extension) external; 40 | 41 | /** 42 | * @notice Fully replace an existing extension of the router. 43 | * @dev The extension with name `extension.name` is the extension being replaced. 44 | * @param extension The extension to replace or overwrite. 45 | */ 46 | function replaceExtension(Extension memory extension) external; 47 | 48 | /** 49 | * @notice Remove an existing extension from the router. 50 | * @param extensionName The name of the extension to remove. 51 | */ 52 | function removeExtension(string memory extensionName) external; 53 | 54 | /** 55 | * @notice Enables a single function in an existing extension. 56 | * @dev Makes the given function callable on the router. 57 | * 58 | * @param extensionName The name of the extension to which `extFunction` belongs. 59 | * @param extFunction The function to enable. 60 | */ 61 | function enableFunctionInExtension(string memory extensionName, ExtensionFunction memory extFunction) external; 62 | 63 | /** 64 | * @notice Disables a single function in an Extension. 65 | * 66 | * @param extensionName The name of the extension to which the function of `functionSelector` belongs. 67 | * @param functionSelector The function to disable. 68 | */ 69 | function disableFunctionInExtension(string memory extensionName, bytes4 functionSelector) external; 70 | } -------------------------------------------------------------------------------- /src/interface/IRouter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /// @title ERC-7504 Dynamic Contracts: IRouter. 5 | /// @author thirdweb (https://github.com/thirdweb-dev/dynamic-contracts) 6 | /// @notice Routes an incoming call to an appropriate implementation address. 7 | /// @dev Fallback function delegateCalls `getImplementationForFunction(msg.sig)` for a given incoming call. 8 | /// NOTE: The ERC-165 identifier for this interface is 0xce0b6013. 9 | 10 | interface IRouter { 11 | 12 | /** 13 | * @notice delegateCalls the appropriate implementation address for the given incoming function call. 14 | * @dev The implementation address to delegateCall MUST be retrieved from calling `getImplementationForFunction` with the 15 | * incoming call's function selector. 16 | */ 17 | fallback() external payable; 18 | 19 | /*/////////////////////////////////////////////////////////////// 20 | View Functions 21 | //////////////////////////////////////////////////////////////*/ 22 | 23 | /** 24 | * @notice Returns the implementation address to delegateCall for the given function selector. 25 | * @param _functionSelector The function selector to get the implementation address for. 26 | * @return implementation The implementation address to delegateCall for the given function selector. 27 | */ 28 | function getImplementationForFunction(bytes4 _functionSelector) external view returns (address implementation); 29 | } -------------------------------------------------------------------------------- /src/interface/IRouterPayable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "./IRouter.sol"; 5 | 6 | /// @title IRouterPayable. 7 | /// @author thirdweb (https://github.com/thirdweb-dev/dynamic-contracts) 8 | /// @notice A Router with `receive` as a fixed function. 9 | 10 | interface IRouterPayable is IRouter { 11 | /// @notice Lets a contract receive native tokens. 12 | receive() external payable; 13 | } -------------------------------------------------------------------------------- /src/interface/IRouterState.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "./IExtension.sol"; 5 | 6 | /// @title ERC-7504 Dynamic Contracts: IRouterState. 7 | /// @author thirdweb (https://github.com/thirdweb-dev/dynamic-contracts) 8 | /// @notice Defines an API to expose a router's extensions. 9 | 10 | interface IRouterState is IExtension { 11 | 12 | /*/////////////////////////////////////////////////////////////// 13 | View Functions 14 | //////////////////////////////////////////////////////////////*/ 15 | 16 | /** 17 | * @notice Returns all extensions of the Router. 18 | * @return allExtensions An array of all extensions. 19 | */ 20 | function getAllExtensions() external view returns (Extension[] memory allExtensions); 21 | } -------------------------------------------------------------------------------- /src/interface/IRouterStateGetters.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "./IExtension.sol"; 5 | 6 | /// @title IRouterStateGetters. 7 | /// @author thirdweb (https://github.com/thirdweb-dev/dynamic-contracts) 8 | /// @notice Helper view functions to inspect a router's state. 9 | 10 | interface IRouterStateGetters is IExtension { 11 | 12 | /*/////////////////////////////////////////////////////////////// 13 | View functions 14 | //////////////////////////////////////////////////////////////*/ 15 | 16 | /** 17 | * @notice Returns the extension metadata for a given function. 18 | * @param functionSelector The function selector to get the extension metadata for. 19 | * @return metadata The extension metadata for a given function. 20 | */ 21 | function getMetadataForFunction(bytes4 functionSelector) external view returns (ExtensionMetadata memory metadata); 22 | 23 | /** 24 | * @notice Returns the extension metadata and functions for a given extension. 25 | * @param extensionName The name of the extension to get the metadata and functions for. 26 | * @return extension The extension metadata and functions for a given extension. 27 | */ 28 | function getExtension(string memory extensionName) external view returns (Extension memory); 29 | } -------------------------------------------------------------------------------- /src/lib/BaseRouterStorage.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /// @title BaseRouterStorage 5 | /// @author thirdweb (https://github.com/thirdweb-dev/dynamic-contracts) 6 | /// @notice Defined storage for base router 7 | 8 | library BaseRouterStorage { 9 | 10 | /// @custom:storage-location erc7201:base.router.storage 11 | bytes32 public constant BASE_ROUTER_STORAGE_POSITION = keccak256(abi.encode(uint256(keccak256("base.router.storage")) - 1)); 12 | 13 | struct Data { 14 | /// @dev Mapping used only for checking default extension validity in constructor. 15 | mapping(bytes4 => bool) functionMap; 16 | /// @dev Mapping used only for checking default extension validity in constructor. 17 | mapping(string => bool) extensionMap; 18 | } 19 | 20 | /// @dev Returns access to base router storage. 21 | function data() internal pure returns (Data storage data_) { 22 | bytes32 position = BASE_ROUTER_STORAGE_POSITION; 23 | assembly { 24 | data_.slot := position 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/lib/ExtensionManagerStorage.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "./StringSet.sol"; 5 | import "../interface/IExtension.sol"; 6 | 7 | /// @title IExtensionManagerStorage 8 | /// @author thirdweb (https://github.com/thirdweb-dev/dynamic-contracts) 9 | /// @notice Defined storage for managing a router's extensions. 10 | 11 | library ExtensionManagerStorage { 12 | 13 | /// @custom:storage-location erc7201:extension.manager.storage 14 | bytes32 public constant EXTENSION_MANAGER_STORAGE_POSITION = keccak256(abi.encode(uint256(keccak256("extension.manager.storage")) - 1)); 15 | 16 | struct Data { 17 | /// @dev Set of names of all extensions of the router. 18 | StringSet.Set extensionNames; 19 | /// @dev Mapping from extension name => `Extension` i.e. extension metadata and functions. 20 | mapping(string => IExtension.Extension) extensions; 21 | /// @dev Mapping from function selector => metadata of the extension the function belongs to. 22 | mapping(bytes4 => IExtension.ExtensionMetadata) extensionMetadata; 23 | } 24 | 25 | /// @dev Returns access to the extension manager's storage. 26 | function data() internal pure returns (Data storage data_) { 27 | bytes32 position = EXTENSION_MANAGER_STORAGE_POSITION; 28 | assembly { 29 | data_.slot := position 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/lib/StringSet.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | library StringSet { 5 | struct Set { 6 | // Storage of set values 7 | string[] _values; 8 | // Position of the value in the `values` array, plus 1 because index 0 9 | // means a value is not in the set. 10 | mapping(string => uint256) _indexes; 11 | } 12 | 13 | /** 14 | * @dev Add a value to a set. O(1). 15 | * 16 | * Returns true if the value was added to the set, that is if it was not 17 | * already present. 18 | */ 19 | function _add(Set storage set, string memory value) private returns (bool) { 20 | if (!_contains(set, value)) { 21 | set._values.push(value); 22 | // The value is stored at length-1, but we add 1 to all indexes 23 | // and use 0 as a sentinel value 24 | set._indexes[value] = set._values.length; 25 | return true; 26 | } else { 27 | return false; 28 | } 29 | } 30 | 31 | /** 32 | * @dev Removes a value from a set. O(1). 33 | * 34 | * Returns true if the value was removed from the set, that is if it was 35 | * present. 36 | */ 37 | function _remove(Set storage set, string memory value) private returns (bool) { 38 | // We read and store the value's index to prevent multiple reads from the same storage slot 39 | uint256 valueIndex = set._indexes[value]; 40 | 41 | if (valueIndex != 0) { 42 | // Equivalent to contains(set, value) 43 | // To delete an element from the _values array in O(1), we swap the element to delete with the last one in 44 | // the array, and then remove the last element (sometimes called as 'swap and pop'). 45 | // This modifies the order of the array, as noted in {at}. 46 | 47 | uint256 toDeleteIndex = valueIndex - 1; 48 | uint256 lastIndex = set._values.length - 1; 49 | 50 | if (lastIndex != toDeleteIndex) { 51 | string memory lastValue = set._values[lastIndex]; 52 | 53 | // Move the last value to the index where the value to delete is 54 | set._values[toDeleteIndex] = lastValue; 55 | // Update the index for the moved value 56 | set._indexes[lastValue] = valueIndex; // Replace lastValue's index to valueIndex 57 | } 58 | 59 | // Delete the slot where the moved value was stored 60 | set._values.pop(); 61 | 62 | // Delete the index for the deleted slot 63 | delete set._indexes[value]; 64 | 65 | return true; 66 | } else { 67 | return false; 68 | } 69 | } 70 | 71 | /** 72 | * @dev Returns true if the value is in the set. O(1). 73 | */ 74 | function _contains(Set storage set, string memory value) private view returns (bool) { 75 | return set._indexes[value] != 0; 76 | } 77 | 78 | /** 79 | * @dev Returns the number of values on the set. O(1). 80 | */ 81 | function _length(Set storage set) private view returns (uint256) { 82 | return set._values.length; 83 | } 84 | 85 | /** 86 | * @dev Returns the value stored at position `index` in the set. O(1). 87 | * 88 | * Note that there are no guarantees on the ordering of values inside the 89 | * array, and it may change when more values are added or removed. 90 | * 91 | * Requirements: 92 | * 93 | * - `index` must be strictly less than {length}. 94 | */ 95 | function _at(Set storage set, uint256 index) private view returns (string memory) { 96 | return set._values[index]; 97 | } 98 | 99 | /** 100 | * @dev Return the entire set in an array 101 | * 102 | * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed 103 | * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that 104 | * this function has an unbounded cost, and using it as part of a state-changing function may render the function 105 | * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. 106 | */ 107 | function _values(Set storage set) private view returns (string[] memory) { 108 | return set._values; 109 | } 110 | 111 | /** 112 | * @dev Add a value to a set. O(1). 113 | * 114 | * Returns true if the value was added to the set, that is if it was not 115 | * already present. 116 | */ 117 | function add(Set storage set, string memory value) internal returns (bool) { 118 | return _add(set, value); 119 | } 120 | 121 | /** 122 | * @dev Removes a value from a set. O(1). 123 | * 124 | * Returns true if the value was removed from the set, that is if it was 125 | * present. 126 | */ 127 | function remove(Set storage set, string memory value) internal returns (bool) { 128 | return _remove(set, value); 129 | } 130 | 131 | /** 132 | * @dev Returns true if the value is in the set. O(1). 133 | */ 134 | function contains(Set storage set, string memory value) internal view returns (bool) { 135 | return _contains(set, value); 136 | } 137 | 138 | /** 139 | * @dev Returns the number of values in the set. O(1). 140 | */ 141 | function length(Set storage set) internal view returns (uint256) { 142 | return _length(set); 143 | } 144 | 145 | /** 146 | * @dev Returns the value stored at position `index` in the set. O(1). 147 | * 148 | * Note that there are no guarantees on the ordering of values inside the 149 | * array, and it may change when more values are added or removed. 150 | * 151 | * Requirements: 152 | * 153 | * - `index` must be strictly less than {length}. 154 | */ 155 | function at(Set storage set, uint256 index) internal view returns (string memory) { 156 | return _at(set, index); 157 | } 158 | 159 | /** 160 | * @dev Return the entire set in an array 161 | * 162 | * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed 163 | * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that 164 | * this function has an unbounded cost, and using it as part of a state-changing function may render the function 165 | * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. 166 | */ 167 | function values(Set storage set) internal view returns (string[] memory) { 168 | return _values(set); 169 | } 170 | } -------------------------------------------------------------------------------- /src/presets/BaseRouter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import { Router, IRouter } from "../core/Router.sol"; 5 | import { IRouterState } from "../interface/IRouterState.sol"; 6 | import { IRouterStateGetters } from "../interface/IRouterStateGetters.sol"; 7 | import { BaseRouterStorage } from "../lib/BaseRouterStorage.sol"; 8 | import { ExtensionManager } from "./ExtensionManager.sol"; 9 | import { StringSet } from "../lib/StringSet.sol"; 10 | import "lib/sstore2/contracts/SSTORE2.sol"; 11 | 12 | /// @title BaseRouter 13 | /// @author thirdweb (https://github.com/thirdweb-dev/dynamic-contracts) 14 | /// @notice A router with an API to manage its extensions. 15 | 16 | abstract contract BaseRouter is Router, ExtensionManager { 17 | 18 | using StringSet for StringSet.Set; 19 | 20 | /// @notice The address where the router's default extension set is stored. 21 | address public immutable defaultExtensions; 22 | 23 | /// @notice Initialize the Router with a set of default extensions. 24 | constructor(Extension[] memory _extensions) { 25 | address pointer; 26 | if(_extensions.length > 0) { 27 | _validateExtensions(_extensions); 28 | pointer = SSTORE2.write(abi.encode(_extensions)); 29 | } 30 | 31 | defaultExtensions = pointer; 32 | } 33 | 34 | /// @notice Initialize the Router with a set of default extensions. 35 | function __BaseRouter_init() internal { 36 | if(defaultExtensions == address(0)) { 37 | return; 38 | } 39 | 40 | bytes memory data = SSTORE2.read(defaultExtensions); 41 | Extension[] memory defaults = abi.decode(data, (Extension[])); 42 | 43 | // Unchecked since we already validated extensions in constructor. 44 | __BaseRouter_init_unchecked(defaults); 45 | } 46 | 47 | /// @notice Initializes the Router with a set of extensions. 48 | function __BaseRouter_init_checked(Extension[] memory _extensions) internal { 49 | _validateExtensions(_extensions); 50 | __BaseRouter_init_unchecked(_extensions); 51 | } 52 | 53 | /// @notice Initializes the Router with a set of extensions. 54 | function __BaseRouter_init_unchecked(Extension[] memory _extensions) internal { 55 | for(uint256 i = 0; i < _extensions.length; i += 1) { 56 | 57 | Extension memory extension = _extensions[i]; 58 | // Store: new extension name. 59 | _extensionManagerStorage().extensionNames.add(extension.metadata.name); 60 | 61 | // 1. Store: metadata for extension. 62 | _setMetadataForExtension(extension.metadata.name, extension.metadata); 63 | 64 | uint256 len = extension.functions.length; 65 | for (uint256 j = 0; j < len; j += 1) { 66 | // 2. Store: name -> extension.functions map 67 | _extensionManagerStorage().extensions[extension.metadata.name].functions.push(extension.functions[j]); 68 | // 3. Store: metadata for function. 69 | _setMetadataForFunction(extension.functions[j].functionSelector, extension.metadata); 70 | } 71 | 72 | emit ExtensionAdded(extension.metadata.name, extension.metadata.implementation, extension); 73 | } 74 | } 75 | 76 | /// @notice Returns the implementation contract address for a given function signature. 77 | function getImplementationForFunction(bytes4 _functionSelector) public view virtual override returns (address) { 78 | return getMetadataForFunction(_functionSelector).implementation; 79 | } 80 | 81 | /// @dev Validates default extensions. 82 | function _validateExtensions(Extension[] memory _extensions) internal { 83 | uint256 len = _extensions.length; 84 | 85 | bool isValid = true; 86 | 87 | for (uint256 i = 0; i < len; i += 1) { 88 | isValid = _isValidExtension(_extensions[i]); 89 | if(!isValid) { 90 | break; 91 | } 92 | } 93 | require(isValid, "BaseRouter: invalid extension."); 94 | } 95 | 96 | function _isValidExtension(Extension memory _extension) internal returns (bool isValid) { 97 | isValid = bytes(_extension.metadata.name).length > 0 // non-empty name 98 | && !BaseRouterStorage.data().extensionMap[_extension.metadata.name] // unused name 99 | && _extension.metadata.implementation != address(0); // non-empty implementation 100 | 101 | BaseRouterStorage.data().extensionMap[_extension.metadata.name] = true; 102 | 103 | if(!isValid) { 104 | return false; 105 | } 106 | 107 | uint256 len = _extension.functions.length; 108 | 109 | for(uint256 i = 0; i < len; i += 1) { 110 | 111 | if(!isValid) { 112 | break; 113 | } 114 | 115 | ExtensionFunction memory _extFunction = _extension.functions[i]; 116 | 117 | /** 118 | * Note: `bytes4(0)` is the function selector for the `receive` function. 119 | * So, we maintain a special fn selector-signature mismatch check for the `receive` function. 120 | **/ 121 | bool mismatch = false; 122 | if(_extFunction.functionSelector == bytes4(0)) { 123 | mismatch = keccak256(abi.encode(_extFunction.functionSignature)) != keccak256(abi.encode("receive()")); 124 | } else { 125 | mismatch = _extFunction.functionSelector != 126 | bytes4(keccak256(abi.encodePacked(_extFunction.functionSignature))); 127 | } 128 | 129 | // No fn signature-selector mismatch and no duplicate function. 130 | isValid = !mismatch && !BaseRouterStorage.data().functionMap[_extFunction.functionSelector]; 131 | 132 | BaseRouterStorage.data().functionMap[_extFunction.functionSelector] = true; 133 | } 134 | } 135 | } -------------------------------------------------------------------------------- /src/presets/ExtensionManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "../interface/IExtensionManager.sol"; 5 | import "../interface/IRouterState.sol"; 6 | import "../interface/IRouterStateGetters.sol"; 7 | import "../lib/ExtensionManagerStorage.sol"; 8 | 9 | /// @title ExtensionManager 10 | /// @author thirdweb (https://github.com/thirdweb-dev/dynamic-contracts) 11 | /// @notice Defined storage and API for managing a router's extensions. 12 | 13 | abstract contract ExtensionManager is IExtensionManager, IRouterState, IRouterStateGetters { 14 | 15 | using StringSet for StringSet.Set; 16 | 17 | /*/////////////////////////////////////////////////////////////// 18 | Modifier 19 | //////////////////////////////////////////////////////////////*/ 20 | 21 | /// @notice Checks that a call to any external function is authorized. 22 | modifier onlyAuthorizedCall() { 23 | require(_isAuthorizedCallToUpgrade(), "ExtensionManager: unauthorized."); 24 | _; 25 | } 26 | 27 | /*/////////////////////////////////////////////////////////////// 28 | View functions 29 | //////////////////////////////////////////////////////////////*/ 30 | 31 | /** 32 | * @notice Returns all extensions of the Router. 33 | * @return allExtensions An array of all extensions. 34 | */ 35 | function getAllExtensions() external view virtual override returns (Extension[] memory allExtensions) { 36 | 37 | string[] memory names = _extensionManagerStorage().extensionNames.values(); 38 | uint256 len = names.length; 39 | 40 | allExtensions = new Extension[](len); 41 | 42 | for (uint256 i = 0; i < len; i += 1) { 43 | allExtensions[i] = _getExtension(names[i]); 44 | } 45 | } 46 | 47 | /** 48 | * @notice Returns the extension metadata for a given function. 49 | * @param functionSelector The function selector to get the extension metadata for. 50 | * @return metadata The extension metadata for a given function. 51 | */ 52 | function getMetadataForFunction(bytes4 functionSelector) public view virtual returns (ExtensionMetadata memory) { 53 | return _extensionManagerStorage().extensionMetadata[functionSelector]; 54 | } 55 | 56 | /** 57 | * @notice Returns the extension metadata and functions for a given extension. 58 | * @param extensionName The name of the extension to get the metadata and functions for. 59 | * @return extension The extension metadata and functions for a given extension. 60 | */ 61 | function getExtension(string memory extensionName) public view virtual returns (Extension memory) { 62 | return _getExtension(extensionName); 63 | } 64 | 65 | /*/////////////////////////////////////////////////////////////// 66 | External functions 67 | //////////////////////////////////////////////////////////////*/ 68 | 69 | /** 70 | * @notice Add a new extension to the router. 71 | * @param _extension The extension to add. 72 | */ 73 | function addExtension(Extension memory _extension) public virtual onlyAuthorizedCall { 74 | _addExtension(_extension); 75 | } 76 | 77 | /** 78 | * @notice Fully replace an existing extension of the router. 79 | * @dev The extension with name `extension.name` is the extension being replaced. 80 | * @param _extension The extension to replace or overwrite. 81 | */ 82 | function replaceExtension(Extension memory _extension) public virtual onlyAuthorizedCall { 83 | _replaceExtension(_extension); 84 | } 85 | 86 | /** 87 | * @notice Remove an existing extension from the router. 88 | * @param _extensionName The name of the extension to remove. 89 | */ 90 | function removeExtension(string memory _extensionName) public virtual onlyAuthorizedCall { 91 | _removeExtension(_extensionName); 92 | } 93 | 94 | /** 95 | * @notice Enables a single function in an existing extension. 96 | * @dev Makes the given function callable on the router. 97 | * 98 | * @param _extensionName The name of the extension to which `extFunction` belongs. 99 | * @param _function The function to enable. 100 | */ 101 | function enableFunctionInExtension(string memory _extensionName, ExtensionFunction memory _function) public virtual onlyAuthorizedCall { 102 | _enableFunctionInExtension(_extensionName, _function); 103 | } 104 | 105 | /** 106 | * @notice Disables a single function in an Extension. 107 | * 108 | * @param _extensionName The name of the extension to which the function of `functionSelector` belongs. 109 | * @param _functionSelector The function to disable. 110 | */ 111 | function disableFunctionInExtension(string memory _extensionName, bytes4 _functionSelector) public virtual onlyAuthorizedCall { 112 | _disableFunctionInExtension(_extensionName, _functionSelector); 113 | } 114 | 115 | /*/////////////////////////////////////////////////////////////// 116 | Internal functions 117 | //////////////////////////////////////////////////////////////*/ 118 | 119 | /// @dev Add a new extension to the router. 120 | function _addExtension(Extension memory _extension) internal virtual { 121 | // Check: extension namespace must not already exist. 122 | // Check: provided extension namespace must not be empty. 123 | // Check: provided extension implementation must be non-zero. 124 | // Store: new extension name. 125 | require(_canAddExtension(_extension), "ExtensionManager: cannot add extension."); 126 | 127 | // 1. Store: metadata for extension. 128 | _setMetadataForExtension(_extension.metadata.name, _extension.metadata); 129 | 130 | uint256 len = _extension.functions.length; 131 | for (uint256 i = 0; i < len; i += 1) { 132 | // 2. Store: function for extension. 133 | _addToFunctionMap(_extension.metadata.name, _extension.functions[i]); 134 | // 3. Store: metadata for function. 135 | _setMetadataForFunction(_extension.functions[i].functionSelector, _extension.metadata); 136 | } 137 | 138 | emit ExtensionAdded(_extension.metadata.name, _extension.metadata.implementation, _extension); 139 | } 140 | 141 | /// @dev Fully replace an existing extension of the router. 142 | function _replaceExtension(Extension memory _extension) internal virtual { 143 | // Check: extension namespace must already exist. 144 | // Check: provided extension implementation must be non-zero. 145 | require(_canReplaceExtension(_extension), "ExtensionManager: cannot replace extension."); 146 | 147 | // 1. Store: metadata for extension. 148 | _setMetadataForExtension(_extension.metadata.name, _extension.metadata); 149 | // 2. Delete: existing extension.functions and metadata for each function. 150 | _removeAllFunctionsFromExtension(_extension.metadata.name); 151 | 152 | uint256 len = _extension.functions.length; 153 | for (uint256 i = 0; i < len; i += 1) { 154 | // 2. Store: function for extension. 155 | _addToFunctionMap(_extension.metadata.name, _extension.functions[i]); 156 | // 3. Store: metadata for function. 157 | _setMetadataForFunction(_extension.functions[i].functionSelector, _extension.metadata); 158 | } 159 | 160 | emit ExtensionReplaced(_extension.metadata.name, _extension.metadata.implementation, _extension); 161 | } 162 | 163 | /// @dev Remove an existing extension from the router. 164 | function _removeExtension(string memory _extensionName) internal virtual { 165 | // Check: extension namespace must already exist. 166 | // Delete: extension namespace. 167 | require(_canRemoveExtension(_extensionName), "ExtensionManager: cannot remove extension."); 168 | 169 | Extension memory extension = _extensionManagerStorage().extensions[_extensionName]; 170 | 171 | // 1. Delete: metadata for extension. 172 | _deleteMetadataForExtension(_extensionName); 173 | // 2. Delete: existing extension.functions and metadata for each function. 174 | _removeAllFunctionsFromExtension(_extensionName); 175 | 176 | emit ExtensionRemoved(_extensionName, extension); 177 | } 178 | 179 | /// @dev Makes the given function callable on the router. 180 | function _enableFunctionInExtension(string memory _extensionName, ExtensionFunction memory _function) internal virtual { 181 | // Check: extension namespace must already exist. 182 | require(_canEnableFunctionInExtension(_extensionName, _function), "ExtensionManager: cannot Store: function for extension."); 183 | 184 | // 1. Store: function for extension. 185 | _addToFunctionMap(_extensionName, _function); 186 | 187 | ExtensionMetadata memory metadata = _extensionManagerStorage().extensions[_extensionName].metadata; 188 | // 2. Store: metadata for function. 189 | _setMetadataForFunction(_function.functionSelector, metadata); 190 | 191 | emit FunctionEnabled(_extensionName, _function.functionSelector, _function, metadata); 192 | } 193 | 194 | /// @dev Disables a single function in an Extension. 195 | function _disableFunctionInExtension(string memory _extensionName, bytes4 _functionSelector) public virtual onlyAuthorizedCall { 196 | // Check: extension namespace must already exist. 197 | // Check: function must be mapped to provided extension. 198 | require(_canDisableFunctionInExtension(_extensionName, _functionSelector), "ExtensionManager: cannot remove function from extension."); 199 | 200 | ExtensionMetadata memory extMetadata = _extensionManagerStorage().extensionMetadata[_functionSelector]; 201 | 202 | // 1. Delete: function from extension. 203 | _deleteFromFunctionMap(_extensionName, _functionSelector); 204 | // 2. Delete: metadata for function. 205 | _deleteMetadataForFunction(_functionSelector); 206 | 207 | emit FunctionDisabled(_extensionName, _functionSelector, extMetadata); 208 | } 209 | 210 | /// @dev Returns the Extension for a given name. 211 | function _getExtension(string memory _extensionName) internal view returns (Extension memory) { 212 | return _extensionManagerStorage().extensions[_extensionName]; 213 | } 214 | 215 | /// @dev Sets the ExtensionMetadata for a given extension. 216 | function _setMetadataForExtension(string memory _extensionName, ExtensionMetadata memory _metadata) internal { 217 | _extensionManagerStorage().extensions[_extensionName].metadata = _metadata; 218 | } 219 | 220 | /// @dev Deletes the ExtensionMetadata for a given extension. 221 | function _deleteMetadataForExtension(string memory _extensionName) internal { 222 | delete _extensionManagerStorage().extensions[_extensionName].metadata; 223 | } 224 | 225 | /// @dev Sets the ExtensionMetadata for a given function. 226 | function _setMetadataForFunction(bytes4 _functionSelector, ExtensionMetadata memory _metadata) internal { 227 | _extensionManagerStorage().extensionMetadata[_functionSelector] = _metadata; 228 | } 229 | 230 | /// @dev Deletes the ExtensionMetadata for a given function. 231 | function _deleteMetadataForFunction(bytes4 _functionSelector) internal { 232 | delete _extensionManagerStorage().extensionMetadata[_functionSelector]; 233 | } 234 | 235 | /// @dev Adds a function to the function map of an extension. 236 | function _addToFunctionMap(string memory _extensionName, ExtensionFunction memory _extFunction) internal virtual { 237 | /** 238 | * Note: `bytes4(0)` is the function selector for the `receive` function. 239 | * So, we maintain a special fn selector-signature mismatch check for the `receive` function. 240 | **/ 241 | bool mismatch = false; 242 | if(_extFunction.functionSelector == bytes4(0)) { 243 | mismatch = keccak256(abi.encode(_extFunction.functionSignature)) != keccak256(abi.encode("receive()")); 244 | } else { 245 | mismatch = _extFunction.functionSelector != 246 | bytes4(keccak256(abi.encodePacked(_extFunction.functionSignature))); 247 | } 248 | 249 | // Check: function selector and signature must match. 250 | require( 251 | !mismatch, 252 | "ExtensionManager: fn selector and signature mismatch." 253 | ); 254 | // Check: function must not already be mapped to an implementation. 255 | require( 256 | _extensionManagerStorage().extensionMetadata[_extFunction.functionSelector].implementation == address(0), 257 | "ExtensionManager: function impl already exists." 258 | ); 259 | 260 | // Store: name -> extension.functions map 261 | _extensionManagerStorage().extensions[_extensionName].functions.push(_extFunction); 262 | } 263 | 264 | /// @dev Deletes a function from an extension's function map. 265 | function _deleteFromFunctionMap(string memory _extensionName, bytes4 _functionSelector) internal { 266 | ExtensionFunction[] memory extensionFunctions = _extensionManagerStorage().extensions[_extensionName].functions; 267 | 268 | uint256 len = extensionFunctions.length; 269 | for (uint256 i = 0; i < len; i += 1) { 270 | if(extensionFunctions[i].functionSelector == _functionSelector) { 271 | 272 | // Delete: particular function from name -> extension.functions map 273 | _extensionManagerStorage().extensions[_extensionName].functions[i] = _extensionManagerStorage().extensions[_extensionName].functions[len - 1]; 274 | _extensionManagerStorage().extensions[_extensionName].functions.pop(); 275 | break; 276 | } 277 | } 278 | } 279 | 280 | /// @dev Removes all functions from an Extension. 281 | function _removeAllFunctionsFromExtension(string memory _extensionName) internal { 282 | ExtensionFunction[] memory functions = _extensionManagerStorage().extensions[_extensionName].functions; 283 | 284 | // Delete: existing name -> extension.functions map 285 | delete _extensionManagerStorage().extensions[_extensionName].functions; 286 | 287 | for(uint256 i = 0; i < functions.length; i += 1) { 288 | // Delete: metadata for function. 289 | _deleteMetadataForFunction(functions[i].functionSelector); 290 | } 291 | } 292 | 293 | /// @dev Returns whether a new extension can be added in the given execution context. 294 | function _canAddExtension(Extension memory _extension) internal virtual returns (bool) { 295 | // Check: provided extension namespace must not be empty. 296 | require(bytes(_extension.metadata.name).length > 0, "ExtensionManager: empty name."); 297 | 298 | // Check: extension namespace must not already exist. 299 | // Store: new extension name. 300 | require(_extensionManagerStorage().extensionNames.add(_extension.metadata.name), "ExtensionManager: extension already exists."); 301 | 302 | // Check: extension implementation must be non-zero. 303 | require(_extension.metadata.implementation != address(0), "ExtensionManager: adding extension without implementation."); 304 | 305 | return true; 306 | } 307 | 308 | /// @dev Returns whether an extension can be replaced in the given execution context. 309 | function _canReplaceExtension(Extension memory _extension) internal virtual returns (bool) { 310 | // Check: extension namespace must already exist. 311 | require(_extensionManagerStorage().extensionNames.contains(_extension.metadata.name), "ExtensionManager: extension does not exist."); 312 | 313 | // Check: extension implementation must be non-zero. 314 | require(_extension.metadata.implementation != address(0), "ExtensionManager: adding extension without implementation."); 315 | 316 | return true; 317 | } 318 | 319 | /// @dev Returns whether an extension can be removed in the given execution context. 320 | function _canRemoveExtension(string memory _extensionName) internal virtual returns (bool) { 321 | // Check: extension namespace must already exist. 322 | // Delete: extension namespace. 323 | require(_extensionManagerStorage().extensionNames.remove(_extensionName), "ExtensionManager: extension does not exist."); 324 | 325 | return true; 326 | } 327 | 328 | /// @dev Returns whether a function can be enabled in an extension in the given execution context. 329 | function _canEnableFunctionInExtension(string memory _extensionName, ExtensionFunction memory) internal view virtual returns (bool) { 330 | // Check: extension namespace must already exist. 331 | require(_extensionManagerStorage().extensionNames.contains(_extensionName), "ExtensionManager: extension does not exist."); 332 | 333 | return true; 334 | } 335 | 336 | /// @dev Returns whether a function can be disabled in an extension in the given execution context. 337 | function _canDisableFunctionInExtension(string memory _extensionName, bytes4 _functionSelector) internal view virtual returns (bool) { 338 | // Check: extension namespace must already exist. 339 | require(_extensionManagerStorage().extensionNames.contains(_extensionName), "ExtensionManager: extension does not exist."); 340 | // Check: function must be mapped to provided extension. 341 | require(keccak256(abi.encode(_extensionManagerStorage().extensionMetadata[_functionSelector].name)) == keccak256(abi.encode(_extensionName)), "ExtensionManager: incorrect extension."); 342 | 343 | return true; 344 | } 345 | 346 | 347 | /// @dev Returns the ExtensionManager storage. 348 | function _extensionManagerStorage() internal pure returns (ExtensionManagerStorage.Data storage data) { 349 | data = ExtensionManagerStorage.data(); 350 | } 351 | 352 | /// @dev To override; returns whether all relevant permission and other checks are met before any upgrade. 353 | function _isAuthorizedCallToUpgrade() internal view virtual returns (bool); 354 | } -------------------------------------------------------------------------------- /test/BaseRouter.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // @author: thirdweb (https://github.com/thirdweb-dev/dynamic-contracts) 3 | 4 | pragma solidity ^0.8.0; 5 | 6 | import "forge-std/Test.sol"; 7 | import "lib/sstore2/contracts/SSTORE2.sol"; 8 | 9 | import "src/interface/IExtension.sol"; 10 | import "src/presets/BaseRouter.sol"; 11 | import "./utils/MockContracts.sol"; 12 | import "./utils/Strings.sol"; 13 | 14 | /// @dev This custom router is written only for testing purposes and must not be used in production. 15 | contract CustomRouter is BaseRouter { 16 | 17 | constructor(Extension[] memory _extensions) BaseRouter(_extensions) {} 18 | 19 | function initialize() public { 20 | __BaseRouter_init(); 21 | } 22 | 23 | /// @dev Returns whether a function can be disabled in an extension in the given execution context. 24 | function _isAuthorizedCallToUpgrade() internal view virtual override returns (bool) { 25 | return true; 26 | } 27 | } 28 | 29 | contract BaseRouterTest is Test, IExtension { 30 | 31 | using Strings for uint256; 32 | 33 | BaseRouter internal router; 34 | 35 | Extension internal defaultExtension1; 36 | Extension internal defaultExtension2; 37 | Extension internal defaultExtension3; 38 | Extension internal defaultExtension4; 39 | Extension internal defaultExtension5; 40 | 41 | uint256 internal defaultExtensionsCount = 2; 42 | 43 | function setUp() public virtual { 44 | 45 | // Set metadata 46 | defaultExtension1.metadata.name = "MultiplyDivide"; 47 | defaultExtension1.metadata.metadataURI = "ipfs://MultiplyDivide"; 48 | defaultExtension1.metadata.implementation = address(new MultiplyDivide()); 49 | 50 | defaultExtension2.metadata.name = "AddSubstract"; 51 | defaultExtension2.metadata.metadataURI = "ipfs://AddSubstract"; 52 | defaultExtension2.metadata.implementation = address(new AddSubstract()); 53 | 54 | defaultExtension3.metadata.name = "RandomExtension"; 55 | defaultExtension3.metadata.metadataURI = "ipfs://RandomExtension"; 56 | defaultExtension3.metadata.implementation = address(0x3456); 57 | 58 | defaultExtension4.metadata.name = "RandomExtension2"; 59 | defaultExtension4.metadata.metadataURI = "ipfs://RandomExtension2"; 60 | defaultExtension4.metadata.implementation = address(0x5678); 61 | 62 | defaultExtension5.metadata.name = "RandomExtension3"; 63 | defaultExtension5.metadata.metadataURI = "ipfs://RandomExtension3"; 64 | defaultExtension5.metadata.implementation = address(0x7890); 65 | 66 | // Set functions 67 | 68 | defaultExtension1.functions.push(ExtensionFunction( 69 | MultiplyDivide.multiplyNumber.selector, 70 | "multiplyNumber(uint256)" 71 | )); 72 | defaultExtension1.functions.push(ExtensionFunction( 73 | MultiplyDivide.divideNumber.selector, 74 | "divideNumber(uint256)" 75 | )); 76 | defaultExtension2.functions.push(ExtensionFunction( 77 | AddSubstract.addNumber.selector, 78 | "addNumber(uint256)" 79 | )); 80 | defaultExtension2.functions.push(ExtensionFunction( 81 | AddSubstract.subtractNumber.selector, 82 | "subtractNumber(uint256)" 83 | )); 84 | 85 | for(uint256 i = 0; i < 10; i++) { 86 | string memory functionSignature = string(abi.encodePacked("randomFunction", i.toString(), "(uint256)")); 87 | bytes4 selector = bytes4(keccak256(bytes(functionSignature))); 88 | defaultExtension3.functions.push(ExtensionFunction( 89 | selector, 90 | functionSignature 91 | )); 92 | } 93 | 94 | for(uint256 i = 0; i < 20; i++) { 95 | string memory functionSignature = string(abi.encodePacked("randomFunctionNew", i.toString(), "(uint256,string,bytes,(uint256,uint256,bool))")); 96 | bytes4 selector = bytes4(keccak256(bytes(functionSignature))); 97 | defaultExtension4.functions.push(ExtensionFunction( 98 | selector, 99 | functionSignature 100 | )); 101 | } 102 | 103 | for(uint256 i = 0; i < 30; i++) { 104 | string memory functionSignature = string(abi.encodePacked("randomFunctionAnother", i.toString(), "(uint256,string,address[])")); 105 | bytes4 selector = bytes4(keccak256(bytes(functionSignature))); 106 | defaultExtension5.functions.push(ExtensionFunction( 107 | selector, 108 | functionSignature 109 | )); 110 | } 111 | 112 | Extension[] memory defaultExtensions = new Extension[](2); 113 | defaultExtensions[0] = defaultExtension1; 114 | defaultExtensions[1] = defaultExtension2; 115 | 116 | // Deploy BaseRouter 117 | router = BaseRouter(payable(address(new CustomRouter(defaultExtensions)))); 118 | CustomRouter(payable(address(router))).initialize(); 119 | } 120 | 121 | /*/////////////////////////////////////////////////////////////// 122 | Helpers 123 | //////////////////////////////////////////////////////////////*/ 124 | 125 | function _validateExtensionDataOnContract(Extension memory _referenceExtension) internal { 126 | 127 | ExtensionFunction[] memory functions = _referenceExtension.functions; 128 | 129 | for(uint256 i = 0; i < functions.length; i += 1) { 130 | 131 | // Check that the correct implementation address is used. 132 | assertEq(router.getImplementationForFunction(functions[i].functionSelector), _referenceExtension.metadata.implementation); 133 | 134 | // Check that the metadata is set correctly 135 | ExtensionMetadata memory metadata = router.getMetadataForFunction(functions[i].functionSelector); 136 | assertEq(metadata.name, _referenceExtension.metadata.name); 137 | assertEq(metadata.metadataURI, _referenceExtension.metadata.metadataURI); 138 | assertEq(metadata.implementation, _referenceExtension.metadata.implementation); 139 | } 140 | 141 | Extension[] memory extensions = router.getAllExtensions(); 142 | for(uint256 i = 0; i < extensions.length; i += 1) { 143 | if( 144 | keccak256(abi.encode(extensions[i].metadata.name)) == keccak256(abi.encode(_referenceExtension.metadata.name)) 145 | ) { 146 | assertEq(extensions[i].metadata.name, _referenceExtension.metadata.name); 147 | assertEq(extensions[i].metadata.metadataURI, _referenceExtension.metadata.metadataURI); 148 | assertEq(extensions[i].metadata.implementation, _referenceExtension.metadata.implementation); 149 | 150 | ExtensionFunction[] memory fns = extensions[i].functions; 151 | assertEq(fns.length, _referenceExtension.functions.length); 152 | 153 | for(uint256 k = 0; k < fns.length; k += 1) { 154 | assertEq(fns[k].functionSelector, _referenceExtension.functions[k].functionSelector); 155 | assertEq(fns[k].functionSignature, _referenceExtension.functions[k].functionSignature); 156 | } 157 | } else { 158 | continue; 159 | } 160 | } 161 | 162 | Extension memory storedExtension = router.getExtension(_referenceExtension.metadata.name); 163 | assertEq(storedExtension.metadata.name, _referenceExtension.metadata.name); 164 | assertEq(storedExtension.metadata.metadataURI, _referenceExtension.metadata.metadataURI); 165 | assertEq(storedExtension.metadata.implementation, _referenceExtension.metadata.implementation); 166 | 167 | assertEq(storedExtension.functions.length, _referenceExtension.functions.length); 168 | for(uint256 l = 0; l < storedExtension.functions.length; l += 1) { 169 | assertEq(storedExtension.functions[l].functionSelector, _referenceExtension.functions[l].functionSelector); 170 | assertEq(storedExtension.functions[l].functionSignature, _referenceExtension.functions[l].functionSignature); 171 | } 172 | 173 | } 174 | 175 | /*/////////////////////////////////////////////////////////////// 176 | Default extensions 177 | //////////////////////////////////////////////////////////////*/ 178 | 179 | /// @notice Check that default extensions are stored correctly. 180 | function test_state_defaultExtensions() public { 181 | Extension[] memory extensions = router.getAllExtensions(); 182 | assertEq(extensions.length, defaultExtensionsCount); 183 | 184 | _validateExtensionDataOnContract(defaultExtension1); 185 | _validateExtensionDataOnContract(defaultExtension2); 186 | } 187 | 188 | /*/////////////////////////////////////////////////////////////// 189 | Transfer method test 190 | //////////////////////////////////////////////////////////////*/ 191 | 192 | function test_transferMethod() public { 193 | uint256 amount = 1 ether; 194 | 195 | assertEq(address(router).balance, 0); 196 | payable(address(router)).transfer(amount); 197 | assertEq(address(router).balance, amount); 198 | } 199 | 200 | /*/////////////////////////////////////////////////////////////// 201 | Deploy / Initialze BaseRouter & SSTORE2 202 | //////////////////////////////////////////////////////////////*/ 203 | 204 | /// @notice Check with a single extension with 10 functions 205 | function test_state_deployBaseRouter() external { 206 | Extension[] memory defaultExtensionsNew = new Extension[](1); 207 | defaultExtensionsNew[0] = defaultExtension3; 208 | CustomRouter routerNew = new CustomRouter(defaultExtensionsNew); 209 | 210 | uint256 size; 211 | address defaultExtensionsAddress = routerNew.defaultExtensions(); 212 | 213 | assembly { 214 | size := extcodesize(defaultExtensionsAddress) 215 | } 216 | 217 | console.log(size); 218 | // ensure size of default extension contract doesn't breach the limit 219 | assertTrue(size < 24575); 220 | 221 | bytes memory data = SSTORE2.read(defaultExtensionsAddress); 222 | Extension[] memory defaults = abi.decode(data, (Extension[])); 223 | assertEq(defaults.length, defaultExtensionsNew.length); 224 | for(uint256 i = 0; i < defaults.length; i++) { 225 | assertEq(defaults[i].functions.length, defaultExtensionsNew[i].functions.length); 226 | 227 | for(uint256 j = 0; j < defaults[i].functions.length; j++) { 228 | assertEq(defaults[i].functions[j].functionSelector, defaultExtensionsNew[i].functions[j].functionSelector); 229 | } 230 | } 231 | } 232 | 233 | /// @notice Check with multiple extensions extension with ~50 functions in total 234 | function test_state_deployBaseRouter_multipleExtensions() external { 235 | Extension[] memory defaultExtensionsNew = new Extension[](3); 236 | defaultExtensionsNew[0] = defaultExtension3; 237 | defaultExtensionsNew[1] = defaultExtension4; 238 | defaultExtensionsNew[2] = defaultExtension5; 239 | CustomRouter routerNew = new CustomRouter(defaultExtensionsNew); 240 | 241 | uint256 size; 242 | address defaultExtensionsAddress = routerNew.defaultExtensions(); 243 | 244 | assembly { 245 | size := extcodesize(defaultExtensionsAddress) 246 | } 247 | 248 | console.log(size); 249 | // ensure size of default extension contract doesn't breach the limit 250 | assertTrue(size < 24575); 251 | 252 | bytes memory data = SSTORE2.read(defaultExtensionsAddress); 253 | Extension[] memory defaults = abi.decode(data, (Extension[])); 254 | assertEq(defaults.length, defaultExtensionsNew.length); 255 | for(uint256 i = 0; i < defaults.length; i++) { 256 | assertEq(defaults[i].functions.length, defaultExtensionsNew[i].functions.length); 257 | 258 | for(uint256 j = 0; j < defaults[i].functions.length; j++) { 259 | assertEq(defaults[i].functions[j].functionSelector, defaultExtensionsNew[i].functions[j].functionSelector); 260 | } 261 | } 262 | } 263 | 264 | /// @notice Two default extensions share the same name. 265 | function test_revert_deployBaesRouter_nameAlreadyUsed() external { 266 | Extension[] memory defaultExtensionsNew = new Extension[](2); 267 | defaultExtensionsNew[0] = defaultExtension3; 268 | defaultExtensionsNew[1] = defaultExtension3; 269 | vm.expectRevert("BaseRouter: invalid extension."); 270 | new CustomRouter(defaultExtensionsNew); 271 | } 272 | 273 | /// @notice The same function exists in two default extensions. 274 | function test_revert_deployBaesRouter_fnAlreadyExists() external { 275 | Extension[] memory defaultExtensionsNew = new Extension[](2); 276 | defaultExtensionsNew[0] = defaultExtension3; 277 | defaultExtensionsNew[1] = defaultExtension4; 278 | 279 | defaultExtensionsNew[1].functions[0] = defaultExtension3.functions[0]; 280 | 281 | vm.expectRevert("BaseRouter: invalid extension."); 282 | new CustomRouter(defaultExtensionsNew); 283 | } 284 | 285 | /// @notice Default extension has empty name. 286 | function test_revert_deployBaesRouter_emptyName() external { 287 | Extension[] memory defaultExtensionsNew = new Extension[](1); 288 | defaultExtensionsNew[0] = defaultExtension3; 289 | defaultExtensionsNew[0].metadata.name = ""; 290 | 291 | vm.expectRevert("BaseRouter: invalid extension."); 292 | new CustomRouter(defaultExtensionsNew); 293 | } 294 | 295 | /// @notice Default extension has empty implementation address. 296 | function test_revert_deployBaesRouter_emptyImplementation() external { 297 | Extension[] memory defaultExtensionsNew = new Extension[](1); 298 | defaultExtensionsNew[0] = defaultExtension3; 299 | defaultExtensionsNew[0].metadata.implementation = address(0); 300 | 301 | vm.expectRevert("BaseRouter: invalid extension."); 302 | new CustomRouter(defaultExtensionsNew); 303 | } 304 | 305 | /// @notice Default extension has function selector signature mismatch. 306 | function test_revert_deployBaesRouter_fnSelectorSignatureMismatch() external { 307 | Extension[] memory defaultExtensionsNew = new Extension[](1); 308 | defaultExtensionsNew[0] = defaultExtension3; 309 | defaultExtensionsNew[0].functions[0].functionSignature = "whatever(uint256)"; 310 | 311 | vm.expectRevert("BaseRouter: invalid extension."); 312 | new CustomRouter(defaultExtensionsNew); 313 | } 314 | 315 | /// @notice Check with a single extension with 10 functions 316 | function test_state_initializeBaseRouter_singleExtension() external { 317 | // vm.pauseGasMetering(); 318 | Extension[] memory defaultExtensionsNew = new Extension[](1); 319 | defaultExtensionsNew[0] = defaultExtension3; 320 | CustomRouter routerNew = new CustomRouter(defaultExtensionsNew); 321 | // vm.resumeGasMetering(); 322 | 323 | routerNew.initialize(); 324 | 325 | Extension[] memory defaultExtensionsAfterInit = routerNew.getAllExtensions(); 326 | assertEq(defaultExtensionsAfterInit.length, defaultExtensionsNew.length); 327 | for(uint256 i = 0; i < defaultExtensionsAfterInit.length; i++) { 328 | assertEq(defaultExtensionsAfterInit[i].functions.length, defaultExtensionsNew[i].functions.length); 329 | 330 | for(uint256 j = 0; j < defaultExtensionsAfterInit[i].functions.length; j++) { 331 | assertEq(defaultExtensionsAfterInit[i].functions[j].functionSelector, defaultExtensionsNew[i].functions[j].functionSelector); 332 | } 333 | } 334 | } 335 | 336 | /// @notice Check with multiple extensions extension with 50-100 functions in total 337 | function test_state_initializeBaseRouter_multipleExtensions() external { 338 | // vm.pauseGasMetering(); 339 | Extension[] memory defaultExtensionsNew = new Extension[](3); 340 | defaultExtensionsNew[0] = defaultExtension3; 341 | defaultExtensionsNew[1] = defaultExtension4; 342 | defaultExtensionsNew[2] = defaultExtension5; 343 | 344 | CustomRouter routerNew = new CustomRouter(defaultExtensionsNew); 345 | // vm.resumeGasMetering(); 346 | 347 | routerNew.initialize(); 348 | 349 | Extension[] memory defaultExtensionsAfterInit = routerNew.getAllExtensions(); 350 | assertEq(defaultExtensionsAfterInit.length, defaultExtensionsNew.length); 351 | for(uint256 i = 0; i < defaultExtensionsAfterInit.length; i++) { 352 | assertEq(defaultExtensionsAfterInit[i].functions.length, defaultExtensionsNew[i].functions.length); 353 | 354 | for(uint256 j = 0; j < defaultExtensionsAfterInit[i].functions.length; j++) { 355 | assertEq(defaultExtensionsAfterInit[i].functions[j].functionSelector, defaultExtensionsNew[i].functions[j].functionSelector); 356 | } 357 | } 358 | } 359 | 360 | /*/////////////////////////////////////////////////////////////// 361 | Adding extensions 362 | //////////////////////////////////////////////////////////////*/ 363 | 364 | /// @notice Add an new extension. 365 | function test_state_addExtension() public { 366 | 367 | // Create Extension struct 368 | Extension memory extension; 369 | 370 | // Set metadata 371 | extension.metadata.name = "IncrementDecrement"; 372 | extension.metadata.metadataURI = "ipfs://IncrementDecrement"; 373 | extension.metadata.implementation = address(new IncrementDecrementGet()); 374 | 375 | // Set functions 376 | extension.functions = new ExtensionFunction[](3); 377 | 378 | extension.functions[0] = ExtensionFunction( 379 | IncrementDecrementGet.incrementNumber.selector, 380 | "incrementNumber()" 381 | ); 382 | extension.functions[1] = ExtensionFunction( 383 | IncrementDecrementGet.decrementNumber.selector, 384 | "decrementNumber()" 385 | ); 386 | extension.functions[2] = ExtensionFunction( 387 | IncrementDecrementGet.getNumber.selector, 388 | "getNumber()" 389 | ); 390 | 391 | // Pre-call checks 392 | assertEq(router.getImplementationForFunction(IncrementDecrementGet.incrementNumber.selector), address(0)); 393 | assertEq(router.getImplementationForFunction(IncrementDecrementGet.decrementNumber.selector), address(0)); 394 | assertEq(router.getImplementationForFunction(IncrementDecrementGet.getNumber.selector), address(0)); 395 | 396 | ExtensionMetadata memory metadata1 = router.getMetadataForFunction(IncrementDecrementGet.incrementNumber.selector); 397 | assertEq(metadata1.name, ""); 398 | assertEq(metadata1.metadataURI, ""); 399 | assertEq(metadata1.implementation, address(0)); 400 | 401 | ExtensionMetadata memory metadata2 = router.getMetadataForFunction(IncrementDecrementGet.decrementNumber.selector); 402 | assertEq(metadata2.name, ""); 403 | assertEq(metadata2.metadataURI, ""); 404 | assertEq(metadata2.implementation, address(0)); 405 | 406 | ExtensionMetadata memory metadata3 = router.getMetadataForFunction(IncrementDecrementGet.getNumber.selector); 407 | assertEq(metadata3.name, ""); 408 | assertEq(metadata3.metadataURI, ""); 409 | assertEq(metadata3.implementation, address(0)); 410 | 411 | assertEq(router.getAllExtensions().length, defaultExtensionsCount); 412 | 413 | // Call: addExtension 414 | router.addExtension(extension); 415 | 416 | // Post-call checks 417 | _validateExtensionDataOnContract(extension); 418 | 419 | // Verify functionality 420 | 421 | IncrementDecrementGet inc = IncrementDecrementGet(address(router)); 422 | 423 | assertEq(inc.getNumber(), 0); 424 | 425 | inc.incrementNumber(); 426 | assertEq(inc.getNumber(), 1); 427 | 428 | inc.incrementNumber(); 429 | assertEq(inc.getNumber(), 2); 430 | 431 | inc.decrementNumber(); 432 | assertEq(inc.getNumber(), 1); 433 | } 434 | 435 | /// @notice Add an extension with the receive function. 436 | function test_state_addExtension_withReceiveFunction() public { 437 | // Create Extension struct 438 | Extension memory extension; 439 | 440 | // Set metadata 441 | extension.metadata.name = "Receive"; 442 | extension.metadata.metadataURI = "ipfs://Receive"; 443 | extension.metadata.implementation = address(new Receive()); 444 | 445 | // Set functions 446 | extension.functions = new ExtensionFunction[](1); 447 | 448 | extension.functions[0] = ExtensionFunction( 449 | bytes4(0), 450 | "receive()" 451 | ); 452 | 453 | // Pre-call checks 454 | address sender = address(0x123); 455 | vm.deal(sender, 100 ether); 456 | 457 | vm.expectRevert(); 458 | vm.prank(sender); 459 | address(router).call{value: 1 ether}(""); 460 | 461 | // Call: addExtension 462 | router.addExtension(extension); 463 | 464 | // Post-call checks 465 | _validateExtensionDataOnContract(extension); 466 | 467 | // Verify functionality 468 | 469 | uint256 balBefore = (address(router)).balance; 470 | uint256 amount = 1 ether; 471 | 472 | vm.prank(sender); 473 | address(router).call{value: 1 ether}(""); 474 | 475 | assertEq((address(router)).balance, balBefore + amount); 476 | } 477 | 478 | /// @notice Revert: add an extension whose name is already used by a default extension. 479 | function test_revert_addExtension_nameAlreadyUsedByDefaultExtension() public { 480 | // Create Extension struct 481 | Extension memory extension1; 482 | 483 | // Set metadata 484 | extension1.metadata.name = defaultExtension1.metadata.name; 485 | extension1.metadata.metadataURI = "ipfs://IncrementDecrement"; 486 | extension1.metadata.implementation = address(new IncrementDecrement()); 487 | 488 | // Set functions 489 | extension1.functions = new ExtensionFunction[](2); 490 | extension1.functions[0] = ExtensionFunction( 491 | IncrementDecrementGet.incrementNumber.selector, 492 | "incrementNumber()" 493 | ); 494 | extension1.functions[1] = ExtensionFunction( 495 | IncrementDecrementGet.decrementNumber.selector, 496 | "decrementNumber()" 497 | ); 498 | 499 | // Call: addExtension 500 | vm.expectRevert("ExtensionManager: extension already exists."); 501 | router.addExtension(extension1); 502 | } 503 | 504 | 505 | /// @notice Revert: add an extension whose name is already used by another non-default extension. 506 | function test_revert_addExtension_nameAlreadyUsed() public { 507 | // Create Extension struct 508 | Extension memory extension1; 509 | Extension memory extension2; 510 | 511 | // Set metadata 512 | extension1.metadata.name = "IncrementDecrement"; 513 | extension1.metadata.metadataURI = "ipfs://IncrementDecrement"; 514 | extension1.metadata.implementation = address(new IncrementDecrement()); 515 | 516 | extension2.metadata.name = extension1.metadata.name; 517 | extension2.metadata.metadataURI = "ipfs://IncrementDecrementGet"; 518 | extension2.metadata.implementation = address(new IncrementDecrementGet()); 519 | 520 | // Set functions 521 | extension1.functions = new ExtensionFunction[](2); 522 | extension1.functions[0] = ExtensionFunction( 523 | IncrementDecrementGet.incrementNumber.selector, 524 | "incrementNumber()" 525 | ); 526 | extension1.functions[1] = ExtensionFunction( 527 | IncrementDecrementGet.decrementNumber.selector, 528 | "decrementNumber()" 529 | ); 530 | 531 | extension2.functions = new ExtensionFunction[](1); 532 | extension2.functions[0] = ExtensionFunction( 533 | IncrementDecrementGet.getNumber.selector, 534 | "getNumber()" 535 | ); 536 | 537 | // Call: addExtension 538 | router.addExtension(extension1); 539 | 540 | vm.expectRevert("ExtensionManager: extension already exists."); 541 | router.addExtension(extension2); 542 | } 543 | 544 | /// @notice Revert: add an extension with an empty name. 545 | function test_revert_addExtension_emptyName() public { 546 | // Create Extension struct 547 | Extension memory extension1; 548 | 549 | // Set metadata 550 | extension1.metadata.name = ""; 551 | extension1.metadata.metadataURI = "ipfs://IncrementDecrement"; 552 | extension1.metadata.implementation = address(new IncrementDecrement()); 553 | 554 | // Set functions 555 | extension1.functions = new ExtensionFunction[](2); 556 | extension1.functions[0] = ExtensionFunction( 557 | IncrementDecrementGet.incrementNumber.selector, 558 | "incrementNumber()" 559 | ); 560 | extension1.functions[1] = ExtensionFunction( 561 | IncrementDecrementGet.decrementNumber.selector, 562 | "decrementNumber()" 563 | ); 564 | 565 | // Call: addExtension 566 | vm.expectRevert("ExtensionManager: empty name."); 567 | router.addExtension(extension1); 568 | } 569 | 570 | /// @notice Revert: add an extension with an empty implementation address. 571 | function test_revert_addExtension_emptyImplementation() public { 572 | // Create Extension struct 573 | Extension memory extension1; 574 | 575 | // Set metadata 576 | extension1.metadata.name = "IncrementDecrement"; 577 | extension1.metadata.metadataURI = "ipfs://IncrementDecrement"; 578 | extension1.metadata.implementation = address(0); 579 | 580 | // Set functions 581 | extension1.functions = new ExtensionFunction[](2); 582 | extension1.functions[0] = ExtensionFunction( 583 | IncrementDecrementGet.incrementNumber.selector, 584 | "incrementNumber()" 585 | ); 586 | extension1.functions[1] = ExtensionFunction( 587 | IncrementDecrementGet.decrementNumber.selector, 588 | "decrementNumber()" 589 | ); 590 | 591 | // Call: addExtension 592 | vm.expectRevert("ExtensionManager: adding extension without implementation."); 593 | router.addExtension(extension1); 594 | } 595 | 596 | /// @notice Revert: add an extension with a function selector-signature mismatch. 597 | function test_revert_addExtension_fnSelectorSignatureMismatch() public { 598 | // Create Extension struct 599 | Extension memory extension1; 600 | 601 | // Set metadata 602 | extension1.metadata.name = "IncrementDecrement"; 603 | extension1.metadata.metadataURI = "ipfs://IncrementDecrement"; 604 | extension1.metadata.implementation = address(new IncrementDecrement()); 605 | 606 | // Set functions 607 | extension1.functions = new ExtensionFunction[](2); 608 | extension1.functions[0] = ExtensionFunction( 609 | IncrementDecrementGet.incrementNumber.selector, 610 | "getNumber()" 611 | ); 612 | extension1.functions[1] = ExtensionFunction( 613 | IncrementDecrementGet.decrementNumber.selector, 614 | "decrementNumber()" 615 | ); 616 | 617 | // Call: addExtension 618 | vm.expectRevert("ExtensionManager: fn selector and signature mismatch."); 619 | router.addExtension(extension1); 620 | } 621 | 622 | /// @notice Revert: add an extension with an empty function signature. 623 | function test_revert_addExtension_emptyFunctionSignature() public { 624 | // Create Extension struct 625 | Extension memory extension1; 626 | 627 | // Set metadata 628 | extension1.metadata.name = "IncrementDecrement"; 629 | extension1.metadata.metadataURI = "ipfs://IncrementDecrement"; 630 | extension1.metadata.implementation = address(new IncrementDecrement()); 631 | 632 | // Set functions 633 | extension1.functions = new ExtensionFunction[](2); 634 | extension1.functions[0] = ExtensionFunction( 635 | IncrementDecrementGet.incrementNumber.selector, 636 | "" 637 | ); 638 | extension1.functions[1] = ExtensionFunction( 639 | IncrementDecrementGet.decrementNumber.selector, 640 | "decrementNumber()" 641 | ); 642 | 643 | // Call: addExtension 644 | vm.expectRevert("ExtensionManager: fn selector and signature mismatch."); 645 | router.addExtension(extension1); 646 | } 647 | 648 | /// @notice Revert: add an extension with an empty function selector. 649 | function test_revert_addExtension_emptyFunctionSelector() public { 650 | // Create Extension struct 651 | Extension memory extension1; 652 | 653 | // Set metadata 654 | extension1.metadata.name = "IncrementDecrement"; 655 | extension1.metadata.metadataURI = "ipfs://IncrementDecrement"; 656 | extension1.metadata.implementation = address(new IncrementDecrement()); 657 | 658 | // Set functions 659 | extension1.functions = new ExtensionFunction[](2); 660 | extension1.functions[0] = ExtensionFunction( 661 | bytes4(0), 662 | "incrementNumber()" 663 | ); 664 | extension1.functions[1] = ExtensionFunction( 665 | IncrementDecrementGet.decrementNumber.selector, 666 | "decrementNumber()" 667 | ); 668 | 669 | // Call: addExtension 670 | vm.expectRevert("ExtensionManager: fn selector and signature mismatch."); 671 | router.addExtension(extension1); 672 | } 673 | 674 | /// @notice Revert: add an extension specifying the same function twice. 675 | function test_revert_addExtension_duplicateFunction() public { 676 | // Create Extension struct 677 | Extension memory extension1; 678 | 679 | // Set metadata 680 | extension1.metadata.name = "IncrementDecrement"; 681 | extension1.metadata.metadataURI = "ipfs://IncrementDecrement"; 682 | extension1.metadata.implementation = address(new IncrementDecrement()); 683 | 684 | // Set functions 685 | extension1.functions = new ExtensionFunction[](2); 686 | extension1.functions[0] = ExtensionFunction( 687 | IncrementDecrementGet.incrementNumber.selector, 688 | "incrementNumber()" 689 | ); 690 | extension1.functions[1] = ExtensionFunction( 691 | IncrementDecrementGet.incrementNumber.selector, 692 | "incrementNumber()" 693 | ); 694 | 695 | // Call: addExtension 696 | vm.expectRevert("ExtensionManager: function impl already exists."); 697 | router.addExtension(extension1); 698 | } 699 | 700 | /// @notice Revert: add an extension with a function that already exists in a default extension. 701 | function test_revert_addExtension_fnAlreadyExistsInDefaultExtension() public { 702 | // Create Extension struct 703 | Extension memory extension1; 704 | 705 | // Set metadata 706 | extension1.metadata.name = "IncrementDecrement"; 707 | extension1.metadata.metadataURI = "ipfs://IncrementDecrement"; 708 | extension1.metadata.implementation = address(new IncrementDecrement()); 709 | 710 | // Set functions 711 | extension1.functions = new ExtensionFunction[](2); 712 | extension1.functions[0] = ExtensionFunction( 713 | IncrementDecrementGet.incrementNumber.selector, 714 | "incrementNumber()" 715 | ); 716 | extension1.functions[1] = defaultExtension1.functions[0]; 717 | 718 | // Call: addExtension 719 | vm.expectRevert("ExtensionManager: function impl already exists."); 720 | router.addExtension(extension1); 721 | 722 | // vm.expectRevert("BaseRouter: function impl already exists."); 723 | // router.addExtension(extension1); 724 | } 725 | 726 | /// @notice Revert: add an extension with a function that already exists in another non-default extension. 727 | function test_revert_addExtension_fnAlreadyExistsInAnotherExtension() public { 728 | // Create Extension struct 729 | Extension memory extension1; 730 | Extension memory extension2; 731 | 732 | // Set metadata 733 | extension1.metadata.name = "IncrementDecrement"; 734 | extension1.metadata.metadataURI = "ipfs://IncrementDecrement"; 735 | extension1.metadata.implementation = address(new IncrementDecrement()); 736 | 737 | extension2.metadata.name = "IncrementDecrementGet"; 738 | extension2.metadata.metadataURI = "ipfs://IncrementDecrementGet"; 739 | extension2.metadata.implementation = address(new IncrementDecrementGet()); 740 | 741 | // Set functions 742 | extension1.functions = new ExtensionFunction[](2); 743 | extension1.functions[0] = ExtensionFunction( 744 | IncrementDecrementGet.incrementNumber.selector, 745 | "incrementNumber()" 746 | ); 747 | extension1.functions[1] = ExtensionFunction( 748 | IncrementDecrementGet.decrementNumber.selector, 749 | "decrementNumber()" 750 | ); 751 | 752 | extension2.functions = new ExtensionFunction[](1); 753 | extension2.functions[0] = ExtensionFunction( 754 | IncrementDecrementGet.incrementNumber.selector, 755 | "incrementNumber()" 756 | ); 757 | 758 | // Call: addExtension 759 | router.addExtension(extension1); 760 | 761 | vm.expectRevert("ExtensionManager: function impl already exists."); 762 | router.addExtension(extension2); 763 | } 764 | 765 | /*/////////////////////////////////////////////////////////////// 766 | Replace extensions 767 | //////////////////////////////////////////////////////////////*/ 768 | 769 | /// @notice Replace a default extension with a new one. 770 | function test_state_replaceExtension_defaultExtension() public { 771 | // Create Extension struct to replace existing extension 772 | Extension memory updatedExtension; 773 | 774 | updatedExtension.metadata = defaultExtension1.metadata; 775 | updatedExtension.metadata.implementation = address(new IncrementDecrementMultiply()); 776 | 777 | updatedExtension.functions = new ExtensionFunction[](3); 778 | updatedExtension.functions[0] = defaultExtension1.functions[0]; 779 | updatedExtension.functions[1] = ExtensionFunction( 780 | IncrementDecrementGet.incrementNumber.selector, 781 | "incrementNumber()" 782 | ); 783 | updatedExtension.functions[2] = ExtensionFunction( 784 | IncrementDecrementGet.getNumber.selector, 785 | "getNumber()" 786 | ); 787 | 788 | // Call: addExtension 789 | router.replaceExtension(updatedExtension); 790 | 791 | // Post-call checks 792 | _validateExtensionDataOnContract(updatedExtension); 793 | 794 | // Verify functionality 795 | assertEq(router.getImplementationForFunction(MultiplyDivide.multiplyNumber.selector), updatedExtension.metadata.implementation); 796 | assertEq(router.getImplementationForFunction(IncrementDecrementGet.incrementNumber.selector), updatedExtension.metadata.implementation); 797 | assertEq(router.getImplementationForFunction(IncrementDecrementGet.getNumber.selector), updatedExtension.metadata.implementation); 798 | 799 | IncrementDecrementMultiply inc = IncrementDecrementMultiply(address(router)); 800 | inc.incrementNumber(); 801 | inc.incrementNumber(); 802 | assertEq(inc.getNumber(), 2); 803 | 804 | 805 | inc.multiplyNumber(2); 806 | assertEq(inc.getNumber(), 4); 807 | 808 | vm.expectRevert("Router: function does not exist."); 809 | MultiplyDivide(address(router)).divideNumber(2); 810 | } 811 | 812 | 813 | 814 | /// @notice Replace a non-default extension with a new one. 815 | function test_state_replaceExtension() public { 816 | // Create Extension struct 817 | Extension memory extension; 818 | 819 | // Set metadata 820 | extension.metadata.name = "IncrementDecrement"; 821 | extension.metadata.metadataURI = "ipfs://IncrementDecrement"; 822 | extension.metadata.implementation = address(new IncrementDecrement()); 823 | 824 | // Set functions 825 | extension.functions = new ExtensionFunction[](2); 826 | extension.functions[0] = ExtensionFunction( 827 | IncrementDecrementGet.incrementNumber.selector, 828 | "incrementNumber()" 829 | ); 830 | extension.functions[1] = ExtensionFunction( 831 | IncrementDecrementGet.decrementNumber.selector, 832 | "decrementNumber()" 833 | ); 834 | 835 | // Call: addExtension 836 | router.addExtension(extension); 837 | _validateExtensionDataOnContract(extension); 838 | 839 | // Create Extension struct to replace existing extension 840 | Extension memory updatedExtension; 841 | 842 | updatedExtension.metadata = extension.metadata; 843 | updatedExtension.metadata.implementation = address(new IncrementDecrementGet()); 844 | 845 | updatedExtension.functions = new ExtensionFunction[](3); 846 | updatedExtension.functions[0] = extension.functions[0]; 847 | updatedExtension.functions[1] = extension.functions[1]; 848 | updatedExtension.functions[2] = ExtensionFunction( 849 | IncrementDecrementGet.getNumber.selector, 850 | "getNumber()" 851 | ); 852 | 853 | // Call: addExtension 854 | router.replaceExtension(updatedExtension); 855 | 856 | // Post-call checks 857 | _validateExtensionDataOnContract(updatedExtension); 858 | 859 | // Verify functionality 860 | assertEq(router.getImplementationForFunction(IncrementDecrementGet.incrementNumber.selector), updatedExtension.metadata.implementation); 861 | assertEq(router.getImplementationForFunction(IncrementDecrementGet.decrementNumber.selector), updatedExtension.metadata.implementation); 862 | assertEq(router.getImplementationForFunction(IncrementDecrementGet.getNumber.selector), updatedExtension.metadata.implementation); 863 | 864 | IncrementDecrementGet inc = IncrementDecrementGet(address(router)); 865 | inc.incrementNumber(); 866 | inc.incrementNumber(); 867 | inc.incrementNumber(); 868 | inc.decrementNumber(); 869 | 870 | assertEq(inc.getNumber(), 2); 871 | } 872 | 873 | 874 | /// @notice Revert: replace a non existent extension. 875 | function test_revert_replaceExtension_extensionDoesNotExist() public { 876 | // Create Extension struct 877 | Extension memory extension; 878 | 879 | // Set metadata 880 | extension.metadata.name = "IncrementDecrement"; 881 | extension.metadata.metadataURI = "ipfs://IncrementDecrement"; 882 | extension.metadata.implementation = address(new IncrementDecrement()); 883 | 884 | // Set functions 885 | extension.functions = new ExtensionFunction[](2); 886 | extension.functions[0] = ExtensionFunction( 887 | IncrementDecrementGet.incrementNumber.selector, 888 | "incrementNumber()" 889 | ); 890 | extension.functions[1] = ExtensionFunction( 891 | IncrementDecrementGet.decrementNumber.selector, 892 | "decrementNumber()" 893 | ); 894 | 895 | // Call: replaceExtension 896 | vm.expectRevert("ExtensionManager: extension does not exist."); 897 | router.replaceExtension(extension); 898 | } 899 | 900 | /// @notice Revert: replace an extension with an empty name. 901 | function test_revert_replaceExtension_emptyName() public { 902 | // Create Extension struct 903 | Extension memory extension; 904 | 905 | // Set metadata 906 | extension.metadata.name = "IncrementDecrement"; 907 | extension.metadata.metadataURI = "ipfs://IncrementDecrement"; 908 | extension.metadata.implementation = address(new IncrementDecrement()); 909 | 910 | // Set functions 911 | extension.functions = new ExtensionFunction[](2); 912 | extension.functions[0] = ExtensionFunction( 913 | IncrementDecrementGet.incrementNumber.selector, 914 | "incrementNumber()" 915 | ); 916 | extension.functions[1] = ExtensionFunction( 917 | IncrementDecrementGet.decrementNumber.selector, 918 | "decrementNumber()" 919 | ); 920 | 921 | // Call: addExtension 922 | router.addExtension(extension); 923 | _validateExtensionDataOnContract(extension); 924 | 925 | IncrementDecrementGet inc = IncrementDecrementGet(address(router)); 926 | inc.incrementNumber(); 927 | inc.incrementNumber(); 928 | 929 | // Create Extension struct to replace existing extension 930 | Extension memory updatedExtension; 931 | 932 | updatedExtension.metadata.name = ""; 933 | updatedExtension.metadata.metadataURI = extension.metadata.metadataURI; 934 | updatedExtension.metadata.implementation = address(new IncrementDecrementGet()); 935 | updatedExtension.functions = new ExtensionFunction[](1); 936 | updatedExtension.functions[0] = ExtensionFunction( 937 | IncrementDecrementGet.getNumber.selector, 938 | "getNumber()" 939 | ); 940 | 941 | // Call: replaceExtension 942 | vm.expectRevert("ExtensionManager: extension does not exist."); 943 | router.replaceExtension(updatedExtension); 944 | } 945 | 946 | /// @notice Revert: replace an extension with an empty implementation address. 947 | function test_revert_replaceExtension_emptyImplementation() public { 948 | // Create Extension struct 949 | Extension memory extension; 950 | 951 | // Set metadata 952 | extension.metadata.name = "IncrementDecrement"; 953 | extension.metadata.metadataURI = "ipfs://IncrementDecrement"; 954 | extension.metadata.implementation = address(new IncrementDecrement()); 955 | 956 | // Set functions 957 | extension.functions = new ExtensionFunction[](2); 958 | extension.functions[0] = ExtensionFunction( 959 | IncrementDecrementGet.incrementNumber.selector, 960 | "incrementNumber()" 961 | ); 962 | extension.functions[1] = ExtensionFunction( 963 | IncrementDecrementGet.decrementNumber.selector, 964 | "decrementNumber()" 965 | ); 966 | 967 | // Call: addExtension 968 | router.addExtension(extension); 969 | _validateExtensionDataOnContract(extension); 970 | 971 | IncrementDecrementGet inc = IncrementDecrementGet(address(router)); 972 | inc.incrementNumber(); 973 | inc.incrementNumber(); 974 | 975 | // Create Extension struct to replace existing extension 976 | Extension memory updatedExtension; 977 | 978 | updatedExtension.metadata.name = extension.metadata.name; 979 | updatedExtension.metadata.metadataURI = extension.metadata.metadataURI; 980 | updatedExtension.metadata.implementation = address(0); 981 | updatedExtension.functions = new ExtensionFunction[](1); 982 | updatedExtension.functions[0] = ExtensionFunction( 983 | IncrementDecrementGet.getNumber.selector, 984 | "getNumber()" 985 | ); 986 | 987 | // Call: replaceExtension 988 | vm.expectRevert("ExtensionManager: adding extension without implementation."); 989 | router.replaceExtension(updatedExtension); 990 | } 991 | 992 | /// @notice Revert: replace an extension with a function selector-signature mismatch. 993 | function test_revert_replaceExtension_fnSelectorSignatureMismatch() public { 994 | // Create Extension struct 995 | Extension memory extension; 996 | 997 | // Set metadata 998 | extension.metadata.name = "IncrementDecrement"; 999 | extension.metadata.metadataURI = "ipfs://IncrementDecrement"; 1000 | extension.metadata.implementation = address(new IncrementDecrement()); 1001 | 1002 | // Set functions 1003 | extension.functions = new ExtensionFunction[](2); 1004 | extension.functions[0] = ExtensionFunction( 1005 | IncrementDecrementGet.incrementNumber.selector, 1006 | "incrementNumber()" 1007 | ); 1008 | extension.functions[1] = ExtensionFunction( 1009 | IncrementDecrementGet.decrementNumber.selector, 1010 | "decrementNumber()" 1011 | ); 1012 | 1013 | // Call: addExtension 1014 | router.addExtension(extension); 1015 | _validateExtensionDataOnContract(extension); 1016 | 1017 | IncrementDecrementGet inc = IncrementDecrementGet(address(router)); 1018 | inc.incrementNumber(); 1019 | inc.incrementNumber(); 1020 | 1021 | // Create Extension struct to replace existing extension 1022 | Extension memory updatedExtension; 1023 | 1024 | updatedExtension.metadata.name = extension.metadata.name; 1025 | updatedExtension.metadata.metadataURI = extension.metadata.metadataURI; 1026 | updatedExtension.metadata.implementation = address(new IncrementDecrementGet()); 1027 | updatedExtension.functions = new ExtensionFunction[](1); 1028 | updatedExtension.functions[0] = ExtensionFunction( 1029 | IncrementDecrementGet.incrementNumber.selector, 1030 | "getNumber()" 1031 | ); 1032 | 1033 | // Call: replaceExtension 1034 | vm.expectRevert("ExtensionManager: fn selector and signature mismatch."); 1035 | router.replaceExtension(updatedExtension); 1036 | } 1037 | 1038 | /// @notice Revert: replace an extension with an empty function signature. 1039 | function test_revert_replaceExtension_emptyFunctionSignature() public { 1040 | // Create Extension struct 1041 | Extension memory extension; 1042 | 1043 | // Set metadata 1044 | extension.metadata.name = "IncrementDecrement"; 1045 | extension.metadata.metadataURI = "ipfs://IncrementDecrement"; 1046 | extension.metadata.implementation = address(new IncrementDecrement()); 1047 | 1048 | // Set functions 1049 | extension.functions = new ExtensionFunction[](2); 1050 | extension.functions[0] = ExtensionFunction( 1051 | IncrementDecrementGet.incrementNumber.selector, 1052 | "incrementNumber()" 1053 | ); 1054 | extension.functions[1] = ExtensionFunction( 1055 | IncrementDecrementGet.decrementNumber.selector, 1056 | "decrementNumber()" 1057 | ); 1058 | 1059 | // Call: addExtension 1060 | router.addExtension(extension); 1061 | _validateExtensionDataOnContract(extension); 1062 | 1063 | IncrementDecrementGet inc = IncrementDecrementGet(address(router)); 1064 | inc.incrementNumber(); 1065 | inc.incrementNumber(); 1066 | 1067 | // Create Extension struct to replace existing extension 1068 | Extension memory updatedExtension; 1069 | 1070 | updatedExtension.metadata.name = extension.metadata.name; 1071 | updatedExtension.metadata.metadataURI = extension.metadata.metadataURI; 1072 | updatedExtension.metadata.implementation = address(new IncrementDecrementGet()); 1073 | updatedExtension.functions = new ExtensionFunction[](1); 1074 | updatedExtension.functions[0] = ExtensionFunction( 1075 | IncrementDecrementGet.getNumber.selector, 1076 | "" 1077 | ); 1078 | 1079 | // Call: replaceExtension 1080 | vm.expectRevert("ExtensionManager: fn selector and signature mismatch."); 1081 | router.replaceExtension(updatedExtension); 1082 | } 1083 | 1084 | /// @notice Revert: replace an extension with an empty function selector. 1085 | function test_revert_replaceExtension_emptyFunctionSelector() public { 1086 | // Create Extension struct 1087 | Extension memory extension; 1088 | 1089 | // Set metadata 1090 | extension.metadata.name = "IncrementDecrement"; 1091 | extension.metadata.metadataURI = "ipfs://IncrementDecrement"; 1092 | extension.metadata.implementation = address(new IncrementDecrement()); 1093 | 1094 | // Set functions 1095 | extension.functions = new ExtensionFunction[](2); 1096 | extension.functions[0] = ExtensionFunction( 1097 | IncrementDecrementGet.incrementNumber.selector, 1098 | "incrementNumber()" 1099 | ); 1100 | extension.functions[1] = ExtensionFunction( 1101 | IncrementDecrementGet.decrementNumber.selector, 1102 | "decrementNumber()" 1103 | ); 1104 | 1105 | // Call: addExtension 1106 | router.addExtension(extension); 1107 | _validateExtensionDataOnContract(extension); 1108 | 1109 | IncrementDecrementGet inc = IncrementDecrementGet(address(router)); 1110 | inc.incrementNumber(); 1111 | inc.incrementNumber(); 1112 | 1113 | // Create Extension struct to replace existing extension 1114 | Extension memory updatedExtension; 1115 | 1116 | updatedExtension.metadata.name = extension.metadata.name; 1117 | updatedExtension.metadata.metadataURI = extension.metadata.metadataURI; 1118 | updatedExtension.metadata.implementation = address(new IncrementDecrementGet()); 1119 | updatedExtension.functions = new ExtensionFunction[](1); 1120 | updatedExtension.functions[0] = ExtensionFunction( 1121 | bytes4(0), 1122 | "getNumber()" 1123 | ); 1124 | 1125 | // Call: replaceExtension 1126 | vm.expectRevert("ExtensionManager: fn selector and signature mismatch."); 1127 | router.replaceExtension(updatedExtension); 1128 | } 1129 | 1130 | /// @notice Revert: replace an extension specifying the same function twice. 1131 | function test_revert_replaceExtension_duplicateFunction() public { 1132 | // Create Extension struct 1133 | Extension memory extension; 1134 | 1135 | // Set metadata 1136 | extension.metadata.name = "IncrementDecrement"; 1137 | extension.metadata.metadataURI = "ipfs://IncrementDecrement"; 1138 | extension.metadata.implementation = address(new IncrementDecrement()); 1139 | 1140 | // Set functions 1141 | extension.functions = new ExtensionFunction[](2); 1142 | extension.functions[0] = ExtensionFunction( 1143 | IncrementDecrementGet.incrementNumber.selector, 1144 | "incrementNumber()" 1145 | ); 1146 | extension.functions[1] = ExtensionFunction( 1147 | IncrementDecrementGet.decrementNumber.selector, 1148 | "decrementNumber()" 1149 | ); 1150 | 1151 | // Call: addExtension 1152 | router.addExtension(extension); 1153 | _validateExtensionDataOnContract(extension); 1154 | 1155 | IncrementDecrementGet inc = IncrementDecrementGet(address(router)); 1156 | inc.incrementNumber(); 1157 | inc.incrementNumber(); 1158 | 1159 | // Create Extension struct to replace existing extension 1160 | Extension memory updatedExtension; 1161 | 1162 | updatedExtension.metadata.name = extension.metadata.name; 1163 | updatedExtension.metadata.metadataURI = extension.metadata.metadataURI; 1164 | updatedExtension.metadata.implementation = address(new IncrementDecrementGet()); 1165 | 1166 | updatedExtension.functions = new ExtensionFunction[](2); 1167 | updatedExtension.functions[0] = ExtensionFunction( 1168 | IncrementDecrementGet.getNumber.selector, 1169 | "getNumber()" 1170 | ); 1171 | updatedExtension.functions[1] = ExtensionFunction( 1172 | IncrementDecrementGet.getNumber.selector, 1173 | "getNumber()" 1174 | ); 1175 | 1176 | // Call: replaceExtension 1177 | vm.expectRevert("ExtensionManager: function impl already exists."); 1178 | router.replaceExtension(updatedExtension); 1179 | } 1180 | 1181 | /// @notice Revert: replace an extension with a function that already exists in a default extension. 1182 | function test_revert_replaceExtension_fnAlreadyExistsInDefaultExtension() public { 1183 | // Create Extension struct 1184 | Extension memory extension; 1185 | 1186 | // Set metadata 1187 | extension.metadata.name = "IncrementDecrementGet"; 1188 | extension.metadata.metadataURI = "ipfs://IncrementDecrementGet"; 1189 | extension.metadata.implementation = address(new IncrementDecrementGet()); 1190 | 1191 | // Set functions 1192 | 1193 | extension.functions = new ExtensionFunction[](1); 1194 | extension.functions[0] = ExtensionFunction( 1195 | IncrementDecrementGet.getNumber.selector, 1196 | "getNumber()" 1197 | ); 1198 | 1199 | // Call: addExtension 1200 | router.addExtension(extension); 1201 | _validateExtensionDataOnContract(extension); 1202 | 1203 | // Create Extension struct to replace existing extension 1204 | Extension memory updatedExtension; 1205 | 1206 | // Set metadata 1207 | updatedExtension.metadata = extension.metadata; 1208 | updatedExtension.metadata.implementation = address(new IncrementDecrementMultiply()); 1209 | 1210 | updatedExtension.functions = new ExtensionFunction[](2); 1211 | updatedExtension.functions[0] = extension.functions[0]; 1212 | updatedExtension.functions[1] = defaultExtension1.functions[0]; 1213 | 1214 | // Call: addExtension 1215 | vm.expectRevert("ExtensionManager: function impl already exists."); 1216 | router.replaceExtension(updatedExtension); 1217 | } 1218 | 1219 | /// @notice Revert: replace an extension with a function that already exists in another non-default extension. 1220 | function test_revert_replaceExtension_fnAlreadyExistsInAnotherExtension() public { 1221 | // Create Extension struct 1222 | Extension memory extension1; 1223 | Extension memory extension2; 1224 | 1225 | // Set metadata 1226 | extension1.metadata.name = "IncrementDecrement"; 1227 | extension1.metadata.metadataURI = "ipfs://IncrementDecrement"; 1228 | extension1.metadata.implementation = address(new IncrementDecrement()); 1229 | 1230 | extension2.metadata.name = "IncrementDecrementGet"; 1231 | extension2.metadata.metadataURI = "ipfs://IncrementDecrementGet"; 1232 | extension2.metadata.implementation = address(new IncrementDecrementGet()); 1233 | 1234 | // Set functions 1235 | extension1.functions = new ExtensionFunction[](2); 1236 | extension1.functions[0] = ExtensionFunction( 1237 | IncrementDecrementGet.incrementNumber.selector, 1238 | "incrementNumber()" 1239 | ); 1240 | extension1.functions[1] = ExtensionFunction( 1241 | IncrementDecrementGet.decrementNumber.selector, 1242 | "decrementNumber()" 1243 | ); 1244 | 1245 | extension2.functions = new ExtensionFunction[](1); 1246 | extension2.functions[0] = ExtensionFunction( 1247 | IncrementDecrementGet.getNumber.selector, 1248 | "getNumber()" 1249 | ); 1250 | 1251 | // Call: addExtension 1252 | router.addExtension(extension1); 1253 | _validateExtensionDataOnContract(extension1); 1254 | 1255 | router.addExtension(extension2); 1256 | _validateExtensionDataOnContract(extension2); 1257 | 1258 | // Create Extension struct to replace existing extension 1259 | Extension memory updatedExtension1; 1260 | 1261 | updatedExtension1.metadata = extension1.metadata; 1262 | updatedExtension1.metadata.implementation = address(new IncrementDecrementGet()); 1263 | 1264 | updatedExtension1.functions = new ExtensionFunction[](3); 1265 | updatedExtension1.functions[0] = extension1.functions[0]; 1266 | updatedExtension1.functions[1] = extension1.functions[1]; 1267 | 1268 | // Already exists in extension2 1269 | updatedExtension1.functions[2] = ExtensionFunction( 1270 | IncrementDecrementGet.getNumber.selector, 1271 | "getNumber()" 1272 | ); 1273 | 1274 | // Call: addExtension 1275 | vm.expectRevert("ExtensionManager: function impl already exists."); 1276 | router.replaceExtension(updatedExtension1); 1277 | } 1278 | 1279 | /*/////////////////////////////////////////////////////////////// 1280 | Removing extensions 1281 | //////////////////////////////////////////////////////////////*/ 1282 | 1283 | /// @notice Remove a default extension. 1284 | function test_state_removeExtension_defautlExtension() public { 1285 | // Call: removeExtension 1286 | 1287 | assertEq(router.getAllExtensions().length, defaultExtensionsCount); 1288 | 1289 | router.removeExtension(defaultExtension1.metadata.name); 1290 | assertEq(router.getAllExtensions().length, defaultExtensionsCount - 1); 1291 | 1292 | assertEq(router.getImplementationForFunction(MultiplyDivide.multiplyNumber.selector), address(0)); 1293 | assertEq(router.getImplementationForFunction(MultiplyDivide.divideNumber.selector), address(0)); 1294 | 1295 | Extension memory ext = router.getExtension(defaultExtension1.metadata.name); 1296 | assertEq(ext.metadata.name, ""); 1297 | assertEq(ext.metadata.metadataURI, ""); 1298 | assertEq(ext.metadata.implementation, address(0)); 1299 | assertEq(ext.functions.length, 0); 1300 | } 1301 | 1302 | /// @notice Remove a non-default extension. 1303 | function test_state_removeExtension() public { 1304 | // Create Extension struct 1305 | Extension memory extension; 1306 | 1307 | // Set metadata 1308 | extension.metadata.name = "IncrementDecrement"; 1309 | extension.metadata.metadataURI = "ipfs://IncrementDecrement"; 1310 | extension.metadata.implementation = address(new IncrementDecrement()); 1311 | 1312 | // Set functions 1313 | extension.functions = new ExtensionFunction[](2); 1314 | extension.functions[0] = ExtensionFunction( 1315 | IncrementDecrementGet.incrementNumber.selector, 1316 | "incrementNumber()" 1317 | ); 1318 | extension.functions[1] = ExtensionFunction( 1319 | IncrementDecrementGet.decrementNumber.selector, 1320 | "decrementNumber()" 1321 | ); 1322 | 1323 | // Call: addExtension 1324 | router.addExtension(extension); 1325 | _validateExtensionDataOnContract(extension); 1326 | 1327 | // Create Extension struct to replace existing extension 1328 | Extension memory updatedExtension; 1329 | 1330 | updatedExtension.metadata = extension.metadata; 1331 | updatedExtension.metadata.implementation = address(new IncrementDecrementGet()); 1332 | 1333 | updatedExtension.functions = new ExtensionFunction[](3); 1334 | updatedExtension.functions[0] = extension.functions[0]; 1335 | updatedExtension.functions[1] = extension.functions[1]; 1336 | updatedExtension.functions[2] = ExtensionFunction( 1337 | IncrementDecrementGet.getNumber.selector, 1338 | "getNumber()" 1339 | ); 1340 | 1341 | // Call: replaceExtension 1342 | router.replaceExtension(updatedExtension); 1343 | _validateExtensionDataOnContract(updatedExtension); 1344 | 1345 | // Call: removeExtension 1346 | assertEq(router.getAllExtensions().length, defaultExtensionsCount + 1); 1347 | 1348 | router.removeExtension(updatedExtension.metadata.name); 1349 | assertEq(router.getAllExtensions().length, defaultExtensionsCount); 1350 | 1351 | assertEq(router.getImplementationForFunction(IncrementDecrementGet.incrementNumber.selector), address(0)); 1352 | assertEq(router.getImplementationForFunction(IncrementDecrementGet.decrementNumber.selector), address(0)); 1353 | assertEq(router.getImplementationForFunction(IncrementDecrementGet.getNumber.selector), address(0)); 1354 | 1355 | Extension memory ext = router.getExtension(updatedExtension.metadata.name); 1356 | assertEq(ext.metadata.name, ""); 1357 | assertEq(ext.metadata.metadataURI, ""); 1358 | assertEq(ext.metadata.implementation, address(0)); 1359 | assertEq(ext.functions.length, 0); 1360 | } 1361 | 1362 | /// @notice Revert: remove a non existent extension. 1363 | function test_revert_removeExtension_extensionDoesNotExist() public { 1364 | vm.expectRevert("ExtensionManager: extension does not exist."); 1365 | router.removeExtension("SomeExtension"); 1366 | } 1367 | 1368 | /// @notice Revert: remove an extension with an empty name. 1369 | function test_revert_removeExtension_emptyName() public { 1370 | vm.expectRevert("ExtensionManager: extension does not exist."); 1371 | router.removeExtension(""); 1372 | } 1373 | 1374 | /*/////////////////////////////////////////////////////////////// 1375 | Disabling function in extension 1376 | //////////////////////////////////////////////////////////////*/ 1377 | 1378 | /// @notice Disable a function in a default extension. 1379 | function test_state_disableFunctionInExtension_defaultExtension() public { 1380 | 1381 | // Call: disableFunctionInExtension 1382 | router.disableFunctionInExtension(defaultExtension1.metadata.name, defaultExtension1.functions[0].functionSelector); 1383 | 1384 | // Post call checks 1385 | assertEq(router.getImplementationForFunction(MultiplyDivide.multiplyNumber.selector), address(0)); 1386 | assertEq(router.getExtension(defaultExtension1.metadata.name).functions.length, 1); 1387 | 1388 | Extension memory updatedExtension; 1389 | updatedExtension.metadata = defaultExtension1.metadata; 1390 | updatedExtension.functions = new ExtensionFunction[](1); 1391 | updatedExtension.functions[0] = defaultExtension1.functions[1]; 1392 | 1393 | _validateExtensionDataOnContract(updatedExtension); 1394 | } 1395 | 1396 | /// @notice Disable a function in a non-default extension. 1397 | function test_state_disableFunctionInExtension() public { 1398 | // Create Extension struct 1399 | Extension memory extension; 1400 | 1401 | // Set metadata 1402 | extension.metadata.name = "IncrementDecrement"; 1403 | extension.metadata.metadataURI = "ipfs://IncrementDecrement"; 1404 | extension.metadata.implementation = address(new IncrementDecrementGet()); 1405 | 1406 | // Set functions 1407 | extension.functions = new ExtensionFunction[](2); 1408 | 1409 | extension.functions[0] = ExtensionFunction( 1410 | IncrementDecrementGet.incrementNumber.selector, 1411 | "incrementNumber()" 1412 | ); 1413 | extension.functions[1] = ExtensionFunction( 1414 | IncrementDecrementGet.decrementNumber.selector, 1415 | "decrementNumber()" 1416 | ); 1417 | 1418 | // Call: addExtension 1419 | router.addExtension(extension); 1420 | _validateExtensionDataOnContract(extension); 1421 | 1422 | // Pre-call checks 1423 | assertEq(router.getImplementationForFunction(IncrementDecrementGet.incrementNumber.selector), extension.metadata.implementation); 1424 | assertEq(router.getExtension(extension.metadata.name).functions.length, 2); 1425 | 1426 | // Call: disableFunctionInExtension 1427 | router.disableFunctionInExtension(extension.metadata.name, IncrementDecrementGet.incrementNumber.selector); 1428 | 1429 | // Post call checks 1430 | assertEq(router.getImplementationForFunction(IncrementDecrementGet.incrementNumber.selector), address(0)); 1431 | assertEq(router.getExtension(extension.metadata.name).functions.length, 1); 1432 | 1433 | Extension memory updatedExtension; 1434 | updatedExtension.metadata = extension.metadata; 1435 | updatedExtension.functions = new ExtensionFunction[](1); 1436 | updatedExtension.functions[0] = extension.functions[1]; 1437 | 1438 | _validateExtensionDataOnContract(updatedExtension); 1439 | } 1440 | 1441 | /// @notice Disable the receive function. 1442 | function test_state_disableFunctionInExtension_receiveFunction() public { 1443 | // Create Extension struct 1444 | Extension memory extension; 1445 | 1446 | // Set metadata 1447 | extension.metadata.name = "IncrementDecrementReceive"; 1448 | extension.metadata.metadataURI = "ipfs://IncrementDecrementReceive"; 1449 | extension.metadata.implementation = address(new IncrementDecrementReceive()); 1450 | 1451 | // Set functions 1452 | extension.functions = new ExtensionFunction[](2); 1453 | 1454 | extension.functions[0] = ExtensionFunction( 1455 | bytes4(0), 1456 | "receive()" 1457 | ); 1458 | extension.functions[1] = ExtensionFunction( 1459 | IncrementDecrementGet.decrementNumber.selector, 1460 | "decrementNumber()" 1461 | ); 1462 | 1463 | // Call: addExtension 1464 | router.addExtension(extension); 1465 | _validateExtensionDataOnContract(extension); 1466 | 1467 | address sender = address(0x123); 1468 | vm.deal(sender, 100 ether); 1469 | 1470 | uint256 balBefore = (address(router)).balance; 1471 | uint256 amount = 1 ether; 1472 | 1473 | vm.prank(sender); 1474 | address(router).call{value: 1 ether}(""); 1475 | 1476 | assertEq((address(router)).balance, balBefore + amount); 1477 | 1478 | // Pre-call checks 1479 | assertEq(router.getImplementationForFunction(bytes4(0)), extension.metadata.implementation); 1480 | assertEq(router.getExtension(extension.metadata.name).functions.length, 2); 1481 | 1482 | // Call: disableFunctionInExtension 1483 | router.disableFunctionInExtension(extension.metadata.name, bytes4(0)); 1484 | 1485 | // Post call checks 1486 | assertEq(router.getImplementationForFunction(bytes4(0)), address(0)); 1487 | assertEq(router.getExtension(extension.metadata.name).functions.length, 1); 1488 | 1489 | Extension memory updatedExtension; 1490 | updatedExtension.metadata = extension.metadata; 1491 | updatedExtension.functions = new ExtensionFunction[](1); 1492 | updatedExtension.functions[0] = extension.functions[1]; 1493 | 1494 | _validateExtensionDataOnContract(updatedExtension); 1495 | 1496 | vm.expectRevert(); 1497 | vm.prank(sender); 1498 | address(router).call{value: 1 ether}(""); 1499 | } 1500 | 1501 | /// @notice Revert: disable a function in a non existent extension. 1502 | function test_revert_disableFunctionInExtension_extensionDoesNotExist() public { 1503 | // Call: disableFunctionInExtension 1504 | vm.expectRevert("ExtensionManager: extension does not exist."); 1505 | router.disableFunctionInExtension("SomeExtension", IncrementDecrementGet.incrementNumber.selector); 1506 | } 1507 | 1508 | /// @notice Revert: disable a function in an extension with an empty name. 1509 | function test_revert_disableFunctionInExtension_emptyName() public { 1510 | // Call: disableFunctionInExtension 1511 | vm.expectRevert("ExtensionManager: extension does not exist."); 1512 | router.disableFunctionInExtension("", IncrementDecrementGet.incrementNumber.selector); 1513 | } 1514 | 1515 | /// @notice Revert: disable a function in an extension that does not have that function. 1516 | function test_revert_disableFunctionInExtension_functionDoesNotExistInExtension() public { 1517 | // Create Extension struct 1518 | Extension memory extension; 1519 | 1520 | // Set metadata 1521 | extension.metadata.name = "IncrementDecrement"; 1522 | extension.metadata.metadataURI = "ipfs://IncrementDecrement"; 1523 | extension.metadata.implementation = address(new IncrementDecrementGet()); 1524 | 1525 | // Set functions 1526 | extension.functions = new ExtensionFunction[](2); 1527 | 1528 | extension.functions[0] = ExtensionFunction( 1529 | IncrementDecrementGet.incrementNumber.selector, 1530 | "incrementNumber()" 1531 | ); 1532 | extension.functions[1] = ExtensionFunction( 1533 | IncrementDecrementGet.decrementNumber.selector, 1534 | "decrementNumber()" 1535 | ); 1536 | 1537 | // Call: addExtension 1538 | router.addExtension(extension); 1539 | _validateExtensionDataOnContract(extension); 1540 | 1541 | // Pre-call checks 1542 | assertEq(router.getImplementationForFunction(IncrementDecrementGet.incrementNumber.selector), extension.metadata.implementation); 1543 | assertEq(router.getExtension(extension.metadata.name).functions.length, 2); 1544 | 1545 | // Call: disableFunctionInExtension 1546 | vm.expectRevert("ExtensionManager: incorrect extension."); 1547 | router.disableFunctionInExtension(extension.metadata.name, IncrementDecrementGet.getNumber.selector); 1548 | } 1549 | 1550 | /*/////////////////////////////////////////////////////////////// 1551 | Enable function in extension 1552 | //////////////////////////////////////////////////////////////*/ 1553 | 1554 | /// @notice Enable a function in a default extension. 1555 | function test_state_enableFunctionInExtension_defaultExtension() public { 1556 | // Call: disableFunctionInExtension 1557 | router.disableFunctionInExtension(defaultExtension1.metadata.name, defaultExtension1.functions[0].functionSelector); 1558 | 1559 | assertEq(router.getImplementationForFunction(MultiplyDivide.multiplyNumber.selector), address(0)); 1560 | assertEq(router.getExtension(defaultExtension1.metadata.name).functions.length, defaultExtension1.functions.length - 1); 1561 | 1562 | // Call: enableFunctionInExtension 1563 | router.enableFunctionInExtension(defaultExtension1.metadata.name, defaultExtension1.functions[0]); 1564 | 1565 | // Post call checks 1566 | assertEq(router.getImplementationForFunction(defaultExtension1.functions[0].functionSelector), defaultExtension1.metadata.implementation); 1567 | assertEq(router.getExtension(defaultExtension1.metadata.name).functions.length, defaultExtension1.functions.length); 1568 | 1569 | Extension memory updatedExtension; 1570 | updatedExtension.metadata = defaultExtension1.metadata; 1571 | updatedExtension.functions = new ExtensionFunction[](2); 1572 | updatedExtension.functions[0] = defaultExtension1.functions[1]; 1573 | updatedExtension.functions[1] = defaultExtension1.functions[0]; 1574 | 1575 | _validateExtensionDataOnContract(updatedExtension); 1576 | } 1577 | 1578 | /// @notice Enable a function in a non-default extension. 1579 | function test_state_enableFunctionInExtension() public { 1580 | // Create Extension struct 1581 | Extension memory extension; 1582 | 1583 | // Set metadata 1584 | extension.metadata.name = "IncrementDecrement"; 1585 | extension.metadata.metadataURI = "ipfs://IncrementDecrement"; 1586 | extension.metadata.implementation = address(new IncrementDecrementGet()); 1587 | 1588 | // Set functions 1589 | extension.functions = new ExtensionFunction[](2); 1590 | 1591 | extension.functions[0] = ExtensionFunction( 1592 | IncrementDecrementGet.incrementNumber.selector, 1593 | "incrementNumber()" 1594 | ); 1595 | extension.functions[1] = ExtensionFunction( 1596 | IncrementDecrementGet.decrementNumber.selector, 1597 | "decrementNumber()" 1598 | ); 1599 | 1600 | // Call: addExtension 1601 | router.addExtension(extension); 1602 | _validateExtensionDataOnContract(extension); 1603 | 1604 | // Pre-call checks 1605 | assertEq(router.getImplementationForFunction(IncrementDecrementGet.getNumber.selector), address(0)); 1606 | assertEq(router.getExtension(extension.metadata.name).functions.length, 2); 1607 | 1608 | // Call: enableFunctionInExtension 1609 | ExtensionFunction memory fn = ExtensionFunction( 1610 | IncrementDecrementGet.getNumber.selector, 1611 | "getNumber()" 1612 | ); 1613 | router.enableFunctionInExtension(extension.metadata.name, fn); 1614 | 1615 | // Post call checks 1616 | assertEq(router.getImplementationForFunction(IncrementDecrementGet.getNumber.selector), extension.metadata.implementation); 1617 | assertEq(router.getExtension(extension.metadata.name).functions.length, 3); 1618 | 1619 | Extension memory updatedExtension; 1620 | updatedExtension.metadata = extension.metadata; 1621 | updatedExtension.functions = new ExtensionFunction[](3); 1622 | updatedExtension.functions[0] = extension.functions[0]; 1623 | updatedExtension.functions[1] = extension.functions[1]; 1624 | updatedExtension.functions[2] = fn; 1625 | 1626 | _validateExtensionDataOnContract(updatedExtension); 1627 | 1628 | // Verify functionality 1629 | IncrementDecrementGet inc = IncrementDecrementGet(address(router)); 1630 | 1631 | assertEq(inc.getNumber(), 0); 1632 | 1633 | inc.incrementNumber(); 1634 | assertEq(inc.getNumber(), 1); 1635 | 1636 | inc.incrementNumber(); 1637 | assertEq(inc.getNumber(), 2); 1638 | 1639 | inc.decrementNumber(); 1640 | assertEq(inc.getNumber(), 1); 1641 | } 1642 | 1643 | /// @notice Enable the receive function. 1644 | function test_state_enableFunctionInExtension_receiveFunction() public { 1645 | // Create Extension struct 1646 | Extension memory extension; 1647 | 1648 | // Set metadata 1649 | extension.metadata.name = "IncrementDecrementReceive"; 1650 | extension.metadata.metadataURI = "ipfs://IncrementDecrementReceive"; 1651 | extension.metadata.implementation = address(new IncrementDecrementReceive()); 1652 | 1653 | // Set functions 1654 | extension.functions = new ExtensionFunction[](2); 1655 | 1656 | extension.functions[0] = ExtensionFunction( 1657 | IncrementDecrementReceive.incrementNumber.selector, 1658 | "incrementNumber()" 1659 | ); 1660 | extension.functions[1] = ExtensionFunction( 1661 | IncrementDecrementReceive.decrementNumber.selector, 1662 | "decrementNumber()" 1663 | ); 1664 | 1665 | // Call: addExtension 1666 | router.addExtension(extension); 1667 | _validateExtensionDataOnContract(extension); 1668 | 1669 | // Pre-call checks 1670 | assertEq(router.getImplementationForFunction(bytes4(0)), address(0)); 1671 | assertEq(router.getExtension(extension.metadata.name).functions.length, 2); 1672 | 1673 | address sender = address(0x123); 1674 | vm.deal(sender, 100 ether); 1675 | 1676 | vm.expectRevert(); 1677 | vm.prank(sender); 1678 | address(router).call{value: 1 ether}(""); 1679 | 1680 | // Call: enableFunctionInExtension 1681 | ExtensionFunction memory fn = ExtensionFunction( 1682 | bytes4(0), 1683 | "receive()" 1684 | ); 1685 | router.enableFunctionInExtension(extension.metadata.name, fn); 1686 | 1687 | // Post call checks 1688 | assertEq(router.getImplementationForFunction(bytes4(0)), extension.metadata.implementation); 1689 | assertEq(router.getExtension(extension.metadata.name).functions.length, 3); 1690 | 1691 | Extension memory updatedExtension; 1692 | updatedExtension.metadata = extension.metadata; 1693 | updatedExtension.functions = new ExtensionFunction[](3); 1694 | updatedExtension.functions[0] = extension.functions[0]; 1695 | updatedExtension.functions[1] = extension.functions[1]; 1696 | updatedExtension.functions[2] = fn; 1697 | 1698 | _validateExtensionDataOnContract(updatedExtension); 1699 | 1700 | // Verify functionality 1701 | uint256 balBefore = (address(router)).balance; 1702 | uint256 amount = 1 ether; 1703 | 1704 | vm.prank(sender); 1705 | address(router).call{value: 1 ether}(""); 1706 | 1707 | assertEq((address(router)).balance, balBefore + amount); 1708 | } 1709 | 1710 | /// @notice Revert: enable a function in a non existent extension. 1711 | function test_revert_enableFunctionInExtension_extensionDoesNotExist() public { 1712 | // Call: enableFunctionInExtension 1713 | ExtensionFunction memory fn = ExtensionFunction( 1714 | IncrementDecrementGet.getNumber.selector, 1715 | "getNumber()" 1716 | ); 1717 | 1718 | vm.expectRevert("ExtensionManager: extension does not exist."); 1719 | router.enableFunctionInExtension("SomeExtension", fn); 1720 | } 1721 | 1722 | /// @notice Revert: enable a function in an extension with an empty name. 1723 | function test_revert_enableFunctionInExtension_emptyName() public { 1724 | // Call: enableFunctionInExtension 1725 | ExtensionFunction memory fn = ExtensionFunction( 1726 | IncrementDecrementGet.getNumber.selector, 1727 | "getNumber()" 1728 | ); 1729 | 1730 | vm.expectRevert("ExtensionManager: extension does not exist."); 1731 | router.enableFunctionInExtension("", fn); 1732 | } 1733 | 1734 | /// @notice Revert: enable a function with empty function signature. 1735 | function test_revert_enableFunctionInExtension_emptyFunctionSignature() public { 1736 | // Create Extension struct 1737 | Extension memory extension; 1738 | 1739 | // Set metadata 1740 | extension.metadata.name = "IncrementDecrement"; 1741 | extension.metadata.metadataURI = "ipfs://IncrementDecrement"; 1742 | extension.metadata.implementation = address(new IncrementDecrementGet()); 1743 | 1744 | // Set functions 1745 | extension.functions = new ExtensionFunction[](2); 1746 | 1747 | extension.functions[0] = ExtensionFunction( 1748 | IncrementDecrementGet.incrementNumber.selector, 1749 | "incrementNumber()" 1750 | ); 1751 | extension.functions[1] = ExtensionFunction( 1752 | IncrementDecrementGet.decrementNumber.selector, 1753 | "decrementNumber()" 1754 | ); 1755 | 1756 | // Call: addExtension 1757 | router.addExtension(extension); 1758 | _validateExtensionDataOnContract(extension); 1759 | 1760 | // Pre-call checks 1761 | assertEq(router.getImplementationForFunction(IncrementDecrementGet.getNumber.selector), address(0)); 1762 | assertEq(router.getExtension(extension.metadata.name).functions.length, 2); 1763 | 1764 | // Call: enableFunctionInExtension 1765 | ExtensionFunction memory fn = ExtensionFunction( 1766 | IncrementDecrementGet.getNumber.selector, 1767 | "" 1768 | ); 1769 | 1770 | vm.expectRevert("ExtensionManager: fn selector and signature mismatch."); 1771 | router.enableFunctionInExtension(extension.metadata.name, fn); 1772 | } 1773 | 1774 | /// @notice Revert: enable a function with empty function selector. 1775 | function test_revert_enableFunctionInExtension_emptyFunctionSelector() public { 1776 | // Create Extension struct 1777 | Extension memory extension; 1778 | 1779 | // Set metadata 1780 | extension.metadata.name = "IncrementDecrement"; 1781 | extension.metadata.metadataURI = "ipfs://IncrementDecrement"; 1782 | extension.metadata.implementation = address(new IncrementDecrementGet()); 1783 | 1784 | // Set functions 1785 | extension.functions = new ExtensionFunction[](2); 1786 | 1787 | extension.functions[0] = ExtensionFunction( 1788 | IncrementDecrementGet.incrementNumber.selector, 1789 | "incrementNumber()" 1790 | ); 1791 | extension.functions[1] = ExtensionFunction( 1792 | IncrementDecrementGet.decrementNumber.selector, 1793 | "decrementNumber()" 1794 | ); 1795 | 1796 | // Call: addExtension 1797 | router.addExtension(extension); 1798 | _validateExtensionDataOnContract(extension); 1799 | 1800 | // Pre-call checks 1801 | assertEq(router.getImplementationForFunction(IncrementDecrementGet.getNumber.selector), address(0)); 1802 | assertEq(router.getExtension(extension.metadata.name).functions.length, 2); 1803 | 1804 | // Call: enableFunctionInExtension 1805 | ExtensionFunction memory fn = ExtensionFunction( 1806 | bytes4(0), 1807 | "getNumber()" 1808 | ); 1809 | 1810 | vm.expectRevert("ExtensionManager: fn selector and signature mismatch."); 1811 | router.enableFunctionInExtension(extension.metadata.name, fn); 1812 | } 1813 | 1814 | /// @notice Revert: enable a function that already exists in another extension. 1815 | function test_revert_enableFunctionInExtension_functionAlreadyExistsInAnotherExtension() public { 1816 | // Create Extension struct 1817 | Extension memory extension1; 1818 | Extension memory extension2; 1819 | 1820 | // Set metadata 1821 | extension1.metadata.name = "IncrementDecrement"; 1822 | extension1.metadata.metadataURI = "ipfs://IncrementDecrement"; 1823 | extension1.metadata.implementation = address(new IncrementDecrement()); 1824 | 1825 | extension2.metadata.name = "IncrementDecrementGet"; 1826 | extension2.metadata.metadataURI = "ipfs://IncrementDecrementGet"; 1827 | extension2.metadata.implementation = address(new IncrementDecrementGet()); 1828 | 1829 | // Set functions 1830 | extension1.functions = new ExtensionFunction[](2); 1831 | extension1.functions[0] = ExtensionFunction( 1832 | IncrementDecrementGet.incrementNumber.selector, 1833 | "incrementNumber()" 1834 | ); 1835 | extension1.functions[1] = ExtensionFunction( 1836 | IncrementDecrementGet.decrementNumber.selector, 1837 | "decrementNumber()" 1838 | ); 1839 | 1840 | extension2.functions = new ExtensionFunction[](1); 1841 | extension2.functions[0] = ExtensionFunction( 1842 | IncrementDecrementGet.getNumber.selector, 1843 | "getNumber()" 1844 | ); 1845 | 1846 | // Call: addExtension 1847 | router.addExtension(extension1); 1848 | router.addExtension(extension2); 1849 | 1850 | // Call: enableFunctionInExtension 1851 | ExtensionFunction memory fn = ExtensionFunction( 1852 | IncrementDecrementGet.getNumber.selector, 1853 | "getNumber()" 1854 | ); 1855 | 1856 | vm.expectRevert("ExtensionManager: function impl already exists."); 1857 | router.enableFunctionInExtension(extension1.metadata.name, fn); 1858 | } 1859 | 1860 | /// @notice Revert: enable a function that already exists in a default extension. 1861 | function test_revert_enableFunctionInExtension_functionAlreadyExistsInDefaultExtension() public { 1862 | // Create Extension struct 1863 | Extension memory extension; 1864 | 1865 | // Set metadata 1866 | extension.metadata.name = "IncrementDecrementMultiply"; 1867 | extension.metadata.metadataURI = "ipfs://IncrementDecrementMultiply"; 1868 | extension.metadata.implementation = address(new IncrementDecrementMultiply()); 1869 | 1870 | // Set functions 1871 | extension.functions = new ExtensionFunction[](2); 1872 | extension.functions[0] = ExtensionFunction( 1873 | IncrementDecrementGet.incrementNumber.selector, 1874 | "incrementNumber()" 1875 | ); 1876 | extension.functions[1] = ExtensionFunction( 1877 | IncrementDecrementGet.decrementNumber.selector, 1878 | "decrementNumber()" 1879 | ); 1880 | 1881 | // Call: addExtension 1882 | router.addExtension(extension); 1883 | 1884 | // Call: enableFunctionInExtension 1885 | ExtensionFunction memory fn = ExtensionFunction( 1886 | MultiplyDivide.multiplyNumber.selector, 1887 | "multiplyNumber(uint256)" 1888 | ); 1889 | 1890 | vm.expectRevert("ExtensionManager: function impl already exists."); 1891 | router.enableFunctionInExtension(extension.metadata.name, fn); 1892 | } 1893 | 1894 | /*/////////////////////////////////////////////////////////////// 1895 | Scenario tests 1896 | //////////////////////////////////////////////////////////////*/ 1897 | 1898 | /// The following tests are for scenarios that may occur in production use of a base router. 1899 | 1900 | /// @notice Upgrade a buggy function in a default extension. 1901 | function test_scenario_upgradeBuggyFunction_defaultExtension() public { 1902 | // Disable buggy function in extension 1903 | router.disableFunctionInExtension(defaultExtension1.metadata.name, defaultExtension1.functions[0].functionSelector); 1904 | 1905 | assertEq(router.getImplementationForFunction(MultiplyDivide.multiplyNumber.selector), address(0)); 1906 | assertEq(router.getExtension(defaultExtension1.metadata.name).functions.length, defaultExtension1.functions.length - 1); 1907 | 1908 | // Create new extension with fixed function 1909 | Extension memory extension; 1910 | 1911 | // Set metadata 1912 | extension.metadata.name = "MultiplyDivide-Fixed-Multiply"; 1913 | extension.metadata.metadataURI = "ipfs://MultiplyDivide-Fixed-Multiply"; 1914 | extension.metadata.implementation = address(new MultiplyDivide()); 1915 | 1916 | // Set functions 1917 | extension.functions = new ExtensionFunction[](1); 1918 | extension.functions[0] = ExtensionFunction( 1919 | MultiplyDivide.multiplyNumber.selector, 1920 | "multiplyNumber(uint256)" 1921 | ); 1922 | 1923 | // Call: addExtension 1924 | router.addExtension(extension); 1925 | 1926 | // Post call checks 1927 | assertEq(router.getImplementationForFunction(MultiplyDivide.multiplyNumber.selector), extension.metadata.implementation); 1928 | assertEq(router.getExtension(defaultExtension1.metadata.name).functions.length, extension.functions.length); 1929 | 1930 | Extension memory updatedExtension; 1931 | updatedExtension.metadata = defaultExtension1.metadata; 1932 | updatedExtension.functions = new ExtensionFunction[](1); 1933 | updatedExtension.functions[0] = defaultExtension1.functions[1]; 1934 | 1935 | _validateExtensionDataOnContract(updatedExtension); 1936 | _validateExtensionDataOnContract(extension); 1937 | } 1938 | 1939 | /// @notice Upgrade a buggy function in a non-default extension. 1940 | function test_scenario_upgradeBuggyFunction() public { 1941 | // Add extension with buggy function 1942 | Extension memory extension; 1943 | 1944 | extension.metadata.name = "IncrementDecrement"; 1945 | extension.metadata.metadataURI = "ipfs://IncrementDecrement"; 1946 | extension.metadata.implementation = address(new IncrementDecrementGet()); 1947 | 1948 | extension.functions = new ExtensionFunction[](3); 1949 | 1950 | extension.functions[0] = ExtensionFunction( 1951 | IncrementDecrementGet.incrementNumber.selector, 1952 | "incrementNumber()" 1953 | ); 1954 | extension.functions[1] = ExtensionFunction( 1955 | IncrementDecrementGet.decrementNumber.selector, 1956 | "decrementNumber()" 1957 | ); 1958 | extension.functions[2] = ExtensionFunction( 1959 | IncrementDecrementGet.getNumber.selector, 1960 | "getNumber()" 1961 | ); 1962 | 1963 | // Call: addExtension 1964 | router.addExtension(extension); 1965 | 1966 | // Disable buggy function in extension 1967 | router.disableFunctionInExtension(extension.metadata.name, IncrementDecrementGet.getNumber.selector); 1968 | 1969 | assertEq(router.getImplementationForFunction(IncrementDecrementGet.getNumber.selector), address(0)); 1970 | assertEq(router.getExtension(extension.metadata.name).functions.length, extension.functions.length - 1); 1971 | 1972 | // Create new extension with fixed function 1973 | Extension memory updatedExtension; 1974 | updatedExtension.metadata = extension.metadata; 1975 | updatedExtension.metadata.name = "IncrementDecrement-getNumber-fixed"; 1976 | updatedExtension.functions = new ExtensionFunction[](1); 1977 | updatedExtension.functions[0] = ExtensionFunction( 1978 | IncrementDecrementGet.getNumber.selector, 1979 | "getNumber()" 1980 | ); 1981 | 1982 | // Call: addExtension 1983 | router.addExtension(updatedExtension); 1984 | 1985 | // Post call checks 1986 | 1987 | assertEq(router.getImplementationForFunction(IncrementDecrementGet.getNumber.selector), updatedExtension.metadata.implementation); 1988 | assertEq(router.getExtension(extension.metadata.name).functions.length, updatedExtension.functions.length); 1989 | 1990 | _validateExtensionDataOnContract(updatedExtension); 1991 | } 1992 | } -------------------------------------------------------------------------------- /test/BaseRouterBenchmark.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // @author: thirdweb (https://github.com/thirdweb-dev/dynamic-contracts) 3 | 4 | pragma solidity ^0.8.0; 5 | 6 | import "forge-std/Test.sol"; 7 | 8 | import "src/interface/IExtension.sol"; 9 | import "src/presets/BaseRouter.sol"; 10 | import "./utils/MockContracts.sol"; 11 | import "./utils/Strings.sol"; 12 | 13 | /// @dev This custom router is written only for testing purposes and must not be used in production. 14 | contract CustomRouter is BaseRouter { 15 | 16 | constructor(Extension[] memory _extensions) BaseRouter(_extensions) {} 17 | 18 | function initialize() public { 19 | __BaseRouter_init(); 20 | } 21 | 22 | /// @dev Returns whether a function can be disabled in an extension in the given execution context. 23 | function _isAuthorizedCallToUpgrade() internal view virtual override returns (bool) { 24 | return true; 25 | } 26 | } 27 | 28 | contract BaseRouterBenchmarkTest is Test, IExtension { 29 | 30 | using Strings for uint256; 31 | 32 | BaseRouter internal router; 33 | 34 | Extension internal defaultExtension1; 35 | Extension internal defaultExtension2; 36 | Extension internal defaultExtension3; 37 | Extension internal defaultExtension4; 38 | Extension internal defaultExtension5; 39 | 40 | uint256 internal defaultExtensionsCount = 2; 41 | 42 | function setUp() public virtual { 43 | 44 | // Set metadata 45 | defaultExtension1.metadata.name = "MultiplyDivide"; 46 | defaultExtension1.metadata.metadataURI = "ipfs://MultiplyDivide"; 47 | defaultExtension1.metadata.implementation = address(new MultiplyDivide()); 48 | 49 | defaultExtension2.metadata.name = "AddSubstract"; 50 | defaultExtension2.metadata.metadataURI = "ipfs://AddSubstract"; 51 | defaultExtension2.metadata.implementation = address(new AddSubstract()); 52 | 53 | defaultExtension3.metadata.name = "RandomExtension"; 54 | defaultExtension3.metadata.metadataURI = "ipfs://RandomExtension"; 55 | defaultExtension3.metadata.implementation = address(0x3456); 56 | 57 | defaultExtension4.metadata.name = "RandomExtension2"; 58 | defaultExtension4.metadata.metadataURI = "ipfs://RandomExtension2"; 59 | defaultExtension4.metadata.implementation = address(0x5678); 60 | 61 | defaultExtension5.metadata.name = "RandomExtension3"; 62 | defaultExtension5.metadata.metadataURI = "ipfs://RandomExtension3"; 63 | defaultExtension5.metadata.implementation = address(0x7890); 64 | 65 | // Set functions 66 | 67 | defaultExtension1.functions.push(ExtensionFunction( 68 | MultiplyDivide.multiplyNumber.selector, 69 | "multiplyNumber(uint256)" 70 | )); 71 | defaultExtension1.functions.push(ExtensionFunction( 72 | MultiplyDivide.divideNumber.selector, 73 | "divideNumber(uint256)" 74 | )); 75 | defaultExtension2.functions.push(ExtensionFunction( 76 | AddSubstract.addNumber.selector, 77 | "addNumber(uint256)" 78 | )); 79 | defaultExtension2.functions.push(ExtensionFunction( 80 | AddSubstract.subtractNumber.selector, 81 | "subtractNumber(uint256)" 82 | )); 83 | 84 | for(uint256 i = 0; i < 10; i++) { 85 | string memory functionSignature = string(abi.encodePacked("randomFunction", i.toString(), "(uint256)")); 86 | bytes4 selector = bytes4(keccak256(bytes(functionSignature))); 87 | defaultExtension3.functions.push(ExtensionFunction( 88 | selector, 89 | functionSignature 90 | )); 91 | } 92 | 93 | for(uint256 i = 0; i < 20; i++) { 94 | string memory functionSignature = string(abi.encodePacked("randomFunctionNew", i.toString(), "(uint256,string,bytes,(uint256,uint256,bool))")); 95 | bytes4 selector = bytes4(keccak256(bytes(functionSignature))); 96 | defaultExtension4.functions.push(ExtensionFunction( 97 | selector, 98 | functionSignature 99 | )); 100 | } 101 | 102 | for(uint256 i = 0; i < 30; i++) { 103 | string memory functionSignature = string(abi.encodePacked("randomFunctionAnother", i.toString(), "(uint256,string,address[])")); 104 | bytes4 selector = bytes4(keccak256(bytes(functionSignature))); 105 | defaultExtension5.functions.push(ExtensionFunction( 106 | selector, 107 | functionSignature 108 | )); 109 | } 110 | 111 | Extension[] memory defaultExtensions = new Extension[](2); 112 | defaultExtensions[0] = defaultExtension1; 113 | defaultExtensions[1] = defaultExtension2; 114 | 115 | // Deploy BaseRouter 116 | router = BaseRouter(payable(address(new CustomRouter(defaultExtensions)))); 117 | CustomRouter(payable(address(router))).initialize(); 118 | } 119 | 120 | /*/////////////////////////////////////////////////////////////// 121 | Deploy / Initialze BaseRouter 122 | //////////////////////////////////////////////////////////////*/ 123 | 124 | /// @notice Check with a single extension with 10 functions 125 | function test_benchmark_deployBaseRouter() external { 126 | Extension[] memory defaultExtensionsNew = new Extension[](1); 127 | defaultExtensionsNew[0] = defaultExtension3; 128 | CustomRouter routerNew = new CustomRouter(defaultExtensionsNew); 129 | } 130 | 131 | /// @notice Check with multiple extensions extension with ~50 functions in total 132 | function test_benchmark_deployBaseRouter_multipleExtensions() external { 133 | Extension[] memory defaultExtensionsNew = new Extension[](3); 134 | defaultExtensionsNew[0] = defaultExtension3; 135 | defaultExtensionsNew[1] = defaultExtension4; 136 | defaultExtensionsNew[2] = defaultExtension5; 137 | CustomRouter routerNew = new CustomRouter(defaultExtensionsNew); 138 | } 139 | 140 | /// @notice Check with a single extension with 10 functions 141 | function test_benchmark_initializeBaseRouter_singleExtension() external { 142 | // vm.pauseGasMetering(); 143 | Extension[] memory defaultExtensionsNew = new Extension[](1); 144 | defaultExtensionsNew[0] = defaultExtension3; 145 | CustomRouter routerNew = new CustomRouter(defaultExtensionsNew); 146 | // vm.resumeGasMetering(); 147 | 148 | uint256 gasBefore = gasleft(); 149 | routerNew.initialize(); 150 | uint256 gasAfter = gasleft(); 151 | console.log(gasBefore - gasAfter); 152 | } 153 | 154 | /// @notice Check with multiple extensions extension with 50-100 functions in total 155 | function test_benchmark_initializeBaseRouter_multipleExtensions() external { 156 | // vm.pauseGasMetering(); 157 | Extension[] memory defaultExtensionsNew = new Extension[](3); 158 | defaultExtensionsNew[0] = defaultExtension3; 159 | defaultExtensionsNew[1] = defaultExtension4; 160 | defaultExtensionsNew[2] = defaultExtension5; 161 | 162 | CustomRouter routerNew = new CustomRouter(defaultExtensionsNew); 163 | // vm.resumeGasMetering(); 164 | 165 | uint256 gasBefore = gasleft(); 166 | routerNew.initialize(); 167 | uint256 gasAfter = gasleft(); 168 | console.log(gasBefore - gasAfter); 169 | } 170 | 171 | 172 | /*/////////////////////////////////////////////////////////////// 173 | Adding extensions 174 | //////////////////////////////////////////////////////////////*/ 175 | 176 | /// @notice Add an new extension. 177 | function test_benchmark_addExtension() public { 178 | vm.pauseGasMetering(); 179 | // Create Extension struct 180 | Extension memory extension; 181 | 182 | // Set metadata 183 | extension.metadata.name = "IncrementDecrement"; 184 | extension.metadata.metadataURI = "ipfs://IncrementDecrement"; 185 | extension.metadata.implementation = address(new IncrementDecrementGet()); 186 | 187 | // Set functions 188 | extension.functions = new ExtensionFunction[](3); 189 | 190 | extension.functions[0] = ExtensionFunction( 191 | IncrementDecrementGet.incrementNumber.selector, 192 | "incrementNumber()" 193 | ); 194 | extension.functions[1] = ExtensionFunction( 195 | IncrementDecrementGet.decrementNumber.selector, 196 | "decrementNumber()" 197 | ); 198 | extension.functions[2] = ExtensionFunction( 199 | IncrementDecrementGet.getNumber.selector, 200 | "getNumber()" 201 | ); 202 | vm.resumeGasMetering(); 203 | 204 | // Call: addExtension 205 | router.addExtension(extension); 206 | } 207 | 208 | /*/////////////////////////////////////////////////////////////// 209 | Replace extensions 210 | //////////////////////////////////////////////////////////////*/ 211 | 212 | /// @notice Replace a default extension with a new one. 213 | function test_benchmark_replaceExtension_defaultExtension() public { 214 | vm.pauseGasMetering(); 215 | // Create Extension struct to replace existing extension 216 | Extension memory updatedExtension; 217 | 218 | updatedExtension.metadata = defaultExtension1.metadata; 219 | updatedExtension.metadata.implementation = address(new IncrementDecrementMultiply()); 220 | 221 | updatedExtension.functions = new ExtensionFunction[](3); 222 | updatedExtension.functions[0] = defaultExtension1.functions[0]; 223 | updatedExtension.functions[1] = ExtensionFunction( 224 | IncrementDecrementGet.incrementNumber.selector, 225 | "incrementNumber()" 226 | ); 227 | updatedExtension.functions[2] = ExtensionFunction( 228 | IncrementDecrementGet.getNumber.selector, 229 | "getNumber()" 230 | ); 231 | vm.resumeGasMetering(); 232 | 233 | // Call: addExtension 234 | router.replaceExtension(updatedExtension); 235 | } 236 | 237 | 238 | 239 | /// @notice Replace a non-default extension with a new one. 240 | function test_benchmark_replaceExtension() public { 241 | vm.pauseGasMetering(); 242 | // Create Extension struct 243 | Extension memory extension; 244 | 245 | // Set metadata 246 | extension.metadata.name = "IncrementDecrement"; 247 | extension.metadata.metadataURI = "ipfs://IncrementDecrement"; 248 | extension.metadata.implementation = address(new IncrementDecrement()); 249 | 250 | // Set functions 251 | extension.functions = new ExtensionFunction[](2); 252 | extension.functions[0] = ExtensionFunction( 253 | IncrementDecrementGet.incrementNumber.selector, 254 | "incrementNumber()" 255 | ); 256 | extension.functions[1] = ExtensionFunction( 257 | IncrementDecrementGet.decrementNumber.selector, 258 | "decrementNumber()" 259 | ); 260 | 261 | // Call: addExtension 262 | router.addExtension(extension); 263 | 264 | // Create Extension struct to replace existing extension 265 | Extension memory updatedExtension; 266 | 267 | updatedExtension.metadata = extension.metadata; 268 | updatedExtension.metadata.implementation = address(new IncrementDecrementGet()); 269 | 270 | updatedExtension.functions = new ExtensionFunction[](3); 271 | updatedExtension.functions[0] = extension.functions[0]; 272 | updatedExtension.functions[1] = extension.functions[1]; 273 | updatedExtension.functions[2] = ExtensionFunction( 274 | IncrementDecrementGet.getNumber.selector, 275 | "getNumber()" 276 | ); 277 | vm.resumeGasMetering(); 278 | 279 | // Call: addExtension 280 | router.replaceExtension(updatedExtension); 281 | } 282 | 283 | /*/////////////////////////////////////////////////////////////// 284 | Removing extensions 285 | //////////////////////////////////////////////////////////////*/ 286 | 287 | /// @notice Remove a default extension. 288 | function test_benchmark_removeExtension_defautlExtension() public { 289 | // Call: removeExtension 290 | 291 | router.removeExtension(defaultExtension1.metadata.name); 292 | } 293 | 294 | /// @notice Remove a non-default extension. 295 | function test_benchmark_removeExtension() public { 296 | vm.pauseGasMetering(); 297 | // Create Extension struct 298 | Extension memory extension; 299 | 300 | // Set metadata 301 | extension.metadata.name = "IncrementDecrement"; 302 | extension.metadata.metadataURI = "ipfs://IncrementDecrement"; 303 | extension.metadata.implementation = address(new IncrementDecrement()); 304 | 305 | // Set functions 306 | extension.functions = new ExtensionFunction[](2); 307 | extension.functions[0] = ExtensionFunction( 308 | IncrementDecrementGet.incrementNumber.selector, 309 | "incrementNumber()" 310 | ); 311 | extension.functions[1] = ExtensionFunction( 312 | IncrementDecrementGet.decrementNumber.selector, 313 | "decrementNumber()" 314 | ); 315 | 316 | // Call: addExtension 317 | router.addExtension(extension); 318 | 319 | // Create Extension struct to replace existing extension 320 | Extension memory updatedExtension; 321 | 322 | updatedExtension.metadata = extension.metadata; 323 | updatedExtension.metadata.implementation = address(new IncrementDecrementGet()); 324 | 325 | updatedExtension.functions = new ExtensionFunction[](3); 326 | updatedExtension.functions[0] = extension.functions[0]; 327 | updatedExtension.functions[1] = extension.functions[1]; 328 | updatedExtension.functions[2] = ExtensionFunction( 329 | IncrementDecrementGet.getNumber.selector, 330 | "getNumber()" 331 | ); 332 | vm.resumeGasMetering(); 333 | 334 | // Call: replaceExtension 335 | router.replaceExtension(updatedExtension); 336 | } 337 | 338 | /*/////////////////////////////////////////////////////////////// 339 | Disabling function in extension 340 | //////////////////////////////////////////////////////////////*/ 341 | 342 | /// @notice Disable a function in a default extension. 343 | function test_benchmark_disableFunctionInExtension_defaultExtension() public { 344 | // Call: disableFunctionInExtension 345 | router.disableFunctionInExtension(defaultExtension1.metadata.name, defaultExtension1.functions[0].functionSelector); 346 | } 347 | 348 | /// @notice Disable a function in a non-default extension. 349 | function test_benchmark_disableFunctionInExtension() public { 350 | vm.pauseGasMetering(); 351 | // Create Extension struct 352 | Extension memory extension; 353 | 354 | // Set metadata 355 | extension.metadata.name = "IncrementDecrement"; 356 | extension.metadata.metadataURI = "ipfs://IncrementDecrement"; 357 | extension.metadata.implementation = address(new IncrementDecrementGet()); 358 | 359 | // Set functions 360 | extension.functions = new ExtensionFunction[](2); 361 | 362 | extension.functions[0] = ExtensionFunction( 363 | IncrementDecrementGet.incrementNumber.selector, 364 | "incrementNumber()" 365 | ); 366 | extension.functions[1] = ExtensionFunction( 367 | IncrementDecrementGet.decrementNumber.selector, 368 | "decrementNumber()" 369 | ); 370 | 371 | // Call: addExtension 372 | router.addExtension(extension); 373 | vm.resumeGasMetering(); 374 | 375 | // Call: disableFunctionInExtension 376 | router.disableFunctionInExtension(extension.metadata.name, IncrementDecrementGet.incrementNumber.selector); 377 | } 378 | 379 | /*/////////////////////////////////////////////////////////////// 380 | Enable function in extension 381 | //////////////////////////////////////////////////////////////*/ 382 | 383 | /// @notice Enable a function in a default extension. 384 | function test_benchmark_enableFunctionInExtension_defaultExtension() public { 385 | vm.pauseGasMetering(); 386 | // Call: disableFunctionInExtension 387 | router.disableFunctionInExtension(defaultExtension1.metadata.name, defaultExtension1.functions[0].functionSelector); 388 | vm.resumeGasMetering(); 389 | 390 | // Call: enableFunctionInExtension 391 | router.enableFunctionInExtension(defaultExtension1.metadata.name, defaultExtension1.functions[0]); 392 | } 393 | 394 | /// @notice Enable a function in a non-default extension. 395 | function test_benchmark_enableFunctionInExtension() public { 396 | vm.pauseGasMetering(); 397 | // Create Extension struct 398 | Extension memory extension; 399 | 400 | // Set metadata 401 | extension.metadata.name = "IncrementDecrement"; 402 | extension.metadata.metadataURI = "ipfs://IncrementDecrement"; 403 | extension.metadata.implementation = address(new IncrementDecrementGet()); 404 | 405 | // Set functions 406 | extension.functions = new ExtensionFunction[](2); 407 | 408 | extension.functions[0] = ExtensionFunction( 409 | IncrementDecrementGet.incrementNumber.selector, 410 | "incrementNumber()" 411 | ); 412 | extension.functions[1] = ExtensionFunction( 413 | IncrementDecrementGet.decrementNumber.selector, 414 | "decrementNumber()" 415 | ); 416 | 417 | // Call: addExtension 418 | router.addExtension(extension); 419 | 420 | // Call: enableFunctionInExtension 421 | ExtensionFunction memory fn = ExtensionFunction( 422 | IncrementDecrementGet.getNumber.selector, 423 | "getNumber()" 424 | ); 425 | vm.resumeGasMetering(); 426 | 427 | router.enableFunctionInExtension(extension.metadata.name, fn); 428 | } 429 | 430 | /*/////////////////////////////////////////////////////////////// 431 | Scenario tests 432 | //////////////////////////////////////////////////////////////*/ 433 | 434 | /// The following tests are for scenarios that may occur in production use of a base router. 435 | 436 | /// @notice Upgrade a buggy function in a default extension. 437 | function test_benchmark_upgradeBuggyFunction_defaultExtension() public { 438 | vm.pauseGasMetering(); 439 | // Disable buggy function in extension 440 | router.disableFunctionInExtension(defaultExtension1.metadata.name, defaultExtension1.functions[0].functionSelector); 441 | 442 | // Create new extension with fixed function 443 | Extension memory extension; 444 | 445 | // Set metadata 446 | extension.metadata.name = "MultiplyDivide-Fixed-Multiply"; 447 | extension.metadata.metadataURI = "ipfs://MultiplyDivide-Fixed-Multiply"; 448 | extension.metadata.implementation = address(new MultiplyDivide()); 449 | 450 | // Set functions 451 | extension.functions = new ExtensionFunction[](1); 452 | extension.functions[0] = ExtensionFunction( 453 | MultiplyDivide.multiplyNumber.selector, 454 | "multiplyNumber(uint256)" 455 | ); 456 | vm.resumeGasMetering(); 457 | 458 | // Call: addExtension 459 | router.addExtension(extension); 460 | } 461 | 462 | /// @notice Upgrade a buggy function in a non-default extension. 463 | function test_benchmark_upgradeBuggyFunction() public { 464 | vm.pauseGasMetering(); 465 | // Add extension with buggy function 466 | Extension memory extension; 467 | 468 | extension.metadata.name = "IncrementDecrement"; 469 | extension.metadata.metadataURI = "ipfs://IncrementDecrement"; 470 | extension.metadata.implementation = address(new IncrementDecrementGet()); 471 | 472 | extension.functions = new ExtensionFunction[](3); 473 | 474 | extension.functions[0] = ExtensionFunction( 475 | IncrementDecrementGet.incrementNumber.selector, 476 | "incrementNumber()" 477 | ); 478 | extension.functions[1] = ExtensionFunction( 479 | IncrementDecrementGet.decrementNumber.selector, 480 | "decrementNumber()" 481 | ); 482 | extension.functions[2] = ExtensionFunction( 483 | IncrementDecrementGet.getNumber.selector, 484 | "getNumber()" 485 | ); 486 | 487 | // Call: addExtension 488 | router.addExtension(extension); 489 | 490 | // Disable buggy function in extension 491 | router.disableFunctionInExtension(extension.metadata.name, IncrementDecrementGet.getNumber.selector); 492 | 493 | // Create new extension with fixed function 494 | Extension memory updatedExtension; 495 | updatedExtension.metadata = extension.metadata; 496 | updatedExtension.metadata.name = "IncrementDecrement-getNumber-fixed"; 497 | updatedExtension.functions = new ExtensionFunction[](1); 498 | updatedExtension.functions[0] = ExtensionFunction( 499 | IncrementDecrementGet.getNumber.selector, 500 | "getNumber()" 501 | ); 502 | vm.resumeGasMetering(); 503 | 504 | // Call: addExtension 505 | router.addExtension(updatedExtension); 506 | } 507 | } -------------------------------------------------------------------------------- /test/utils/MockContracts.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | library NumberStorage { 5 | 6 | /// @custom:storage-location erc7201:number.storage 7 | bytes32 public constant NUMBER_STORAGE_POSITION = keccak256(abi.encode(uint256(keccak256("number.storage")) - 1)); 8 | 9 | struct Data { 10 | uint256 number; 11 | } 12 | 13 | function data() internal pure returns (Data storage data_) { 14 | bytes32 position = NUMBER_STORAGE_POSITION; 15 | assembly { 16 | data_.slot := position 17 | } 18 | } 19 | } 20 | 21 | contract IncrementDecrement { 22 | 23 | function incrementNumber() public { 24 | NumberStorage.data().number += 1; 25 | } 26 | 27 | function decrementNumber() public { 28 | NumberStorage.data().number -= 1; 29 | } 30 | } 31 | 32 | contract IncrementDecrementGetBug { 33 | 34 | function incrementNumber() public { 35 | NumberStorage.data().number += 1; 36 | } 37 | 38 | function decrementNumber() public { 39 | // Bug: += instead of -= 40 | NumberStorage.data().number += 1; 41 | } 42 | 43 | function getNumber() public view returns (uint256) { 44 | return NumberStorage.data().number; 45 | } 46 | } 47 | 48 | contract DecrementFixed { 49 | function decrementNumber() public { 50 | // Bug: -= instead of += 51 | NumberStorage.data().number -= 1; 52 | } 53 | } 54 | 55 | contract IncrementDecrementGet { 56 | 57 | function incrementNumber() public { 58 | NumberStorage.data().number += 1; 59 | } 60 | 61 | function decrementNumber() public { 62 | NumberStorage.data().number -= 1; 63 | } 64 | 65 | function getNumber() public view returns (uint256) { 66 | return NumberStorage.data().number; 67 | } 68 | } 69 | 70 | contract Receive { 71 | receive() external payable {} 72 | } 73 | 74 | contract IncrementDecrementReceive is Receive { 75 | 76 | function incrementNumber() public { 77 | NumberStorage.data().number += 1; 78 | } 79 | 80 | function decrementNumber() public { 81 | NumberStorage.data().number -= 1; 82 | } 83 | 84 | function getNumber() public view returns (uint256) { 85 | return NumberStorage.data().number; 86 | } 87 | } 88 | 89 | contract MultiplyDivide { 90 | 91 | function multiplyNumber(uint256 _multiplier) public { 92 | NumberStorage.data().number *= _multiplier; 93 | } 94 | 95 | function divideNumber(uint256 _divisor) public { 96 | NumberStorage.data().number /= _divisor; 97 | } 98 | } 99 | 100 | contract IncrementDecrementMultiply is IncrementDecrementGet, MultiplyDivide {} 101 | 102 | contract MultiplyDivideGet { 103 | 104 | function multiplyNumber(uint256 _multiplier) public { 105 | NumberStorage.data().number *= _multiplier; 106 | } 107 | 108 | function divideNumber(uint256 _divisor) public { 109 | NumberStorage.data().number /= _divisor; 110 | } 111 | 112 | function getNumber() public view returns (uint256) { 113 | return NumberStorage.data().number; 114 | } 115 | } 116 | 117 | contract AddSubstract { 118 | 119 | function addNumber(uint256 _addend) public { 120 | NumberStorage.data().number += _addend; 121 | } 122 | 123 | function subtractNumber(uint256 _subtrahend) public { 124 | NumberStorage.data().number -= _subtrahend; 125 | } 126 | } 127 | 128 | contract AddSubstractGet { 129 | 130 | function addNumber(uint256 _addend) public { 131 | NumberStorage.data().number += _addend; 132 | } 133 | 134 | function subtractNumber(uint256 _subtrahend) public { 135 | NumberStorage.data().number -= _subtrahend; 136 | } 137 | 138 | function getNumber() public view returns (uint256) { 139 | return NumberStorage.data().number; 140 | } 141 | } -------------------------------------------------------------------------------- /test/utils/Strings.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache 2.0 2 | pragma solidity ^0.8.0; 3 | 4 | /** 5 | * @dev String operations. 6 | */ 7 | library Strings { 8 | bytes16 private constant _HEX_SYMBOLS = "0123456789abcdef"; 9 | 10 | /** 11 | * @dev Converts a `uint256` to its ASCII `string` decimal representation. 12 | */ 13 | function toString(uint256 value) internal pure returns (string memory) { 14 | // Inspired by OraclizeAPI's implementation - MIT licence 15 | // https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol 16 | 17 | if (value == 0) { 18 | return "0"; 19 | } 20 | uint256 temp = value; 21 | uint256 digits; 22 | while (temp != 0) { 23 | digits++; 24 | temp /= 10; 25 | } 26 | bytes memory buffer = new bytes(digits); 27 | while (value != 0) { 28 | digits -= 1; 29 | buffer[digits] = bytes1(uint8(48 + uint256(value % 10))); 30 | value /= 10; 31 | } 32 | return string(buffer); 33 | } 34 | 35 | /** 36 | * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation. 37 | */ 38 | function toHexString(uint256 value) internal pure returns (string memory) { 39 | if (value == 0) { 40 | return "0x00"; 41 | } 42 | uint256 temp = value; 43 | uint256 length = 0; 44 | while (temp != 0) { 45 | length++; 46 | temp >>= 8; 47 | } 48 | return toHexString(value, length); 49 | } 50 | 51 | /** 52 | * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length. 53 | */ 54 | function toHexString(uint256 value, uint256 length) internal pure returns (string memory) { 55 | bytes memory buffer = new bytes(2 * length + 2); 56 | buffer[0] = "0"; 57 | buffer[1] = "x"; 58 | for (uint256 i = 2 * length + 1; i > 1; --i) { 59 | buffer[i] = _HEX_SYMBOLS[value & 0xf]; 60 | value >>= 4; 61 | } 62 | require(value == 0, "Strings: hex length insufficient"); 63 | return string(buffer); 64 | } 65 | } 66 | --------------------------------------------------------------------------------