Upgrade a DAO Plugin
How to upgrade an Upgradeable 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
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 "How to create a subsequent build implementation to an Upgradeable Plugin" guide.
// 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 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;
}
}
2. 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 "How to create a subsequent build implementation to an Upgradeable Plugin" guide. 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 _date
and pass it to the initializeFromBuild1
function taking care of initializing the storage that was added in this build.
3. 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.