diff --git a/README.md b/README.md index 3a8fdfe..ce74964 100644 --- a/README.md +++ b/README.md @@ -41,10 +41,10 @@ The simplest way to write a `Router` contract is to extend the preset [`BaseRout import "lib/dynamic-contracts/src/presets/BaseRouter.sol"; ``` -The `BaseRouter` contract comes with an API to add/update/remove extensions from the contract. It is an abstract contract, and expects its consumer to implement the `_canSetExtension()` function, which specifies the conditions under which `Extensions` can be added, updated or removed. The rest of the implementation is generic and usable for all purposes. +The `BaseRouter` contract comes with an API to add/update/remove extensions from the contract. It is an abstract contract, and expects its consumer to implement the `_canSetExtension(...)` function, which specifies the conditions under which `Extensions` can be added, updated or removed. The rest of the implementation is generic and usable for all purposes. ```solidity -function _canSetExtension() internal view virtual returns (bool); +function _canSetExtension(Extension memory _extension) internal view virtual returns (bool); ``` Here's a very simple example that allows only the original contract deployer to add/update/remove `Extensions`. @@ -66,25 +66,30 @@ contract SimpleRouter is BaseRouter { } /// @dev Returns whether extensions can be set in the given execution context. - function _canSetExtension() internal view virtual override returns (bool) { + function _canSetExtension(Extension memory _extension) internal view virtual override returns (bool) { return msg.sender == deployer; } } ``` + #### Choosing a permission model: The main decision as a `Router` contract author is to decide the permission model to add/update/remove extensions. This repository offers some presets for a few possible permission models: -- #### [`RouterUpgradeable`](/src/presets/example/RouterUpgradeable.sol) +- #### [`RouterUpgradeable`](/src/presets/example/RouterUpgradeable.sol) This a is a preset that **allows the contract owner to add / upgrade / remove extensions**. The contract owner can be changed. This is a very basic permission model, but enough for some use cases. You can expand on this and use a permission based model instead for example. -- #### [`RouterImmutable`](/src/presets/example/RouterImmutable.sol) +- #### [`RouterImmutable`](/src/presets/example/RouterImmutable.sol) This is a preset you can use to **create static contracts that cannot be updated or get new functionality**. This still allows you to create modular contracts that go beyond the contract size limit, but guarantees that the original functionality cannot be altered. With this model, you would pass all the `Extensions` for this contract at construction time, and guarantee that the functionality is immutable. Other permissions models might include an explicit list of extensions that can be added or removed for example. The implementation is up to the Router author. +- #### [`RouterRegistryConstrained`](/src/presets/example/RouterRegistryConstrained.sol) + +This is a preset that **allows the owner to change extensions if they are defined on a given registry contract**. This is meant to demonstrate how a protocol ecosystem could constrain extensions to known, audited contracts, for instance. The registry and router upgrade models are of course too basic for production as written. + ### 2. `Extensions` - implementing routeable contracts An `Extension` contract is written like any other smart contract, except that its state must be defined using a `struct` within a `library` and at a well defined storage location. This storage technique is known as [storage structs](https://mirror.xyz/horsefacts.eth/EPB4o-eyDl0N8gu0gEz1uw7BTITheaZUqIAOEK1m-jE). This is important to ensure that state defined in an `Extension` doesn't conflict with the state of another `Extension` of the same `Router` at the same storage location. diff --git a/foundry.toml b/foundry.toml index adfe950..809713f 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,4 +1,4 @@ -[default] +[profile.default] libs = ['lib'] out = 'out' remappings = [] diff --git a/src/interface/IBaseRouter.sol b/src/interface/IBaseRouter.sol index 61930a5..8a604e3 100644 --- a/src/interface/IBaseRouter.sol +++ b/src/interface/IBaseRouter.sol @@ -17,7 +17,7 @@ interface IBaseRouter is IDefaultExtensionSet { function updateExtension(Extension memory extension) external; /// @dev Removes an existing extension from the router. - function removeExtension(string memory extensionName) external; + function removeExtension(Extension memory extension) external; /// @dev Returns all extensions stored. function getAllExtensions() external view returns (Extension[] memory allExtensions); diff --git a/src/presets/BaseRouter.sol b/src/presets/BaseRouter.sol index 5d3e615..08eb702 100644 --- a/src/presets/BaseRouter.sol +++ b/src/presets/BaseRouter.sol @@ -36,6 +36,7 @@ abstract contract BaseRouter is IBaseRouter, Router, ExtensionState { uint256 len = _extensions.length; for (uint256 i = 0; i < len; i += 1) { + require(_canSetExtension(_extensions[i]), "BaseRouter: not authorized."); map.setExtension(_extensions[i]); } } @@ -55,23 +56,23 @@ abstract contract BaseRouter is IBaseRouter, Router, ExtensionState { /// @dev Adds a new extension to the router. function addExtension(Extension memory _extension) external { - require(_canSetExtension(), "BaseRouter: caller not authorized."); + require(_canSetExtension(_extension), "BaseRouter: not authorized."); _addExtension(_extension); } /// @dev Updates an existing extension in the router, or overrides a default extension. function updateExtension(Extension memory _extension) external { - require(_canSetExtension(), "BaseRouter: caller not authorized."); + require(_canSetExtension(_extension), "BaseRouter: not authorized."); _updateExtension(_extension); } /// @dev Removes an existing extension from the router. - function removeExtension(string memory _extensionName) external { - require(_canSetExtension(), "BaseRouter: caller not authorized."); + function removeExtension(Extension memory _extension) external { + require(_canSetExtension(_extension), "BaseRouter: not authorized."); - _removeExtension(_extensionName); + _removeExtension(_extension.metadata.name); } /*/////////////////////////////////////////////////////////////// @@ -159,5 +160,5 @@ abstract contract BaseRouter is IBaseRouter, Router, ExtensionState { //////////////////////////////////////////////////////////////*/ /// @dev Returns whether a extension can be set in the given execution context. - function _canSetExtension() internal view virtual returns (bool); + function _canSetExtension(Extension memory _extension) internal view virtual returns (bool); } \ No newline at end of file diff --git a/src/presets/example/RouterImmutable.sol b/src/presets/example/RouterImmutable.sol index 3e71d1d..99e3901 100644 --- a/src/presets/example/RouterImmutable.sol +++ b/src/presets/example/RouterImmutable.sol @@ -18,7 +18,7 @@ contract RouterImmutable is BaseRouter { //////////////////////////////////////////////////////////////*/ /// @dev Returns whether extensions can be set in the given execution context. - function _canSetExtension() internal pure override returns (bool) { + function _canSetExtension(Extension memory) internal pure override returns (bool) { return false; } } diff --git a/src/presets/example/RouterRegistryConstrained.sol b/src/presets/example/RouterRegistryConstrained.sol new file mode 100644 index 0000000..a8b3cf0 --- /dev/null +++ b/src/presets/example/RouterRegistryConstrained.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT +// @author: thirdweb (https://github.com/thirdweb-dev/dynamic-contracts) + +pragma solidity ^0.8.0; + +import "../BaseRouter.sol"; + +/** + * This smart contract is an EXAMPLE, and is not meant for use in production. + */ +contract ExtensionRegistry { + + address public immutable admin; + mapping (address => bool) public isRegistered; + + constructor() { + admin = msg.sender; + } + + function setExtensionRegistered(address _extension, bool _isRegistered) external { + require(msg.sender == admin, "ExtensionRegistry: Only admin can alter extension registry"); + isRegistered[_extension] = _isRegistered; + } +} + +/** + * This smart contract is an EXAMPLE, and is not meant for use in production. + */ +contract RouterRegistryConstrained is BaseRouter { + + address public admin; + ExtensionRegistry public registry; + + // @dev Cannot initialize with extensions before registry is set, so we pass empty array to base constructor. + constructor(address _registry) BaseRouter(new Extension[](0)) { + admin = msg.sender; + registry = ExtensionRegistry(_registry); + } + + // @dev Sets the admin address. + function setAdmin(address _admin) external { + require(msg.sender == admin, "RouterUpgradeable: Only admin can set a new admin"); + admin = _admin; + } + + /*/////////////////////////////////////////////////////////////// + Overrides + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns whether extensions can be set in the given execution context. + function _canSetExtension(Extension memory _extension) internal view virtual override returns (bool) { + return msg.sender == admin && registry.isRegistered(_extension.metadata.implementation); + } +} \ No newline at end of file diff --git a/src/presets/example/RouterUpgradeable.sol b/src/presets/example/RouterUpgradeable.sol index 9b1c6fa..2628d98 100644 --- a/src/presets/example/RouterUpgradeable.sol +++ b/src/presets/example/RouterUpgradeable.sol @@ -27,7 +27,7 @@ contract RouterUpgradeable is BaseRouter { //////////////////////////////////////////////////////////////*/ /// @dev Returns whether extensions can be set in the given execution context. - function _canSetExtension() internal view virtual override returns (bool) { + function _canSetExtension(Extension memory) internal view virtual override returns (bool) { return msg.sender == admin; } }