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
94 changes: 94 additions & 0 deletions src/factory/MultiSigKernelFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

import "./KernelFactory.sol";
import "src/validator/MultiSigValidator.sol";
import "src/interfaces/IMultiSigAddressBook.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract MultiSigKernelFactory is IMultiSigAddressBook, Ownable {
KernelFactory public immutable singletonFactory;
MultiSigValidator public immutable validator;
IEntryPoint public immutable entryPoint;

address[] public owners;
uint256 public threshold;

constructor(KernelFactory _singletonFactory, MultiSigValidator _validator, IEntryPoint _entryPoint) {
singletonFactory = _singletonFactory;
validator = _validator;
entryPoint = _entryPoint;
}

function setOwners(address[] calldata _owners, uint256 _threshold) external onlyOwner {
require(_owners.length >= _threshold, "MultiSigKernelFactory: threshold must be less than or equal to the number of owners");
owners = _owners;
threshold = _threshold;
}

function getOwners() external view override returns(address[] memory) {
return owners;
}

function getThreshold() external view override returns(uint256) {
return threshold;
}

function createAccount(uint256 _index) external returns (EIP1967Proxy proxy) {
bytes memory data = abi.encodePacked(address(this));
proxy = singletonFactory.createAccount(validator, data, _index);
}

function getAccountAddress(uint256 _index) public view returns (address) {
bytes memory data = abi.encodePacked(address(this));
return singletonFactory.getAccountAddress(validator, data, _index);
}

/**
* add a deposit for this factory, used for paying for transaction fees
*/
function deposit() public payable {
entryPoint.depositTo{value : msg.value}(address(this));
}

/**
* withdraw value from the deposit
* @param withdrawAddress target to send to
* @param amount to withdraw
*/
function withdrawTo(address payable withdrawAddress, uint256 amount) public onlyOwner {
entryPoint.withdrawTo(withdrawAddress, amount);
}
/**
* add stake for this factory.
* This method can also carry eth value to add to the current stake.
* @param unstakeDelaySec - the unstake delay for this factory. Can only be increased.
*/
function addStake(uint32 unstakeDelaySec) external payable onlyOwner {
entryPoint.addStake{value : msg.value}(unstakeDelaySec);
}

/**
* return current factory's deposit on the entryPoint.
*/
function getDeposit() public view returns (uint256) {
return entryPoint.balanceOf(address(this));
}

/**
* unlock the stake, in order to withdraw it.
* The factory can't serve requests once unlocked, until it calls addStake again
*/
function unlockStake() external onlyOwner {
entryPoint.unlockStake();
}

/**
* withdraw the entire factory's stake.
* stake must be unlocked first (and then wait for the unstakeDelay to be over)
* @param withdrawAddress the address to send withdrawn value.
*/
function withdrawStake(address payable withdrawAddress) external onlyOwner {
entryPoint.withdrawStake(withdrawAddress);
}
}
7 changes: 7 additions & 0 deletions src/interfaces/IMultiSigAddressBook.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

interface IMultiSigAddressBook {
function getOwners() external view returns(address[] memory);
function getThreshold() external view returns(uint256);
}
36 changes: 36 additions & 0 deletions src/utils/SignatureDecoder.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.7.0 <0.9.0;

/**
* @title SignatureDecoder - Decodes signatures encoded as bytes
* @author Richard Meissner - @rmeissner
*/
abstract contract SignatureDecoder {
/**
* @notice Splits signature bytes into `uint8 v, bytes32 r, bytes32 s`.
* @dev Make sure to perform a bounds check for @param pos, to avoid out of bounds access on @param signatures
* The signature format is a compact form of {bytes32 r}{bytes32 s}{uint8 v}
* Compact means uint8 is not padded to 32 bytes.
* @param pos Which signature to read.
* A prior bounds check of this parameter should be performed, to avoid out of bounds access.
* @param signatures Concatenated {r, s, v} signatures.
* @return v Recovery ID or Safe signature type.
* @return r Output value r of the signature.
* @return s Output value s of the signature.
*/
function signatureSplit(bytes memory signatures, uint256 pos) internal pure returns (uint8 v, bytes32 r, bytes32 s) {
// solhint-disable-next-line no-inline-assembly
assembly {
let signaturePos := mul(0x41, pos)
r := mload(add(signatures, add(signaturePos, 0x20)))
s := mload(add(signatures, add(signaturePos, 0x40)))
/**
* Here we are loading the last 32 bytes, including 31 bytes
* of 's'. There is no 'mload8' to do this.
* 'byte' is not working due to the Solidity parser, so lets
* use the second best option, 'and'
*/
v := and(mload(add(signatures, add(signaturePos, 0x41))), 0xff)
}
}
}
110 changes: 110 additions & 0 deletions src/validator/MultiSigValidator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./IValidator.sol";
import "openzeppelin-contracts/contracts/utils/cryptography/EIP712.sol";
import "src/utils/KernelHelper.sol";
import "src/utils/SignatureDecoder.sol";
import "src/interfaces/IMultiSigAddressBook.sol";
import "forge-std/Test.sol";
Copy link
Collaborator

