diff --git a/src/account/AccountLoupe.sol b/src/account/AccountLoupe.sol index ca4da1dd..dc8803d7 100644 --- a/src/account/AccountLoupe.sol +++ b/src/account/AccountLoupe.sol @@ -11,6 +11,7 @@ import { AccountStorage, getAccountStorage, SelectorData, + toFunctionReference, toFunctionReferenceArray, toExecutionHook } from "./AccountStorage.sol"; @@ -38,7 +39,7 @@ abstract contract AccountLoupe is IAccountLoupe { config.plugin = _storage.selectorData[selector].plugin; } - config.validationFunction = _storage.selectorData[selector].validation; + config.defaultValidationFunction = toFunctionReference(_storage.selectorData[selector].validations.at(0)); } /// @inheritdoc IAccountLoupe @@ -55,6 +56,10 @@ abstract contract AccountLoupe is IAccountLoupe { } } + function getValidationFunctions(bytes4 selector) external view returns (FunctionReference[] memory) { + return toFunctionReferenceArray(getAccountStorage().selectorData[selector].validations); + } + /// @inheritdoc IAccountLoupe function getPreValidationHooks(bytes4 selector) external diff --git a/src/account/AccountStorage.sol b/src/account/AccountStorage.sol index 5562fdcd..270d86a5 100644 --- a/src/account/AccountStorage.sol +++ b/src/account/AccountStorage.sol @@ -41,7 +41,7 @@ struct SelectorData { // but it packs alongside `plugin` while still leaving some other space in the slot for future packing. uint48 denyExecutionCount; // User operation validation and runtime validation share a function reference. - FunctionReference validation; + EnumerableSet.Bytes32Set validations; // The pre validation hooks for this function selector. EnumerableSet.Bytes32Set preValidationHooks; // The execution hooks for this function selector. diff --git a/src/account/PluginManagerInternals.sol b/src/account/PluginManagerInternals.sol index 8f228932..ea59ccae 100644 --- a/src/account/PluginManagerInternals.sol +++ b/src/account/PluginManagerInternals.sol @@ -82,11 +82,11 @@ abstract contract PluginManagerInternals is IPluginManager { { SelectorData storage _selectorData = getAccountStorage().selectorData[selector]; - if (_selectorData.validation.notEmpty()) { + // Fail on duplicate definitions - otherwise dependencies could shadow non-depdency + // validation functions, leading to partial uninstalls. + if (!_selectorData.validations.add(toSetValue(validationFunction))) { revert ValidationFunctionAlreadySet(selector, validationFunction); } - - _selectorData.validation = validationFunction; } function _removeValidationFunction(bytes4 selector, FunctionReference validationFunction) @@ -95,7 +95,9 @@ abstract contract PluginManagerInternals is IPluginManager { { SelectorData storage _selectorData = getAccountStorage().selectorData[selector]; - _selectorData.validation = FunctionReferenceLib._EMPTY_FUNCTION_REFERENCE; + // May ignore return value, as the manifest hash is validated to ensure that the validation function + // exists. + _selectorData.validations.remove(toSetValue(validationFunction)); } function _addExecHooks( diff --git a/src/account/UpgradeableModularAccount.sol b/src/account/UpgradeableModularAccount.sol index 47192abd..b95e2517 100644 --- a/src/account/UpgradeableModularAccount.sol +++ b/src/account/UpgradeableModularAccount.sol @@ -22,7 +22,8 @@ import { getPermittedCallKey, SelectorData, toFunctionReference, - toExecutionHook + toExecutionHook, + toSetValue } from "./AccountStorage.sol"; import {AccountStorageInitializable} from "./AccountStorageInitializable.sol"; import {PluginManagerInternals} from "./PluginManagerInternals.sol"; @@ -71,7 +72,7 @@ contract UpgradeableModularAccount is // Wraps execution of a native function with runtime validation and hooks // Used for upgradeTo, upgradeToAndCall, execute, executeBatch, installPlugin, uninstallPlugin modifier wrapNativeFunction() { - _doRuntimeValidationIfNotFromEP(); + _doRuntimeValidationIfNotFromEPorSelf(); PostExecToRun[] memory postExecHooks = _doPreExecHooks(msg.sig, msg.data); @@ -123,7 +124,7 @@ contract UpgradeableModularAccount is revert UnrecognizedFunction(msg.sig); } - _doRuntimeValidationIfNotFromEP(); + _doRuntimeValidationIfNotFromEPorSelf(); PostExecToRun[] memory postExecHooks; // Cache post-exec hooks in memory @@ -171,6 +172,33 @@ contract UpgradeableModularAccount is } } + function executeWithValidation(FunctionReference validation, bytes calldata data) + external + returns (bytes memory) + { + // If the call is from the entry point or the account itself, we skip runtime validation because + // User Op validation must have occurred. + if (msg.sender != address(_ENTRY_POINT) && msg.sender != address(this)) { + bytes4 selector = bytes4(data); // If extending, the call will fail in the sub-call anyways. + + if (!getAccountStorage().selectorData[selector].validations.contains(toSetValue(validation))) { + revert RuntimeValidationFunctionMissing(selector); + } + + _doRuntimeValidation(validation); + } + + (bool success, bytes memory returnData) = address(this).call(data); + + if (!success) { + assembly ("memory-safe") { + revert(add(returnData, 32), mload(returnData)) + } + } + + return returnData; + } + /// @inheritdoc IPluginExecutor function executeFromPlugin(bytes calldata data) external payable override returns (bytes memory) { bytes4 selector = bytes4(data[:4]); @@ -333,9 +361,28 @@ contract UpgradeableModularAccount is revert AlwaysDenyRule(); } - FunctionReference userOpValidationFunction = getAccountStorage().selectorData[selector].validation; + // Unless otherwise specified, use the validator at validations[0] as the "default validation". + // This is typically the first validation function added, but ordering is not guaranteed by the + // OZ EnumerableSet. todo: some manual control over this. + + if (selector == UpgradeableModularAccount.executeWithValidation.selector) { + (FunctionReference userOpValidationFunction, bytes memory actualData) = + abi.decode(userOp.callData[4:], (FunctionReference, bytes)); + bytes4 actualSelector = bytes4(actualData); + + if (!_storage.selectorData[actualSelector].validations.contains(toSetValue(userOpValidationFunction))) + { + revert UserOpValidationFunctionMissing(selector); + } - validationData = _doUserOpValidation(selector, userOpValidationFunction, userOp, userOpHash); + validationData = _doUserOpValidation(selector, userOpValidationFunction, userOp, userOpHash); + (userOpValidationFunction); + } else { + FunctionReference userOpValidationFunction = + toFunctionReference(getAccountStorage().selectorData[selector].validations.at(0)); + + validationData = _doUserOpValidation(selector, userOpValidationFunction, userOp, userOpHash); + } } // To support gas estimation, we don't fail early when the failure is caused by a signature failure @@ -387,16 +434,25 @@ contract UpgradeableModularAccount is } } - function _doRuntimeValidationIfNotFromEP() internal { + function _doRuntimeValidationIfNotFromEPorSelf() internal { AccountStorage storage _storage = getAccountStorage(); if (_storage.selectorData[msg.sig].denyExecutionCount > 0) { revert AlwaysDenyRule(); } - if (msg.sender == address(_ENTRY_POINT)) return; + if (msg.sender == address(_ENTRY_POINT) || msg.sender == address(this)) return; + + // Unless otherwise specified, use the validator at validations[0] as the "default validation". + // This is typically the first validation function added, but ordering is not guaranteed by the + // OZ EnumerableSet. todo: some manual control over this. + FunctionReference runtimeValidationFunction = + toFunctionReference(_storage.selectorData[msg.sig].validations.at(0)); + + _doRuntimeValidation(runtimeValidationFunction); + } - FunctionReference runtimeValidationFunction = _storage.selectorData[msg.sig].validation; + function _doRuntimeValidation(FunctionReference runtimeValidationFunction) internal { // run all preRuntimeValidation hooks EnumerableSet.Bytes32Set storage preRuntimeValidationHooks = getAccountStorage().selectorData[msg.sig].preValidationHooks; diff --git a/src/interfaces/IAccountLoupe.sol b/src/interfaces/IAccountLoupe.sol index a1b3c15f..4d30b79b 100644 --- a/src/interfaces/IAccountLoupe.sol +++ b/src/interfaces/IAccountLoupe.sol @@ -15,7 +15,7 @@ interface IAccountLoupe { /// @notice Config for an execution function, given a selector. struct ExecutionFunctionConfig { address plugin; - FunctionReference validationFunction; + FunctionReference defaultValidationFunction; } /// @notice Get the validation functions and plugin address for a selector. @@ -29,6 +29,11 @@ interface IAccountLoupe { /// @return The pre and post execution hooks for this selector. function getExecutionHooks(bytes4 selector) external view returns (ExecutionHook[] memory); + // todo: natspec + function getValidationFunctions(bytes4 selector) external view returns (FunctionReference[] memory); + + // todo: should we support a validation checking function (bytes4, FunctionReference) -> bool? + /// @notice Get the pre user op and runtime validation hooks associated with a selector. /// @param selector The selector to get the hooks for. /// @return preValidationHooks The pre validation hooks for this selector. diff --git a/src/interfaces/IStandardExecutor.sol b/src/interfaces/IStandardExecutor.sol index 30b6deee..e232671b 100644 --- a/src/interfaces/IStandardExecutor.sol +++ b/src/interfaces/IStandardExecutor.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.25; +import {FunctionReference} from "./IPluginManager.sol"; + struct Call { // The target address for the account to call. address target; @@ -25,4 +27,9 @@ interface IStandardExecutor { /// @param calls The array of calls. /// @return An array containing the return data from the calls. function executeBatch(Call[] calldata calls) external payable returns (bytes[] memory); + + // todo: natspec + function executeWithValidation(FunctionReference validation, bytes calldata data) + external + returns (bytes memory); } diff --git a/src/plugins/owner/SingleOwnerPlugin.sol b/src/plugins/owner/SingleOwnerPlugin.sol index b1d5d5e1..167df4e6 100644 --- a/src/plugins/owner/SingleOwnerPlugin.sol +++ b/src/plugins/owner/SingleOwnerPlugin.sol @@ -84,7 +84,7 @@ contract SingleOwnerPlugin is BasePlugin, ISingleOwnerPlugin, IERC1271 { { if (functionId == uint8(FunctionId.VALIDATION_OWNER_OR_SELF)) { // Validate that the sender is the owner of the account or self. - if (sender != _owners[msg.sender] && sender != msg.sender) { + if (sender != _owners[msg.sender]) { revert NotAuthorized(); } return; diff --git a/test/account/AccountLoupe.t.sol b/test/account/AccountLoupe.t.sol index 9fbc1e21..b2d58243 100644 --- a/test/account/AccountLoupe.t.sol +++ b/test/account/AccountLoupe.t.sol @@ -66,7 +66,7 @@ contract AccountLoupeTest is AccountTestBase { assertEq(config.plugin, address(account1)); assertEq( - FunctionReference.unwrap(config.validationFunction), + FunctionReference.unwrap(config.defaultValidationFunction), FunctionReference.unwrap(expectedValidations[i]) ); } @@ -93,7 +93,7 @@ contract AccountLoupeTest is AccountTestBase { assertEq(config.plugin, expectedPluginAddress[i]); assertEq( - FunctionReference.unwrap(config.validationFunction), + FunctionReference.unwrap(config.defaultValidationFunction), FunctionReference.unwrap(expectedValidations[i]) ); } diff --git a/test/account/MultiValidation.t.sol b/test/account/MultiValidation.t.sol new file mode 100644 index 00000000..83a78ead --- /dev/null +++ b/test/account/MultiValidation.t.sol @@ -0,0 +1,188 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.21; + +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol"; +import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; + +import {IEntryPoint} from "@eth-infinitism/account-abstraction/interfaces/IEntryPoint.sol"; + +import {UpgradeableModularAccount} from "../../src/account/UpgradeableModularAccount.sol"; +import {FunctionReference} from "../../src/interfaces/IPluginManager.sol"; +import {IStandardExecutor} from "../../src/interfaces/IStandardExecutor.sol"; +import {FunctionReferenceLib} from "../../src/helpers/FunctionReferenceLib.sol"; +import {SingleOwnerPlugin2} from "../mocks/plugins/SingleOwnerPlugin2.sol"; +import {ISingleOwnerPlugin} from "../../src/plugins/owner/ISingleOwnerPlugin.sol"; + +import {AccountTestBase} from "../utils/AccountTestBase.sol"; + +contract MultiValidationTest is AccountTestBase { + using ECDSA for bytes32; + using MessageHashUtils for bytes32; + + SingleOwnerPlugin2 public validator2; + + address public owner2; + uint256 public owner2Key; + + uint256 public constant CALL_GAS_LIMIT = 50000; + uint256 public constant VERIFICATION_GAS_LIMIT = 1200000; + + function setUp() public { + validator2 = new SingleOwnerPlugin2(); + + (owner2, owner2Key) = makeAddrAndKey("owner2"); + } + + function test_overlappingValidationInstall() public { + bytes32 manifestHash = keccak256(abi.encode(validator2.pluginManifest())); + vm.prank(owner1); + account1.installPlugin(address(validator2), manifestHash, abi.encode(owner2), new FunctionReference[](0)); + + FunctionReference[] memory validations = new FunctionReference[](2); + validations[0] = FunctionReferenceLib.pack( + address(singleOwnerPlugin), uint8(ISingleOwnerPlugin.FunctionId.VALIDATION_OWNER_OR_SELF) + ); + validations[1] = FunctionReferenceLib.pack( + address(validator2), uint8(ISingleOwnerPlugin.FunctionId.VALIDATION_OWNER_OR_SELF) + ); + FunctionReference[] memory validations2 = + account1.getValidationFunctions(IStandardExecutor.execute.selector); + assertEq(validations2.length, 2); + assertEq(FunctionReference.unwrap(validations2[0]), FunctionReference.unwrap(validations[0])); + assertEq(FunctionReference.unwrap(validations2[1]), FunctionReference.unwrap(validations[1])); + } + + function test_runtimeValidation_default() public { + test_overlappingValidationInstall(); + + // Assert that the default runtime validation is the first validation function. + + vm.prank(owner1); + account1.execute(address(0), 0, ""); + + vm.prank(owner2); + vm.expectRevert( + abi.encodeWithSelector( + UpgradeableModularAccount.RuntimeValidationFunctionReverted.selector, + address(singleOwnerPlugin), + 0, + abi.encodePacked(ISingleOwnerPlugin.NotAuthorized.selector) + ) + ); + account1.execute(address(0), 0, ""); + } + + function test_runtimeValidation_specify() public { + test_overlappingValidationInstall(); + + // Assert that the runtime validation can be specified. + + vm.prank(owner1); + vm.expectRevert( + abi.encodeWithSelector( + UpgradeableModularAccount.RuntimeValidationFunctionReverted.selector, + address(validator2), + 0, + abi.encodeWithSignature("Error(string)", "NotAuthorized()") + ) + ); + account1.executeWithValidation( + FunctionReferenceLib.pack( + address(validator2), uint8(ISingleOwnerPlugin.FunctionId.VALIDATION_OWNER_OR_SELF) + ), + abi.encodeCall(IStandardExecutor.execute, (address(0), 0, "")) + ); + + vm.prank(owner2); + account1.executeWithValidation( + FunctionReferenceLib.pack( + address(validator2), uint8(ISingleOwnerPlugin.FunctionId.VALIDATION_OWNER_OR_SELF) + ), + abi.encodeCall(IStandardExecutor.execute, (address(0), 0, "")) + ); + } + + function test_userOpValidation_default() public { + test_overlappingValidationInstall(); + + // Assert that the default userOp validation is the first validation function. + + PackedUserOperation memory userOp = PackedUserOperation({ + sender: address(account1), + nonce: 0, + initCode: "", + callData: abi.encodeCall(UpgradeableModularAccount.execute, (address(0), 0, "")), + accountGasLimits: _encodeGas(VERIFICATION_GAS_LIMIT, CALL_GAS_LIMIT), + preVerificationGas: 0, + gasFees: _encodeGas(1, 1), + paymasterAndData: "", + signature: "" + }); + + // Generate signature + bytes32 userOpHash = entryPoint.getUserOpHash(userOp); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(owner1Key, userOpHash.toEthSignedMessageHash()); + userOp.signature = abi.encodePacked(r, s, v); + + PackedUserOperation[] memory userOps = new PackedUserOperation[](1); + userOps[0] = userOp; + + entryPoint.handleOps(userOps, beneficiary); + + // Sign with owner 2, expect fail + userOp.nonce = 1; + (v, r, s) = vm.sign(owner2Key, userOpHash.toEthSignedMessageHash()); + userOp.signature = abi.encodePacked(r, s, v); + + userOps[0] = userOp; + vm.expectRevert(abi.encodeWithSelector(IEntryPoint.FailedOp.selector, 0, "AA24 signature error")); + entryPoint.handleOps(userOps, beneficiary); + } + + function test_userOpValidation_specify() public { + test_overlappingValidationInstall(); + + // Assert that the userOp validation can be specified. + + PackedUserOperation memory userOp = PackedUserOperation({ + sender: address(account1), + nonce: 0, + initCode: "", + callData: abi.encodeCall( + UpgradeableModularAccount.executeWithValidation, + ( + FunctionReferenceLib.pack( + address(validator2), uint8(ISingleOwnerPlugin.FunctionId.VALIDATION_OWNER_OR_SELF) + ), + abi.encodeCall(UpgradeableModularAccount.execute, (address(0), 0, "")) + ) + ), + accountGasLimits: _encodeGas(VERIFICATION_GAS_LIMIT, CALL_GAS_LIMIT), + preVerificationGas: 0, + gasFees: _encodeGas(1, 1), + paymasterAndData: "", + signature: "" + }); + + // Generate signature + bytes32 userOpHash = entryPoint.getUserOpHash(userOp); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(owner2Key, userOpHash.toEthSignedMessageHash()); + userOp.signature = abi.encodePacked(r, s, v); + + PackedUserOperation[] memory userOps = new PackedUserOperation[](1); + userOps[0] = userOp; + + entryPoint.handleOps(userOps, beneficiary); + + // Sign with owner 1, expect fail + + userOp.nonce = 1; + (v, r, s) = vm.sign(owner1Key, userOpHash.toEthSignedMessageHash()); + userOp.signature = abi.encodePacked(r, s, v); + + userOps[0] = userOp; + vm.expectRevert(abi.encodeWithSelector(IEntryPoint.FailedOp.selector, 0, "AA24 signature error")); + entryPoint.handleOps(userOps, beneficiary); + } +} diff --git a/test/mocks/plugins/SingleOwnerPlugin2.sol b/test/mocks/plugins/SingleOwnerPlugin2.sol new file mode 100644 index 00000000..e8838233 --- /dev/null +++ b/test/mocks/plugins/SingleOwnerPlugin2.sol @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.25; + +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol"; +import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; + +import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; +import {IPluginManager} from "../../../src/interfaces/IPluginManager.sol"; +import { + ManifestFunction, + ManifestAssociatedFunctionType, + ManifestAssociatedFunction, + PluginManifest, + PluginMetadata, + SelectorPermission +} from "../../../src/interfaces/IPlugin.sol"; +import {IStandardExecutor} from "../../../src/interfaces/IStandardExecutor.sol"; +import {BasePlugin} from "../../../src/plugins/BasePlugin.sol"; +import {ISingleOwnerPlugin} from "../../../src/plugins/owner/ISingleOwnerPlugin.sol"; + +/// Copy of SingleOwnerPlugin with differently-named execution functions, +// so it can be installed with overlapping validation to the regular SingleOwnerPlugin. +// Also missing isValidSignature +contract SingleOwnerPlugin2 is BasePlugin { + using ECDSA for bytes32; + using MessageHashUtils for bytes32; + + string public constant NAME = "Single Owner Plugin"; + string public constant VERSION = "1.0.0"; + string public constant AUTHOR = "ERC-6900 Authors"; + + uint256 internal constant _SIG_VALIDATION_PASSED = 0; + uint256 internal constant _SIG_VALIDATION_FAILED = 1; + + event OwnershipTransferred(address indexed account, address indexed previousOwner, address indexed newOwner); + + mapping(address => address) internal _owners; + + // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + // ┃ Execution functions ┃ + // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + + function transferOwnership2(address newOwner) external { + _transferOwnership(newOwner); + } + + // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + // ┃ Plugin interface functions ┃ + // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + + function onInstall(bytes calldata data) external override { + _transferOwnership(abi.decode(data, (address))); + } + + function onUninstall(bytes calldata) external override { + _transferOwnership(address(0)); + } + + function runtimeValidationFunction(uint8 functionId, address sender, uint256, bytes calldata) + external + view + override + { + if (functionId == uint8(ISingleOwnerPlugin.FunctionId.VALIDATION_OWNER_OR_SELF)) { + // Validate that the sender is the owner of the account or self. + if (sender != _owners[msg.sender]) { + // solhint-disable-next-line custom-errors + revert("NotAuthorized()"); + } + return; + } + revert NotImplemented(); + } + + function userOpValidationFunction(uint8 functionId, PackedUserOperation calldata userOp, bytes32 userOpHash) + external + view + override + returns (uint256) + { + if (functionId == uint8(ISingleOwnerPlugin.FunctionId.VALIDATION_OWNER_OR_SELF)) { + // Validate the user op signature against the owner. + (address signer,,) = (userOpHash.toEthSignedMessageHash()).tryRecover(userOp.signature); + if (signer == address(0) || signer != _owners[msg.sender]) { + return _SIG_VALIDATION_FAILED; + } + return _SIG_VALIDATION_PASSED; + } + revert NotImplemented(); + } + + // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + // ┃ Execution view functions ┃ + // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + + function owner2() external view returns (address) { + return _owners[msg.sender]; + } + + // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + // ┃ Plugin view functions ┃ + // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + + function ownerOf(address account) external view returns (address) { + return _owners[account]; + } + + function pluginManifest() external pure override returns (PluginManifest memory) { + PluginManifest memory manifest; + + manifest.executionFunctions = new bytes4[](2); + manifest.executionFunctions[0] = this.transferOwnership2.selector; + manifest.executionFunctions[1] = this.owner2.selector; + + ManifestFunction memory ownerValidationFunction = ManifestFunction({ + functionType: ManifestAssociatedFunctionType.SELF, + functionId: uint8(ISingleOwnerPlugin.FunctionId.VALIDATION_OWNER_OR_SELF), + dependencyIndex: 0 // Unused. + }); + manifest.validationFunctions = new ManifestAssociatedFunction[](6); + manifest.validationFunctions[0] = ManifestAssociatedFunction({ + executionSelector: this.transferOwnership2.selector, + associatedFunction: ownerValidationFunction + }); + manifest.validationFunctions[1] = ManifestAssociatedFunction({ + executionSelector: IStandardExecutor.execute.selector, + associatedFunction: ownerValidationFunction + }); + manifest.validationFunctions[2] = ManifestAssociatedFunction({ + executionSelector: IStandardExecutor.executeBatch.selector, + associatedFunction: ownerValidationFunction + }); + manifest.validationFunctions[3] = ManifestAssociatedFunction({ + executionSelector: IPluginManager.installPlugin.selector, + associatedFunction: ownerValidationFunction + }); + manifest.validationFunctions[4] = ManifestAssociatedFunction({ + executionSelector: IPluginManager.uninstallPlugin.selector, + associatedFunction: ownerValidationFunction + }); + manifest.validationFunctions[5] = ManifestAssociatedFunction({ + executionSelector: UUPSUpgradeable.upgradeToAndCall.selector, + associatedFunction: ownerValidationFunction + }); + + return manifest; + } + + function pluginMetadata() external pure virtual override returns (PluginMetadata memory) { + PluginMetadata memory metadata; + metadata.name = NAME; + metadata.version = VERSION; + metadata.author = AUTHOR; + + // Permission strings + string memory modifyOwnershipPermission = "Modify Ownership"; + + // Permission descriptions + metadata.permissionDescriptors = new SelectorPermission[](1); + metadata.permissionDescriptors[0] = SelectorPermission({ + functionSelector: this.transferOwnership2.selector, + permissionDescription: modifyOwnershipPermission + }); + + return metadata; + } + + // ┏━━━━━━━━━━━━━━━┓ + // ┃ EIP-165 ┃ + // ┗━━━━━━━━━━━━━━━┛ + + function supportsInterface(bytes4 interfaceId) public view override returns (bool) { + return interfaceId == type(ISingleOwnerPlugin).interfaceId || super.supportsInterface(interfaceId); + } + + // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + // ┃ Internal / Private functions ┃ + // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + + function _transferOwnership(address newOwner) internal { + address previousOwner = _owners[msg.sender]; + _owners[msg.sender] = newOwner; + emit OwnershipTransferred(msg.sender, previousOwner, newOwner); + } +}