diff --git a/packages/contracts/docs/modules/ROOT/pages/guide-develop-plugin/design-your-plugin.adoc b/packages/contracts/docs/modules/ROOT/pages/guide-develop-plugin/design-your-plugin.adoc index e4cd1e033..fd34cce7d 100644 --- a/packages/contracts/docs/modules/ROOT/pages/guide-develop-plugin/design-your-plugin.adoc +++ b/packages/contracts/docs/modules/ROOT/pages/guide-develop-plugin/design-your-plugin.adoc @@ -1,12 +1,12 @@ = Designing your plugin -== Governance Plugins +This guide explains how to design plugins for Aragon OSx, covering governance plugins, membership management, and upgradeability patterns. You'll learn about the core interfaces, implementation patterns, and how to choose the right base contract for your specific use case. -### How to Build a Governance Plugin +== Governance Plugins One of the most common use cases for plugins are governance plugins. Governance plugins are plugins DAOs install to help them make decisions. -#### What are Governance Plugins +=== What are Governance Plugins Governance plugins are characterized by the **ability to execute actions in the DAO** they have been installed to. Accordingly, the `EXECUTE_PERMISSION_ID` is granted on installation on the installing DAO to the governance plugin contract. @@ -23,7 +23,7 @@ Beyond this fundamental ability, governance plugins usually implement two interf - xref:guide-develop-plugin/design-your-plugin#proposals[The `IProposal` interface] introducing the **notion of proposals** and how they are created and executed. - xref:guide-develop-plugin/design-your-plugin#membership[The `IMembership` interface] introducing the **notion of membership** to the DAO. -#### Examples of Governance Plugins +=== Examples of Governance Plugins Some examples of governance plugins are: @@ -36,11 +36,7 @@ Some examples of governance plugins are: // - - -### Proposals - -#### The `IProposal` Interface +=== The `IProposal` Interface The `IProposal` interface is used to create and execute proposals containing actions and a description. @@ -78,7 +74,7 @@ interface IProposal { This interface contains two events and one function -##### `ProposalCreated` event +==== `ProposalCreated` event This event should be emitted when a proposal is created. It contains the following parameters: @@ -90,15 +86,15 @@ This event should be emitted when a proposal is created. It contains the followi - `actions`: The actions that will be executed if the proposal passes. - `allowFailureMap`: A bitmap allowing the proposal to succeed, even if individual actions might revert. If the bit at index `i` is 1, the proposal succeeds even if the `i`th action reverts. A failure map value of 0 requires every action to not revert. -##### `ProposalExecuted` event +==== `ProposalExecuted` event This event should be emitted when a proposal is executed. It contains the proposal ID as a parameter. -##### `proposalCount` function +==== `proposalCount` function This function should return the proposal count determining the next proposal ID. -#### Usage +==== Usage ```solidity contract MyPlugin is IProposal { @@ -134,9 +130,9 @@ contract MyPlugin is IProposal { } ``` -### Membership -#### The `IMembership` Interface + +=== The `IMembership` Interface The `IMembership` interface defines common functions and events for for plugins that keep track of membership in a DAO. This plugins can be used to define who can vote on proposals, who can create proposals, etc. The list of members can be defined in the plugin itself or by a contract that defines the membership like an ERC20 or ERC721 token. @@ -167,25 +163,25 @@ interface IMembership { The interface contains three events and one function. -##### `MembersAdded` event +==== `MembersAdded` event The members added event should be emitted when members are added to the DAO plugin. It only contains one `address[] members` parameter that references the list of new members being added. - `members`: The list of new members being added. -##### `MembersRemoved` event +==== `MembersRemoved` event The members added event should be emitted when members are removed from the DAO plugin. It only contains one `address[] members` parameter that references the list of members being removed. -##### `MembershipContractAnnounced` event +==== `MembershipContractAnnounced` event This event should be emitted during the initialization of the membership plugin to announce the membership being defined by a contract. It contains the defining contract as a parameter. -##### `isMember` function +==== `isMember` function This is a simple function that should be implemented in the plugin contract that introduces the members to the DAO. It checks if an account is a member of the DAO and returns a boolean value. -#### Usage +==== Usage ```solidity @@ -222,7 +218,7 @@ contract MyPlugin is IMembership { == Choosing the Plugin Upgradeability -### How to Choose the Base Contract for Your Plugin +=== How to Choose your Plugin Upgradeability Although it is not mandatory to choose one of our interfaces as the base contracts for your plugins, we do offer some options for you to inherit from and speed up development. @@ -240,7 +236,7 @@ In this regard, we provide 3 options for base contracts you can choose from: Let's take a look at what this means for you. -### Upgradeability & Deployment +=== Upgradeability & Deployment Upgradeability and the deployment method of a plugin contract go hand in hand. The motivation behind upgrading smart contracts is nicely summarized by OpenZeppelin: @@ -272,10 +268,10 @@ Some key things to keep in mind: | |`new` Instantiation | Minimal Proxy (Clones)| Transparent Proxy| UUPS Proxy | upgradeability -|no +| [.no-cell]#no# | no -| yes | yes +| yes | gas costs | high | very low @@ -292,10 +288,10 @@ Accordingly, we recommend to use link:https://eips.ethereum.org/EIPS/eip-1167[mi To help you with developing and deploying your plugin within the Aragon infrastructure, we provide the following implementation that you can inherit from: - `Plugin` for instantiation via `new` -- `PluginClones` for [minimal proxy pattern link:https://eips.ethereum.org/EIPS/eip-1167[ERC-1167]] deployment -- `PluginUUPSUpgradeable` for [UUPS pattern link:https://eips.ethereum.org/EIPS/eip-1822[ERC-1822]] deployment +- `PluginClones` for link:https://eips.ethereum.org/EIPS/eip-1167[minimal proxy pattern ERC-1167] deployment +- `PluginUUPSUpgradeable` for link:https://eips.ethereum.org/EIPS/eip-1822[UUPS pattern ERC-1822] deployment -#### Caveats of Non-upgradeable Plugins +=== Caveats of Non-upgradeable Plugins Aragon plugins using the non-upgradeable smart contracts bases (`Plugin`, `PluginCloneable`) can be cheap to deploy (i.e., using clones) but **cannot be updated**. diff --git a/packages/contracts/docs/modules/ROOT/pages/guide-develop-plugin/follow-best-practices.adoc b/packages/contracts/docs/modules/ROOT/pages/guide-develop-plugin/follow-best-practices.adoc index 50e22f821..4288f537e 100644 --- a/packages/contracts/docs/modules/ROOT/pages/guide-develop-plugin/follow-best-practices.adoc +++ b/packages/contracts/docs/modules/ROOT/pages/guide-develop-plugin/follow-best-practices.adoc @@ -2,7 +2,7 @@ == Advice for Developing a Plugin -### DOs 👌 +=== DOs 👌 - Document your contracts using link:https://docs.soliditylang.org/en/v0.8.17/natspec-format.html[NatSpec]. - Test your contracts, e.g., using toolkits such as link:https://hardhat.org/hardhat-runner/docs/guides/test-contracts[hardhat (JS)] or link:https://book.getfoundry.sh/forge/tests[Foundry (Rust)]. @@ -12,7 +12,7 @@ - Plan the lifecycle of your plugin (need for upgrades). - Follow our xref:guide-develop-plugin/publishing-plugin.adoc#how_to_add_a_new_version_of_your_plugin[versioning guidelines]. -### DON'Ts ✋ +=== DON'Ts ✋ - Leave any contract uninitialized. - Grant the `ROOT_PERMISSION_ID` permission to anything or anyone. diff --git a/packages/contracts/docs/modules/ROOT/pages/guide-develop-plugin/index.adoc b/packages/contracts/docs/modules/ROOT/pages/guide-develop-plugin/index.adoc index 640013fd8..64f7af6a8 100644 --- a/packages/contracts/docs/modules/ROOT/pages/guide-develop-plugin/index.adoc +++ b/packages/contracts/docs/modules/ROOT/pages/guide-develop-plugin/index.adoc @@ -1,12 +1,10 @@ = How to build a DAO Plugin -== Plugin Development Quickstart Guide - Plugins are how we extend the functionality for DAOs. In Aragon OSx, everything a DAO can do is based on Plugin functionality enabled through permissions. In this Quickstart guide, we will use the Aragon Hardhat template to set up a plugin. -## Setting up your environment +== Setting up your environment We recommend using our link:https://github.com/aragon/osx-plugin-template-hardhat[hardhat template] to get started. If you don't have it installed, you can do so by running: @@ -61,10 +59,10 @@ For more information on how to develop a plugin, you can check our plugin develo - xref:guide-develop-plugin/write-plugin-setup-contract.adoc[Writing your plugin setup contract] - xref:guide-develop-plugin/write-upgradeable-plugin.adoc[Writing upgradeable plugin] - xref:guide-develop-plugin/upgrade-plugin.adoc[Upgrading your plugin] -- xref:guide-develop-plugin/follow-best-practices.adoc[Best practices and patterns] +- xref:guide-develop-plugin/follow-best-practices.adoc[Following best practices] - xref:guide-develop-plugin/publishing-plugin.adoc[Publishing your plugin] -IMPORTANT: This plugin template uses version TODO:GIORGI `1.4.0-alpha.5` of the Aragon OSx protocol. This version is still in development and +IMPORTANT: This plugin template uses version `1.4.0-alpha.5` of the Aragon OSx protocol. This version is still in development and is not audited yet. diff --git a/packages/contracts/docs/modules/ROOT/pages/guide-develop-plugin/publishing-plugin.adoc b/packages/contracts/docs/modules/ROOT/pages/guide-develop-plugin/publishing-plugin.adoc index 9c3fb815b..4d76edc0a 100644 --- a/packages/contracts/docs/modules/ROOT/pages/guide-develop-plugin/publishing-plugin.adoc +++ b/packages/contracts/docs/modules/ROOT/pages/guide-develop-plugin/publishing-plugin.adoc @@ -1,16 +1,18 @@ = Publication of your Plugin into Aragon OSx -== How to publish a plugin into Aragon's plugin registry - Once you've deployed your Plugin Setup contract, you will be able to publish your plugin into Aragon's plugin registry so any Aragon DAO can install it. -### 1. Make sure your plugin is deployed in the right network +== How to publish new plugin + +Publishing a plugin to Aragon OSx involves a few key steps to ensure your plugin is properly registered and accessible to DAOs. + +=== Make sure your plugin is deployed in a supported network Make sure your Plugin Setup contract is deployed in your network of choice (you can find all of the networks we support link:https://github.com/aragon/osx-commons/tree/develop/configs/src/deployments/json[here]). You will need the address of your Plugin Setup contract to be able to publish the plugin into the protocol. -### 2. Publishing your plugin +=== Publishing your plugin Every plugin in Aragon can have future versions, so when publishing a plugin to the Aragon protocol, we're really creating a link:https://github.com/aragon/osx/blob/develop/packages/contracts/src/framework/plugin/repo/PluginRepo.sol[PluginRepo] instance for each plugin, which will contain all of the plugin's versions. @@ -24,14 +26,13 @@ You can find all of the addresses of `PluginRepoFactory` contracts by network li To create more versions of your plugin in the future, you'll call on the link:https://github.com/aragon/osx/blob/develop/packages/contracts/src/framework/plugin/repo/PluginRepo.sol#L128[createVersion function] from the `PluginRepo` instance of your plugin. When you publish your plugin, you'll be able to find the address of your plugin's `PluginRepo` instance within the transaction data. -### 3. Publishing subsequent builds +=== Publishing subsequent builds When publishing subsequent builds, you want to use the `createVersion` function in the `PluginRepo` contract (link:https://github.com/aragon/osx/blob/develop/packages/contracts/src/framework/plugin/repo/PluginRepo.sol#L132[check out the function's source code here]). To deploy your plugin, follow the steps in the link:https://github.com/aragon/osx-plugin-template-hardhat/blob/main/README.md#deployment[osx-plugin-template-hardhat README.md]. - == How to add a new version of your plugin The Aragon OSx protocol has an on-chain versioning system built-in, which distinguishes between releases and builds. @@ -94,7 +95,7 @@ Currently, two kinds of metadata exist: 1. Release metadata 2. Build metadata -### Release Metadata +=== Release Metadata The release metadata is a `.json` file stored on IPFS with its IPFS CID published for each release in the xref:framework/plugin-repos.adoc[PluginRepo](see also the section about xref:#how_to_add_a_new_version_of_your_plugin[versioning]). @@ -123,7 +124,7 @@ The `release-metadata.json` file consists of the following entries: |=== -#### Example +==== Example ```json { @@ -133,7 +134,7 @@ The `release-metadata.json` file consists of the following entries: } ``` -### Build Metadata +=== Build Metadata The build metadata is a `.json` file stored on IPFS with its IPFS CID published for each build **only once** in the xref:framework/plugin-repos.adoc[PluginRepo] (see also the section about xref:#how_to_add_a_new_version_of_your_plugin[versioning]). @@ -184,7 +185,7 @@ Each `"prepare..."` object contains: By following the Solidity JSON ABI format for the inputs, we followed an established standard, have support for complex types (tuples, arrays, nested versions of the prior) and allow for future extensibility (such as the human readable description texts that we have added). -#### Example +==== Example ```json { @@ -234,7 +235,4 @@ By following the Solidity JSON ABI format for the inputs, we followed an establi } } } -``` - - -// TODO merge this sections and clean up redundancy, consider also framework and core sections. \ No newline at end of file +``` \ No newline at end of file diff --git a/packages/contracts/docs/modules/ROOT/pages/guide-develop-plugin/upgrade-plugin.adoc b/packages/contracts/docs/modules/ROOT/pages/guide-develop-plugin/upgrade-plugin.adoc index 055814855..b3bfe2052 100644 --- a/packages/contracts/docs/modules/ROOT/pages/guide-develop-plugin/upgrade-plugin.adoc +++ b/packages/contracts/docs/modules/ROOT/pages/guide-develop-plugin/upgrade-plugin.adoc @@ -1,23 +1,184 @@ -= Subsequent Builds - - -== How to upgrade an Upgradeable Plugin += Upgrading your plugin Updating an Upgradeable plugin means we want to direct the implementation of our functionality to a new build, rather than the existing one. In this tutorial, we will go through how to update the version of an Upgradeable plugin and each component needed. -### 1. Create the new build implementation contract +NOTE: You can skip this section if you are working on a non-upgradeable plugin. + +== How to create new builds to an Upgradeable Plugin + +A build is a new implementation of your Upgradeable Plugin. Upgradeable contracts offer advantages because you can cheaply change +or fix the logic of your contract without losing the storage of your contract. + +The Aragon OSx protocol has an on-chain versioning system built-in, which distinguishes between releases and builds. + +- **Releases** contain breaking changes, which are incompatible with preexisting installations. Updates to a different release are +not possible. Instead, you must install the new plugin release and uninstall the old one. +- **Builds** are minor/patch versions within a release, and they are meant for compatible upgrades +only (adding a feature or fixing a bug without changing anything else). + +In this section, we'll go through how we can create these builds for our plugins. Specifically, we'll showcase two specific +types of builds - one that modifies the storage of the plugins, another one which modifies its bytecode. Both are possible and +can be implemented within the same build implementation as well. + +Before moving on, make sure you have at least one build already deployed and published into the Aragon protocol, you can check out our xref:guide-develop-plugin/publishing-plugin.adoc[publishing guide] to ensure this step is done. + +=== Create a new build implementation modifying storage + +In this second build implementation we want to update the functionality of our plugin - in this case, we want to update +the storage of our plugin with new values. Specifically, we will add a second storage variable `address public account`. +Additional to the `initializeFromBuild2` function, we also want to add a second setter function `storeAccount` that uses +the same permission as `storeNumber`. -Firstly, you want to create the new build implementation contract the plugin should use. You can read more about how to do -this in the xref:guide-develop-plugin/upgrade-plugin.adoc[How to create a subsequent build implementation to an Upgradeable Plugin] guide. +As you can see, we're still inheriting from the `PluginUUPSUpgradeable` contract and simply overriding some implementation +from the previous build. The idea is that when someone upgrades the plugin and calls on these functions, they'll use this +new upgraded implementation, rather than the older one. ```solidity -// SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity 0.8.21; +import {PluginUUPSUpgradeable, IDAO} '@aragon/osx/core/plugin/PluginUUPSUpgradeable.sol'; -import {IDAO, PluginUUPSUpgradeable} from '@aragon/osx/core/plugin/PluginUUPSUpgradeable.sol'; +/// @title SimpleStorage build 2 +contract SimpleStorageBuild2 is PluginUUPSUpgradeable { + bytes32 public constant STORE_PERMISSION_ID = keccak256('STORE_PERMISSION'); + + uint256 public number; // added in build 1 + address public account; // added in build 2 + + /// @notice Initializes the plugin when build 2 is installed. + function initializeBuild2( + IDAO _dao, + uint256 _number, + address _account + ) external reinitializer(2) { + __PluginUUPSUpgradeable_init(_dao); + number = _number; + account = _account; + } + + /// @notice Initializes the plugin when the update from build 1 to build 2 is applied. + /// @dev The initialization of `SimpleStorageBuild1` has already happened. + function initializeFromBuild1(address _account) external reinitializer(2) { + account = _account; + } + + function storeNumber(uint256 _number) external auth(STORE_PERMISSION_ID) { + number = _number; + } + + function storeAccount(address _account) external auth(STORE_PERMISSION_ID) { + account = _account; + } +} +``` + +Builds that you publish don't necessarily need to introduce new storage variables of your contracts and don't necessarily need to +change the storage. To read more about Upgradeability, check out link:https://docs.openzeppelin.com/contracts/4.x/api/proxy#UUPSUpgradeable[OpenZeppelin's UUPSUpgradeability implementation here]. + +NOTE: Note that because these contracts are Upgradeable, keeping storage gaps `uint256 [50] __gap;` in dependencies is a must in +order to avoid storage corruption. To learn more about storage gaps, review OpenZeppelin's documentation link:https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable#storage-gaps[here]. + +=== Create a build implementation modifying bytecode + +Updates for your contracts don't necessarily need to affect the storage, they can also modify the plugin's bytecode. +Modifying the contract's bytecode, means making changes to: + +- functions +- constants +- immutables +- events +- errors + +For this third build, then, we want to change the bytecode of our implementation as an example, so we 've introduced two +separate permissions for the `storeNumber` and `storeAccount` functions and named them `STORE_NUMBER_PERMISSION_ID` and `STORE_ACCOUNT_PERMISSION_ID` permission, respectively. +Additionally, we decided to add the `NumberStored` and `AccountStored` events as well as an error preventing users from setting the +same value twice. All these changes only affect the contract bytecode and not the storage. + +Here, it is important to remember how Solidity stores `constant`s (and `immutable`s). In contrast to normal variables, they are directly +written into the bytecode on contract creation so that we don't need to worry that the second `bytes32` constant that we added +shifts down the storage so that the value in `uint256 public number` gets lost. It is also important to note that, the `initializeFromBuild2` +could be left empty. Here, we just emit the events with the currently stored values. + +```solidity +import {PluginUUPSUpgradeable, IDAO} '@aragon/osx/core/plugin/PluginUUPSUpgradeable.sol'; + +/// @title SimpleStorage build 3 +contract SimpleStorageBuild3 is PluginUUPSUpgradeable { + bytes32 public constant STORE_NUMBER_PERMISSION_ID = keccak256('STORE_NUMBER_PERMISSION'); // changed in build 3 + bytes32 public constant STORE_ACCOUNT_PERMISSION_ID = keccak256('STORE_ACCOUNT_PERMISSION'); // added in build 3 + + uint256 public number; // added in build 1 + address public account; // added in build 2 + + // added in build 3 + event NumberStored(uint256 number); + event AccountStored(address number); + error AlreadyStored(); + + /// @notice Initializes the plugin when build 3 is installed. + function initializeBuild3( + IDAO _dao, + uint256 _number, + address _account + ) external reinitializer(3) { + __PluginUUPSUpgradeable_init(_dao); + number = _number; + account = _account; + + emit NumberStored({number: _number}); + emit AccountStored({account: _account}); + } + + /// @notice Initializes the plugin when the update from build 2 to build 3 is applied. + /// @dev The initialization of `SimpleStorageBuild2` has already happened. + function initializeFromBuild2() external reinitializer(3) { + emit NumberStored({number: number}); + emit AccountStored({account: account}); + } + + /// @notice Initializes the plugin when the update from build 1 to build 3 is applied. + /// @dev The initialization of `SimpleStorageBuild1` has already happened. + function initializeFromBuild1(address _account) external reinitializer(3) { + account = _account; + + emit NumberStored({number: number}); + emit AccountStored({account: _account}); + } + + function storeNumber(uint256 _number) external auth(STORE_NUMBER_PERMISSION_ID) { + if (_number == number) revert AlreadyStored(); + + number = _number; + + emit NumberStored({number: _number}); + } + + function storeAccount(address _account) external auth(STORE_ACCOUNT_PERMISSION_ID) { + if (_account == account) revert AlreadyStored(); + + account = _account; + + emit AccountStored({account: _account}); + } +} +``` + +NOTE: Despite no storage-related changes happening in build 3, we must apply the `reinitializer(3)` modifier to all `initialize` functions so that +none of them can be called twice or in the wrong order. + + +== How to upgrade your plugin + +Now that we understand how to create new builds, let's walk through the process of upgrading your plugin to use them, while maintaining your plugin's functionality and data integrity. + + +=== Create the new build implementation contract + +In the previous section, we created a new build implementation contract, we will use this as the new implementation for our plugin. + +```solidity +import {PluginUUPSUpgradeable, IDAO} '@aragon/osx/core/plugin/PluginUUPSUpgradeable.sol'; /// @title SimpleStorage build 2 contract SimpleStorageBuild2 is PluginUUPSUpgradeable { @@ -53,15 +214,14 @@ contract SimpleStorageBuild2 is PluginUUPSUpgradeable { } ``` -### 2. Write a new Plugin Setup contract +=== Write a new Plugin Setup contract In order to do update a plugin, we need a `prepareUpdate()` function in our Plugin Setup contract which points the functionality to a -new build, as we described in the xref:guide-develop-plugin/upgrade-plugin.adoc[How to create a subsequent build implementation to an Upgradeable Plugin] guide. +new build, as we described before. The `prepareUpdate()` function must transition the plugin from the old build state into the new one so that it ends up having the same permissions (and helpers) as if it had been freshly installed. -In contrast to the original build 1, build 2 requires two input arguments: `uint256 _number` and `address _account` that we decode -from the bytes-encoded input `_data`. +In contrast to the original build 1, build 2 requires two input arguments: `uint256 _number` and `address _account` that we decode from the bytes-encoded input `_data`. ```solidity // SPDX-License-Identifier: AGPL-3.0-or-later @@ -151,12 +311,9 @@ contract SimpleStorageBuild2Setup is PluginSetup { } ``` -The key thing to review in this new Plugin Setup contract is its `prepareUpdate()` function. The function only contains a -condition checking from which build number the update is transitioning to build `2`. Here, it is the build number `1` as this is the -only update path we support. Inside, we decode the `address _account` input argument provided with `bytes _data` and pass -it to the `initializeFromBuild1` function taking care of initializing the storage that was added in this build. +The key thing to review in this new Plugin Setup contract is its `prepareUpdate()` function. The function only contains a condition checking from which build number the update is transitioning to build `2`. Here, it is the build number `1` as this is the only update path we support. Inside, we decode the `address _account` input argument provided with `bytes _data` and pass it to the `initializeFromBuild1` function taking care of initializing the storage that was added in this build. -### 3. Future builds +=== Future builds For each build we add, we will need to add a `prepareUpdate()` function with any parameters needed to update to that implementation. @@ -367,170 +524,4 @@ In the second case, `initializeFromBuild2` is called taking care of initializing Lastly, the `prepareUpdate()` function takes care of modifying the permissions by revoking the `STORE_PERMISSION_ID` and granting the more specific `STORE_NUMBER_PERMISSION_ID` and `STORE_ACCOUNT_PERMISSION_ID` permissions, that are also granted if build 2 is -freshly installed. This must happen for both update paths so this code is outside the `if` statements. - -== How to create a subsequent build to an Upgradeable Plugin - -A build is a new implementation of your Upgradeable Plugin. Upgradeable contracts offer advantages because you can cheaply change -or fix the logic of your contract without losing the storage of your contract. - -The Aragon OSx protocol has an on-chain versioning system built-in, which distinguishes between releases and builds. - -- **Releases** contain breaking changes, which are incompatible with preexisting installations. Updates to a different release are -not possible. Instead, you must install the new plugin release and uninstall the old one. -- **Builds** are minor/patch versions within a release, and they are meant for compatible upgrades -only (adding a feature or fixing a bug without changing anything else). - -In this how to guide, we'll go through how we can create these builds for our plugins. Specifically, we'll showcase two specific -types of builds - one that modifies the storage of the plugins, another one which modifies its bytecode. Both are possible and -can be implemented within the same build implementation as well. - -### 1. Make sure your previous build is deployed and published - -Make sure you have at least one build already deployed and published into the Aragon protocol. Make sure to check out our -xref:guide-develop-plugin/publishing-plugin.adoc[publishing guide] to ensure this step is done. - -### 2. Create a new build implementation - -In this second build implementation we want to update the functionality of our plugin - in this case, we want to update -the storage of our plugin with new values. Specifically, we will add a second storage variable `address public account;`. -Additional to the `initializeFromBuild2` function, we also want to add a second setter function `storeAccount` that uses -the same permission as `storeNumber`. - -As you can see, we're still inheriting from the `PluginUUPSUpgradeable` contract and simply overriding some implementation -from the previous build. The idea is that when someone upgrades the plugin and calls on these functions, they'll use this -new upgraded implementation, rather than the older one. - -```solidity -import {PluginUUPSUpgradeable, IDAO} '@aragon/osx/core/plugin/PluginUUPSUpgradeable.sol'; - -/// @title SimpleStorage build 2 -contract SimpleStorageBuild2 is PluginUUPSUpgradeable { - bytes32 public constant STORE_PERMISSION_ID = keccak256('STORE_PERMISSION'); - - uint256 public number; // added in build 1 - address public account; // added in build 2 - - /// @notice Initializes the plugin when build 2 is installed. - function initializeBuild2( - IDAO _dao, - uint256 _number, - address _account - ) external reinitializer(2) { - __PluginUUPSUpgradeable_init(_dao); - number = _number; - account = _account; - } - - /// @notice Initializes the plugin when the update from build 1 to build 2 is applied. - /// @dev The initialization of `SimpleStorageBuild1` has already happened. - function initializeFromBuild1(address _account) external reinitializer(2) { - account = _account; - } - - function storeNumber(uint256 _number) external auth(STORE_PERMISSION_ID) { - number = _number; - } - - function storeAccount(address _account) external auth(STORE_PERMISSION_ID) { - account = _account; - } -} -``` - -Builds that you publish don't necessarily need to introduce new storage variables of your contracts and don't necessarily need to -change the storage. To read more about Upgradeability, check out link:https://docs.openzeppelin.com/contracts/4.x/api/proxy#UUPSUpgradeable[OpenZeppelin's UUPSUpgradeability implementation here]. - -NOTE: Note that because these contracts are Upgradeable, keeping storage gaps `uint256 [50] __gap;` in dependencies is a must in -order to avoid storage corruption. To learn more about storage gaps, review OpenZeppelin's documentation link:https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable#storage-gaps[here]. - -### 3. Alternatively, a build implementation modifying bytecode - -Updates for your contracts don't necessarily need to affect the storage, they can also modify the plugin's bytecode. -Modifying the contract's bytecode, means making changes to: - -- functions -- constants -- immutables -- events -- errors - -For this third build, then, we want to change the bytecode of our implementation as an example, so we 've introduced two -separate permissions for the `storeNumber` and `storeAccount` functions and named them `STORE_NUMBER_PERMISSION_ID` and `STORE_ACCOUNT_PERMISSION_ID` permission, respectively. -Additionally, we decided to add the `NumberStored` and `AccountStored` events as well as an error preventing users from setting the -same value twice. All these changes only affect the contract bytecode and not the storage. - -Here, it is important to remember how Solidity stores `constant`s (and `immutable`s). In contrast to normal variables, they are directly -written into the bytecode on contract creation so that we don't need to worry that the second `bytes32` constant that we added -shifts down the storage so that the value in `uint256 public number` gets lost. It is also important to note that, the `initializeFromBuild2` -could be left empty. Here, we just emit the events with the currently stored values. - -```solidity -import {PluginUUPSUpgradeable, IDAO} '@aragon/osx/core/plugin/PluginUUPSUpgradeable.sol'; - -/// @title SimpleStorage build 3 -contract SimpleStorageBuild3 is PluginUUPSUpgradeable { - bytes32 public constant STORE_NUMBER_PERMISSION_ID = keccak256('STORE_NUMBER_PERMISSION'); // changed in build 3 - bytes32 public constant STORE_ACCOUNT_PERMISSION_ID = keccak256('STORE_ACCOUNT_PERMISSION'); // added in build 3 - - uint256 public number; // added in build 1 - address public account; // added in build 2 - - // added in build 3 - event NumberStored(uint256 number); - event AccountStored(address number); - error AlreadyStored(); - - /// @notice Initializes the plugin when build 3 is installed. - function initializeBuild3( - IDAO _dao, - uint256 _number, - address _account - ) external reinitializer(3) { - __PluginUUPSUpgradeable_init(_dao); - number = _number; - account = _account; - - emit NumberStored({number: _number}); - emit AccountStored({account: _account}); - } - - /// @notice Initializes the plugin when the update from build 2 to build 3 is applied. - /// @dev The initialization of `SimpleStorageBuild2` has already happened. - function initializeFromBuild2() external reinitializer(3) { - emit NumberStored({number: number}); - emit AccountStored({account: account}); - } - - /// @notice Initializes the plugin when the update from build 1 to build 3 is applied. - /// @dev The initialization of `SimpleStorageBuild1` has already happened. - function initializeFromBuild1(address _account) external reinitializer(3) { - account = _account; - - emit NumberStored({number: number}); - emit AccountStored({account: _account}); - } - - function storeNumber(uint256 _number) external auth(STORE_NUMBER_PERMISSION_ID) { - if (_number == number) revert AlreadyStored(); - - number = _number; - - emit NumberStored({number: _number}); - } - - function storeAccount(address _account) external auth(STORE_ACCOUNT_PERMISSION_ID) { - if (_account == account) revert AlreadyStored(); - - account = _account; - - emit AccountStored({account: _account}); - } -} -``` - -NOTE: Despite no storage-related changes happening in build 3, we must apply the `reinitializer(3)` modifier to all `initialize` functions so that -none of them can be called twice or in the wrong order. - - -// TODO merge this sections and clean up redundancy \ No newline at end of file +freshly installed. This must happen for both update paths so this code is outside the `if` statements. \ No newline at end of file diff --git a/packages/contracts/docs/modules/ROOT/pages/guide-develop-plugin/write-plugin-contract.adoc b/packages/contracts/docs/modules/ROOT/pages/guide-develop-plugin/write-plugin-contract.adoc index 4d0f4c834..1e057fb68 100644 --- a/packages/contracts/docs/modules/ROOT/pages/guide-develop-plugin/write-plugin-contract.adoc +++ b/packages/contracts/docs/modules/ROOT/pages/guide-develop-plugin/write-plugin-contract.adoc @@ -1,7 +1,5 @@ = Plugin Contract -// == How to develop Plugin - This section will be focuses on non-upgradeable plugins development, for upgradeable plugins please check out our xref:guide-develop-plugin/write-upgradeable-plugin.adoc[guide here]. Before continuing make sure you've read our documentation on xref:guide-develop-plugin/design-your-plugin.adoc#choosing_the_plugin_upgradeability[Choosing the Best Type for Your Plugin] to make sure you're selecting the right type of contract for your Plugin. @@ -21,16 +19,13 @@ contract SimpleAdmin is Plugin { } ``` -The way we set up the plugin's `initialize()` function depends on the plugin type selected. To review plugin types in depth, check out -our xref:guide-develop-plugin/design-your-plugin.adoc#choosing_the_plugin_upgradeability[guide here]. - -Additionally, the way we deploy our contracts is directly correlated with how they're initialized. For Non-Upgradeable Plugins, +The way we set up the plugin's `initialize()` function depends on the plugin type selected. Additionally, the way we deploy our contracts is directly correlated with how they're initialized. For Non-Upgradeable Plugins, there's two ways in which we can deploy our plugin: -- Deployment via Solidity's `new` keyword, OR +- Deployment via Solidity's `new` keyword, or - Deployment via the Minimal Proxy Pattern -### Option A: Deployment via Solidity's `new` Keyword +=== Option A: Deployment via Solidity's `new` Keyword To instantiate the contract via Solidity's `new` keyword, you should inherit from the `Plugin` Base Template Aragon created. You can find it link:https://github.com/aragon/osx-commons/blob/develop/contracts/src/plugin/Plugin.sol[here]. @@ -62,7 +57,7 @@ the constructor. This type of constructor implementation stores the `IDAO _dao` reference in the right place. If your plugin is deployed often, which we could expect, we can link:https://blog.openzeppelin.com/workshop-recap-cheap-contract-deployment-through-clones/[save significant amounts of gas by deployment through using the minimal proxy pattern]. -### Option B: Deployment via the Minimal Proxy Pattern +=== Option B: Deployment via the Minimal Proxy Pattern To deploy our plugin via the link:https://eips.ethereum.org/EIPS/eip-1167(minimal clones pattern (ERC-1167)), you inherit from the `PluginCloneable` contract introducing the same features as `Plugin`. The only difference is that you now have to remember to write an `initialize` function. @@ -101,7 +96,7 @@ with a DAO and cannot use the DAO's `PermissionManager`. Once we've initialized our plugin (take a look at our guide on xref:how_to_initialize_the_plugin[how to initialize the plugin here]), we can start using the Non-Upgradeable Base Template to perform actions on the DAO. -### 1. Set the Permission Identifier +=== 1. Set the Permission Identifier Firstly, we want to define a xref:core/permissions.adoc#permission_identifiers[permission identifier] `bytes32` constant at the top of the contract and establish a `keccak256` hash of the permission name we want to choose. @@ -135,7 +130,7 @@ However, it is important that the permission names are descriptive and cannot be Setting this permission is key because it ensures only signers who have been granted that permission are able to execute functions. -### 2. Add the logic implementation +=== 2. Add the logic implementation Now that we have created the permission, we will use it to protect the implementation. We want to make sure only the authorized callers holding the `ADMIN_EXECUTE_PERMISSION`, can use the function. @@ -178,7 +173,7 @@ For now, we used default values for the `callId` and `allowFailureMap` parameter With this, the plugin implementation could be used and deployed already. Feel free to add any additional logic to your plugin's capabilities here. -### 3. Plugin done, Setup contract next! +=== 3. Plugin done, Setup contract next! Now that we have the logic for the plugin implemented, we'll need to define how this plugin should be installed/uninstalled from a DAO. In the next step, we'll write the `PluginSetup` contract - the one containing the installation, uninstallation, and diff --git a/packages/contracts/docs/modules/ROOT/pages/guide-develop-plugin/write-plugin-setup-contract.adoc b/packages/contracts/docs/modules/ROOT/pages/guide-develop-plugin/write-plugin-setup-contract.adoc index ea9f4dae2..b18423c88 100644 --- a/packages/contracts/docs/modules/ROOT/pages/guide-develop-plugin/write-plugin-setup-contract.adoc +++ b/packages/contracts/docs/modules/ROOT/pages/guide-develop-plugin/write-plugin-setup-contract.adoc @@ -1,12 +1,15 @@ = Plugin Setup Contract + +NOTE: This section is a continuation of the xref:guide-develop-plugin/write-plugin-contract.adoc[Writing your plugin contract], and is also focused on non-upgradeable plugins, for upgradeable plugins, see xref:guide-develop-plugin/write-upgradeable-plugin.adoc[Writing your upgradeable plugin]. + == What is the Plugin Setup contract? The Plugin Setup contract is the contract defining the instructions for installing, uninstalling, or upgrading plugins into DAOs. This contract prepares the permission granting or revoking that needs to happen in order for plugins to be able to perform actions on behalf of the DAO. You need it for the plugin to be installed into the DAO. -### 1. Finish the Plugin contract +=== 1. Finish the Plugin contract Before building your Plugin Setup contract, make sure you have the logic for your plugin implemented. In this case, we're building a simple admin plugin which grants one address permission to execute actions on behalf of the DAO. @@ -33,7 +36,7 @@ contract SimpleAdmin is PluginCloneable { } ``` -### 2. How to initialize the Plugin Setup contract +=== 2. How to initialize the Plugin Setup contract Each `PluginSetup` contract is deployed only once and we will publish a separate `PluginSetup` instance for each version. Accordingly, we instantiate the `implementation` contract via Solidity's `new` keyword as deployment with the minimal proxy pattern would be more expensive in this case. @@ -60,7 +63,7 @@ contract SimpleAdminSetup is PluginSetup { } ``` -### 3. Build the Skeleton +=== 3. Build the Skeleton In order for the Plugin to be easily installed into the DAO, we need to define the permissions the plugin will need. @@ -107,7 +110,7 @@ As you can see, we have a constructor storing the implementation contract instan Next, we will add the implementation for the `prepareInstallation` and `prepareUninstallation` functions. -### 4. Implementing the `prepareInstallation()` function +=== 4. Implementing the `prepareInstallation()` function The `prepareInstallation()` function should take in two parameters: @@ -183,7 +186,7 @@ Finally, we construct and return an array with the permissions that we need for - First, we request granting the `ADMIN_EXECUTE_PERMISSION_ID` to the `admin` address received. This is what gives the address access to use `plugin`'s functionality - in this case, call on the plugin's `execute` function so it can execute actions on behalf of the DAO. - Second, we request that our newly deployed plugin can use the `EXECUTE_PERMISSION_ID` permission on the `_dao`. We don't add conditions to the permissions in this case, so we use the `NO_CONDITION` constant provided by `PermissionLib`. -### 5. Implementing the `prepareUninstallation()` function +=== 5. Implementing the `prepareUninstallation()` function For the uninstallation, we have to make sure to revoke the two permissions that have been granted during the installation process. First, we revoke the `ADMIN_EXECUTE_PERMISSION_ID` from the `admin` address that we have stored in the implementation contract. @@ -219,7 +222,7 @@ function prepareUninstallation( } ``` -#### 6. Putting Everything Together +=== 6. Putting Everything Together Now, it's time to wrap up everything together. You should have a contract that looks like this: @@ -330,7 +333,7 @@ contract SimpleAdminSetup is PluginSetup { Once done, our plugin is ready to be published on the Aragon plugin registry. With the address of the `SimpleAdminSetup` contract, we are ready for creating our `PluginRepo`, the plugin's repository where all plugin versions will live. Check out our how to guides on xref:guide-develop-plugin/publishing-plugin.adoc[publishing your plugin here]. -### In the future: Subsequent Builds +=== In the future: Subsequent Builds For subsequent builds or releases of your plugin, you'll simply write a new implementation and associated Plugin Setup contract providing a new `prepareInstallation` and `prepareUninstallation` function. diff --git a/packages/contracts/docs/modules/ROOT/pages/guide-develop-plugin/write-upgradeable-plugin.adoc b/packages/contracts/docs/modules/ROOT/pages/guide-develop-plugin/write-upgradeable-plugin.adoc index e4b64afca..dad60788b 100644 --- a/packages/contracts/docs/modules/ROOT/pages/guide-develop-plugin/write-upgradeable-plugin.adoc +++ b/packages/contracts/docs/modules/ROOT/pages/guide-develop-plugin/write-upgradeable-plugin.adoc @@ -45,7 +45,7 @@ contract SimpleStorageBuild1 is PluginUUPSUpgradeable { NOTE: Keep in mind that in order to discriminate between the different initialize functions of your different builds, we name the initialize function `initializeBuild1`. This becomes more demanding for subsequent builds of your plugin. -### Initializing Subsequent Builds +=== Initializing Subsequent Builds Since you have chosen to build an upgradeable plugin, you can publish subsequent builds of plugin and **allow the users to update from an earlier build without losing the storage**. @@ -98,17 +98,17 @@ for build 4 `reinitializer(4)` and so on. == How to build an Upgradeable Plugin implementation contract -In this guide, we'll build a `SimpleStorage` Upgradeable plugin which all it does is storing a number. +For teaching purposes, we'll build a `SimpleStorage` Upgradeable plugin which all it does is storing a number. The Plugin contract is the one containing all the logic we'd like to implement on the DAO. -### 1. Set up the initialize function +=== 1. Set up the initialize function Make sure you have the initializer of your plugin well set up. Please review xref:#how_to_initialize_upgradeable_plugins[our guide on how to do that here] if you haven't already. Once you this is done, let's dive into several implementations and builds, as can be expected for Upgradeable plugins. -### 2. Adding your plugin implementation logic +=== 2. Adding your plugin implementation logic In our first build, we want to add an authorized `storeNumber` function to the contract - allowing a caller holding the `STORE_PERMISSION_ID` permission to change the stored value similar to what we did for xref:guide-develop-plugin/write-plugin-contract.adoc[the non-upgradeable `SimpleAdmin` Plugin]. @@ -133,50 +133,17 @@ contract SimpleStorageBuild1 is PluginUUPSUpgradeable { } ``` -### 3. Plugin done, PluginSetup contract next! +=== 3. Plugin done, PluginSetup contract next! Now that we have the logic for the plugin implemented, we'll need to define how this plugin should be installed/uninstalled from a DAO. In the next step, we'll write the `PluginSetup` contract - the one containing the installation, uninstallation, and upgrading instructions for the plugin. +== Building the Plugin Setup contract -// TODO set up contract should be very similar to non-upgradeable plugin setup contract check this section and minimize redundancy -// todo consider sorting this differently if after cleaning/merging/phrasing this file is still too long +As explained in previous sections, the Plugin Setup contract is the contract defining the instructions for installing, uninstalling, or upgrading plugins into DAOs. This contract prepares the permission granting or revoking that needs to happen in order for plugins to be able to perform actions on behalf of the DAO. -== How to build the Plugin Setup Contract for Upgradeable Plugins +NOTE: Before building the Plugin Setup contract, make sure you have the logic for your plugin implemented. -The Plugin Setup contract is the contract defining the instructions for installing, uninstalling, or upgrading plugins into DAOs. -This contract prepares the permission granting or revoking that needs to happen in order for plugins to be able to perform -actions on behalf of the DAO. - -### 1. Finish the Plugin contract's first build - -Before building the Plugin Setup contract, make sure you have the logic for your plugin implemented. In this case, - we're building a simple storage plugin which stores a number. - -```solidity -// SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity 0.8.21; - -import {IDAO, PluginUUPSUpgradeable} from '@aragon/osx/core/plugin/PluginUUPSUpgradeable.sol'; - -/// @title SimpleStorage build 1 -contract SimpleStorageBuild1 is PluginUUPSUpgradeable { - bytes32 public constant STORE_PERMISSION_ID = keccak256('STORE_PERMISSION'); - - uint256 public number; // added in build 1 - - /// @notice Initializes the plugin when build 1 is installed. - function initializeBuild1(IDAO _dao, uint256 _number) external initializer { - __PluginUUPSUpgradeable_init(_dao); - number = _number; - } - - function storeNumber(uint256 _number) external auth(STORE_PERMISSION_ID) { - number = _number; - } -} -``` - -### 2. Add the `prepareInstallation()` and `prepareUninstallation()` functions +=== 1. Add the `prepareInstallation()` and `prepareUninstallation()` functions Each `PluginSetup` contract is deployed only once and each plugin version will have its own `PluginSetup` contract deployed. Accordingly, we instantiate the `implementation` contract via Solidity's `new` keyword as deployment with the minimal proxy @@ -269,7 +236,7 @@ variable `implementation` to save gas and an `implementation` function to return NOTE: Specifically important for this type of plugin is the `prepareUpdate()` function. Since we don't know the parameters we will require when updating the plugin to the next version, we can't add the `prepareUpdate()` function just yet. However, keep in mind that we will need to deploy new Plugin Setup contracts in subsequent builds to add in the `prepareUpdate()` function with each build requirements. We see this in depth in the xref:guide-develop-plugin/upgrade-plugin.adoc[How to update an Upgradeable Plugin] section. -### 3. Deployment +=== 2. Deployment Once you're done with your Plugin Setup contract, we'll need to deploy it so we can publish it into the Aragon OSx protocol. You can deploy your contract with a basic deployment script. @@ -349,7 +316,7 @@ Finally, run this in your terminal to execute the command: npx hardhat run scripts/deploy.ts ``` -### 4. Publishing the Plugin to the Aragon OSx Protocol +=== 3. Publishing the Plugin to the Aragon OSx Protocol Once done, our plugin is ready to be published on the Aragon plugin registry. With the address of the `SimpleAdminSetup` contract deployed, we're almost ready for creating our `PluginRepo`, the plugin's repository where all plugin versions will live. diff --git a/packages/contracts/docs/modules/ROOT/pages/how-to-guides/plugin-development/index.adoc b/packages/contracts/docs/modules/ROOT/pages/how-to-guides/plugin-development/index.adoc index 9ffac00d5..c7a96f764 100644 --- a/packages/contracts/docs/modules/ROOT/pages/how-to-guides/plugin-development/index.adoc +++ b/packages/contracts/docs/modules/ROOT/pages/how-to-guides/plugin-development/index.adoc @@ -62,5 +62,4 @@ For more information on how to develop a plugin, you can check our plugin develo - xref:how-to-guides/plugin-development/upgradeable-plugin/index.adoc[Upgradeable plugin] - xref:how-to-guides/plugin-development/governance-plugins/index.adoc[Governance plugin] -IMPORTANT: This plugin template uses version TODO:GIORGI `1.4.0-alpha.5` of the Aragon OSx protocol. This version is still in development and -is not audited yet. +IMPORTANT: This plugin template uses version TODO:GIORGI `1.4.0-alpha.5` of the Aragon OSx protocol. This version is still in development and is not audited yet.