Writing an 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. If you want to review plugin types in depth, check out our guide on plugin types here.

The drawbacks however, are that:

  • there are plenty of ways to make a mistake, and

  • the changeable logic poses a new attack surface.

Although we’ve abstracted away most of the complications of the upgrade process through our PluginUUPSUpgradeable base class, please know that writing an upgradeable contract is an advanced topic.

Developing upgradeable plugins can be challenging, ensure you are familiar with Upgradeable contracts development before you start.

How to Initialize Upgradeable Plugins

To deploy your implementation contract via the UUPS pattern (ERC-1822), you inherit from the PluginUUPSUpgradeable contract.

We must protect it from being set up multiple times by using OpenZeppelin’s initializer modifier made available through Initializable. In order to do this, we will call the internal function __PluginUUPSUpgradeable_init(IDAO _dao) function available through the PluginUUPSUpgradeable base contract to store the IDAO _dao reference in the right place.

This has to be called - otherwise, anyone else could call the plugin’s initialization with whatever params they wanted.
// SPDX-License-Identifier: AGPL-3.0-or-later
pragma solidity 0.8.21;

import {PluginUUPSUpgradeable, IDAO} '@aragon/osx/core/plugin/PluginUUPSUpgradeable.sol';

/// @title SimpleStorage build 1
contract SimpleStorageBuild1 is PluginUUPSUpgradeable {
  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;
  }
}
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

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.

Do not inherit from previous versions as this can mess up the inheritance chain. Instead, write self-contained contracts by simply copying the code or modifying the file in your git repo.

In this example, we wrote a SimpleStorageBuild2 contract and added a new storage variable address public account;. Because users can freshly install the new version or update from build 1, we now have to write two initializer functions: initializeBuild2 and initializeFromBuild1 in our Plugin implementation contract.

/// @title SimpleStorage build 2
contract SimpleStorageBuild2 is PluginUUPSUpgradeable {
  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(IDAO _dao, address _account) external reinitializer(2) {
    account = _account;
  }
}

In general, for each version for which you want to support updates from, you have to provide a separate initializeFromBuildX function taking care of initializing the storage and transferring the helpers and permissions of the previous version into the same state as if it had been freshly installed.

Each initializeBuildX must be protected with a modifier that allows it to be only called once.

In contrast to build 1, we now must use OpenZeppelin’s modifier reinitializer(uint8 build) for build 2 instead of modifier initializer because it allows us to execute 255 subsequent initializations. More specifically, we used reinitializer(2) here for our build 2. Note that we could also have used function initializeBuild1(IDAO _dao, uint256 _number) external reinitializer(1) for build 1 because initializer and reinitializer(1) are equivalent statements. For build 3, we must use reinitializer(3), for build 4 reinitializer(4) and so on.

How to build an Upgradeable Plugin implementation contract

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

Make sure you have the initializer of your plugin well set up. Please review 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

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 the non-upgradeable SimpleAdmin Plugin.

import {PluginUUPSUpgradeable, IDAO} '@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;
  }
}

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

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.

Before building the Plugin Setup contract, make sure you have the logic for your plugin implemented.

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 pattern would be more expensive in this case.

In order for the Plugin to be easily installed into the DAO, we need to define the instructions for the plugin to work effectively. We have to tell the DAO’s Permission Manager which permissions it needs to grant or revoke.

Hence, we will create a prepareInstallation() function, as well as a prepareUninstallation() function. These are the functions the PluginSetupProcessor.sol (the contract in charge of installing plugins into the DAO) will use.

The prepareInstallation() function takes in two parameters:

  1. the DAO it should prepare the installation for, and

  2. the _data parameter containing all the information needed for this function to work properly, encoded as a bytes memory. In this case, we get the number we want to store.

Hence, the first thing we should do when working on the prepareInstallation() function is decode the information from the _data parameter. Similarly, the prepareUninstallation() function takes in a payload.

// SPDX-License-Identifier: AGPL-3.0-or-later

pragma solidity 0.8.21;

import {PermissionLib} from '@aragon/osx/core/permission/PermissionLib.sol';
import {PluginSetup, IPluginSetup} from '@aragon/osx/framework/plugin/setup/PluginSetup.sol';
import {SimpleStorageBuild1} from './SimpleStorageBuild1.sol';

