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

Start initial work on ERC4337 interface and helpers #4991

Draft
wants to merge 55 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
5e82076
Add P256 implementation and testing
Amxx Feb 7, 2024
da0f27e
enable optimizations by default
Amxx Feb 7, 2024
aa59c67
test recovering address
Amxx Feb 7, 2024
9512947
improved testing
Amxx Feb 7, 2024
a60bf48
spelling
Amxx Feb 7, 2024
9185026
fix lint
Amxx Feb 7, 2024
025e360
expose imports tick
Amxx Feb 7, 2024
803e735
fix lint
Amxx Feb 7, 2024
57fcecd
fix lint
Amxx Feb 7, 2024
4dae298
add changeset
Amxx Feb 7, 2024
6cf039d
improve doc
Amxx Feb 7, 2024
c094fa1
add envvar to force allowUnlimitedContractSize
Amxx Feb 7, 2024
20a03df
fix lint
Amxx Feb 7, 2024
15f1a6b
fix stack too deep error in coverage
Amxx Feb 7, 2024
e2040e4
reoder arguments to match ecrecover and EIP-7212
Amxx Feb 13, 2024
695b732
reduce diff
Amxx Mar 13, 2024
41aaf71
Merge branch 'master' into feature/P256
Amxx Mar 13, 2024
f36f183
Start initial work on ERC4337 interface and helpers
Amxx Apr 4, 2024
a1532d0
Packing library
Amxx Apr 4, 2024
00e0eea
reorder UserOperationUtils.sol
Amxx Apr 5, 2024
8fa363e
4337 account wip
Amxx Apr 8, 2024
9309f71
wip
Amxx Apr 10, 2024
1616a96
wip
Amxx Apr 11, 2024
aef2168
refactor
Amxx Apr 11, 2024
04a6fb0
refactor
Amxx Apr 12, 2024
0f3c3fa
entrypoint deploys account
Amxx Apr 12, 2024
3bf4557
Update contracts/utils/cryptography/P256.sol
Amxx Apr 24, 2024
3cbf426
Merge branch 'master' into feature/P256
Amxx Apr 25, 2024
bba7fa3
update pseudocode reference
Amxx Apr 25, 2024
2812ed8
Update contracts/utils/cryptography/P256.sol
Amxx Apr 25, 2024
e0ef63b
refactor neutral element in jAdd
Amxx Apr 26, 2024
a13ad48
refactor entrypoint
Amxx Apr 26, 2024
342256c
erc4337 js helper
Amxx Apr 26, 2024
30c2d12
update 4337 helper
Amxx Apr 29, 2024
b1f3b60
Merge remote-tracking branch 'origin' into erc4337/interfaces-and-hel…
Amxx Apr 30, 2024
2f07188
Working on abstract account primitives
Amxx Apr 30, 2024
a129a45
improve tests
Amxx Apr 30, 2024
57b4fb2
use getBytes
Amxx Apr 30, 2024
c641f0a
update Account
Amxx Apr 30, 2024
3124b88
Merge branch 'feature/P256' into erc4337/interfaces-and-helpers
Amxx Apr 30, 2024
618b563
Add ECDSA and P256 variants of SimpleAccount
Amxx Apr 30, 2024
14474fc
move AccountECDSA and AccountP256 to a "modules" subfolder
Amxx Apr 30, 2024
7babdf1
Merge branch 'master' into erc4337/interfaces-and-helpers
Amxx Apr 30, 2024
6a5c91b
fix comments
Amxx May 1, 2024
1487fdb
inline documentation
Amxx May 2, 2024
6373a08
Inline documentation
Amxx May 2, 2024
4512481
add AccountMultisig module
Amxx May 3, 2024
ee47efc
update
Amxx May 3, 2024
003c232
refactor signature processing
Amxx May 4, 2024
318c372
add AccountAllSignatures.sol that support ECDSA & P256 identities in …
Amxx May 13, 2024
03dfa71
up
Amxx May 15, 2024
82d6bde
Add Account7702
Amxx May 20, 2024
612cadc
AccountCommon
Amxx May 21, 2024
b81817a
rename
Amxx May 21, 2024
fbf4ca5
Merge branch 'erc4337/EIP-7702-account' into erc4337/interfaces-and-h…
Amxx May 21, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/odd-lobsters-wash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

