Skip to content

Commit

Permalink
Add extension-dependent extension changes (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
travs authored Jul 25, 2023
1 parent 63c7e31 commit a20ce7b
Show file tree
Hide file tree
Showing 7 changed files with 75 additions and 15 deletions.
15 changes: 10 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[default]
[profile.default]
libs = ['lib']
out = 'out'
remappings = []
Expand Down
2 changes: 1 addition & 1 deletion src/interface/IBaseRouter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
13 changes: 7 additions & 6 deletions src/presets/BaseRouter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
}
}
Expand All @@ -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);
}

/*///////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -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);
}
2 changes: 1 addition & 1 deletion src/presets/example/RouterImmutable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
54 changes: 54 additions & 0 deletions src/presets/example/RouterRegistryConstrained.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
2 changes: 1 addition & 1 deletion src/presets/example/RouterUpgradeable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

0 comments on commit a20ce7b

Please sign in to comment.