설계

  1. replace할 때 기존 플러그인 내 데이터를 새로운 플러그인으로 옮길 수 있어야 한다. 이때, 사용자가 직접 플러그인 내 스토리지에 접근할 수 있도록 하는 것은 보안 상 위험한 결과를 낳을 수도 있기 때문에, 각 플러그인 내에 migration data를 반환하는 view 함수를 두어서 이를 활용한다.

  2. 각 플러그인은 semantic versioning을 따르는 버전을 가지고 있다. replacePlugin이 적용될 수 있는 범위는, patch version이 바뀌는 업그레이드 혹은 다운그레이드로 정의한다. 이는 가스비 최적화, 버그 수정 등 사소한 수준의 업데이트만 허용한다는 것을 의미한다. 그런데, 버전은 각 플러그인 내에서 정의되기 때문에, 이를 악용하는 엣지 케이스들이 발생할 수 있다 (major 또는 minor change인데도 불구하고 patch update인 척하는 경우). 이에 따라 replacePlugin 에서 이를 확인해주어야 한다. 온체인에서 이 엣지 케이스를 식별하는 것은 거의 불가능에 가깝기 때문에, 미리 지정된 committee가 확인하는 등의 오프체인 support가 필요하다.

    이를 위해 각 플러그인에 VersionRegistry를 정의할 수 있게끔 한다. 이는 다음과 같은 스펙을 가진다.

    1. 각 플러그인은 VersionRegistry의 owner만 등록할 수 있다.

      어떤 플러그인의 새로운 버전을 등록할 때, 정해진 owner (혹은 committee) 가 해당 버전의 호환성을 오프체인에서 체크 후 등록하는 방식을 택한다. 이는 위에서 언급했던 보안적인 엣지 케이스를 방지하기 위함이다.

    2. 각 플러그인이 각자의 Registry를 사용할 수 있다.

      하나의 글로벌 Registry를 사용해 모든 플러그인의 버전 관리를 사용하는 솔루션도 있을 수 있다. 그러나, 이는 다음과 같은 단점들이 존재한다.

      • 중앙화에 대한 리스크

      • 플러그인 개발자들의 자유도를 낮춤

      • 각 플러그인을 구분하기 어려움.

        Registry에서는 서로 호환되는 플러그인의 리스트나 맵핑을 유지하고 있을 텐데, 이를 구분하려면 플러그인의 이름 혹은 저자 등을 해시하는 등의 방법을 써야 할 것이다. 이때 플러그인의 이름으로 구분하는 방식을 사용한다면 같은 이름을 가진 다른 플러그인들은 등록을 하지 못하는 이슈가 있을 수 있다.

      이에 따라 각 플러그인에서 각자가 원하는 형태의 Registry를 등록할 수 있게끔 한다. ERC-6900 ref implementation에 들어갈 코드는 이에 대한 최소한의 인터페이스와, 기본적인 형태의 VersionRegistry 코드가 될 것이다.

    3. 사용되는 버전 정보는 pluginMetadata에서 가지고 온다.

      이에 따라 Version string을 아래와 같은 version struct로 디코딩하는 함수가 필요할 것이다.

      struct Version {
      	uint256 major;
      	uint256 minor;
      	uint256 patch;
      }
      

Interface

  1. replacePlugin

    function replacePlugin(address oldPlugin, address newPlugin, bytes32 newManifestHash) external;
    
  2. 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;
    }
    
  3. 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 에 들어가는 migrationDatagetDataForReplacement 에서 가져온 데이터이다.

Considerations & Questions

우리는 현재 다음과 같은 부분에서 고민을 계속하고 있다.