`P256`: Add a library for verification/recovery of Secp256r1 (Aka P256) signatures.
2 changes: 2 additions & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
- name: Check proceduraly generated contracts are up-to-date
run: npm run test:generation
- name: Compare gas costs
uses: ./.github/actions/gas-compare

Check failure on line 47 in .github/workflows/checks.yml

View workflow job for this annotation

GitHub Actions / lint

description is required in metadata of "Compare gas costs" action at "/github/workspace/.github/actions/gas-compare/action.yml"
with:
token: ${{ github.token }}

Expand All @@ -66,10 +66,12 @@
run: bash scripts/upgradeable/transpile.sh
- name: Run tests
run: npm run test
env:
UNLIMITED: true
- name: Check linearisation of the inheritance graph
run: npm run test:inheritance
- name: Check storage layout
uses: ./.github/actions/storage-layout

Check failure on line 74 in .github/workflows/checks.yml

View workflow job for this annotation

GitHub Actions / lint

description is required in metadata of "Compare storage layouts" action at "/github/workspace/.github/actions/storage-layout/action.yml"
continue-on-error: ${{ contains(github.event.pull_request.labels.*.name, 'breaking change') }}
with:
token: ${{ github.token }}
Expand Down
133 changes: 133 additions & 0 deletions contracts/abstraction/account/Account.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {PackedUserOperation, IAccount, IEntryPoint} from "../../interfaces/IERC4337.sol";
import {SignatureChecker} from "../../utils/cryptography/SignatureChecker.sol";
import {ERC4337Utils} from "./../utils/ERC4337Utils.sol";

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

/****************************************************************************************************************
* Modifiers *
****************************************************************************************************************/

modifier onlyEntryPoint() {
if (msg.sender != address(entryPoint())) {
revert AccountEntryPointRestricted();
}
_;
}

/****************************************************************************************************************
* Hooks *
****************************************************************************************************************/

/**
* @dev Return the entryPoint used by this account.
*
* Subclass should return the current entryPoint used by this account.
*/
function entryPoint() public view virtual returns (IEntryPoint);

/**
* @dev Return weither an address (identity) is authorized to operate on this account. Depending on how the
* account is configured, this can be interpreted as either the owner of the account (if operating using a single
* owner -- default) or as an authorized signer if operating using as a multisig account.
*
* Subclass must implement this using their own access control mechanism.
*/
function _isAuthorized(address) internal virtual returns (bool);

/**
* @dev Recover the signer for a given signature and user operation hash. This function does not need to verify
* that the recovered signer is authorized.
*
* Subclass must implement this using their own choice of cryptography.
*/
function _recoverSigner(bytes memory signature, bytes32 userOpHash) internal virtual returns (address);

/****************************************************************************************************************
* Public interface *
****************************************************************************************************************/

/**
* @dev Return the account nonce for the canonical sequence.
*/
function getNonce() public view virtual returns (uint256) {
return entryPoint().getNonce(address(this), 0);
}

/**
* @dev Return the account nonce for a given sequence (key).
*/
function getNonce(uint192 key) public view virtual returns (uint256) {
return entryPoint().getNonce(address(this), key);
}

/// @inheritdoc IAccount
function validateUserOp(
PackedUserOperation calldata userOp,
bytes32 userOpHash,
uint256 missingAccountFunds
) public virtual override onlyEntryPoint returns (uint256 validationData) {
(bool valid, , uint48 validAfter, uint48 validUntil) = _processSignature(userOp.signature, userOpHash);
_validateNonce(userOp.nonce);
_payPrefund(missingAccountFunds);
return ERC4337Utils.packValidationData(valid, validAfter, validUntil);
}

