Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 50 additions & 1 deletion src/account/UpgradeableModularAccount.sol
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ contract UpgradeableModularAccount is

event ModularAccountInitialized(IEntryPoint indexed entryPoint);

event GotHere();

error AuthorizeUpgradeReverted(bytes revertReason);
error ExecFromPluginNotPermitted(address plugin, bytes4 selector);
error ExecFromPluginExternalNotPermitted(address plugin, address target, uint256 value, bytes data);
Expand Down Expand Up @@ -410,14 +412,61 @@ contract UpgradeableModularAccount is
userOp.signature = signatureSegment.getBody();

(address plugin, uint8 functionId) = userOpValidationFunction.unpack();
uint256 currentValidationData = IValidation(plugin).validateUserOp(functionId, userOp, userOpHash);
(uint256 currentValidationData, bytes memory validationComposition) =
IValidation(plugin).validateUserOp(functionId, userOp, userOpHash);

if (preUserOpValidationHooks.length != 0) {
// If we have other validation data we need to coalesce with
validationData = _coalesceValidation(validationData, currentValidationData);
} else {
validationData = currentValidationData;
}

if (validationComposition.length > 0) {
// We have additional validations we need to run, in addition to the current one.

//todo: enforce user op hash uniqueness

FunctionReference[] memory chainedValidations =
abi.decode(validationComposition, (FunctionReference[]));

emit GotHere();
currentValidationData =
_doChainedValidation(chainedValidations, userOp, signatureSegment.getBody(), userOpHash);

validationData = _coalescePreValidation(validationData, currentValidationData);
}
}

return validationData;
}

