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.

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 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.

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.

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 OpenZeppelin’s UUPSUpgradeability implementation here.

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 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.

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});
  }
}
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.

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;
  }
}

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 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.

// 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 {SimpleStorageBuild2} from './SimpleStorageBuild2.sol';

/// @title SimpleStorageSetup build 2
contract SimpleStorageBuild2Setup is PluginSetup {
  address private immutable simpleStorageImplementation;

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

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

    plugin = createERC1967Proxy(
      simpleStorageImplementation,
      abi.encodeWithSelector(SimpleStorageBuild2.initializeBuild2.selector, _dao, _number, _account)
    );

    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: SimpleStorageBuild2(this.implementation()).STORE_PERMISSION_ID()
    });

    preparedSetupData.permissions = permissions;
  }

  /// @inheritdoc IPluginSetup
  function prepareUpdate(
    address _dao,
    uint16 _currentBuild,
    SetupPayload calldata _payload
  )
    external
    pure
    override
    returns (bytes memory initData, PreparedSetupData memory preparedSetupData)
  {
    (_dao, preparedSetupData);

    if (_currentBuild == 0) {
      address _account = abi.decode(_payload.data, (address));
      initData = abi.encodeWithSelector(
        SimpleStorageBuild2.initializeFromBuild1.selector,
        _account
      );
    }
  }

  /// @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: SimpleStorageBuild2(this.implementation()).STORE_PERMISSION_ID()
    });
  }

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

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.

Future builds

For each build we add, we will need to add a prepareUpdate() function with any parameters needed to update to that implementation.

In this third build, for example, we are modifying the bytecode of the plugin.

Third plugin build example, modifying the plugin’s bytecode.

// 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 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 account);
  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});
  }
}

With each new build implementation, we will need to update the Plugin Setup contract to be able to update to that new version. We do this through updating the prepareUpdate() function to support any new features that need to be set up.

Third plugin setup example, modifying prepareUpdate function.

// 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 {SimpleStorageBuild2} from '../build2/SimpleStorageBuild2.sol';
import {SimpleStorageBuild3} from './SimpleStorageBuild3.sol';

/// @title SimpleStorageSetup build 3
contract SimpleStorageBuild3Setup is PluginSetup {
  address private immutable simpleStorageImplementation;

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

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

    plugin = createERC1967Proxy(
      simpleStorageImplementation,
      abi.encodeWithSelector(SimpleStorageBuild3.initializeBuild3.selector, _dao, _number, _account)
    );

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

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

    permissions[1] = permissions[0];
    permissions[1].permissionId = SimpleStorageBuild3(this.implementation())
      .STORE_ACCOUNT_PERMISSION_ID();

    preparedSetupData.permissions = permissions;
  }

  /// @inheritdoc IPluginSetup
  function prepareUpdate(
    address _dao,
    uint16 _currentBuild,
    SetupPayload calldata _payload
  )
    external
    view
    override
    returns (bytes memory initData, PreparedSetupData memory preparedSetupData)
  {
    if (_currentBuild == 0) {
      address _account = abi.decode(_payload.data, (address));
      initData = abi.encodeWithSelector(
        SimpleStorageBuild3.initializeFromBuild1.selector,
        _account
      );
    } else if (_currentBuild == 1) {
      initData = abi.encodeWithSelector(SimpleStorageBuild3.initializeFromBuild2.selector);
    }

    PermissionLib.MultiTargetPermission[]
      memory permissions = new PermissionLib.MultiTargetPermission[](3);
    permissions[0] = PermissionLib.MultiTargetPermission({
      operation: PermissionLib.Operation.Revoke,
      where: _dao,
      who: _payload.plugin,
      condition: PermissionLib.NO_CONDITION,
      permissionId: keccak256('STORE_PERMISSION')
    });

    permissions[1] = permissions[0];
    permissions[1].operation = PermissionLib.Operation.Grant;
    permissions[1].permissionId = SimpleStorageBuild3(this.implementation())
      .STORE_NUMBER_PERMISSION_ID();

    permissions[2] = permissions[1];
    permissions[2].permissionId = SimpleStorageBuild3(this.implementation())
      .STORE_ACCOUNT_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[](2);

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

    permissions[1] = permissions[1];
    permissions[1].permissionId = SimpleStorageBuild3(this.implementation())
      .STORE_ACCOUNT_PERMISSION_ID();
  }

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

In this case, the prepareUpdate() function only contains a condition checking from which build number the update is transitioning to build 2. Here, we can update from build 0 or build 1 and different operations must happen for each case to transition to SimpleAdminBuild3.

In the first case, initializeFromBuild1 is called taking care of initializing address _account that was added in build 1 and emitting the events added in build 2.

In the second case, initializeFromBuild2 is called taking care of initializing the build. Here, only the two events will be emitted.

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.