diff --git a/src/factory/MultiSigKernelFactory.sol b/src/factory/MultiSigKernelFactory.sol new file mode 100644 index 00000000..e32f73bb --- /dev/null +++ b/src/factory/MultiSigKernelFactory.sol @@ -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); + } +} diff --git a/src/interfaces/IMultiSigAddressBook.sol b/src/interfaces/IMultiSigAddressBook.sol new file mode 100644 index 00000000..5cf90023 --- /dev/null +++ b/src/interfaces/IMultiSigAddressBook.sol @@ -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); +} diff --git a/src/utils/SignatureDecoder.sol b/src/utils/SignatureDecoder.sol new file mode 100644 index 00000000..b505cbf6 --- /dev/null +++ b/src/utils/SignatureDecoder.sol @@ -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) + } + } +} diff --git a/src/validator/MultiSigValidator.sol b/src/validator/MultiSigValidator.sol new file mode 100644 index 00000000..3738965c --- /dev/null +++ b/src/validator/MultiSigValidator.sol @@ -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"; + +struct MultiSigValidatorStorage { + mapping(address => bool) isOwner; + uint256 threshold; + uint256 ownerCount; +} + +contract MultiSigValidator is IKernelValidator, SignatureDecoder, 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; + } +} diff --git a/test/foundry/KernelMultiSig.test.sol b/test/foundry/KernelMultiSig.test.sol new file mode 100644 index 00000000..647af58d --- /dev/null +++ b/test/foundry/KernelMultiSig.test.sol @@ -0,0 +1,280 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "src/factory/KernelFactory.sol"; +import "src/factory/TempKernel.sol"; +import "src/factory/MultiSigKernelFactory.sol"; +import "src/Kernel.sol"; +import "src/validator/MultiSigValidator.sol"; +import "src/factory/EIP1967Proxy.sol"; +// test artifacts +import "src/test/TestValidator.sol"; +// test utils +import "forge-std/Test.sol"; +import {ERC4337Utils} from "./ERC4337Utils.sol"; +import "src/utils/SignatureDecoder.sol"; + +using ERC4337Utils for EntryPoint; + +contract KernelMultiSigTest is Test, SignatureDecoder { + Kernel kernel; + KernelFactory factory; + MultiSigKernelFactory multiSigFactory; + EntryPoint entryPoint; + MultiSigValidator validator; + address owner1; + uint256 owner1Key; + address owner2; + uint256 owner2Key; + address owner3; + uint256 owner3Key; + uint256 threshold; + address payable beneficiary; + + function setUp() public { + (owner1, owner1Key) = makeAddrAndKey("owner1"); + (owner2, owner2Key) = makeAddrAndKey("owner2"); + (owner3, owner3Key) = makeAddrAndKey("owner3"); + threshold = 2; + entryPoint = new EntryPoint(); + factory = new KernelFactory(entryPoint); + + validator = new MultiSigValidator(); + multiSigFactory = new MultiSigKernelFactory(factory, validator, entryPoint); + address[] memory owners = new address[](3); + owners[0] = owner1; + owners[1] = owner2; + owners[2] = owner3; + multiSigFactory.setOwners(owners, threshold); + + kernel = Kernel(payable(multiSigFactory.createAccount(0))); + vm.deal(address(kernel), 1e30); + beneficiary = payable(address(makeAddr("beneficiary"))); + console.log("beneficiary", beneficiary); + } + + function test_initialize_twice() external { + vm.expectRevert(); + kernel.initialize(validator, abi.encodePacked(owner1)); + } + + function test_validate_signature() external { + Kernel kernel2 = Kernel(payable(address(multiSigFactory.createAccount(1)))); + bytes32 hash = keccak256(abi.encodePacked("hello world")); + bytes memory signatures; + { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(owner2Key, hash); + signatures = abi.encodePacked(r, s, v); + } + { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(owner1Key, hash); + signatures = abi.encodePacked(signatures, r, s, v); + } + // signatures are sorted by address manually and packed (owner2 < owner1) + // [TODO] - write a library to sort signatures + assertEq(kernel2.isValidSignature(hash, signatures), Kernel.isValidSignature.selector); + } + + function test_validate_signature_with_prefix() external { + Kernel kernel2 = Kernel(payable(address(multiSigFactory.createAccount(1)))); + bytes32 hash = keccak256(abi.encodePacked("hello world")); + bytes memory signatures; + { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(owner2Key, hash); + signatures = abi.encodePacked(r, s, v); + } + { + bytes32 hashWithPrefix = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(owner1Key, hashWithPrefix); + // When signed with EIP-191 prefix, adjust v to be 27/28 + 4 + signatures = abi.encodePacked(signatures, r, s, v + 4); + } + // signatures are sorted by address manually and packed (owner2 < owner1) + // [TODO] - write a library to sort signatures + assertEq(kernel2.isValidSignature(hash, signatures), Kernel.isValidSignature.selector); + } + + function test_revert_when_signer_unauthorized() external { + Kernel kernel2 = Kernel(payable(address(multiSigFactory.createAccount(1)))); + bytes32 hash = keccak256(abi.encodePacked("hello world")); + bytes memory signatures; + { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(owner2Key, hash); + signatures = abi.encodePacked(r, s, v); + } + { + (address nonOwner, uint256 nonOwnerKey) = makeAddrAndKey("nonOwner"); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(nonOwnerKey, hash); + signatures = abi.encodePacked(signatures, r, s, v); + } + assertEq(kernel2.isValidSignature(hash, signatures), bytes4(0xffffffff)); + } + + function test_revert_when_duplicate_signatures() external { + Kernel kernel2 = Kernel(payable(address(multiSigFactory.createAccount(1)))); + bytes32 hash = keccak256(abi.encodePacked("hello world")); + bytes memory signatures; + { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(owner2Key, hash); + signatures = abi.encodePacked(r, s, v); + } + { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(owner2Key, hash); + signatures = abi.encodePacked(signatures, r, s, v); + } + assertEq(kernel2.isValidSignature(hash, signatures), bytes4(0xffffffff)); + } + + function test_revert_when_signatures_below_threshold() external { + Kernel kernel2 = Kernel(payable(address(multiSigFactory.createAccount(1)))); + bytes32 hash = keccak256(abi.encodePacked("hello world")); + bytes memory signatures; + { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(owner2Key, hash); + signatures = abi.encodePacked(r, s, v); + } + assertEq(kernel2.isValidSignature(hash, signatures), bytes4(0xffffffff)); + } + + function test_set_default_validator() external { + TestValidator newValidator = new TestValidator(); + bytes memory empty; + UserOperation memory op = entryPoint.fillUserOp( + address(kernel), + abi.encodeWithSelector(KernelStorage.setDefaultValidator.selector, address(newValidator), empty) + ); + op.signature = abi.encodePacked(bytes4(0x00000000)); + { + bytes memory signature = entryPoint.signUserOpHash(vm, owner2Key, op); + (uint8 v, bytes32 r, bytes32 s) = signatureSplit(signature, 0); + signature = abi.encodePacked(r, s, v + 4); + op.signature = abi.encodePacked(op.signature, signature); + } + { + bytes memory signature = entryPoint.signUserOpHash(vm, owner1Key, op); + (uint8 v, bytes32 r, bytes32 s) = signatureSplit(signature, 0); + signature = abi.encodePacked(r, s, v + 4); + op.signature = abi.encodePacked(op.signature, signature); + } + + UserOperation[] memory ops = new UserOperation[](1); + ops[0] = op; + entryPoint.handleOps(ops, beneficiary); + assertEq(address(KernelStorage(address(kernel)).getDefaultValidator()), address(newValidator)); + } + + function test_disable_mode() external { + bytes memory empty; + UserOperation memory op = entryPoint.fillUserOp( + address(kernel), + abi.encodeWithSelector(KernelStorage.disableMode.selector, bytes4(0x00000001), address(0), empty) + ); + op.signature = abi.encodePacked(bytes4(0x00000000)); + { + bytes memory signature = entryPoint.signUserOpHash(vm, owner2Key, op); + (uint8 v, bytes32 r, bytes32 s) = signatureSplit(signature, 0); + signature = abi.encodePacked(r, s, v + 4); + op.signature = abi.encodePacked(op.signature, signature); + } + { + bytes memory signature = entryPoint.signUserOpHash(vm, owner1Key, op); + (uint8 v, bytes32 r, bytes32 s) = signatureSplit(signature, 0); + signature = abi.encodePacked(r, s, v + 4); + op.signature = abi.encodePacked(op.signature, signature); + } + + UserOperation[] memory ops = new UserOperation[](1); + ops[0] = op; + entryPoint.handleOps(ops, beneficiary); + assertEq(uint256(bytes32(KernelStorage(address(kernel)).getDisabledMode())), 1 << 224); + } + + function test_set_execution() external { + console.log("owner1", owner1); + console.log("owner2", owner2); + TestValidator newValidator = new TestValidator(); + UserOperation memory op = entryPoint.fillUserOp( + address(kernel), + abi.encodeWithSelector( + KernelStorage.setExecution.selector, + bytes4(0xdeadbeef), + address(0xdead), + address(newValidator), + uint48(0), + uint48(0), + bytes("") + ) + ); + op.signature = abi.encodePacked(bytes4(0x00000000)); + { + bytes memory signature = entryPoint.signUserOpHash(vm, owner2Key, op); + (uint8 v, bytes32 r, bytes32 s) = signatureSplit(signature, 0); + signature = abi.encodePacked(r, s, v + 4); + op.signature = abi.encodePacked(op.signature, signature); + } + { + bytes memory signature = entryPoint.signUserOpHash(vm, owner1Key, op); + (uint8 v, bytes32 r, bytes32 s) = signatureSplit(signature, 0); + signature = abi.encodePacked(r, s, v + 4); + op.signature = abi.encodePacked(op.signature, signature); + } + + UserOperation[] memory ops = new UserOperation[](1); + ops[0] = op; + entryPoint.handleOps(ops, beneficiary); + ExecutionDetail memory execution = KernelStorage(address(kernel)).getExecution(bytes4(0xdeadbeef)); + assertEq(execution.executor, address(0xdead)); + assertEq(address(execution.validator), address(newValidator)); + assertEq(uint256(execution.validUntil), uint256(0)); + assertEq(uint256(execution.validAfter), uint256(0)); + } + + function test_callcode() external { + CallCodeTester t = new CallCodeTester(); + address(t).call{value: 1e18}(""); + Target target = new Target(); + t.callcodeTest(address(target)); + console.log("target balance", address(target).balance); + console.log("t balance", address(t).balance); + console.log("t slot1", t.slot1()); + console.log("t slot2", t.slot2()); + } +} + +contract CallCodeTester { + uint256 public slot1; + uint256 public slot2; + receive() external payable { + } + function callcodeTest(address _target) external { + bool success; + bytes memory ret; + uint256 b = address(this).balance / 1000; + bytes memory data; + assembly { + let result := callcode(gas(), _target, b, add(data, 0x20), mload(data), 0, 0) + // Load free memory location + let ptr := mload(0x40) + // We allocate memory for the return data by setting the free memory location to + // current free memory location + data size + 32 bytes for data size value + mstore(0x40, add(ptr, add(returndatasize(), 0x20))) + // Store the size + mstore(ptr, returndatasize()) + // Store the data + returndatacopy(add(ptr, 0x20), 0, returndatasize()) + // Point the return data to the correct memory location + ret := ptr + success := result + } + require(success, "callcode failed"); + } +} + +contract Target { + uint256 public count; + uint256 public amount; + fallback() external payable { + count++; + amount += msg.value; + } +}