https://github.com/erc6900/resources/issues/13
When replacing a plugin, it's necessary to transfer data from the old plugin to the new one. Since allowing users direct access to the plugin's internal storage can pose security risks, a view
function will be implemented in each plugin to return migration data. This approach leverages the view function to safely transfer necessary data without compromising security.
Each plugin follows semantic versioning for its versioning scheme. The scope that replacePlugin
can be applied is defined to include upgrades or downgrades that change only the patch version. This policy allows only updates with gas optimization or bug fixes, thereby preventing major / minor changes from being misrepresented as patch updates. Since versioning is defined within each plugin, there's a potential for misuse, such as significant changes being falsely presented as minor updates. Identifying such edge cases on-chain is nearly impossible, necessitating off-chain support, such as verification by a predetermined committee.
To address this, a VersionRegistry
will be established for each plugin, with the following specifications:
Only the owner of a VersionRegistry
can register new versions of a plugin.
When registering a new version of a plugin, a designated owner (or committee) checks the compatibility of the version off-chain before registration. This process aims to prevent the security risks associated with the misuse of versioning.
Each plugin can use its own VersionRegistry
.
While a single global registry could manage the versions of all plugins, this approach has several drawbacks:
Risk of centralization
Reduced flexibility for plugin developers
Difficulty in distinguishing between different plugins
If the registry were to maintain a list or mapping of compatible plugins, distinguishing them might require hashing the plugin's name or author. If the registry uses the plugin's name for identification, it could lead to issues where plugins with the same name cannot be registered.
Therefore, allowing each plugin to register its own preferred form of registry is advised. The ERC-6900 reference implementation will include a minimal interface and a basic form of the VersionRegistry
code.
Version information is retrieved from pluginMetadata
.
Consequently, a function to decode the version string into a version struct is required.
struct Version {
uint256 major;
uint256 minor;
uint256 patch;
}
replacePlugin
function replacePlugin(address oldPlugin, address newPlugin, bytes32 newManifestHash) external;
VersionRegistry
Implement a VersionRegistry
with the following interface.
interface IVersionRegistry {
/// @notice Register a new plugin version in the registry.
/// @dev This function can be restricted to only be callable by the contract owner or a specific role.
/// @param plugin The address of the plugin to register.
function registerPlugin(address plugin) external;
/// @notice Retrieve the version information of a given plugin.
/// @param plugin The address of the plugin whose version information is being queried.
/// @return The version information of the plugin.
function getPluginVersion(address plugin) external view returns (Version memory);
/// @notice Checks if the given two plugins are compatible for the replacement.
/// @param oldPlugin The address of plugin to be replaced.
/// @param newPlugin The address of plugin replacing the existing plugin.
/// @return A boolean indicating the compatibility of two plugins.
function isPluginCompatible(address oldPlugin, address newPlugin) external view returns (bool);
}
Register each plugin in the registry before deployment. Accordingly, the form of PluginManifest
needs to be changed as follows.
struct PluginManifest {
// List of ERC-165 interface IDs to add to account to support introspection checks. This MUST NOT include
// IPlugin's interface ID.
bytes4[] interfaceIds;
// If this plugin depends on other plugins' validation functions, the interface IDs of those plugins MUST be
// provided here, with its position in the array matching the `dependencyIndex` members of `ManifestFunction`
// structs used in the manifest.
bytes4[] dependencyInterfaceIds;
// Execution functions defined in this plugin to be installed on the MSCA.
bytes4[] executionFunctions;
// Plugin execution functions already installed on the MSCA that this plugin will be able to call.
bytes4[] permittedExecutionSelectors;
// Boolean to indicate whether the plugin can call any external address.
bool permitAnyExternalAddress;
// Boolean to indicate whether the plugin needs access to spend native tokens of the account. If false, the
// plugin MUST still be able to spend up to the balance that it sends to the account in the same call.
bool canSpendNativeToken;
**// address of version registry for this plugin, which is used in replacePlugin operation.
address versionRegistry;**
ManifestExternalCallPermission[] permittedExternalCalls;
ManifestAssociatedFunction[] userOpValidationFunctions;
ManifestAssociatedFunction[] runtimeValidationFunctions;
ManifestAssociatedFunction[] preUserOpValidationHooks;
ManifestAssociatedFunction[] preRuntimeValidationHooks;
ManifestExecutionHook[] executionHooks;
}
Data Migration during replacement
Add the following functions within IPlugin
.
/// @notice Retrieves data for migrating from the old plugin to a new plugin.
/// @dev Called by the plugin manager during the plugin replacement process.
/// It should return all the necessary state information of the plugin in a serialized format.
/// In the case of SingleOwnerPlugin, it returns the owner's address.
/// @return bytes Migration data to migrate from old plugin to new plugin
function getDataForReplacement() external view returns (bytes memory);
/// @notice Cleans up the plugin data when the plugin is being replaced.
/// @dev This function is called during the plugin replacement process to allow the current (old) plugin
/// to clean up its data or state before being replaced. For the SingleOwnerPlugin, this might involve
/// resetting ownership information.
function onReplaceForOldPlugin() external;
/// @notice Initialize new plugin with migrated data.
/// @dev Called during the plugin replacement process. This function initializes the state of the new plugin
/// with the data provided. For SingleOwnerPlugin, it sets the new owner based on the migrated data.
/// @param migrationData Migrationdata from old plugin, exported form getDataForMigration() function.
function onReplaceForNewPlugin(bytes memory migrationData) external;
The migrationData
entering onReplaceForNewPlugin
is the data taken from getDataForReplacement
.
We are currently contemplating the following aspects.
In replacePlugin
, we are changing the addresses of stored values using previously defined functions as follows (an example in replacing UserOpValidation function):
_removeUserOpValidationFunction(
mv.executionSelector,
_resolveManifestFunction(
mv.associatedFunction, oldPlugin, dependencies, ManifestAssociatedFunctionType.NONE
)
);
_addUserOpValidationFunction(
mv.executionSelector,
_resolveManifestFunction(
mv.associatedFunction, newPlugin, dependencies, ManifestAssociatedFunctionType.NONE
)
);
We are seeking a more gas-efficient method that uses less bytecode. Currently, implementing in this manner results in the UpgradeableModularAccount
exceeding the size limit of 24KB.
We are deliberating whether it is appropriate to design the system so that each plugin can be linked to its own Registry. The main issue with this approach is that plugin developers might need to implement not only the plugin but also the VersionRegistry
. This could potentially lower the plugin development experience.
On the other hand, if we use a single global VersionRegistry, it could lead to some issues like centralization and reduced flexibility, as mentioned above. We are currently weighing this tradeoff to determine the best solution.
To test the current code, it is necessary to hardcode the registry address in advance (as defined in the Manifest
). Currently, we are writing the code by hardcoding it in the BasePlugin
, which could confuse plugin developers when moving to production level (for example, if they deploy a plugin without modifying this address). Is there a cleaner way to solve this problem?