Choose a reason for hiding this comment

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

Remove Test.sol


struct MultiSigValidatorStorage {
mapping(address => bool) isOwner;
uint256 threshold;
uint256 ownerCount;
}

contract MultiSigValidator is IKernelValidator, SignatureDecoder, Test {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why is this inheriting the Test?

event OwnerAdded(address indexed kernel, address indexed owner);
event OwnerRemoved(address indexed kernel, address indexed owner);
event ThresholdChanged(address indexed kernel, uint256 indexed threshold);

mapping(address => MultiSigValidatorStorage)
public multiSigValidatorStorage;

function disable(bytes calldata _data) external override {
address[] memory owners = abi.decode(_data, (address[]));
for (uint256 i = 0; i < owners.length; i++) {
multiSigValidatorStorage[msg.sender].isOwner[owners[i]] = false;
emit OwnerRemoved(msg.sender, owners[i]);
}
}

function enable(bytes calldata _data) external override {
address addressBook = address(bytes20(_data));
address[] memory owners = IMultiSigAddressBook(addressBook).getOwners();
uint256 threshold = IMultiSigAddressBook(addressBook).getThreshold();
for (uint256 i = 0; i < owners.length; i++) {
if (!multiSigValidatorStorage[msg.sender].isOwner[owners[i]]) {
multiSigValidatorStorage[msg.sender].isOwner[owners[i]] = true;
emit OwnerAdded(msg.sender, owners[i]);
}
}
multiSigValidatorStorage[msg.sender].threshold = threshold;
emit ThresholdChanged(msg.sender, threshold);
}

function validateUserOp(
UserOperation calldata _userOp,
bytes32 _userOpHash,
uint256
) external view override returns (uint256 validationData) {
if (!_signaturesAreValid(_userOpHash, _userOp.signature)) {
return SIG_VALIDATION_FAILED;
}
return 0;
}

function validateSignature(
bytes32 hash,
bytes calldata signature
) public view override returns (uint256) {
if (!_signaturesAreValid(hash, signature)) {
return SIG_VALIDATION_FAILED;
}
return 0;
}

function _signaturesAreValid(
bytes32 hash,
bytes calldata signatures
) internal view returns (bool) {
MultiSigValidatorStorage storage validatorStorage = multiSigValidatorStorage[msg.sender];
uint256 threshold = validatorStorage.threshold;
address lastOwner = address(0);
address currentOwner;
uint8 v;
bytes32 r;
bytes32 s;
uint256 i;

if (signatures.length < threshold * 65) {
return false;
}
for (i = 0; i < threshold; i++) {
(v, r, s) = signatureSplit(signatures, i);
if (v > 30) {
// If v > 30 then default v (27,28) has been adjusted for eth_sign flow
// To support eth_sign and similar we adjust v and hash the messageHash with the Ethereum message prefix before applying ecrecover
currentOwner = ecrecover(
ECDSA.toEthSignedMessageHash(hash),
v - 4,
r,
s
);
} else {
// Default is the ecrecover flow with the provided data hash
// Use ecrecover with the messageHash for EOA signatures
currentOwner = ecrecover(hash, v, r, s);
}
// To prevent signer reuse
// signatures are ordered by address
if (currentOwner <= lastOwner || !validatorStorage.isOwner[currentOwner]) {
return false;
}
lastOwner = currentOwner;
}
return true;
}
}
Loading