/****************************************************************************************************************
* Internal mechanisms *
****************************************************************************************************************/

/**
* @dev Process the signature is valid for this message.
* @param signature - The user's signature
* @param userOpHash - Hash of the request that must be signed (includes the entrypoint and chain id)
* @return valid - Signature is valid
* @return signer - Address of the signer that produced the signature
* @return validAfter - first timestamp this operation is valid
* @return validUntil - last timestamp this operation is valid. 0 for "indefinite"
*/
function _processSignature(
bytes memory signature,
bytes32 userOpHash
) internal virtual returns (bool valid, address signer, uint48 validAfter, uint48 validUntil) {
address recovered = _recoverSigner(signature, userOpHash);
return (recovered != address(0) && _isAuthorized(recovered), recovered, 0, 0);
}

/**
* @dev Validate the nonce of the UserOperation.
* This method may validate the nonce requirement of this account.
* e.g.
* To limit the nonce to use sequenced UserOps only (no "out of order" UserOps):
* `require(nonce < type(uint64).max)`
*
* The actual nonce uniqueness is managed by the EntryPoint, and thus no other
* action is needed by the account itself.
*
* @param nonce to validate
*/
function _validateNonce(uint256 nonce) internal view virtual {}

/**
* @dev Sends to the entrypoint (msg.sender) the missing funds for this transaction.
* SubClass MAY override this method for better funds management
* (e.g. send to the entryPoint more than the minimum required, so that in future transactions
* it will not be required to send again).
* @param missingAccountFunds - The minimum value this method should send the entrypoint.
* This value MAY be zero, in case there is enough deposit,
* or the userOp has a paymaster.
*/
function _payPrefund(uint256 missingAccountFunds) internal virtual {
if (missingAccountFunds > 0) {
(bool success, ) = payable(msg.sender).call{value: missingAccountFunds}("");
success;
//ignore failure (its EntryPoint's job to verify, not account.)
}
}
}
46 changes: 46 additions & 0 deletions contracts/abstraction/account/AccountCommon.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {IEntryPoint} from "../../interfaces/IERC4337.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.sol";

abstract contract AccountCommon is Account, ERC721Holder, ERC1155Holder {
IEntryPoint private immutable _entryPoint;

constructor(IEntryPoint entryPoint_) {
_entryPoint = entryPoint_;
}

receive() external payable {}

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

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);
}
}
29 changes: 29 additions & 0 deletions contracts/abstraction/account/modules/AccountAllSignatures.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {PackedUserOperation} from "../../../interfaces/IERC4337.sol";
import {AccountECDSA} from "./AccountECDSA.sol";
import {AccountP256} from "./AccountP256.sol";

abstract contract AccountAllSignatures is AccountECDSA, AccountP256 {
enum SignatureType {
ECDSA, // secp256k1
P256 // secp256r1
}

function _recoverSigner(
bytes memory signature,
bytes32 userOpHash
) internal virtual override(AccountECDSA, AccountP256) returns (address) {
(SignatureType sigType, bytes memory sigData) = abi.decode(signature, (SignatureType, bytes));

if (sigType == SignatureType.ECDSA) {
return AccountECDSA._recoverSigner(sigData, userOpHash);
} else if (sigType == SignatureType.P256) {
return AccountP256._recoverSigner(sigData, userOpHash);
} else {
return address(0);
}
}
}
45 changes: 45 additions & 0 deletions contracts/abstraction/account/modules/AccountECDSA.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {PackedUserOperation} from "../../../interfaces/IERC4337.sol";
import {MessageHashUtils} from "../../../utils/cryptography/MessageHashUtils.sol";
import {ECDSA} from "../../../utils/cryptography/ECDSA.sol";
import {Account} from "../Account.sol";

