Skip to content

Commit

Permalink
add AccountMultisig module
Browse files Browse the repository at this point in the history
  • Loading branch information
Amxx committed May 3, 2024
1 parent 6373a08 commit 4512481
Show file tree
Hide file tree
Showing 11 changed files with 352 additions and 64 deletions.
12 changes: 2 additions & 10 deletions contracts/abstraction/account/Account.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {ERC4337Utils} from "./../utils/ERC4337Utils.sol";

abstract contract Account is IAccount {
error AccountEntryPointRestricted();
error AccountUserRestricted();
error AccountInvalidBatchLength();

/****************************************************************************************************************
Expand All @@ -22,13 +21,6 @@ abstract contract Account is IAccount {
_;
}

modifier onlyAuthorizedOrEntryPoint() {
if (msg.sender != address(entryPoint()) && !_isAuthorized(msg.sender)) {
revert AccountUserRestricted();
}
_;
}

/****************************************************************************************************************
* Hooks *
****************************************************************************************************************/
Expand All @@ -51,7 +43,7 @@ abstract contract Account is IAccount {
* If a signature is ill-formed, address(0) should be returned.
*/
function _processSignature(
PackedUserOperation calldata userOp,
bytes memory signature,
bytes32 userOpHash
) internal virtual returns (address, uint48, uint48);

Expand Down Expand Up @@ -106,7 +98,7 @@ abstract contract Account is IAccount {
PackedUserOperation calldata userOp,
bytes32 userOpHash
) internal virtual returns (uint256 validationData) {
(address signer, uint48 validAfter, uint48 validUntil) = _processSignature(userOp, userOpHash);
(address signer, uint48 validAfter, uint48 validUntil) = _processSignature(userOp.signature, userOpHash);
return ERC4337Utils.packValidationData(signer != address(0) && _isAuthorized(signer), validAfter, validUntil);
}

Expand Down
13 changes: 6 additions & 7 deletions contracts/abstraction/account/modules/AccountECDSA.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {Account} from "../Account.sol";

abstract contract AccountECDSA is Account {
function _processSignature(
PackedUserOperation calldata userOp,
bytes memory signature,
bytes32 userOpHash
) internal virtual override returns (address, uint48, uint48) {
bytes32 msgHash = MessageHashUtils.toEthSignedMessageHash(userOpHash);
Expand All @@ -18,25 +18,24 @@ abstract contract AccountECDSA is Account {
// - If signature length is 65, process as "normal" signature (R,S,V)
// - If signature length is 64, process as https://eips.ethereum.org/EIPS/eip-2098[ERC-2098 short signature] (R,SV) ECDSA signature
// This is safe because the UserOperations include a nonce (which is managed by the entrypoint) for replay protection.
bytes calldata signature = userOp.signature;
if (signature.length == 65) {
bytes32 r;
bytes32 s;
uint8 v;
/// @solidity memory-safe-assembly
assembly {
r := calldataload(add(signature.offset, 0x00))
s := calldataload(add(signature.offset, 0x20))
v := byte(0, calldataload(add(signature.offset, 0x40)))
r := mload(add(signature, 0x20))
s := mload(add(signature, 0x40))
v := byte(0, mload(add(signature, 0x60)))
}
return (ECDSA.recover(msgHash, v, r, s), 0, 0);
} else if (signature.length == 64) {
bytes32 r;
bytes32 vs;
/// @solidity memory-safe-assembly
assembly {
r := calldataload(add(signature.offset, 0x00))
vs := calldataload(add(signature.offset, 0x20))
r := mload(add(signature, 0x20))
vs := mload(add(signature, 0x40))
}
return (ECDSA.recover(msgHash, r, vs), 0, 0);
} else {
Expand Down
41 changes: 41 additions & 0 deletions contracts/abstraction/account/modules/AccountMultisig.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {PackedUserOperation} from "../../../interfaces/IERC4337.sol";
import {Math} from "./../../../utils/math/Math.sol";
import {ERC4337Utils} from "./../../utils/ERC4337Utils.sol";
import {Account} from "../Account.sol";

abstract contract AccountMultisig is Account {
function requiredSignatures(PackedUserOperation calldata userOp) public view virtual returns (uint256);

function _validateSignature(
PackedUserOperation calldata userOp,
bytes32 userOpHash
) internal virtual override returns (uint256 validationData) {
bytes[] memory signatures = abi.decode(userOp.signature, (bytes[]));

if (signatures.length < requiredSignatures(userOp)) {
return ERC4337Utils.SIG_VALIDATION_FAILED;
}

address lastSigner = address(0);
uint48 globalValidAfter = 0;
uint48 globalValidUntil = 0;

for (uint256 i = 0; i < signatures.length; ++i) {
(address signer, uint48 validAfter, uint48 validUntil) = _processSignature(signatures[i], userOpHash);
if (_isAuthorized(signer) && signer > lastSigner) {
lastSigner = signer;
globalValidAfter = uint48(Math.ternary(validUntil < globalValidUntil, globalValidUntil, validAfter));
globalValidUntil = uint48(
Math.ternary(validUntil > globalValidUntil || validUntil == 0, globalValidUntil, validUntil)
);
} else {
return ERC4337Utils.SIG_VALIDATION_FAILED;
}
}
return ERC4337Utils.packValidationData(true, globalValidAfter, globalValidUntil);
}
}
9 changes: 4 additions & 5 deletions contracts/abstraction/account/modules/AccountP256.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,21 @@ abstract contract AccountP256 is Account {
error P256InvalidSignatureLength(uint256 length);

function _processSignature(
PackedUserOperation calldata userOp,
bytes memory signature,
bytes32 userOpHash
) internal virtual override returns (address, uint48, uint48) {
bytes32 msgHash = MessageHashUtils.toEthSignedMessageHash(userOpHash);

// This implementation support signature that are 65 bytes long in the (R,S,V) format
bytes calldata signature = userOp.signature;
if (signature.length == 65) {
uint256 r;
uint256 s;
uint8 v;
/// @solidity memory-safe-assembly
assembly {
r := calldataload(add(signature.offset, 0x00))
s := calldataload(add(signature.offset, 0x20))
v := byte(0, calldataload(add(signature.offset, 0x40)))
r := mload(add(signature, 0x20))
s := mload(add(signature, 0x40))
v := byte(0, mload(add(signature, 0x60)))
}
return (P256.recoveryAddress(uint256(msgHash), v, r, s), 0, 0);
} else {
Expand Down
107 changes: 107 additions & 0 deletions contracts/abstraction/mocks/AdvancedAccount.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {PackedUserOperation, IEntryPoint} from "../../interfaces/IERC4337.sol";
import {AccessControl} from "../../access/AccessControl.sol";
import {ERC721Holder} from "../../token/ERC721/utils/ERC721Holder.sol";
import {ERC1155Holder} from "../../token/ERC1155/utils/ERC1155Holder.sol";
import {Address} from "../../utils/Address.sol";
import {Account} from "../account/Account.sol";
import {AccountMultisig} from "../account/modules/AccountMultisig.sol";
import {AccountECDSA} from "../account/modules/AccountECDSA.sol";
import {AccountP256} from "../account/modules/AccountP256.sol";

abstract contract AdvancedAccount is AccountMultisig, AccessControl, ERC721Holder, ERC1155Holder {
bytes32 public constant SIGNER_ROLE = keccak256("SIGNER_ROLE");

IEntryPoint private immutable _entryPoint;
uint256 private _requiredSignatures;

constructor(IEntryPoint entryPoint_, address admin_, address[] memory signers_, uint256 requiredSignatures_) {
_grantRole(DEFAULT_ADMIN_ROLE, admin_);
for (uint256 i = 0; i < signers_.length; ++i) {
_grantRole(SIGNER_ROLE, signers_[i]);
}

_entryPoint = entryPoint_;
_requiredSignatures = requiredSignatures_;
}

receive() external payable {}

function supportsInterface(
bytes4 interfaceId
) public view virtual override(AccessControl, ERC1155Holder) returns (bool) {
return super.supportsInterface(interfaceId);
}

function entryPoint() public view virtual override returns (IEntryPoint) {
return _entryPoint;
}

function requiredSignatures(
PackedUserOperation calldata /*userOp*/
) public view virtual override returns (uint256) {
return _requiredSignatures;
}

function _isAuthorized(address user) internal view virtual override returns (bool) {
return hasRole(SIGNER_ROLE, user);
}

function execute(address target, uint256 value, bytes calldata data) public virtual onlyEntryPoint {
_call(target, value, data);
}

function executeBatch(
address[] calldata targets,
uint256[] calldata values,
bytes[] calldata calldatas
) public virtual onlyEntryPoint {
if (targets.length != calldatas.length || (values.length != 0 && values.length != targets.length)) {
revert AccountInvalidBatchLength();
}

for (uint256 i = 0; i < targets.length; ++i) {
_call(targets[i], (values.length == 0 ? 0 : values[i]), calldatas[i]);
}
}

function _call(address target, uint256 value, bytes memory data) internal {
(bool success, bytes memory returndata) = target.call{value: value}(data);
Address.verifyCallResult(success, returndata);
}
}

contract AdvancedAccountECDSA is AdvancedAccount, AccountECDSA {
constructor(
IEntryPoint entryPoint_,
address admin_,
address[] memory signers_,
uint256 requiredSignatures_
) AdvancedAccount(entryPoint_, admin_, signers_, requiredSignatures_) {}

function _validateSignature(
PackedUserOperation calldata userOp,
bytes32 userOpHash
) internal virtual override(Account, AccountMultisig) returns (uint256 validationData) {
return AccountMultisig._validateSignature(userOp, userOpHash);
}
}

contract AdvancedAccountP256 is AdvancedAccount, AccountP256 {
constructor(
IEntryPoint entryPoint_,
address admin_,
address[] memory signers_,
uint256 requiredSignatures_
) AdvancedAccount(entryPoint_, admin_, signers_, requiredSignatures_) {}

function _validateSignature(
PackedUserOperation calldata userOp,
bytes32 userOpHash
) internal virtual override(Account, AccountMultisig) returns (uint256 validationData) {
return AccountMultisig._validateSignature(userOp, userOpHash);
}
}
19 changes: 14 additions & 5 deletions contracts/abstraction/mocks/SimpleAccount.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,16 @@ import {AccountP256} from "../account/modules/AccountP256.sol";
abstract contract SimpleAccount is Account, Ownable, ERC721Holder, ERC1155Holder {
IEntryPoint private immutable _entryPoint;

constructor(IEntryPoint entryPoint_, address initialOwner) Ownable(initialOwner) {
error AccountUserRestricted();

modifier onlyOwnerOrEntryPoint() {
if (msg.sender != address(entryPoint()) && msg.sender != owner()) {
revert AccountUserRestricted();
}
_;
}

constructor(IEntryPoint entryPoint_, address owner_) Ownable(owner_) {
_entryPoint = entryPoint_;
}

Expand All @@ -28,15 +37,15 @@ abstract contract SimpleAccount is Account, Ownable, ERC721Holder, ERC1155Holder
return user == owner();
}

function execute(address target, uint256 value, bytes calldata data) public virtual onlyAuthorizedOrEntryPoint {
function execute(address target, uint256 value, bytes calldata data) public virtual onlyOwnerOrEntryPoint {
_call(target, value, data);
}

function executeBatch(
address[] calldata targets,
uint256[] calldata values,
bytes[] calldata calldatas
) public virtual onlyAuthorizedOrEntryPoint {
) public virtual onlyOwnerOrEntryPoint {
if (targets.length != calldatas.length || (values.length != 0 && values.length != targets.length)) {
revert AccountInvalidBatchLength();
}
Expand All @@ -53,9 +62,9 @@ abstract contract SimpleAccount is Account, Ownable, ERC721Holder, ERC1155Holder
}

contract SimpleAccountECDSA is SimpleAccount, AccountECDSA {
constructor(IEntryPoint entryPoint_, address initialOwner) SimpleAccount(entryPoint_, initialOwner) {}
constructor(IEntryPoint entryPoint_, address owner_) SimpleAccount(entryPoint_, owner_) {}
}

contract SimpleAccountP256 is SimpleAccount, AccountP256 {
constructor(IEntryPoint entryPoint_, address initialOwner) SimpleAccount(entryPoint_, initialOwner) {}
constructor(IEntryPoint entryPoint_, address owner_) SimpleAccount(entryPoint_, owner_) {}
}
18 changes: 10 additions & 8 deletions test/abstraction/accountECDSA.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,32 @@ const { ERC4337Helper } = require('../helpers/erc4337');

async function fixture() {
const accounts = await ethers.getSigners();
accounts.user = accounts.shift();
accounts.beneficiary = accounts.shift();

const target = await ethers.deployContract('CallReceiverMock');
const helper = new ERC4337Helper('SimpleAccountECDSA');
await helper.wait();
const sender = await helper.newAccount(accounts.user);

return {
accounts,
target,
helper,
entrypoint: helper.entrypoint,
factory: helper.factory,
sender,
};
}

describe('EntryPoint', function () {
describe('AccountECDSA', function () {
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
this.user = this.accounts.shift();
this.beneficiary = this.accounts.shift();
this.sender = await this.helper.newAccount(this.user);
});

describe('execute operation', function () {
beforeEach('fund account', async function () {
await this.user.sendTransaction({ to: this.sender, value: ethers.parseEther('1') });
await this.accounts.user.sendTransaction({ to: this.sender, value: ethers.parseEther('1') });
});

describe('account not deployed yet', function () {
Expand All @@ -45,7 +47,7 @@ describe('EntryPoint', function () {
.then(op => op.addInitCode())
.then(op => op.sign());

await expect(this.entrypoint.handleOps([operation.packed], this.beneficiary))
await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary))
.to.emit(this.entrypoint, 'AccountDeployed')
.withArgs(operation.hash, this.sender, this.factory, ethers.ZeroAddress)
.to.emit(this.target, 'MockFunctionCalledExtra')
Expand All @@ -69,7 +71,7 @@ describe('EntryPoint', function () {
})
.then(op => op.sign());

await expect(this.entrypoint.handleOps([operation.packed], this.beneficiary))
await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary))
.to.emit(this.target, 'MockFunctionCalledExtra')
.withArgs(this.sender, 42);
});
Expand All @@ -88,7 +90,7 @@ describe('EntryPoint', function () {
// compact signature
operation.signature = ethers.Signature.from(operation.signature).compactSerialized;

await expect(this.entrypoint.handleOps([operation.packed], this.beneficiary))
await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary))
.to.emit(this.target, 'MockFunctionCalledExtra')
.withArgs(this.sender, 42);
});
Expand Down

0 comments on commit 4512481

Please sign in to comment.