/// @title SimpleStorageSetup build 1
contract SimpleStorageBuild1Setup is PluginSetup {
  address private immutable simpleStorageImplementation;

  constructor() {
    simpleStorageImplementation = address(new SimpleStorageBuild1());
  }

  /// @inheritdoc IPluginSetup
  function prepareInstallation(
    address _dao,
    bytes memory _data
  ) external returns (address plugin, PreparedSetupData memory preparedSetupData) {
    uint256 number = abi.decode(_data, (uint256));

    plugin = createERC1967Proxy(
      simpleStorageImplementation,
      abi.encodeCall(SimpleStorageBuild1.initializeBuild1, (IDAO(_dao), number))
    );

    PermissionLib.MultiTargetPermission[]
      memory permissions = new PermissionLib.MultiTargetPermission[](1);

    permissions[0] = PermissionLib.MultiTargetPermission({
      operation: PermissionLib.Operation.Grant,
      where: plugin,
      who: _dao,
      condition: PermissionLib.NO_CONDITION,
      permissionId: SimpleStorageBuild1(this.implementation()).STORE_PERMISSION_ID()
    });

    preparedSetupData.permissions = permissions;
  }

  /// @inheritdoc IPluginSetup
  function prepareUninstallation(
    address _dao,
    SetupPayload calldata _payload
  ) external view returns (PermissionLib.MultiTargetPermission[] memory permissions) {
    permissions = new PermissionLib.MultiTargetPermission[](1);

    permissions[0] = PermissionLib.MultiTargetPermission({
      operation: PermissionLib.Operation.Revoke,
      where: _payload.plugin,
      who: _dao,
      condition: PermissionLib.NO_CONDITION,
      permissionId: SimpleStorageBuild1(this.implementation()).STORE_PERMISSION_ID()
    });
  }

  /// @inheritdoc IPluginSetup
  function implementation() external view returns (address) {
    return simpleStorageImplementation;
  }
}

As you can see, we have a constructor storing the implementation contract instantiated via the new method in the private immutable variable implementation to save gas and an implementation function to return it.

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 How to update an Upgradeable Plugin section.

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.

Firstly, we’ll make sure our preferred network is well setup within our hardhat.config.js file, which should look something like:

import '@nomicfoundation/hardhat-toolbox';

// To find your Alchemy key, go to https://dashboard.alchemy.com/. Infura or any other provider would work here as well.
const goerliAlchemyKey = 'add-your-own-alchemy-key';
// To find a private key, go to your wallet of choice and export a private key. Remember this must be kept secret at all times.
const privateKeyGoerli = 'add-your-account-private-key';

module.exports = {
  defaultNetwork: 'hardhat',
  networks: {
    hardhat: {},
    goerli: {
      url: `https://eth-goerli.g.alchemy.com/v2/${goerliAlchemyKey}`,
      accounts: [privateKeyGoerli],
    },
  },
  solidity: {
    version: '0.8.17',
    settings: {
      optimizer: {
        enabled: true,
        runs: 200,
      },
    },
  },
  paths: {
    sources: './contracts',
    tests: './test',
    cache: './cache',
    artifacts: './artifacts',
  },
  mocha: {
    timeout: 40000,
  },
};

Then, create a scripts/deploy.js file and add a simple deploy script. We’ll only be deploying the PluginSetup contract, since this should deploy the Plugin contract within its constructor.

import {ethers} from 'hardhat';

async function main() {
  const [deployer] = await ethers.getSigners();

  console.log('Deploying contracts with the account:', deployer.address);
  console.log('Account balance:', (await deployer.getBalance()).toString());

  const getSimpleStorageSetup =
    await ethers.getContractFactory('SimpleStorageSetup');
  const SimpleStorageSetup = await SimpleStorageSetup.deploy();

  await SimpleStorageSetup.deployed();

  console.log('SimpleStorageSetup address:', SimpleStorageSetup.address);
}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch(error => {
  console.error(error);
  process.exitCode = 1;
});

Finally, run this in your terminal to execute the command:

npx hardhat run scripts/deploy.ts

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. Check out our how to guides on publishing your plugin here.