abstract contract AccountECDSA is Account {
function _recoverSigner(
bytes memory signature,
bytes32 userOpHash
) internal virtual override returns (address signer) {
bytes32 msgHash = MessageHashUtils.toEthSignedMessageHash(userOpHash);

// This implementation support both "normal" and short signature formats:
// - 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.
if (signature.length == 65) {
bytes32 r;
bytes32 s;
uint8 v;
/// @solidity memory-safe-assembly
assembly {
r := mload(add(signature, 0x20))
s := mload(add(signature, 0x40))
v := byte(0, mload(add(signature, 0x60)))
}
(signer, , ) = ECDSA.tryRecover(msgHash, v, r, s); // return address(0) on errors
} else if (signature.length == 64) {
bytes32 r;
bytes32 vs;
/// @solidity memory-safe-assembly
assembly {
r := mload(add(signature, 0x20))
vs := mload(add(signature, 0x40))
}
(signer, , ) = ECDSA.tryRecover(msgHash, r, vs);
} else {
return address(0);
}
}
}
11 changes: 11 additions & 0 deletions contracts/abstraction/account/modules/AccountEIP7702.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

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

abstract contract Account7702 is Account {
function _isAuthorized(address user) internal view virtual override returns (bool) {
return user == address(this);
}
}
44 changes: 44 additions & 0 deletions contracts/abstraction/account/modules/AccountMultisig.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// 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() public view virtual returns (uint256);

function _processSignature(
bytes memory signature,
bytes32 userOpHash
) internal virtual override returns (bool, address, uint48, uint48) {
bytes[] memory signatures = abi.decode(signature, (bytes[]));

if (signatures.length < requiredSignatures()) {
return (false, address(0), 0, 0);
}

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

for (uint256 i = 0; i < signatures.length; ++i) {
(bool valid, address signer, uint48 validAfter, uint48 validUntil) = super._processSignature(
signatures[i],
userOpHash
);
if (valid && signer > lastSigner) {
lastSigner = signer;
globalValidAfter = uint48(Math.ternary(validUntil < globalValidUntil, globalValidUntil, validAfter));
globalValidUntil = uint48(
Math.ternary(validUntil > globalValidUntil || validUntil == 0, globalValidUntil, validUntil)
);
} else {
return (false, address(0), 0, 0);
}
}
return (true, address(this), globalValidAfter, globalValidUntil);
}
}
48 changes: 48 additions & 0 deletions contracts/abstraction/account/modules/AccountP256.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {PackedUserOperation} from "../../../interfaces/IERC4337.sol";
import {MessageHashUtils} from "../../../utils/cryptography/MessageHashUtils.sol";
import {P256} from "../../../utils/cryptography/P256.sol";
import {Account} from "../Account.sol";

abstract contract AccountP256 is Account {
error P256InvalidSignatureLength(uint256 length);

function _recoverSigner(bytes memory signature, bytes32 userOpHash) internal virtual override returns (address) {
bytes32 msgHash = MessageHashUtils.toEthSignedMessageHash(userOpHash);

// This implementation support signature that are 65 bytes long in the (R,S,V) format
if (signature.length == 65) {
uint256 r;
uint256 s;
uint8 v;
/// @solidity memory-safe-assembly
assembly {
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);
} else if (signature.length == 128) {
uint256 qx;
uint256 qy;
uint256 r;
uint256 s;
/// @solidity memory-safe-assembly
assembly {
qx := mload(add(signature, 0x20))
qy := mload(add(signature, 0x40))
r := mload(add(signature, 0x60))
s := mload(add(signature, 0x80))
}
// can qx be reconstructed from qy to reduce size of signatures?

// this can leverage EIP-7212 precompile if available
return P256.verify(uint256(msgHash), r, s, qx, qy) ? P256.getAddress(qx, qy) : address(0);
} else {
return address(0);
}
}
}