replace할 때 기존 플러그인 내 데이터를 새로운 플러그인으로 옮길 수 있어야 한다. 이때, 사용자가 직접 플러그인 내 스토리지에 접근할 수 있도록 하는 것은 보안 상 위험한 결과를 낳을 수도 있기 때문에, 각 플러그인 내에 migration data를 반환하는 view 함수를 두어서 이를 활용한다.
각 플러그인은 semantic versioning을 따르는 버전을 가지고 있다. replacePlugin이 적용될 수 있는 범위는, patch version이 바뀌는 업그레이드 혹은 다운그레이드로 정의한다. 이는 가스비 최적화, 버그 수정 등 사소한 수준의 업데이트만 허용한다는 것을 의미한다. 그런데, 버전은 각 플러그인 내에서 정의되기 때문에, 이를 악용하는 엣지 케이스들이 발생할 수 있다 (major 또는 minor change인데도 불구하고 patch update인 척하는 경우). 이에 따라 replacePlugin 에서 이를 확인해주어야 한다. 온체인에서 이 엣지 케이스를 식별하는 것은 거의 불가능에 가깝기 때문에, 미리 지정된 committee가 확인하는 등의 오프체인 support가 필요하다.
이를 위해 각 플러그인에 VersionRegistry를 정의할 수 있게끔 한다. 이는 다음과 같은 스펙을 가진다.
각 플러그인은 VersionRegistry의 owner만 등록할 수 있다.
어떤 플러그인의 새로운 버전을 등록할 때, 정해진 owner (혹은 committee) 가 해당 버전의 호환성을 오프체인에서 체크 후 등록하는 방식을 택한다. 이는 위에서 언급했던 보안적인 엣지 케이스를 방지하기 위함이다.
각 플러그인이 각자의 Registry를 사용할 수 있다.
하나의 글로벌 Registry를 사용해 모든 플러그인의 버전 관리를 사용하는 솔루션도 있을 수 있다. 그러나, 이는 다음과 같은 단점들이 존재한다.
중앙화에 대한 리스크
플러그인 개발자들의 자유도를 낮춤
각 플러그인을 구분하기 어려움.
Registry에서는 서로 호환되는 플러그인의 리스트나 맵핑을 유지하고 있을 텐데, 이를 구분하려면 플러그인의 이름 혹은 저자 등을 해시하는 등의 방법을 써야 할 것이다. 이때 플러그인의 이름으로 구분하는 방식을 사용한다면 같은 이름을 가진 다른 플러그인들은 등록을 하지 못하는 이슈가 있을 수 있다.
이에 따라 각 플러그인에서 각자가 원하는 형태의 Registry를 등록할 수 있게끔 한다. ERC-6900 ref implementation에 들어갈 코드는 이에 대한 최소한의 인터페이스와, 기본적인 형태의 VersionRegistry 코드가 될 것이다.
사용되는 버전 정보는 pluginMetadata에서 가지고 온다.
이에 따라 Version string을 아래와 같은 version struct로 디코딩하는 함수가 필요할 것이다.
struct Version {
	uint256 major;
	uint256 minor;
	uint256 patch;
}
replacePlugin
function replacePlugin(address oldPlugin, address newPlugin, bytes32 newManifestHash) external;
VersionRegistry
아래와 같은 인터페이스를 가진 VersionRegistry를 구현한다.
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);
}
각 플러그인마다 Registry를 배포 전에 등록한다. 이에 따라 아래와 같이 PluginManifest의 형태가 바뀌어야 한다.
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 versionRegistry;**
    ManifestExternalCallPermission[] permittedExternalCalls;
    ManifestAssociatedFunction[] userOpValidationFunctions;
    ManifestAssociatedFunction[] runtimeValidationFunctions;
    ManifestAssociatedFunction[] preUserOpValidationHooks;
    ManifestAssociatedFunction[] preRuntimeValidationHooks;
    ManifestExecutionHook[] executionHooks;
}
Data Migration during replacement
아래와 같은 함수들을 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;
이때 onReplaceForNewPlugin 에 들어가는 migrationData는 getDataForReplacement 에서 가져온 데이터이다.
우리는 현재 다음과 같은 부분에서 고민을 계속하고 있다.
replacePlugin 에서 저장된 값들의 주소를 변경하는 행위를 기존에 정의된 함수들을 써서 다음과 같은 방법을 통해 하고 있다.
_removeUserOpValidationFunction(
    mv.executionSelector,
    _resolveManifestFunction(
        mv.associatedFunction, oldPlugin, dependencies, ManifestAssociatedFunctionType.NONE
    )
);
_addUserOpValidationFunction(
    mv.executionSelector,
    _resolveManifestFunction(
        mv.associatedFunction, newPlugin, dependencies, ManifestAssociatedFunctionType.NONE
    )
);
이러한 방식보다 더 가스 효율적이고, 바이트코드를 덜 소모하는 방법을 찾고 있다. 현재 이러한 방식으로 구현하면, UpgradeableModularAccount의 크기가 24KB를 넘어가게 되는 문제가 존재한다.
플러그인 별로 각각 Registry를 연결할 수 있게끔 구현하는 것이 옳은지 고민하고 있다. 이러한 방식의 최대 문제점은, 플러그인 개발자가 플러그인 뿐만 아니라 VersionRegistry도 구현해야 할 수도 있다는 점이다. 이는 플러그인 개발 경험을 낮추는 문제로 이어질 수 있다.
한편 글로벌로 단 하나의 VersionRegistry를 사용하게 된다면, 위에서 언급했던 중앙화나 자유도의 문제가 발생할 수 있다. 우리는 현재 이 tradeoff를 고려해서 최적의 솔루션이 무엇인지 고민하고 있다.
현재 있는 코드에서 테스트를 진행하기 위해선, 레지스트리 주소를 미리 코드에 하드코딩해야 한다(Manifest에 정의하였기 때문). 현재는 BasePlugin에다가 하드코딩을 하는 방식으로 코드를 작성하고 있는데, 이 코드가 프로덕션 레벨로 가면 플러그인 개발자들에게 혼란을 줄 수도 있을 것이다(예를 들어, 이 주소를 수정 안하고 플러그인을 배포한다거나 하는 문제). 깔끔하게 이 문제를 해결할 수 있는 방법이 없을까?