Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DRAFT] feat: Composable Validation #82

Closed
wants to merge 8 commits into from
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
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@
[submodule "lib/forge-std"]
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "lib/FreshCryptoLib"]
path = lib/FreshCryptoLib
url = https://github.com/rdubois-crypto/FreshCryptoLib
1 change: 1 addition & 0 deletions lib/FreshCryptoLib
Submodule FreshCryptoLib added at fd2a0e
19 changes: 10 additions & 9 deletions src/plugins/owner/ISingleOwnerPlugin.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity ^0.8.25;

import {IValidation} from "../../interfaces/IValidation.sol";
import {Signer} from "../../validators/IStatelessValidator.sol";

interface ISingleOwnerPlugin is IValidation {
enum FunctionId {
Expand All @@ -11,27 +12,27 @@ interface ISingleOwnerPlugin is IValidation {

/// @notice This event is emitted when ownership of the account changes.
/// @param account The account whose ownership changed.
/// @param previousOwner The address of the previous owner.
/// @param newOwner The address of the new owner.
event OwnershipTransferred(address indexed account, address indexed previousOwner, address indexed newOwner);
/// @param previousOwner The details of the previous owner.
/// @param newOwner The details of the new owner.
event OwnershipTransferred(address indexed account, Signer previousOwner, Signer newOwner);

error NotAuthorized();

/// @notice Transfer ownership of the account to `newOwner`.
/// @dev This function is installed on the account as part of plugin installation, and should
/// only be called from an account.
/// @param newOwner The address of the new owner.
function transferOwnership(address newOwner) external;
/// @param newOwner The details of the new owner.
function transferOwnership(Signer calldata newOwner) external;

/// @notice Get the owner of the account.
/// @dev This function is installed on the account as part of plugin installation, and should
/// only be called from an account.
/// @return The address of the owner.
function owner() external view returns (address);
/// @return The details of the owner.
function owner() external view returns (Signer memory);

/// @notice Get the owner of `account`.
/// @dev This function is not installed on the account, and can be called by anyone.
/// @param account The account to get the owner of.
/// @return The address of the owner.
function ownerOf(address account) external view returns (address);
/// @return The details of the owner.
function ownerOf(address account) external view returns (Signer memory);
}
55 changes: 33 additions & 22 deletions src/plugins/owner/SingleOwnerPlugin.sol
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.25;

import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {SignatureChecker} from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";
import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol";
import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";

import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";
import {IPluginManager} from "../../interfaces/IPluginManager.sol";

import {
ManifestFunction,
ManifestAssociatedFunctionType,
Expand All @@ -16,9 +13,11 @@ import {
PluginMetadata,
SelectorPermission
} from "../../interfaces/IPlugin.sol";
import {IStandardExecutor} from "../../interfaces/IStandardExecutor.sol";
import {IPlugin} from "../../interfaces/IPlugin.sol";
import {IPluginManager} from "../../interfaces/IPluginManager.sol";
import {IStandardExecutor} from "../../interfaces/IStandardExecutor.sol";
import {IValidation} from "../../interfaces/IValidation.sol";
import {Signer} from "../../validators/IStatelessValidator.sol";
import {BasePlugin, IERC165} from "../BasePlugin.sol";
import {ISingleOwnerPlugin} from "./ISingleOwnerPlugin.sol";

Expand All @@ -39,7 +38,6 @@ import {ISingleOwnerPlugin} from "./ISingleOwnerPlugin.sol";
/// owner of a modular account may not be another modular account if you want to
/// send user operations through a bundler.
contract SingleOwnerPlugin is ISingleOwnerPlugin, BasePlugin {
using ECDSA for bytes32;
using MessageHashUtils for bytes32;

string public constant NAME = "Single Owner Plugin";
Expand All @@ -53,14 +51,14 @@ contract SingleOwnerPlugin is ISingleOwnerPlugin, BasePlugin {
bytes4 internal constant _1271_MAGIC_VALUE = 0x1626ba7e;
bytes4 internal constant _1271_INVALID = 0xffffffff;

mapping(address => address) internal _owners;
mapping(address => Signer) internal _owners;

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

/// @inheritdoc ISingleOwnerPlugin
function transferOwnership(address newOwner) external {
function transferOwnership(Signer calldata newOwner) external {
_transferOwnership(newOwner);
}

Expand All @@ -70,12 +68,13 @@ contract SingleOwnerPlugin is ISingleOwnerPlugin, BasePlugin {

/// @inheritdoc IPlugin
function onInstall(bytes calldata data) external override {
_transferOwnership(abi.decode(data, (address)));
_transferOwnership(abi.decode(data, (Signer)));
}

/// @inheritdoc IPlugin
function onUninstall(bytes calldata) external override {
_transferOwnership(address(0));
Signer memory empty;
_transferOwnership(empty);
}

/// @inheritdoc IValidation
Expand All @@ -86,7 +85,13 @@ 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 != msg.sender
&& (
_owners[msg.sender].data.length == 0
|| sender != abi.decode(_owners[msg.sender].data, (address))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not guaranteed to have this abi encoding though, right? For example for WebAuthnValidator has a different encoding to ECDSA one which is assumed here

)
) {
revert NotAuthorized();
}
return;
Expand All @@ -103,11 +108,14 @@ contract SingleOwnerPlugin is ISingleOwnerPlugin, BasePlugin {
{
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]) {
Signer memory signer = _owners[msg.sender];

if (address(signer.validator) == address(0)) {
return _SIG_VALIDATION_FAILED;
}
return _SIG_VALIDATION_PASSED;
(bool isValid,) =
signer.validator.validate(signer.data, userOpHash.toEthSignedMessageHash(), userOp.signature);
return isValid ? _SIG_VALIDATION_PASSED : _SIG_VALIDATION_FAILED;
}
revert NotImplemented();
}
Expand All @@ -130,16 +138,19 @@ contract SingleOwnerPlugin is ISingleOwnerPlugin, BasePlugin {
returns (bytes4)
{
if (functionId == uint8(FunctionId.SIG_VALIDATION)) {
if (SignatureChecker.isValidSignatureNow(_owners[msg.sender], digest, signature)) {
return _1271_MAGIC_VALUE;
Signer memory signer = _owners[msg.sender];
if (address(signer.validator) == address(0)) {
return _1271_INVALID;
}
return _1271_INVALID;

(bool isValid,) = signer.validator.validate(signer.data, digest, signature);
return isValid ? _1271_MAGIC_VALUE : _1271_INVALID;
}
revert NotImplemented();
}

/// @inheritdoc ISingleOwnerPlugin
function owner() external view returns (address) {
function owner() external view returns (Signer memory) {
return _owners[msg.sender];
}

Expand All @@ -148,7 +159,7 @@ contract SingleOwnerPlugin is ISingleOwnerPlugin, BasePlugin {
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

/// @inheritdoc ISingleOwnerPlugin
function ownerOf(address account) external view returns (address) {
function ownerOf(address account) external view returns (Signer memory) {
return _owners[account];
}

Expand Down Expand Up @@ -222,8 +233,8 @@ contract SingleOwnerPlugin is ISingleOwnerPlugin, BasePlugin {
// ┃ Internal / Private functions ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

function _transferOwnership(address newOwner) internal {
address previousOwner = _owners[msg.sender];
function _transferOwnership(Signer memory newOwner) internal {
Signer memory previousOwner = _owners[msg.sender];
_owners[msg.sender] = newOwner;
emit OwnershipTransferred(msg.sender, previousOwner, newOwner);
}
Expand Down
32 changes: 32 additions & 0 deletions src/validators/Base64URL.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

import {Base64} from "@openzeppelin/contracts/utils/Base64.sol";

/// TODO this library is not necessary once Openzeppelin 5.0.2 Base64 is supported
library Base64URL {
function encode(bytes memory data) internal pure returns (string memory) {
string memory strb64 = Base64.encode(data);
bytes memory b64 = bytes(strb64);

// Base64 can end with "=" or "=="; Base64URL has no padding.
uint256 equalsCount = 0;
if (b64.length > 2 && b64[b64.length - 2] == "=") equalsCount = 2;
else if (b64.length > 1 && b64[b64.length - 1] == "=") equalsCount = 1;

uint256 len = b64.length - equalsCount;
bytes memory result = new bytes(len);

for (uint256 i = 0; i < len; i++) {
if (b64[i] == "+") {
result[i] = "-";
} else if (b64[i] == "/") {
result[i] = "_";
} else {
result[i] = b64[i];
}
}

return string(result);
}
}
32 changes: 32 additions & 0 deletions src/validators/EcdsaValidator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.25;

import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

import {IStatelessValidator} from "./IStatelessValidator.sol";

contract EcdsaValidator is IStatelessValidator {
using ECDSA for bytes32;

/// @dev result always returns the correct singer of the signature.
function validate(bytes memory signerData, bytes32 hash, bytes memory signature)
external
view
override
returns (bool isValid, bytes memory result)
{
if (signerData.length == 0) {
isValid = false;
} else {
address expectedSigner = abi.decode(signerData, (address));

address signer = hash.recover(signature);
isValid = signer == expectedSigner;
result = abi.encode(signer);
}
}

function encodeSignerData(address signer) external pure returns (bytes memory data) {
data = abi.encode(signer);
}
}
27 changes: 27 additions & 0 deletions src/validators/Erc1271Validator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.25;

import {SignatureChecker} from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";

import {IStatelessValidator} from "./IStatelessValidator.sol";

contract Erc1271Validator is IStatelessValidator {
function validate(bytes memory signerData, bytes32 hash, bytes memory signature)
external
view
override
returns (bool isValid, bytes memory)
{
if (signerData.length == 0) {
isValid = false;
} else {
address expectedSigner = abi.decode(signerData, (address));

isValid = SignatureChecker.isValidERC1271SignatureNow(expectedSigner, hash, signature);
}
}

function encodeSignerData(address signer) external pure returns (bytes memory data) {
data = abi.encode(signer);
}
}
15 changes: 15 additions & 0 deletions src/validators/IStatelessValidator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.25;

struct Signer {
IStatelessValidator validator;
/// data is passed as signedData to the validator
bytes data;
}

interface IStatelessValidator {
function validate(bytes memory signerData, bytes32 hash, bytes memory signature)
external
view
returns (bool isValid, bytes memory result);
}
Loading
Loading