function _doChainedValidation(
FunctionReference[] memory chainedValidations,
PackedUserOperation memory userOp,
bytes calldata outerSignature,
bytes32 userOpHash
) internal returns (uint256) {
uint256 validationData;
uint256 currentValidationData;

bytes calldata signatureSegment;
(signatureSegment, outerSignature) = outerSignature.getNextSegment();

for (uint256 i = 0; i < chainedValidations.length; ++i) {
if (signatureSegment.getIndex() != i) {
// For chained validation, all signature segments must be explicitly given
revert SignatureSegmentOutOfOrder();
}

currentValidationData =
_doUserOpValidation(chainedValidations[i], userOp, signatureSegment.getBody(), userOpHash);
validationData = _coalescePreValidation(validationData, currentValidationData);

// Load the next per-validation data segment, if one exists
if (i + 1 < chainedValidations.length) {
(signatureSegment, outerSignature) = outerSignature.getNextSegment();
}
}

return validationData;
Expand Down
2 changes: 1 addition & 1 deletion src/interfaces/IValidation.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ interface IValidation is IPlugin {
/// @return Packed validation data for validAfter (6 bytes), validUntil (6 bytes), and authorizer (20 bytes).
function validateUserOp(uint8 functionId, PackedUserOperation calldata userOp, bytes32 userOpHash)
external
returns (uint256);
returns (uint256, bytes memory);

/// @notice Run the runtime validationFunction specified by the `functionId`.
/// @dev To indicate the entire call should revert, the function MUST revert.
Expand Down
117 changes: 117 additions & 0 deletions src/plugins/owner/ComposableMultisigPlugin.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.25;

import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol";

import {FunctionReference} from "../../helpers/FunctionReferenceLib.sol";
import {IPlugin} from "../../interfaces/IPlugin.sol";
import {IValidation} from "../../interfaces/IValidation.sol";
import {BasePlugin} from "../BasePlugin.sol";
import {PluginManifest, PluginMetadata} from "../../interfaces/IPlugin.sol";

// Non-threshold based multisig plugin - all owners must sign.
// Supports up to 100 owners per id.
contract ComposableMultisigPlugin is IValidation, BasePlugin {
struct OwnerInfo {
uint256 length;
FunctionReference[100] validations;
}

uint256 internal constant _SIG_VALIDATION_PASSED = 0;
uint256 internal constant _SIG_VALIDATION_FAILED = 1;

mapping(uint8 id => mapping(address account => OwnerInfo)) public ownerInfo;

error AlreadyInitialized();
error NotAuthorized();
error NotInitialized();
error InvalidOwners();

/// @inheritdoc IPlugin
function onInstall(bytes calldata data) external override {
uint8 id = uint8(bytes1(data[:1]));

if (ownerInfo[id][msg.sender].length != 0) {
revert AlreadyInitialized();
}

FunctionReference[] memory validations = abi.decode(data[1:], (FunctionReference[]));

if (validations.length == 0 || validations.length > 100) {
revert InvalidOwners();
}

ownerInfo[id][msg.sender].length = validations.length;

for (uint256 i = 0; i < validations.length; i++) {
ownerInfo[id][msg.sender].validations[i] = validations[i];
}
}

/// @inheritdoc IPlugin
function onUninstall(bytes calldata data) external override {
uint8 id = uint8(bytes1(data[:1]));

uint256 length = ownerInfo[id][msg.sender].length;

if (length == 0) {
revert NotInitialized();
}

for (uint256 i = 0; i < length; i++) {
ownerInfo[id][msg.sender].validations[i] = FunctionReference.wrap(bytes21(0));
}

ownerInfo[id][msg.sender].length = 0;
}

/// @inheritdoc IValidation
function validateUserOp(uint8 functionId, PackedUserOperation calldata, bytes32)
external
view
override
returns (uint256, bytes memory)
{
OwnerInfo storage info = ownerInfo[functionId][msg.sender];

if (info.length == 0) {
revert NotInitialized();
}

FunctionReference[] memory validations = new FunctionReference[](info.length);

for (uint256 i = 0; i < info.length; i++) {
validations[i] = info.validations[i];
}

return (_SIG_VALIDATION_PASSED, abi.encode(validations));
}

/// @inheritdoc IValidation
function validateRuntime(uint8, address, uint256, bytes calldata, bytes calldata) external pure override {
revert NotImplemented();
}

// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ Execution view functions ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

/// @inheritdoc IValidation
/// @dev The signature is valid if it is signed by the owner's private key
/// (if the owner is an EOA) or if it is a valid ERC-1271 signature from the
/// owner (if the owner is a contract). Note that unlike the signature
/// validation used in `validateUserOp`, this does **not** wrap the digest in
/// an "Ethereum Signed Message" envelope before checking the signature in
/// the EOA-owner case.
function validateSignature(uint8, address, bytes32, bytes calldata) external pure override returns (bytes4) {
revert NotImplemented();
}

/// @inheritdoc IPlugin
// solhint-disable-next-line no-empty-blocks
function pluginManifest() external pure override returns (PluginManifest memory) {}

/// @inheritdoc IPlugin
// solhint-disable-next-line no-empty-blocks
function pluginMetadata() external pure virtual override returns (PluginMetadata memory) {}
}
111 changes: 111 additions & 0 deletions src/plugins/owner/ECDSAValidationPlugin.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// SPDX-License-Identifier: UNLICENSED
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 {IPlugin} from "../../interfaces/IPlugin.sol";
import {IValidation} from "../../interfaces/IValidation.sol";
import {BasePlugin} from "../BasePlugin.sol";
import {PluginManifest, PluginMetadata} from "../../interfaces/IPlugin.sol";

contract ECDSAValidationPlugin is IValidation, BasePlugin {
using ECDSA for bytes32;
using MessageHashUtils for bytes32;

uint256 internal constant _SIG_VALIDATION_PASSED = 0;
uint256 internal constant _SIG_VALIDATION_FAILED = 1;

// bytes4(keccak256("isValidSignature(bytes32,bytes)"))
bytes4 internal constant _1271_MAGIC_VALUE = 0x1626ba7e;
bytes4 internal constant _1271_INVALID = 0xffffffff;

mapping(uint8 id => mapping(address account => address)) public owners;

error AlreadyInitialized();
error NotAuthorized();
error NotInitialized();

/// @inheritdoc IPlugin
function onInstall(bytes calldata data) external override {
uint8 id = uint8(bytes1(data[:1]));

if (owners[id][msg.sender] != address(0)) {
revert AlreadyInitialized();
}

address owner = abi.decode(data[1:], (address));
owners[id][msg.sender] = owner;
}

/// @inheritdoc IPlugin
function onUninstall(bytes calldata data) external override {
uint8 id = uint8(bytes1(data[:1]));

if (owners[id][msg.sender] == address(0)) {
revert NotInitialized();
}

delete owners[id][msg.sender];
}

/// @inheritdoc IValidation
function validateRuntime(uint8 functionId, address sender, uint256, bytes calldata, bytes calldata)
external
view
override
{
// Validate that the sender is the owner of the account or self.
if (sender != owners[functionId][msg.sender]) {
revert NotAuthorized();
}
return;
}

/// @inheritdoc IValidation
function validateUserOp(uint8 functionId, PackedUserOperation calldata userOp, bytes32 userOpHash)
external
view
override
returns (uint256, bytes memory)
{
// Validate the user op signature against the owner.
(address signer,,) = (userOpHash.toEthSignedMessageHash()).tryRecover(userOp.signature);
if (signer == address(0) || signer != owners[functionId][msg.sender]) {
return (_SIG_VALIDATION_FAILED, "");
}
return (_SIG_VALIDATION_PASSED, "");
}

// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ Execution view functions ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

/// @inheritdoc IValidation
/// @dev The signature is valid if it is signed by the owner's private key
/// (if the owner is an EOA) or if it is a valid ERC-1271 signature from the
/// owner (if the owner is a contract). Note that unlike the signature
/// validation used in `validateUserOp`, this does **not** wrap the digest in
/// an "Ethereum Signed Message" envelope before checking the signature in
/// the EOA-owner case.
function validateSignature(uint8 functionId, address, bytes32 digest, bytes calldata signature)
external
view
override
returns (bytes4)
{
if (digest.recover(signature) == owners[functionId][msg.sender]) {
return _1271_MAGIC_VALUE;
}
return _1271_INVALID;
}

/// @inheritdoc IPlugin
// solhint-disable-next-line no-empty-blocks
function pluginManifest() external pure override returns (PluginManifest memory) {}

/// @inheritdoc IPlugin
// solhint-disable-next-line no-empty-blocks
function pluginMetadata() external pure virtual override returns (PluginMetadata memory) {}
}
8 changes: 4 additions & 4 deletions src/plugins/owner/SingleOwnerPlugin.sol
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ contract SingleOwnerPlugin is ISingleOwnerPlugin, BasePlugin {
{
if (functionId == uint8(FunctionId.VALIDATION_OWNER)) {
// 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;
Expand All @@ -99,15 +99,15 @@ contract SingleOwnerPlugin is ISingleOwnerPlugin, BasePlugin {
external
view
override
returns (uint256)
returns (uint256, bytes memory)
{
if (functionId == uint8(FunctionId.VALIDATION_OWNER)) {
// 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_FAILED, "");
}
return _SIG_VALIDATION_PASSED;
return (_SIG_VALIDATION_PASSED, "");
}
revert NotImplemented();
}
Expand Down
Loading