From a7b170a9777d7c1523e1ddcce6ea94a77cf269cf Mon Sep 17 00:00:00 2001 From: ernestognw Date: Mon, 25 Nov 2024 20:51:13 -0600 Subject: [PATCH 01/51] WIP: Migrate Account code --- CHANGELOG.md | 28 +- contracts/account/draft-AccountBase.sol | 128 +++++++ contracts/account/draft-AccountECDSA.sol | 68 ++++ contracts/account/draft-AccountP256.sol | 73 ++++ contracts/account/draft-AccountRSA.sol | 75 +++++ contracts/mocks/CallReceiverMock.sol | 78 +++++ contracts/mocks/ERC7739SignerECDSA.sol | 23 ++ contracts/mocks/ERC7739SignerMock.sol | 20 -- contracts/mocks/ERC7739SignerP256.sol | 26 ++ contracts/mocks/ERC7739SignerRSA.sol | 24 ++ contracts/mocks/account/AccountBaseMock.sol | 20 ++ .../token/ERC1155/utils/ERC1155HolderLean.sol | 31 ++ .../cryptography/draft-ERC7739Signer.sol | 22 +- package-lock.json | 17 - package.json | 5 +- remappings.txt | 9 +- test/account/Account.behavior.js | 312 ++++++++++++++++++ test/account/draft-AccountBase.test.js | 24 ++ test/account/draft-AccountECDSA.js | 44 +++ test/account/draft-AccountP256.test.js | 44 +++ test/account/draft-AccountRSA.test.js | 44 +++ test/helpers/erc4337.js | 43 +++ test/helpers/signers.js | 131 ++++++++ .../cryptography/ERC7739Signer.behavior.js | 101 ++++++ .../cryptography/draft-ERC7739Signer.test.js | 128 ++----- 25 files changed, 1360 insertions(+), 158 deletions(-) create mode 100644 contracts/account/draft-AccountBase.sol create mode 100644 contracts/account/draft-AccountECDSA.sol create mode 100644 contracts/account/draft-AccountP256.sol create mode 100644 contracts/account/draft-AccountRSA.sol create mode 100644 contracts/mocks/CallReceiverMock.sol create mode 100644 contracts/mocks/ERC7739SignerECDSA.sol delete mode 100644 contracts/mocks/ERC7739SignerMock.sol create mode 100644 contracts/mocks/ERC7739SignerP256.sol create mode 100644 contracts/mocks/ERC7739SignerRSA.sol create mode 100644 contracts/mocks/account/AccountBaseMock.sol create mode 100644 contracts/token/ERC1155/utils/ERC1155HolderLean.sol create mode 100644 test/account/Account.behavior.js create mode 100644 test/account/draft-AccountBase.test.js create mode 100644 test/account/draft-AccountECDSA.js create mode 100644 test/account/draft-AccountP256.test.js create mode 100644 test/account/draft-AccountRSA.test.js create mode 100644 test/helpers/erc4337.js create mode 100644 test/helpers/signers.js create mode 100644 test/utils/cryptography/ERC7739Signer.behavior.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f5cfb8d..f998e75d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,29 +1,33 @@ +## XX-XX-2024 + +- `AccountECDSA`, `AccountP256` and `AccountRSA`: Add implementations of `AccountBase` based on the available signature schemes (i.e. ECDSA, P256, RSA). +- `AccountBase`: Added a simple ERC-4337 account implementation with the minimal logic to process user operations. + ## 06-11-2024 -* `ERC7739Utils`: Add a library that implements a defensive rehashing mechanism to prevent replayability of smart contract signatures based on the ERC-7739. -* `ERC7739Signer`: An abstract contract to validate signatures following the rehashing scheme from `ERC7739Utils`. +- `ERC7739Utils`: Add a library that implements a defensive rehashing mechanism to prevent replayability of smart contract signatures based on the ERC-7739. +- `ERC7739Signer`: An abstract contract to validate signatures following the rehashing scheme from `ERC7739Utils`. ## 15-10-2024 -* `ERC20Collateral`: Extension of ERC-20 that limits the supply of tokens based on a collateral and time-based expiration. +- `ERC20Collateral`: Extension of ERC-20 that limits the supply of tokens based on a collateral and time-based expiration. ## 10-10-2024 -* `ERC20Allowlist`: Extension of ERC-20 that implements an allow list to enable token transfers, disabled by default. -* `ERC20Blocklist`: Extension of ERC-20 that implements a block list to restrict token transfers, enabled by default. -* `ERC20Custodian`: Extension of ERC-20 that allows a custodian to freeze user's tokens by a certain amount. +- `ERC20Allowlist`: Extension of ERC-20 that implements an allow list to enable token transfers, disabled by default. +- `ERC20Blocklist`: Extension of ERC-20 that implements a block list to restrict token transfers, enabled by default. +- `ERC20Custodian`: Extension of ERC-20 that allows a custodian to freeze user's tokens by a certain amount. ## 03-10-2024 -* `OnTokenTransferAdapter`: An adapter that exposes `transferAndCall` on top of an ERC-1363 receiver. +- `OnTokenTransferAdapter`: An adapter that exposes `transferAndCall` on top of an ERC-1363 receiver. ## 15-05-2024 -* `HybridProxy`: Add a proxy contract that can either use a beacon to retrieve the implementation or fallback to an address in the ERC-1967's implementation slot. +- `HybridProxy`: Add a proxy contract that can either use a beacon to retrieve the implementation or fallback to an address in the ERC-1967's implementation slot. ## 11-05-2024 -* `AccessManagerLight`: Add a simpler version of the `AccessManager` in OpenZeppelin Contracts. -* `ERC4626Fees`: Extension of ERC-4626 that implements fees on entry and exit from the vault. -* `Masks`: Add library to handle `bytes32` masks. - +- `AccessManagerLight`: Add a simpler version of the `AccessManager` in OpenZeppelin Contracts. +- `ERC4626Fees`: Extension of ERC-4626 that implements fees on entry and exit from the vault. +- `Masks`: Add library to handle `bytes32` masks. diff --git a/contracts/account/draft-AccountBase.sol b/contracts/account/draft-AccountBase.sol new file mode 100644 index 00000000..c38cbd4e --- /dev/null +++ b/contracts/account/draft-AccountBase.sol @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {PackedUserOperation, IAccount, IEntryPoint, IAccountExecute} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +/** + * @dev A simple ERC4337 account implementation. + * + * This base implementation only includes the minimal logic to process user operations. + * Developers must implement the {_validateUserOp} function to define the account's validation logic. + */ +abstract contract AccountBase is IAccount, IAccountExecute { + /** + * @dev Unauthorized call to the account. + */ + error AccountUnauthorized(address sender); + + /** + * @dev Revert if the caller is not the entry point or the account itself. + */ + modifier onlyEntryPointOrSelf() { + _checkEntryPointOrSelf(); + _; + } + + /** + * @dev Revert if the caller is not the entry point. + */ + modifier onlyEntryPoint() { + _checkEntryPoint(); + _; + } + + /** + * @dev Canonical entry point for the account that forwards and validates user operations. + */ + function entryPoint() public view virtual returns (IEntryPoint) { + return IEntryPoint(0x0000000071727De22E5E9d8BAf0edAc6f37da032); + } + + /** + * @dev Return the account nonce for the canonical sequence. + */ + function getNonce() public view virtual returns (uint256) { + return getNonce(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 onlyEntryPoint returns (uint256) { + uint256 validationData = _validateUserOp(userOp, userOpHash); + _payPrefund(missingAccountFunds); + return validationData; + } + + /** + * @inheritdoc IAccountExecute + */ + function executeUserOp( + PackedUserOperation calldata userOp, + bytes32 /*userOpHash*/ + ) public virtual onlyEntryPointOrSelf { + (address target, uint256 value, bytes memory data) = abi.decode(userOp.callData[4:], (address, uint256, bytes)); + Address.functionCallWithValue(target, data, value); + } + + /** + * @dev Validation logic for {validateUserOp}. + * + * IMPORTANT: Implementing a mechanism to validate user operations is a security-sensitive operation + * as it may allow an attacker to bypass the account's security measures. Check out {AccountECDSA}, + * {AccountP256}, or {AccountRSA} for digital signature validation implementations. + */ + function _validateUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash + ) internal virtual returns (uint256 validationData); + + /** + * @dev Sends the missing funds for executing the user operation to the {entrypoint}. + * The `missingAccountFunds` must be defined by the entrypoint when calling {validateUserOp}. + */ + function _payPrefund(uint256 missingAccountFunds) internal virtual { + if (missingAccountFunds > 0) { + (bool success, ) = payable(msg.sender).call{value: missingAccountFunds}(""); + success; // Silence warning. The entrypoint should validate the result. + } + } + + /** + * @dev Ensures the caller is the {entrypoint}. + */ + function _checkEntryPoint() internal view virtual { + address sender = msg.sender; + if (sender != address(entryPoint())) { + revert AccountUnauthorized(sender); + } + } + + /** + * @dev Ensures the caller is the {entrypoint} or the account itself. + */ + function _checkEntryPointOrSelf() internal view virtual { + address sender = msg.sender; + if (sender != address(this) && sender != address(entryPoint())) { + revert AccountUnauthorized(sender); + } + } + + /** + * @dev Receive Ether. + */ + receive() external payable virtual {} +} diff --git a/contracts/account/draft-AccountECDSA.sol b/contracts/account/draft-AccountECDSA.sol new file mode 100644 index 00000000..c5dd6487 --- /dev/null +++ b/contracts/account/draft-AccountECDSA.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; +import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol"; +import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; +import {ERC1155HolderLean, IERC1155Receiver} from "../token/ERC1155/utils/ERC1155HolderLean.sol"; +import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {AccountBase} from "./draft-AccountBase.sol"; +import {ERC7739Signer} from "../utils/cryptography/draft-ERC7739Signer.sol"; + +/** + * @dev Account implementation using {ECDSA} signatures and {ERC7739Signer} for replay protection. + */ +abstract contract AccountECDSA is ERC165, ERC7739Signer, ERC721Holder, ERC1155HolderLean, AccountBase { + address private immutable _signer; + + /** + * @dev Initializes the account with the address of the native signer. + */ + constructor(address signerAddr) { + _signer = signerAddr; + } + + /** + * @dev Return the account's signer address. + */ + function signer() public view virtual returns (address) { + return _signer; + } + + /** + * @dev Internal version of {validateUserOp} that relies on {_validateNestedEIP712Signature}. + * + * NOTE: To override the signature functionality, try overriding {_validateNestedEIP712Signature} instead. + */ + function _validateUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash + ) internal view override returns (uint256) { + return + _validateNestedEIP712Signature(userOpHash, userOp.signature) + ? ERC4337Utils.SIG_VALIDATION_SUCCESS + : ERC4337Utils.SIG_VALIDATION_FAILED; + } + + /** + * @dev Validates the signature using the account's signer. + * + * This function provides a nested EIP-712 hash. Developers must override only this + * function to ensure no raw message signing is possible. + */ + function _validateNestedEIP712Signature( + bytes32 nestedEIP712Hash, + bytes calldata signature + ) internal view virtual override returns (bool) { + (address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(nestedEIP712Hash, signature); + return signer() == recovered && err == ECDSA.RecoverError.NoError; + } + + /// @inheritdoc ERC165 + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { + return interfaceId == type(IERC1155Receiver).interfaceId || super.supportsInterface(interfaceId); + } +} diff --git a/contracts/account/draft-AccountP256.sol b/contracts/account/draft-AccountP256.sol new file mode 100644 index 00000000..1804517f --- /dev/null +++ b/contracts/account/draft-AccountP256.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; +import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol"; +import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; +import {ERC1155HolderLean, IERC1155Receiver} from "../token/ERC1155/utils/ERC1155HolderLean.sol"; +import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {P256} from "@openzeppelin/contracts/utils/cryptography/P256.sol"; +import {AccountBase} from "./draft-AccountBase.sol"; +import {ERC7739Signer} from "../utils/cryptography/draft-ERC7739Signer.sol"; + +/** + * @dev Account implementation using {P256} signatures and {ERC7739Signer} for replay protection. + */ +abstract contract AccountP256 is ERC165, ERC7739Signer, ERC721Holder, ERC1155HolderLean, AccountBase { + bytes32 private immutable _qx; + bytes32 private immutable _qy; + + /** + * @dev Initializes the account with the P256 public key. + */ + constructor(bytes32 qx, bytes32 qy) { + _qx = qx; + _qy = qy; + } + + /** + * @dev Return the account's signer P256 public key. + */ + function signer() public view virtual returns (bytes32 qx, bytes32 qy) { + return (_qx, _qy); + } + + /** + * @dev Internal version of {validateUserOp} that relies on {_validateNestedEIP712Signature}. + * + * NOTE: To override the signature functionality, try overriding {_validateNestedEIP712Signature} instead. + */ + function _validateUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash + ) internal view override returns (uint256) { + return + _isValidSignature(userOpHash, userOp.signature) + ? ERC4337Utils.SIG_VALIDATION_SUCCESS + : ERC4337Utils.SIG_VALIDATION_FAILED; + } + + /** + * @dev Validates the signature using the account's signer. + * + * This function provides a nested EIP-712 hash. Developers must override only this + * function to ensure no raw message signing is possible. + */ + function _validateNestedEIP712Signature( + bytes32 hash, + bytes calldata signature + ) internal view virtual override returns (bool) { + if (signature.length < 0x40) return false; + bytes32 r = bytes32(signature[0x00:0x20]); + bytes32 s = bytes32(signature[0x20:0x40]); + (bytes32 qx, bytes32 qy) = signer(); + return P256.verify(hash, r, s, qx, qy); + } + + /// @inheritdoc ERC165 + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { + return interfaceId == type(IERC1155Receiver).interfaceId || super.supportsInterface(interfaceId); + } +} diff --git a/contracts/account/draft-AccountRSA.sol b/contracts/account/draft-AccountRSA.sol new file mode 100644 index 00000000..f79cee45 --- /dev/null +++ b/contracts/account/draft-AccountRSA.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; +import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol"; +import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; +import {ERC1155HolderLean, IERC1155Receiver} from "../token/ERC1155/utils/ERC1155HolderLean.sol"; +import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {RSA} from "@openzeppelin/contracts/utils/cryptography/RSA.sol"; +import {AccountBase} from "./draft-AccountBase.sol"; +import {ERC7739Signer} from "../utils/cryptography/draft-ERC7739Signer.sol"; + +/** + * @dev Account implementation using {RSA} signatures and {ERC7739Signer} for replay protection. + * + * NOTE: Storing `_e` and `_n` in regular storage violate ERC-7562 validation rules if the contract + * is used as an ERC-1271 signer during the validation phase of a different account contract. + * Consider deploying this contract through a factory that sets `_e` and `_n` as immutable arguments + * (see {Clones-cloneDeterministicWithImmutableArgs}). + */ +abstract contract AccountRSA is ERC165, ERC7739Signer, ERC721Holder, ERC1155HolderLean, AccountBase { + bytes private _e; + bytes private _n; + + /** + * @dev Initializes the account with the RSA public key. + */ + constructor(bytes memory e, bytes memory n) { + _e = e; + _n = n; + } + + /** + * @dev Return the account's signer RSA public key. + */ + function signer() public view virtual returns (bytes memory e, bytes memory n) { + return (_e, _n); + } + + /** + * @dev Internal version of {validateUserOp} that relies on {_validateNestedEIP712Signature}. + * + * NOTE: To override the signature functionality, try overriding {_validateNestedEIP712Signature} instead. + */ + function _validateUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash + ) internal view override returns (uint256) { + return + _isValidSignature(userOpHash, userOp.signature) + ? ERC4337Utils.SIG_VALIDATION_SUCCESS + : ERC4337Utils.SIG_VALIDATION_FAILED; + } + + /** + * @dev Validates the signature using the account's signer. + * + * This function provides a nested EIP-712 hash. Developers must override only this + * function to ensure no raw message signing is possible. + */ + function _validateNestedEIP712Signature( + bytes32 hash, + bytes calldata signature + ) internal view virtual override returns (bool) { + (bytes memory e, bytes memory n) = signer(); + return RSA.pkcs1Sha256(hash, signature, e, n); + } + + /// @inheritdoc ERC165 + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { + return interfaceId == type(IERC1155Receiver).interfaceId || super.supportsInterface(interfaceId); + } +} diff --git a/contracts/mocks/CallReceiverMock.sol b/contracts/mocks/CallReceiverMock.sol new file mode 100644 index 00000000..7f6bfd0b --- /dev/null +++ b/contracts/mocks/CallReceiverMock.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +contract CallReceiverMock { + event MockFunctionCalled(); + event MockFunctionCalledWithArgs(uint256 a, uint256 b); + event MockFunctionCalledExtra(address caller, uint256 value); + + uint256[] private _array; + + function mockFunction() public payable returns (string memory) { + emit MockFunctionCalled(); + + return "0x1234"; + } + + function mockFunctionExtra() public payable { + emit MockFunctionCalledExtra(msg.sender, msg.value); + } + + function mockFunctionEmptyReturn() public payable { + emit MockFunctionCalled(); + } + + function mockFunctionWithArgs(uint256 a, uint256 b) public payable returns (string memory) { + emit MockFunctionCalledWithArgs(a, b); + + return "0x1234"; + } + + function mockFunctionNonPayable() public returns (string memory) { + emit MockFunctionCalled(); + + return "0x1234"; + } + + function mockStaticFunction() public pure returns (string memory) { + return "0x1234"; + } + + function mockFunctionRevertsNoReason() public payable { + revert(); + } + + function mockFunctionRevertsReason() public payable { + revert("CallReceiverMock: reverting"); + } + + function mockFunctionThrows() public payable { + assert(false); + } + + function mockFunctionOutOfGas() public payable { + for (uint256 i = 0; ; ++i) { + _array.push(i); + } + } + + function mockFunctionWritesStorage(bytes32 slot, bytes32 value) public returns (string memory) { + assembly { + sstore(slot, value) + } + return "0x1234"; + } +} + +contract CallReceiverMockTrustingForwarder is CallReceiverMock { + address private _trustedForwarder; + + constructor(address trustedForwarder_) { + _trustedForwarder = trustedForwarder_; + } + + function isTrustedForwarder(address forwarder) public view virtual returns (bool) { + return forwarder == _trustedForwarder; + } +} diff --git a/contracts/mocks/ERC7739SignerECDSA.sol b/contracts/mocks/ERC7739SignerECDSA.sol new file mode 100644 index 00000000..c6c81b4f --- /dev/null +++ b/contracts/mocks/ERC7739SignerECDSA.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; +import {ERC7739Signer} from "../utils/cryptography/draft-ERC7739Signer.sol"; + +contract ERC7739SignerECDSA is ERC7739Signer { + address private immutable _signer; + + constructor(address signerAddr) EIP712("ERC7739SignerECDSA", "1") { + _signer = signerAddr; + } + + function _validateNestedEIP712Signature( + bytes32 hash, + bytes calldata signature + ) internal view virtual override returns (bool) { + (address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(hash, signature); + return _signer == recovered && err == ECDSA.RecoverError.NoError; + } +} diff --git a/contracts/mocks/ERC7739SignerMock.sol b/contracts/mocks/ERC7739SignerMock.sol deleted file mode 100644 index a5f12fca..00000000 --- a/contracts/mocks/ERC7739SignerMock.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.20; - -import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; -import {ERC7739Signer} from "../utils/cryptography/draft-ERC7739Signer.sol"; - -contract ERC7739SignerMock is ERC7739Signer { - address private immutable _eoa; - - constructor(address eoa) EIP712("ERC7739SignerMock", "1") { - _eoa = eoa; - } - - function _validateSignature(bytes32 hash, bytes calldata signature) internal view virtual override returns (bool) { - (address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(hash, signature); - return _eoa == recovered && err == ECDSA.RecoverError.NoError; - } -} diff --git a/contracts/mocks/ERC7739SignerP256.sol b/contracts/mocks/ERC7739SignerP256.sol new file mode 100644 index 00000000..18c1daa3 --- /dev/null +++ b/contracts/mocks/ERC7739SignerP256.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {P256} from "@openzeppelin/contracts/utils/cryptography/P256.sol"; +import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; +import {ERC7739Signer} from "../utils/cryptography/draft-ERC7739Signer.sol"; + +contract ERC7739SignerP256 is ERC7739Signer { + bytes32 private immutable _qx; + bytes32 private immutable _qy; + + constructor(bytes32 qx, bytes32 qy) EIP712("ERC7739SignerP256", "1") { + _qx = qx; + _qy = qy; + } + + function _validateNestedEIP712Signature( + bytes32 hash, + bytes calldata signature + ) internal view virtual override returns (bool) { + bytes32 r = bytes32(signature[0x00:0x20]); + bytes32 s = bytes32(signature[0x20:0x40]); + return P256.verify(hash, r, s, _qx, _qy); + } +} diff --git a/contracts/mocks/ERC7739SignerRSA.sol b/contracts/mocks/ERC7739SignerRSA.sol new file mode 100644 index 00000000..9abad594 --- /dev/null +++ b/contracts/mocks/ERC7739SignerRSA.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {RSA} from "@openzeppelin/contracts/utils/cryptography/RSA.sol"; +import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; +import {ERC7739Signer} from "../utils/cryptography/draft-ERC7739Signer.sol"; + +contract ERC7739SignerRSA is ERC7739Signer { + bytes private _e; + bytes private _n; + + constructor(bytes memory e, bytes memory n) EIP712("ERC7739SignerRSA", "1") { + _e = e; + _n = n; + } + + function _validateNestedEIP712Signature( + bytes32 hash, + bytes calldata signature + ) internal view virtual override returns (bool) { + return RSA.pkcs1Sha256(hash, signature, _e, _n); + } +} diff --git a/contracts/mocks/account/AccountBaseMock.sol b/contracts/mocks/account/AccountBaseMock.sol new file mode 100644 index 00000000..42098311 --- /dev/null +++ b/contracts/mocks/account/AccountBaseMock.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; +import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol"; +import {AccountBase} from "../../account/draft-AccountBase.sol"; + +contract AccountBaseMock is AccountBase { + /// Validates a user operation with a boolean signature. + function _validateUserOp( + PackedUserOperation calldata userOp, + bytes32 /* userOpHash */ + ) internal pure override returns (uint256 validationData) { + return + bytes1(userOp.signature[0:1]) == bytes1(0x01) + ? ERC4337Utils.SIG_VALIDATION_SUCCESS + : ERC4337Utils.SIG_VALIDATION_FAILED; + } +} diff --git a/contracts/token/ERC1155/utils/ERC1155HolderLean.sol b/contracts/token/ERC1155/utils/ERC1155HolderLean.sol new file mode 100644 index 00000000..f55e39c8 --- /dev/null +++ b/contracts/token/ERC1155/utils/ERC1155HolderLean.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC1155/utils/ERC1155Holder.sol) + +pragma solidity ^0.8.20; + +import {IERC1155Receiver} from "@openzeppelin/contracts/interfaces/IERC1155Receiver.sol"; + +/** + * @dev Version of {ERC1155Holder} that doesn't include {IERC165} detection. + */ +abstract contract ERC1155HolderLean is IERC1155Receiver { + function onERC1155Received( + address, + address, + uint256, + uint256, + bytes memory + ) public virtual override returns (bytes4) { + return this.onERC1155Received.selector; + } + + function onERC1155BatchReceived( + address, + address, + uint256[] memory, + uint256[] memory, + bytes memory + ) public virtual override returns (bytes4) { + return this.onERC1155BatchReceived.selector; + } +} diff --git a/contracts/utils/cryptography/draft-ERC7739Signer.sol b/contracts/utils/cryptography/draft-ERC7739Signer.sol index 6ccbc040..d15aad22 100644 --- a/contracts/utils/cryptography/draft-ERC7739Signer.sol +++ b/contracts/utils/cryptography/draft-ERC7739Signer.sol @@ -60,16 +60,23 @@ abstract contract ERC7739Signer is EIP712, IERC1271 { bytes32 hash, bytes calldata signature ) internal view virtual returns (bool) { - return _validateSignature(_domainSeparatorV4().toTypedDataHash(hash.personalSignStructHash()), signature); + return + _validateNestedEIP712Signature( + _domainSeparatorV4().toTypedDataHash(hash.personalSignStructHash()), + signature + ); } /** * @dev Nested EIP-712 typed data verification. + * + * NOTE: Instead of overriding this function, try with {_validateNestedEIP712Signature}. It encapsulates + * nested EIP-712 hashes. */ function _isValidNestedTypedDataSignature( bytes32 hash, bytes calldata encodedSignature - ) internal view virtual returns (bool) { + ) internal view returns (bool) { // decode signature ( bytes calldata signature, @@ -93,7 +100,7 @@ abstract contract ERC7739Signer is EIP712, IERC1271 { return hash == appSeparator.toTypedDataHash(contentsHash) && bytes(contentsDescr).length != 0 && - _validateSignature( + _validateNestedEIP712Signature( appSeparator.toTypedDataHash( ERC7739Utils.typedDataSignStructHash( contentsDescr, @@ -108,9 +115,16 @@ abstract contract ERC7739Signer is EIP712, IERC1271 { /** * @dev Signature validation algorithm. * + * To ensure there's no way to inherit from this ERC-7739 signer and still be able to sign raw messages, + * this function provides a nested EIP-712 hash. Developers must implement only this function to ensure + * no raw message signing is possible. + * * WARNING: Implementing a signature validation algorithm is a security-sensitive operation as it involves * cryptographic verification. It is important to review and test thoroughly before deployment. Consider * using one of the signature verification libraries ({ECDSA}, {P256} or {RSA}). */ - function _validateSignature(bytes32 hash, bytes calldata signature) internal view virtual returns (bool); + function _validateNestedEIP712Signature( + bytes32 nestedEIP712Hash, + bytes calldata signature + ) internal view virtual returns (bool); } diff --git a/package-lock.json b/package-lock.json index 3715b23e..335c47d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,10 +8,6 @@ "name": "@openzeppelin/community-contracts", "version": "0.0.1", "license": "MIT", - "dependencies": { - "@openzeppelin/contracts": "^5.0.2", - "@openzeppelin/contracts-upgradeable": "^5.0.2" - }, "devDependencies": { "@nomicfoundation/hardhat-chai-matchers": "^2.0.6", "@nomicfoundation/hardhat-ethers": "^3.0.5", @@ -1114,19 +1110,6 @@ "node": ">= 10" } }, - "node_modules/@openzeppelin/contracts": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-5.0.2.tgz", - "integrity": "sha512-ytPc6eLGcHHnapAZ9S+5qsdomhjo6QBHTDRRBFfTxXIpsicMhVPouPgmUPebZZZGX7vt9USA+Z+0M0dSVtSUEA==" - }, - "node_modules/@openzeppelin/contracts-upgradeable": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-5.0.2.tgz", - "integrity": "sha512-0MmkHSHiW2NRFiT9/r5Lu4eJq5UJ4/tzlOgYXNAIj/ONkQTVnz22pLxDvp4C4uZ9he7ZFvGn3Driptn1/iU7tQ==", - "peerDependencies": { - "@openzeppelin/contracts": "5.0.2" - } - }, "node_modules/@scure/base": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.6.tgz", diff --git a/package.json b/package.json index 5aae5a05..1f19a7be 100644 --- a/package.json +++ b/package.json @@ -36,10 +36,7 @@ "security", "zeppelin" ], - "dependencies": { - "@openzeppelin/contracts": "^5.0.2", - "@openzeppelin/contracts-upgradeable": "^5.0.2" - }, + "dependencies": {}, "devDependencies": { "@nomicfoundation/hardhat-chai-matchers": "^2.0.6", "@nomicfoundation/hardhat-ethers": "^3.0.5", diff --git a/remappings.txt b/remappings.txt index e72851a0..d74d317e 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,7 +1,2 @@ -@openzeppelin/contracts@v5/=node_modules/@openzeppelin/contracts/ -@openzeppelin/contracts-upgradeable@v5/=node_modules/@openzeppelin/contracts-upgradeable/ - -@openzeppelin/contracts@master/=lib/@openzeppelin-contracts/contracts/ -@openzeppelin/contracts-upgradeable@master/=lib/@openzeppelin-contracts-upgradeable/contracts/ - -@openzeppelin/community-contracts/=contracts/ +@openzeppelin/contracts/=lib/@openzeppelin-contracts/contracts/ +@openzeppelin/contracts-upgradeable/=lib/@openzeppelin-contracts-upgradeable/contracts/ diff --git a/test/account/Account.behavior.js b/test/account/Account.behavior.js new file mode 100644 index 00000000..a52de182 --- /dev/null +++ b/test/account/Account.behavior.js @@ -0,0 +1,312 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { impersonate } = require('../../lib/@openzeppelin-contracts/test/helpers/account'); +const { + SIG_VALIDATION_SUCCESS, + SIG_VALIDATION_FAILURE, +} = require('../../lib/@openzeppelin-contracts/test/helpers/erc4337'); +const { setBalance } = require('@nomicfoundation/hardhat-network-helpers'); +const { + shouldSupportInterfaces, +} = require('../../lib/@openzeppelin-contracts/test/utils/introspection/SupportsInterface.behavior'); + +function shouldBehaveLikeAnAccountBase() { + describe('entryPoint', function () { + it('should return the canonical entrypoint', async function () { + await this.smartAccount.deploy(); + expect(await this.smartAccount.entryPoint()).to.equal(this.entrypoint.target); + }); + }); + + describe('validateUserOp', function () { + beforeEach(async function () { + await setBalance(this.smartAccount.target, ethers.parseEther('1')); + await this.smartAccount.deploy(); + }); + + it('should revert if the caller is not the canonical entrypoint', async function () { + const selector = this.smartAccount.interface.getFunction('executeUserOp').selector; + const operation = await this.smartAccount + .createOp({ + callData: ethers.concat([ + selector, + ethers.AbiCoder.defaultAbiCoder().encode( + ['address', 'uint256', 'bytes'], + [this.target.target, 0, this.target.interface.encodeFunctionData('mockFunctionExtra')], + ), + ]), + }) + .then(op => op.sign(this.domain, this.signer)); + + await expect(this.smartAccount.connect(this.other).validateUserOp(operation.packed, operation.hash, 0)) + .to.be.revertedWithCustomError(this.smartAccount, 'AccountUnauthorized') + .withArgs(this.other); + }); + + describe('when the caller is the canonical entrypoint', function () { + beforeEach(async function () { + this.entrypointAsSigner = await impersonate(this.entrypoint.target); + }); + + it('should return SIG_VALIDATION_SUCCESS if the signature is valid', async function () { + const selector = this.smartAccount.interface.getFunction('executeUserOp').selector; + const operation = await this.smartAccount + .createOp({ + callData: ethers.concat([ + selector, + ethers.AbiCoder.defaultAbiCoder().encode( + ['address', 'uint256', 'bytes'], + [this.target.target, 0, this.target.interface.encodeFunctionData('mockFunctionExtra')], + ), + ]), + }) + .then(op => op.sign(this.domain, this.signer)); + + expect( + await this.smartAccount + .connect(this.entrypointAsSigner) + .validateUserOp.staticCall(operation.packed, operation.hash, 0), + ).to.eq(SIG_VALIDATION_SUCCESS); + }); + + it('should return SIG_VALIDATION_FAILURE if the signature is invalid', async function () { + const selector = this.smartAccount.interface.getFunction('executeUserOp').selector; + const operation = await this.smartAccount.createOp({ + callData: ethers.concat([ + selector, + ethers.AbiCoder.defaultAbiCoder().encode( + ['address', 'uint256', 'bytes'], + [this.target.target, 0, this.target.interface.encodeFunctionData('mockFunctionExtra')], + ), + ]), + }); + + operation.signature = '0x00'; + + expect( + await this.smartAccount + .connect(this.entrypointAsSigner) + .validateUserOp.staticCall(operation.packed, operation.hash, 0), + ).to.eq(SIG_VALIDATION_FAILURE); + }); + + it('should pay missing account funds for execution', async function () { + const selector = this.smartAccount.interface.getFunction('executeUserOp').selector; + const operation = await this.smartAccount + .createOp({ + callData: ethers.concat([ + selector, + ethers.AbiCoder.defaultAbiCoder().encode( + ['address', 'uint256', 'bytes'], + [this.target.target, 0, this.target.interface.encodeFunctionData('mockFunctionExtra')], + ), + ]), + }) + .then(op => op.sign(this.domain, this.signer)); + + const prevAccountBalance = await ethers.provider.getBalance(this.smartAccount.target); + const prevEntrypointBalance = await ethers.provider.getBalance(this.entrypoint.target); + const amount = ethers.parseEther('0.1'); + + const tx = await this.smartAccount + .connect(this.entrypointAsSigner) + .validateUserOp(operation.packed, operation.hash, amount); + + const receipt = await tx.wait(); + const callerFees = receipt.gasUsed * tx.gasPrice; + + expect(await ethers.provider.getBalance(this.smartAccount.target)).to.equal(prevAccountBalance - amount); + expect(await ethers.provider.getBalance(this.entrypoint.target)).to.equal( + prevEntrypointBalance + amount - callerFees, + ); + }); + }); + }); + + describe('fallback', function () { + it('should receive ether', async function () { + await this.smartAccount.deploy(); + await setBalance(this.other.address, ethers.parseEther('1')); + + const prevBalance = await ethers.provider.getBalance(this.smartAccount.target); + const amount = ethers.parseEther('0.1'); + await this.other.sendTransaction({ to: this.smartAccount.target, value: amount }); + + expect(await ethers.provider.getBalance(this.smartAccount.target)).to.equal(prevBalance + amount); + }); + }); +} + +function shouldBehaveLikeAccountHolder() { + describe('onReceived', function () { + beforeEach(async function () { + await this.smartAccount.deploy(); + }); + + shouldSupportInterfaces(['ERC1155Receiver']); + + describe('onERC1155Received', function () { + const ids = [1n, 2n, 3n]; + const values = [1000n, 2000n, 3000n]; + const data = '0x12345678'; + + beforeEach(async function () { + [this.owner] = await ethers.getSigners(); + this.token = await ethers.deployContract('$ERC1155', ['https://somedomain.com/{id}.json']); + await this.token.$_mintBatch(this.owner, ids, values, '0x'); + }); + + it('receives ERC1155 tokens from a single ID', async function () { + await this.token.connect(this.owner).safeTransferFrom(this.owner, this.smartAccount, ids[0], values[0], data); + expect(await this.token.balanceOf(this.smartAccount, ids[0])).to.equal(values[0]); + for (let i = 1; i < ids.length; i++) { + expect(await this.token.balanceOf(this.smartAccount, ids[i])).to.equal(0n); + } + }); + + it('receives ERC1155 tokens from a multiple IDs', async function () { + expect( + await this.token.balanceOfBatch( + ids.map(() => this.smartAccount), + ids, + ), + ).to.deep.equal(ids.map(() => 0n)); + await this.token.connect(this.owner).safeBatchTransferFrom(this.owner, this.smartAccount, ids, values, data); + expect( + await this.token.balanceOfBatch( + ids.map(() => this.smartAccount), + ids, + ), + ).to.deep.equal(values); + }); + }); + + describe('onERC721Received', function () { + it('receives an ERC721 token', async function () { + const name = 'Some NFT'; + const symbol = 'SNFT'; + const tokenId = 1n; + + const [owner] = await ethers.getSigners(); + + const token = await ethers.deployContract('$ERC721', [name, symbol]); + await token.$_mint(owner, tokenId); + + await token.connect(owner).safeTransferFrom(owner, this.smartAccount, tokenId); + + expect(await token.ownerOf(tokenId)).to.equal(this.smartAccount.target); + }); + }); + }); +} + +function shouldBehaveLikeAnAccountBaseExecutor({ deployable = true } = {}) { + describe('executeUserOp', function () { + beforeEach(async function () { + await setBalance(this.smartAccount.target, ethers.parseEther('1')); + expect(await ethers.provider.getCode(this.smartAccount.target)).to.equal('0x'); + this.entrypointAsSigner = await impersonate(this.entrypoint.target); + }); + + it('should revert if the caller is not the canonical entrypoint or the account itself', async function () { + await this.smartAccount.deploy(); + + const selector = this.smartAccount.interface.getFunction('executeUserOp').selector; + const operation = await this.smartAccount + .createOp({ + callData: ethers.concat([ + selector, + ethers.AbiCoder.defaultAbiCoder().encode( + ['address', 'uint256', 'bytes'], + [this.target.target, 0, this.target.interface.encodeFunctionData('mockFunctionExtra')], + ), + ]), + }) + .then(op => op.sign(this.domain, this.signer)); + + await expect(this.smartAccount.connect(this.other).executeUserOp(operation.packed, operation.hash)) + .to.be.revertedWithCustomError(this.smartAccount, 'AccountUnauthorized') + .withArgs(this.other); + }); + + if (deployable) { + describe('when not deployed', function () { + it('should be created with handleOps and increase nonce', async function () { + const selector = this.smartAccount.interface.getFunction('executeUserOp').selector; + const operation = await this.smartAccount + .createOp({ + callData: ethers.concat([ + selector, + ethers.AbiCoder.defaultAbiCoder().encode( + ['address', 'uint256', 'bytes'], + [this.target.target, 17, this.target.interface.encodeFunctionData('mockFunctionExtra')], + ), + ]), + }) + .then(op => op.addInitCode()) + .then(op => op.sign(this.domain, this.signer)); + + await expect(this.entrypoint.connect(this.entrypointAsSigner).handleOps([operation.packed], this.beneficiary)) + .to.emit(this.entrypoint, 'AccountDeployed') + .withArgs(operation.hash, this.smartAccount, this.factory, ethers.ZeroAddress) + .to.emit(this.target, 'MockFunctionCalledExtra') + .withArgs(this.smartAccount, 17); + expect(await this.smartAccount.getNonce()).to.equal(1); + }); + + it('should revert if the signature is invalid', async function () { + const selector = this.smartAccount.interface.getFunction('executeUserOp').selector; + const operation = await this.smartAccount + .createOp({ + callData: ethers.concat([ + selector, + ethers.AbiCoder.defaultAbiCoder().encode( + ['address', 'uint256', 'bytes'], + [this.target.target, 17, this.target.interface.encodeFunctionData('mockFunctionExtra')], + ), + ]), + }) + .then(op => op.addInitCode()); + + operation.signature = '0x00'; + + await expect(this.entrypoint.connect(this.entrypointAsSigner).handleOps([operation.packed], this.beneficiary)) + .to.be.reverted; + }); + }); + } + + describe('when deployed', function () { + beforeEach(async function () { + await this.smartAccount.deploy(); + }); + + it('should increase nonce and call target', async function () { + const selector = this.smartAccount.interface.getFunction('executeUserOp').selector; + const operation = await this.smartAccount + .createOp({ + callData: ethers.concat([ + selector, + ethers.AbiCoder.defaultAbiCoder().encode( + ['address', 'uint256', 'bytes'], + [this.target.target, 42, this.target.interface.encodeFunctionData('mockFunctionExtra')], + ), + ]), + }) + .then(op => op.sign(this.domain, this.signer)); + + expect(await this.smartAccount.getNonce()).to.equal(0); + await expect(this.entrypoint.connect(this.entrypointAsSigner).handleOps([operation.packed], this.beneficiary)) + .to.emit(this.target, 'MockFunctionCalledExtra') + .withArgs(this.smartAccount, 42); + expect(await this.smartAccount.getNonce()).to.equal(1); + }); + }); + }); +} + +module.exports = { + shouldBehaveLikeAnAccountBase, + shouldBehaveLikeAccountHolder, + shouldBehaveLikeAnAccountBaseExecutor, +}; diff --git a/test/account/draft-AccountBase.test.js b/test/account/draft-AccountBase.test.js new file mode 100644 index 00000000..3099417e --- /dev/null +++ b/test/account/draft-AccountBase.test.js @@ -0,0 +1,24 @@ +const { ethers } = require('hardhat'); +const { shouldBehaveLikeAnAccountBase, shouldBehaveLikeAnAccountBaseExecutor } = require('./Account.behavior'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { ERC4337Helper } = require('../helpers/erc4337'); +const { BooleanSigner } = require('../helpers/signers'); + +async function fixture() { + const [beneficiary, other] = await ethers.getSigners(); + const target = await ethers.deployContract('CallReceiverMock'); + const signer = new BooleanSigner(); + const helper = new ERC4337Helper('$AccountBaseMock'); + const smartAccount = await helper.newAccount(); + + return { ...helper, smartAccount, signer, target, beneficiary, other }; +} + +describe('AccountBase', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + shouldBehaveLikeAnAccountBase(); + shouldBehaveLikeAnAccountBaseExecutor(); +}); diff --git a/test/account/draft-AccountECDSA.js b/test/account/draft-AccountECDSA.js new file mode 100644 index 00000000..06bc9263 --- /dev/null +++ b/test/account/draft-AccountECDSA.js @@ -0,0 +1,44 @@ +const { ethers } = require('hardhat'); +const { + shouldBehaveLikeAnAccountBase, + shouldBehaveLikeAnAccountBaseExecutor, + shouldBehaveLikeAccountHolder, +} = require('./Account.behavior'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { ERC4337Helper } = require('../helpers/erc4337'); +const { ECDSASigner } = require('../helpers/signers'); +const { shouldBehaveLikeERC7739Signer } = require('../utils/cryptography/ERC7739Signer.behavior'); + +async function fixture() { + const [beneficiary, other] = await ethers.getSigners(); + const target = await ethers.deployContract('CallReceiverMock'); + const signer = new ECDSASigner(); + const helper = new ERC4337Helper('$AccountECDSA'); + const smartAccount = await helper.newAccount(['AccountECDSA', '1', signer.EOA.address]); + const domain = { + name: 'AccountECDSA', + version: '1', + chainId: helper.chainId, + verifyingContract: smartAccount.address, + }; + + return { ...helper, domain, smartAccount, signer, target, beneficiary, other }; +} + +describe('AccountECDSA', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + shouldBehaveLikeAnAccountBase(); + shouldBehaveLikeAnAccountBaseExecutor(); + shouldBehaveLikeAccountHolder(); + + describe('ERC7739Signer', function () { + beforeEach(async function () { + this.mock = await this.smartAccount.deploy(); + }); + + shouldBehaveLikeERC7739Signer(); + }); +}); diff --git a/test/account/draft-AccountP256.test.js b/test/account/draft-AccountP256.test.js new file mode 100644 index 00000000..b3d45289 --- /dev/null +++ b/test/account/draft-AccountP256.test.js @@ -0,0 +1,44 @@ +const { ethers } = require('hardhat'); +const { + shouldBehaveLikeAnAccountBase, + shouldBehaveLikeAnAccountBaseExecutor, + shouldBehaveLikeAccountHolder, +} = require('./Account.behavior'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { ERC4337Helper } = require('../helpers/erc4337'); +const { P256Signer } = require('../helpers/signers'); +const { shouldBehaveLikeERC7739Signer } = require('../utils/cryptography/ERC7739Signer.behavior'); + +async function fixture() { + const [beneficiary, other] = await ethers.getSigners(); + const target = await ethers.deployContract('CallReceiverMock'); + const signer = new P256Signer(); + const helper = new ERC4337Helper('$AccountP256'); + const smartAccount = await helper.newAccount(['AccountP256', '1', signer.publicKey.qx, signer.publicKey.qy]); + const domain = { + name: 'AccountP256', + version: '1', + chainId: helper.chainId, + verifyingContract: smartAccount.address, + }; + + return { ...helper, domain, smartAccount, signer, target, beneficiary, other }; +} + +describe('AccountP256', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + shouldBehaveLikeAnAccountBase(); + shouldBehaveLikeAnAccountBaseExecutor(); + shouldBehaveLikeAccountHolder(); + + describe('ERC7739Signer', function () { + beforeEach(async function () { + this.mock = await this.smartAccount.deploy(); + }); + + shouldBehaveLikeERC7739Signer(); + }); +}); diff --git a/test/account/draft-AccountRSA.test.js b/test/account/draft-AccountRSA.test.js new file mode 100644 index 00000000..66813a07 --- /dev/null +++ b/test/account/draft-AccountRSA.test.js @@ -0,0 +1,44 @@ +const { ethers } = require('hardhat'); +const { + shouldBehaveLikeAnAccountBase, + shouldBehaveLikeAnAccountBaseExecutor, + shouldBehaveLikeAccountHolder, +} = require('./Account.behavior'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { ERC4337Helper } = require('../helpers/erc4337'); +const { RSASigner } = require('../helpers/signers'); +const { shouldBehaveLikeERC7739Signer } = require('../utils/cryptography/ERC7739Signer.behavior'); + +async function fixture() { + const [beneficiary, other] = await ethers.getSigners(); + const target = await ethers.deployContract('CallReceiverMock'); + const signer = new RSASigner(); + const helper = new ERC4337Helper('$AccountRSA'); + const smartAccount = await helper.newAccount(['AccountRSA', '1', signer.publicKey.e, signer.publicKey.n]); + const domain = { + name: 'AccountRSA', + version: '1', + chainId: helper.chainId, + verifyingContract: smartAccount.address, + }; + + return { ...helper, domain, smartAccount, signer, target, beneficiary, other }; +} + +describe('AccountRSA', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + shouldBehaveLikeAnAccountBase(); + shouldBehaveLikeAnAccountBaseExecutor(); + shouldBehaveLikeAccountHolder(); + + describe('ERC7739Signer', function () { + beforeEach(async function () { + this.mock = await this.smartAccount.deploy(); + }); + + shouldBehaveLikeERC7739Signer(); + }); +}); diff --git a/test/helpers/erc4337.js b/test/helpers/erc4337.js new file mode 100644 index 00000000..1893ce31 --- /dev/null +++ b/test/helpers/erc4337.js @@ -0,0 +1,43 @@ +const { setCode } = require('@nomicfoundation/hardhat-network-helpers'); +const { ethers } = require('hardhat'); + +/// Global ERC-4337 environment helper. +class ERC4337Helper { + constructor(account, params = {}) { + this.entrypointAsPromise = ethers.deployContract('EntryPoint'); + this.factoryAsPromise = ethers.deployContract('$Create2'); + this.accountContractAsPromise = ethers.getContractFactory(account); + this.chainIdAsPromise = ethers.provider.getNetwork().then(({ chainId }) => chainId); + this.senderCreatorAsPromise = ethers.deployContract('SenderCreator'); + this.params = params; + } + + async wait() { + const entrypoint = await this.entrypointAsPromise; + await entrypoint.getDeployedCode().then(code => setCode(CANONICAL_ENTRYPOINT, code)); + this.entrypoint = entrypoint.attach(CANONICAL_ENTRYPOINT); + this.entrypointAsPromise = Promise.resolve(this.entrypoint); + + this.factory = await this.factoryAsPromise; + this.accountContract = await this.accountContractAsPromise; + this.chainId = await this.chainIdAsPromise; + this.senderCreator = await this.senderCreatorAsPromise; + return this; + } + + async newAccount(extraArgs = [], salt = ethers.randomBytes(32)) { + await this.wait(); + const initCode = await this.accountContract + .getDeployTransaction(...extraArgs) + .then(tx => this.factory.interface.encodeFunctionData('$deploy', [0, salt, tx.data])) + .then(deployCode => ethers.concat([this.factory.target, deployCode])); + const instance = await this.senderCreator.createSender + .staticCall(initCode) + .then(address => this.accountContract.attach(address)); + return new SmartAccount(instance, initCode, this); + } +} + +module.exports = { + ERC4337Helper, +}; diff --git a/test/helpers/signers.js b/test/helpers/signers.js new file mode 100644 index 00000000..61b3ee36 --- /dev/null +++ b/test/helpers/signers.js @@ -0,0 +1,131 @@ +const { ethers } = require('hardhat'); +const { secp256k1 } = require('@noble/curves/secp256k1'); +const { secp256r1 } = require('@noble/curves/p256'); +const { generateKeyPairSync, privateEncrypt } = require('crypto'); + +const ensureLowerOrderS = (N, { s, recovery, ...rest }) => { + if (s > N / 2n) { + s = N - s; + recovery = 1 - recovery; + } + return { s, recovery, ...rest }; +}; + +class BooleanSigner { + signPersonal() { + return '0x01'; + } + + signNestedTypedData() { + return '0x01'; + } +} + +class ERC7739Signer { + signPersonal(domain, contents) { + return this._signRaw( + hashTypedData( + domain, + ethers.solidityPackedKeccak256( + ['bytes32', 'bytes32'], + [ + ethers.solidityPackedKeccak256(['string'], ['PersonalSign(bytes prefixed)']), + ethers.solidityPackedKeccak256(['string', 'bytes32'], ['\x19Ethereum Signed Message:\n32', contents]), + ], + ), + ), + ); + } + + signNestedTypedData(localDomain, appDomain, contents, contentsType) { + return this._signRaw(hashTypedData(appDomain, hashNestedTypedDataStruct(localDomain, contents, contentsType))); + } + + wrapTypedDataSig(originalSig, appSeparator, contents, contentsType) { + const contentsTypeLength = ethers.toBeHex(ethers.dataLength(contentsType), 2); + return ethers.concat([originalSig, appSeparator, contents, contentsType, contentsTypeLength]); + } +} + +class ECDSASigner extends ERC7739Signer { + N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n; + + constructor() { + super(); + this._privateKey = secp256k1.utils.randomPrivateKey(); + this.publicKey = secp256k1.getPublicKey(this._privateKey, false); + } + + _signRaw(messageHash) { + const sig = this._ensureLowerOrderS(secp256k1.sign(messageHash.replace(/0x/, ''), this._privateKey)); + return ethers.Signature.from({ + r: sig.r, + v: sig.recovery + 27, + s: sig.s, + }).serialized; + } + + get EOA() { + return new ethers.Wallet(ethers.hexlify(this._privateKey)); + } + + _ensureLowerOrderS({ s, recovery, ...rest }) { + return ensureLowerOrderS(this.N, { s, recovery, ...rest }); + } +} + +class P256Signer extends ERC7739Signer { + N = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551n; + + constructor() { + super(); + this._privateKey = secp256r1.utils.randomPrivateKey(); + const [qx, qy] = [ + secp256r1.getPublicKey(this._privateKey, false).slice(0x01, 0x21), + secp256r1.getPublicKey(this._privateKey, false).slice(0x21, 0x41), + ].map(ethers.hexlify); + this.publicKey = { + qx, + qy, + }; + } + + _signRaw(messageHash) { + const sig = this._ensureLowerOrderS(secp256r1.sign(messageHash.replace(/0x/, ''), this._privateKey)); + return ethers.Signature.from({ + r: sig.r, + v: sig.recovery + 27, + s: sig.s, + }).serialized; + } + + _ensureLowerOrderS({ s, recovery, ...rest }) { + return ensureLowerOrderS(this.N, { s, recovery, ...rest }); + } +} + +class RSASigner extends ERC7739Signer { + constructor() { + super(); + const keyPair = generateKeyPairSync('rsa', { + modulusLength: 2048, + }); + const jwk = keyPair.publicKey.export({ format: 'jwk' }); + const [e, n] = [jwk.e, jwk.n].map(ethers.decodeBase64); + this._privateKey = keyPair.privateKey; + this.publicKey = { e, n }; + } + + _signRaw(messageHash) { + // SHA256 OID = 608648016503040201 (9 bytes) | NULL = 0500 (2 bytes) (explicit) | OCTET_STRING length (0x20) = 0420 (2 bytes) + const dataToSign = ethers.concat(['0x3031300d060960864801650304020105000420', messageHash]); + return '0x' + privateEncrypt(this._privateKey, ethers.getBytes(dataToSign)).toString('hex'); + } +} + +module.exports = { + BooleanSigner, + ECDSASigner, + P256Signer, + RSASigner, +}; diff --git a/test/utils/cryptography/ERC7739Signer.behavior.js b/test/utils/cryptography/ERC7739Signer.behavior.js new file mode 100644 index 00000000..a211b0bc --- /dev/null +++ b/test/utils/cryptography/ERC7739Signer.behavior.js @@ -0,0 +1,101 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { Permit, formatType, getDomain } = require('../../../lib/@openzeppelin-contracts/test/helpers/eip712'); +const { PersonalSignHelper, TypedDataSignHelper } = require('../../helpers/erc7739'); + +function shouldBehaveLikeERC7739Signer() { + const MAGIC_VALUE = '0x1626ba7e'; + + describe('isValidSignature', function () { + describe('PersonalSign', function () { + it('returns true for a valid personal signature', async function () { + const text = 'Hello, world!'; + + const hash = PersonalSignHelper.hash(text); + const signature = await PersonalSignHelper.sign(this.signTypedData, text, this.domain); + + expect(this.mock.isValidSignature(hash, signature)).to.eventually.equal(MAGIC_VALUE); + }); + + it('returns false for an invalid personal signature', async function () { + const hash = PersonalSignHelper.hash('Message the app expects'); + const signature = await PersonalSignHelper.sign(this.signTypedData, 'Message signed is different', this.domain); + + expect(this.mock.isValidSignature(hash, signature)).to.eventually.not.equal(MAGIC_VALUE); + }); + }); + + describe('TypedDataSign', function () { + beforeEach(async function () { + // Dummy app domain, different from the ERC7739Signer's domain + // Note the difference of format (signer domain doesn't include a salt, but app domain does) + this.appDomain = { + name: 'SomeApp', + version: '1', + chainId: this.domain.chainId, + verifyingContract: '0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512', + salt: '0x02cb3d8cb5e8928c9c6de41e935e16a4e28b2d54e7e7ba47e99f16071efab785', + }; + }); + + it('returns true for a valid typed data signature', async function () { + const contents = { + owner: '0x1ab5E417d9AF00f1ca9d159007e12c401337a4bb', + spender: '0xD68E96620804446c4B1faB3103A08C98d4A8F55f', + value: 1_000_000n, + nonce: 0n, + deadline: ethers.MaxUint256, + }; + const message = TypedDataSignHelper.prepare(contents, this.domain); + + const hash = ethers.TypedDataEncoder.hash(this.appDomain, { Permit }, message.contents); + const signature = await TypedDataSignHelper.sign(this.signTypedData, this.appDomain, { Permit }, message); + + expect(this.mock.isValidSignature(hash, signature)).to.eventually.equal(MAGIC_VALUE); + }); + + it('returns true for valid typed data signature (nested types)', async function () { + const contentsTypes = { + B: formatType({ z: 'Z' }), + Z: formatType({ a: 'A' }), + A: formatType({ v: 'uint256' }), + }; + + const contents = { z: { a: { v: 1n } } }; + const message = TypedDataSignHelper.prepare(contents, this.domain); + + const hash = TypedDataSignHelper.hash(this.appDomain, contentsTypes, message.contents); + const signature = await TypedDataSignHelper.sign(this.signTypedData, this.appDomain, contentsTypes, message); + + expect(this.mock.isValidSignature(hash, signature)).to.eventually.equal(MAGIC_VALUE); + }); + + it('returns false for an invalid typed data signature', async function () { + const appContents = { + owner: '0x1ab5E417d9AF00f1ca9d159007e12c401337a4bb', + spender: '0xD68E96620804446c4B1faB3103A08C98d4A8F55f', + value: 1_000_000n, + nonce: 0n, + deadline: ethers.MaxUint256, + }; + // message signed by the user is for a lower amount. + const message = TypedDataSignHelper.prepare({ ...appContents, value: 1_000n }, this.domain); + + const hash = ethers.TypedDataEncoder.hash(this.appDomain, { Permit }, appContents); + const signature = await TypedDataSignHelper.sign(this.signTypedData, this.appDomain, { Permit }, message); + + expect(this.mock.isValidSignature(hash, signature)).to.eventually.not.equal(MAGIC_VALUE); + }); + }); + + it('support detection', function () { + expect( + this.mock.isValidSignature('0x7739773977397739773977397739773977397739773977397739773977397739', ''), + ).to.eventually.equal('0x77390001'); + }); + }); +} + +module.exports = { + shouldBehaveLikeERC7739Signer, +}; diff --git a/test/utils/cryptography/draft-ERC7739Signer.test.js b/test/utils/cryptography/draft-ERC7739Signer.test.js index 4a2e1c8f..eb82e936 100644 --- a/test/utils/cryptography/draft-ERC7739Signer.test.js +++ b/test/utils/cryptography/draft-ERC7739Signer.test.js @@ -1,23 +1,25 @@ -const { ethers } = require('hardhat'); -const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { ethers } = require('hardhat'); +const { shouldBehaveLikeERC7739Signer } = require('./ERC7739Signer.behavior'); +const { ECDSASigner, P256Signer, RSASigner } = require('../../helpers/signers'); -const { Permit, formatType, getDomain } = require('../../../lib/@openzeppelin-contracts/test/helpers/eip712'); -const { PersonalSignHelper, TypedDataSignHelper } = require('../../helpers/erc7739'); +async function fixture() { + const ECDSA = new ECDSASigner(); + const ECDSAMock = await ethers.deployContract('ERC7739SignerECDSA', [ECDSA.EOA.address]); -// Constant -const MAGIC_VALUE = '0x1626ba7e'; + const P256 = new P256Signer(); + const P256Mock = await ethers.deployContract('ERC7739SignerP256', [P256.publicKey.qx, P256.publicKey.qy]); -// Fixture -async function fixture() { - const [signer] = await ethers.getSigners(); - const mock = await ethers.deployContract('$ERC7739SignerMock', [signer]); - const domain = await getDomain(mock); + const RSA = new RSASigner(); + const RSAMock = await ethers.deployContract('ERC7739SignerRSA', [RSA.publicKey.e, RSA.publicKey.n]); return { - mock, - domain, - signTypedData: signer.signTypedData.bind(signer), + ECDSA, + ECDSAMock, + P256, + P256Mock, + RSA, + RSAMock, }; } @@ -26,92 +28,30 @@ describe('ERC7739Signer', function () { Object.assign(this, await loadFixture(fixture)); }); - describe('isValidSignature', function () { - describe('PersonalSign', function () { - it('returns true for a valid personal signature', async function () { - const text = 'Hello, world!'; - - const hash = PersonalSignHelper.hash(text); - const signature = await PersonalSignHelper.sign(this.signTypedData, text, this.domain); - - expect(this.mock.isValidSignature(hash, signature)).to.eventually.equal(MAGIC_VALUE); - }); - - it('returns false for an invalid personal signature', async function () { - const hash = PersonalSignHelper.hash('Message the app expects'); - const signature = await PersonalSignHelper.sign(this.signTypedData, 'Message signed is different', this.domain); - - expect(this.mock.isValidSignature(hash, signature)).to.eventually.not.equal(MAGIC_VALUE); - }); + describe('for an ECDSA signer', function () { + beforeEach(function () { + this.signer = this.ECDSA; + this.mock = this.ECDSAMock; }); - describe('TypedDataSign', function () { - beforeEach(async function () { - // Dummy app domain, different from the ERC7739Signer's domain - // Note the difference of format (signer domain doesn't include a salt, but app domain does) - this.appDomain = { - name: 'SomeApp', - version: '1', - chainId: this.domain.chainId, - verifyingContract: '0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512', - salt: '0x02cb3d8cb5e8928c9c6de41e935e16a4e28b2d54e7e7ba47e99f16071efab785', - }; - }); - - it('returns true for a valid typed data signature', async function () { - const contents = { - owner: '0x1ab5E417d9AF00f1ca9d159007e12c401337a4bb', - spender: '0xD68E96620804446c4B1faB3103A08C98d4A8F55f', - value: 1_000_000n, - nonce: 0n, - deadline: ethers.MaxUint256, - }; - const message = TypedDataSignHelper.prepare(contents, this.domain); - - const hash = ethers.TypedDataEncoder.hash(this.appDomain, { Permit }, message.contents); - const signature = await TypedDataSignHelper.sign(this.signTypedData, this.appDomain, { Permit }, message); - - expect(this.mock.isValidSignature(hash, signature)).to.eventually.equal(MAGIC_VALUE); - }); - - it('returns true for valid typed data signature (nested types)', async function () { - const contentsTypes = { - B: formatType({ z: 'Z' }), - Z: formatType({ a: 'A' }), - A: formatType({ v: 'uint256' }), - }; - - const contents = { z: { a: { v: 1n } } }; - const message = TypedDataSignHelper.prepare(contents, this.domain); - - const hash = TypedDataSignHelper.hash(this.appDomain, contentsTypes, message.contents); - const signature = await TypedDataSignHelper.sign(this.signTypedData, this.appDomain, contentsTypes, message); - - expect(this.mock.isValidSignature(hash, signature)).to.eventually.equal(MAGIC_VALUE); - }); + shouldBehaveLikeERC7739Signer(); + }); - it('returns false for an invalid typed data signature', async function () { - const appContents = { - owner: '0x1ab5E417d9AF00f1ca9d159007e12c401337a4bb', - spender: '0xD68E96620804446c4B1faB3103A08C98d4A8F55f', - value: 1_000_000n, - nonce: 0n, - deadline: ethers.MaxUint256, - }; - // message signed by the user is for a lower amount. - const message = TypedDataSignHelper.prepare({ ...appContents, value: 1_000n }, this.domain); + describe('for a P256 signer', function () { + beforeEach(function () { + this.signer = this.P256; + this.mock = this.P256Mock; + }); - const hash = ethers.TypedDataEncoder.hash(this.appDomain, { Permit }, appContents); - const signature = await TypedDataSignHelper.sign(this.signTypedData, this.appDomain, { Permit }, message); + shouldBehaveLikeERC7739Signer(); + }); - expect(this.mock.isValidSignature(hash, signature)).to.eventually.not.equal(MAGIC_VALUE); - }); + describe('for an RSA signer', function () { + beforeEach(function () { + this.signer = this.RSA; + this.mock = this.RSAMock; }); - it('support detection', function () { - expect( - this.mock.isValidSignature('0x7739773977397739773977397739773977397739773977397739773977397739', ''), - ).to.eventually.equal('0x77390001'); - }); + shouldBehaveLikeERC7739Signer(); }); }); From 25de3ac24a7af129f7afcfbf09a401aa1b307c93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Wed, 4 Dec 2024 21:43:31 -0600 Subject: [PATCH 02/51] Checkpoint --- contracts/account/README.adoc | 15 + contracts/mocks/Create2Mock.sol | 19 + contracts/mocks/ERC721Mock.sol | 13 + .../utils/cryptography/ERC7739SignerECDSA.sol | 21 - .../cryptography/ERC7739SignerECDSAMock.sol} | 4 +- .../cryptography/ERC7739SignerP256Mock.sol} | 4 +- .../cryptography/ERC7739SignerRSAMock.sol} | 4 +- contracts/vendor/erc4337-entrypoint/README.md | 1 + .../erc4337-entrypoint/core/Entrypoint.sol | 725 ++++++++++++++++++ .../erc4337-entrypoint/core/Helpers.sol | 106 +++ .../erc4337-entrypoint/core/NonceManager.sol | 43 ++ .../erc4337-entrypoint/core/SenderCreator.sol | 38 + .../erc4337-entrypoint/core/StakeManager.sol | 145 ++++ .../core/UserOperationLib.sol | 139 ++++ .../interfaces/IAccount.sol | 39 + .../interfaces/IAccountExecute.sol | 20 + .../interfaces/IAggregator.sol | 44 ++ .../interfaces/IEntryPoint.sol | 223 ++++++ .../interfaces/INonceManager.sol | 27 + .../interfaces/IPaymaster.sol | 63 ++ .../interfaces/IStakeManager.sol | 111 +++ .../interfaces/PackedUserOperation.sol | 28 + .../vendor/erc4337-entrypoint/utils/Exec.sol | 56 ++ test/account/Account.behavior.js | 51 +- ...untECDSA.js => draft-AccountECDSA.test.js} | 24 +- test/helpers/erc4337.js | 66 +- test/helpers/signers.js | 33 +- .../cryptography/ERC7739Signer.behavior.js | 2 +- .../cryptography/draft-ERC7739Signer.test.js | 19 +- 29 files changed, 2003 insertions(+), 80 deletions(-) create mode 100644 contracts/account/README.adoc create mode 100644 contracts/mocks/Create2Mock.sol create mode 100644 contracts/mocks/ERC721Mock.sol delete mode 100644 contracts/mocks/docs/utils/cryptography/ERC7739SignerECDSA.sol rename contracts/mocks/{ERC7739SignerECDSA.sol => docs/utils/cryptography/ERC7739SignerECDSAMock.sol} (83%) rename contracts/mocks/{ERC7739SignerP256.sol => docs/utils/cryptography/ERC7739SignerP256Mock.sol} (84%) rename contracts/mocks/{ERC7739SignerRSA.sol => docs/utils/cryptography/ERC7739SignerRSAMock.sol} (81%) create mode 100644 contracts/vendor/erc4337-entrypoint/README.md create mode 100644 contracts/vendor/erc4337-entrypoint/core/Entrypoint.sol create mode 100644 contracts/vendor/erc4337-entrypoint/core/Helpers.sol create mode 100644 contracts/vendor/erc4337-entrypoint/core/NonceManager.sol create mode 100644 contracts/vendor/erc4337-entrypoint/core/SenderCreator.sol create mode 100644 contracts/vendor/erc4337-entrypoint/core/StakeManager.sol create mode 100644 contracts/vendor/erc4337-entrypoint/core/UserOperationLib.sol create mode 100644 contracts/vendor/erc4337-entrypoint/interfaces/IAccount.sol create mode 100644 contracts/vendor/erc4337-entrypoint/interfaces/IAccountExecute.sol create mode 100644 contracts/vendor/erc4337-entrypoint/interfaces/IAggregator.sol create mode 100644 contracts/vendor/erc4337-entrypoint/interfaces/IEntryPoint.sol create mode 100644 contracts/vendor/erc4337-entrypoint/interfaces/INonceManager.sol create mode 100644 contracts/vendor/erc4337-entrypoint/interfaces/IPaymaster.sol create mode 100644 contracts/vendor/erc4337-entrypoint/interfaces/IStakeManager.sol create mode 100644 contracts/vendor/erc4337-entrypoint/interfaces/PackedUserOperation.sol create mode 100644 contracts/vendor/erc4337-entrypoint/utils/Exec.sol rename test/account/{draft-AccountECDSA.js => draft-AccountECDSA.test.js} (77%) diff --git a/contracts/account/README.adoc b/contracts/account/README.adoc new file mode 100644 index 00000000..a1188bf4 --- /dev/null +++ b/contracts/account/README.adoc @@ -0,0 +1,15 @@ += Account +[.readme-notice] +NOTE: This document is better viewed at https://docs.openzeppelin.com/community-contracts/api/account + +This directory includes contracts to build accounts for ERC-4337. + +== Core + +{{AccountBase}} + +{{AccountECDSA}} + +{{AccountP256}} + +{{AccountRSA}} diff --git a/contracts/mocks/Create2Mock.sol b/contracts/mocks/Create2Mock.sol new file mode 100644 index 00000000..81cf5335 --- /dev/null +++ b/contracts/mocks/Create2Mock.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; + +contract Create2Mock { + function $deploy(uint256 amount, bytes32 salt, bytes memory bytecode) external returns (address) { + return Create2.deploy(amount, salt, bytecode); + } + + function $computeAddress(bytes32 salt, bytes32 bytecodeHash) external view returns (address) { + return Create2.computeAddress(salt, bytecodeHash, address(this)); + } + + function $computeAddress(bytes32 salt, bytes32 bytecodeHash, address deployer) external pure returns (address) { + return Create2.computeAddress(salt, bytecodeHash, deployer); + } +} diff --git a/contracts/mocks/ERC721Mock.sol b/contracts/mocks/ERC721Mock.sol new file mode 100644 index 00000000..31e16bf6 --- /dev/null +++ b/contracts/mocks/ERC721Mock.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + +contract ERC721Mock is ERC721 { + constructor(string memory _name, string memory _symbol) ERC721(_name, _symbol) {} + + function $mint(address owner, uint256 tokenId) external virtual { + _mint(owner, tokenId); + } +} diff --git a/contracts/mocks/docs/utils/cryptography/ERC7739SignerECDSA.sol b/contracts/mocks/docs/utils/cryptography/ERC7739SignerECDSA.sol deleted file mode 100644 index fca13d35..00000000 --- a/contracts/mocks/docs/utils/cryptography/ERC7739SignerECDSA.sol +++ /dev/null @@ -1,21 +0,0 @@ -// contracts/ERC7739SignerECDSA.sol -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; - -import {ERC7739Signer} from "../../../../utils/cryptography/draft-ERC7739Signer.sol"; - -contract ERC7739SignerECDSA is ERC7739Signer { - address private immutable _signer; - - constructor(address signerAddr) EIP712("ERC7739SignerECDSA", "1") { - _signer = signerAddr; - } - - function _validateSignature(bytes32 hash, bytes calldata signature) internal view virtual override returns (bool) { - (address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(hash, signature); - return _signer == recovered && err == ECDSA.RecoverError.NoError; - } -} diff --git a/contracts/mocks/ERC7739SignerECDSA.sol b/contracts/mocks/docs/utils/cryptography/ERC7739SignerECDSAMock.sol similarity index 83% rename from contracts/mocks/ERC7739SignerECDSA.sol rename to contracts/mocks/docs/utils/cryptography/ERC7739SignerECDSAMock.sol index c6c81b4f..7e30e240 100644 --- a/contracts/mocks/ERC7739SignerECDSA.sol +++ b/contracts/mocks/docs/utils/cryptography/ERC7739SignerECDSAMock.sol @@ -4,9 +4,9 @@ pragma solidity ^0.8.20; import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; -import {ERC7739Signer} from "../utils/cryptography/draft-ERC7739Signer.sol"; +import {ERC7739Signer} from "../../../../utils/cryptography/draft-ERC7739Signer.sol"; -contract ERC7739SignerECDSA is ERC7739Signer { +contract ERC7739SignerECDSAMock is ERC7739Signer { address private immutable _signer; constructor(address signerAddr) EIP712("ERC7739SignerECDSA", "1") { diff --git a/contracts/mocks/ERC7739SignerP256.sol b/contracts/mocks/docs/utils/cryptography/ERC7739SignerP256Mock.sol similarity index 84% rename from contracts/mocks/ERC7739SignerP256.sol rename to contracts/mocks/docs/utils/cryptography/ERC7739SignerP256Mock.sol index 18c1daa3..b2ff2c56 100644 --- a/contracts/mocks/ERC7739SignerP256.sol +++ b/contracts/mocks/docs/utils/cryptography/ERC7739SignerP256Mock.sol @@ -4,9 +4,9 @@ pragma solidity ^0.8.20; import {P256} from "@openzeppelin/contracts/utils/cryptography/P256.sol"; import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; -import {ERC7739Signer} from "../utils/cryptography/draft-ERC7739Signer.sol"; +import {ERC7739Signer} from "../../../../utils/cryptography/draft-ERC7739Signer.sol"; -contract ERC7739SignerP256 is ERC7739Signer { +contract ERC7739SignerP256Mock is ERC7739Signer { bytes32 private immutable _qx; bytes32 private immutable _qy; diff --git a/contracts/mocks/ERC7739SignerRSA.sol b/contracts/mocks/docs/utils/cryptography/ERC7739SignerRSAMock.sol similarity index 81% rename from contracts/mocks/ERC7739SignerRSA.sol rename to contracts/mocks/docs/utils/cryptography/ERC7739SignerRSAMock.sol index 9abad594..a242892e 100644 --- a/contracts/mocks/ERC7739SignerRSA.sol +++ b/contracts/mocks/docs/utils/cryptography/ERC7739SignerRSAMock.sol @@ -4,9 +4,9 @@ pragma solidity ^0.8.20; import {RSA} from "@openzeppelin/contracts/utils/cryptography/RSA.sol"; import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; -import {ERC7739Signer} from "../utils/cryptography/draft-ERC7739Signer.sol"; +import {ERC7739Signer} from "../../../../utils/cryptography/draft-ERC7739Signer.sol"; -contract ERC7739SignerRSA is ERC7739Signer { +contract ERC7739SignerRSAMock is ERC7739Signer { bytes private _e; bytes private _n; diff --git a/contracts/vendor/erc4337-entrypoint/README.md b/contracts/vendor/erc4337-entrypoint/README.md new file mode 100644 index 00000000..e0b91fe9 --- /dev/null +++ b/contracts/vendor/erc4337-entrypoint/README.md @@ -0,0 +1 @@ +Files in this directory are vendored from https://github.com/eth-infinitism/account-abstraction/commit/6f02f5a28a20e804d0410b4b5b570dd4b076dcf9 diff --git a/contracts/vendor/erc4337-entrypoint/core/Entrypoint.sol b/contracts/vendor/erc4337-entrypoint/core/Entrypoint.sol new file mode 100644 index 00000000..c1d05e03 --- /dev/null +++ b/contracts/vendor/erc4337-entrypoint/core/Entrypoint.sol @@ -0,0 +1,725 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.23; +/* solhint-disable avoid-low-level-calls */ +/* solhint-disable no-inline-assembly */ + +import "../interfaces/IAccount.sol"; +import "../interfaces/IAccountExecute.sol"; +import "../interfaces/IPaymaster.sol"; +import "../interfaces/IEntryPoint.sol"; + +import "../utils/Exec.sol"; +import "./StakeManager.sol"; +import "./SenderCreator.sol"; +import "./Helpers.sol"; +import "./NonceManager.sol"; +import "./UserOperationLib.sol"; + +import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; + +/* + * Account-Abstraction (EIP-4337) singleton EntryPoint implementation. + * Only one instance required on each chain. + */ + +/// @custom:security-contact https://bounty.ethereum.org +contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, ERC165 { + using UserOperationLib for PackedUserOperation; + + SenderCreator private immutable _senderCreator = new SenderCreator(); + + function senderCreator() internal view virtual returns (SenderCreator) { + return _senderCreator; + } + + //compensate for innerHandleOps' emit message and deposit refund. + // allow some slack for future gas price changes. + uint256 private constant INNER_GAS_OVERHEAD = 10000; + + // Marker for inner call revert on out of gas + bytes32 private constant INNER_OUT_OF_GAS = hex"deaddead"; + bytes32 private constant INNER_REVERT_LOW_PREFUND = hex"deadaa51"; + + uint256 private constant REVERT_REASON_MAX_LEN = 2048; + uint256 private constant PENALTY_PERCENT = 10; + + /// @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + // note: solidity "type(IEntryPoint).interfaceId" is without inherited methods but we want to check everything + return + interfaceId == + (type(IEntryPoint).interfaceId ^ type(IStakeManager).interfaceId ^ type(INonceManager).interfaceId) || + interfaceId == type(IEntryPoint).interfaceId || + interfaceId == type(IStakeManager).interfaceId || + interfaceId == type(INonceManager).interfaceId || + super.supportsInterface(interfaceId); + } + + /** + * Compensate the caller's beneficiary address with the collected fees of all UserOperations. + * @param beneficiary - The address to receive the fees. + * @param amount - Amount to transfer. + */ + function _compensate(address payable beneficiary, uint256 amount) internal { + require(beneficiary != address(0), "AA90 invalid beneficiary"); + (bool success, ) = beneficiary.call{value: amount}(""); + require(success, "AA91 failed send to beneficiary"); + } + + /** + * Execute a user operation. + * @param opIndex - Index into the opInfo array. + * @param userOp - The userOp to execute. + * @param opInfo - The opInfo filled by validatePrepayment for this userOp. + * @return collected - The total amount this userOp paid. + */ + function _executeUserOp( + uint256 opIndex, + PackedUserOperation calldata userOp, + UserOpInfo memory opInfo + ) internal returns (uint256 collected) { + uint256 preGas = gasleft(); + bytes memory context = getMemoryBytesFromOffset(opInfo.contextOffset); + bool success; + { + uint256 saveFreePtr; + assembly ("memory-safe") { + saveFreePtr := mload(0x40) + } + bytes calldata callData = userOp.callData; + bytes memory innerCall; + bytes4 methodSig; + assembly { + let len := callData.length + if gt(len, 3) { + methodSig := calldataload(callData.offset) + } + } + if (methodSig == IAccountExecute.executeUserOp.selector) { + bytes memory executeUserOp = abi.encodeCall(IAccountExecute.executeUserOp, (userOp, opInfo.userOpHash)); + innerCall = abi.encodeCall(this.innerHandleOp, (executeUserOp, opInfo, context)); + } else { + innerCall = abi.encodeCall(this.innerHandleOp, (callData, opInfo, context)); + } + assembly ("memory-safe") { + success := call(gas(), address(), 0, add(innerCall, 0x20), mload(innerCall), 0, 32) + collected := mload(0) + mstore(0x40, saveFreePtr) + } + } + if (!success) { + bytes32 innerRevertCode; + assembly ("memory-safe") { + let len := returndatasize() + if eq(32, len) { + returndatacopy(0, 0, 32) + innerRevertCode := mload(0) + } + } + if (innerRevertCode == INNER_OUT_OF_GAS) { + // handleOps was called with gas limit too low. abort entire bundle. + //can only be caused by bundler (leaving not enough gas for inner call) + revert FailedOp(opIndex, "AA95 out of gas"); + } else if (innerRevertCode == INNER_REVERT_LOW_PREFUND) { + // innerCall reverted on prefund too low. treat entire prefund as "gas cost" + uint256 actualGas = preGas - gasleft() + opInfo.preOpGas; + uint256 actualGasCost = opInfo.prefund; + emitPrefundTooLow(opInfo); + emitUserOperationEvent(opInfo, false, actualGasCost, actualGas); + collected = actualGasCost; + } else { + emit PostOpRevertReason( + opInfo.userOpHash, + opInfo.mUserOp.sender, + opInfo.mUserOp.nonce, + Exec.getReturnData(REVERT_REASON_MAX_LEN) + ); + + uint256 actualGas = preGas - gasleft() + opInfo.preOpGas; + collected = _postExecution(IPaymaster.PostOpMode.postOpReverted, opInfo, context, actualGas); + } + } + } + + function emitUserOperationEvent( + UserOpInfo memory opInfo, + bool success, + uint256 actualGasCost, + uint256 actualGas + ) internal virtual { + emit UserOperationEvent( + opInfo.userOpHash, + opInfo.mUserOp.sender, + opInfo.mUserOp.paymaster, + opInfo.mUserOp.nonce, + success, + actualGasCost, + actualGas + ); + } + + function emitPrefundTooLow(UserOpInfo memory opInfo) internal virtual { + emit UserOperationPrefundTooLow(opInfo.userOpHash, opInfo.mUserOp.sender, opInfo.mUserOp.nonce); + } + + /// @inheritdoc IEntryPoint + function handleOps(PackedUserOperation[] calldata ops, address payable beneficiary) public nonReentrant { + uint256 opslen = ops.length; + UserOpInfo[] memory opInfos = new UserOpInfo[](opslen); + + unchecked { + for (uint256 i = 0; i < opslen; i++) { + UserOpInfo memory opInfo = opInfos[i]; + (uint256 validationData, uint256 pmValidationData) = _validatePrepayment(i, ops[i], opInfo); + _validateAccountAndPaymasterValidationData(i, validationData, pmValidationData, address(0)); + } + + uint256 collected = 0; + emit BeforeExecution(); + + for (uint256 i = 0; i < opslen; i++) { + collected += _executeUserOp(i, ops[i], opInfos[i]); + } + + _compensate(beneficiary, collected); + } + } + + /// @inheritdoc IEntryPoint + function handleAggregatedOps( + UserOpsPerAggregator[] calldata opsPerAggregator, + address payable beneficiary + ) public nonReentrant { + uint256 opasLen = opsPerAggregator.length; + uint256 totalOps = 0; + for (uint256 i = 0; i < opasLen; i++) { + UserOpsPerAggregator calldata opa = opsPerAggregator[i]; + PackedUserOperation[] calldata ops = opa.userOps; + IAggregator aggregator = opa.aggregator; + + //address(1) is special marker of "signature error" + require(address(aggregator) != address(1), "AA96 invalid aggregator"); + + if (address(aggregator) != address(0)) { + // solhint-disable-next-line no-empty-blocks + try aggregator.validateSignatures(ops, opa.signature) {} catch { + revert SignatureValidationFailed(address(aggregator)); + } + } + + totalOps += ops.length; + } + + UserOpInfo[] memory opInfos = new UserOpInfo[](totalOps); + + uint256 opIndex = 0; + for (uint256 a = 0; a < opasLen; a++) { + UserOpsPerAggregator calldata opa = opsPerAggregator[a]; + PackedUserOperation[] calldata ops = opa.userOps; + IAggregator aggregator = opa.aggregator; + + uint256 opslen = ops.length; + for (uint256 i = 0; i < opslen; i++) { + UserOpInfo memory opInfo = opInfos[opIndex]; + (uint256 validationData, uint256 paymasterValidationData) = _validatePrepayment( + opIndex, + ops[i], + opInfo + ); + _validateAccountAndPaymasterValidationData( + i, + validationData, + paymasterValidationData, + address(aggregator) + ); + opIndex++; + } + } + + emit BeforeExecution(); + + uint256 collected = 0; + opIndex = 0; + for (uint256 a = 0; a < opasLen; a++) { + UserOpsPerAggregator calldata opa = opsPerAggregator[a]; + emit SignatureAggregatorChanged(address(opa.aggregator)); + PackedUserOperation[] calldata ops = opa.userOps; + uint256 opslen = ops.length; + + for (uint256 i = 0; i < opslen; i++) { + collected += _executeUserOp(opIndex, ops[i], opInfos[opIndex]); + opIndex++; + } + } + emit SignatureAggregatorChanged(address(0)); + + _compensate(beneficiary, collected); + } + + /** + * A memory copy of UserOp static fields only. + * Excluding: callData, initCode and signature. Replacing paymasterAndData with paymaster. + */ + struct MemoryUserOp { + address sender; + uint256 nonce; + uint256 verificationGasLimit; + uint256 callGasLimit; + uint256 paymasterVerificationGasLimit; + uint256 paymasterPostOpGasLimit; + uint256 preVerificationGas; + address paymaster; + uint256 maxFeePerGas; + uint256 maxPriorityFeePerGas; + } + + struct UserOpInfo { + MemoryUserOp mUserOp; + bytes32 userOpHash; + uint256 prefund; + uint256 contextOffset; + uint256 preOpGas; + } + + /** + * Inner function to handle a UserOperation. + * Must be declared "external" to open a call context, but it can only be called by handleOps. + * @param callData - The callData to execute. + * @param opInfo - The UserOpInfo struct. + * @param context - The context bytes. + * @return actualGasCost - the actual cost in eth this UserOperation paid for gas + */ + function innerHandleOp( + bytes memory callData, + UserOpInfo memory opInfo, + bytes calldata context + ) external returns (uint256 actualGasCost) { + uint256 preGas = gasleft(); + require(msg.sender == address(this), "AA92 internal call only"); + MemoryUserOp memory mUserOp = opInfo.mUserOp; + + uint256 callGasLimit = mUserOp.callGasLimit; + unchecked { + // handleOps was called with gas limit too low. abort entire bundle. + if ((gasleft() * 63) / 64 < callGasLimit + mUserOp.paymasterPostOpGasLimit + INNER_GAS_OVERHEAD) { + assembly ("memory-safe") { + mstore(0, INNER_OUT_OF_GAS) + revert(0, 32) + } + } + } + + IPaymaster.PostOpMode mode = IPaymaster.PostOpMode.opSucceeded; + if (callData.length > 0) { + bool success = Exec.call(mUserOp.sender, 0, callData, callGasLimit); + if (!success) { + bytes memory result = Exec.getReturnData(REVERT_REASON_MAX_LEN); + if (result.length > 0) { + emit UserOperationRevertReason(opInfo.userOpHash, mUserOp.sender, mUserOp.nonce, result); + } + mode = IPaymaster.PostOpMode.opReverted; + } + } + + unchecked { + uint256 actualGas = preGas - gasleft() + opInfo.preOpGas; + return _postExecution(mode, opInfo, context, actualGas); + } + } + + /// @inheritdoc IEntryPoint + function getUserOpHash(PackedUserOperation calldata userOp) public view returns (bytes32) { + return keccak256(abi.encode(userOp.hash(), address(this), block.chainid)); + } + + /** + * Copy general fields from userOp into the memory opInfo structure. + * @param userOp - The user operation. + * @param mUserOp - The memory user operation. + */ + function _copyUserOpToMemory(PackedUserOperation calldata userOp, MemoryUserOp memory mUserOp) internal pure { + mUserOp.sender = userOp.sender; + mUserOp.nonce = userOp.nonce; + (mUserOp.verificationGasLimit, mUserOp.callGasLimit) = UserOperationLib.unpackUints(userOp.accountGasLimits); + mUserOp.preVerificationGas = userOp.preVerificationGas; + (mUserOp.maxPriorityFeePerGas, mUserOp.maxFeePerGas) = UserOperationLib.unpackUints(userOp.gasFees); + bytes calldata paymasterAndData = userOp.paymasterAndData; + if (paymasterAndData.length > 0) { + require(paymasterAndData.length >= UserOperationLib.PAYMASTER_DATA_OFFSET, "AA93 invalid paymasterAndData"); + ( + mUserOp.paymaster, + mUserOp.paymasterVerificationGasLimit, + mUserOp.paymasterPostOpGasLimit + ) = UserOperationLib.unpackPaymasterStaticFields(paymasterAndData); + } else { + mUserOp.paymaster = address(0); + mUserOp.paymasterVerificationGasLimit = 0; + mUserOp.paymasterPostOpGasLimit = 0; + } + } + + /** + * Get the required prefunded gas fee amount for an operation. + * @param mUserOp - The user operation in memory. + */ + function _getRequiredPrefund(MemoryUserOp memory mUserOp) internal pure returns (uint256 requiredPrefund) { + unchecked { + uint256 requiredGas = mUserOp.verificationGasLimit + + mUserOp.callGasLimit + + mUserOp.paymasterVerificationGasLimit + + mUserOp.paymasterPostOpGasLimit + + mUserOp.preVerificationGas; + + requiredPrefund = requiredGas * mUserOp.maxFeePerGas; + } + } + + /** + * Create sender smart contract account if init code is provided. + * @param opIndex - The operation index. + * @param opInfo - The operation info. + * @param initCode - The init code for the smart contract account. + */ + function _createSenderIfNeeded(uint256 opIndex, UserOpInfo memory opInfo, bytes calldata initCode) internal { + if (initCode.length != 0) { + address sender = opInfo.mUserOp.sender; + if (sender.code.length != 0) revert FailedOp(opIndex, "AA10 sender already constructed"); + address sender1 = senderCreator().createSender{gas: opInfo.mUserOp.verificationGasLimit}(initCode); + if (sender1 == address(0)) revert FailedOp(opIndex, "AA13 initCode failed or OOG"); + if (sender1 != sender) revert FailedOp(opIndex, "AA14 initCode must return sender"); + if (sender1.code.length == 0) revert FailedOp(opIndex, "AA15 initCode must create sender"); + address factory = address(bytes20(initCode[0:20])); + emit AccountDeployed(opInfo.userOpHash, sender, factory, opInfo.mUserOp.paymaster); + } + } + + /// @inheritdoc IEntryPoint + function getSenderAddress(bytes calldata initCode) public { + address sender = senderCreator().createSender(initCode); + revert SenderAddressResult(sender); + } + + /** + * Call account.validateUserOp. + * Revert (with FailedOp) in case validateUserOp reverts, or account didn't send required prefund. + * Decrement account's deposit if needed. + * @param opIndex - The operation index. + * @param op - The user operation. + * @param opInfo - The operation info. + * @param requiredPrefund - The required prefund amount. + */ + function _validateAccountPrepayment( + uint256 opIndex, + PackedUserOperation calldata op, + UserOpInfo memory opInfo, + uint256 requiredPrefund, + uint256 verificationGasLimit + ) internal returns (uint256 validationData) { + unchecked { + MemoryUserOp memory mUserOp = opInfo.mUserOp; + address sender = mUserOp.sender; + _createSenderIfNeeded(opIndex, opInfo, op.initCode); + address paymaster = mUserOp.paymaster; + uint256 missingAccountFunds = 0; + if (paymaster == address(0)) { + uint256 bal = balanceOf(sender); + missingAccountFunds = bal > requiredPrefund ? 0 : requiredPrefund - bal; + } + try + IAccount(sender).validateUserOp{gas: verificationGasLimit}(op, opInfo.userOpHash, missingAccountFunds) + returns (uint256 _validationData) { + validationData = _validationData; + } catch { + revert FailedOpWithRevert(opIndex, "AA23 reverted", Exec.getReturnData(REVERT_REASON_MAX_LEN)); + } + if (paymaster == address(0)) { + DepositInfo storage senderInfo = deposits[sender]; + uint256 deposit = senderInfo.deposit; + if (requiredPrefund > deposit) { + revert FailedOp(opIndex, "AA21 didn't pay prefund"); + } + senderInfo.deposit = deposit - requiredPrefund; + } + } + } + + /** + * In case the request has a paymaster: + * - Validate paymaster has enough deposit. + * - Call paymaster.validatePaymasterUserOp. + * - Revert with proper FailedOp in case paymaster reverts. + * - Decrement paymaster's deposit. + * @param opIndex - The operation index. + * @param op - The user operation. + * @param opInfo - The operation info. + * @param requiredPreFund - The required prefund amount. + */ + function _validatePaymasterPrepayment( + uint256 opIndex, + PackedUserOperation calldata op, + UserOpInfo memory opInfo, + uint256 requiredPreFund + ) internal returns (bytes memory context, uint256 validationData) { + unchecked { + uint256 preGas = gasleft(); + MemoryUserOp memory mUserOp = opInfo.mUserOp; + address paymaster = mUserOp.paymaster; + DepositInfo storage paymasterInfo = deposits[paymaster]; + uint256 deposit = paymasterInfo.deposit; + if (deposit < requiredPreFund) { + revert FailedOp(opIndex, "AA31 paymaster deposit too low"); + } + paymasterInfo.deposit = deposit - requiredPreFund; + uint256 pmVerificationGasLimit = mUserOp.paymasterVerificationGasLimit; + try + IPaymaster(paymaster).validatePaymasterUserOp{gas: pmVerificationGasLimit}( + op, + opInfo.userOpHash, + requiredPreFund + ) + returns (bytes memory _context, uint256 _validationData) { + context = _context; + validationData = _validationData; + } catch { + revert FailedOpWithRevert(opIndex, "AA33 reverted", Exec.getReturnData(REVERT_REASON_MAX_LEN)); + } + if (preGas - gasleft() > pmVerificationGasLimit) { + revert FailedOp(opIndex, "AA36 over paymasterVerificationGasLimit"); + } + } + } + + /** + * Revert if either account validationData or paymaster validationData is expired. + * @param opIndex - The operation index. + * @param validationData - The account validationData. + * @param paymasterValidationData - The paymaster validationData. + * @param expectedAggregator - The expected aggregator. + */ + function _validateAccountAndPaymasterValidationData( + uint256 opIndex, + uint256 validationData, + uint256 paymasterValidationData, + address expectedAggregator + ) internal view { + (address aggregator, bool outOfTimeRange) = _getValidationData(validationData); + if (expectedAggregator != aggregator) { + revert FailedOp(opIndex, "AA24 signature error"); + } + if (outOfTimeRange) { + revert FailedOp(opIndex, "AA22 expired or not due"); + } + // pmAggregator is not a real signature aggregator: we don't have logic to handle it as address. + // Non-zero address means that the paymaster fails due to some signature check (which is ok only during estimation). + address pmAggregator; + (pmAggregator, outOfTimeRange) = _getValidationData(paymasterValidationData); + if (pmAggregator != address(0)) { + revert FailedOp(opIndex, "AA34 signature error"); + } + if (outOfTimeRange) { + revert FailedOp(opIndex, "AA32 paymaster expired or not due"); + } + } + + /** + * Parse validationData into its components. + * @param validationData - The packed validation data (sigFailed, validAfter, validUntil). + * @return aggregator the aggregator of the validationData + * @return outOfTimeRange true if current time is outside the time range of this validationData. + */ + function _getValidationData( + uint256 validationData + ) internal view returns (address aggregator, bool outOfTimeRange) { + if (validationData == 0) { + return (address(0), false); + } + ValidationData memory data = _parseValidationData(validationData); + // solhint-disable-next-line not-rely-on-time + outOfTimeRange = block.timestamp > data.validUntil || block.timestamp < data.validAfter; + aggregator = data.aggregator; + } + + /** + * Validate account and paymaster (if defined) and + * also make sure total validation doesn't exceed verificationGasLimit. + * This method is called off-chain (simulateValidation()) and on-chain (from handleOps) + * @param opIndex - The index of this userOp into the "opInfos" array. + * @param userOp - The userOp to validate. + */ + function _validatePrepayment( + uint256 opIndex, + PackedUserOperation calldata userOp, + UserOpInfo memory outOpInfo + ) internal returns (uint256 validationData, uint256 paymasterValidationData) { + uint256 preGas = gasleft(); + MemoryUserOp memory mUserOp = outOpInfo.mUserOp; + _copyUserOpToMemory(userOp, mUserOp); + outOpInfo.userOpHash = getUserOpHash(userOp); + + // Validate all numeric values in userOp are well below 128 bit, so they can safely be added + // and multiplied without causing overflow. + uint256 verificationGasLimit = mUserOp.verificationGasLimit; + uint256 maxGasValues = mUserOp.preVerificationGas | + verificationGasLimit | + mUserOp.callGasLimit | + mUserOp.paymasterVerificationGasLimit | + mUserOp.paymasterPostOpGasLimit | + mUserOp.maxFeePerGas | + mUserOp.maxPriorityFeePerGas; + require(maxGasValues <= type(uint120).max, "AA94 gas values overflow"); + + uint256 requiredPreFund = _getRequiredPrefund(mUserOp); + validationData = _validateAccountPrepayment(opIndex, userOp, outOpInfo, requiredPreFund, verificationGasLimit); + + if (!_validateAndUpdateNonce(mUserOp.sender, mUserOp.nonce)) { + revert FailedOp(opIndex, "AA25 invalid account nonce"); + } + + unchecked { + if (preGas - gasleft() > verificationGasLimit) { + revert FailedOp(opIndex, "AA26 over verificationGasLimit"); + } + } + + bytes memory context; + if (mUserOp.paymaster != address(0)) { + (context, paymasterValidationData) = _validatePaymasterPrepayment( + opIndex, + userOp, + outOpInfo, + requiredPreFund + ); + } + unchecked { + outOpInfo.prefund = requiredPreFund; + outOpInfo.contextOffset = getOffsetOfMemoryBytes(context); + outOpInfo.preOpGas = preGas - gasleft() + userOp.preVerificationGas; + } + } + + /** + * Process post-operation, called just after the callData is executed. + * If a paymaster is defined and its validation returned a non-empty context, its postOp is called. + * The excess amount is refunded to the account (or paymaster - if it was used in the request). + * @param mode - Whether is called from innerHandleOp, or outside (postOpReverted). + * @param opInfo - UserOp fields and info collected during validation. + * @param context - The context returned in validatePaymasterUserOp. + * @param actualGas - The gas used so far by this user operation. + */ + function _postExecution( + IPaymaster.PostOpMode mode, + UserOpInfo memory opInfo, + bytes memory context, + uint256 actualGas + ) private returns (uint256 actualGasCost) { + uint256 preGas = gasleft(); + unchecked { + address refundAddress; + MemoryUserOp memory mUserOp = opInfo.mUserOp; + uint256 gasPrice = getUserOpGasPrice(mUserOp); + + address paymaster = mUserOp.paymaster; + if (paymaster == address(0)) { + refundAddress = mUserOp.sender; + } else { + refundAddress = paymaster; + if (context.length > 0) { + actualGasCost = actualGas * gasPrice; + if (mode != IPaymaster.PostOpMode.postOpReverted) { + try + IPaymaster(paymaster).postOp{gas: mUserOp.paymasterPostOpGasLimit}( + mode, + context, + actualGasCost, + gasPrice + ) + // solhint-disable-next-line no-empty-blocks + { + + } catch { + bytes memory reason = Exec.getReturnData(REVERT_REASON_MAX_LEN); + revert PostOpReverted(reason); + } + } + } + } + actualGas += preGas - gasleft(); + + // Calculating a penalty for unused execution gas + { + uint256 executionGasLimit = mUserOp.callGasLimit + mUserOp.paymasterPostOpGasLimit; + uint256 executionGasUsed = actualGas - opInfo.preOpGas; + // this check is required for the gas used within EntryPoint and not covered by explicit gas limits + if (executionGasLimit > executionGasUsed) { + uint256 unusedGas = executionGasLimit - executionGasUsed; + uint256 unusedGasPenalty = (unusedGas * PENALTY_PERCENT) / 100; + actualGas += unusedGasPenalty; + } + } + + actualGasCost = actualGas * gasPrice; + uint256 prefund = opInfo.prefund; + if (prefund < actualGasCost) { + if (mode == IPaymaster.PostOpMode.postOpReverted) { + actualGasCost = prefund; + emitPrefundTooLow(opInfo); + emitUserOperationEvent(opInfo, false, actualGasCost, actualGas); + } else { + assembly ("memory-safe") { + mstore(0, INNER_REVERT_LOW_PREFUND) + revert(0, 32) + } + } + } else { + uint256 refund = prefund - actualGasCost; + _incrementDeposit(refundAddress, refund); + bool success = mode == IPaymaster.PostOpMode.opSucceeded; + emitUserOperationEvent(opInfo, success, actualGasCost, actualGas); + } + } // unchecked + } + + /** + * The gas price this UserOp agrees to pay. + * Relayer/block builder might submit the TX with higher priorityFee, but the user should not. + * @param mUserOp - The userOp to get the gas price from. + */ + function getUserOpGasPrice(MemoryUserOp memory mUserOp) internal view returns (uint256) { + unchecked { + uint256 maxFeePerGas = mUserOp.maxFeePerGas; + uint256 maxPriorityFeePerGas = mUserOp.maxPriorityFeePerGas; + if (maxFeePerGas == maxPriorityFeePerGas) { + //legacy mode (for networks that don't support basefee opcode) + return maxFeePerGas; + } + return min(maxFeePerGas, maxPriorityFeePerGas + block.basefee); + } + } + + /** + * The offset of the given bytes in memory. + * @param data - The bytes to get the offset of. + */ + function getOffsetOfMemoryBytes(bytes memory data) internal pure returns (uint256 offset) { + assembly { + offset := data + } + } + + /** + * The bytes in memory at the given offset. + * @param offset - The offset to get the bytes from. + */ + function getMemoryBytesFromOffset(uint256 offset) internal pure returns (bytes memory data) { + assembly ("memory-safe") { + data := offset + } + } + + /// @inheritdoc IEntryPoint + function delegateAndRevert(address target, bytes calldata data) external { + (bool success, bytes memory ret) = target.delegatecall(data); + revert DelegateAndRevert(success, ret); + } +} diff --git a/contracts/vendor/erc4337-entrypoint/core/Helpers.sol b/contracts/vendor/erc4337-entrypoint/core/Helpers.sol new file mode 100644 index 00000000..85790086 --- /dev/null +++ b/contracts/vendor/erc4337-entrypoint/core/Helpers.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.23; + +/* solhint-disable no-inline-assembly */ + + + /* + * For simulation purposes, validateUserOp (and validatePaymasterUserOp) + * must return this value in case of signature failure, instead of revert. + */ +uint256 constant SIG_VALIDATION_FAILED = 1; + + +/* + * For simulation purposes, validateUserOp (and validatePaymasterUserOp) + * return this value on success. + */ +uint256 constant SIG_VALIDATION_SUCCESS = 0; + + +/** + * Returned data from validateUserOp. + * validateUserOp returns a uint256, which is created by `_packedValidationData` and + * parsed by `_parseValidationData`. + * @param aggregator - address(0) - The account validated the signature by itself. + * address(1) - The account failed to validate the signature. + * otherwise - This is an address of a signature aggregator that must + * be used to validate the signature. + * @param validAfter - This UserOp is valid only after this timestamp. + * @param validaUntil - This UserOp is valid only up to this timestamp. + */ +struct ValidationData { + address aggregator; + uint48 validAfter; + uint48 validUntil; +} + +/** + * Extract sigFailed, validAfter, validUntil. + * Also convert zero validUntil to type(uint48).max. + * @param validationData - The packed validation data. + */ +function _parseValidationData( + uint256 validationData +) pure returns (ValidationData memory data) { + address aggregator = address(uint160(validationData)); + uint48 validUntil = uint48(validationData >> 160); + if (validUntil == 0) { + validUntil = type(uint48).max; + } + uint48 validAfter = uint48(validationData >> (48 + 160)); + return ValidationData(aggregator, validAfter, validUntil); +} + +/** + * Helper to pack the return value for validateUserOp. + * @param data - The ValidationData to pack. + */ +function _packValidationData( + ValidationData memory data +) pure returns (uint256) { + return + uint160(data.aggregator) | + (uint256(data.validUntil) << 160) | + (uint256(data.validAfter) << (160 + 48)); +} + +/** + * Helper to pack the return value for validateUserOp, when not using an aggregator. + * @param sigFailed - True for signature failure, false for success. + * @param validUntil - Last timestamp this UserOperation is valid (or zero for infinite). + * @param validAfter - First timestamp this UserOperation is valid. + */ +function _packValidationData( + bool sigFailed, + uint48 validUntil, + uint48 validAfter +) pure returns (uint256) { + return + (sigFailed ? 1 : 0) | + (uint256(validUntil) << 160) | + (uint256(validAfter) << (160 + 48)); +} + +/** + * keccak function over calldata. + * @dev copy calldata into memory, do keccak and drop allocated memory. Strangely, this is more efficient than letting solidity do it. + */ + function calldataKeccak(bytes calldata data) pure returns (bytes32 ret) { + assembly ("memory-safe") { + let mem := mload(0x40) + let len := data.length + calldatacopy(mem, data.offset, len) + ret := keccak256(mem, len) + } + } + + +/** + * The minimum of two numbers. + * @param a - First number. + * @param b - Second number. + */ + function min(uint256 a, uint256 b) pure returns (uint256) { + return a < b ? a : b; + } diff --git a/contracts/vendor/erc4337-entrypoint/core/NonceManager.sol b/contracts/vendor/erc4337-entrypoint/core/NonceManager.sol new file mode 100644 index 00000000..7bef62e9 --- /dev/null +++ b/contracts/vendor/erc4337-entrypoint/core/NonceManager.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.23; + +import "../interfaces/INonceManager.sol"; + +/** + * nonce management functionality + */ +abstract contract NonceManager is INonceManager { + + /** + * The next valid sequence number for a given nonce key. + */ + mapping(address => mapping(uint192 => uint256)) public nonceSequenceNumber; + + /// @inheritdoc INonceManager + function getNonce(address sender, uint192 key) + public view override returns (uint256 nonce) { + return nonceSequenceNumber[sender][key] | (uint256(key) << 64); + } + + // allow an account to manually increment its own nonce. + // (mainly so that during construction nonce can be made non-zero, + // to "absorb" the gas cost of first nonce increment to 1st transaction (construction), + // not to 2nd transaction) + function incrementNonce(uint192 key) public override { + nonceSequenceNumber[msg.sender][key]++; + } + + /** + * validate nonce uniqueness for this account. + * called just after validateUserOp() + * @return true if the nonce was incremented successfully. + * false if the current nonce doesn't match the given one. + */ + function _validateAndUpdateNonce(address sender, uint256 nonce) internal returns (bool) { + + uint192 key = uint192(nonce >> 64); + uint64 seq = uint64(nonce); + return nonceSequenceNumber[sender][key]++ == seq; + } + +} diff --git a/contracts/vendor/erc4337-entrypoint/core/SenderCreator.sol b/contracts/vendor/erc4337-entrypoint/core/SenderCreator.sol new file mode 100644 index 00000000..43ea8036 --- /dev/null +++ b/contracts/vendor/erc4337-entrypoint/core/SenderCreator.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.23; + +/** + * Helper contract for EntryPoint, to call userOp.initCode from a "neutral" address, + * which is explicitly not the entryPoint itself. + */ +contract SenderCreator { + /** + * Call the "initCode" factory to create and return the sender account address. + * @param initCode - The initCode value from a UserOp. contains 20 bytes of factory address, + * followed by calldata. + * @return sender - The returned address of the created account, or zero address on failure. + */ + function createSender( + bytes calldata initCode + ) external returns (address sender) { + address factory = address(bytes20(initCode[0:20])); + bytes memory initCallData = initCode[20:]; + bool success; + /* solhint-disable no-inline-assembly */ + assembly ("memory-safe") { + success := call( + gas(), + factory, + 0, + add(initCallData, 0x20), + mload(initCallData), + 0, + 32 + ) + sender := mload(0) + } + if (!success) { + sender = address(0); + } + } +} diff --git a/contracts/vendor/erc4337-entrypoint/core/StakeManager.sol b/contracts/vendor/erc4337-entrypoint/core/StakeManager.sol new file mode 100644 index 00000000..f90210b7 --- /dev/null +++ b/contracts/vendor/erc4337-entrypoint/core/StakeManager.sol @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.23; + +import "../interfaces/IStakeManager.sol"; + +/* solhint-disable avoid-low-level-calls */ +/* solhint-disable not-rely-on-time */ + +/** + * Manage deposits and stakes. + * Deposit is just a balance used to pay for UserOperations (either by a paymaster or an account). + * Stake is value locked for at least "unstakeDelay" by a paymaster. + */ +abstract contract StakeManager is IStakeManager { + /// maps paymaster to their deposits and stakes + mapping(address => DepositInfo) public deposits; + + /// @inheritdoc IStakeManager + function getDepositInfo( + address account + ) public view returns (DepositInfo memory info) { + return deposits[account]; + } + + /** + * Internal method to return just the stake info. + * @param addr - The account to query. + */ + function _getStakeInfo( + address addr + ) internal view returns (StakeInfo memory info) { + DepositInfo storage depositInfo = deposits[addr]; + info.stake = depositInfo.stake; + info.unstakeDelaySec = depositInfo.unstakeDelaySec; + } + + /// @inheritdoc IStakeManager + function balanceOf(address account) public view returns (uint256) { + return deposits[account].deposit; + } + + receive() external payable { + depositTo(msg.sender); + } + + /** + * Increments an account's deposit. + * @param account - The account to increment. + * @param amount - The amount to increment by. + * @return the updated deposit of this account + */ + function _incrementDeposit(address account, uint256 amount) internal returns (uint256) { + DepositInfo storage info = deposits[account]; + uint256 newAmount = info.deposit + amount; + info.deposit = newAmount; + return newAmount; + } + + /** + * Add to the deposit of the given account. + * @param account - The account to add to. + */ + function depositTo(address account) public virtual payable { + uint256 newDeposit = _incrementDeposit(account, msg.value); + emit Deposited(account, newDeposit); + } + + /** + * Add to the account's stake - amount and delay + * any pending unstake is first cancelled. + * @param unstakeDelaySec The new lock duration before the deposit can be withdrawn. + */ + function addStake(uint32 unstakeDelaySec) public payable { + DepositInfo storage info = deposits[msg.sender]; + require(unstakeDelaySec > 0, "must specify unstake delay"); + require( + unstakeDelaySec >= info.unstakeDelaySec, + "cannot decrease unstake time" + ); + uint256 stake = info.stake + msg.value; + require(stake > 0, "no stake specified"); + require(stake <= type(uint112).max, "stake overflow"); + deposits[msg.sender] = DepositInfo( + info.deposit, + true, + uint112(stake), + unstakeDelaySec, + 0 + ); + emit StakeLocked(msg.sender, stake, unstakeDelaySec); + } + + /** + * Attempt to unlock the stake. + * The value can be withdrawn (using withdrawStake) after the unstake delay. + */ + function unlockStake() external { + DepositInfo storage info = deposits[msg.sender]; + require(info.unstakeDelaySec != 0, "not staked"); + require(info.staked, "already unstaking"); + uint48 withdrawTime = uint48(block.timestamp) + info.unstakeDelaySec; + info.withdrawTime = withdrawTime; + info.staked = false; + emit StakeUnlocked(msg.sender, withdrawTime); + } + + /** + * Withdraw from the (unlocked) stake. + * Must first call unlockStake and wait for the unstakeDelay to pass. + * @param withdrawAddress - The address to send withdrawn value. + */ + function withdrawStake(address payable withdrawAddress) external { + DepositInfo storage info = deposits[msg.sender]; + uint256 stake = info.stake; + require(stake > 0, "No stake to withdraw"); + require(info.withdrawTime > 0, "must call unlockStake() first"); + require( + info.withdrawTime <= block.timestamp, + "Stake withdrawal is not due" + ); + info.unstakeDelaySec = 0; + info.withdrawTime = 0; + info.stake = 0; + emit StakeWithdrawn(msg.sender, withdrawAddress, stake); + (bool success,) = withdrawAddress.call{value: stake}(""); + require(success, "failed to withdraw stake"); + } + + /** + * Withdraw from the deposit. + * @param withdrawAddress - The address to send withdrawn value. + * @param withdrawAmount - The amount to withdraw. + */ + function withdrawTo( + address payable withdrawAddress, + uint256 withdrawAmount + ) external { + DepositInfo storage info = deposits[msg.sender]; + require(withdrawAmount <= info.deposit, "Withdraw amount too large"); + info.deposit = info.deposit - withdrawAmount; + emit Withdrawn(msg.sender, withdrawAddress, withdrawAmount); + (bool success,) = withdrawAddress.call{value: withdrawAmount}(""); + require(success, "failed to withdraw"); + } +} diff --git a/contracts/vendor/erc4337-entrypoint/core/UserOperationLib.sol b/contracts/vendor/erc4337-entrypoint/core/UserOperationLib.sol new file mode 100644 index 00000000..dcf5740c --- /dev/null +++ b/contracts/vendor/erc4337-entrypoint/core/UserOperationLib.sol @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.23; + +/* solhint-disable no-inline-assembly */ + +import "../interfaces/PackedUserOperation.sol"; +import {calldataKeccak, min} from "./Helpers.sol"; + +/** + * Utility functions helpful when working with UserOperation structs. + */ +library UserOperationLib { + + uint256 public constant PAYMASTER_VALIDATION_GAS_OFFSET = 20; + uint256 public constant PAYMASTER_POSTOP_GAS_OFFSET = 36; + uint256 public constant PAYMASTER_DATA_OFFSET = 52; + /** + * Get sender from user operation data. + * @param userOp - The user operation data. + */ + function getSender( + PackedUserOperation calldata userOp + ) internal pure returns (address) { + address data; + //read sender from userOp, which is first userOp member (saves 800 gas...) + assembly { + data := calldataload(userOp) + } + return address(uint160(data)); + } + + /** + * Relayer/block builder might submit the TX with higher priorityFee, + * but the user should not pay above what he signed for. + * @param userOp - The user operation data. + */ + function gasPrice( + PackedUserOperation calldata userOp + ) internal view returns (uint256) { + unchecked { + (uint256 maxPriorityFeePerGas, uint256 maxFeePerGas) = unpackUints(userOp.gasFees); + if (maxFeePerGas == maxPriorityFeePerGas) { + //legacy mode (for networks that don't support basefee opcode) + return maxFeePerGas; + } + return min(maxFeePerGas, maxPriorityFeePerGas + block.basefee); + } + } + + /** + * Pack the user operation data into bytes for hashing. + * @param userOp - The user operation data. + */ + function encode( + PackedUserOperation calldata userOp + ) internal pure returns (bytes memory ret) { + address sender = getSender(userOp); + uint256 nonce = userOp.nonce; + bytes32 hashInitCode = calldataKeccak(userOp.initCode); + bytes32 hashCallData = calldataKeccak(userOp.callData); + bytes32 accountGasLimits = userOp.accountGasLimits; + uint256 preVerificationGas = userOp.preVerificationGas; + bytes32 gasFees = userOp.gasFees; + bytes32 hashPaymasterAndData = calldataKeccak(userOp.paymasterAndData); + + return abi.encode( + sender, nonce, + hashInitCode, hashCallData, + accountGasLimits, preVerificationGas, gasFees, + hashPaymasterAndData + ); + } + + function unpackUints( + bytes32 packed + ) internal pure returns (uint256 high128, uint256 low128) { + return (uint128(bytes16(packed)), uint128(uint256(packed))); + } + + //unpack just the high 128-bits from a packed value + function unpackHigh128(bytes32 packed) internal pure returns (uint256) { + return uint256(packed) >> 128; + } + + // unpack just the low 128-bits from a packed value + function unpackLow128(bytes32 packed) internal pure returns (uint256) { + return uint128(uint256(packed)); + } + + function unpackMaxPriorityFeePerGas(PackedUserOperation calldata userOp) + internal pure returns (uint256) { + return unpackHigh128(userOp.gasFees); + } + + function unpackMaxFeePerGas(PackedUserOperation calldata userOp) + internal pure returns (uint256) { + return unpackLow128(userOp.gasFees); + } + + function unpackVerificationGasLimit(PackedUserOperation calldata userOp) + internal pure returns (uint256) { + return unpackHigh128(userOp.accountGasLimits); + } + + function unpackCallGasLimit(PackedUserOperation calldata userOp) + internal pure returns (uint256) { + return unpackLow128(userOp.accountGasLimits); + } + + function unpackPaymasterVerificationGasLimit(PackedUserOperation calldata userOp) + internal pure returns (uint256) { + return uint128(bytes16(userOp.paymasterAndData[PAYMASTER_VALIDATION_GAS_OFFSET : PAYMASTER_POSTOP_GAS_OFFSET])); + } + + function unpackPostOpGasLimit(PackedUserOperation calldata userOp) + internal pure returns (uint256) { + return uint128(bytes16(userOp.paymasterAndData[PAYMASTER_POSTOP_GAS_OFFSET : PAYMASTER_DATA_OFFSET])); + } + + function unpackPaymasterStaticFields( + bytes calldata paymasterAndData + ) internal pure returns (address paymaster, uint256 validationGasLimit, uint256 postOpGasLimit) { + return ( + address(bytes20(paymasterAndData[: PAYMASTER_VALIDATION_GAS_OFFSET])), + uint128(bytes16(paymasterAndData[PAYMASTER_VALIDATION_GAS_OFFSET : PAYMASTER_POSTOP_GAS_OFFSET])), + uint128(bytes16(paymasterAndData[PAYMASTER_POSTOP_GAS_OFFSET : PAYMASTER_DATA_OFFSET])) + ); + } + + /** + * Hash the user operation data. + * @param userOp - The user operation data. + */ + function hash( + PackedUserOperation calldata userOp + ) internal pure returns (bytes32) { + return keccak256(encode(userOp)); + } +} diff --git a/contracts/vendor/erc4337-entrypoint/interfaces/IAccount.sol b/contracts/vendor/erc4337-entrypoint/interfaces/IAccount.sol new file mode 100644 index 00000000..e3b355fb --- /dev/null +++ b/contracts/vendor/erc4337-entrypoint/interfaces/IAccount.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.7.5; + +import "./PackedUserOperation.sol"; + +interface IAccount { + /** + * Validate user's signature and nonce + * the entryPoint will make the call to the recipient only if this validation call returns successfully. + * signature failure should be reported by returning SIG_VALIDATION_FAILED (1). + * This allows making a "simulation call" without a valid signature + * Other failures (e.g. nonce mismatch, or invalid signature format) should still revert to signal failure. + * + * @dev Must validate caller is the entryPoint. + * Must validate the signature and nonce + * @param userOp - The operation that is about to be executed. + * @param userOpHash - Hash of the user's request data. can be used as the basis for signature. + * @param missingAccountFunds - Missing funds on the account's deposit in the entrypoint. + * This is the minimum amount to transfer to the sender(entryPoint) to be + * able to make the call. The excess is left as a deposit in the entrypoint + * for future calls. Can be withdrawn anytime using "entryPoint.withdrawTo()". + * In case there is a paymaster in the request (or the current deposit is high + * enough), this value will be zero. + * @return validationData - Packaged ValidationData structure. use `_packValidationData` and + * `_unpackValidationData` to encode and decode. + * <20-byte> sigAuthorizer - 0 for valid signature, 1 to mark signature failure, + * otherwise, an address of an "authorizer" contract. + * <6-byte> validUntil - Last timestamp this operation is valid. 0 for "indefinite" + * <6-byte> validAfter - First timestamp this operation is valid + * If an account doesn't use time-range, it is enough to + * return SIG_VALIDATION_FAILED value (1) for signature failure. + * Note that the validation code cannot use block.timestamp (or block.number) directly. + */ + function validateUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash, + uint256 missingAccountFunds + ) external returns (uint256 validationData); +} diff --git a/contracts/vendor/erc4337-entrypoint/interfaces/IAccountExecute.sol b/contracts/vendor/erc4337-entrypoint/interfaces/IAccountExecute.sol new file mode 100644 index 00000000..4433c80c --- /dev/null +++ b/contracts/vendor/erc4337-entrypoint/interfaces/IAccountExecute.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.7.5; + +import "./PackedUserOperation.sol"; + +interface IAccountExecute { + /** + * Account may implement this execute method. + * passing this methodSig at the beginning of callData will cause the entryPoint to pass the full UserOp (and hash) + * to the account. + * The account should skip the methodSig, and use the callData (and optionally, other UserOp fields) + * + * @param userOp - The operation that was just validated. + * @param userOpHash - Hash of the user's request data. + */ + function executeUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash + ) external; +} diff --git a/contracts/vendor/erc4337-entrypoint/interfaces/IAggregator.sol b/contracts/vendor/erc4337-entrypoint/interfaces/IAggregator.sol new file mode 100644 index 00000000..070d8f27 --- /dev/null +++ b/contracts/vendor/erc4337-entrypoint/interfaces/IAggregator.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.7.5; + +import "./PackedUserOperation.sol"; + +/** + * Aggregated Signatures validator. + */ +interface IAggregator { + /** + * Validate aggregated signature. + * Revert if the aggregated signature does not match the given list of operations. + * @param userOps - Array of UserOperations to validate the signature for. + * @param signature - The aggregated signature. + */ + function validateSignatures( + PackedUserOperation[] calldata userOps, + bytes calldata signature + ) external view; + + /** + * Validate signature of a single userOp. + * This method should be called by bundler after EntryPointSimulation.simulateValidation() returns + * the aggregator this account uses. + * First it validates the signature over the userOp. Then it returns data to be used when creating the handleOps. + * @param userOp - The userOperation received from the user. + * @return sigForUserOp - The value to put into the signature field of the userOp when calling handleOps. + * (usually empty, unless account and aggregator support some kind of "multisig". + */ + function validateUserOpSignature( + PackedUserOperation calldata userOp + ) external view returns (bytes memory sigForUserOp); + + /** + * Aggregate multiple signatures into a single value. + * This method is called off-chain to calculate the signature to pass with handleOps() + * bundler MAY use optimized custom code perform this aggregation. + * @param userOps - Array of UserOperations to collect the signatures from. + * @return aggregatedSignature - The aggregated signature. + */ + function aggregateSignatures( + PackedUserOperation[] calldata userOps + ) external view returns (bytes memory aggregatedSignature); +} diff --git a/contracts/vendor/erc4337-entrypoint/interfaces/IEntryPoint.sol b/contracts/vendor/erc4337-entrypoint/interfaces/IEntryPoint.sol new file mode 100644 index 00000000..28c26f98 --- /dev/null +++ b/contracts/vendor/erc4337-entrypoint/interfaces/IEntryPoint.sol @@ -0,0 +1,223 @@ +/** + ** Account-Abstraction (EIP-4337) singleton EntryPoint implementation. + ** Only one instance required on each chain. + **/ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.7.5; + +/* solhint-disable avoid-low-level-calls */ +/* solhint-disable no-inline-assembly */ +/* solhint-disable reason-string */ + +import "./PackedUserOperation.sol"; +import "./IStakeManager.sol"; +import "./IAggregator.sol"; +import "./INonceManager.sol"; + +interface IEntryPoint is IStakeManager, INonceManager { + /*** + * An event emitted after each successful request. + * @param userOpHash - Unique identifier for the request (hash its entire content, except signature). + * @param sender - The account that generates this request. + * @param paymaster - If non-null, the paymaster that pays for this request. + * @param nonce - The nonce value from the request. + * @param success - True if the sender transaction succeeded, false if reverted. + * @param actualGasCost - Actual amount paid (by account or paymaster) for this UserOperation. + * @param actualGasUsed - Total gas used by this UserOperation (including preVerification, creation, + * validation and execution). + */ + event UserOperationEvent( + bytes32 indexed userOpHash, + address indexed sender, + address indexed paymaster, + uint256 nonce, + bool success, + uint256 actualGasCost, + uint256 actualGasUsed + ); + + /** + * Account "sender" was deployed. + * @param userOpHash - The userOp that deployed this account. UserOperationEvent will follow. + * @param sender - The account that is deployed + * @param factory - The factory used to deploy this account (in the initCode) + * @param paymaster - The paymaster used by this UserOp + */ + event AccountDeployed( + bytes32 indexed userOpHash, + address indexed sender, + address factory, + address paymaster + ); + + /** + * An event emitted if the UserOperation "callData" reverted with non-zero length. + * @param userOpHash - The request unique identifier. + * @param sender - The sender of this request. + * @param nonce - The nonce used in the request. + * @param revertReason - The return bytes from the (reverted) call to "callData". + */ + event UserOperationRevertReason( + bytes32 indexed userOpHash, + address indexed sender, + uint256 nonce, + bytes revertReason + ); + + /** + * An event emitted if the UserOperation Paymaster's "postOp" call reverted with non-zero length. + * @param userOpHash - The request unique identifier. + * @param sender - The sender of this request. + * @param nonce - The nonce used in the request. + * @param revertReason - The return bytes from the (reverted) call to "callData". + */ + event PostOpRevertReason( + bytes32 indexed userOpHash, + address indexed sender, + uint256 nonce, + bytes revertReason + ); + + /** + * UserOp consumed more than prefund. The UserOperation is reverted, and no refund is made. + * @param userOpHash - The request unique identifier. + * @param sender - The sender of this request. + * @param nonce - The nonce used in the request. + */ + event UserOperationPrefundTooLow( + bytes32 indexed userOpHash, + address indexed sender, + uint256 nonce + ); + + /** + * An event emitted by handleOps(), before starting the execution loop. + * Any event emitted before this event, is part of the validation. + */ + event BeforeExecution(); + + /** + * Signature aggregator used by the following UserOperationEvents within this bundle. + * @param aggregator - The aggregator used for the following UserOperationEvents. + */ + event SignatureAggregatorChanged(address indexed aggregator); + + /** + * A custom revert error of handleOps, to identify the offending op. + * Should be caught in off-chain handleOps simulation and not happen on-chain. + * Useful for mitigating DoS attempts against batchers or for troubleshooting of factory/account/paymaster reverts. + * NOTE: If simulateValidation passes successfully, there should be no reason for handleOps to fail on it. + * @param opIndex - Index into the array of ops to the failed one (in simulateValidation, this is always zero). + * @param reason - Revert reason. The string starts with a unique code "AAmn", + * where "m" is "1" for factory, "2" for account and "3" for paymaster issues, + * so a failure can be attributed to the correct entity. + */ + error FailedOp(uint256 opIndex, string reason); + + /** + * A custom revert error of handleOps, to report a revert by account or paymaster. + * @param opIndex - Index into the array of ops to the failed one (in simulateValidation, this is always zero). + * @param reason - Revert reason. see FailedOp(uint256,string), above + * @param inner - data from inner cought revert reason + * @dev note that inner is truncated to 2048 bytes + */ + error FailedOpWithRevert(uint256 opIndex, string reason, bytes inner); + + error PostOpReverted(bytes returnData); + + /** + * Error case when a signature aggregator fails to verify the aggregated signature it had created. + * @param aggregator The aggregator that failed to verify the signature + */ + error SignatureValidationFailed(address aggregator); + + // Return value of getSenderAddress. + error SenderAddressResult(address sender); + + // UserOps handled, per aggregator. + struct UserOpsPerAggregator { + PackedUserOperation[] userOps; + // Aggregator address + IAggregator aggregator; + // Aggregated signature + bytes signature; + } + + /** + * Execute a batch of UserOperations. + * No signature aggregator is used. + * If any account requires an aggregator (that is, it returned an aggregator when + * performing simulateValidation), then handleAggregatedOps() must be used instead. + * @param ops - The operations to execute. + * @param beneficiary - The address to receive the fees. + */ + function handleOps( + PackedUserOperation[] calldata ops, + address payable beneficiary + ) external; + + /** + * Execute a batch of UserOperation with Aggregators + * @param opsPerAggregator - The operations to execute, grouped by aggregator (or address(0) for no-aggregator accounts). + * @param beneficiary - The address to receive the fees. + */ + function handleAggregatedOps( + UserOpsPerAggregator[] calldata opsPerAggregator, + address payable beneficiary + ) external; + + /** + * Generate a request Id - unique identifier for this request. + * The request ID is a hash over the content of the userOp (except the signature), the entrypoint and the chainid. + * @param userOp - The user operation to generate the request ID for. + * @return hash the hash of this UserOperation + */ + function getUserOpHash( + PackedUserOperation calldata userOp + ) external view returns (bytes32); + + /** + * Gas and return values during simulation. + * @param preOpGas - The gas used for validation (including preValidationGas) + * @param prefund - The required prefund for this operation + * @param accountValidationData - returned validationData from account. + * @param paymasterValidationData - return validationData from paymaster. + * @param paymasterContext - Returned by validatePaymasterUserOp (to be passed into postOp) + */ + struct ReturnInfo { + uint256 preOpGas; + uint256 prefund; + uint256 accountValidationData; + uint256 paymasterValidationData; + bytes paymasterContext; + } + + /** + * Returned aggregated signature info: + * The aggregator returned by the account, and its current stake. + */ + struct AggregatorStakeInfo { + address aggregator; + StakeInfo stakeInfo; + } + + /** + * Get counterfactual sender address. + * Calculate the sender contract address that will be generated by the initCode and salt in the UserOperation. + * This method always revert, and returns the address in SenderAddressResult error + * @param initCode - The constructor code to be passed into the UserOperation. + */ + function getSenderAddress(bytes memory initCode) external; + + error DelegateAndRevert(bool success, bytes ret); + + /** + * Helper method for dry-run testing. + * @dev calling this method, the EntryPoint will make a delegatecall to the given data, and report (via revert) the result. + * The method always revert, so is only useful off-chain for dry run calls, in cases where state-override to replace + * actual EntryPoint code is less convenient. + * @param target a target contract to make a delegatecall from entrypoint + * @param data data to pass to target in a delegatecall + */ + function delegateAndRevert(address target, bytes calldata data) external; +} diff --git a/contracts/vendor/erc4337-entrypoint/interfaces/INonceManager.sol b/contracts/vendor/erc4337-entrypoint/interfaces/INonceManager.sol new file mode 100644 index 00000000..2f993f68 --- /dev/null +++ b/contracts/vendor/erc4337-entrypoint/interfaces/INonceManager.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.7.5; + +interface INonceManager { + + /** + * Return the next nonce for this sender. + * Within a given key, the nonce values are sequenced (starting with zero, and incremented by one on each userop) + * But UserOp with different keys can come with arbitrary order. + * + * @param sender the account address + * @param key the high 192 bit of the nonce + * @return nonce a full nonce to pass for next UserOp with this sender. + */ + function getNonce(address sender, uint192 key) + external view returns (uint256 nonce); + + /** + * Manually increment the nonce of the sender. + * This method is exposed just for completeness.. + * Account does NOT need to call it, neither during validation, nor elsewhere, + * as the EntryPoint will update the nonce regardless. + * Possible use-case is call it with various keys to "initialize" their nonces to one, so that future + * UserOperations will not pay extra for the first transaction with a given key. + */ + function incrementNonce(uint192 key) external; +} diff --git a/contracts/vendor/erc4337-entrypoint/interfaces/IPaymaster.sol b/contracts/vendor/erc4337-entrypoint/interfaces/IPaymaster.sol new file mode 100644 index 00000000..9176a0b2 --- /dev/null +++ b/contracts/vendor/erc4337-entrypoint/interfaces/IPaymaster.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.7.5; + +import "./PackedUserOperation.sol"; + +/** + * The interface exposed by a paymaster contract, who agrees to pay the gas for user's operations. + * A paymaster must hold a stake to cover the required entrypoint stake and also the gas for the transaction. + */ +interface IPaymaster { + enum PostOpMode { + // User op succeeded. + opSucceeded, + // User op reverted. Still has to pay for gas. + opReverted, + // Only used internally in the EntryPoint (cleanup after postOp reverts). Never calling paymaster with this value + postOpReverted + } + + /** + * Payment validation: check if paymaster agrees to pay. + * Must verify sender is the entryPoint. + * Revert to reject this request. + * Note that bundlers will reject this method if it changes the state, unless the paymaster is trusted (whitelisted). + * The paymaster pre-pays using its deposit, and receive back a refund after the postOp method returns. + * @param userOp - The user operation. + * @param userOpHash - Hash of the user's request data. + * @param maxCost - The maximum cost of this transaction (based on maximum gas and gas price from userOp). + * @return context - Value to send to a postOp. Zero length to signify postOp is not required. + * @return validationData - Signature and time-range of this operation, encoded the same as the return + * value of validateUserOperation. + * <20-byte> sigAuthorizer - 0 for valid signature, 1 to mark signature failure, + * other values are invalid for paymaster. + * <6-byte> validUntil - last timestamp this operation is valid. 0 for "indefinite" + * <6-byte> validAfter - first timestamp this operation is valid + * Note that the validation code cannot use block.timestamp (or block.number) directly. + */ + function validatePaymasterUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash, + uint256 maxCost + ) external returns (bytes memory context, uint256 validationData); + + /** + * Post-operation handler. + * Must verify sender is the entryPoint. + * @param mode - Enum with the following options: + * opSucceeded - User operation succeeded. + * opReverted - User op reverted. The paymaster still has to pay for gas. + * postOpReverted - never passed in a call to postOp(). + * @param context - The context value returned by validatePaymasterUserOp + * @param actualGasCost - Actual gas used so far (without this postOp call). + * @param actualUserOpFeePerGas - the gas price this UserOp pays. This value is based on the UserOp's maxFeePerGas + * and maxPriorityFee (and basefee) + * It is not the same as tx.gasprice, which is what the bundler pays. + */ + function postOp( + PostOpMode mode, + bytes calldata context, + uint256 actualGasCost, + uint256 actualUserOpFeePerGas + ) external; +} diff --git a/contracts/vendor/erc4337-entrypoint/interfaces/IStakeManager.sol b/contracts/vendor/erc4337-entrypoint/interfaces/IStakeManager.sol new file mode 100644 index 00000000..69083e93 --- /dev/null +++ b/contracts/vendor/erc4337-entrypoint/interfaces/IStakeManager.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.7.5; + +/** + * Manage deposits and stakes. + * Deposit is just a balance used to pay for UserOperations (either by a paymaster or an account). + * Stake is value locked for at least "unstakeDelay" by the staked entity. + */ +interface IStakeManager { + event Deposited(address indexed account, uint256 totalDeposit); + + event Withdrawn( + address indexed account, + address withdrawAddress, + uint256 amount + ); + + // Emitted when stake or unstake delay are modified. + event StakeLocked( + address indexed account, + uint256 totalStaked, + uint256 unstakeDelaySec + ); + + // Emitted once a stake is scheduled for withdrawal. + event StakeUnlocked(address indexed account, uint256 withdrawTime); + + event StakeWithdrawn( + address indexed account, + address withdrawAddress, + uint256 amount + ); + + /** + * @param deposit - The entity's deposit. + * @param staked - True if this entity is staked. + * @param stake - Actual amount of ether staked for this entity. + * @param unstakeDelaySec - Minimum delay to withdraw the stake. + * @param withdrawTime - First block timestamp where 'withdrawStake' will be callable, or zero if already locked. + * @dev Sizes were chosen so that deposit fits into one cell (used during handleOp) + * and the rest fit into a 2nd cell (used during stake/unstake) + * - 112 bit allows for 10^15 eth + * - 48 bit for full timestamp + * - 32 bit allows 150 years for unstake delay + */ + struct DepositInfo { + uint256 deposit; + bool staked; + uint112 stake; + uint32 unstakeDelaySec; + uint48 withdrawTime; + } + + // API struct used by getStakeInfo and simulateValidation. + struct StakeInfo { + uint256 stake; + uint256 unstakeDelaySec; + } + + /** + * Get deposit info. + * @param account - The account to query. + * @return info - Full deposit information of given account. + */ + function getDepositInfo( + address account + ) external view returns (DepositInfo memory info); + + /** + * Get account balance. + * @param account - The account to query. + * @return - The deposit (for gas payment) of the account. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * Add to the deposit of the given account. + * @param account - The account to add to. + */ + function depositTo(address account) external payable; + + /** + * Add to the account's stake - amount and delay + * any pending unstake is first cancelled. + * @param _unstakeDelaySec - The new lock duration before the deposit can be withdrawn. + */ + function addStake(uint32 _unstakeDelaySec) external payable; + + /** + * Attempt to unlock the stake. + * The value can be withdrawn (using withdrawStake) after the unstake delay. + */ + function unlockStake() external; + + /** + * Withdraw from the (unlocked) stake. + * Must first call unlockStake and wait for the unstakeDelay to pass. + * @param withdrawAddress - The address to send withdrawn value. + */ + function withdrawStake(address payable withdrawAddress) external; + + /** + * Withdraw from the deposit. + * @param withdrawAddress - The address to send withdrawn value. + * @param withdrawAmount - The amount to withdraw. + */ + function withdrawTo( + address payable withdrawAddress, + uint256 withdrawAmount + ) external; +} diff --git a/contracts/vendor/erc4337-entrypoint/interfaces/PackedUserOperation.sol b/contracts/vendor/erc4337-entrypoint/interfaces/PackedUserOperation.sol new file mode 100644 index 00000000..fe20de56 --- /dev/null +++ b/contracts/vendor/erc4337-entrypoint/interfaces/PackedUserOperation.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.7.5; + +/** + * User Operation struct + * @param sender - The sender account of this request. + * @param nonce - Unique value the sender uses to verify it is not a replay. + * @param initCode - If set, the account contract will be created by this constructor/ + * @param callData - The method call to execute on this account. + * @param accountGasLimits - Packed gas limits for validateUserOp and gas limit passed to the callData method call. + * @param preVerificationGas - Gas not calculated by the handleOps method, but added to the gas paid. + * Covers batch overhead. + * @param gasFees - packed gas fields maxPriorityFeePerGas and maxFeePerGas - Same as EIP-1559 gas parameters. + * @param paymasterAndData - If set, this field holds the paymaster address, verification gas limit, postOp gas limit and paymaster-specific extra data + * The paymaster will pay for the transaction instead of the sender. + * @param signature - Sender-verified signature over the entire request, the EntryPoint address and the chain ID. + */ +struct PackedUserOperation { + address sender; + uint256 nonce; + bytes initCode; + bytes callData; + bytes32 accountGasLimits; + uint256 preVerificationGas; + bytes32 gasFees; + bytes paymasterAndData; + bytes signature; +} diff --git a/contracts/vendor/erc4337-entrypoint/utils/Exec.sol b/contracts/vendor/erc4337-entrypoint/utils/Exec.sol new file mode 100644 index 00000000..a245dedd --- /dev/null +++ b/contracts/vendor/erc4337-entrypoint/utils/Exec.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.23; + +// solhint-disable no-inline-assembly + +/** + * Utility functions helpful when making different kinds of contract calls in Solidity. + */ +library Exec { + function call(address to, uint256 value, bytes memory data, uint256 txGas) internal returns (bool success) { + assembly ("memory-safe") { + success := call(txGas, to, value, add(data, 0x20), mload(data), 0, 0) + } + } + + function staticcall(address to, bytes memory data, uint256 txGas) internal view returns (bool success) { + assembly ("memory-safe") { + success := staticcall(txGas, to, add(data, 0x20), mload(data), 0, 0) + } + } + + function delegateCall(address to, bytes memory data, uint256 txGas) internal returns (bool success) { + assembly ("memory-safe") { + success := delegatecall(txGas, to, add(data, 0x20), mload(data), 0, 0) + } + } + + // get returned data from last call or calldelegate + function getReturnData(uint256 maxLen) internal pure returns (bytes memory returnData) { + assembly ("memory-safe") { + let len := returndatasize() + if gt(len, maxLen) { + len := maxLen + } + let ptr := mload(0x40) + mstore(0x40, add(ptr, add(len, 0x20))) + mstore(ptr, len) + returndatacopy(add(ptr, 0x20), 0, len) + returnData := ptr + } + } + + // revert with explicit byte array (probably reverted info from call) + function revertWithData(bytes memory returnData) internal pure { + assembly ("memory-safe") { + revert(add(returnData, 32), mload(returnData)) + } + } + + function callAndRevert(address to, bytes memory data, uint256 maxLen) internal { + bool success = call(to, 0, data, gasleft()); + if (!success) { + revertWithData(getReturnData(maxLen)); + } + } +} diff --git a/test/account/Account.behavior.js b/test/account/Account.behavior.js index a52de182..b9e9b1f4 100644 --- a/test/account/Account.behavior.js +++ b/test/account/Account.behavior.js @@ -37,8 +37,15 @@ function shouldBehaveLikeAnAccountBase() { ]), }) .then(op => op.sign(this.domain, this.signer)); - - await expect(this.smartAccount.connect(this.other).validateUserOp(operation.packed, operation.hash, 0)) + await expect( + this.smartAccount + .connect(this.other) + .validateUserOp( + operation.packed, + operation.hash(operation.context.entrypoint.target, operation.context.chainId), + 0, + ), + ) .to.be.revertedWithCustomError(this.smartAccount, 'AccountUnauthorized') .withArgs(this.other); }); @@ -65,7 +72,11 @@ function shouldBehaveLikeAnAccountBase() { expect( await this.smartAccount .connect(this.entrypointAsSigner) - .validateUserOp.staticCall(operation.packed, operation.hash, 0), + .validateUserOp.staticCall( + operation.packed, + operation.hash(operation.context.entrypoint.target, operation.context.chainId), + 0, + ), ).to.eq(SIG_VALIDATION_SUCCESS); }); @@ -86,7 +97,11 @@ function shouldBehaveLikeAnAccountBase() { expect( await this.smartAccount .connect(this.entrypointAsSigner) - .validateUserOp.staticCall(operation.packed, operation.hash, 0), + .validateUserOp.staticCall( + operation.packed, + operation.hash(operation.context.entrypoint.target, operation.context.chainId), + 0, + ), ).to.eq(SIG_VALIDATION_FAILURE); }); @@ -110,7 +125,11 @@ function shouldBehaveLikeAnAccountBase() { const tx = await this.smartAccount .connect(this.entrypointAsSigner) - .validateUserOp(operation.packed, operation.hash, amount); + .validateUserOp( + operation.packed, + operation.hash(operation.context.entrypoint.target, operation.context.chainId), + amount, + ); const receipt = await tx.wait(); const callerFees = receipt.gasUsed * tx.gasPrice; @@ -189,8 +208,8 @@ function shouldBehaveLikeAccountHolder() { const [owner] = await ethers.getSigners(); - const token = await ethers.deployContract('$ERC721', [name, symbol]); - await token.$_mint(owner, tokenId); + const token = await ethers.deployContract('ERC721Mock', [name, symbol]); + await token.$mint(owner, tokenId); await token.connect(owner).safeTransferFrom(owner, this.smartAccount, tokenId); @@ -224,7 +243,14 @@ function shouldBehaveLikeAnAccountBaseExecutor({ deployable = true } = {}) { }) .then(op => op.sign(this.domain, this.signer)); - await expect(this.smartAccount.connect(this.other).executeUserOp(operation.packed, operation.hash)) + await expect( + this.smartAccount + .connect(this.other) + .executeUserOp( + operation.packed, + operation.hash(operation.context.entrypoint.target, operation.context.chainId), + ), + ) .to.be.revertedWithCustomError(this.smartAccount, 'AccountUnauthorized') .withArgs(this.other); }); @@ -247,8 +273,13 @@ function shouldBehaveLikeAnAccountBaseExecutor({ deployable = true } = {}) { .then(op => op.sign(this.domain, this.signer)); await expect(this.entrypoint.connect(this.entrypointAsSigner).handleOps([operation.packed], this.beneficiary)) - .to.emit(this.entrypoint, 'AccountDeployed') - .withArgs(operation.hash, this.smartAccount, this.factory, ethers.ZeroAddress) + // .to.emit(this.entrypoint, 'AccountDeployed') + // .withArgs( + // operation.hash(operation.context.entrypoint.target, operation.context.chainId), + // this.smartAccount, + // this.factory, + // ethers.ZeroAddress, + // ) .to.emit(this.target, 'MockFunctionCalledExtra') .withArgs(this.smartAccount, 17); expect(await this.smartAccount.getNonce()).to.equal(1); diff --git a/test/account/draft-AccountECDSA.js b/test/account/draft-AccountECDSA.test.js similarity index 77% rename from test/account/draft-AccountECDSA.js rename to test/account/draft-AccountECDSA.test.js index 06bc9263..6b66cc79 100644 --- a/test/account/draft-AccountECDSA.js +++ b/test/account/draft-AccountECDSA.test.js @@ -22,7 +22,15 @@ async function fixture() { verifyingContract: smartAccount.address, }; - return { ...helper, domain, smartAccount, signer, target, beneficiary, other }; + return { + ...helper, + domain, + smartAccount, + signer, + target, + beneficiary, + other, + }; } describe('AccountECDSA', function () { @@ -32,13 +40,13 @@ describe('AccountECDSA', function () { shouldBehaveLikeAnAccountBase(); shouldBehaveLikeAnAccountBaseExecutor(); - shouldBehaveLikeAccountHolder(); + // shouldBehaveLikeAccountHolder(); - describe('ERC7739Signer', function () { - beforeEach(async function () { - this.mock = await this.smartAccount.deploy(); - }); + // describe('ERC7739Signer', function () { + // beforeEach(async function () { + // this.mock = await this.smartAccount.deploy(); + // }); - shouldBehaveLikeERC7739Signer(); - }); + // shouldBehaveLikeERC7739Signer(); + // }); }); diff --git a/test/helpers/erc4337.js b/test/helpers/erc4337.js index 1893ce31..96f7c5f3 100644 --- a/test/helpers/erc4337.js +++ b/test/helpers/erc4337.js @@ -1,11 +1,15 @@ const { setCode } = require('@nomicfoundation/hardhat-network-helpers'); const { ethers } = require('hardhat'); +const { UserOperation: UserOperationVanilla } = require('../../lib/@openzeppelin-contracts/test/helpers/erc4337'); +const { PersonalSignHelper } = require('./erc7739'); + +const CANONICAL_ENTRYPOINT = '0x0000000071727De22E5E9d8BAf0edAc6f37da032'; /// Global ERC-4337 environment helper. class ERC4337Helper { constructor(account, params = {}) { this.entrypointAsPromise = ethers.deployContract('EntryPoint'); - this.factoryAsPromise = ethers.deployContract('$Create2'); + this.factoryAsPromise = ethers.deployContract('Create2Mock'); this.accountContractAsPromise = ethers.getContractFactory(account); this.chainIdAsPromise = ethers.provider.getNetwork().then(({ chainId }) => chainId); this.senderCreatorAsPromise = ethers.deployContract('SenderCreator'); @@ -38,6 +42,66 @@ class ERC4337Helper { } } +/// Represent one ERC-4337 account contract. +class SmartAccount extends ethers.BaseContract { + constructor(instance, initCode, context) { + super(instance.target, instance.interface, instance.runner, instance.deployTx); + this.address = instance.target; + this.initCode = initCode; + this.context = context; + } + + async deploy(account = this.runner) { + this.deployTx = await account.sendTransaction({ + to: '0x' + this.initCode.replace(/0x/, '').slice(0, 40), + data: '0x' + this.initCode.replace(/0x/, '').slice(40), + }); + return this; + } + + async createOp(args = {}) { + const params = Object.assign({ sender: this }, args); + // fetch nonce + if (!params.nonce) { + params.nonce = await this.context.entrypointAsPromise.then(entrypoint => entrypoint.getNonce(this, 0)); + } + // prepare paymaster and data + if (ethers.isAddressable(params.paymaster)) { + params.paymaster = await ethers.resolveAddress(params.paymaster); + params.paymasterVerificationGasLimit ??= 100_000n; + params.paymasterPostOpGasLimit ??= 100_000n; + params.paymasterAndData = ethers.solidityPacked( + ['address', 'uint128', 'uint128'], + [params.paymaster, params.paymasterVerificationGasLimit, params.paymasterPostOpGasLimit], + ); + } + return new UserOperation(params); + } +} + +class UserOperation extends UserOperationVanilla { + constructor(params) { + super(params); + this.context = params.sender.context; + this.senderInitCode = params.sender.initCode; + } + + addInitCode() { + this.initCode = this.senderInitCode; + return this; + } + + async sign(domain, signer) { + this.signature = await PersonalSignHelper.sign( + signer.signTypedData, + this.hash(this.context.entrypoint.target, this.context.chainId), + domain, + ); + + return this; + } +} + module.exports = { ERC4337Helper, }; diff --git a/test/helpers/signers.js b/test/helpers/signers.js index 61b3ee36..bdc6fb97 100644 --- a/test/helpers/signers.js +++ b/test/helpers/signers.js @@ -2,6 +2,7 @@ const { ethers } = require('hardhat'); const { secp256k1 } = require('@noble/curves/secp256k1'); const { secp256r1 } = require('@noble/curves/p256'); const { generateKeyPairSync, privateEncrypt } = require('crypto'); +const { hashTypedData } = require('../../lib/@openzeppelin-contracts/test/helpers/eip712'); const ensureLowerOrderS = (N, { s, recovery, ...rest }) => { if (s > N / 2n) { @@ -12,38 +13,16 @@ const ensureLowerOrderS = (N, { s, recovery, ...rest }) => { }; class BooleanSigner { - signPersonal() { - return '0x01'; - } - - signNestedTypedData() { + signTypedData() { return '0x01'; } } class ERC7739Signer { - signPersonal(domain, contents) { - return this._signRaw( - hashTypedData( - domain, - ethers.solidityPackedKeccak256( - ['bytes32', 'bytes32'], - [ - ethers.solidityPackedKeccak256(['string'], ['PersonalSign(bytes prefixed)']), - ethers.solidityPackedKeccak256(['string', 'bytes32'], ['\x19Ethereum Signed Message:\n32', contents]), - ], - ), - ), - ); - } - - signNestedTypedData(localDomain, appDomain, contents, contentsType) { - return this._signRaw(hashTypedData(appDomain, hashNestedTypedDataStruct(localDomain, contents, contentsType))); - } - - wrapTypedDataSig(originalSig, appSeparator, contents, contentsType) { - const contentsTypeLength = ethers.toBeHex(ethers.dataLength(contentsType), 2); - return ethers.concat([originalSig, appSeparator, contents, contentsType, contentsTypeLength]); + signTypedData(domain, types, contents) { + const encoder = ethers.TypedDataEncoder.from(types); + console.log(this); + return this._signRaw(hashTypedData(domain, encoder.hash(contents))); } } diff --git a/test/utils/cryptography/ERC7739Signer.behavior.js b/test/utils/cryptography/ERC7739Signer.behavior.js index a211b0bc..fa44e0f8 100644 --- a/test/utils/cryptography/ERC7739Signer.behavior.js +++ b/test/utils/cryptography/ERC7739Signer.behavior.js @@ -1,6 +1,6 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); -const { Permit, formatType, getDomain } = require('../../../lib/@openzeppelin-contracts/test/helpers/eip712'); +const { Permit, formatType } = require('../../../lib/@openzeppelin-contracts/test/helpers/eip712'); const { PersonalSignHelper, TypedDataSignHelper } = require('../../helpers/erc7739'); function shouldBehaveLikeERC7739Signer() { diff --git a/test/utils/cryptography/draft-ERC7739Signer.test.js b/test/utils/cryptography/draft-ERC7739Signer.test.js index eb82e936..b7833af3 100644 --- a/test/utils/cryptography/draft-ERC7739Signer.test.js +++ b/test/utils/cryptography/draft-ERC7739Signer.test.js @@ -2,24 +2,28 @@ const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { ethers } = require('hardhat'); const { shouldBehaveLikeERC7739Signer } = require('./ERC7739Signer.behavior'); const { ECDSASigner, P256Signer, RSASigner } = require('../../helpers/signers'); +const { getDomain } = require('../../../lib/@openzeppelin-contracts/test/helpers/eip712'); async function fixture() { const ECDSA = new ECDSASigner(); - const ECDSAMock = await ethers.deployContract('ERC7739SignerECDSA', [ECDSA.EOA.address]); + const ECDSAMock = await ethers.deployContract('ERC7739SignerECDSAMock', [ECDSA.EOA.address]); const P256 = new P256Signer(); - const P256Mock = await ethers.deployContract('ERC7739SignerP256', [P256.publicKey.qx, P256.publicKey.qy]); + const P256Mock = await ethers.deployContract('ERC7739SignerP256Mock', [P256.publicKey.qx, P256.publicKey.qy]); const RSA = new RSASigner(); - const RSAMock = await ethers.deployContract('ERC7739SignerRSA', [RSA.publicKey.e, RSA.publicKey.n]); + const RSAMock = await ethers.deployContract('ERC7739SignerRSAMock', [RSA.publicKey.e, RSA.publicKey.n]); return { ECDSA, ECDSAMock, + ECDSAMockDomain: await getDomain(ECDSAMock), P256, P256Mock, + P256MockDomain: await getDomain(P256Mock), RSA, RSAMock, + RSAMockDomain: await getDomain(RSAMock), }; } @@ -30,8 +34,9 @@ describe('ERC7739Signer', function () { describe('for an ECDSA signer', function () { beforeEach(function () { - this.signer = this.ECDSA; this.mock = this.ECDSAMock; + this.domain = this.ECDSAMockDomain; + this.signTypedData = this.ECDSA.signTypedData.bind(this.ECDSA); }); shouldBehaveLikeERC7739Signer(); @@ -39,8 +44,9 @@ describe('ERC7739Signer', function () { describe('for a P256 signer', function () { beforeEach(function () { - this.signer = this.P256; this.mock = this.P256Mock; + this.domain = this.P256MockDomain; + this.signTypedData = this.P256.signTypedData.bind(this.P256); }); shouldBehaveLikeERC7739Signer(); @@ -48,8 +54,9 @@ describe('ERC7739Signer', function () { describe('for an RSA signer', function () { beforeEach(function () { - this.signer = this.RSA; this.mock = this.RSAMock; + this.domain = this.RSAMockDomain; + this.signTypedData = this.RSA.signTypedData.bind(this.RSA); }); shouldBehaveLikeERC7739Signer(); From 292dae1a898a3564a88c4e2491b15b0ca8d3063f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Wed, 4 Dec 2024 21:48:19 -0600 Subject: [PATCH 03/51] Fix lint --- package.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 49d5ea22..2c9ada00 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "license": "MIT", "files": [ "/contracts/**/*.sol", - "!/contracts/mocks/**/*" + "!/contracts/mocks/**/*", + "!/contracts/vendor/erc4337-entrypoint" ], "scripts": { "compile": "hardhat compile", @@ -19,8 +20,8 @@ "lint:fix": "npm run lint:js:fix && npm run lint:sol:fix", "lint:js": "prettier --log-level warn --ignore-path .gitignore '**/*.{js,ts}' --check", "lint:js:fix": "prettier --log-level warn --ignore-path .gitignore '**/*.{js,ts}' --write", - "lint:sol": "prettier --log-level warn --ignore-path .gitignore '{contracts,test}/**/*.sol' --check", - "lint:sol:fix": "prettier --log-level warn --ignore-path .gitignore '{contracts,test}/**/*.sol' --write", + "lint:sol": "prettier --log-level warn --ignore-path .gitignore '{contracts,test}/!(vendor)/**/*.sol' --check", + "lint:sol:fix": "prettier --log-level warn --ignore-path .gitignore '{contracts,test}/!(vendor)/**/*.sol' --write", "coverage": "scripts/checks/coverage.sh", "test": "hardhat test" }, From f8657e72549cdb63b68a9f9ede2b2cdfd19037c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Thu, 5 Dec 2024 16:12:48 -0600 Subject: [PATCH 04/51] Checkpoint --- contracts/mocks/ERC1155Mock.sol | 13 +++++++++++++ test/account/Account.behavior.js | 4 ++-- test/account/draft-AccountECDSA.test.js | 15 ++++++++------- test/account/draft-AccountP256.test.js | 1 + test/account/draft-AccountRSA.test.js | 1 + test/helpers/erc4337.js | 2 +- test/helpers/signers.js | 1 - 7 files changed, 26 insertions(+), 11 deletions(-) create mode 100644 contracts/mocks/ERC1155Mock.sol diff --git a/contracts/mocks/ERC1155Mock.sol b/contracts/mocks/ERC1155Mock.sol new file mode 100644 index 00000000..45b0bdca --- /dev/null +++ b/contracts/mocks/ERC1155Mock.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {ERC1155} from "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; + +contract ERC1155Mock is ERC1155 { + constructor(string memory _uri) ERC1155(_uri) {} + + function $mintBatch(address to, uint256[] memory ids, uint256[] memory values, bytes memory data) external virtual { + _mintBatch(to, ids, values, data); + } +} diff --git a/test/account/Account.behavior.js b/test/account/Account.behavior.js index b9e9b1f4..c47f8de6 100644 --- a/test/account/Account.behavior.js +++ b/test/account/Account.behavior.js @@ -171,8 +171,8 @@ function shouldBehaveLikeAccountHolder() { beforeEach(async function () { [this.owner] = await ethers.getSigners(); - this.token = await ethers.deployContract('$ERC1155', ['https://somedomain.com/{id}.json']); - await this.token.$_mintBatch(this.owner, ids, values, '0x'); + this.token = await ethers.deployContract('ERC1155Mock', ['https://somedomain.com/{id}.json']); + await this.token.$mintBatch(this.owner, ids, values, '0x'); }); it('receives ERC1155 tokens from a single ID', async function () { diff --git a/test/account/draft-AccountECDSA.test.js b/test/account/draft-AccountECDSA.test.js index 6b66cc79..7e795d45 100644 --- a/test/account/draft-AccountECDSA.test.js +++ b/test/account/draft-AccountECDSA.test.js @@ -40,13 +40,14 @@ describe('AccountECDSA', function () { shouldBehaveLikeAnAccountBase(); shouldBehaveLikeAnAccountBaseExecutor(); - // shouldBehaveLikeAccountHolder(); + shouldBehaveLikeAccountHolder(); - // describe('ERC7739Signer', function () { - // beforeEach(async function () { - // this.mock = await this.smartAccount.deploy(); - // }); + describe('ERC7739Signer', function () { + beforeEach(async function () { + this.mock = await this.smartAccount.deploy(); + this.signTypedData = this.signer.signTypedData.bind(this.signer); + }); - // shouldBehaveLikeERC7739Signer(); - // }); + shouldBehaveLikeERC7739Signer(); + }); }); diff --git a/test/account/draft-AccountP256.test.js b/test/account/draft-AccountP256.test.js index b3d45289..540208d5 100644 --- a/test/account/draft-AccountP256.test.js +++ b/test/account/draft-AccountP256.test.js @@ -37,6 +37,7 @@ describe('AccountP256', function () { describe('ERC7739Signer', function () { beforeEach(async function () { this.mock = await this.smartAccount.deploy(); + this.signTypedData = this.signer.signTypedData.bind(this.signer); }); shouldBehaveLikeERC7739Signer(); diff --git a/test/account/draft-AccountRSA.test.js b/test/account/draft-AccountRSA.test.js index 66813a07..a6203b80 100644 --- a/test/account/draft-AccountRSA.test.js +++ b/test/account/draft-AccountRSA.test.js @@ -37,6 +37,7 @@ describe('AccountRSA', function () { describe('ERC7739Signer', function () { beforeEach(async function () { this.mock = await this.smartAccount.deploy(); + this.signTypedData = this.signer.signTypedData.bind(this.signer); }); shouldBehaveLikeERC7739Signer(); diff --git a/test/helpers/erc4337.js b/test/helpers/erc4337.js index 96f7c5f3..90e3979d 100644 --- a/test/helpers/erc4337.js +++ b/test/helpers/erc4337.js @@ -93,7 +93,7 @@ class UserOperation extends UserOperationVanilla { async sign(domain, signer) { this.signature = await PersonalSignHelper.sign( - signer.signTypedData, + signer.signTypedData.bind(signer), this.hash(this.context.entrypoint.target, this.context.chainId), domain, ); diff --git a/test/helpers/signers.js b/test/helpers/signers.js index bdc6fb97..f043ecf5 100644 --- a/test/helpers/signers.js +++ b/test/helpers/signers.js @@ -21,7 +21,6 @@ class BooleanSigner { class ERC7739Signer { signTypedData(domain, types, contents) { const encoder = ethers.TypedDataEncoder.from(types); - console.log(this); return this._signRaw(hashTypedData(domain, encoder.hash(contents))); } } From 86992321c434fb24837feebd22f74a6f2c1c814b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Thu, 5 Dec 2024 18:01:44 -0600 Subject: [PATCH 05/51] up --- test/account/Account.behavior.js | 106 ++++++++++++------------ test/account/draft-AccountECDSA.test.js | 4 +- test/account/draft-AccountP256.test.js | 4 +- test/account/draft-AccountRSA.test.js | 4 +- 4 files changed, 60 insertions(+), 58 deletions(-) diff --git a/test/account/Account.behavior.js b/test/account/Account.behavior.js index c47f8de6..9b9353f7 100644 --- a/test/account/Account.behavior.js +++ b/test/account/Account.behavior.js @@ -13,20 +13,20 @@ const { function shouldBehaveLikeAnAccountBase() { describe('entryPoint', function () { it('should return the canonical entrypoint', async function () { - await this.smartAccount.deploy(); - expect(await this.smartAccount.entryPoint()).to.equal(this.entrypoint.target); + await this.mock.deploy(); + expect(await this.mock.entryPoint()).to.equal(this.entrypoint.target); }); }); describe('validateUserOp', function () { beforeEach(async function () { - await setBalance(this.smartAccount.target, ethers.parseEther('1')); - await this.smartAccount.deploy(); + await setBalance(this.mock.target, ethers.parseEther('1')); + await this.mock.deploy(); }); it('should revert if the caller is not the canonical entrypoint', async function () { - const selector = this.smartAccount.interface.getFunction('executeUserOp').selector; - const operation = await this.smartAccount + const selector = this.mock.interface.getFunction('executeUserOp').selector; + const operation = await this.mock .createOp({ callData: ethers.concat([ selector, @@ -38,7 +38,7 @@ function shouldBehaveLikeAnAccountBase() { }) .then(op => op.sign(this.domain, this.signer)); await expect( - this.smartAccount + this.mock .connect(this.other) .validateUserOp( operation.packed, @@ -46,7 +46,7 @@ function shouldBehaveLikeAnAccountBase() { 0, ), ) - .to.be.revertedWithCustomError(this.smartAccount, 'AccountUnauthorized') + .to.be.revertedWithCustomError(this.mock, 'AccountUnauthorized') .withArgs(this.other); }); @@ -56,8 +56,8 @@ function shouldBehaveLikeAnAccountBase() { }); it('should return SIG_VALIDATION_SUCCESS if the signature is valid', async function () { - const selector = this.smartAccount.interface.getFunction('executeUserOp').selector; - const operation = await this.smartAccount + const selector = this.mock.interface.getFunction('executeUserOp').selector; + const operation = await this.mock .createOp({ callData: ethers.concat([ selector, @@ -70,7 +70,7 @@ function shouldBehaveLikeAnAccountBase() { .then(op => op.sign(this.domain, this.signer)); expect( - await this.smartAccount + await this.mock .connect(this.entrypointAsSigner) .validateUserOp.staticCall( operation.packed, @@ -81,8 +81,8 @@ function shouldBehaveLikeAnAccountBase() { }); it('should return SIG_VALIDATION_FAILURE if the signature is invalid', async function () { - const selector = this.smartAccount.interface.getFunction('executeUserOp').selector; - const operation = await this.smartAccount.createOp({ + const selector = this.mock.interface.getFunction('executeUserOp').selector; + const operation = await this.mock.createOp({ callData: ethers.concat([ selector, ethers.AbiCoder.defaultAbiCoder().encode( @@ -95,7 +95,7 @@ function shouldBehaveLikeAnAccountBase() { operation.signature = '0x00'; expect( - await this.smartAccount + await this.mock .connect(this.entrypointAsSigner) .validateUserOp.staticCall( operation.packed, @@ -106,8 +106,8 @@ function shouldBehaveLikeAnAccountBase() { }); it('should pay missing account funds for execution', async function () { - const selector = this.smartAccount.interface.getFunction('executeUserOp').selector; - const operation = await this.smartAccount + const selector = this.mock.interface.getFunction('executeUserOp').selector; + const operation = await this.mock .createOp({ callData: ethers.concat([ selector, @@ -119,11 +119,11 @@ function shouldBehaveLikeAnAccountBase() { }) .then(op => op.sign(this.domain, this.signer)); - const prevAccountBalance = await ethers.provider.getBalance(this.smartAccount.target); + const prevAccountBalance = await ethers.provider.getBalance(this.mock.target); const prevEntrypointBalance = await ethers.provider.getBalance(this.entrypoint.target); const amount = ethers.parseEther('0.1'); - const tx = await this.smartAccount + const tx = await this.mock .connect(this.entrypointAsSigner) .validateUserOp( operation.packed, @@ -134,7 +134,7 @@ function shouldBehaveLikeAnAccountBase() { const receipt = await tx.wait(); const callerFees = receipt.gasUsed * tx.gasPrice; - expect(await ethers.provider.getBalance(this.smartAccount.target)).to.equal(prevAccountBalance - amount); + expect(await ethers.provider.getBalance(this.mock.target)).to.equal(prevAccountBalance - amount); expect(await ethers.provider.getBalance(this.entrypoint.target)).to.equal( prevEntrypointBalance + amount - callerFees, ); @@ -144,14 +144,14 @@ function shouldBehaveLikeAnAccountBase() { describe('fallback', function () { it('should receive ether', async function () { - await this.smartAccount.deploy(); + await this.mock.deploy(); await setBalance(this.other.address, ethers.parseEther('1')); - const prevBalance = await ethers.provider.getBalance(this.smartAccount.target); + const prevBalance = await ethers.provider.getBalance(this.mock.target); const amount = ethers.parseEther('0.1'); - await this.other.sendTransaction({ to: this.smartAccount.target, value: amount }); + await this.other.sendTransaction({ to: this.mock.target, value: amount }); - expect(await ethers.provider.getBalance(this.smartAccount.target)).to.equal(prevBalance + amount); + expect(await ethers.provider.getBalance(this.mock.target)).to.equal(prevBalance + amount); }); }); } @@ -159,7 +159,7 @@ function shouldBehaveLikeAnAccountBase() { function shouldBehaveLikeAccountHolder() { describe('onReceived', function () { beforeEach(async function () { - await this.smartAccount.deploy(); + await this.mock.deploy(); }); shouldSupportInterfaces(['ERC1155Receiver']); @@ -176,24 +176,24 @@ function shouldBehaveLikeAccountHolder() { }); it('receives ERC1155 tokens from a single ID', async function () { - await this.token.connect(this.owner).safeTransferFrom(this.owner, this.smartAccount, ids[0], values[0], data); - expect(await this.token.balanceOf(this.smartAccount, ids[0])).to.equal(values[0]); + await this.token.connect(this.owner).safeTransferFrom(this.owner, this.mock, ids[0], values[0], data); + expect(await this.token.balanceOf(this.mock, ids[0])).to.equal(values[0]); for (let i = 1; i < ids.length; i++) { - expect(await this.token.balanceOf(this.smartAccount, ids[i])).to.equal(0n); + expect(await this.token.balanceOf(this.mock, ids[i])).to.equal(0n); } }); it('receives ERC1155 tokens from a multiple IDs', async function () { expect( await this.token.balanceOfBatch( - ids.map(() => this.smartAccount), + ids.map(() => this.mock), ids, ), ).to.deep.equal(ids.map(() => 0n)); - await this.token.connect(this.owner).safeBatchTransferFrom(this.owner, this.smartAccount, ids, values, data); + await this.token.connect(this.owner).safeBatchTransferFrom(this.owner, this.mock, ids, values, data); expect( await this.token.balanceOfBatch( - ids.map(() => this.smartAccount), + ids.map(() => this.mock), ids, ), ).to.deep.equal(values); @@ -211,9 +211,9 @@ function shouldBehaveLikeAccountHolder() { const token = await ethers.deployContract('ERC721Mock', [name, symbol]); await token.$mint(owner, tokenId); - await token.connect(owner).safeTransferFrom(owner, this.smartAccount, tokenId); + await token.connect(owner).safeTransferFrom(owner, this.mock, tokenId); - expect(await token.ownerOf(tokenId)).to.equal(this.smartAccount.target); + expect(await token.ownerOf(tokenId)).to.equal(this.mock.target); }); }); }); @@ -222,16 +222,16 @@ function shouldBehaveLikeAccountHolder() { function shouldBehaveLikeAnAccountBaseExecutor({ deployable = true } = {}) { describe('executeUserOp', function () { beforeEach(async function () { - await setBalance(this.smartAccount.target, ethers.parseEther('1')); - expect(await ethers.provider.getCode(this.smartAccount.target)).to.equal('0x'); + await setBalance(this.mock.target, ethers.parseEther('1')); + expect(await ethers.provider.getCode(this.mock.target)).to.equal('0x'); this.entrypointAsSigner = await impersonate(this.entrypoint.target); }); it('should revert if the caller is not the canonical entrypoint or the account itself', async function () { - await this.smartAccount.deploy(); + await this.mock.deploy(); - const selector = this.smartAccount.interface.getFunction('executeUserOp').selector; - const operation = await this.smartAccount + const selector = this.mock.interface.getFunction('executeUserOp').selector; + const operation = await this.mock .createOp({ callData: ethers.concat([ selector, @@ -244,22 +244,24 @@ function shouldBehaveLikeAnAccountBaseExecutor({ deployable = true } = {}) { .then(op => op.sign(this.domain, this.signer)); await expect( - this.smartAccount + this.mock .connect(this.other) .executeUserOp( operation.packed, operation.hash(operation.context.entrypoint.target, operation.context.chainId), ), ) - .to.be.revertedWithCustomError(this.smartAccount, 'AccountUnauthorized') + .to.be.revertedWithCustomError(this.mock, 'AccountUnauthorized') .withArgs(this.other); }); if (deployable) { describe('when not deployed', function () { it('should be created with handleOps and increase nonce', async function () { - const selector = this.smartAccount.interface.getFunction('executeUserOp').selector; - const operation = await this.smartAccount + await this.mock.deploy(); + + const selector = this.mock.interface.getFunction('executeUserOp').selector; + const operation = await this.mock .createOp({ callData: ethers.concat([ selector, @@ -276,18 +278,18 @@ function shouldBehaveLikeAnAccountBaseExecutor({ deployable = true } = {}) { // .to.emit(this.entrypoint, 'AccountDeployed') // .withArgs( // operation.hash(operation.context.entrypoint.target, operation.context.chainId), - // this.smartAccount, + // this.mock, // this.factory, // ethers.ZeroAddress, // ) .to.emit(this.target, 'MockFunctionCalledExtra') - .withArgs(this.smartAccount, 17); - expect(await this.smartAccount.getNonce()).to.equal(1); + .withArgs(this.mock, 17); + expect(await this.mock.getNonce()).to.equal(1); }); it('should revert if the signature is invalid', async function () { - const selector = this.smartAccount.interface.getFunction('executeUserOp').selector; - const operation = await this.smartAccount + const selector = this.mock.interface.getFunction('executeUserOp').selector; + const operation = await this.mock .createOp({ callData: ethers.concat([ selector, @@ -309,12 +311,12 @@ function shouldBehaveLikeAnAccountBaseExecutor({ deployable = true } = {}) { describe('when deployed', function () { beforeEach(async function () { - await this.smartAccount.deploy(); + await this.mock.deploy(); }); it('should increase nonce and call target', async function () { - const selector = this.smartAccount.interface.getFunction('executeUserOp').selector; - const operation = await this.smartAccount + const selector = this.mock.interface.getFunction('executeUserOp').selector; + const operation = await this.mock .createOp({ callData: ethers.concat([ selector, @@ -326,11 +328,11 @@ function shouldBehaveLikeAnAccountBaseExecutor({ deployable = true } = {}) { }) .then(op => op.sign(this.domain, this.signer)); - expect(await this.smartAccount.getNonce()).to.equal(0); + expect(await this.mock.getNonce()).to.equal(0); await expect(this.entrypoint.connect(this.entrypointAsSigner).handleOps([operation.packed], this.beneficiary)) .to.emit(this.target, 'MockFunctionCalledExtra') - .withArgs(this.smartAccount, 42); - expect(await this.smartAccount.getNonce()).to.equal(1); + .withArgs(this.mock, 42); + expect(await this.mock.getNonce()).to.equal(1); }); }); }); diff --git a/test/account/draft-AccountECDSA.test.js b/test/account/draft-AccountECDSA.test.js index 7e795d45..c8f3cc59 100644 --- a/test/account/draft-AccountECDSA.test.js +++ b/test/account/draft-AccountECDSA.test.js @@ -25,7 +25,7 @@ async function fixture() { return { ...helper, domain, - smartAccount, + mock: smartAccount, signer, target, beneficiary, @@ -44,7 +44,7 @@ describe('AccountECDSA', function () { describe('ERC7739Signer', function () { beforeEach(async function () { - this.mock = await this.smartAccount.deploy(); + this.mock = await this.mock.deploy(); this.signTypedData = this.signer.signTypedData.bind(this.signer); }); diff --git a/test/account/draft-AccountP256.test.js b/test/account/draft-AccountP256.test.js index 540208d5..1dabc6ca 100644 --- a/test/account/draft-AccountP256.test.js +++ b/test/account/draft-AccountP256.test.js @@ -22,7 +22,7 @@ async function fixture() { verifyingContract: smartAccount.address, }; - return { ...helper, domain, smartAccount, signer, target, beneficiary, other }; + return { ...helper, domain, mock: smartAccount, signer, target, beneficiary, other }; } describe('AccountP256', function () { @@ -36,7 +36,7 @@ describe('AccountP256', function () { describe('ERC7739Signer', function () { beforeEach(async function () { - this.mock = await this.smartAccount.deploy(); + this.mock = await this.mock.deploy(); this.signTypedData = this.signer.signTypedData.bind(this.signer); }); diff --git a/test/account/draft-AccountRSA.test.js b/test/account/draft-AccountRSA.test.js index a6203b80..8cd1e70f 100644 --- a/test/account/draft-AccountRSA.test.js +++ b/test/account/draft-AccountRSA.test.js @@ -22,7 +22,7 @@ async function fixture() { verifyingContract: smartAccount.address, }; - return { ...helper, domain, smartAccount, signer, target, beneficiary, other }; + return { ...helper, domain, mock: smartAccount, signer, target, beneficiary, other }; } describe('AccountRSA', function () { @@ -36,7 +36,7 @@ describe('AccountRSA', function () { describe('ERC7739Signer', function () { beforeEach(async function () { - this.mock = await this.smartAccount.deploy(); + this.mock = await this.mock.deploy(); this.signTypedData = this.signer.signTypedData.bind(this.signer); }); From 7bdf69e54772e62dfd6553c8a724b0d5c0884ea7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Thu, 5 Dec 2024 18:27:28 -0600 Subject: [PATCH 06/51] up --- test/account/Account.behavior.js | 16 +++++++--------- test/account/draft-AccountBase.test.js | 2 +- test/helpers/erc4337.js | 12 ++++++++---- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/test/account/Account.behavior.js b/test/account/Account.behavior.js index 9b9353f7..bfebc8ba 100644 --- a/test/account/Account.behavior.js +++ b/test/account/Account.behavior.js @@ -258,8 +258,6 @@ function shouldBehaveLikeAnAccountBaseExecutor({ deployable = true } = {}) { if (deployable) { describe('when not deployed', function () { it('should be created with handleOps and increase nonce', async function () { - await this.mock.deploy(); - const selector = this.mock.interface.getFunction('executeUserOp').selector; const operation = await this.mock .createOp({ @@ -275,13 +273,13 @@ function shouldBehaveLikeAnAccountBaseExecutor({ deployable = true } = {}) { .then(op => op.sign(this.domain, this.signer)); await expect(this.entrypoint.connect(this.entrypointAsSigner).handleOps([operation.packed], this.beneficiary)) - // .to.emit(this.entrypoint, 'AccountDeployed') - // .withArgs( - // operation.hash(operation.context.entrypoint.target, operation.context.chainId), - // this.mock, - // this.factory, - // ethers.ZeroAddress, - // ) + .to.emit(this.entrypoint, 'AccountDeployed') + .withArgs( + operation.hash(operation.context.entrypoint.target, operation.context.chainId), + this.mock, + this.factory, + ethers.ZeroAddress, + ) .to.emit(this.target, 'MockFunctionCalledExtra') .withArgs(this.mock, 17); expect(await this.mock.getNonce()).to.equal(1); diff --git a/test/account/draft-AccountBase.test.js b/test/account/draft-AccountBase.test.js index 3099417e..80c08dc5 100644 --- a/test/account/draft-AccountBase.test.js +++ b/test/account/draft-AccountBase.test.js @@ -11,7 +11,7 @@ async function fixture() { const helper = new ERC4337Helper('$AccountBaseMock'); const smartAccount = await helper.newAccount(); - return { ...helper, smartAccount, signer, target, beneficiary, other }; + return { ...helper, mock: smartAccount, signer, target, beneficiary, other }; } describe('AccountBase', function () { diff --git a/test/helpers/erc4337.js b/test/helpers/erc4337.js index 90e3979d..4e7ba86f 100644 --- a/test/helpers/erc4337.js +++ b/test/helpers/erc4337.js @@ -48,13 +48,15 @@ class SmartAccount extends ethers.BaseContract { super(instance.target, instance.interface, instance.runner, instance.deployTx); this.address = instance.target; this.initCode = initCode; + this.factory = '0x' + initCode.replace(/0x/, '').slice(0, 40); + this.factoryData = '0x' + initCode.replace(/0x/, '').slice(40); this.context = context; } async deploy(account = this.runner) { this.deployTx = await account.sendTransaction({ - to: '0x' + this.initCode.replace(/0x/, '').slice(0, 40), - data: '0x' + this.initCode.replace(/0x/, '').slice(40), + to: this.factory, + data: this.factoryData, }); return this; } @@ -83,11 +85,13 @@ class UserOperation extends UserOperationVanilla { constructor(params) { super(params); this.context = params.sender.context; - this.senderInitCode = params.sender.initCode; + this.senderFactory = params.sender.factory; + this.senderFactoryData = params.sender.factoryData; } addInitCode() { - this.initCode = this.senderInitCode; + this.factory = this.senderFactory; + this.factoryData = this.senderFactoryData; return this; } From efd52cd8ec8b4911766e976ba40f0704cdbd16fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Thu, 5 Dec 2024 18:48:28 -0600 Subject: [PATCH 07/51] Adjust --- contracts/account/draft-AccountECDSA.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/account/draft-AccountECDSA.sol b/contracts/account/draft-AccountECDSA.sol index c5dd6487..20ba6d9c 100644 --- a/contracts/account/draft-AccountECDSA.sol +++ b/contracts/account/draft-AccountECDSA.sol @@ -42,7 +42,7 @@ abstract contract AccountECDSA is ERC165, ERC7739Signer, ERC721Holder, ERC1155Ho bytes32 userOpHash ) internal view override returns (uint256) { return - _validateNestedEIP712Signature(userOpHash, userOp.signature) + _isValidSignature(userOpHash, userOp.signature) ? ERC4337Utils.SIG_VALIDATION_SUCCESS : ERC4337Utils.SIG_VALIDATION_FAILED; } From 87fcd0e7cf3539698bfa9793ed7fafa56c437abe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Fri, 6 Dec 2024 15:59:33 -0600 Subject: [PATCH 08/51] up --- contracts/account/draft-AccountECDSA.sol | 7 ++-- contracts/account/draft-AccountP256.sol | 7 ++-- contracts/account/draft-AccountRSA.sol | 7 ++-- test/account/Account.behavior.js | 44 ++++-------------------- test/helpers/erc4337.js | 7 ++-- test/helpers/signers.js | 3 +- 6 files changed, 27 insertions(+), 48 deletions(-) diff --git a/contracts/account/draft-AccountECDSA.sol b/contracts/account/draft-AccountECDSA.sol index 20ba6d9c..d53b913f 100644 --- a/contracts/account/draft-AccountECDSA.sol +++ b/contracts/account/draft-AccountECDSA.sol @@ -9,6 +9,7 @@ import {ERC1155HolderLean, IERC1155Receiver} from "../token/ERC1155/utils/ERC115 import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import {AccountBase} from "./draft-AccountBase.sol"; import {ERC7739Signer} from "../utils/cryptography/draft-ERC7739Signer.sol"; @@ -16,6 +17,8 @@ import {ERC7739Signer} from "../utils/cryptography/draft-ERC7739Signer.sol"; * @dev Account implementation using {ECDSA} signatures and {ERC7739Signer} for replay protection. */ abstract contract AccountECDSA is ERC165, ERC7739Signer, ERC721Holder, ERC1155HolderLean, AccountBase { + using MessageHashUtils for bytes32; + address private immutable _signer; /** @@ -40,9 +43,9 @@ abstract contract AccountECDSA is ERC165, ERC7739Signer, ERC721Holder, ERC1155Ho function _validateUserOp( PackedUserOperation calldata userOp, bytes32 userOpHash - ) internal view override returns (uint256) { + ) internal view virtual override returns (uint256) { return - _isValidSignature(userOpHash, userOp.signature) + _isValidSignature(userOpHash.toEthSignedMessageHash(), userOp.signature) ? ERC4337Utils.SIG_VALIDATION_SUCCESS : ERC4337Utils.SIG_VALIDATION_FAILED; } diff --git a/contracts/account/draft-AccountP256.sol b/contracts/account/draft-AccountP256.sol index 1804517f..ab8576fa 100644 --- a/contracts/account/draft-AccountP256.sol +++ b/contracts/account/draft-AccountP256.sol @@ -9,6 +9,7 @@ import {ERC1155HolderLean, IERC1155Receiver} from "../token/ERC1155/utils/ERC115 import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import {P256} from "@openzeppelin/contracts/utils/cryptography/P256.sol"; +import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import {AccountBase} from "./draft-AccountBase.sol"; import {ERC7739Signer} from "../utils/cryptography/draft-ERC7739Signer.sol"; @@ -16,6 +17,8 @@ import {ERC7739Signer} from "../utils/cryptography/draft-ERC7739Signer.sol"; * @dev Account implementation using {P256} signatures and {ERC7739Signer} for replay protection. */ abstract contract AccountP256 is ERC165, ERC7739Signer, ERC721Holder, ERC1155HolderLean, AccountBase { + using MessageHashUtils for bytes32; + bytes32 private immutable _qx; bytes32 private immutable _qy; @@ -42,9 +45,9 @@ abstract contract AccountP256 is ERC165, ERC7739Signer, ERC721Holder, ERC1155Hol function _validateUserOp( PackedUserOperation calldata userOp, bytes32 userOpHash - ) internal view override returns (uint256) { + ) internal view virtual override returns (uint256) { return - _isValidSignature(userOpHash, userOp.signature) + _isValidSignature(userOpHash.toEthSignedMessageHash(), userOp.signature) ? ERC4337Utils.SIG_VALIDATION_SUCCESS : ERC4337Utils.SIG_VALIDATION_FAILED; } diff --git a/contracts/account/draft-AccountRSA.sol b/contracts/account/draft-AccountRSA.sol index f79cee45..bd57e30d 100644 --- a/contracts/account/draft-AccountRSA.sol +++ b/contracts/account/draft-AccountRSA.sol @@ -9,6 +9,7 @@ import {ERC1155HolderLean, IERC1155Receiver} from "../token/ERC1155/utils/ERC115 import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import {RSA} from "@openzeppelin/contracts/utils/cryptography/RSA.sol"; +import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import {AccountBase} from "./draft-AccountBase.sol"; import {ERC7739Signer} from "../utils/cryptography/draft-ERC7739Signer.sol"; @@ -21,6 +22,8 @@ import {ERC7739Signer} from "../utils/cryptography/draft-ERC7739Signer.sol"; * (see {Clones-cloneDeterministicWithImmutableArgs}). */ abstract contract AccountRSA is ERC165, ERC7739Signer, ERC721Holder, ERC1155HolderLean, AccountBase { + using MessageHashUtils for bytes32; + bytes private _e; bytes private _n; @@ -47,9 +50,9 @@ abstract contract AccountRSA is ERC165, ERC7739Signer, ERC721Holder, ERC1155Hold function _validateUserOp( PackedUserOperation calldata userOp, bytes32 userOpHash - ) internal view override returns (uint256) { + ) internal view virtual override returns (uint256) { return - _isValidSignature(userOpHash, userOp.signature) + _isValidSignature(userOpHash.toEthSignedMessageHash(), userOp.signature) ? ERC4337Utils.SIG_VALIDATION_SUCCESS : ERC4337Utils.SIG_VALIDATION_FAILED; } diff --git a/test/account/Account.behavior.js b/test/account/Account.behavior.js index bfebc8ba..b741b0d9 100644 --- a/test/account/Account.behavior.js +++ b/test/account/Account.behavior.js @@ -37,15 +37,7 @@ function shouldBehaveLikeAnAccountBase() { ]), }) .then(op => op.sign(this.domain, this.signer)); - await expect( - this.mock - .connect(this.other) - .validateUserOp( - operation.packed, - operation.hash(operation.context.entrypoint.target, operation.context.chainId), - 0, - ), - ) + await expect(this.mock.connect(this.other).validateUserOp(operation.packed, operation.hash(), 0)) .to.be.revertedWithCustomError(this.mock, 'AccountUnauthorized') .withArgs(this.other); }); @@ -72,11 +64,7 @@ function shouldBehaveLikeAnAccountBase() { expect( await this.mock .connect(this.entrypointAsSigner) - .validateUserOp.staticCall( - operation.packed, - operation.hash(operation.context.entrypoint.target, operation.context.chainId), - 0, - ), + .validateUserOp.staticCall(operation.packed, operation.hash(), 0), ).to.eq(SIG_VALIDATION_SUCCESS); }); @@ -97,11 +85,7 @@ function shouldBehaveLikeAnAccountBase() { expect( await this.mock .connect(this.entrypointAsSigner) - .validateUserOp.staticCall( - operation.packed, - operation.hash(operation.context.entrypoint.target, operation.context.chainId), - 0, - ), + .validateUserOp.staticCall(operation.packed, operation.hash(), 0), ).to.eq(SIG_VALIDATION_FAILURE); }); @@ -125,11 +109,7 @@ function shouldBehaveLikeAnAccountBase() { const tx = await this.mock .connect(this.entrypointAsSigner) - .validateUserOp( - operation.packed, - operation.hash(operation.context.entrypoint.target, operation.context.chainId), - amount, - ); + .validateUserOp(operation.packed, operation.hash(), amount); const receipt = await tx.wait(); const callerFees = receipt.gasUsed * tx.gasPrice; @@ -243,14 +223,7 @@ function shouldBehaveLikeAnAccountBaseExecutor({ deployable = true } = {}) { }) .then(op => op.sign(this.domain, this.signer)); - await expect( - this.mock - .connect(this.other) - .executeUserOp( - operation.packed, - operation.hash(operation.context.entrypoint.target, operation.context.chainId), - ), - ) + await expect(this.mock.connect(this.other).executeUserOp(operation.packed, operation.hash())) .to.be.revertedWithCustomError(this.mock, 'AccountUnauthorized') .withArgs(this.other); }); @@ -274,12 +247,7 @@ function shouldBehaveLikeAnAccountBaseExecutor({ deployable = true } = {}) { await expect(this.entrypoint.connect(this.entrypointAsSigner).handleOps([operation.packed], this.beneficiary)) .to.emit(this.entrypoint, 'AccountDeployed') - .withArgs( - operation.hash(operation.context.entrypoint.target, operation.context.chainId), - this.mock, - this.factory, - ethers.ZeroAddress, - ) + .withArgs(operation.hash(), this.mock, this.factory, ethers.ZeroAddress) .to.emit(this.target, 'MockFunctionCalledExtra') .withArgs(this.mock, 17); expect(await this.mock.getNonce()).to.equal(1); diff --git a/test/helpers/erc4337.js b/test/helpers/erc4337.js index 4e7ba86f..2865b49a 100644 --- a/test/helpers/erc4337.js +++ b/test/helpers/erc4337.js @@ -95,13 +95,16 @@ class UserOperation extends UserOperationVanilla { return this; } + hash() { + return super.hash(this.context.entrypoint.target, this.context.chainId); + } + async sign(domain, signer) { this.signature = await PersonalSignHelper.sign( signer.signTypedData.bind(signer), - this.hash(this.context.entrypoint.target, this.context.chainId), + ethers.getBytes(this.hash()), domain, ); - return this; } } diff --git a/test/helpers/signers.js b/test/helpers/signers.js index f043ecf5..27ec908b 100644 --- a/test/helpers/signers.js +++ b/test/helpers/signers.js @@ -20,8 +20,7 @@ class BooleanSigner { class ERC7739Signer { signTypedData(domain, types, contents) { - const encoder = ethers.TypedDataEncoder.from(types); - return this._signRaw(hashTypedData(domain, encoder.hash(contents))); + return this._signRaw(hashTypedData(domain, ethers.TypedDataEncoder.from(types).hash(contents))); } } From 549ab8eadff85f346ae252717ea4775818e1de94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Fri, 6 Dec 2024 16:01:45 -0600 Subject: [PATCH 09/51] Simplify CallReceiverMock --- contracts/mocks/CallReceiverMock.sol | 71 ++----------------------- test/account/draft-AccountBase.test.js | 2 +- test/account/draft-AccountECDSA.test.js | 2 +- test/account/draft-AccountP256.test.js | 2 +- test/account/draft-AccountRSA.test.js | 2 +- 5 files changed, 7 insertions(+), 72 deletions(-) diff --git a/contracts/mocks/CallReceiverMock.sol b/contracts/mocks/CallReceiverMock.sol index 7f6bfd0b..70a00ca9 100644 --- a/contracts/mocks/CallReceiverMock.sol +++ b/contracts/mocks/CallReceiverMock.sol @@ -2,77 +2,12 @@ pragma solidity ^0.8.20; -contract CallReceiverMock { - event MockFunctionCalled(); - event MockFunctionCalledWithArgs(uint256 a, uint256 b); - event MockFunctionCalledExtra(address caller, uint256 value); - - uint256[] private _array; +import {CallReceiverMock} from "@openzeppelin/contracts/mocks/CallReceiverMock.sol"; - function mockFunction() public payable returns (string memory) { - emit MockFunctionCalled(); - - return "0x1234"; - } +contract CallReceiverMockExtended is CallReceiverMock { + event MockFunctionCalledExtra(address caller, uint256 value); function mockFunctionExtra() public payable { emit MockFunctionCalledExtra(msg.sender, msg.value); } - - function mockFunctionEmptyReturn() public payable { - emit MockFunctionCalled(); - } - - function mockFunctionWithArgs(uint256 a, uint256 b) public payable returns (string memory) { - emit MockFunctionCalledWithArgs(a, b); - - return "0x1234"; - } - - function mockFunctionNonPayable() public returns (string memory) { - emit MockFunctionCalled(); - - return "0x1234"; - } - - function mockStaticFunction() public pure returns (string memory) { - return "0x1234"; - } - - function mockFunctionRevertsNoReason() public payable { - revert(); - } - - function mockFunctionRevertsReason() public payable { - revert("CallReceiverMock: reverting"); - } - - function mockFunctionThrows() public payable { - assert(false); - } - - function mockFunctionOutOfGas() public payable { - for (uint256 i = 0; ; ++i) { - _array.push(i); - } - } - - function mockFunctionWritesStorage(bytes32 slot, bytes32 value) public returns (string memory) { - assembly { - sstore(slot, value) - } - return "0x1234"; - } -} - -contract CallReceiverMockTrustingForwarder is CallReceiverMock { - address private _trustedForwarder; - - constructor(address trustedForwarder_) { - _trustedForwarder = trustedForwarder_; - } - - function isTrustedForwarder(address forwarder) public view virtual returns (bool) { - return forwarder == _trustedForwarder; - } } diff --git a/test/account/draft-AccountBase.test.js b/test/account/draft-AccountBase.test.js index 80c08dc5..7744089c 100644 --- a/test/account/draft-AccountBase.test.js +++ b/test/account/draft-AccountBase.test.js @@ -6,7 +6,7 @@ const { BooleanSigner } = require('../helpers/signers'); async function fixture() { const [beneficiary, other] = await ethers.getSigners(); - const target = await ethers.deployContract('CallReceiverMock'); + const target = await ethers.deployContract('CallReceiverMockExtended'); const signer = new BooleanSigner(); const helper = new ERC4337Helper('$AccountBaseMock'); const smartAccount = await helper.newAccount(); diff --git a/test/account/draft-AccountECDSA.test.js b/test/account/draft-AccountECDSA.test.js index c8f3cc59..3eb86f64 100644 --- a/test/account/draft-AccountECDSA.test.js +++ b/test/account/draft-AccountECDSA.test.js @@ -11,7 +11,7 @@ const { shouldBehaveLikeERC7739Signer } = require('../utils/cryptography/ERC7739 async function fixture() { const [beneficiary, other] = await ethers.getSigners(); - const target = await ethers.deployContract('CallReceiverMock'); + const target = await ethers.deployContract('CallReceiverMockExtended'); const signer = new ECDSASigner(); const helper = new ERC4337Helper('$AccountECDSA'); const smartAccount = await helper.newAccount(['AccountECDSA', '1', signer.EOA.address]); diff --git a/test/account/draft-AccountP256.test.js b/test/account/draft-AccountP256.test.js index 1dabc6ca..23d9c275 100644 --- a/test/account/draft-AccountP256.test.js +++ b/test/account/draft-AccountP256.test.js @@ -11,7 +11,7 @@ const { shouldBehaveLikeERC7739Signer } = require('../utils/cryptography/ERC7739 async function fixture() { const [beneficiary, other] = await ethers.getSigners(); - const target = await ethers.deployContract('CallReceiverMock'); + const target = await ethers.deployContract('CallReceiverMockExtended'); const signer = new P256Signer(); const helper = new ERC4337Helper('$AccountP256'); const smartAccount = await helper.newAccount(['AccountP256', '1', signer.publicKey.qx, signer.publicKey.qy]); diff --git a/test/account/draft-AccountRSA.test.js b/test/account/draft-AccountRSA.test.js index 8cd1e70f..3793b635 100644 --- a/test/account/draft-AccountRSA.test.js +++ b/test/account/draft-AccountRSA.test.js @@ -11,7 +11,7 @@ const { shouldBehaveLikeERC7739Signer } = require('../utils/cryptography/ERC7739 async function fixture() { const [beneficiary, other] = await ethers.getSigners(); - const target = await ethers.deployContract('CallReceiverMock'); + const target = await ethers.deployContract('CallReceiverMockExtended'); const signer = new RSASigner(); const helper = new ERC4337Helper('$AccountRSA'); const smartAccount = await helper.newAccount(['AccountRSA', '1', signer.publicKey.e, signer.publicKey.n]); From 90c7f5e2378c877da6e7379a9c373e291f24addd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Fri, 6 Dec 2024 16:06:39 -0600 Subject: [PATCH 10/51] Fix slither + Codespell --- .github/workflows/checks.yml | 2 +- slither.config.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index b770d58c..b67fa0bd 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -72,4 +72,4 @@ jobs: uses: codespell-project/actions-codespell@v2.0 with: check_filenames: true - skip: package-lock.json + skip: package-lock.json,vendor diff --git a/slither.config.json b/slither.config.json index b17b7ebe..47892af0 100644 --- a/slither.config.json +++ b/slither.config.json @@ -1,4 +1,4 @@ { "detectors_to_run": "arbitrary-send-erc20,array-by-reference,incorrect-shift,name-reused,rtlo,suicidal,uninitialized-state,uninitialized-storage,arbitrary-send-erc20-permit,controlled-array-length,controlled-delegatecall,delegatecall-loop,msg-value-loop,reentrancy-eth,unchecked-transfer,weak-prng,domain-separator-collision,erc20-interface,erc721-interface,locked-ether,mapping-deletion,shadowing-abstract,tautology,write-after-write,boolean-cst,reentrancy-no-eth,reused-constructor,tx-origin,unchecked-lowlevel,unchecked-send,variable-scope,void-cst,events-access,events-maths,incorrect-unary,boolean-equal,cyclomatic-complexity,deprecated-standards,erc20-indexed,function-init-state,pragma,unused-state,reentrancy-unlimited-gas,constable-states,immutable-states,var-read-using-this", - "filter_paths": "contracts/mocks,contracts-exposed" + "filter_paths": "contracts/mocks,contracts/vendor,contracts-exposed" } From 2e345c3fcf1064600890ee630b40587b5cd59614 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Fri, 6 Dec 2024 16:10:57 -0600 Subject: [PATCH 11/51] Fix coverage --- .solcover.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.solcover.js b/.solcover.js index eb6a6a25..bdfee3cd 100644 --- a/.solcover.js +++ b/.solcover.js @@ -1,4 +1,12 @@ module.exports = { skipFiles: ['mocks'], istanbulReporter: ['html', 'lcov', 'text-summary'], + // Work around stack too deep for coverage + configureYulOptimizer: true, + solcOptimizerDetails: { + yul: true, + yulDetails: { + optimizerSteps: '', + }, + }, }; From 4d03f45839fee42ba176a788c64ab5745073e0e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Fri, 6 Dec 2024 16:44:56 -0600 Subject: [PATCH 12/51] Remove entrypoint --- .../erc4337-entrypoint/core/EntryPoint.sol | 800 ------------------ .../erc4337-entrypoint/core/Entrypoint.sol | 800 ------------------ 2 files changed, 1600 deletions(-) delete mode 100644 contracts/vendor/erc4337-entrypoint/core/EntryPoint.sol delete mode 100644 contracts/vendor/erc4337-entrypoint/core/Entrypoint.sol diff --git a/contracts/vendor/erc4337-entrypoint/core/EntryPoint.sol b/contracts/vendor/erc4337-entrypoint/core/EntryPoint.sol deleted file mode 100644 index 44501524..00000000 --- a/contracts/vendor/erc4337-entrypoint/core/EntryPoint.sol +++ /dev/null @@ -1,800 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.23; -/* solhint-disable avoid-low-level-calls */ -/* solhint-disable no-inline-assembly */ - -import "../interfaces/IAccount.sol"; -import "../interfaces/IAccountExecute.sol"; -import "../interfaces/IPaymaster.sol"; -import "../interfaces/IEntryPoint.sol"; - -import "../utils/Exec.sol"; -import "./StakeManager.sol"; -import "./SenderCreator.sol"; -import "./Helpers.sol"; -import "./NonceManager.sol"; -import "./UserOperationLib.sol"; - -import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; -import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; - -/* - * Account-Abstraction (EIP-4337) singleton EntryPoint implementation. - * Only one instance required on each chain. - */ - -/// @custom:security-contact https://bounty.ethereum.org -contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, ERC165 { - - using UserOperationLib for PackedUserOperation; - - SenderCreator private immutable _senderCreator = new SenderCreator(); - - function senderCreator() internal view virtual returns (SenderCreator) { - return _senderCreator; - } - - //compensate for innerHandleOps' emit message and deposit refund. - // allow some slack for future gas price changes. - uint256 private constant INNER_GAS_OVERHEAD = 10000; - - // Marker for inner call revert on out of gas - bytes32 private constant INNER_OUT_OF_GAS = hex"deaddead"; - bytes32 private constant INNER_REVERT_LOW_PREFUND = hex"deadaa51"; - - uint256 private constant REVERT_REASON_MAX_LEN = 2048; - uint256 private constant PENALTY_PERCENT = 10; - - /// @inheritdoc IERC165 - function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { - // note: solidity "type(IEntryPoint).interfaceId" is without inherited methods but we want to check everything - return interfaceId == (type(IEntryPoint).interfaceId ^ type(IStakeManager).interfaceId ^ type(INonceManager).interfaceId) || - interfaceId == type(IEntryPoint).interfaceId || - interfaceId == type(IStakeManager).interfaceId || - interfaceId == type(INonceManager).interfaceId || - super.supportsInterface(interfaceId); - } - - /** - * Compensate the caller's beneficiary address with the collected fees of all UserOperations. - * @param beneficiary - The address to receive the fees. - * @param amount - Amount to transfer. - */ - function _compensate(address payable beneficiary, uint256 amount) internal { - require(beneficiary != address(0), "AA90 invalid beneficiary"); - (bool success, ) = beneficiary.call{value: amount}(""); - require(success, "AA91 failed send to beneficiary"); - } - - /** - * Execute a user operation. - * @param opIndex - Index into the opInfo array. - * @param userOp - The userOp to execute. - * @param opInfo - The opInfo filled by validatePrepayment for this userOp. - * @return collected - The total amount this userOp paid. - */ - function _executeUserOp( - uint256 opIndex, - PackedUserOperation calldata userOp, - UserOpInfo memory opInfo - ) - internal - returns - (uint256 collected) { - uint256 preGas = gasleft(); - bytes memory context = getMemoryBytesFromOffset(opInfo.contextOffset); - bool success; - { - uint256 saveFreePtr; - assembly ("memory-safe") { - saveFreePtr := mload(0x40) - } - bytes calldata callData = userOp.callData; - bytes memory innerCall; - bytes4 methodSig; - assembly { - let len := callData.length - if gt(len, 3) { - methodSig := calldataload(callData.offset) - } - } - if (methodSig == IAccountExecute.executeUserOp.selector) { - bytes memory executeUserOp = abi.encodeCall(IAccountExecute.executeUserOp, (userOp, opInfo.userOpHash)); - innerCall = abi.encodeCall(this.innerHandleOp, (executeUserOp, opInfo, context)); - } else - { - innerCall = abi.encodeCall(this.innerHandleOp, (callData, opInfo, context)); - } - assembly ("memory-safe") { - success := call(gas(), address(), 0, add(innerCall, 0x20), mload(innerCall), 0, 32) - collected := mload(0) - mstore(0x40, saveFreePtr) - } - } - if (!success) { - bytes32 innerRevertCode; - assembly ("memory-safe") { - let len := returndatasize() - if eq(32,len) { - returndatacopy(0, 0, 32) - innerRevertCode := mload(0) - } - } - if (innerRevertCode == INNER_OUT_OF_GAS) { - // handleOps was called with gas limit too low. abort entire bundle. - //can only be caused by bundler (leaving not enough gas for inner call) - revert FailedOp(opIndex, "AA95 out of gas"); - } else if (innerRevertCode == INNER_REVERT_LOW_PREFUND) { - // innerCall reverted on prefund too low. treat entire prefund as "gas cost" - uint256 actualGas = preGas - gasleft() + opInfo.preOpGas; - uint256 actualGasCost = opInfo.prefund; - emitPrefundTooLow(opInfo); - emitUserOperationEvent(opInfo, false, actualGasCost, actualGas); - collected = actualGasCost; - } else { - emit PostOpRevertReason( - opInfo.userOpHash, - opInfo.mUserOp.sender, - opInfo.mUserOp.nonce, - Exec.getReturnData(REVERT_REASON_MAX_LEN) - ); - - uint256 actualGas = preGas - gasleft() + opInfo.preOpGas; - collected = _postExecution( - IPaymaster.PostOpMode.postOpReverted, - opInfo, - context, - actualGas - ); - } - } - } - - function emitUserOperationEvent(UserOpInfo memory opInfo, bool success, uint256 actualGasCost, uint256 actualGas) internal virtual { - emit UserOperationEvent( - opInfo.userOpHash, - opInfo.mUserOp.sender, - opInfo.mUserOp.paymaster, - opInfo.mUserOp.nonce, - success, - actualGasCost, - actualGas - ); - } - - function emitPrefundTooLow(UserOpInfo memory opInfo) internal virtual { - emit UserOperationPrefundTooLow( - opInfo.userOpHash, - opInfo.mUserOp.sender, - opInfo.mUserOp.nonce - ); - } - - /// @inheritdoc IEntryPoint - function handleOps( - PackedUserOperation[] calldata ops, - address payable beneficiary - ) public nonReentrant { - uint256 opslen = ops.length; - UserOpInfo[] memory opInfos = new UserOpInfo[](opslen); - - unchecked { - for (uint256 i = 0; i < opslen; i++) { - UserOpInfo memory opInfo = opInfos[i]; - ( - uint256 validationData, - uint256 pmValidationData - ) = _validatePrepayment(i, ops[i], opInfo); - _validateAccountAndPaymasterValidationData( - i, - validationData, - pmValidationData, - address(0) - ); - } - - uint256 collected = 0; - emit BeforeExecution(); - - for (uint256 i = 0; i < opslen; i++) { - collected += _executeUserOp(i, ops[i], opInfos[i]); - } - - _compensate(beneficiary, collected); - } - } - - /// @inheritdoc IEntryPoint - function handleAggregatedOps( - UserOpsPerAggregator[] calldata opsPerAggregator, - address payable beneficiary - ) public nonReentrant { - - uint256 opasLen = opsPerAggregator.length; - uint256 totalOps = 0; - for (uint256 i = 0; i < opasLen; i++) { - UserOpsPerAggregator calldata opa = opsPerAggregator[i]; - PackedUserOperation[] calldata ops = opa.userOps; - IAggregator aggregator = opa.aggregator; - - //address(1) is special marker of "signature error" - require( - address(aggregator) != address(1), - "AA96 invalid aggregator" - ); - - if (address(aggregator) != address(0)) { - // solhint-disable-next-line no-empty-blocks - try aggregator.validateSignatures(ops, opa.signature) {} catch { - revert SignatureValidationFailed(address(aggregator)); - } - } - - totalOps += ops.length; - } - - UserOpInfo[] memory opInfos = new UserOpInfo[](totalOps); - - uint256 opIndex = 0; - for (uint256 a = 0; a < opasLen; a++) { - UserOpsPerAggregator calldata opa = opsPerAggregator[a]; - PackedUserOperation[] calldata ops = opa.userOps; - IAggregator aggregator = opa.aggregator; - - uint256 opslen = ops.length; - for (uint256 i = 0; i < opslen; i++) { - UserOpInfo memory opInfo = opInfos[opIndex]; - ( - uint256 validationData, - uint256 paymasterValidationData - ) = _validatePrepayment(opIndex, ops[i], opInfo); - _validateAccountAndPaymasterValidationData( - i, - validationData, - paymasterValidationData, - address(aggregator) - ); - opIndex++; - } - } - - emit BeforeExecution(); - - uint256 collected = 0; - opIndex = 0; - for (uint256 a = 0; a < opasLen; a++) { - UserOpsPerAggregator calldata opa = opsPerAggregator[a]; - emit SignatureAggregatorChanged(address(opa.aggregator)); - PackedUserOperation[] calldata ops = opa.userOps; - uint256 opslen = ops.length; - - for (uint256 i = 0; i < opslen; i++) { - collected += _executeUserOp(opIndex, ops[i], opInfos[opIndex]); - opIndex++; - } - } - emit SignatureAggregatorChanged(address(0)); - - _compensate(beneficiary, collected); - } - - /** - * A memory copy of UserOp static fields only. - * Excluding: callData, initCode and signature. Replacing paymasterAndData with paymaster. - */ - struct MemoryUserOp { - address sender; - uint256 nonce; - uint256 verificationGasLimit; - uint256 callGasLimit; - uint256 paymasterVerificationGasLimit; - uint256 paymasterPostOpGasLimit; - uint256 preVerificationGas; - address paymaster; - uint256 maxFeePerGas; - uint256 maxPriorityFeePerGas; - } - - struct UserOpInfo { - MemoryUserOp mUserOp; - bytes32 userOpHash; - uint256 prefund; - uint256 contextOffset; - uint256 preOpGas; - } - - /** - * Inner function to handle a UserOperation. - * Must be declared "external" to open a call context, but it can only be called by handleOps. - * @param callData - The callData to execute. - * @param opInfo - The UserOpInfo struct. - * @param context - The context bytes. - * @return actualGasCost - the actual cost in eth this UserOperation paid for gas - */ - function innerHandleOp( - bytes memory callData, - UserOpInfo memory opInfo, - bytes calldata context - ) external returns (uint256 actualGasCost) { - uint256 preGas = gasleft(); - require(msg.sender == address(this), "AA92 internal call only"); - MemoryUserOp memory mUserOp = opInfo.mUserOp; - - uint256 callGasLimit = mUserOp.callGasLimit; - unchecked { - // handleOps was called with gas limit too low. abort entire bundle. - if ( - gasleft() * 63 / 64 < - callGasLimit + - mUserOp.paymasterPostOpGasLimit + - INNER_GAS_OVERHEAD - ) { - assembly ("memory-safe") { - mstore(0, INNER_OUT_OF_GAS) - revert(0, 32) - } - } - } - - IPaymaster.PostOpMode mode = IPaymaster.PostOpMode.opSucceeded; - if (callData.length > 0) { - bool success = Exec.call(mUserOp.sender, 0, callData, callGasLimit); - if (!success) { - bytes memory result = Exec.getReturnData(REVERT_REASON_MAX_LEN); - if (result.length > 0) { - emit UserOperationRevertReason( - opInfo.userOpHash, - mUserOp.sender, - mUserOp.nonce, - result - ); - } - mode = IPaymaster.PostOpMode.opReverted; - } - } - - unchecked { - uint256 actualGas = preGas - gasleft() + opInfo.preOpGas; - return _postExecution(mode, opInfo, context, actualGas); - } - } - - /// @inheritdoc IEntryPoint - function getUserOpHash( - PackedUserOperation calldata userOp - ) public view returns (bytes32) { - return - keccak256(abi.encode(userOp.hash(), address(this), block.chainid)); - } - - /** - * Copy general fields from userOp into the memory opInfo structure. - * @param userOp - The user operation. - * @param mUserOp - The memory user operation. - */ - function _copyUserOpToMemory( - PackedUserOperation calldata userOp, - MemoryUserOp memory mUserOp - ) internal pure { - mUserOp.sender = userOp.sender; - mUserOp.nonce = userOp.nonce; - (mUserOp.verificationGasLimit, mUserOp.callGasLimit) = UserOperationLib.unpackUints(userOp.accountGasLimits); - mUserOp.preVerificationGas = userOp.preVerificationGas; - (mUserOp.maxPriorityFeePerGas, mUserOp.maxFeePerGas) = UserOperationLib.unpackUints(userOp.gasFees); - bytes calldata paymasterAndData = userOp.paymasterAndData; - if (paymasterAndData.length > 0) { - require( - paymasterAndData.length >= UserOperationLib.PAYMASTER_DATA_OFFSET, - "AA93 invalid paymasterAndData" - ); - (mUserOp.paymaster, mUserOp.paymasterVerificationGasLimit, mUserOp.paymasterPostOpGasLimit) = UserOperationLib.unpackPaymasterStaticFields(paymasterAndData); - } else { - mUserOp.paymaster = address(0); - mUserOp.paymasterVerificationGasLimit = 0; - mUserOp.paymasterPostOpGasLimit = 0; - } - } - - /** - * Get the required prefunded gas fee amount for an operation. - * @param mUserOp - The user operation in memory. - */ - function _getRequiredPrefund( - MemoryUserOp memory mUserOp - ) internal pure returns (uint256 requiredPrefund) { - unchecked { - uint256 requiredGas = mUserOp.verificationGasLimit + - mUserOp.callGasLimit + - mUserOp.paymasterVerificationGasLimit + - mUserOp.paymasterPostOpGasLimit + - mUserOp.preVerificationGas; - - requiredPrefund = requiredGas * mUserOp.maxFeePerGas; - } - } - - /** - * Create sender smart contract account if init code is provided. - * @param opIndex - The operation index. - * @param opInfo - The operation info. - * @param initCode - The init code for the smart contract account. - */ - function _createSenderIfNeeded( - uint256 opIndex, - UserOpInfo memory opInfo, - bytes calldata initCode - ) internal { - if (initCode.length != 0) { - address sender = opInfo.mUserOp.sender; - if (sender.code.length != 0) - revert FailedOp(opIndex, "AA10 sender already constructed"); - address sender1 = senderCreator().createSender{ - gas: opInfo.mUserOp.verificationGasLimit - }(initCode); - if (sender1 == address(0)) - revert FailedOp(opIndex, "AA13 initCode failed or OOG"); - if (sender1 != sender) - revert FailedOp(opIndex, "AA14 initCode must return sender"); - if (sender1.code.length == 0) - revert FailedOp(opIndex, "AA15 initCode must create sender"); - address factory = address(bytes20(initCode[0:20])); - emit AccountDeployed( - opInfo.userOpHash, - sender, - factory, - opInfo.mUserOp.paymaster - ); - } - } - - /// @inheritdoc IEntryPoint - function getSenderAddress(bytes calldata initCode) public { - address sender = senderCreator().createSender(initCode); - revert SenderAddressResult(sender); - } - - /** - * Call account.validateUserOp. - * Revert (with FailedOp) in case validateUserOp reverts, or account didn't send required prefund. - * Decrement account's deposit if needed. - * @param opIndex - The operation index. - * @param op - The user operation. - * @param opInfo - The operation info. - * @param requiredPrefund - The required prefund amount. - */ - function _validateAccountPrepayment( - uint256 opIndex, - PackedUserOperation calldata op, - UserOpInfo memory opInfo, - uint256 requiredPrefund, - uint256 verificationGasLimit - ) - internal - returns ( - uint256 validationData - ) - { - unchecked { - MemoryUserOp memory mUserOp = opInfo.mUserOp; - address sender = mUserOp.sender; - _createSenderIfNeeded(opIndex, opInfo, op.initCode); - address paymaster = mUserOp.paymaster; - uint256 missingAccountFunds = 0; - if (paymaster == address(0)) { - uint256 bal = balanceOf(sender); - missingAccountFunds = bal > requiredPrefund - ? 0 - : requiredPrefund - bal; - } - try - IAccount(sender).validateUserOp{ - gas: verificationGasLimit - }(op, opInfo.userOpHash, missingAccountFunds) - returns (uint256 _validationData) { - validationData = _validationData; - } catch { - revert FailedOpWithRevert(opIndex, "AA23 reverted", Exec.getReturnData(REVERT_REASON_MAX_LEN)); - } - if (paymaster == address(0)) { - DepositInfo storage senderInfo = deposits[sender]; - uint256 deposit = senderInfo.deposit; - if (requiredPrefund > deposit) { - revert FailedOp(opIndex, "AA21 didn't pay prefund"); - } - senderInfo.deposit = deposit - requiredPrefund; - } - } - } - - /** - * In case the request has a paymaster: - * - Validate paymaster has enough deposit. - * - Call paymaster.validatePaymasterUserOp. - * - Revert with proper FailedOp in case paymaster reverts. - * - Decrement paymaster's deposit. - * @param opIndex - The operation index. - * @param op - The user operation. - * @param opInfo - The operation info. - * @param requiredPreFund - The required prefund amount. - */ - function _validatePaymasterPrepayment( - uint256 opIndex, - PackedUserOperation calldata op, - UserOpInfo memory opInfo, - uint256 requiredPreFund - ) internal returns (bytes memory context, uint256 validationData) { - unchecked { - uint256 preGas = gasleft(); - MemoryUserOp memory mUserOp = opInfo.mUserOp; - address paymaster = mUserOp.paymaster; - DepositInfo storage paymasterInfo = deposits[paymaster]; - uint256 deposit = paymasterInfo.deposit; - if (deposit < requiredPreFund) { - revert FailedOp(opIndex, "AA31 paymaster deposit too low"); - } - paymasterInfo.deposit = deposit - requiredPreFund; - uint256 pmVerificationGasLimit = mUserOp.paymasterVerificationGasLimit; - try - IPaymaster(paymaster).validatePaymasterUserOp{gas: pmVerificationGasLimit}( - op, - opInfo.userOpHash, - requiredPreFund - ) - returns (bytes memory _context, uint256 _validationData) { - context = _context; - validationData = _validationData; - } catch { - revert FailedOpWithRevert(opIndex, "AA33 reverted", Exec.getReturnData(REVERT_REASON_MAX_LEN)); - } - if (preGas - gasleft() > pmVerificationGasLimit) { - revert FailedOp(opIndex, "AA36 over paymasterVerificationGasLimit"); - } - } - } - - /** - * Revert if either account validationData or paymaster validationData is expired. - * @param opIndex - The operation index. - * @param validationData - The account validationData. - * @param paymasterValidationData - The paymaster validationData. - * @param expectedAggregator - The expected aggregator. - */ - function _validateAccountAndPaymasterValidationData( - uint256 opIndex, - uint256 validationData, - uint256 paymasterValidationData, - address expectedAggregator - ) internal view { - (address aggregator, bool outOfTimeRange) = _getValidationData( - validationData - ); - if (expectedAggregator != aggregator) { - revert FailedOp(opIndex, "AA24 signature error"); - } - if (outOfTimeRange) { - revert FailedOp(opIndex, "AA22 expired or not due"); - } - // pmAggregator is not a real signature aggregator: we don't have logic to handle it as address. - // Non-zero address means that the paymaster fails due to some signature check (which is ok only during estimation). - address pmAggregator; - (pmAggregator, outOfTimeRange) = _getValidationData( - paymasterValidationData - ); - if (pmAggregator != address(0)) { - revert FailedOp(opIndex, "AA34 signature error"); - } - if (outOfTimeRange) { - revert FailedOp(opIndex, "AA32 paymaster expired or not due"); - } - } - - /** - * Parse validationData into its components. - * @param validationData - The packed validation data (sigFailed, validAfter, validUntil). - * @return aggregator the aggregator of the validationData - * @return outOfTimeRange true if current time is outside the time range of this validationData. - */ - function _getValidationData( - uint256 validationData - ) internal view returns (address aggregator, bool outOfTimeRange) { - if (validationData == 0) { - return (address(0), false); - } - ValidationData memory data = _parseValidationData(validationData); - // solhint-disable-next-line not-rely-on-time - outOfTimeRange = block.timestamp > data.validUntil || block.timestamp < data.validAfter; - aggregator = data.aggregator; - } - - /** - * Validate account and paymaster (if defined) and - * also make sure total validation doesn't exceed verificationGasLimit. - * This method is called off-chain (simulateValidation()) and on-chain (from handleOps) - * @param opIndex - The index of this userOp into the "opInfos" array. - * @param userOp - The userOp to validate. - */ - function _validatePrepayment( - uint256 opIndex, - PackedUserOperation calldata userOp, - UserOpInfo memory outOpInfo - ) - internal - returns (uint256 validationData, uint256 paymasterValidationData) - { - uint256 preGas = gasleft(); - MemoryUserOp memory mUserOp = outOpInfo.mUserOp; - _copyUserOpToMemory(userOp, mUserOp); - outOpInfo.userOpHash = getUserOpHash(userOp); - - // Validate all numeric values in userOp are well below 128 bit, so they can safely be added - // and multiplied without causing overflow. - uint256 verificationGasLimit = mUserOp.verificationGasLimit; - uint256 maxGasValues = mUserOp.preVerificationGas | - verificationGasLimit | - mUserOp.callGasLimit | - mUserOp.paymasterVerificationGasLimit | - mUserOp.paymasterPostOpGasLimit | - mUserOp.maxFeePerGas | - mUserOp.maxPriorityFeePerGas; - require(maxGasValues <= type(uint120).max, "AA94 gas values overflow"); - - uint256 requiredPreFund = _getRequiredPrefund(mUserOp); - validationData = _validateAccountPrepayment( - opIndex, - userOp, - outOpInfo, - requiredPreFund, - verificationGasLimit - ); - - if (!_validateAndUpdateNonce(mUserOp.sender, mUserOp.nonce)) { - revert FailedOp(opIndex, "AA25 invalid account nonce"); - } - - unchecked { - if (preGas - gasleft() > verificationGasLimit) { - revert FailedOp(opIndex, "AA26 over verificationGasLimit"); - } - } - - bytes memory context; - if (mUserOp.paymaster != address(0)) { - (context, paymasterValidationData) = _validatePaymasterPrepayment( - opIndex, - userOp, - outOpInfo, - requiredPreFund - ); - } - unchecked { - outOpInfo.prefund = requiredPreFund; - outOpInfo.contextOffset = getOffsetOfMemoryBytes(context); - outOpInfo.preOpGas = preGas - gasleft() + userOp.preVerificationGas; - } - } - - /** - * Process post-operation, called just after the callData is executed. - * If a paymaster is defined and its validation returned a non-empty context, its postOp is called. - * The excess amount is refunded to the account (or paymaster - if it was used in the request). - * @param mode - Whether is called from innerHandleOp, or outside (postOpReverted). - * @param opInfo - UserOp fields and info collected during validation. - * @param context - The context returned in validatePaymasterUserOp. - * @param actualGas - The gas used so far by this user operation. - */ - function _postExecution( - IPaymaster.PostOpMode mode, - UserOpInfo memory opInfo, - bytes memory context, - uint256 actualGas - ) private returns (uint256 actualGasCost) { - uint256 preGas = gasleft(); - unchecked { - address refundAddress; - MemoryUserOp memory mUserOp = opInfo.mUserOp; - uint256 gasPrice = getUserOpGasPrice(mUserOp); - - address paymaster = mUserOp.paymaster; - if (paymaster == address(0)) { - refundAddress = mUserOp.sender; - } else { - refundAddress = paymaster; - if (context.length > 0) { - actualGasCost = actualGas * gasPrice; - if (mode != IPaymaster.PostOpMode.postOpReverted) { - try IPaymaster(paymaster).postOp{ - gas: mUserOp.paymasterPostOpGasLimit - }(mode, context, actualGasCost, gasPrice) - // solhint-disable-next-line no-empty-blocks - {} catch { - bytes memory reason = Exec.getReturnData(REVERT_REASON_MAX_LEN); - revert PostOpReverted(reason); - } - } - } - } - actualGas += preGas - gasleft(); - - // Calculating a penalty for unused execution gas - { - uint256 executionGasLimit = mUserOp.callGasLimit + mUserOp.paymasterPostOpGasLimit; - uint256 executionGasUsed = actualGas - opInfo.preOpGas; - // this check is required for the gas used within EntryPoint and not covered by explicit gas limits - if (executionGasLimit > executionGasUsed) { - uint256 unusedGas = executionGasLimit - executionGasUsed; - uint256 unusedGasPenalty = (unusedGas * PENALTY_PERCENT) / 100; - actualGas += unusedGasPenalty; - } - } - - actualGasCost = actualGas * gasPrice; - uint256 prefund = opInfo.prefund; - if (prefund < actualGasCost) { - if (mode == IPaymaster.PostOpMode.postOpReverted) { - actualGasCost = prefund; - emitPrefundTooLow(opInfo); - emitUserOperationEvent(opInfo, false, actualGasCost, actualGas); - } else { - assembly ("memory-safe") { - mstore(0, INNER_REVERT_LOW_PREFUND) - revert(0, 32) - } - } - } else { - uint256 refund = prefund - actualGasCost; - _incrementDeposit(refundAddress, refund); - bool success = mode == IPaymaster.PostOpMode.opSucceeded; - emitUserOperationEvent(opInfo, success, actualGasCost, actualGas); - } - } // unchecked - } - - /** - * The gas price this UserOp agrees to pay. - * Relayer/block builder might submit the TX with higher priorityFee, but the user should not. - * @param mUserOp - The userOp to get the gas price from. - */ - function getUserOpGasPrice( - MemoryUserOp memory mUserOp - ) internal view returns (uint256) { - unchecked { - uint256 maxFeePerGas = mUserOp.maxFeePerGas; - uint256 maxPriorityFeePerGas = mUserOp.maxPriorityFeePerGas; - if (maxFeePerGas == maxPriorityFeePerGas) { - //legacy mode (for networks that don't support basefee opcode) - return maxFeePerGas; - } - return min(maxFeePerGas, maxPriorityFeePerGas + block.basefee); - } - } - - /** - * The offset of the given bytes in memory. - * @param data - The bytes to get the offset of. - */ - function getOffsetOfMemoryBytes( - bytes memory data - ) internal pure returns (uint256 offset) { - assembly { - offset := data - } - } - - /** - * The bytes in memory at the given offset. - * @param offset - The offset to get the bytes from. - */ - function getMemoryBytesFromOffset( - uint256 offset - ) internal pure returns (bytes memory data) { - assembly ("memory-safe") { - data := offset - } - } - - /// @inheritdoc IEntryPoint - function delegateAndRevert(address target, bytes calldata data) external { - (bool success, bytes memory ret) = target.delegatecall(data); - revert DelegateAndRevert(success, ret); - } -} diff --git a/contracts/vendor/erc4337-entrypoint/core/Entrypoint.sol b/contracts/vendor/erc4337-entrypoint/core/Entrypoint.sol deleted file mode 100644 index 44501524..00000000 --- a/contracts/vendor/erc4337-entrypoint/core/Entrypoint.sol +++ /dev/null @@ -1,800 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.23; -/* solhint-disable avoid-low-level-calls */ -/* solhint-disable no-inline-assembly */ - -import "../interfaces/IAccount.sol"; -import "../interfaces/IAccountExecute.sol"; -import "../interfaces/IPaymaster.sol"; -import "../interfaces/IEntryPoint.sol"; - -import "../utils/Exec.sol"; -import "./StakeManager.sol"; -import "./SenderCreator.sol"; -import "./Helpers.sol"; -import "./NonceManager.sol"; -import "./UserOperationLib.sol"; - -import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; -import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; - -/* - * Account-Abstraction (EIP-4337) singleton EntryPoint implementation. - * Only one instance required on each chain. - */ - -/// @custom:security-contact https://bounty.ethereum.org -contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, ERC165 { - - using UserOperationLib for PackedUserOperation; - - SenderCreator private immutable _senderCreator = new SenderCreator(); - - function senderCreator() internal view virtual returns (SenderCreator) { - return _senderCreator; - } - - //compensate for innerHandleOps' emit message and deposit refund. - // allow some slack for future gas price changes. - uint256 private constant INNER_GAS_OVERHEAD = 10000; - - // Marker for inner call revert on out of gas - bytes32 private constant INNER_OUT_OF_GAS = hex"deaddead"; - bytes32 private constant INNER_REVERT_LOW_PREFUND = hex"deadaa51"; - - uint256 private constant REVERT_REASON_MAX_LEN = 2048; - uint256 private constant PENALTY_PERCENT = 10; - - /// @inheritdoc IERC165 - function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { - // note: solidity "type(IEntryPoint).interfaceId" is without inherited methods but we want to check everything - return interfaceId == (type(IEntryPoint).interfaceId ^ type(IStakeManager).interfaceId ^ type(INonceManager).interfaceId) || - interfaceId == type(IEntryPoint).interfaceId || - interfaceId == type(IStakeManager).interfaceId || - interfaceId == type(INonceManager).interfaceId || - super.supportsInterface(interfaceId); - } - - /** - * Compensate the caller's beneficiary address with the collected fees of all UserOperations. - * @param beneficiary - The address to receive the fees. - * @param amount - Amount to transfer. - */ - function _compensate(address payable beneficiary, uint256 amount) internal { - require(beneficiary != address(0), "AA90 invalid beneficiary"); - (bool success, ) = beneficiary.call{value: amount}(""); - require(success, "AA91 failed send to beneficiary"); - } - - /** - * Execute a user operation. - * @param opIndex - Index into the opInfo array. - * @param userOp - The userOp to execute. - * @param opInfo - The opInfo filled by validatePrepayment for this userOp. - * @return collected - The total amount this userOp paid. - */ - function _executeUserOp( - uint256 opIndex, - PackedUserOperation calldata userOp, - UserOpInfo memory opInfo - ) - internal - returns - (uint256 collected) { - uint256 preGas = gasleft(); - bytes memory context = getMemoryBytesFromOffset(opInfo.contextOffset); - bool success; - { - uint256 saveFreePtr; - assembly ("memory-safe") { - saveFreePtr := mload(0x40) - } - bytes calldata callData = userOp.callData; - bytes memory innerCall; - bytes4 methodSig; - assembly { - let len := callData.length - if gt(len, 3) { - methodSig := calldataload(callData.offset) - } - } - if (methodSig == IAccountExecute.executeUserOp.selector) { - bytes memory executeUserOp = abi.encodeCall(IAccountExecute.executeUserOp, (userOp, opInfo.userOpHash)); - innerCall = abi.encodeCall(this.innerHandleOp, (executeUserOp, opInfo, context)); - } else - { - innerCall = abi.encodeCall(this.innerHandleOp, (callData, opInfo, context)); - } - assembly ("memory-safe") { - success := call(gas(), address(), 0, add(innerCall, 0x20), mload(innerCall), 0, 32) - collected := mload(0) - mstore(0x40, saveFreePtr) - } - } - if (!success) { - bytes32 innerRevertCode; - assembly ("memory-safe") { - let len := returndatasize() - if eq(32,len) { - returndatacopy(0, 0, 32) - innerRevertCode := mload(0) - } - } - if (innerRevertCode == INNER_OUT_OF_GAS) { - // handleOps was called with gas limit too low. abort entire bundle. - //can only be caused by bundler (leaving not enough gas for inner call) - revert FailedOp(opIndex, "AA95 out of gas"); - } else if (innerRevertCode == INNER_REVERT_LOW_PREFUND) { - // innerCall reverted on prefund too low. treat entire prefund as "gas cost" - uint256 actualGas = preGas - gasleft() + opInfo.preOpGas; - uint256 actualGasCost = opInfo.prefund; - emitPrefundTooLow(opInfo); - emitUserOperationEvent(opInfo, false, actualGasCost, actualGas); - collected = actualGasCost; - } else { - emit PostOpRevertReason( - opInfo.userOpHash, - opInfo.mUserOp.sender, - opInfo.mUserOp.nonce, - Exec.getReturnData(REVERT_REASON_MAX_LEN) - ); - - uint256 actualGas = preGas - gasleft() + opInfo.preOpGas; - collected = _postExecution( - IPaymaster.PostOpMode.postOpReverted, - opInfo, - context, - actualGas - ); - } - } - } - - function emitUserOperationEvent(UserOpInfo memory opInfo, bool success, uint256 actualGasCost, uint256 actualGas) internal virtual { - emit UserOperationEvent( - opInfo.userOpHash, - opInfo.mUserOp.sender, - opInfo.mUserOp.paymaster, - opInfo.mUserOp.nonce, - success, - actualGasCost, - actualGas - ); - } - - function emitPrefundTooLow(UserOpInfo memory opInfo) internal virtual { - emit UserOperationPrefundTooLow( - opInfo.userOpHash, - opInfo.mUserOp.sender, - opInfo.mUserOp.nonce - ); - } - - /// @inheritdoc IEntryPoint - function handleOps( - PackedUserOperation[] calldata ops, - address payable beneficiary - ) public nonReentrant { - uint256 opslen = ops.length; - UserOpInfo[] memory opInfos = new UserOpInfo[](opslen); - - unchecked { - for (uint256 i = 0; i < opslen; i++) { - UserOpInfo memory opInfo = opInfos[i]; - ( - uint256 validationData, - uint256 pmValidationData - ) = _validatePrepayment(i, ops[i], opInfo); - _validateAccountAndPaymasterValidationData( - i, - validationData, - pmValidationData, - address(0) - ); - } - - uint256 collected = 0; - emit BeforeExecution(); - - for (uint256 i = 0; i < opslen; i++) { - collected += _executeUserOp(i, ops[i], opInfos[i]); - } - - _compensate(beneficiary, collected); - } - } - - /// @inheritdoc IEntryPoint - function handleAggregatedOps( - UserOpsPerAggregator[] calldata opsPerAggregator, - address payable beneficiary - ) public nonReentrant { - - uint256 opasLen = opsPerAggregator.length; - uint256 totalOps = 0; - for (uint256 i = 0; i < opasLen; i++) { - UserOpsPerAggregator calldata opa = opsPerAggregator[i]; - PackedUserOperation[] calldata ops = opa.userOps; - IAggregator aggregator = opa.aggregator; - - //address(1) is special marker of "signature error" - require( - address(aggregator) != address(1), - "AA96 invalid aggregator" - ); - - if (address(aggregator) != address(0)) { - // solhint-disable-next-line no-empty-blocks - try aggregator.validateSignatures(ops, opa.signature) {} catch { - revert SignatureValidationFailed(address(aggregator)); - } - } - - totalOps += ops.length; - } - - UserOpInfo[] memory opInfos = new UserOpInfo[](totalOps); - - uint256 opIndex = 0; - for (uint256 a = 0; a < opasLen; a++) { - UserOpsPerAggregator calldata opa = opsPerAggregator[a]; - PackedUserOperation[] calldata ops = opa.userOps; - IAggregator aggregator = opa.aggregator; - - uint256 opslen = ops.length; - for (uint256 i = 0; i < opslen; i++) { - UserOpInfo memory opInfo = opInfos[opIndex]; - ( - uint256 validationData, - uint256 paymasterValidationData - ) = _validatePrepayment(opIndex, ops[i], opInfo); - _validateAccountAndPaymasterValidationData( - i, - validationData, - paymasterValidationData, - address(aggregator) - ); - opIndex++; - } - } - - emit BeforeExecution(); - - uint256 collected = 0; - opIndex = 0; - for (uint256 a = 0; a < opasLen; a++) { - UserOpsPerAggregator calldata opa = opsPerAggregator[a]; - emit SignatureAggregatorChanged(address(opa.aggregator)); - PackedUserOperation[] calldata ops = opa.userOps; - uint256 opslen = ops.length; - - for (uint256 i = 0; i < opslen; i++) { - collected += _executeUserOp(opIndex, ops[i], opInfos[opIndex]); - opIndex++; - } - } - emit SignatureAggregatorChanged(address(0)); - - _compensate(beneficiary, collected); - } - - /** - * A memory copy of UserOp static fields only. - * Excluding: callData, initCode and signature. Replacing paymasterAndData with paymaster. - */ - struct MemoryUserOp { - address sender; - uint256 nonce; - uint256 verificationGasLimit; - uint256 callGasLimit; - uint256 paymasterVerificationGasLimit; - uint256 paymasterPostOpGasLimit; - uint256 preVerificationGas; - address paymaster; - uint256 maxFeePerGas; - uint256 maxPriorityFeePerGas; - } - - struct UserOpInfo { - MemoryUserOp mUserOp; - bytes32 userOpHash; - uint256 prefund; - uint256 contextOffset; - uint256 preOpGas; - } - - /** - * Inner function to handle a UserOperation. - * Must be declared "external" to open a call context, but it can only be called by handleOps. - * @param callData - The callData to execute. - * @param opInfo - The UserOpInfo struct. - * @param context - The context bytes. - * @return actualGasCost - the actual cost in eth this UserOperation paid for gas - */ - function innerHandleOp( - bytes memory callData, - UserOpInfo memory opInfo, - bytes calldata context - ) external returns (uint256 actualGasCost) { - uint256 preGas = gasleft(); - require(msg.sender == address(this), "AA92 internal call only"); - MemoryUserOp memory mUserOp = opInfo.mUserOp; - - uint256 callGasLimit = mUserOp.callGasLimit; - unchecked { - // handleOps was called with gas limit too low. abort entire bundle. - if ( - gasleft() * 63 / 64 < - callGasLimit + - mUserOp.paymasterPostOpGasLimit + - INNER_GAS_OVERHEAD - ) { - assembly ("memory-safe") { - mstore(0, INNER_OUT_OF_GAS) - revert(0, 32) - } - } - } - - IPaymaster.PostOpMode mode = IPaymaster.PostOpMode.opSucceeded; - if (callData.length > 0) { - bool success = Exec.call(mUserOp.sender, 0, callData, callGasLimit); - if (!success) { - bytes memory result = Exec.getReturnData(REVERT_REASON_MAX_LEN); - if (result.length > 0) { - emit UserOperationRevertReason( - opInfo.userOpHash, - mUserOp.sender, - mUserOp.nonce, - result - ); - } - mode = IPaymaster.PostOpMode.opReverted; - } - } - - unchecked { - uint256 actualGas = preGas - gasleft() + opInfo.preOpGas; - return _postExecution(mode, opInfo, context, actualGas); - } - } - - /// @inheritdoc IEntryPoint - function getUserOpHash( - PackedUserOperation calldata userOp - ) public view returns (bytes32) { - return - keccak256(abi.encode(userOp.hash(), address(this), block.chainid)); - } - - /** - * Copy general fields from userOp into the memory opInfo structure. - * @param userOp - The user operation. - * @param mUserOp - The memory user operation. - */ - function _copyUserOpToMemory( - PackedUserOperation calldata userOp, - MemoryUserOp memory mUserOp - ) internal pure { - mUserOp.sender = userOp.sender; - mUserOp.nonce = userOp.nonce; - (mUserOp.verificationGasLimit, mUserOp.callGasLimit) = UserOperationLib.unpackUints(userOp.accountGasLimits); - mUserOp.preVerificationGas = userOp.preVerificationGas; - (mUserOp.maxPriorityFeePerGas, mUserOp.maxFeePerGas) = UserOperationLib.unpackUints(userOp.gasFees); - bytes calldata paymasterAndData = userOp.paymasterAndData; - if (paymasterAndData.length > 0) { - require( - paymasterAndData.length >= UserOperationLib.PAYMASTER_DATA_OFFSET, - "AA93 invalid paymasterAndData" - ); - (mUserOp.paymaster, mUserOp.paymasterVerificationGasLimit, mUserOp.paymasterPostOpGasLimit) = UserOperationLib.unpackPaymasterStaticFields(paymasterAndData); - } else { - mUserOp.paymaster = address(0); - mUserOp.paymasterVerificationGasLimit = 0; - mUserOp.paymasterPostOpGasLimit = 0; - } - } - - /** - * Get the required prefunded gas fee amount for an operation. - * @param mUserOp - The user operation in memory. - */ - function _getRequiredPrefund( - MemoryUserOp memory mUserOp - ) internal pure returns (uint256 requiredPrefund) { - unchecked { - uint256 requiredGas = mUserOp.verificationGasLimit + - mUserOp.callGasLimit + - mUserOp.paymasterVerificationGasLimit + - mUserOp.paymasterPostOpGasLimit + - mUserOp.preVerificationGas; - - requiredPrefund = requiredGas * mUserOp.maxFeePerGas; - } - } - - /** - * Create sender smart contract account if init code is provided. - * @param opIndex - The operation index. - * @param opInfo - The operation info. - * @param initCode - The init code for the smart contract account. - */ - function _createSenderIfNeeded( - uint256 opIndex, - UserOpInfo memory opInfo, - bytes calldata initCode - ) internal { - if (initCode.length != 0) { - address sender = opInfo.mUserOp.sender; - if (sender.code.length != 0) - revert FailedOp(opIndex, "AA10 sender already constructed"); - address sender1 = senderCreator().createSender{ - gas: opInfo.mUserOp.verificationGasLimit - }(initCode); - if (sender1 == address(0)) - revert FailedOp(opIndex, "AA13 initCode failed or OOG"); - if (sender1 != sender) - revert FailedOp(opIndex, "AA14 initCode must return sender"); - if (sender1.code.length == 0) - revert FailedOp(opIndex, "AA15 initCode must create sender"); - address factory = address(bytes20(initCode[0:20])); - emit AccountDeployed( - opInfo.userOpHash, - sender, - factory, - opInfo.mUserOp.paymaster - ); - } - } - - /// @inheritdoc IEntryPoint - function getSenderAddress(bytes calldata initCode) public { - address sender = senderCreator().createSender(initCode); - revert SenderAddressResult(sender); - } - - /** - * Call account.validateUserOp. - * Revert (with FailedOp) in case validateUserOp reverts, or account didn't send required prefund. - * Decrement account's deposit if needed. - * @param opIndex - The operation index. - * @param op - The user operation. - * @param opInfo - The operation info. - * @param requiredPrefund - The required prefund amount. - */ - function _validateAccountPrepayment( - uint256 opIndex, - PackedUserOperation calldata op, - UserOpInfo memory opInfo, - uint256 requiredPrefund, - uint256 verificationGasLimit - ) - internal - returns ( - uint256 validationData - ) - { - unchecked { - MemoryUserOp memory mUserOp = opInfo.mUserOp; - address sender = mUserOp.sender; - _createSenderIfNeeded(opIndex, opInfo, op.initCode); - address paymaster = mUserOp.paymaster; - uint256 missingAccountFunds = 0; - if (paymaster == address(0)) { - uint256 bal = balanceOf(sender); - missingAccountFunds = bal > requiredPrefund - ? 0 - : requiredPrefund - bal; - } - try - IAccount(sender).validateUserOp{ - gas: verificationGasLimit - }(op, opInfo.userOpHash, missingAccountFunds) - returns (uint256 _validationData) { - validationData = _validationData; - } catch { - revert FailedOpWithRevert(opIndex, "AA23 reverted", Exec.getReturnData(REVERT_REASON_MAX_LEN)); - } - if (paymaster == address(0)) { - DepositInfo storage senderInfo = deposits[sender]; - uint256 deposit = senderInfo.deposit; - if (requiredPrefund > deposit) { - revert FailedOp(opIndex, "AA21 didn't pay prefund"); - } - senderInfo.deposit = deposit - requiredPrefund; - } - } - } - - /** - * In case the request has a paymaster: - * - Validate paymaster has enough deposit. - * - Call paymaster.validatePaymasterUserOp. - * - Revert with proper FailedOp in case paymaster reverts. - * - Decrement paymaster's deposit. - * @param opIndex - The operation index. - * @param op - The user operation. - * @param opInfo - The operation info. - * @param requiredPreFund - The required prefund amount. - */ - function _validatePaymasterPrepayment( - uint256 opIndex, - PackedUserOperation calldata op, - UserOpInfo memory opInfo, - uint256 requiredPreFund - ) internal returns (bytes memory context, uint256 validationData) { - unchecked { - uint256 preGas = gasleft(); - MemoryUserOp memory mUserOp = opInfo.mUserOp; - address paymaster = mUserOp.paymaster; - DepositInfo storage paymasterInfo = deposits[paymaster]; - uint256 deposit = paymasterInfo.deposit; - if (deposit < requiredPreFund) { - revert FailedOp(opIndex, "AA31 paymaster deposit too low"); - } - paymasterInfo.deposit = deposit - requiredPreFund; - uint256 pmVerificationGasLimit = mUserOp.paymasterVerificationGasLimit; - try - IPaymaster(paymaster).validatePaymasterUserOp{gas: pmVerificationGasLimit}( - op, - opInfo.userOpHash, - requiredPreFund - ) - returns (bytes memory _context, uint256 _validationData) { - context = _context; - validationData = _validationData; - } catch { - revert FailedOpWithRevert(opIndex, "AA33 reverted", Exec.getReturnData(REVERT_REASON_MAX_LEN)); - } - if (preGas - gasleft() > pmVerificationGasLimit) { - revert FailedOp(opIndex, "AA36 over paymasterVerificationGasLimit"); - } - } - } - - /** - * Revert if either account validationData or paymaster validationData is expired. - * @param opIndex - The operation index. - * @param validationData - The account validationData. - * @param paymasterValidationData - The paymaster validationData. - * @param expectedAggregator - The expected aggregator. - */ - function _validateAccountAndPaymasterValidationData( - uint256 opIndex, - uint256 validationData, - uint256 paymasterValidationData, - address expectedAggregator - ) internal view { - (address aggregator, bool outOfTimeRange) = _getValidationData( - validationData - ); - if (expectedAggregator != aggregator) { - revert FailedOp(opIndex, "AA24 signature error"); - } - if (outOfTimeRange) { - revert FailedOp(opIndex, "AA22 expired or not due"); - } - // pmAggregator is not a real signature aggregator: we don't have logic to handle it as address. - // Non-zero address means that the paymaster fails due to some signature check (which is ok only during estimation). - address pmAggregator; - (pmAggregator, outOfTimeRange) = _getValidationData( - paymasterValidationData - ); - if (pmAggregator != address(0)) { - revert FailedOp(opIndex, "AA34 signature error"); - } - if (outOfTimeRange) { - revert FailedOp(opIndex, "AA32 paymaster expired or not due"); - } - } - - /** - * Parse validationData into its components. - * @param validationData - The packed validation data (sigFailed, validAfter, validUntil). - * @return aggregator the aggregator of the validationData - * @return outOfTimeRange true if current time is outside the time range of this validationData. - */ - function _getValidationData( - uint256 validationData - ) internal view returns (address aggregator, bool outOfTimeRange) { - if (validationData == 0) { - return (address(0), false); - } - ValidationData memory data = _parseValidationData(validationData); - // solhint-disable-next-line not-rely-on-time - outOfTimeRange = block.timestamp > data.validUntil || block.timestamp < data.validAfter; - aggregator = data.aggregator; - } - - /** - * Validate account and paymaster (if defined) and - * also make sure total validation doesn't exceed verificationGasLimit. - * This method is called off-chain (simulateValidation()) and on-chain (from handleOps) - * @param opIndex - The index of this userOp into the "opInfos" array. - * @param userOp - The userOp to validate. - */ - function _validatePrepayment( - uint256 opIndex, - PackedUserOperation calldata userOp, - UserOpInfo memory outOpInfo - ) - internal - returns (uint256 validationData, uint256 paymasterValidationData) - { - uint256 preGas = gasleft(); - MemoryUserOp memory mUserOp = outOpInfo.mUserOp; - _copyUserOpToMemory(userOp, mUserOp); - outOpInfo.userOpHash = getUserOpHash(userOp); - - // Validate all numeric values in userOp are well below 128 bit, so they can safely be added - // and multiplied without causing overflow. - uint256 verificationGasLimit = mUserOp.verificationGasLimit; - uint256 maxGasValues = mUserOp.preVerificationGas | - verificationGasLimit | - mUserOp.callGasLimit | - mUserOp.paymasterVerificationGasLimit | - mUserOp.paymasterPostOpGasLimit | - mUserOp.maxFeePerGas | - mUserOp.maxPriorityFeePerGas; - require(maxGasValues <= type(uint120).max, "AA94 gas values overflow"); - - uint256 requiredPreFund = _getRequiredPrefund(mUserOp); - validationData = _validateAccountPrepayment( - opIndex, - userOp, - outOpInfo, - requiredPreFund, - verificationGasLimit - ); - - if (!_validateAndUpdateNonce(mUserOp.sender, mUserOp.nonce)) { - revert FailedOp(opIndex, "AA25 invalid account nonce"); - } - - unchecked { - if (preGas - gasleft() > verificationGasLimit) { - revert FailedOp(opIndex, "AA26 over verificationGasLimit"); - } - } - - bytes memory context; - if (mUserOp.paymaster != address(0)) { - (context, paymasterValidationData) = _validatePaymasterPrepayment( - opIndex, - userOp, - outOpInfo, - requiredPreFund - ); - } - unchecked { - outOpInfo.prefund = requiredPreFund; - outOpInfo.contextOffset = getOffsetOfMemoryBytes(context); - outOpInfo.preOpGas = preGas - gasleft() + userOp.preVerificationGas; - } - } - - /** - * Process post-operation, called just after the callData is executed. - * If a paymaster is defined and its validation returned a non-empty context, its postOp is called. - * The excess amount is refunded to the account (or paymaster - if it was used in the request). - * @param mode - Whether is called from innerHandleOp, or outside (postOpReverted). - * @param opInfo - UserOp fields and info collected during validation. - * @param context - The context returned in validatePaymasterUserOp. - * @param actualGas - The gas used so far by this user operation. - */ - function _postExecution( - IPaymaster.PostOpMode mode, - UserOpInfo memory opInfo, - bytes memory context, - uint256 actualGas - ) private returns (uint256 actualGasCost) { - uint256 preGas = gasleft(); - unchecked { - address refundAddress; - MemoryUserOp memory mUserOp = opInfo.mUserOp; - uint256 gasPrice = getUserOpGasPrice(mUserOp); - - address paymaster = mUserOp.paymaster; - if (paymaster == address(0)) { - refundAddress = mUserOp.sender; - } else { - refundAddress = paymaster; - if (context.length > 0) { - actualGasCost = actualGas * gasPrice; - if (mode != IPaymaster.PostOpMode.postOpReverted) { - try IPaymaster(paymaster).postOp{ - gas: mUserOp.paymasterPostOpGasLimit - }(mode, context, actualGasCost, gasPrice) - // solhint-disable-next-line no-empty-blocks - {} catch { - bytes memory reason = Exec.getReturnData(REVERT_REASON_MAX_LEN); - revert PostOpReverted(reason); - } - } - } - } - actualGas += preGas - gasleft(); - - // Calculating a penalty for unused execution gas - { - uint256 executionGasLimit = mUserOp.callGasLimit + mUserOp.paymasterPostOpGasLimit; - uint256 executionGasUsed = actualGas - opInfo.preOpGas; - // this check is required for the gas used within EntryPoint and not covered by explicit gas limits - if (executionGasLimit > executionGasUsed) { - uint256 unusedGas = executionGasLimit - executionGasUsed; - uint256 unusedGasPenalty = (unusedGas * PENALTY_PERCENT) / 100; - actualGas += unusedGasPenalty; - } - } - - actualGasCost = actualGas * gasPrice; - uint256 prefund = opInfo.prefund; - if (prefund < actualGasCost) { - if (mode == IPaymaster.PostOpMode.postOpReverted) { - actualGasCost = prefund; - emitPrefundTooLow(opInfo); - emitUserOperationEvent(opInfo, false, actualGasCost, actualGas); - } else { - assembly ("memory-safe") { - mstore(0, INNER_REVERT_LOW_PREFUND) - revert(0, 32) - } - } - } else { - uint256 refund = prefund - actualGasCost; - _incrementDeposit(refundAddress, refund); - bool success = mode == IPaymaster.PostOpMode.opSucceeded; - emitUserOperationEvent(opInfo, success, actualGasCost, actualGas); - } - } // unchecked - } - - /** - * The gas price this UserOp agrees to pay. - * Relayer/block builder might submit the TX with higher priorityFee, but the user should not. - * @param mUserOp - The userOp to get the gas price from. - */ - function getUserOpGasPrice( - MemoryUserOp memory mUserOp - ) internal view returns (uint256) { - unchecked { - uint256 maxFeePerGas = mUserOp.maxFeePerGas; - uint256 maxPriorityFeePerGas = mUserOp.maxPriorityFeePerGas; - if (maxFeePerGas == maxPriorityFeePerGas) { - //legacy mode (for networks that don't support basefee opcode) - return maxFeePerGas; - } - return min(maxFeePerGas, maxPriorityFeePerGas + block.basefee); - } - } - - /** - * The offset of the given bytes in memory. - * @param data - The bytes to get the offset of. - */ - function getOffsetOfMemoryBytes( - bytes memory data - ) internal pure returns (uint256 offset) { - assembly { - offset := data - } - } - - /** - * The bytes in memory at the given offset. - * @param offset - The offset to get the bytes from. - */ - function getMemoryBytesFromOffset( - uint256 offset - ) internal pure returns (bytes memory data) { - assembly ("memory-safe") { - data := offset - } - } - - /// @inheritdoc IEntryPoint - function delegateAndRevert(address target, bytes calldata data) external { - (bool success, bytes memory ret) = target.delegatecall(data); - revert DelegateAndRevert(success, ret); - } -} From 9caded9966a58bf0a8cd72f1e2a9b656d37660ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Fri, 6 Dec 2024 16:45:10 -0600 Subject: [PATCH 13/51] Readd entrypoint --- .../erc4337-entrypoint/core/EntryPoint.sol | 800 ++++++++++++++++++ 1 file changed, 800 insertions(+) create mode 100644 contracts/vendor/erc4337-entrypoint/core/EntryPoint.sol diff --git a/contracts/vendor/erc4337-entrypoint/core/EntryPoint.sol b/contracts/vendor/erc4337-entrypoint/core/EntryPoint.sol new file mode 100644 index 00000000..44501524 --- /dev/null +++ b/contracts/vendor/erc4337-entrypoint/core/EntryPoint.sol @@ -0,0 +1,800 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.23; +/* solhint-disable avoid-low-level-calls */ +/* solhint-disable no-inline-assembly */ + +import "../interfaces/IAccount.sol"; +import "../interfaces/IAccountExecute.sol"; +import "../interfaces/IPaymaster.sol"; +import "../interfaces/IEntryPoint.sol"; + +import "../utils/Exec.sol"; +import "./StakeManager.sol"; +import "./SenderCreator.sol"; +import "./Helpers.sol"; +import "./NonceManager.sol"; +import "./UserOperationLib.sol"; + +import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; + +/* + * Account-Abstraction (EIP-4337) singleton EntryPoint implementation. + * Only one instance required on each chain. + */ + +/// @custom:security-contact https://bounty.ethereum.org +contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, ERC165 { + + using UserOperationLib for PackedUserOperation; + + SenderCreator private immutable _senderCreator = new SenderCreator(); + + function senderCreator() internal view virtual returns (SenderCreator) { + return _senderCreator; + } + + //compensate for innerHandleOps' emit message and deposit refund. + // allow some slack for future gas price changes. + uint256 private constant INNER_GAS_OVERHEAD = 10000; + + // Marker for inner call revert on out of gas + bytes32 private constant INNER_OUT_OF_GAS = hex"deaddead"; + bytes32 private constant INNER_REVERT_LOW_PREFUND = hex"deadaa51"; + + uint256 private constant REVERT_REASON_MAX_LEN = 2048; + uint256 private constant PENALTY_PERCENT = 10; + + /// @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + // note: solidity "type(IEntryPoint).interfaceId" is without inherited methods but we want to check everything + return interfaceId == (type(IEntryPoint).interfaceId ^ type(IStakeManager).interfaceId ^ type(INonceManager).interfaceId) || + interfaceId == type(IEntryPoint).interfaceId || + interfaceId == type(IStakeManager).interfaceId || + interfaceId == type(INonceManager).interfaceId || + super.supportsInterface(interfaceId); + } + + /** + * Compensate the caller's beneficiary address with the collected fees of all UserOperations. + * @param beneficiary - The address to receive the fees. + * @param amount - Amount to transfer. + */ + function _compensate(address payable beneficiary, uint256 amount) internal { + require(beneficiary != address(0), "AA90 invalid beneficiary"); + (bool success, ) = beneficiary.call{value: amount}(""); + require(success, "AA91 failed send to beneficiary"); + } + + /** + * Execute a user operation. + * @param opIndex - Index into the opInfo array. + * @param userOp - The userOp to execute. + * @param opInfo - The opInfo filled by validatePrepayment for this userOp. + * @return collected - The total amount this userOp paid. + */ + function _executeUserOp( + uint256 opIndex, + PackedUserOperation calldata userOp, + UserOpInfo memory opInfo + ) + internal + returns + (uint256 collected) { + uint256 preGas = gasleft(); + bytes memory context = getMemoryBytesFromOffset(opInfo.contextOffset); + bool success; + { + uint256 saveFreePtr; + assembly ("memory-safe") { + saveFreePtr := mload(0x40) + } + bytes calldata callData = userOp.callData; + bytes memory innerCall; + bytes4 methodSig; + assembly { + let len := callData.length + if gt(len, 3) { + methodSig := calldataload(callData.offset) + } + } + if (methodSig == IAccountExecute.executeUserOp.selector) { + bytes memory executeUserOp = abi.encodeCall(IAccountExecute.executeUserOp, (userOp, opInfo.userOpHash)); + innerCall = abi.encodeCall(this.innerHandleOp, (executeUserOp, opInfo, context)); + } else + { + innerCall = abi.encodeCall(this.innerHandleOp, (callData, opInfo, context)); + } + assembly ("memory-safe") { + success := call(gas(), address(), 0, add(innerCall, 0x20), mload(innerCall), 0, 32) + collected := mload(0) + mstore(0x40, saveFreePtr) + } + } + if (!success) { + bytes32 innerRevertCode; + assembly ("memory-safe") { + let len := returndatasize() + if eq(32,len) { + returndatacopy(0, 0, 32) + innerRevertCode := mload(0) + } + } + if (innerRevertCode == INNER_OUT_OF_GAS) { + // handleOps was called with gas limit too low. abort entire bundle. + //can only be caused by bundler (leaving not enough gas for inner call) + revert FailedOp(opIndex, "AA95 out of gas"); + } else if (innerRevertCode == INNER_REVERT_LOW_PREFUND) { + // innerCall reverted on prefund too low. treat entire prefund as "gas cost" + uint256 actualGas = preGas - gasleft() + opInfo.preOpGas; + uint256 actualGasCost = opInfo.prefund; + emitPrefundTooLow(opInfo); + emitUserOperationEvent(opInfo, false, actualGasCost, actualGas); + collected = actualGasCost; + } else { + emit PostOpRevertReason( + opInfo.userOpHash, + opInfo.mUserOp.sender, + opInfo.mUserOp.nonce, + Exec.getReturnData(REVERT_REASON_MAX_LEN) + ); + + uint256 actualGas = preGas - gasleft() + opInfo.preOpGas; + collected = _postExecution( + IPaymaster.PostOpMode.postOpReverted, + opInfo, + context, + actualGas + ); + } + } + } + + function emitUserOperationEvent(UserOpInfo memory opInfo, bool success, uint256 actualGasCost, uint256 actualGas) internal virtual { + emit UserOperationEvent( + opInfo.userOpHash, + opInfo.mUserOp.sender, + opInfo.mUserOp.paymaster, + opInfo.mUserOp.nonce, + success, + actualGasCost, + actualGas + ); + } + + function emitPrefundTooLow(UserOpInfo memory opInfo) internal virtual { + emit UserOperationPrefundTooLow( + opInfo.userOpHash, + opInfo.mUserOp.sender, + opInfo.mUserOp.nonce + ); + } + + /// @inheritdoc IEntryPoint + function handleOps( + PackedUserOperation[] calldata ops, + address payable beneficiary + ) public nonReentrant { + uint256 opslen = ops.length; + UserOpInfo[] memory opInfos = new UserOpInfo[](opslen); + + unchecked { + for (uint256 i = 0; i < opslen; i++) { + UserOpInfo memory opInfo = opInfos[i]; + ( + uint256 validationData, + uint256 pmValidationData + ) = _validatePrepayment(i, ops[i], opInfo); + _validateAccountAndPaymasterValidationData( + i, + validationData, + pmValidationData, + address(0) + ); + } + + uint256 collected = 0; + emit BeforeExecution(); + + for (uint256 i = 0; i < opslen; i++) { + collected += _executeUserOp(i, ops[i], opInfos[i]); + } + + _compensate(beneficiary, collected); + } + } + + /// @inheritdoc IEntryPoint + function handleAggregatedOps( + UserOpsPerAggregator[] calldata opsPerAggregator, + address payable beneficiary + ) public nonReentrant { + + uint256 opasLen = opsPerAggregator.length; + uint256 totalOps = 0; + for (uint256 i = 0; i < opasLen; i++) { + UserOpsPerAggregator calldata opa = opsPerAggregator[i]; + PackedUserOperation[] calldata ops = opa.userOps; + IAggregator aggregator = opa.aggregator; + + //address(1) is special marker of "signature error" + require( + address(aggregator) != address(1), + "AA96 invalid aggregator" + ); + + if (address(aggregator) != address(0)) { + // solhint-disable-next-line no-empty-blocks + try aggregator.validateSignatures(ops, opa.signature) {} catch { + revert SignatureValidationFailed(address(aggregator)); + } + } + + totalOps += ops.length; + } + + UserOpInfo[] memory opInfos = new UserOpInfo[](totalOps); + + uint256 opIndex = 0; + for (uint256 a = 0; a < opasLen; a++) { + UserOpsPerAggregator calldata opa = opsPerAggregator[a]; + PackedUserOperation[] calldata ops = opa.userOps; + IAggregator aggregator = opa.aggregator; + + uint256 opslen = ops.length; + for (uint256 i = 0; i < opslen; i++) { + UserOpInfo memory opInfo = opInfos[opIndex]; + ( + uint256 validationData, + uint256 paymasterValidationData + ) = _validatePrepayment(opIndex, ops[i], opInfo); + _validateAccountAndPaymasterValidationData( + i, + validationData, + paymasterValidationData, + address(aggregator) + ); + opIndex++; + } + } + + emit BeforeExecution(); + + uint256 collected = 0; + opIndex = 0; + for (uint256 a = 0; a < opasLen; a++) { + UserOpsPerAggregator calldata opa = opsPerAggregator[a]; + emit SignatureAggregatorChanged(address(opa.aggregator)); + PackedUserOperation[] calldata ops = opa.userOps; + uint256 opslen = ops.length; + + for (uint256 i = 0; i < opslen; i++) { + collected += _executeUserOp(opIndex, ops[i], opInfos[opIndex]); + opIndex++; + } + } + emit SignatureAggregatorChanged(address(0)); + + _compensate(beneficiary, collected); + } + + /** + * A memory copy of UserOp static fields only. + * Excluding: callData, initCode and signature. Replacing paymasterAndData with paymaster. + */ + struct MemoryUserOp { + address sender; + uint256 nonce; + uint256 verificationGasLimit; + uint256 callGasLimit; + uint256 paymasterVerificationGasLimit; + uint256 paymasterPostOpGasLimit; + uint256 preVerificationGas; + address paymaster; + uint256 maxFeePerGas; + uint256 maxPriorityFeePerGas; + } + + struct UserOpInfo { + MemoryUserOp mUserOp; + bytes32 userOpHash; + uint256 prefund; + uint256 contextOffset; + uint256 preOpGas; + } + + /** + * Inner function to handle a UserOperation. + * Must be declared "external" to open a call context, but it can only be called by handleOps. + * @param callData - The callData to execute. + * @param opInfo - The UserOpInfo struct. + * @param context - The context bytes. + * @return actualGasCost - the actual cost in eth this UserOperation paid for gas + */ + function innerHandleOp( + bytes memory callData, + UserOpInfo memory opInfo, + bytes calldata context + ) external returns (uint256 actualGasCost) { + uint256 preGas = gasleft(); + require(msg.sender == address(this), "AA92 internal call only"); + MemoryUserOp memory mUserOp = opInfo.mUserOp; + + uint256 callGasLimit = mUserOp.callGasLimit; + unchecked { + // handleOps was called with gas limit too low. abort entire bundle. + if ( + gasleft() * 63 / 64 < + callGasLimit + + mUserOp.paymasterPostOpGasLimit + + INNER_GAS_OVERHEAD + ) { + assembly ("memory-safe") { + mstore(0, INNER_OUT_OF_GAS) + revert(0, 32) + } + } + } + + IPaymaster.PostOpMode mode = IPaymaster.PostOpMode.opSucceeded; + if (callData.length > 0) { + bool success = Exec.call(mUserOp.sender, 0, callData, callGasLimit); + if (!success) { + bytes memory result = Exec.getReturnData(REVERT_REASON_MAX_LEN); + if (result.length > 0) { + emit UserOperationRevertReason( + opInfo.userOpHash, + mUserOp.sender, + mUserOp.nonce, + result + ); + } + mode = IPaymaster.PostOpMode.opReverted; + } + } + + unchecked { + uint256 actualGas = preGas - gasleft() + opInfo.preOpGas; + return _postExecution(mode, opInfo, context, actualGas); + } + } + + /// @inheritdoc IEntryPoint + function getUserOpHash( + PackedUserOperation calldata userOp + ) public view returns (bytes32) { + return + keccak256(abi.encode(userOp.hash(), address(this), block.chainid)); + } + + /** + * Copy general fields from userOp into the memory opInfo structure. + * @param userOp - The user operation. + * @param mUserOp - The memory user operation. + */ + function _copyUserOpToMemory( + PackedUserOperation calldata userOp, + MemoryUserOp memory mUserOp + ) internal pure { + mUserOp.sender = userOp.sender; + mUserOp.nonce = userOp.nonce; + (mUserOp.verificationGasLimit, mUserOp.callGasLimit) = UserOperationLib.unpackUints(userOp.accountGasLimits); + mUserOp.preVerificationGas = userOp.preVerificationGas; + (mUserOp.maxPriorityFeePerGas, mUserOp.maxFeePerGas) = UserOperationLib.unpackUints(userOp.gasFees); + bytes calldata paymasterAndData = userOp.paymasterAndData; + if (paymasterAndData.length > 0) { + require( + paymasterAndData.length >= UserOperationLib.PAYMASTER_DATA_OFFSET, + "AA93 invalid paymasterAndData" + ); + (mUserOp.paymaster, mUserOp.paymasterVerificationGasLimit, mUserOp.paymasterPostOpGasLimit) = UserOperationLib.unpackPaymasterStaticFields(paymasterAndData); + } else { + mUserOp.paymaster = address(0); + mUserOp.paymasterVerificationGasLimit = 0; + mUserOp.paymasterPostOpGasLimit = 0; + } + } + + /** + * Get the required prefunded gas fee amount for an operation. + * @param mUserOp - The user operation in memory. + */ + function _getRequiredPrefund( + MemoryUserOp memory mUserOp + ) internal pure returns (uint256 requiredPrefund) { + unchecked { + uint256 requiredGas = mUserOp.verificationGasLimit + + mUserOp.callGasLimit + + mUserOp.paymasterVerificationGasLimit + + mUserOp.paymasterPostOpGasLimit + + mUserOp.preVerificationGas; + + requiredPrefund = requiredGas * mUserOp.maxFeePerGas; + } + } + + /** + * Create sender smart contract account if init code is provided. + * @param opIndex - The operation index. + * @param opInfo - The operation info. + * @param initCode - The init code for the smart contract account. + */ + function _createSenderIfNeeded( + uint256 opIndex, + UserOpInfo memory opInfo, + bytes calldata initCode + ) internal { + if (initCode.length != 0) { + address sender = opInfo.mUserOp.sender; + if (sender.code.length != 0) + revert FailedOp(opIndex, "AA10 sender already constructed"); + address sender1 = senderCreator().createSender{ + gas: opInfo.mUserOp.verificationGasLimit + }(initCode); + if (sender1 == address(0)) + revert FailedOp(opIndex, "AA13 initCode failed or OOG"); + if (sender1 != sender) + revert FailedOp(opIndex, "AA14 initCode must return sender"); + if (sender1.code.length == 0) + revert FailedOp(opIndex, "AA15 initCode must create sender"); + address factory = address(bytes20(initCode[0:20])); + emit AccountDeployed( + opInfo.userOpHash, + sender, + factory, + opInfo.mUserOp.paymaster + ); + } + } + + /// @inheritdoc IEntryPoint + function getSenderAddress(bytes calldata initCode) public { + address sender = senderCreator().createSender(initCode); + revert SenderAddressResult(sender); + } + + /** + * Call account.validateUserOp. + * Revert (with FailedOp) in case validateUserOp reverts, or account didn't send required prefund. + * Decrement account's deposit if needed. + * @param opIndex - The operation index. + * @param op - The user operation. + * @param opInfo - The operation info. + * @param requiredPrefund - The required prefund amount. + */ + function _validateAccountPrepayment( + uint256 opIndex, + PackedUserOperation calldata op, + UserOpInfo memory opInfo, + uint256 requiredPrefund, + uint256 verificationGasLimit + ) + internal + returns ( + uint256 validationData + ) + { + unchecked { + MemoryUserOp memory mUserOp = opInfo.mUserOp; + address sender = mUserOp.sender; + _createSenderIfNeeded(opIndex, opInfo, op.initCode); + address paymaster = mUserOp.paymaster; + uint256 missingAccountFunds = 0; + if (paymaster == address(0)) { + uint256 bal = balanceOf(sender); + missingAccountFunds = bal > requiredPrefund + ? 0 + : requiredPrefund - bal; + } + try + IAccount(sender).validateUserOp{ + gas: verificationGasLimit + }(op, opInfo.userOpHash, missingAccountFunds) + returns (uint256 _validationData) { + validationData = _validationData; + } catch { + revert FailedOpWithRevert(opIndex, "AA23 reverted", Exec.getReturnData(REVERT_REASON_MAX_LEN)); + } + if (paymaster == address(0)) { + DepositInfo storage senderInfo = deposits[sender]; + uint256 deposit = senderInfo.deposit; + if (requiredPrefund > deposit) { + revert FailedOp(opIndex, "AA21 didn't pay prefund"); + } + senderInfo.deposit = deposit - requiredPrefund; + } + } + } + + /** + * In case the request has a paymaster: + * - Validate paymaster has enough deposit. + * - Call paymaster.validatePaymasterUserOp. + * - Revert with proper FailedOp in case paymaster reverts. + * - Decrement paymaster's deposit. + * @param opIndex - The operation index. + * @param op - The user operation. + * @param opInfo - The operation info. + * @param requiredPreFund - The required prefund amount. + */ + function _validatePaymasterPrepayment( + uint256 opIndex, + PackedUserOperation calldata op, + UserOpInfo memory opInfo, + uint256 requiredPreFund + ) internal returns (bytes memory context, uint256 validationData) { + unchecked { + uint256 preGas = gasleft(); + MemoryUserOp memory mUserOp = opInfo.mUserOp; + address paymaster = mUserOp.paymaster; + DepositInfo storage paymasterInfo = deposits[paymaster]; + uint256 deposit = paymasterInfo.deposit; + if (deposit < requiredPreFund) { + revert FailedOp(opIndex, "AA31 paymaster deposit too low"); + } + paymasterInfo.deposit = deposit - requiredPreFund; + uint256 pmVerificationGasLimit = mUserOp.paymasterVerificationGasLimit; + try + IPaymaster(paymaster).validatePaymasterUserOp{gas: pmVerificationGasLimit}( + op, + opInfo.userOpHash, + requiredPreFund + ) + returns (bytes memory _context, uint256 _validationData) { + context = _context; + validationData = _validationData; + } catch { + revert FailedOpWithRevert(opIndex, "AA33 reverted", Exec.getReturnData(REVERT_REASON_MAX_LEN)); + } + if (preGas - gasleft() > pmVerificationGasLimit) { + revert FailedOp(opIndex, "AA36 over paymasterVerificationGasLimit"); + } + } + } + + /** + * Revert if either account validationData or paymaster validationData is expired. + * @param opIndex - The operation index. + * @param validationData - The account validationData. + * @param paymasterValidationData - The paymaster validationData. + * @param expectedAggregator - The expected aggregator. + */ + function _validateAccountAndPaymasterValidationData( + uint256 opIndex, + uint256 validationData, + uint256 paymasterValidationData, + address expectedAggregator + ) internal view { + (address aggregator, bool outOfTimeRange) = _getValidationData( + validationData + ); + if (expectedAggregator != aggregator) { + revert FailedOp(opIndex, "AA24 signature error"); + } + if (outOfTimeRange) { + revert FailedOp(opIndex, "AA22 expired or not due"); + } + // pmAggregator is not a real signature aggregator: we don't have logic to handle it as address. + // Non-zero address means that the paymaster fails due to some signature check (which is ok only during estimation). + address pmAggregator; + (pmAggregator, outOfTimeRange) = _getValidationData( + paymasterValidationData + ); + if (pmAggregator != address(0)) { + revert FailedOp(opIndex, "AA34 signature error"); + } + if (outOfTimeRange) { + revert FailedOp(opIndex, "AA32 paymaster expired or not due"); + } + } + + /** + * Parse validationData into its components. + * @param validationData - The packed validation data (sigFailed, validAfter, validUntil). + * @return aggregator the aggregator of the validationData + * @return outOfTimeRange true if current time is outside the time range of this validationData. + */ + function _getValidationData( + uint256 validationData + ) internal view returns (address aggregator, bool outOfTimeRange) { + if (validationData == 0) { + return (address(0), false); + } + ValidationData memory data = _parseValidationData(validationData); + // solhint-disable-next-line not-rely-on-time + outOfTimeRange = block.timestamp > data.validUntil || block.timestamp < data.validAfter; + aggregator = data.aggregator; + } + + /** + * Validate account and paymaster (if defined) and + * also make sure total validation doesn't exceed verificationGasLimit. + * This method is called off-chain (simulateValidation()) and on-chain (from handleOps) + * @param opIndex - The index of this userOp into the "opInfos" array. + * @param userOp - The userOp to validate. + */ + function _validatePrepayment( + uint256 opIndex, + PackedUserOperation calldata userOp, + UserOpInfo memory outOpInfo + ) + internal + returns (uint256 validationData, uint256 paymasterValidationData) + { + uint256 preGas = gasleft(); + MemoryUserOp memory mUserOp = outOpInfo.mUserOp; + _copyUserOpToMemory(userOp, mUserOp); + outOpInfo.userOpHash = getUserOpHash(userOp); + + // Validate all numeric values in userOp are well below 128 bit, so they can safely be added + // and multiplied without causing overflow. + uint256 verificationGasLimit = mUserOp.verificationGasLimit; + uint256 maxGasValues = mUserOp.preVerificationGas | + verificationGasLimit | + mUserOp.callGasLimit | + mUserOp.paymasterVerificationGasLimit | + mUserOp.paymasterPostOpGasLimit | + mUserOp.maxFeePerGas | + mUserOp.maxPriorityFeePerGas; + require(maxGasValues <= type(uint120).max, "AA94 gas values overflow"); + + uint256 requiredPreFund = _getRequiredPrefund(mUserOp); + validationData = _validateAccountPrepayment( + opIndex, + userOp, + outOpInfo, + requiredPreFund, + verificationGasLimit + ); + + if (!_validateAndUpdateNonce(mUserOp.sender, mUserOp.nonce)) { + revert FailedOp(opIndex, "AA25 invalid account nonce"); + } + + unchecked { + if (preGas - gasleft() > verificationGasLimit) { + revert FailedOp(opIndex, "AA26 over verificationGasLimit"); + } + } + + bytes memory context; + if (mUserOp.paymaster != address(0)) { + (context, paymasterValidationData) = _validatePaymasterPrepayment( + opIndex, + userOp, + outOpInfo, + requiredPreFund + ); + } + unchecked { + outOpInfo.prefund = requiredPreFund; + outOpInfo.contextOffset = getOffsetOfMemoryBytes(context); + outOpInfo.preOpGas = preGas - gasleft() + userOp.preVerificationGas; + } + } + + /** + * Process post-operation, called just after the callData is executed. + * If a paymaster is defined and its validation returned a non-empty context, its postOp is called. + * The excess amount is refunded to the account (or paymaster - if it was used in the request). + * @param mode - Whether is called from innerHandleOp, or outside (postOpReverted). + * @param opInfo - UserOp fields and info collected during validation. + * @param context - The context returned in validatePaymasterUserOp. + * @param actualGas - The gas used so far by this user operation. + */ + function _postExecution( + IPaymaster.PostOpMode mode, + UserOpInfo memory opInfo, + bytes memory context, + uint256 actualGas + ) private returns (uint256 actualGasCost) { + uint256 preGas = gasleft(); + unchecked { + address refundAddress; + MemoryUserOp memory mUserOp = opInfo.mUserOp; + uint256 gasPrice = getUserOpGasPrice(mUserOp); + + address paymaster = mUserOp.paymaster; + if (paymaster == address(0)) { + refundAddress = mUserOp.sender; + } else { + refundAddress = paymaster; + if (context.length > 0) { + actualGasCost = actualGas * gasPrice; + if (mode != IPaymaster.PostOpMode.postOpReverted) { + try IPaymaster(paymaster).postOp{ + gas: mUserOp.paymasterPostOpGasLimit + }(mode, context, actualGasCost, gasPrice) + // solhint-disable-next-line no-empty-blocks + {} catch { + bytes memory reason = Exec.getReturnData(REVERT_REASON_MAX_LEN); + revert PostOpReverted(reason); + } + } + } + } + actualGas += preGas - gasleft(); + + // Calculating a penalty for unused execution gas + { + uint256 executionGasLimit = mUserOp.callGasLimit + mUserOp.paymasterPostOpGasLimit; + uint256 executionGasUsed = actualGas - opInfo.preOpGas; + // this check is required for the gas used within EntryPoint and not covered by explicit gas limits + if (executionGasLimit > executionGasUsed) { + uint256 unusedGas = executionGasLimit - executionGasUsed; + uint256 unusedGasPenalty = (unusedGas * PENALTY_PERCENT) / 100; + actualGas += unusedGasPenalty; + } + } + + actualGasCost = actualGas * gasPrice; + uint256 prefund = opInfo.prefund; + if (prefund < actualGasCost) { + if (mode == IPaymaster.PostOpMode.postOpReverted) { + actualGasCost = prefund; + emitPrefundTooLow(opInfo); + emitUserOperationEvent(opInfo, false, actualGasCost, actualGas); + } else { + assembly ("memory-safe") { + mstore(0, INNER_REVERT_LOW_PREFUND) + revert(0, 32) + } + } + } else { + uint256 refund = prefund - actualGasCost; + _incrementDeposit(refundAddress, refund); + bool success = mode == IPaymaster.PostOpMode.opSucceeded; + emitUserOperationEvent(opInfo, success, actualGasCost, actualGas); + } + } // unchecked + } + + /** + * The gas price this UserOp agrees to pay. + * Relayer/block builder might submit the TX with higher priorityFee, but the user should not. + * @param mUserOp - The userOp to get the gas price from. + */ + function getUserOpGasPrice( + MemoryUserOp memory mUserOp + ) internal view returns (uint256) { + unchecked { + uint256 maxFeePerGas = mUserOp.maxFeePerGas; + uint256 maxPriorityFeePerGas = mUserOp.maxPriorityFeePerGas; + if (maxFeePerGas == maxPriorityFeePerGas) { + //legacy mode (for networks that don't support basefee opcode) + return maxFeePerGas; + } + return min(maxFeePerGas, maxPriorityFeePerGas + block.basefee); + } + } + + /** + * The offset of the given bytes in memory. + * @param data - The bytes to get the offset of. + */ + function getOffsetOfMemoryBytes( + bytes memory data + ) internal pure returns (uint256 offset) { + assembly { + offset := data + } + } + + /** + * The bytes in memory at the given offset. + * @param offset - The offset to get the bytes from. + */ + function getMemoryBytesFromOffset( + uint256 offset + ) internal pure returns (bytes memory data) { + assembly ("memory-safe") { + data := offset + } + } + + /// @inheritdoc IEntryPoint + function delegateAndRevert(address target, bytes calldata data) external { + (bool success, bytes memory ret) = target.delegatecall(data); + revert DelegateAndRevert(success, ret); + } +} From f6b4454a2a7c6ab69bd8ab3521ff0be9f201d00b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Fri, 6 Dec 2024 16:54:08 -0600 Subject: [PATCH 14/51] Run --ir-minimum in forge coverage --- scripts/checks/coverage.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/checks/coverage.sh b/scripts/checks/coverage.sh index 12e23235..a591069c 100755 --- a/scripts/checks/coverage.sh +++ b/scripts/checks/coverage.sh @@ -10,7 +10,7 @@ hardhat coverage if [ "${CI:-"false"}" == "true" ]; then # Foundry coverage - forge coverage --report lcov + forge coverage --report lcov --ir-minimum # Remove zero hits sed -i '/,0/d' lcov.info fi From 2d2300f31c53b1d1427e8e342b7399719db59f67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Fri, 6 Dec 2024 19:18:00 -0600 Subject: [PATCH 15/51] up --- contracts/account/draft-AccountBase.sol | 21 ++++++++++++++++--- contracts/account/draft-AccountECDSA.sol | 16 ++++++++++++-- contracts/account/draft-AccountP256.sol | 16 ++++++++++++-- contracts/account/draft-AccountRSA.sol | 18 +++++++++++++--- contracts/mocks/account/AccountBaseMock.sol | 7 +++++++ .../cryptography/draft-ERC7739Signer.sol | 8 +++---- test/account/draft-AccountRSA.test.js | 4 ++-- test/helpers/signers.js | 7 +++++++ 8 files changed, 81 insertions(+), 16 deletions(-) diff --git a/contracts/account/draft-AccountBase.sol b/contracts/account/draft-AccountBase.sol index c38cbd4e..b050bba5 100644 --- a/contracts/account/draft-AccountBase.sol +++ b/contracts/account/draft-AccountBase.sol @@ -54,6 +54,21 @@ abstract contract AccountBase is IAccount, IAccountExecute { return entryPoint().getNonce(address(this), key); } + /** + * @dev Returns the digest the offchain signer signed instead of the opaque `userOpHash`. + * + * Given the `userOpHash` calculation is defined by ERC-4337, offchain signers + * may need to sign again this hash by rehashing it with other schemes. + * + * Returns the `userOpHash` by default. + */ + function _userOpSignedHash( + PackedUserOperation calldata /* userOp */, + bytes32 userOpHash + ) internal view virtual returns (bytes32) { + return userOpHash; + } + /** * @inheritdoc IAccount */ @@ -62,7 +77,7 @@ abstract contract AccountBase is IAccount, IAccountExecute { bytes32 userOpHash, uint256 missingAccountFunds ) public virtual onlyEntryPoint returns (uint256) { - uint256 validationData = _validateUserOp(userOp, userOpHash); + uint256 validationData = _validateUserOp(userOp, _userOpSignedHash(userOp, userOpHash)); _payPrefund(missingAccountFunds); return validationData; } @@ -79,7 +94,7 @@ abstract contract AccountBase is IAccount, IAccountExecute { } /** - * @dev Validation logic for {validateUserOp}. + * @dev Validation logic for {validateUserOp}. The `userOpSignedHash` is the digest from {_userOpSignedHash}. * * IMPORTANT: Implementing a mechanism to validate user operations is a security-sensitive operation * as it may allow an attacker to bypass the account's security measures. Check out {AccountECDSA}, @@ -87,7 +102,7 @@ abstract contract AccountBase is IAccount, IAccountExecute { */ function _validateUserOp( PackedUserOperation calldata userOp, - bytes32 userOpHash + bytes32 userOpSignedHash ) internal virtual returns (uint256 validationData); /** diff --git a/contracts/account/draft-AccountECDSA.sol b/contracts/account/draft-AccountECDSA.sol index d53b913f..9308ad95 100644 --- a/contracts/account/draft-AccountECDSA.sol +++ b/contracts/account/draft-AccountECDSA.sol @@ -35,17 +35,29 @@ abstract contract AccountECDSA is ERC165, ERC7739Signer, ERC721Holder, ERC1155Ho return _signer; } + /** + * @dev Returns the ERC-191 signed `userOpHash` hashed with keccak256 using `personal_sign`. + */ + function _userOpSignedHash( + PackedUserOperation calldata /* userOp */, + bytes32 userOpHash + ) internal view virtual override returns (bytes32) { + return userOpHash.toEthSignedMessageHash(); + } + /** * @dev Internal version of {validateUserOp} that relies on {_validateNestedEIP712Signature}. * + * The `userOpSignedHash` is the digest from {_userOpSignedHash}. + * * NOTE: To override the signature functionality, try overriding {_validateNestedEIP712Signature} instead. */ function _validateUserOp( PackedUserOperation calldata userOp, - bytes32 userOpHash + bytes32 userOpSignedHash ) internal view virtual override returns (uint256) { return - _isValidSignature(userOpHash.toEthSignedMessageHash(), userOp.signature) + _isValidSignature(userOpSignedHash, userOp.signature) ? ERC4337Utils.SIG_VALIDATION_SUCCESS : ERC4337Utils.SIG_VALIDATION_FAILED; } diff --git a/contracts/account/draft-AccountP256.sol b/contracts/account/draft-AccountP256.sol index ab8576fa..b2a033d2 100644 --- a/contracts/account/draft-AccountP256.sol +++ b/contracts/account/draft-AccountP256.sol @@ -37,17 +37,29 @@ abstract contract AccountP256 is ERC165, ERC7739Signer, ERC721Holder, ERC1155Hol return (_qx, _qy); } + /** + * @dev Returns the ERC-191 signed `userOpHash` hashed with keccak256 using `personal_sign`. + */ + function _userOpSignedHash( + PackedUserOperation calldata /* userOp */, + bytes32 userOpHash + ) internal view virtual override returns (bytes32) { + return userOpHash.toEthSignedMessageHash(); + } + /** * @dev Internal version of {validateUserOp} that relies on {_validateNestedEIP712Signature}. * + * The `userOpSignedHash` is the digest from {_userOpSignedHash}. + * * NOTE: To override the signature functionality, try overriding {_validateNestedEIP712Signature} instead. */ function _validateUserOp( PackedUserOperation calldata userOp, - bytes32 userOpHash + bytes32 userOpSignedHash ) internal view virtual override returns (uint256) { return - _isValidSignature(userOpHash.toEthSignedMessageHash(), userOp.signature) + _isValidSignature(userOpSignedHash, userOp.signature) ? ERC4337Utils.SIG_VALIDATION_SUCCESS : ERC4337Utils.SIG_VALIDATION_FAILED; } diff --git a/contracts/account/draft-AccountRSA.sol b/contracts/account/draft-AccountRSA.sol index bd57e30d..61b761f3 100644 --- a/contracts/account/draft-AccountRSA.sol +++ b/contracts/account/draft-AccountRSA.sol @@ -42,17 +42,29 @@ abstract contract AccountRSA is ERC165, ERC7739Signer, ERC721Holder, ERC1155Hold return (_e, _n); } + /** + * @dev Returns the ERC-191 signed `userOpHash` hashed with keccak256 using `personal_sign`. + */ + function _userOpSignedHash( + PackedUserOperation calldata /* userOp */, + bytes32 userOpHash + ) internal view virtual override returns (bytes32) { + return userOpHash.toEthSignedMessageHash(); + } + /** * @dev Internal version of {validateUserOp} that relies on {_validateNestedEIP712Signature}. * + * The `userOpSignedHash` is the digest from {_userOpSignedHash}. + * * NOTE: To override the signature functionality, try overriding {_validateNestedEIP712Signature} instead. */ function _validateUserOp( PackedUserOperation calldata userOp, - bytes32 userOpHash + bytes32 userOpSignedHash ) internal view virtual override returns (uint256) { return - _isValidSignature(userOpHash.toEthSignedMessageHash(), userOp.signature) + _isValidSignature(userOpSignedHash, userOp.signature) ? ERC4337Utils.SIG_VALIDATION_SUCCESS : ERC4337Utils.SIG_VALIDATION_FAILED; } @@ -68,7 +80,7 @@ abstract contract AccountRSA is ERC165, ERC7739Signer, ERC721Holder, ERC1155Hold bytes calldata signature ) internal view virtual override returns (bool) { (bytes memory e, bytes memory n) = signer(); - return RSA.pkcs1Sha256(hash, signature, e, n); + return RSA.pkcs1Sha256(abi.encodePacked(hash), signature, e, n); } /// @inheritdoc ERC165 diff --git a/contracts/mocks/account/AccountBaseMock.sol b/contracts/mocks/account/AccountBaseMock.sol index 42098311..bdb5a709 100644 --- a/contracts/mocks/account/AccountBaseMock.sol +++ b/contracts/mocks/account/AccountBaseMock.sol @@ -17,4 +17,11 @@ contract AccountBaseMock is AccountBase { ? ERC4337Utils.SIG_VALIDATION_SUCCESS : ERC4337Utils.SIG_VALIDATION_FAILED; } + + function _userOpSignedHash( + PackedUserOperation calldata /* userOp */, + bytes32 userOpHash + ) internal pure override returns (bytes32) { + return userOpHash; + } } diff --git a/contracts/utils/cryptography/draft-ERC7739Signer.sol b/contracts/utils/cryptography/draft-ERC7739Signer.sol index d15aad22..59390fef 100644 --- a/contracts/utils/cryptography/draft-ERC7739Signer.sol +++ b/contracts/utils/cryptography/draft-ERC7739Signer.sol @@ -55,11 +55,11 @@ abstract contract ERC7739Signer is EIP712, IERC1271 { /** * @dev Nested personal signature verification. + * + * NOTE: Instead of overriding this function, try with {_validateNestedEIP712Signature}. It encapsulates + * nested EIP-712 hashes. */ - function _isValidNestedPersonalSignSignature( - bytes32 hash, - bytes calldata signature - ) internal view virtual returns (bool) { + function _isValidNestedPersonalSignSignature(bytes32 hash, bytes calldata signature) internal view returns (bool) { return _validateNestedEIP712Signature( _domainSeparatorV4().toTypedDataHash(hash.personalSignStructHash()), diff --git a/test/account/draft-AccountRSA.test.js b/test/account/draft-AccountRSA.test.js index 3793b635..b069ffa2 100644 --- a/test/account/draft-AccountRSA.test.js +++ b/test/account/draft-AccountRSA.test.js @@ -6,13 +6,13 @@ const { } = require('./Account.behavior'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { ERC4337Helper } = require('../helpers/erc4337'); -const { RSASigner } = require('../helpers/signers'); +const { RSASignerSHA256 } = require('../helpers/signers'); const { shouldBehaveLikeERC7739Signer } = require('../utils/cryptography/ERC7739Signer.behavior'); async function fixture() { const [beneficiary, other] = await ethers.getSigners(); const target = await ethers.deployContract('CallReceiverMockExtended'); - const signer = new RSASigner(); + const signer = new RSASignerSHA256(); const helper = new ERC4337Helper('$AccountRSA'); const smartAccount = await helper.newAccount(['AccountRSA', '1', signer.publicKey.e, signer.publicKey.n]); const domain = { diff --git a/test/helpers/signers.js b/test/helpers/signers.js index 27ec908b..4645998a 100644 --- a/test/helpers/signers.js +++ b/test/helpers/signers.js @@ -100,9 +100,16 @@ class RSASigner extends ERC7739Signer { } } +class RSASignerSHA256 extends RSASigner { + _signRaw(messageHash) { + return super._signRaw(ethers.sha256(ethers.solidityPacked(['bytes32'], [messageHash]))); + } +} + module.exports = { BooleanSigner, ECDSASigner, P256Signer, RSASigner, + RSASignerSHA256, }; From d6ba190d3c6d0e7717bcb605df61f4be19ad1eae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Fri, 6 Dec 2024 20:47:13 -0600 Subject: [PATCH 16/51] Make Accounts initializable --- contracts/account/draft-AccountECDSA.sol | 13 ++++++++++--- contracts/account/draft-AccountP256.sol | 12 +++++++++--- contracts/account/draft-AccountRSA.sol | 8 +++++++- contracts/mocks/account/AccountECDSAMock.sol | 14 ++++++++++++++ contracts/mocks/account/AccountP256Mock.sol | 14 ++++++++++++++ contracts/mocks/account/AccountRSAMock.sol | 14 ++++++++++++++ test/account/draft-AccountECDSA.test.js | 2 +- test/account/draft-AccountP256.test.js | 2 +- test/account/draft-AccountRSA.test.js | 2 +- 9 files changed, 71 insertions(+), 10 deletions(-) create mode 100644 contracts/mocks/account/AccountECDSAMock.sol create mode 100644 contracts/mocks/account/AccountP256Mock.sol create mode 100644 contracts/mocks/account/AccountRSAMock.sol diff --git a/contracts/account/draft-AccountECDSA.sol b/contracts/account/draft-AccountECDSA.sol index 9308ad95..152c7f18 100644 --- a/contracts/account/draft-AccountECDSA.sol +++ b/contracts/account/draft-AccountECDSA.sol @@ -11,7 +11,8 @@ import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import {AccountBase} from "./draft-AccountBase.sol"; -import {ERC7739Signer} from "../utils/cryptography/draft-ERC7739Signer.sol"; +import {ERC7739Signer, EIP712} from "../utils/cryptography/draft-ERC7739Signer.sol"; +import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; /** * @dev Account implementation using {ECDSA} signatures and {ERC7739Signer} for replay protection. @@ -19,12 +20,18 @@ import {ERC7739Signer} from "../utils/cryptography/draft-ERC7739Signer.sol"; abstract contract AccountECDSA is ERC165, ERC7739Signer, ERC721Holder, ERC1155HolderLean, AccountBase { using MessageHashUtils for bytes32; - address private immutable _signer; + /** + * @dev The {signer} is already initialized. + */ + error AccountECDSAUninitializedSigner(address signer); + + address private _signer; /** * @dev Initializes the account with the address of the native signer. */ - constructor(address signerAddr) { + function _initializeSigner(address signerAddr) internal { + if (_signer != address(0)) revert AccountECDSAUninitializedSigner(signerAddr); _signer = signerAddr; } diff --git a/contracts/account/draft-AccountP256.sol b/contracts/account/draft-AccountP256.sol index b2a033d2..83c6a2b8 100644 --- a/contracts/account/draft-AccountP256.sol +++ b/contracts/account/draft-AccountP256.sol @@ -19,13 +19,19 @@ import {ERC7739Signer} from "../utils/cryptography/draft-ERC7739Signer.sol"; abstract contract AccountP256 is ERC165, ERC7739Signer, ERC721Holder, ERC1155HolderLean, AccountBase { using MessageHashUtils for bytes32; - bytes32 private immutable _qx; - bytes32 private immutable _qy; + /** + * @dev The {signer} is already initialized. + */ + error AccountP256UninitializedSigner(bytes32 qx, bytes32 qy); + + bytes32 private _qx; + bytes32 private _qy; /** * @dev Initializes the account with the P256 public key. */ - constructor(bytes32 qx, bytes32 qy) { + function _initializeSigner(bytes32 qx, bytes32 qy) internal { + if (_qx != 0 || _qy != 0) revert AccountP256UninitializedSigner(qx, qy); _qx = qx; _qy = qy; } diff --git a/contracts/account/draft-AccountRSA.sol b/contracts/account/draft-AccountRSA.sol index 61b761f3..9e5dfdbe 100644 --- a/contracts/account/draft-AccountRSA.sol +++ b/contracts/account/draft-AccountRSA.sol @@ -24,13 +24,19 @@ import {ERC7739Signer} from "../utils/cryptography/draft-ERC7739Signer.sol"; abstract contract AccountRSA is ERC165, ERC7739Signer, ERC721Holder, ERC1155HolderLean, AccountBase { using MessageHashUtils for bytes32; + /** + * @dev The {signer} is already initialized. + */ + error AccountP256UninitializedSigner(bytes e, bytes n); + bytes private _e; bytes private _n; /** * @dev Initializes the account with the RSA public key. */ - constructor(bytes memory e, bytes memory n) { + function _initializeSigner(bytes memory e, bytes memory n) internal { + if (_e.length != 0 || _n.length != 0) revert AccountP256UninitializedSigner(e, n); _e = e; _n = n; } diff --git a/contracts/mocks/account/AccountECDSAMock.sol b/contracts/mocks/account/AccountECDSAMock.sol new file mode 100644 index 00000000..13d6bd34 --- /dev/null +++ b/contracts/mocks/account/AccountECDSAMock.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; +import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol"; +import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; +import {AccountECDSA} from "../../account/draft-AccountECDSA.sol"; + +contract AccountECDSAMock is AccountECDSA { + constructor(string memory name, string memory version, address signerAddr) EIP712(name, version) { + _initializeSigner(signerAddr); + } +} diff --git a/contracts/mocks/account/AccountP256Mock.sol b/contracts/mocks/account/AccountP256Mock.sol new file mode 100644 index 00000000..9a348d79 --- /dev/null +++ b/contracts/mocks/account/AccountP256Mock.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; +import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol"; +import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; +import {AccountP256} from "../../account/draft-AccountP256.sol"; + +contract AccountP256Mock is AccountP256 { + constructor(string memory name, string memory version, bytes32 qx, bytes32 qy) EIP712(name, version) { + _initializeSigner(qx, qy); + } +} diff --git a/contracts/mocks/account/AccountRSAMock.sol b/contracts/mocks/account/AccountRSAMock.sol new file mode 100644 index 00000000..d8c0c9b6 --- /dev/null +++ b/contracts/mocks/account/AccountRSAMock.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; +import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol"; +import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; +import {AccountRSA} from "../../account/draft-AccountRSA.sol"; + +contract AccountRSAMock is AccountRSA { + constructor(string memory name, string memory version, bytes memory e, bytes memory n) EIP712(name, version) { + _initializeSigner(e, n); + } +} diff --git a/test/account/draft-AccountECDSA.test.js b/test/account/draft-AccountECDSA.test.js index 3eb86f64..b8848932 100644 --- a/test/account/draft-AccountECDSA.test.js +++ b/test/account/draft-AccountECDSA.test.js @@ -13,7 +13,7 @@ async function fixture() { const [beneficiary, other] = await ethers.getSigners(); const target = await ethers.deployContract('CallReceiverMockExtended'); const signer = new ECDSASigner(); - const helper = new ERC4337Helper('$AccountECDSA'); + const helper = new ERC4337Helper('$AccountECDSAMock'); const smartAccount = await helper.newAccount(['AccountECDSA', '1', signer.EOA.address]); const domain = { name: 'AccountECDSA', diff --git a/test/account/draft-AccountP256.test.js b/test/account/draft-AccountP256.test.js index 23d9c275..59f3c744 100644 --- a/test/account/draft-AccountP256.test.js +++ b/test/account/draft-AccountP256.test.js @@ -13,7 +13,7 @@ async function fixture() { const [beneficiary, other] = await ethers.getSigners(); const target = await ethers.deployContract('CallReceiverMockExtended'); const signer = new P256Signer(); - const helper = new ERC4337Helper('$AccountP256'); + const helper = new ERC4337Helper('$AccountP256Mock'); const smartAccount = await helper.newAccount(['AccountP256', '1', signer.publicKey.qx, signer.publicKey.qy]); const domain = { name: 'AccountP256', diff --git a/test/account/draft-AccountRSA.test.js b/test/account/draft-AccountRSA.test.js index b069ffa2..56163432 100644 --- a/test/account/draft-AccountRSA.test.js +++ b/test/account/draft-AccountRSA.test.js @@ -13,7 +13,7 @@ async function fixture() { const [beneficiary, other] = await ethers.getSigners(); const target = await ethers.deployContract('CallReceiverMockExtended'); const signer = new RSASignerSHA256(); - const helper = new ERC4337Helper('$AccountRSA'); + const helper = new ERC4337Helper('$AccountRSAMock'); const smartAccount = await helper.newAccount(['AccountRSA', '1', signer.publicKey.e, signer.publicKey.n]); const domain = { name: 'AccountRSA', From 208386fe59a0405838af3c6a4a62f9d8fd6f4f56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Sat, 7 Dec 2024 14:11:33 -0600 Subject: [PATCH 17/51] Finish docs --- contracts/account/draft-AccountBase.sol | 2 +- contracts/account/draft-AccountECDSA.sol | 8 +- contracts/account/draft-AccountP256.sol | 6 + contracts/account/draft-AccountRSA.sol | 9 +- contracts/mocks/account/AccountBaseMock.sol | 7 - .../mocks/docs/account/MyAccountCustom.sol | 44 +++++ .../mocks/docs/account/MyAccountECDSA.sol | 22 +++ .../mocks/docs/account/MyAccountP256.sol | 22 +++ contracts/mocks/docs/account/MyAccountRSA.sol | 22 +++ .../docs/account/MyFactoryAccountECDSA.sol | 37 +++++ docs/modules/ROOT/nav.adoc | 1 + .../ROOT/pages/account-abstraction.adoc | 154 ++++++++++++++++++ 12 files changed, 321 insertions(+), 13 deletions(-) create mode 100644 contracts/mocks/docs/account/MyAccountCustom.sol create mode 100644 contracts/mocks/docs/account/MyAccountECDSA.sol create mode 100644 contracts/mocks/docs/account/MyAccountP256.sol create mode 100644 contracts/mocks/docs/account/MyAccountRSA.sol create mode 100644 contracts/mocks/docs/account/MyFactoryAccountECDSA.sol create mode 100644 docs/modules/ROOT/pages/account-abstraction.adoc diff --git a/contracts/account/draft-AccountBase.sol b/contracts/account/draft-AccountBase.sol index b050bba5..eeea0cd0 100644 --- a/contracts/account/draft-AccountBase.sol +++ b/contracts/account/draft-AccountBase.sol @@ -103,7 +103,7 @@ abstract contract AccountBase is IAccount, IAccountExecute { function _validateUserOp( PackedUserOperation calldata userOp, bytes32 userOpSignedHash - ) internal virtual returns (uint256 validationData); + ) internal view virtual returns (uint256 validationData); /** * @dev Sends the missing funds for executing the user operation to the {entrypoint}. diff --git a/contracts/account/draft-AccountECDSA.sol b/contracts/account/draft-AccountECDSA.sol index 152c7f18..3ed35738 100644 --- a/contracts/account/draft-AccountECDSA.sol +++ b/contracts/account/draft-AccountECDSA.sol @@ -16,6 +16,12 @@ import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.s /** * @dev Account implementation using {ECDSA} signatures and {ERC7739Signer} for replay protection. + * + * An {_initializeSigner} function is provided to set the account's signer address. Doing so it's + * easier for a factory, whose likely to use initializable clones of this contract. + * + * IMPORTANT: Avoiding to call {_initializeSigner} either during construction (if used standalone) + * or during initialization (if used as a clone) may leave the account unusable. */ abstract contract AccountECDSA is ERC165, ERC7739Signer, ERC721Holder, ERC1155HolderLean, AccountBase { using MessageHashUtils for bytes32; @@ -28,7 +34,7 @@ abstract contract AccountECDSA is ERC165, ERC7739Signer, ERC721Holder, ERC1155Ho address private _signer; /** - * @dev Initializes the account with the address of the native signer. + * @dev Initializes the account with the address of the native signer. This function is called only once. */ function _initializeSigner(address signerAddr) internal { if (_signer != address(0)) revert AccountECDSAUninitializedSigner(signerAddr); diff --git a/contracts/account/draft-AccountP256.sol b/contracts/account/draft-AccountP256.sol index 83c6a2b8..89a76cc4 100644 --- a/contracts/account/draft-AccountP256.sol +++ b/contracts/account/draft-AccountP256.sol @@ -15,6 +15,12 @@ import {ERC7739Signer} from "../utils/cryptography/draft-ERC7739Signer.sol"; /** * @dev Account implementation using {P256} signatures and {ERC7739Signer} for replay protection. + * + * An {_initializeSigner} function is provided to set the account's signer address. Doing so it's + * easier for a factory, whose likely to use initializable clones of this contract. + * + * IMPORTANT: Avoiding to call {_initializeSigner} either during construction (if used standalone) + * or during initialization (if used as a clone) may leave the account unusable. */ abstract contract AccountP256 is ERC165, ERC7739Signer, ERC721Holder, ERC1155HolderLean, AccountBase { using MessageHashUtils for bytes32; diff --git a/contracts/account/draft-AccountRSA.sol b/contracts/account/draft-AccountRSA.sol index 9e5dfdbe..67626b85 100644 --- a/contracts/account/draft-AccountRSA.sol +++ b/contracts/account/draft-AccountRSA.sol @@ -16,10 +16,11 @@ import {ERC7739Signer} from "../utils/cryptography/draft-ERC7739Signer.sol"; /** * @dev Account implementation using {RSA} signatures and {ERC7739Signer} for replay protection. * - * NOTE: Storing `_e` and `_n` in regular storage violate ERC-7562 validation rules if the contract - * is used as an ERC-1271 signer during the validation phase of a different account contract. - * Consider deploying this contract through a factory that sets `_e` and `_n` as immutable arguments - * (see {Clones-cloneDeterministicWithImmutableArgs}). + * An {_initializeSigner} function is provided to set the account's signer address. Doing so it's + * easier for a factory, whose likely to use initializable clones of this contract. + * + * IMPORTANT: Avoiding to call {_initializeSigner} either during construction (if used standalone) + * or during initialization (if used as a clone) may leave the account unusable. */ abstract contract AccountRSA is ERC165, ERC7739Signer, ERC721Holder, ERC1155HolderLean, AccountBase { using MessageHashUtils for bytes32; diff --git a/contracts/mocks/account/AccountBaseMock.sol b/contracts/mocks/account/AccountBaseMock.sol index bdb5a709..42098311 100644 --- a/contracts/mocks/account/AccountBaseMock.sol +++ b/contracts/mocks/account/AccountBaseMock.sol @@ -17,11 +17,4 @@ contract AccountBaseMock is AccountBase { ? ERC4337Utils.SIG_VALIDATION_SUCCESS : ERC4337Utils.SIG_VALIDATION_FAILED; } - - function _userOpSignedHash( - PackedUserOperation calldata /* userOp */, - bytes32 userOpHash - ) internal pure override returns (bytes32) { - return userOpHash; - } } diff --git a/contracts/mocks/docs/account/MyAccountCustom.sol b/contracts/mocks/docs/account/MyAccountCustom.sol new file mode 100644 index 00000000..69a30c5c --- /dev/null +++ b/contracts/mocks/docs/account/MyAccountCustom.sol @@ -0,0 +1,44 @@ +// contracts/MyAccountCustom.sol +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; +import {ERC4337Utils, PackedUserOperation} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol"; +import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import {AccountBase} from "../../../account/draft-AccountBase.sol"; +import {ERC7739Signer} from "../../../utils/cryptography/draft-ERC7739Signer.sol"; + +contract MyAccountCustom is ERC7739Signer, AccountBase, Initializable { + /** + * NOTE: EIP-712 domain is set at construction because each account clone + * will recalculate its domain separator based on their own address. + */ + constructor() EIP712("MyAccountCustom", "1") { + _disableInitializers(); + } + + /// @dev Set up the account (e.g. load public keys to storage). + function initialize() public virtual initializer { + // Custom initialization logic + } + + /// @dev Receives an `userOpSignedHash` to validate. See {_userOpSignedHash}. + function _validateUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpSignedHash + ) internal view virtual override returns (uint256) { + return + _isValidSignature(userOpSignedHash, userOp.signature) + ? ERC4337Utils.SIG_VALIDATION_SUCCESS + : ERC4337Utils.SIG_VALIDATION_FAILED; + } + + /// @dev Receives a hash wrapped in an EIP-712 domain separator. + function _validateNestedEIP712Signature( + bytes32 hash, + bytes calldata signature + ) internal view virtual override returns (bool) { + // Custom signing logic + } +} diff --git a/contracts/mocks/docs/account/MyAccountECDSA.sol b/contracts/mocks/docs/account/MyAccountECDSA.sol new file mode 100644 index 00000000..39bbfb7f --- /dev/null +++ b/contracts/mocks/docs/account/MyAccountECDSA.sol @@ -0,0 +1,22 @@ +// contracts/MyAccountECDSA.sol +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; +import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import {AccountECDSA} from "../../../account/draft-AccountECDSA.sol"; + +contract MyAccountECDSA is AccountECDSA, Initializable { + /** + * NOTE: EIP-712 domain is set at construction because each account clone + * will recalculate its domain separator based on their own address. + */ + constructor() EIP712("MyAccountECDSA", "1") { + _disableInitializers(); + } + + function initializeSigner(address signerAddr) public virtual initializer { + _initializeSigner(signerAddr); + } +} diff --git a/contracts/mocks/docs/account/MyAccountP256.sol b/contracts/mocks/docs/account/MyAccountP256.sol new file mode 100644 index 00000000..dc2e5d6e --- /dev/null +++ b/contracts/mocks/docs/account/MyAccountP256.sol @@ -0,0 +1,22 @@ +// contracts/MyAccountP256.sol +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; +import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import {AccountP256} from "../../../account/draft-AccountP256.sol"; + +contract MyAccountP256 is AccountP256, Initializable { + /** + * NOTE: EIP-712 domain is set at construction because each account clone + * will recalculate its domain separator based on their own address. + */ + constructor() EIP712("MyAccountP256", "1") { + _disableInitializers(); + } + + function initializeSigner(bytes32 qx, bytes32 qy) public virtual initializer { + _initializeSigner(qx, qy); + } +} diff --git a/contracts/mocks/docs/account/MyAccountRSA.sol b/contracts/mocks/docs/account/MyAccountRSA.sol new file mode 100644 index 00000000..0cf88109 --- /dev/null +++ b/contracts/mocks/docs/account/MyAccountRSA.sol @@ -0,0 +1,22 @@ +// contracts/MyAccountRSA.sol +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; +import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import {AccountRSA} from "../../../account/draft-AccountRSA.sol"; + +contract MyAccountRSA is AccountRSA, Initializable { + /** + * NOTE: EIP-712 domain is set at construction because each account clone + * will recalculate its domain separator based on their own address. + */ + constructor() EIP712("MyAccountRSA", "1") { + _disableInitializers(); + } + + function initializeSigner(bytes memory e, bytes memory n) public virtual initializer { + _initializeSigner(e, n); + } +} diff --git a/contracts/mocks/docs/account/MyFactoryAccountECDSA.sol b/contracts/mocks/docs/account/MyFactoryAccountECDSA.sol new file mode 100644 index 00000000..b3c244b1 --- /dev/null +++ b/contracts/mocks/docs/account/MyFactoryAccountECDSA.sol @@ -0,0 +1,37 @@ +// contracts/MyFactoryAccountECDSA.sol +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {MyAccountECDSA} from "./MyAccountECDSA.sol"; + +/** + * @dev An abstract factory contract to create ECDSA accounts on demand. + */ +contract MyFactoryAccountECDSA { + using Clones for address; + + address private immutable _impl = address(new MyAccountECDSA()); + + /// @dev Predict the address of the account + function predictAddress(bytes32 salt) public view returns (address) { + return _impl.predictDeterministicAddress(salt, address(this)); + } + + /// @dev Create clone accounts on demand + function cloneAndInitialize(bytes32 salt, address signer) public returns (address) { + return _cloneAndInitialize(salt, signer); + } + + /// @dev Create clone accounts on demand and return the address. Uses `signer` to initialize the clone. + function _cloneAndInitialize(bytes32 salt, address signer) internal returns (address) { + address predicted = predictAddress(salt); + if (predicted.code.length == 0) { + _impl.cloneDeterministic(salt); + Address.functionCall(predicted, abi.encodeCall(MyAccountECDSA.initializeSigner, (signer))); + } + return predicted; + } +} diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 7c0d852c..f46339c1 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -1,2 +1,3 @@ * xref:index.adoc[Overview] +* xref:account-abstraction.adoc[Account Abstraction] * xref:utilities.adoc[Utilities] diff --git a/docs/modules/ROOT/pages/account-abstraction.adoc b/docs/modules/ROOT/pages/account-abstraction.adoc new file mode 100644 index 00000000..53c8d826 --- /dev/null +++ b/docs/modules/ROOT/pages/account-abstraction.adoc @@ -0,0 +1,154 @@ += Account Abstraction + +Unlike Externally Owned Accounts (EOAs), smart contracts may contain arbitrary verification logic based on authentication mechanisms different to Ethereum's native xref:api:utils.adoc#ECDSA[ECDSA] and have execution advantages such as batching or gas sponsorship. To leverage these properties of smart contracts, the community has widely adopted https://eips.ethereum.org/EIPS/eip-4337[ERC-4337], a standard to process user operations through an alternative mempool. + +The library provides multiple contracts for Account Abstraction following this standard as it enables more flexible and user-friendly interactions with applications. Account Abstraction use cases include wallets in novel contexts (e.g. embedded wallets), more granular configuration of accounts and recovery mechanisms. These capabilities can be supercharged with a modularity approach following standards such as https://eips.ethereum.org/EIPS/eip-7579[ERC-7579]. + +== Smart Accounts + +OpenZeppelin provides an abstract implementation of an xref:api:account.adoc#AccountBase[`AccountBase`] that implements the basic logic to handle user operations in compliance with ERC-4337. Developers who want to build their own Account implementation can use this to bootstrap. + +Aside from the xref:api:account.adoc#AccountBase[`AccountBase`], the library includes various specialized accounts that implement xref:api:account.adoc#AccountBase-validateUserOp-struct-PackedUserOperation-bytes32-uint256-[`_validateUserOp`] using a digital signature verification algorithm like https://docs.openzeppelin.com/contracts/5.x/api/utils#ECDSA[`ECDSA`], https://docs.openzeppelin.com/contracts/5.x/api/utils#P256[`P256`] or https://docs.openzeppelin.com/contracts/5.x/api/utils#RSA[`RSA`]. + +These specialized accounts are ready to use and implement an opinionated set of layers: + +* xref:api:utils.adoc#ERC7739Signer[ERC7739Signer]: An implementation of the https://eips.ethereum.org/EIPS/eip-1271[ERC-1271] interface for smart contract signatures. This layer adds a defensive rehashing mechanism that prevents signatures for this account to be replayed in another controlled by the same signer. +* https://docs.openzeppelin.com/contracts/api/token/erc721#ERC721Holder[ERC721Holder]: Allows the account to hold https://eips.ethereum.org/EIPS/eip-721[ERC-721] tokens +* https://docs.openzeppelin.com/contracts/api/token/erc721#ERC721Holder[ERC1155Holder]: Allows the account to hold https://eips.ethereum.org/EIPS/eip-1155[ERC-1155] tokens + +=== Setting up an account + +To setup an account, you can either bring your own validation logic and start with xref:api:account.adoc#AccountBase[`AccountBase`], or import any of the predefined accounts we provide and are controlled by a signing key. For example, to setup a contract controlled by a regular EVM private key, you can leverage xref:api:account.adoc#AccountECDSA[`AccountECDSA`]. + +These accounts can be deployed by a factory right away (see xref:account-abstraction.adoc#account_factory[Account Factory]) or used standalone if required. In both cases, the xref:api:account.adoc#AccountECDSA[`AccountECDSA`], xref:api:account.adoc#AccountP256[`AccountP256`] and xref:api:account.adoc#AccountRSA[`AccountRSA`] expose an internal xref:api:account.adoc#AccountECDSA-_initializeSigner-address-[`_initializeSigner`] functions that set the public key (or address) that controls them. + +WARNING: Leaving an account uninitialized may leave it unusable since no public key was associated with it. + +Since smart accounts are deployed by a factory, the best practice is to create https://docs.openzeppelin.com/contracts/5.x/api/proxy#minimal_clones[minimal clones] of initializable contracts. These account implementations provide an initializable design by default so that the factory can interact with the account to set it up after deployment in a single transaction. + +[source,solidity] +---- +include::api:example$account/MyAccountECDSA.sol[] +---- + +NOTE: xref:api:account.adoc#AccountECDSA[`AccountECDSA`] initializes xref:api:utils.adoc#EIP712[`EIP712`] to generate a domain separator that prevents replayability in other accounts controlled by the same key. See xref:account-abstraction.adoc#eip712_typed_signatures[EIP-712 Typed signatures] + +Along with the regular ECDSA verification, the library also provides the xref:api:account.adoc#AccountP256[`AccountP256`], which is a widely used _elliptic curve_ verification algorithm that's present in mobile device security enclaves, FIDO keys, and corporate environments (i.e. public key infrastructures). + +[source,solidity] +---- +include::api:example$account/MyAccountP256.sol[] +---- + +Similarly, some government and corporate public key infrastructures use RSA for signature verification. For those cases, the xref:api:account.adoc#AccountRSA[`AccountRSA`] may be a good fit. + +[source,solidity] +---- +include::api:example$account/MyAccountRSA.sol[] +---- + +=== Roll your own validation + +When developers want to start by implementing their own validation logic, they should extend from xref:api:account.adoc#AccountBase[`AccountBase`]. Also, we recommend using xref:api:utils.adoc#ERC7739Signer[`ERC7739Signer`] to prevent replayability of user operations across smart accounts as noted in xref:account-abstraction.adoc#eip712_typed_signatures[EIP-712 Typed signatures]: + +[source,solidity] +---- +include::api:example$account/MyAccountCustom.sol[] +---- + +== Account Factory + +The first time a user sends an user operation, the account will be created deterministically (i.e. its code and address can be predicted) using the the `initCode` field in the UserOperation. This field contains both the address of a smart contract (the factory) and the data required to call it and deploy the smart account. + +For this purpose, developers can create an account factory using the https://docs.openzeppelin.com/contracts/5.x/api/proxy#Clones[Clones library from OpenZeppelin Contracts]. It exposes methods to calculate the address of an account before deployment. + +[source,solidity] +---- +include::api:example$account/MyFactoryAccountECDSA.sol[] +---- + +You've setup your own account and its corresponding factory. Both are ready to be used with ERC-4337 infrastructure. Customizing the factory to other validation mechanisms must be straightforward. + +== ERC-4337 Overview + +The ERC-4337 is a detailed specification of how to implement the necessary logic to handle operations without making changes to the protocol level (i.e. the rules of the blockchain itself). This specification defines the following components: + +=== UserOperation + +An `UserOperation` is a higher-layer pseudo-transaction object that represents the intent of the account. This shares some similarities with regular EVM transactions like the concept of `gasFees` or `callData` but includes fields that enable new capabilities. + +```solidity +struct PackedUserOperation { + address sender; + uint256 nonce; + bytes initCode; // concatenation of factory address and factoryData (or empty) + bytes callData; + bytes32 accountGasLimits; // concatenation of verificationGas (16 bytes) and callGas (16 bytes) + uint256 preVerificationGas; + bytes32 gasFees; // concatenation of maxPriorityFee (16 bytes) and maxFeePerGas (16 bytes) + bytes paymasterAndData; // concatenation of paymaster fields (or empty) + bytes signature; +} +``` + +=== Entrypoint + +Each `UserOperation` is executed through a contract known as the https://etherscan.io/address/0x0000000071727de22e5e9d8baf0edac6f37da032#code[`EntryPoint`]. This contract is a singleton deployed across multiple networks at the same address although other custom implementations may be used. + +The Entrypoint contracts is considered a trusted entity by the account. + +=== Bundlers + +The bundler is a piece of _offchain_ infrastructure that is in charge of processing an alternative mempool of user operations. Bundlers themselves call the Entrypoint contract's `handleOps` function with an array of UserOperations that are executed and included in a block. + +During the process, the bundler pays for the gas of executing the transaction and gets refunded during the execution phase of the Entrypoint contract. + +=== Account Contract + +The Account Contract is a type of smart contract implements the logic required to validate an `UserOperation` in the context of ERC-4337. Any smart contract account should conform with the `IAccount` interface to validate operations. + +```solidity +interface IAccount { + function validateUserOp(PackedUserOperation calldata, bytes32, uint256) external returns (uint256 validationData); +} +``` + +Similarly, an Account should have a way to execute these operations by either handling arbitrary calldata on its `fallback` or implementing the `IAccountExecute` interface: + +```solidity +interface IAccountExecute { + function executeUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash) external; +} +``` + +To build your own account, see xref:account-abstraction.adoc#smart_accounts[Smart Accounts]. + +=== Factory Contract + +The smart contract accounts are created by a Factory contract defined by the Account developer. This factory receives arbitrary bytes as `initData` and returns an `address` where the logic of the account is deployed. + +To build your own factory, see xref:account-abstraction.adoc#account_factory[Account Factory] + +=== Paymaster Contract + +A Paymaster is an optional entity that can sponsor gas fees for Accounts, or allow them to pay for those fees in ERC-20 instead of native currency. This abstracts gas away of the user experience in the same way that computational costs of cloud servers are abstracted away from end-users. + +== Further notes + +=== EIP712 Typed Signatures + +A common security practice to prevent user operation https://mirror.xyz/curiousapple.eth/pFqAdW2LiJ-6S4sg_u1z08k4vK6BCJ33LcyXpnNb8yU[replayability across smart contract accounts controlled by the same private key] (i.e. multiple accounts for the same signer) is to link the signature to the `address` and `chainId` of the account. This can be done by asking the user to sign the hash of the user operation along with these values. + +The problem with this approach is that the user might be prompted by the wallet provider to sign an https://x.com/howydev/status/1780353754333634738[obfuscated message], which is a phishing vector that may lead to a user losing its assets. + +To prevent this, each account using a signature verification algorithm inherits from xref:api:account#ERC7739Signer[`ERC7739Signer`], a utility that implements xref:api:interfaces#IERC1271[`IERC1271`] for smart contract signatures with a defensive rehashing mechanism based on a https://github.com/frangio/eip712-wrapper-for-eip1271[nested EIP-712 approach] to wrap the signature request in a context where there's clearer information for the end user. + +=== ERC-7562 Validation Rules + +To process a bundle of `UserOperations`, bundlers call xref:api:account.adoc#AccountBase-validateUserOp-struct-PackedUserOperation-bytes32-uint256-[`validateUserOp`] on each operation sender to check whether the operation can be executed. However, the bundler has no guarantee that the state of the blockchain will remain the same after the validation phase. To overcome this problem, https://eips.ethereum.org/EIPS/eip-7562[ERC-7562] proposes a set of limitations to EVM code so that bundlers (or node operators) are protected from unexpected state changes. + +These rules outline the requirements for operations to be processed by the canonical mempool. + +Accounts can access its own storage during the validation phase, they might easily violate ERC-7562 storage access rules in undirect ways. For example, most accounts access their public keys from storage when validating a signature, limiting the ability of having accounts that validate operations for other accounts (e.g. via ERC-1271) + +TIP: Although any Account that breaks such rules may still be processed by a private bundler, developers should keep in mind the centralization tradeoffs of relying on private infrastructure instead of _permissionless_ execution. From d715e4cfdcd0043f50d9847d391aca8a7ba793d2 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 9 Dec 2024 16:19:08 +0100 Subject: [PATCH 18/51] rewrite helpers/signers as alternative to ethers.SigningKey and ethers.BaseWallet --- hardhat.config.js | 3 - package.json | 4 +- test/account/draft-AccountBase.test.js | 4 +- test/account/draft-AccountECDSA.test.js | 5 +- test/account/draft-AccountP256.test.js | 11 +- test/account/draft-AccountRSA.test.js | 11 +- test/helpers/signers.js | 209 +++++++++++------- .../cryptography/ERC7739Signer.behavior.js | 7 +- .../cryptography/draft-ERC7739Signer.test.js | 58 ++--- 9 files changed, 175 insertions(+), 137 deletions(-) diff --git a/hardhat.config.js b/hardhat.config.js index 1a944bba..07cc76c2 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -34,8 +34,5 @@ module.exports = { hardfork: argv.hardfork, }, }, - exposed: { - exclude: ['@axelar-network/**/*'], - }, docgen: require('./docs/config'), }; diff --git a/package.json b/package.json index 921d8a97..6997e631 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "yargs": "^17.7.2" }, "lint-staged": { - "*.{js,ts}": ["prettier --log-level warn --ignore-path .gitignore --check", "eslint"], - "*.sol": ["prettier --log-level warn --ignore-path .gitignore --check", "solhint"] + "*.{js,ts}": ["prettier --log-level warn --ignore-path .gitignore --check"], + "*.sol": ["prettier --log-level warn --ignore-path .gitignore --check"] } } diff --git a/test/account/draft-AccountBase.test.js b/test/account/draft-AccountBase.test.js index 7744089c..81bc0efd 100644 --- a/test/account/draft-AccountBase.test.js +++ b/test/account/draft-AccountBase.test.js @@ -2,12 +2,12 @@ const { ethers } = require('hardhat'); const { shouldBehaveLikeAnAccountBase, shouldBehaveLikeAnAccountBaseExecutor } = require('./Account.behavior'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { ERC4337Helper } = require('../helpers/erc4337'); -const { BooleanSigner } = require('../helpers/signers'); +const { NonNativeSigner } = require('../helpers/signers'); async function fixture() { const [beneficiary, other] = await ethers.getSigners(); const target = await ethers.deployContract('CallReceiverMockExtended'); - const signer = new BooleanSigner(); + const signer = new NonNativeSigner({ sign: () => ({ serialized: '0x01' }) }); const helper = new ERC4337Helper('$AccountBaseMock'); const smartAccount = await helper.newAccount(); diff --git a/test/account/draft-AccountECDSA.test.js b/test/account/draft-AccountECDSA.test.js index b8848932..2192044e 100644 --- a/test/account/draft-AccountECDSA.test.js +++ b/test/account/draft-AccountECDSA.test.js @@ -6,15 +6,14 @@ const { } = require('./Account.behavior'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { ERC4337Helper } = require('../helpers/erc4337'); -const { ECDSASigner } = require('../helpers/signers'); const { shouldBehaveLikeERC7739Signer } = require('../utils/cryptography/ERC7739Signer.behavior'); async function fixture() { const [beneficiary, other] = await ethers.getSigners(); const target = await ethers.deployContract('CallReceiverMockExtended'); - const signer = new ECDSASigner(); + const signer = ethers.Wallet.createRandom(); const helper = new ERC4337Helper('$AccountECDSAMock'); - const smartAccount = await helper.newAccount(['AccountECDSA', '1', signer.EOA.address]); + const smartAccount = await helper.newAccount(['AccountECDSA', '1', signer.address]); const domain = { name: 'AccountECDSA', version: '1', diff --git a/test/account/draft-AccountP256.test.js b/test/account/draft-AccountP256.test.js index 59f3c744..3fd6e0a1 100644 --- a/test/account/draft-AccountP256.test.js +++ b/test/account/draft-AccountP256.test.js @@ -6,15 +6,20 @@ const { } = require('./Account.behavior'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { ERC4337Helper } = require('../helpers/erc4337'); -const { P256Signer } = require('../helpers/signers'); +const { NonNativeSigner, P256SigningKey } = require('../helpers/signers'); const { shouldBehaveLikeERC7739Signer } = require('../utils/cryptography/ERC7739Signer.behavior'); async function fixture() { const [beneficiary, other] = await ethers.getSigners(); const target = await ethers.deployContract('CallReceiverMockExtended'); - const signer = new P256Signer(); + const signer = new NonNativeSigner(P256SigningKey.random()); const helper = new ERC4337Helper('$AccountP256Mock'); - const smartAccount = await helper.newAccount(['AccountP256', '1', signer.publicKey.qx, signer.publicKey.qy]); + const smartAccount = await helper.newAccount([ + 'AccountP256', + '1', + signer.signingKey.publicKey.qx, + signer.signingKey.publicKey.qy, + ]); const domain = { name: 'AccountP256', version: '1', diff --git a/test/account/draft-AccountRSA.test.js b/test/account/draft-AccountRSA.test.js index 56163432..dba47506 100644 --- a/test/account/draft-AccountRSA.test.js +++ b/test/account/draft-AccountRSA.test.js @@ -6,15 +6,20 @@ const { } = require('./Account.behavior'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { ERC4337Helper } = require('../helpers/erc4337'); -const { RSASignerSHA256 } = require('../helpers/signers'); +const { NonNativeSigner, RSASHA256SigningKey } = require('../helpers/signers'); const { shouldBehaveLikeERC7739Signer } = require('../utils/cryptography/ERC7739Signer.behavior'); async function fixture() { const [beneficiary, other] = await ethers.getSigners(); const target = await ethers.deployContract('CallReceiverMockExtended'); - const signer = new RSASignerSHA256(); + const signer = new NonNativeSigner(RSASHA256SigningKey.random()); const helper = new ERC4337Helper('$AccountRSAMock'); - const smartAccount = await helper.newAccount(['AccountRSA', '1', signer.publicKey.e, signer.publicKey.n]); + const smartAccount = await helper.newAccount([ + 'AccountRSA', + '1', + signer.signingKey.publicKey.e, + signer.signingKey.publicKey.n, + ]); const domain = { name: 'AccountRSA', version: '1', diff --git a/test/helpers/signers.js b/test/helpers/signers.js index 4645998a..e0b1692a 100644 --- a/test/helpers/signers.js +++ b/test/helpers/signers.js @@ -1,115 +1,168 @@ -const { ethers } = require('hardhat'); -const { secp256k1 } = require('@noble/curves/secp256k1'); +const { + AbstractSigner, + Signature, + TypedDataEncoder, + assert, + assertArgument, + concat, + dataLength, + decodeBase64, + getBytes, + getBytesCopy, + hashMessage, + hexlify, + sha256, + toBeHex, +} = require('ethers'); const { secp256r1 } = require('@noble/curves/p256'); const { generateKeyPairSync, privateEncrypt } = require('crypto'); -const { hashTypedData } = require('../../lib/@openzeppelin-contracts/test/helpers/eip712'); -const ensureLowerOrderS = (N, { s, recovery, ...rest }) => { - if (s > N / 2n) { - s = N - s; - recovery = 1 - recovery; +// Lightweight version of BaseWallet +class NonNativeSigner extends AbstractSigner { + #signingKey; + + constructor(privateKey, provider) { + super(provider); + assertArgument( + privateKey && typeof privateKey.sign === 'function', + 'invalid private key', + 'privateKey', + '[ REDACTED ]', + ); + this.#signingKey = privateKey; } - return { s, recovery, ...rest }; -}; -class BooleanSigner { - signTypedData() { - return '0x01'; + get signingKey() { + return this.#signingKey; } -} - -class ERC7739Signer { - signTypedData(domain, types, contents) { - return this._signRaw(hashTypedData(domain, ethers.TypedDataEncoder.from(types).hash(contents))); + get privateKey() { + return this.signingKey.privateKey; } -} -class ECDSASigner extends ERC7739Signer { - N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n; + async getAddress() { + throw new Error("NonNativeSigner doesn't have an address"); + } - constructor() { - super(); - this._privateKey = secp256k1.utils.randomPrivateKey(); - this.publicKey = secp256k1.getPublicKey(this._privateKey, false); + connect(provider) { + return new NonNativeSigner(this.#signingKey, provider); } - _signRaw(messageHash) { - const sig = this._ensureLowerOrderS(secp256k1.sign(messageHash.replace(/0x/, ''), this._privateKey)); - return ethers.Signature.from({ - r: sig.r, - v: sig.recovery + 27, - s: sig.s, - }).serialized; + async signTransaction(/*tx: TransactionRequest*/) { + throw new Error('NonNativeSigner cannot send transactions'); } - get EOA() { - return new ethers.Wallet(ethers.hexlify(this._privateKey)); + async signMessage(message /*: string | Uint8Array*/) /*: Promise*/ { + return this.signingKey.sign(hashMessage(message)).serialized; } - _ensureLowerOrderS({ s, recovery, ...rest }) { - return ensureLowerOrderS(this.N, { s, recovery, ...rest }); + async signTypedData( + domain /*: TypedDataDomain*/, + types /*: Record>*/, + value /*: Record*/, + ) /*: Promise*/ { + // Populate any ENS names + const populated = await TypedDataEncoder.resolveNames(domain, types, value, async name => { + assert(this.provider != null, 'cannot resolve ENS names without a provider', 'UNSUPPORTED_OPERATION', { + operation: 'resolveName', + info: { name }, + }); + const address = await this.provider.resolveName(name); + assert(address != null, 'unconfigured ENS name', 'UNCONFIGURED_NAME', { value: name }); + return address; + }); + + return this.signingKey.sign(TypedDataEncoder.hash(populated.domain, types, populated.value)).serialized; } } -class P256Signer extends ERC7739Signer { - N = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551n; - - constructor() { - super(); - this._privateKey = secp256r1.utils.randomPrivateKey(); - const [qx, qy] = [ - secp256r1.getPublicKey(this._privateKey, false).slice(0x01, 0x21), - secp256r1.getPublicKey(this._privateKey, false).slice(0x21, 0x41), - ].map(ethers.hexlify); - this.publicKey = { - qx, - qy, - }; +class P256SigningKey { + #privateKey; + + constructor(privateKey) { + this.#privateKey = getBytes(privateKey); } - _signRaw(messageHash) { - const sig = this._ensureLowerOrderS(secp256r1.sign(messageHash.replace(/0x/, ''), this._privateKey)); - return ethers.Signature.from({ - r: sig.r, - v: sig.recovery + 27, - s: sig.s, - }).serialized; + static random() { + return new P256SigningKey(secp256r1.utils.randomPrivateKey()); } - _ensureLowerOrderS({ s, recovery, ...rest }) { - return ensureLowerOrderS(this.N, { s, recovery, ...rest }); + get privateKey() { + return hexlify(this.#privateKey); } -} -class RSASigner extends ERC7739Signer { - constructor() { - super(); - const keyPair = generateKeyPairSync('rsa', { - modulusLength: 2048, + get publicKey() { + const publicKeyBytes = secp256r1.getPublicKey(this.#privateKey, false); + return { + qx: hexlify(publicKeyBytes.slice(0x01, 0x21)), + qy: hexlify(publicKeyBytes.slice(0x21, 0x41)), + }; + } + + sign(digest /*: BytesLike*/) /*: Signature*/ { + assertArgument(dataLength(digest) === 32, 'invalid digest length', 'digest', digest); + + const sig = secp256r1.sign(getBytesCopy(digest), getBytesCopy(this.#privateKey), { + lowS: true, + }); + + return Signature.from({ + r: toBeHex(sig.r, 32), + s: toBeHex(sig.s, 32), + v: sig.recovery ? 0x1c : 0x1b, }); + } +} + +class RSASigningKey { + #privateKey; + #publicKey; + + constructor(keyPair) { const jwk = keyPair.publicKey.export({ format: 'jwk' }); - const [e, n] = [jwk.e, jwk.n].map(ethers.decodeBase64); - this._privateKey = keyPair.privateKey; - this.publicKey = { e, n }; + this.#privateKey = keyPair.privateKey; + this.#publicKey = { e: decodeBase64(jwk.e), n: decodeBase64(jwk.n) }; + } + + static random(modulusLength = 2048) { + return new RSASigningKey(generateKeyPairSync('rsa', { modulusLength })); } - _signRaw(messageHash) { + get privateKey() { + return hexlify(this.#privateKey); + } + + get publicKey() { + return { + e: hexlify(this.#publicKey.e), + n: hexlify(this.#publicKey.n), + }; + } + + sign(digest /*: BytesLike*/) /*: Signature*/ { + assertArgument(dataLength(digest) === 32, 'invalid digest length', 'digest', digest); // SHA256 OID = 608648016503040201 (9 bytes) | NULL = 0500 (2 bytes) (explicit) | OCTET_STRING length (0x20) = 0420 (2 bytes) - const dataToSign = ethers.concat(['0x3031300d060960864801650304020105000420', messageHash]); - return '0x' + privateEncrypt(this._privateKey, ethers.getBytes(dataToSign)).toString('hex'); + return { + serialized: hexlify( + privateEncrypt(this.#privateKey, getBytes(concat(['0x3031300d060960864801650304020105000420', digest]))), + ), + }; } } -class RSASignerSHA256 extends RSASigner { - _signRaw(messageHash) { - return super._signRaw(ethers.sha256(ethers.solidityPacked(['bytes32'], [messageHash]))); +class RSASHA256SigningKey extends RSASigningKey { + static random(modulusLength = 2048) { + return new RSASHA256SigningKey(generateKeyPairSync('rsa', { modulusLength })); + } + + sign(digest /*: BytesLike*/) /*: Signature*/ { + assertArgument(dataLength(digest) === 32, 'invalid digest length', 'digest', digest); + return super.sign(sha256(getBytes(digest))); } } module.exports = { - BooleanSigner, - ECDSASigner, - P256Signer, - RSASigner, - RSASignerSHA256, + NonNativeSigner, + P256SigningKey, + RSASigningKey, + RSASHA256SigningKey, }; diff --git a/test/utils/cryptography/ERC7739Signer.behavior.js b/test/utils/cryptography/ERC7739Signer.behavior.js index fa44e0f8..b6b4055c 100644 --- a/test/utils/cryptography/ERC7739Signer.behavior.js +++ b/test/utils/cryptography/ERC7739Signer.behavior.js @@ -1,12 +1,17 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); -const { Permit, formatType } = require('../../../lib/@openzeppelin-contracts/test/helpers/eip712'); +const { Permit, formatType, getDomain } = require('../../../lib/@openzeppelin-contracts/test/helpers/eip712'); const { PersonalSignHelper, TypedDataSignHelper } = require('../../helpers/erc7739'); function shouldBehaveLikeERC7739Signer() { const MAGIC_VALUE = '0x1626ba7e'; describe('isValidSignature', function () { + beforeEach(async function () { + this.signTypedData ??= this.signer.signTypedData.bind(this.signer); + this.domain ??= await getDomain(this.mock); + }); + describe('PersonalSign', function () { it('returns true for a valid personal signature', async function () { const text = 'Hello, world!'; diff --git a/test/utils/cryptography/draft-ERC7739Signer.test.js b/test/utils/cryptography/draft-ERC7739Signer.test.js index b7833af3..8fe0b711 100644 --- a/test/utils/cryptography/draft-ERC7739Signer.test.js +++ b/test/utils/cryptography/draft-ERC7739Signer.test.js @@ -1,62 +1,36 @@ -const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { ethers } = require('hardhat'); const { shouldBehaveLikeERC7739Signer } = require('./ERC7739Signer.behavior'); -const { ECDSASigner, P256Signer, RSASigner } = require('../../helpers/signers'); -const { getDomain } = require('../../../lib/@openzeppelin-contracts/test/helpers/eip712'); - -async function fixture() { - const ECDSA = new ECDSASigner(); - const ECDSAMock = await ethers.deployContract('ERC7739SignerECDSAMock', [ECDSA.EOA.address]); - - const P256 = new P256Signer(); - const P256Mock = await ethers.deployContract('ERC7739SignerP256Mock', [P256.publicKey.qx, P256.publicKey.qy]); - - const RSA = new RSASigner(); - const RSAMock = await ethers.deployContract('ERC7739SignerRSAMock', [RSA.publicKey.e, RSA.publicKey.n]); - - return { - ECDSA, - ECDSAMock, - ECDSAMockDomain: await getDomain(ECDSAMock), - P256, - P256Mock, - P256MockDomain: await getDomain(P256Mock), - RSA, - RSAMock, - RSAMockDomain: await getDomain(RSAMock), - }; -} +const { NonNativeSigner, P256SigningKey, RSASigningKey } = require('../../helpers/signers'); describe('ERC7739Signer', function () { - beforeEach(async function () { - Object.assign(this, await loadFixture(fixture)); - }); - describe('for an ECDSA signer', function () { - beforeEach(function () { - this.mock = this.ECDSAMock; - this.domain = this.ECDSAMockDomain; - this.signTypedData = this.ECDSA.signTypedData.bind(this.ECDSA); + before(async function () { + this.signer = ethers.Wallet.createRandom(); + this.mock = await ethers.deployContract('ERC7739SignerECDSAMock', [this.signer.address]); }); shouldBehaveLikeERC7739Signer(); }); describe('for a P256 signer', function () { - beforeEach(function () { - this.mock = this.P256Mock; - this.domain = this.P256MockDomain; - this.signTypedData = this.P256.signTypedData.bind(this.P256); + before(async function () { + this.signer = new NonNativeSigner(P256SigningKey.random()); + this.mock = await ethers.deployContract('ERC7739SignerP256Mock', [ + this.signer.signingKey.publicKey.qx, + this.signer.signingKey.publicKey.qy, + ]); }); shouldBehaveLikeERC7739Signer(); }); describe('for an RSA signer', function () { - beforeEach(function () { - this.mock = this.RSAMock; - this.domain = this.RSAMockDomain; - this.signTypedData = this.RSA.signTypedData.bind(this.RSA); + before(async function () { + this.signer = new NonNativeSigner(RSASigningKey.random()); + this.mock = await ethers.deployContract('ERC7739SignerRSAMock', [ + this.signer.signingKey.publicKey.e, + this.signer.signingKey.publicKey.n, + ]); }); shouldBehaveLikeERC7739Signer(); From 8ad95a1dc7db61fc9b1350449850cf7cf13dbce8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Mon, 9 Dec 2024 15:49:59 -0600 Subject: [PATCH 19/51] Rename _validateNestedEIP712Signature -> _validateSignature --- contracts/account/draft-AccountECDSA.sol | 6 +++--- contracts/account/draft-AccountP256.sol | 6 +++--- contracts/account/draft-AccountRSA.sol | 6 +++--- contracts/mocks/docs/account/MyAccountCustom.sol | 2 +- .../docs/utils/cryptography/ERC7739SignerECDSAMock.sol | 2 +- .../docs/utils/cryptography/ERC7739SignerP256Mock.sol | 2 +- .../docs/utils/cryptography/ERC7739SignerRSAMock.sol | 2 +- contracts/utils/cryptography/draft-ERC7739Signer.sol | 10 +++++----- 8 files changed, 18 insertions(+), 18 deletions(-) diff --git a/contracts/account/draft-AccountECDSA.sol b/contracts/account/draft-AccountECDSA.sol index 3ed35738..df08e0f8 100644 --- a/contracts/account/draft-AccountECDSA.sol +++ b/contracts/account/draft-AccountECDSA.sol @@ -59,11 +59,11 @@ abstract contract AccountECDSA is ERC165, ERC7739Signer, ERC721Holder, ERC1155Ho } /** - * @dev Internal version of {validateUserOp} that relies on {_validateNestedEIP712Signature}. + * @dev Internal version of {validateUserOp} that relies on {_validateSignature}. * * The `userOpSignedHash` is the digest from {_userOpSignedHash}. * - * NOTE: To override the signature functionality, try overriding {_validateNestedEIP712Signature} instead. + * NOTE: To override the signature functionality, try overriding {_validateSignature} instead. */ function _validateUserOp( PackedUserOperation calldata userOp, @@ -81,7 +81,7 @@ abstract contract AccountECDSA is ERC165, ERC7739Signer, ERC721Holder, ERC1155Ho * This function provides a nested EIP-712 hash. Developers must override only this * function to ensure no raw message signing is possible. */ - function _validateNestedEIP712Signature( + function _validateSignature( bytes32 nestedEIP712Hash, bytes calldata signature ) internal view virtual override returns (bool) { diff --git a/contracts/account/draft-AccountP256.sol b/contracts/account/draft-AccountP256.sol index 89a76cc4..039a86d5 100644 --- a/contracts/account/draft-AccountP256.sol +++ b/contracts/account/draft-AccountP256.sol @@ -60,11 +60,11 @@ abstract contract AccountP256 is ERC165, ERC7739Signer, ERC721Holder, ERC1155Hol } /** - * @dev Internal version of {validateUserOp} that relies on {_validateNestedEIP712Signature}. + * @dev Internal version of {validateUserOp} that relies on {_validateSignature}. * * The `userOpSignedHash` is the digest from {_userOpSignedHash}. * - * NOTE: To override the signature functionality, try overriding {_validateNestedEIP712Signature} instead. + * NOTE: To override the signature functionality, try overriding {_validateSignature} instead. */ function _validateUserOp( PackedUserOperation calldata userOp, @@ -82,7 +82,7 @@ abstract contract AccountP256 is ERC165, ERC7739Signer, ERC721Holder, ERC1155Hol * This function provides a nested EIP-712 hash. Developers must override only this * function to ensure no raw message signing is possible. */ - function _validateNestedEIP712Signature( + function _validateSignature( bytes32 hash, bytes calldata signature ) internal view virtual override returns (bool) { diff --git a/contracts/account/draft-AccountRSA.sol b/contracts/account/draft-AccountRSA.sol index 67626b85..cc8cd1c4 100644 --- a/contracts/account/draft-AccountRSA.sol +++ b/contracts/account/draft-AccountRSA.sol @@ -60,11 +60,11 @@ abstract contract AccountRSA is ERC165, ERC7739Signer, ERC721Holder, ERC1155Hold } /** - * @dev Internal version of {validateUserOp} that relies on {_validateNestedEIP712Signature}. + * @dev Internal version of {validateUserOp} that relies on {_validateSignature}. * * The `userOpSignedHash` is the digest from {_userOpSignedHash}. * - * NOTE: To override the signature functionality, try overriding {_validateNestedEIP712Signature} instead. + * NOTE: To override the signature functionality, try overriding {_validateSignature} instead. */ function _validateUserOp( PackedUserOperation calldata userOp, @@ -82,7 +82,7 @@ abstract contract AccountRSA is ERC165, ERC7739Signer, ERC721Holder, ERC1155Hold * This function provides a nested EIP-712 hash. Developers must override only this * function to ensure no raw message signing is possible. */ - function _validateNestedEIP712Signature( + function _validateSignature( bytes32 hash, bytes calldata signature ) internal view virtual override returns (bool) { diff --git a/contracts/mocks/docs/account/MyAccountCustom.sol b/contracts/mocks/docs/account/MyAccountCustom.sol index 69a30c5c..2ee3ebed 100644 --- a/contracts/mocks/docs/account/MyAccountCustom.sol +++ b/contracts/mocks/docs/account/MyAccountCustom.sol @@ -35,7 +35,7 @@ contract MyAccountCustom is ERC7739Signer, AccountBase, Initializable { } /// @dev Receives a hash wrapped in an EIP-712 domain separator. - function _validateNestedEIP712Signature( + function _validateSignature( bytes32 hash, bytes calldata signature ) internal view virtual override returns (bool) { diff --git a/contracts/mocks/docs/utils/cryptography/ERC7739SignerECDSAMock.sol b/contracts/mocks/docs/utils/cryptography/ERC7739SignerECDSAMock.sol index 7e30e240..34c3845d 100644 --- a/contracts/mocks/docs/utils/cryptography/ERC7739SignerECDSAMock.sol +++ b/contracts/mocks/docs/utils/cryptography/ERC7739SignerECDSAMock.sol @@ -13,7 +13,7 @@ contract ERC7739SignerECDSAMock is ERC7739Signer { _signer = signerAddr; } - function _validateNestedEIP712Signature( + function _validateSignature( bytes32 hash, bytes calldata signature ) internal view virtual override returns (bool) { diff --git a/contracts/mocks/docs/utils/cryptography/ERC7739SignerP256Mock.sol b/contracts/mocks/docs/utils/cryptography/ERC7739SignerP256Mock.sol index b2ff2c56..bbd1dc30 100644 --- a/contracts/mocks/docs/utils/cryptography/ERC7739SignerP256Mock.sol +++ b/contracts/mocks/docs/utils/cryptography/ERC7739SignerP256Mock.sol @@ -15,7 +15,7 @@ contract ERC7739SignerP256Mock is ERC7739Signer { _qy = qy; } - function _validateNestedEIP712Signature( + function _validateSignature( bytes32 hash, bytes calldata signature ) internal view virtual override returns (bool) { diff --git a/contracts/mocks/docs/utils/cryptography/ERC7739SignerRSAMock.sol b/contracts/mocks/docs/utils/cryptography/ERC7739SignerRSAMock.sol index a242892e..cd2ef0b4 100644 --- a/contracts/mocks/docs/utils/cryptography/ERC7739SignerRSAMock.sol +++ b/contracts/mocks/docs/utils/cryptography/ERC7739SignerRSAMock.sol @@ -15,7 +15,7 @@ contract ERC7739SignerRSAMock is ERC7739Signer { _n = n; } - function _validateNestedEIP712Signature( + function _validateSignature( bytes32 hash, bytes calldata signature ) internal view virtual override returns (bool) { diff --git a/contracts/utils/cryptography/draft-ERC7739Signer.sol b/contracts/utils/cryptography/draft-ERC7739Signer.sol index 59390fef..57e60e8b 100644 --- a/contracts/utils/cryptography/draft-ERC7739Signer.sol +++ b/contracts/utils/cryptography/draft-ERC7739Signer.sol @@ -56,12 +56,12 @@ abstract contract ERC7739Signer is EIP712, IERC1271 { /** * @dev Nested personal signature verification. * - * NOTE: Instead of overriding this function, try with {_validateNestedEIP712Signature}. It encapsulates + * NOTE: Instead of overriding this function, try with {_validateSignature}. It encapsulates * nested EIP-712 hashes. */ function _isValidNestedPersonalSignSignature(bytes32 hash, bytes calldata signature) internal view returns (bool) { return - _validateNestedEIP712Signature( + _validateSignature( _domainSeparatorV4().toTypedDataHash(hash.personalSignStructHash()), signature ); @@ -70,7 +70,7 @@ abstract contract ERC7739Signer is EIP712, IERC1271 { /** * @dev Nested EIP-712 typed data verification. * - * NOTE: Instead of overriding this function, try with {_validateNestedEIP712Signature}. It encapsulates + * NOTE: Instead of overriding this function, try with {_validateSignature}. It encapsulates * nested EIP-712 hashes. */ function _isValidNestedTypedDataSignature( @@ -100,7 +100,7 @@ abstract contract ERC7739Signer is EIP712, IERC1271 { return hash == appSeparator.toTypedDataHash(contentsHash) && bytes(contentsDescr).length != 0 && - _validateNestedEIP712Signature( + _validateSignature( appSeparator.toTypedDataHash( ERC7739Utils.typedDataSignStructHash( contentsDescr, @@ -123,7 +123,7 @@ abstract contract ERC7739Signer is EIP712, IERC1271 { * cryptographic verification. It is important to review and test thoroughly before deployment. Consider * using one of the signature verification libraries ({ECDSA}, {P256} or {RSA}). */ - function _validateNestedEIP712Signature( + function _validateSignature( bytes32 nestedEIP712Hash, bytes calldata signature ) internal view virtual returns (bool); From 885efdd1dbb572f8fce47e136796c7a5a1a835ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Mon, 9 Dec 2024 15:53:39 -0600 Subject: [PATCH 20/51] Read virtual to ERC7739Signer functions --- .../utils/cryptography/draft-ERC7739Signer.sol | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/contracts/utils/cryptography/draft-ERC7739Signer.sol b/contracts/utils/cryptography/draft-ERC7739Signer.sol index 57e60e8b..7682dbe3 100644 --- a/contracts/utils/cryptography/draft-ERC7739Signer.sol +++ b/contracts/utils/cryptography/draft-ERC7739Signer.sol @@ -59,12 +59,11 @@ abstract contract ERC7739Signer is EIP712, IERC1271 { * NOTE: Instead of overriding this function, try with {_validateSignature}. It encapsulates * nested EIP-712 hashes. */ - function _isValidNestedPersonalSignSignature(bytes32 hash, bytes calldata signature) internal view returns (bool) { - return - _validateSignature( - _domainSeparatorV4().toTypedDataHash(hash.personalSignStructHash()), - signature - ); + function _isValidNestedPersonalSignSignature( + bytes32 hash, + bytes calldata signature + ) internal view virtual returns (bool) { + return _validateSignature(_domainSeparatorV4().toTypedDataHash(hash.personalSignStructHash()), signature); } /** @@ -76,7 +75,7 @@ abstract contract ERC7739Signer is EIP712, IERC1271 { function _isValidNestedTypedDataSignature( bytes32 hash, bytes calldata encodedSignature - ) internal view returns (bool) { + ) internal view virtual returns (bool) { // decode signature ( bytes calldata signature, From d5f0dac22440ecd70192c81c5f158688c888b9bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Mon, 9 Dec 2024 15:57:11 -0600 Subject: [PATCH 21/51] lint --- contracts/account/draft-AccountP256.sol | 5 +---- contracts/account/draft-AccountRSA.sol | 5 +---- contracts/mocks/docs/account/MyAccountCustom.sol | 5 +---- .../mocks/docs/utils/cryptography/ERC7739SignerECDSAMock.sol | 5 +---- .../mocks/docs/utils/cryptography/ERC7739SignerP256Mock.sol | 5 +---- .../mocks/docs/utils/cryptography/ERC7739SignerRSAMock.sol | 5 +---- 6 files changed, 6 insertions(+), 24 deletions(-) diff --git a/contracts/account/draft-AccountP256.sol b/contracts/account/draft-AccountP256.sol index 039a86d5..43f4c7f9 100644 --- a/contracts/account/draft-AccountP256.sol +++ b/contracts/account/draft-AccountP256.sol @@ -82,10 +82,7 @@ abstract contract AccountP256 is ERC165, ERC7739Signer, ERC721Holder, ERC1155Hol * This function provides a nested EIP-712 hash. Developers must override only this * function to ensure no raw message signing is possible. */ - function _validateSignature( - bytes32 hash, - bytes calldata signature - ) internal view virtual override returns (bool) { + function _validateSignature(bytes32 hash, bytes calldata signature) internal view virtual override returns (bool) { if (signature.length < 0x40) return false; bytes32 r = bytes32(signature[0x00:0x20]); bytes32 s = bytes32(signature[0x20:0x40]); diff --git a/contracts/account/draft-AccountRSA.sol b/contracts/account/draft-AccountRSA.sol index cc8cd1c4..06f363a6 100644 --- a/contracts/account/draft-AccountRSA.sol +++ b/contracts/account/draft-AccountRSA.sol @@ -82,10 +82,7 @@ abstract contract AccountRSA is ERC165, ERC7739Signer, ERC721Holder, ERC1155Hold * This function provides a nested EIP-712 hash. Developers must override only this * function to ensure no raw message signing is possible. */ - function _validateSignature( - bytes32 hash, - bytes calldata signature - ) internal view virtual override returns (bool) { + function _validateSignature(bytes32 hash, bytes calldata signature) internal view virtual override returns (bool) { (bytes memory e, bytes memory n) = signer(); return RSA.pkcs1Sha256(abi.encodePacked(hash), signature, e, n); } diff --git a/contracts/mocks/docs/account/MyAccountCustom.sol b/contracts/mocks/docs/account/MyAccountCustom.sol index 2ee3ebed..cc51f009 100644 --- a/contracts/mocks/docs/account/MyAccountCustom.sol +++ b/contracts/mocks/docs/account/MyAccountCustom.sol @@ -35,10 +35,7 @@ contract MyAccountCustom is ERC7739Signer, AccountBase, Initializable { } /// @dev Receives a hash wrapped in an EIP-712 domain separator. - function _validateSignature( - bytes32 hash, - bytes calldata signature - ) internal view virtual override returns (bool) { + function _validateSignature(bytes32 hash, bytes calldata signature) internal view virtual override returns (bool) { // Custom signing logic } } diff --git a/contracts/mocks/docs/utils/cryptography/ERC7739SignerECDSAMock.sol b/contracts/mocks/docs/utils/cryptography/ERC7739SignerECDSAMock.sol index 34c3845d..60e34dea 100644 --- a/contracts/mocks/docs/utils/cryptography/ERC7739SignerECDSAMock.sol +++ b/contracts/mocks/docs/utils/cryptography/ERC7739SignerECDSAMock.sol @@ -13,10 +13,7 @@ contract ERC7739SignerECDSAMock is ERC7739Signer { _signer = signerAddr; } - function _validateSignature( - bytes32 hash, - bytes calldata signature - ) internal view virtual override returns (bool) { + function _validateSignature(bytes32 hash, bytes calldata signature) internal view virtual override returns (bool) { (address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(hash, signature); return _signer == recovered && err == ECDSA.RecoverError.NoError; } diff --git a/contracts/mocks/docs/utils/cryptography/ERC7739SignerP256Mock.sol b/contracts/mocks/docs/utils/cryptography/ERC7739SignerP256Mock.sol index bbd1dc30..d0782416 100644 --- a/contracts/mocks/docs/utils/cryptography/ERC7739SignerP256Mock.sol +++ b/contracts/mocks/docs/utils/cryptography/ERC7739SignerP256Mock.sol @@ -15,10 +15,7 @@ contract ERC7739SignerP256Mock is ERC7739Signer { _qy = qy; } - function _validateSignature( - bytes32 hash, - bytes calldata signature - ) internal view virtual override returns (bool) { + function _validateSignature(bytes32 hash, bytes calldata signature) internal view virtual override returns (bool) { bytes32 r = bytes32(signature[0x00:0x20]); bytes32 s = bytes32(signature[0x20:0x40]); return P256.verify(hash, r, s, _qx, _qy); diff --git a/contracts/mocks/docs/utils/cryptography/ERC7739SignerRSAMock.sol b/contracts/mocks/docs/utils/cryptography/ERC7739SignerRSAMock.sol index cd2ef0b4..b2d270e6 100644 --- a/contracts/mocks/docs/utils/cryptography/ERC7739SignerRSAMock.sol +++ b/contracts/mocks/docs/utils/cryptography/ERC7739SignerRSAMock.sol @@ -15,10 +15,7 @@ contract ERC7739SignerRSAMock is ERC7739Signer { _n = n; } - function _validateSignature( - bytes32 hash, - bytes calldata signature - ) internal view virtual override returns (bool) { + function _validateSignature(bytes32 hash, bytes calldata signature) internal view virtual override returns (bool) { return RSA.pkcs1Sha256(hash, signature, _e, _n); } } From 130ce044ddd61a72f7443592a31bbc2f127b3e3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Mon, 9 Dec 2024 16:03:54 -0600 Subject: [PATCH 22/51] Implement review recommendations --- contracts/mocks/account/AccountECDSAMock.sol | 2 -- contracts/mocks/account/AccountP256Mock.sol | 2 -- contracts/mocks/account/AccountRSAMock.sol | 2 -- contracts/mocks/docs/account/MyFactoryAccountECDSA.sol | 3 +-- 4 files changed, 1 insertion(+), 8 deletions(-) diff --git a/contracts/mocks/account/AccountECDSAMock.sol b/contracts/mocks/account/AccountECDSAMock.sol index 13d6bd34..2e22e1e8 100644 --- a/contracts/mocks/account/AccountECDSAMock.sol +++ b/contracts/mocks/account/AccountECDSAMock.sol @@ -2,8 +2,6 @@ pragma solidity ^0.8.20; -import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; -import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol"; import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; import {AccountECDSA} from "../../account/draft-AccountECDSA.sol"; diff --git a/contracts/mocks/account/AccountP256Mock.sol b/contracts/mocks/account/AccountP256Mock.sol index 9a348d79..0e23b9c3 100644 --- a/contracts/mocks/account/AccountP256Mock.sol +++ b/contracts/mocks/account/AccountP256Mock.sol @@ -2,8 +2,6 @@ pragma solidity ^0.8.20; -import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; -import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol"; import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; import {AccountP256} from "../../account/draft-AccountP256.sol"; diff --git a/contracts/mocks/account/AccountRSAMock.sol b/contracts/mocks/account/AccountRSAMock.sol index d8c0c9b6..fa4b8410 100644 --- a/contracts/mocks/account/AccountRSAMock.sol +++ b/contracts/mocks/account/AccountRSAMock.sol @@ -2,8 +2,6 @@ pragma solidity ^0.8.20; -import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; -import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol"; import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; import {AccountRSA} from "../../account/draft-AccountRSA.sol"; diff --git a/contracts/mocks/docs/account/MyFactoryAccountECDSA.sol b/contracts/mocks/docs/account/MyFactoryAccountECDSA.sol index b3c244b1..475da95c 100644 --- a/contracts/mocks/docs/account/MyFactoryAccountECDSA.sol +++ b/contracts/mocks/docs/account/MyFactoryAccountECDSA.sol @@ -4,7 +4,6 @@ pragma solidity ^0.8.20; import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; -import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {MyAccountECDSA} from "./MyAccountECDSA.sol"; /** @@ -30,7 +29,7 @@ contract MyFactoryAccountECDSA { address predicted = predictAddress(salt); if (predicted.code.length == 0) { _impl.cloneDeterministic(salt); - Address.functionCall(predicted, abi.encodeCall(MyAccountECDSA.initializeSigner, (signer))); + MyAccountECDSA(payable(predicted)).initializeSigner(signer); } return predicted; } From dcdae8d6f4c99b9627dc197b2912cf410884fa9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Mon, 9 Dec 2024 16:07:22 -0600 Subject: [PATCH 23/51] Include signer into account factory hash --- contracts/mocks/docs/account/MyFactoryAccountECDSA.sol | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/contracts/mocks/docs/account/MyFactoryAccountECDSA.sol b/contracts/mocks/docs/account/MyFactoryAccountECDSA.sol index 475da95c..20c095a6 100644 --- a/contracts/mocks/docs/account/MyFactoryAccountECDSA.sol +++ b/contracts/mocks/docs/account/MyFactoryAccountECDSA.sol @@ -26,9 +26,12 @@ contract MyFactoryAccountECDSA { /// @dev Create clone accounts on demand and return the address. Uses `signer` to initialize the clone. function _cloneAndInitialize(bytes32 salt, address signer) internal returns (address) { - address predicted = predictAddress(salt); + // Scope salt to the signer to avoid front-running the salt with a different signer + bytes32 _signerSalt = keccak256(abi.encodePacked(salt, signer)); + + address predicted = predictAddress(_signerSalt); if (predicted.code.length == 0) { - _impl.cloneDeterministic(salt); + _impl.cloneDeterministic(_signerSalt); MyAccountECDSA(payable(predicted)).initializeSigner(signer); } return predicted; From 03d935d3b79e416696cd6a3f7424f8e341bbd98b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Tue, 10 Dec 2024 11:51:31 -0600 Subject: [PATCH 24/51] Update Account inheritance order --- contracts/account/draft-AccountECDSA.sol | 10 +++++----- contracts/account/draft-AccountP256.sol | 11 ++++++----- contracts/account/draft-AccountRSA.sol | 11 ++++++----- contracts/mocks/ERC1155Mock.sol | 8 +------- test/account/Account.behavior.js | 4 ++-- 5 files changed, 20 insertions(+), 24 deletions(-) diff --git a/contracts/account/draft-AccountECDSA.sol b/contracts/account/draft-AccountECDSA.sol index df08e0f8..b80d3bed 100644 --- a/contracts/account/draft-AccountECDSA.sol +++ b/contracts/account/draft-AccountECDSA.sol @@ -2,17 +2,17 @@ pragma solidity ^0.8.20; +import {IERC5267} from "@openzeppelin/contracts/interfaces/IERC5267.sol"; import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol"; import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; -import {ERC1155HolderLean, IERC1155Receiver} from "../token/ERC1155/utils/ERC1155HolderLean.sol"; +import {ERC1155Holder, IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import {AccountBase} from "./draft-AccountBase.sol"; import {ERC7739Signer, EIP712} from "../utils/cryptography/draft-ERC7739Signer.sol"; -import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; /** * @dev Account implementation using {ECDSA} signatures and {ERC7739Signer} for replay protection. @@ -23,7 +23,7 @@ import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.s * IMPORTANT: Avoiding to call {_initializeSigner} either during construction (if used standalone) * or during initialization (if used as a clone) may leave the account unusable. */ -abstract contract AccountECDSA is ERC165, ERC7739Signer, ERC721Holder, ERC1155HolderLean, AccountBase { +abstract contract AccountECDSA is ERC165, IERC5267, AccountBase, ERC7739Signer, ERC721Holder, ERC1155Holder { using MessageHashUtils for bytes32; /** @@ -90,7 +90,7 @@ abstract contract AccountECDSA is ERC165, ERC7739Signer, ERC721Holder, ERC1155Ho } /// @inheritdoc ERC165 - function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { - return interfaceId == type(IERC1155Receiver).interfaceId || super.supportsInterface(interfaceId); + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, ERC1155Holder) returns (bool) { + return super.supportsInterface(interfaceId); } } diff --git a/contracts/account/draft-AccountP256.sol b/contracts/account/draft-AccountP256.sol index 43f4c7f9..b194129a 100644 --- a/contracts/account/draft-AccountP256.sol +++ b/contracts/account/draft-AccountP256.sol @@ -2,16 +2,17 @@ pragma solidity ^0.8.20; +import {IERC5267} from "@openzeppelin/contracts/interfaces/IERC5267.sol"; import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol"; import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; -import {ERC1155HolderLean, IERC1155Receiver} from "../token/ERC1155/utils/ERC1155HolderLean.sol"; +import {ERC1155Holder, IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import {P256} from "@openzeppelin/contracts/utils/cryptography/P256.sol"; import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import {AccountBase} from "./draft-AccountBase.sol"; -import {ERC7739Signer} from "../utils/cryptography/draft-ERC7739Signer.sol"; +import {ERC7739Signer, EIP712} from "../utils/cryptography/draft-ERC7739Signer.sol"; /** * @dev Account implementation using {P256} signatures and {ERC7739Signer} for replay protection. @@ -22,7 +23,7 @@ import {ERC7739Signer} from "../utils/cryptography/draft-ERC7739Signer.sol"; * IMPORTANT: Avoiding to call {_initializeSigner} either during construction (if used standalone) * or during initialization (if used as a clone) may leave the account unusable. */ -abstract contract AccountP256 is ERC165, ERC7739Signer, ERC721Holder, ERC1155HolderLean, AccountBase { +abstract contract AccountP256 is ERC165, IERC5267, AccountBase, ERC7739Signer, ERC721Holder, ERC1155Holder { using MessageHashUtils for bytes32; /** @@ -91,7 +92,7 @@ abstract contract AccountP256 is ERC165, ERC7739Signer, ERC721Holder, ERC1155Hol } /// @inheritdoc ERC165 - function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { - return interfaceId == type(IERC1155Receiver).interfaceId || super.supportsInterface(interfaceId); + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, ERC1155Holder) returns (bool) { + return super.supportsInterface(interfaceId); } } diff --git a/contracts/account/draft-AccountRSA.sol b/contracts/account/draft-AccountRSA.sol index 06f363a6..b62c9976 100644 --- a/contracts/account/draft-AccountRSA.sol +++ b/contracts/account/draft-AccountRSA.sol @@ -2,16 +2,17 @@ pragma solidity ^0.8.20; +import {IERC5267} from "@openzeppelin/contracts/interfaces/IERC5267.sol"; import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol"; import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; -import {ERC1155HolderLean, IERC1155Receiver} from "../token/ERC1155/utils/ERC1155HolderLean.sol"; +import {ERC1155Holder, IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import {RSA} from "@openzeppelin/contracts/utils/cryptography/RSA.sol"; import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import {AccountBase} from "./draft-AccountBase.sol"; -import {ERC7739Signer} from "../utils/cryptography/draft-ERC7739Signer.sol"; +import {ERC7739Signer, EIP712} from "../utils/cryptography/draft-ERC7739Signer.sol"; /** * @dev Account implementation using {RSA} signatures and {ERC7739Signer} for replay protection. @@ -22,7 +23,7 @@ import {ERC7739Signer} from "../utils/cryptography/draft-ERC7739Signer.sol"; * IMPORTANT: Avoiding to call {_initializeSigner} either during construction (if used standalone) * or during initialization (if used as a clone) may leave the account unusable. */ -abstract contract AccountRSA is ERC165, ERC7739Signer, ERC721Holder, ERC1155HolderLean, AccountBase { +abstract contract AccountRSA is ERC165, IERC5267, AccountBase, ERC7739Signer, ERC721Holder, ERC1155Holder { using MessageHashUtils for bytes32; /** @@ -88,7 +89,7 @@ abstract contract AccountRSA is ERC165, ERC7739Signer, ERC721Holder, ERC1155Hold } /// @inheritdoc ERC165 - function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { - return interfaceId == type(IERC1155Receiver).interfaceId || super.supportsInterface(interfaceId); + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, ERC1155Holder) returns (bool) { + return super.supportsInterface(interfaceId); } } diff --git a/contracts/mocks/ERC1155Mock.sol b/contracts/mocks/ERC1155Mock.sol index 45b0bdca..290284eb 100644 --- a/contracts/mocks/ERC1155Mock.sol +++ b/contracts/mocks/ERC1155Mock.sol @@ -4,10 +4,4 @@ pragma solidity ^0.8.20; import {ERC1155} from "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; -contract ERC1155Mock is ERC1155 { - constructor(string memory _uri) ERC1155(_uri) {} - - function $mintBatch(address to, uint256[] memory ids, uint256[] memory values, bytes memory data) external virtual { - _mintBatch(to, ids, values, data); - } -} +abstract contract ERC1155Mock is ERC1155 {} diff --git a/test/account/Account.behavior.js b/test/account/Account.behavior.js index b741b0d9..4fc65e66 100644 --- a/test/account/Account.behavior.js +++ b/test/account/Account.behavior.js @@ -151,8 +151,8 @@ function shouldBehaveLikeAccountHolder() { beforeEach(async function () { [this.owner] = await ethers.getSigners(); - this.token = await ethers.deployContract('ERC1155Mock', ['https://somedomain.com/{id}.json']); - await this.token.$mintBatch(this.owner, ids, values, '0x'); + this.token = await ethers.deployContract('$ERC1155Mock', ['https://somedomain.com/{id}.json']); + await this.token.$_mintBatch(this.owner, ids, values, '0x'); }); it('receives ERC1155 tokens from a single ID', async function () { From 5456f26a03a31dfc809098a630392e8ed06cda28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Tue, 10 Dec 2024 11:54:17 -0600 Subject: [PATCH 25/51] up --- contracts/mocks/ERC721Mock.sol | 8 +------- test/account/Account.behavior.js | 4 ++-- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/contracts/mocks/ERC721Mock.sol b/contracts/mocks/ERC721Mock.sol index 31e16bf6..f72a940a 100644 --- a/contracts/mocks/ERC721Mock.sol +++ b/contracts/mocks/ERC721Mock.sol @@ -4,10 +4,4 @@ pragma solidity ^0.8.20; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -contract ERC721Mock is ERC721 { - constructor(string memory _name, string memory _symbol) ERC721(_name, _symbol) {} - - function $mint(address owner, uint256 tokenId) external virtual { - _mint(owner, tokenId); - } -} +abstract contract ERC721Mock is ERC721 {} diff --git a/test/account/Account.behavior.js b/test/account/Account.behavior.js index 4fc65e66..e076cc03 100644 --- a/test/account/Account.behavior.js +++ b/test/account/Account.behavior.js @@ -188,8 +188,8 @@ function shouldBehaveLikeAccountHolder() { const [owner] = await ethers.getSigners(); - const token = await ethers.deployContract('ERC721Mock', [name, symbol]); - await token.$mint(owner, tokenId); + const token = await ethers.deployContract('$ERC721Mock', [name, symbol]); + await token.$_mint(owner, tokenId); await token.connect(owner).safeTransferFrom(owner, this.mock, tokenId); From 524bf15852de2c865987a7094e1b416c7c23f8b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Tue, 10 Dec 2024 13:34:19 -0600 Subject: [PATCH 26/51] Remove ERC1155HolderLean --- .../token/ERC1155/utils/ERC1155HolderLean.sol | 31 ------------------- 1 file changed, 31 deletions(-) delete mode 100644 contracts/token/ERC1155/utils/ERC1155HolderLean.sol diff --git a/contracts/token/ERC1155/utils/ERC1155HolderLean.sol b/contracts/token/ERC1155/utils/ERC1155HolderLean.sol deleted file mode 100644 index f55e39c8..00000000 --- a/contracts/token/ERC1155/utils/ERC1155HolderLean.sol +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC1155/utils/ERC1155Holder.sol) - -pragma solidity ^0.8.20; - -import {IERC1155Receiver} from "@openzeppelin/contracts/interfaces/IERC1155Receiver.sol"; - -/** - * @dev Version of {ERC1155Holder} that doesn't include {IERC165} detection. - */ -abstract contract ERC1155HolderLean is IERC1155Receiver { - function onERC1155Received( - address, - address, - uint256, - uint256, - bytes memory - ) public virtual override returns (bytes4) { - return this.onERC1155Received.selector; - } - - function onERC1155BatchReceived( - address, - address, - uint256[] memory, - uint256[] memory, - bytes memory - ) public virtual override returns (bytes4) { - return this.onERC1155BatchReceived.selector; - } -} From 4718b87c9f8786869190d8d7cd48ef7290d8d85c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Tue, 10 Dec 2024 14:25:14 -0600 Subject: [PATCH 27/51] Abstract AccountSignerDomain --- contracts/account/draft-AccountECDSA.sol | 38 ++++--------------- contracts/account/draft-AccountP256.sol | 31 +++------------ contracts/account/draft-AccountRSA.sol | 31 +++------------ .../extensions/draft-AccountSignerDomain.sol | 32 ++++++++++++++++ 4 files changed, 49 insertions(+), 83 deletions(-) create mode 100644 contracts/account/extensions/draft-AccountSignerDomain.sol diff --git a/contracts/account/draft-AccountECDSA.sol b/contracts/account/draft-AccountECDSA.sol index b80d3bed..7d94e419 100644 --- a/contracts/account/draft-AccountECDSA.sol +++ b/contracts/account/draft-AccountECDSA.sol @@ -2,20 +2,16 @@ pragma solidity ^0.8.20; -import {IERC5267} from "@openzeppelin/contracts/interfaces/IERC5267.sol"; import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; -import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol"; -import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; -import {ERC1155Holder, IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; -import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; -import {AccountBase} from "./draft-AccountBase.sol"; -import {ERC7739Signer, EIP712} from "../utils/cryptography/draft-ERC7739Signer.sol"; +import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; +import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; +import {AccountSignerDomain} from "./extensions/draft-AccountSignerDomain.sol"; /** - * @dev Account implementation using {ECDSA} signatures and {ERC7739Signer} for replay protection. + * @dev Account implementation using {ECDSA} signatures and {AccountSignerDomain} for replay protection. * * An {_initializeSigner} function is provided to set the account's signer address. Doing so it's * easier for a factory, whose likely to use initializable clones of this contract. @@ -23,7 +19,7 @@ import {ERC7739Signer, EIP712} from "../utils/cryptography/draft-ERC7739Signer.s * IMPORTANT: Avoiding to call {_initializeSigner} either during construction (if used standalone) * or during initialization (if used as a clone) may leave the account unusable. */ -abstract contract AccountECDSA is ERC165, IERC5267, AccountBase, ERC7739Signer, ERC721Holder, ERC1155Holder { +abstract contract AccountECDSA is AccountSignerDomain, ERC721Holder, ERC1155Holder { using MessageHashUtils for bytes32; /** @@ -58,34 +54,14 @@ abstract contract AccountECDSA is ERC165, IERC5267, AccountBase, ERC7739Signer, return userOpHash.toEthSignedMessageHash(); } - /** - * @dev Internal version of {validateUserOp} that relies on {_validateSignature}. - * - * The `userOpSignedHash` is the digest from {_userOpSignedHash}. - * - * NOTE: To override the signature functionality, try overriding {_validateSignature} instead. - */ - function _validateUserOp( - PackedUserOperation calldata userOp, - bytes32 userOpSignedHash - ) internal view virtual override returns (uint256) { - return - _isValidSignature(userOpSignedHash, userOp.signature) - ? ERC4337Utils.SIG_VALIDATION_SUCCESS - : ERC4337Utils.SIG_VALIDATION_FAILED; - } - /** * @dev Validates the signature using the account's signer. * * This function provides a nested EIP-712 hash. Developers must override only this * function to ensure no raw message signing is possible. */ - function _validateSignature( - bytes32 nestedEIP712Hash, - bytes calldata signature - ) internal view virtual override returns (bool) { - (address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(nestedEIP712Hash, signature); + function _validateSignature(bytes32 hash, bytes calldata signature) internal view virtual override returns (bool) { + (address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(hash, signature); return signer() == recovered && err == ECDSA.RecoverError.NoError; } diff --git a/contracts/account/draft-AccountP256.sol b/contracts/account/draft-AccountP256.sol index b194129a..2e2aca96 100644 --- a/contracts/account/draft-AccountP256.sol +++ b/contracts/account/draft-AccountP256.sol @@ -2,20 +2,16 @@ pragma solidity ^0.8.20; -import {IERC5267} from "@openzeppelin/contracts/interfaces/IERC5267.sol"; import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; -import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol"; -import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; -import {ERC1155Holder, IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; -import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import {P256} from "@openzeppelin/contracts/utils/cryptography/P256.sol"; import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; -import {AccountBase} from "./draft-AccountBase.sol"; -import {ERC7739Signer, EIP712} from "../utils/cryptography/draft-ERC7739Signer.sol"; +import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; +import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; +import {AccountSignerDomain} from "./extensions/draft-AccountSignerDomain.sol"; /** - * @dev Account implementation using {P256} signatures and {ERC7739Signer} for replay protection. + * @dev Account implementation using {P256} signatures and {AccountSignerDomain} for replay protection. * * An {_initializeSigner} function is provided to set the account's signer address. Doing so it's * easier for a factory, whose likely to use initializable clones of this contract. @@ -23,7 +19,7 @@ import {ERC7739Signer, EIP712} from "../utils/cryptography/draft-ERC7739Signer.s * IMPORTANT: Avoiding to call {_initializeSigner} either during construction (if used standalone) * or during initialization (if used as a clone) may leave the account unusable. */ -abstract contract AccountP256 is ERC165, IERC5267, AccountBase, ERC7739Signer, ERC721Holder, ERC1155Holder { +abstract contract AccountP256 is AccountSignerDomain, ERC721Holder, ERC1155Holder { using MessageHashUtils for bytes32; /** @@ -60,23 +56,6 @@ abstract contract AccountP256 is ERC165, IERC5267, AccountBase, ERC7739Signer, E return userOpHash.toEthSignedMessageHash(); } - /** - * @dev Internal version of {validateUserOp} that relies on {_validateSignature}. - * - * The `userOpSignedHash` is the digest from {_userOpSignedHash}. - * - * NOTE: To override the signature functionality, try overriding {_validateSignature} instead. - */ - function _validateUserOp( - PackedUserOperation calldata userOp, - bytes32 userOpSignedHash - ) internal view virtual override returns (uint256) { - return - _isValidSignature(userOpSignedHash, userOp.signature) - ? ERC4337Utils.SIG_VALIDATION_SUCCESS - : ERC4337Utils.SIG_VALIDATION_FAILED; - } - /** * @dev Validates the signature using the account's signer. * diff --git a/contracts/account/draft-AccountRSA.sol b/contracts/account/draft-AccountRSA.sol index b62c9976..b8eaf656 100644 --- a/contracts/account/draft-AccountRSA.sol +++ b/contracts/account/draft-AccountRSA.sol @@ -2,20 +2,16 @@ pragma solidity ^0.8.20; -import {IERC5267} from "@openzeppelin/contracts/interfaces/IERC5267.sol"; import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; -import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol"; -import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; -import {ERC1155Holder, IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; -import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import {RSA} from "@openzeppelin/contracts/utils/cryptography/RSA.sol"; import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; -import {AccountBase} from "./draft-AccountBase.sol"; -import {ERC7739Signer, EIP712} from "../utils/cryptography/draft-ERC7739Signer.sol"; +import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; +import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; +import {AccountSignerDomain} from "./extensions/draft-AccountSignerDomain.sol"; /** - * @dev Account implementation using {RSA} signatures and {ERC7739Signer} for replay protection. + * @dev Account implementation using {RSA} signatures and {AccountSignerDomain} for replay protection. * * An {_initializeSigner} function is provided to set the account's signer address. Doing so it's * easier for a factory, whose likely to use initializable clones of this contract. @@ -23,7 +19,7 @@ import {ERC7739Signer, EIP712} from "../utils/cryptography/draft-ERC7739Signer.s * IMPORTANT: Avoiding to call {_initializeSigner} either during construction (if used standalone) * or during initialization (if used as a clone) may leave the account unusable. */ -abstract contract AccountRSA is ERC165, IERC5267, AccountBase, ERC7739Signer, ERC721Holder, ERC1155Holder { +abstract contract AccountRSA is AccountSignerDomain, ERC721Holder, ERC1155Holder { using MessageHashUtils for bytes32; /** @@ -60,23 +56,6 @@ abstract contract AccountRSA is ERC165, IERC5267, AccountBase, ERC7739Signer, ER return userOpHash.toEthSignedMessageHash(); } - /** - * @dev Internal version of {validateUserOp} that relies on {_validateSignature}. - * - * The `userOpSignedHash` is the digest from {_userOpSignedHash}. - * - * NOTE: To override the signature functionality, try overriding {_validateSignature} instead. - */ - function _validateUserOp( - PackedUserOperation calldata userOp, - bytes32 userOpSignedHash - ) internal view virtual override returns (uint256) { - return - _isValidSignature(userOpSignedHash, userOp.signature) - ? ERC4337Utils.SIG_VALIDATION_SUCCESS - : ERC4337Utils.SIG_VALIDATION_FAILED; - } - /** * @dev Validates the signature using the account's signer. * diff --git a/contracts/account/extensions/draft-AccountSignerDomain.sol b/contracts/account/extensions/draft-AccountSignerDomain.sol new file mode 100644 index 00000000..f4624734 --- /dev/null +++ b/contracts/account/extensions/draft-AccountSignerDomain.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +import {IERC5267} from "@openzeppelin/contracts/interfaces/IERC5267.sol"; +import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; +import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol"; +import {ERC7739Signer} from "../../utils/cryptography/draft-ERC7739Signer.sol"; +import {AccountBase} from "../draft-AccountBase.sol"; + +/** + * @dev An ERC-4337 account implementation that validates domain-specific signatures following ERC-7739. + */ +abstract contract AccountSignerDomain is ERC165, IERC5267, ERC7739Signer, AccountBase { + /** + * @dev Internal version of {validateUserOp} that relies on {_validateSignature}. + * + * The `userOpSignedHash` is the digest from {_userOpSignedHash}. + * + * NOTE: To override the signature functionality, try overriding {_validateSignature} instead. + */ + function _validateUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpSignedHash + ) internal view virtual override returns (uint256) { + return + _isValidSignature(userOpSignedHash, userOp.signature) + ? ERC4337Utils.SIG_VALIDATION_SUCCESS + : ERC4337Utils.SIG_VALIDATION_FAILED; + } +} From c8ad19d4a0c2d8ac578f28488672cc5da576ce25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Tue, 10 Dec 2024 14:25:37 -0600 Subject: [PATCH 28/51] up --- contracts/account/draft-AccountBase.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/account/draft-AccountBase.sol b/contracts/account/draft-AccountBase.sol index eeea0cd0..b050bba5 100644 --- a/contracts/account/draft-AccountBase.sol +++ b/contracts/account/draft-AccountBase.sol @@ -103,7 +103,7 @@ abstract contract AccountBase is IAccount, IAccountExecute { function _validateUserOp( PackedUserOperation calldata userOp, bytes32 userOpSignedHash - ) internal view virtual returns (uint256 validationData); + ) internal virtual returns (uint256 validationData); /** * @dev Sends the missing funds for executing the user operation to the {entrypoint}. From c41956c70ada76fceb18897c34433dedae869994 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Tue, 10 Dec 2024 18:02:37 -0600 Subject: [PATCH 29/51] Remove signed hash fn --- contracts/account/draft-AccountBase.sol | 24 +++++-------------- contracts/account/draft-AccountECDSA.sol | 14 +++++------ contracts/account/draft-AccountP256.sol | 14 +++++------ contracts/account/draft-AccountRSA.sol | 14 +++++------ ...nerDomain.sol => draft-AccountERC7739.sol} | 8 +++---- .../mocks/docs/account/MyAccountCustom.sol | 6 ++--- 6 files changed, 33 insertions(+), 47 deletions(-) rename contracts/account/extensions/{draft-AccountSignerDomain.sol => draft-AccountERC7739.sol} (80%) diff --git a/contracts/account/draft-AccountBase.sol b/contracts/account/draft-AccountBase.sol index b050bba5..3e29e30d 100644 --- a/contracts/account/draft-AccountBase.sol +++ b/contracts/account/draft-AccountBase.sol @@ -54,21 +54,6 @@ abstract contract AccountBase is IAccount, IAccountExecute { return entryPoint().getNonce(address(this), key); } - /** - * @dev Returns the digest the offchain signer signed instead of the opaque `userOpHash`. - * - * Given the `userOpHash` calculation is defined by ERC-4337, offchain signers - * may need to sign again this hash by rehashing it with other schemes. - * - * Returns the `userOpHash` by default. - */ - function _userOpSignedHash( - PackedUserOperation calldata /* userOp */, - bytes32 userOpHash - ) internal view virtual returns (bytes32) { - return userOpHash; - } - /** * @inheritdoc IAccount */ @@ -77,7 +62,7 @@ abstract contract AccountBase is IAccount, IAccountExecute { bytes32 userOpHash, uint256 missingAccountFunds ) public virtual onlyEntryPoint returns (uint256) { - uint256 validationData = _validateUserOp(userOp, _userOpSignedHash(userOp, userOpHash)); + uint256 validationData = _validateUserOp(userOp, userOpHash); _payPrefund(missingAccountFunds); return validationData; } @@ -94,7 +79,10 @@ abstract contract AccountBase is IAccount, IAccountExecute { } /** - * @dev Validation logic for {validateUserOp}. The `userOpSignedHash` is the digest from {_userOpSignedHash}. + * @dev Validation logic for {validateUserOp}. + * + * Given the `userOpHash` calculation is defined by ERC-4337, offchain signers + * may need to sign this hash by wrapping it in other schemes (e.g. ERC-191) * * IMPORTANT: Implementing a mechanism to validate user operations is a security-sensitive operation * as it may allow an attacker to bypass the account's security measures. Check out {AccountECDSA}, @@ -102,7 +90,7 @@ abstract contract AccountBase is IAccount, IAccountExecute { */ function _validateUserOp( PackedUserOperation calldata userOp, - bytes32 userOpSignedHash + bytes32 userOpHash ) internal virtual returns (uint256 validationData); /** diff --git a/contracts/account/draft-AccountECDSA.sol b/contracts/account/draft-AccountECDSA.sol index 7d94e419..803fbaf4 100644 --- a/contracts/account/draft-AccountECDSA.sol +++ b/contracts/account/draft-AccountECDSA.sol @@ -8,10 +8,10 @@ import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; -import {AccountSignerDomain} from "./extensions/draft-AccountSignerDomain.sol"; +import {AccountERC7739} from "./extensions/draft-AccountERC7739.sol"; /** - * @dev Account implementation using {ECDSA} signatures and {AccountSignerDomain} for replay protection. + * @dev Account implementation using {ECDSA} signatures and {AccountERC7739} for replay protection. * * An {_initializeSigner} function is provided to set the account's signer address. Doing so it's * easier for a factory, whose likely to use initializable clones of this contract. @@ -19,7 +19,7 @@ import {AccountSignerDomain} from "./extensions/draft-AccountSignerDomain.sol"; * IMPORTANT: Avoiding to call {_initializeSigner} either during construction (if used standalone) * or during initialization (if used as a clone) may leave the account unusable. */ -abstract contract AccountECDSA is AccountSignerDomain, ERC721Holder, ERC1155Holder { +abstract contract AccountECDSA is AccountERC7739, ERC721Holder, ERC1155Holder { using MessageHashUtils for bytes32; /** @@ -47,11 +47,11 @@ abstract contract AccountECDSA is AccountSignerDomain, ERC721Holder, ERC1155Hold /** * @dev Returns the ERC-191 signed `userOpHash` hashed with keccak256 using `personal_sign`. */ - function _userOpSignedHash( - PackedUserOperation calldata /* userOp */, + function _validateUserOp( + PackedUserOperation calldata userOp, bytes32 userOpHash - ) internal view virtual override returns (bytes32) { - return userOpHash.toEthSignedMessageHash(); + ) internal view virtual override returns (uint256) { + return super._validateUserOp(userOp, userOpHash.toEthSignedMessageHash()); } /** diff --git a/contracts/account/draft-AccountP256.sol b/contracts/account/draft-AccountP256.sol index 2e2aca96..c74d1c35 100644 --- a/contracts/account/draft-AccountP256.sol +++ b/contracts/account/draft-AccountP256.sol @@ -8,10 +8,10 @@ import {P256} from "@openzeppelin/contracts/utils/cryptography/P256.sol"; import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; -import {AccountSignerDomain} from "./extensions/draft-AccountSignerDomain.sol"; +import {AccountERC7739} from "./extensions/draft-AccountERC7739.sol"; /** - * @dev Account implementation using {P256} signatures and {AccountSignerDomain} for replay protection. + * @dev Account implementation using {P256} signatures and {AccountERC7739} for replay protection. * * An {_initializeSigner} function is provided to set the account's signer address. Doing so it's * easier for a factory, whose likely to use initializable clones of this contract. @@ -19,7 +19,7 @@ import {AccountSignerDomain} from "./extensions/draft-AccountSignerDomain.sol"; * IMPORTANT: Avoiding to call {_initializeSigner} either during construction (if used standalone) * or during initialization (if used as a clone) may leave the account unusable. */ -abstract contract AccountP256 is AccountSignerDomain, ERC721Holder, ERC1155Holder { +abstract contract AccountP256 is AccountERC7739, ERC721Holder, ERC1155Holder { using MessageHashUtils for bytes32; /** @@ -49,11 +49,11 @@ abstract contract AccountP256 is AccountSignerDomain, ERC721Holder, ERC1155Holde /** * @dev Returns the ERC-191 signed `userOpHash` hashed with keccak256 using `personal_sign`. */ - function _userOpSignedHash( - PackedUserOperation calldata /* userOp */, + function _validateUserOp( + PackedUserOperation calldata userOp, bytes32 userOpHash - ) internal view virtual override returns (bytes32) { - return userOpHash.toEthSignedMessageHash(); + ) internal view virtual override returns (uint256) { + return super._validateUserOp(userOp, userOpHash.toEthSignedMessageHash()); } /** diff --git a/contracts/account/draft-AccountRSA.sol b/contracts/account/draft-AccountRSA.sol index b8eaf656..2aa09f06 100644 --- a/contracts/account/draft-AccountRSA.sol +++ b/contracts/account/draft-AccountRSA.sol @@ -8,10 +8,10 @@ import {RSA} from "@openzeppelin/contracts/utils/cryptography/RSA.sol"; import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; -import {AccountSignerDomain} from "./extensions/draft-AccountSignerDomain.sol"; +import {AccountERC7739} from "./extensions/draft-AccountERC7739.sol"; /** - * @dev Account implementation using {RSA} signatures and {AccountSignerDomain} for replay protection. + * @dev Account implementation using {RSA} signatures and {AccountERC7739} for replay protection. * * An {_initializeSigner} function is provided to set the account's signer address. Doing so it's * easier for a factory, whose likely to use initializable clones of this contract. @@ -19,7 +19,7 @@ import {AccountSignerDomain} from "./extensions/draft-AccountSignerDomain.sol"; * IMPORTANT: Avoiding to call {_initializeSigner} either during construction (if used standalone) * or during initialization (if used as a clone) may leave the account unusable. */ -abstract contract AccountRSA is AccountSignerDomain, ERC721Holder, ERC1155Holder { +abstract contract AccountRSA is AccountERC7739, ERC721Holder, ERC1155Holder { using MessageHashUtils for bytes32; /** @@ -49,11 +49,11 @@ abstract contract AccountRSA is AccountSignerDomain, ERC721Holder, ERC1155Holder /** * @dev Returns the ERC-191 signed `userOpHash` hashed with keccak256 using `personal_sign`. */ - function _userOpSignedHash( - PackedUserOperation calldata /* userOp */, + function _validateUserOp( + PackedUserOperation calldata userOp, bytes32 userOpHash - ) internal view virtual override returns (bytes32) { - return userOpHash.toEthSignedMessageHash(); + ) internal view virtual override returns (uint256) { + return super._validateUserOp(userOp, userOpHash.toEthSignedMessageHash()); } /** diff --git a/contracts/account/extensions/draft-AccountSignerDomain.sol b/contracts/account/extensions/draft-AccountERC7739.sol similarity index 80% rename from contracts/account/extensions/draft-AccountSignerDomain.sol rename to contracts/account/extensions/draft-AccountERC7739.sol index f4624734..202100b0 100644 --- a/contracts/account/extensions/draft-AccountSignerDomain.sol +++ b/contracts/account/extensions/draft-AccountERC7739.sol @@ -12,20 +12,18 @@ import {AccountBase} from "../draft-AccountBase.sol"; /** * @dev An ERC-4337 account implementation that validates domain-specific signatures following ERC-7739. */ -abstract contract AccountSignerDomain is ERC165, IERC5267, ERC7739Signer, AccountBase { +abstract contract AccountERC7739 is ERC165, IERC5267, ERC7739Signer, AccountBase { /** * @dev Internal version of {validateUserOp} that relies on {_validateSignature}. * - * The `userOpSignedHash` is the digest from {_userOpSignedHash}. - * * NOTE: To override the signature functionality, try overriding {_validateSignature} instead. */ function _validateUserOp( PackedUserOperation calldata userOp, - bytes32 userOpSignedHash + bytes32 userOpHash ) internal view virtual override returns (uint256) { return - _isValidSignature(userOpSignedHash, userOp.signature) + _isValidSignature(userOpHash, userOp.signature) ? ERC4337Utils.SIG_VALIDATION_SUCCESS : ERC4337Utils.SIG_VALIDATION_FAILED; } diff --git a/contracts/mocks/docs/account/MyAccountCustom.sol b/contracts/mocks/docs/account/MyAccountCustom.sol index cc51f009..bf4a8560 100644 --- a/contracts/mocks/docs/account/MyAccountCustom.sol +++ b/contracts/mocks/docs/account/MyAccountCustom.sol @@ -23,13 +23,13 @@ contract MyAccountCustom is ERC7739Signer, AccountBase, Initializable { // Custom initialization logic } - /// @dev Receives an `userOpSignedHash` to validate. See {_userOpSignedHash}. + /// @inheritdoc AccountBase function _validateUserOp( PackedUserOperation calldata userOp, - bytes32 userOpSignedHash + bytes32 userOpHash ) internal view virtual override returns (uint256) { return - _isValidSignature(userOpSignedHash, userOp.signature) + _isValidSignature(userOpHash, userOp.signature) ? ERC4337Utils.SIG_VALIDATION_SUCCESS : ERC4337Utils.SIG_VALIDATION_FAILED; } From 7e51cd2c1e7f4f3334a52d6d607471f4a3953571 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Fri, 13 Dec 2024 09:30:36 -0600 Subject: [PATCH 30/51] Add standalone example of usage --- contracts/account/draft-AccountECDSA.sol | 17 +++++++++++++++-- contracts/account/draft-AccountP256.sol | 17 +++++++++++++++-- contracts/account/draft-AccountRSA.sol | 17 +++++++++++++++-- 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/contracts/account/draft-AccountECDSA.sol b/contracts/account/draft-AccountECDSA.sol index 803fbaf4..66b22bb9 100644 --- a/contracts/account/draft-AccountECDSA.sol +++ b/contracts/account/draft-AccountECDSA.sol @@ -16,8 +16,21 @@ import {AccountERC7739} from "./extensions/draft-AccountERC7739.sol"; * An {_initializeSigner} function is provided to set the account's signer address. Doing so it's * easier for a factory, whose likely to use initializable clones of this contract. * + * Example of usage: + * + * ```solidity + * contract MyAccountECDSA is AccountECDSA { + * constructor() EIP712("MyAccountECDSA", "1") {} + * + * function initializeSigner(address signerAddr) public virtual initializer { + * // Will revert if the signer is already initialized + * _initializeSigner(signerAddr); + * } + * } + * ``` + * * IMPORTANT: Avoiding to call {_initializeSigner} either during construction (if used standalone) - * or during initialization (if used as a clone) may leave the account unusable. + * or during initialization (if used as a clone) may leave the account either front-runnable or unusable. */ abstract contract AccountECDSA is AccountERC7739, ERC721Holder, ERC1155Holder { using MessageHashUtils for bytes32; @@ -30,7 +43,7 @@ abstract contract AccountECDSA is AccountERC7739, ERC721Holder, ERC1155Holder { address private _signer; /** - * @dev Initializes the account with the address of the native signer. This function is called only once. + * @dev Initializes the account with the address of the native signer. This function can be called only once. */ function _initializeSigner(address signerAddr) internal { if (_signer != address(0)) revert AccountECDSAUninitializedSigner(signerAddr); diff --git a/contracts/account/draft-AccountP256.sol b/contracts/account/draft-AccountP256.sol index c74d1c35..25c5ab60 100644 --- a/contracts/account/draft-AccountP256.sol +++ b/contracts/account/draft-AccountP256.sol @@ -16,8 +16,21 @@ import {AccountERC7739} from "./extensions/draft-AccountERC7739.sol"; * An {_initializeSigner} function is provided to set the account's signer address. Doing so it's * easier for a factory, whose likely to use initializable clones of this contract. * + * Example of usage: + * + * ```solidity + * contract MyAccountP256 is AccountP256 { + * constructor() EIP712("MyAccountP256", "1") {} + * + * function initializeSigner(bytes32 qx, bytes32 qy) public virtual initializer { + * // Will revert if the signer is already initialized + * _initializeSigner(qx, qy); + * } + * } + * ``` + * * IMPORTANT: Avoiding to call {_initializeSigner} either during construction (if used standalone) - * or during initialization (if used as a clone) may leave the account unusable. + * or during initialization (if used as a clone) may leave the account either front-runnable or unusable. */ abstract contract AccountP256 is AccountERC7739, ERC721Holder, ERC1155Holder { using MessageHashUtils for bytes32; @@ -31,7 +44,7 @@ abstract contract AccountP256 is AccountERC7739, ERC721Holder, ERC1155Holder { bytes32 private _qy; /** - * @dev Initializes the account with the P256 public key. + * @dev Initializes the account with the P256 public key. This function can be called only once. */ function _initializeSigner(bytes32 qx, bytes32 qy) internal { if (_qx != 0 || _qy != 0) revert AccountP256UninitializedSigner(qx, qy); diff --git a/contracts/account/draft-AccountRSA.sol b/contracts/account/draft-AccountRSA.sol index 2aa09f06..3665eaec 100644 --- a/contracts/account/draft-AccountRSA.sol +++ b/contracts/account/draft-AccountRSA.sol @@ -16,8 +16,21 @@ import {AccountERC7739} from "./extensions/draft-AccountERC7739.sol"; * An {_initializeSigner} function is provided to set the account's signer address. Doing so it's * easier for a factory, whose likely to use initializable clones of this contract. * + * Example of usage: + * + * ```solidity + * contract MyAccountRSA is AccountRSA { + * constructor() EIP712("MyAccountRSA", "1") {} + * + * function initializeSigner(bytes memory e, bytes memory n) external { + * // Will revert if the signer is already initialized + * _initializeSigner(e, n); + * } + * } + * ``` + * * IMPORTANT: Avoiding to call {_initializeSigner} either during construction (if used standalone) - * or during initialization (if used as a clone) may leave the account unusable. + * or during initialization (if used as a clone) may leave the account either front-runnable or unusable. */ abstract contract AccountRSA is AccountERC7739, ERC721Holder, ERC1155Holder { using MessageHashUtils for bytes32; @@ -31,7 +44,7 @@ abstract contract AccountRSA is AccountERC7739, ERC721Holder, ERC1155Holder { bytes private _n; /** - * @dev Initializes the account with the RSA public key. + * @dev Initializes the account with the RSA public key. This function can be called only once. */ function _initializeSigner(bytes memory e, bytes memory n) internal { if (_e.length != 0 || _n.length != 0) revert AccountP256UninitializedSigner(e, n); From f497fd1a7da15ef596d2eae46e0536d7e35f0856 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Fri, 13 Dec 2024 09:48:31 -0600 Subject: [PATCH 31/51] Remove docs --- contracts/account/README.adoc | 15 -- docs/modules/ROOT/nav.adoc | 1 - .../ROOT/pages/account-abstraction.adoc | 154 ------------------ 3 files changed, 170 deletions(-) delete mode 100644 contracts/account/README.adoc delete mode 100644 docs/modules/ROOT/pages/account-abstraction.adoc diff --git a/contracts/account/README.adoc b/contracts/account/README.adoc deleted file mode 100644 index a1188bf4..00000000 --- a/contracts/account/README.adoc +++ /dev/null @@ -1,15 +0,0 @@ -= Account -[.readme-notice] -NOTE: This document is better viewed at https://docs.openzeppelin.com/community-contracts/api/account - -This directory includes contracts to build accounts for ERC-4337. - -== Core - -{{AccountBase}} - -{{AccountECDSA}} - -{{AccountP256}} - -{{AccountRSA}} diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index f46339c1..7c0d852c 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -1,3 +1,2 @@ * xref:index.adoc[Overview] -* xref:account-abstraction.adoc[Account Abstraction] * xref:utilities.adoc[Utilities] diff --git a/docs/modules/ROOT/pages/account-abstraction.adoc b/docs/modules/ROOT/pages/account-abstraction.adoc deleted file mode 100644 index 53c8d826..00000000 --- a/docs/modules/ROOT/pages/account-abstraction.adoc +++ /dev/null @@ -1,154 +0,0 @@ -= Account Abstraction - -Unlike Externally Owned Accounts (EOAs), smart contracts may contain arbitrary verification logic based on authentication mechanisms different to Ethereum's native xref:api:utils.adoc#ECDSA[ECDSA] and have execution advantages such as batching or gas sponsorship. To leverage these properties of smart contracts, the community has widely adopted https://eips.ethereum.org/EIPS/eip-4337[ERC-4337], a standard to process user operations through an alternative mempool. - -The library provides multiple contracts for Account Abstraction following this standard as it enables more flexible and user-friendly interactions with applications. Account Abstraction use cases include wallets in novel contexts (e.g. embedded wallets), more granular configuration of accounts and recovery mechanisms. These capabilities can be supercharged with a modularity approach following standards such as https://eips.ethereum.org/EIPS/eip-7579[ERC-7579]. - -== Smart Accounts - -OpenZeppelin provides an abstract implementation of an xref:api:account.adoc#AccountBase[`AccountBase`] that implements the basic logic to handle user operations in compliance with ERC-4337. Developers who want to build their own Account implementation can use this to bootstrap. - -Aside from the xref:api:account.adoc#AccountBase[`AccountBase`], the library includes various specialized accounts that implement xref:api:account.adoc#AccountBase-validateUserOp-struct-PackedUserOperation-bytes32-uint256-[`_validateUserOp`] using a digital signature verification algorithm like https://docs.openzeppelin.com/contracts/5.x/api/utils#ECDSA[`ECDSA`], https://docs.openzeppelin.com/contracts/5.x/api/utils#P256[`P256`] or https://docs.openzeppelin.com/contracts/5.x/api/utils#RSA[`RSA`]. - -These specialized accounts are ready to use and implement an opinionated set of layers: - -* xref:api:utils.adoc#ERC7739Signer[ERC7739Signer]: An implementation of the https://eips.ethereum.org/EIPS/eip-1271[ERC-1271] interface for smart contract signatures. This layer adds a defensive rehashing mechanism that prevents signatures for this account to be replayed in another controlled by the same signer. -* https://docs.openzeppelin.com/contracts/api/token/erc721#ERC721Holder[ERC721Holder]: Allows the account to hold https://eips.ethereum.org/EIPS/eip-721[ERC-721] tokens -* https://docs.openzeppelin.com/contracts/api/token/erc721#ERC721Holder[ERC1155Holder]: Allows the account to hold https://eips.ethereum.org/EIPS/eip-1155[ERC-1155] tokens - -=== Setting up an account - -To setup an account, you can either bring your own validation logic and start with xref:api:account.adoc#AccountBase[`AccountBase`], or import any of the predefined accounts we provide and are controlled by a signing key. For example, to setup a contract controlled by a regular EVM private key, you can leverage xref:api:account.adoc#AccountECDSA[`AccountECDSA`]. - -These accounts can be deployed by a factory right away (see xref:account-abstraction.adoc#account_factory[Account Factory]) or used standalone if required. In both cases, the xref:api:account.adoc#AccountECDSA[`AccountECDSA`], xref:api:account.adoc#AccountP256[`AccountP256`] and xref:api:account.adoc#AccountRSA[`AccountRSA`] expose an internal xref:api:account.adoc#AccountECDSA-_initializeSigner-address-[`_initializeSigner`] functions that set the public key (or address) that controls them. - -WARNING: Leaving an account uninitialized may leave it unusable since no public key was associated with it. - -Since smart accounts are deployed by a factory, the best practice is to create https://docs.openzeppelin.com/contracts/5.x/api/proxy#minimal_clones[minimal clones] of initializable contracts. These account implementations provide an initializable design by default so that the factory can interact with the account to set it up after deployment in a single transaction. - -[source,solidity] ----- -include::api:example$account/MyAccountECDSA.sol[] ----- - -NOTE: xref:api:account.adoc#AccountECDSA[`AccountECDSA`] initializes xref:api:utils.adoc#EIP712[`EIP712`] to generate a domain separator that prevents replayability in other accounts controlled by the same key. See xref:account-abstraction.adoc#eip712_typed_signatures[EIP-712 Typed signatures] - -Along with the regular ECDSA verification, the library also provides the xref:api:account.adoc#AccountP256[`AccountP256`], which is a widely used _elliptic curve_ verification algorithm that's present in mobile device security enclaves, FIDO keys, and corporate environments (i.e. public key infrastructures). - -[source,solidity] ----- -include::api:example$account/MyAccountP256.sol[] ----- - -Similarly, some government and corporate public key infrastructures use RSA for signature verification. For those cases, the xref:api:account.adoc#AccountRSA[`AccountRSA`] may be a good fit. - -[source,solidity] ----- -include::api:example$account/MyAccountRSA.sol[] ----- - -=== Roll your own validation - -When developers want to start by implementing their own validation logic, they should extend from xref:api:account.adoc#AccountBase[`AccountBase`]. Also, we recommend using xref:api:utils.adoc#ERC7739Signer[`ERC7739Signer`] to prevent replayability of user operations across smart accounts as noted in xref:account-abstraction.adoc#eip712_typed_signatures[EIP-712 Typed signatures]: - -[source,solidity] ----- -include::api:example$account/MyAccountCustom.sol[] ----- - -== Account Factory - -The first time a user sends an user operation, the account will be created deterministically (i.e. its code and address can be predicted) using the the `initCode` field in the UserOperation. This field contains both the address of a smart contract (the factory) and the data required to call it and deploy the smart account. - -For this purpose, developers can create an account factory using the https://docs.openzeppelin.com/contracts/5.x/api/proxy#Clones[Clones library from OpenZeppelin Contracts]. It exposes methods to calculate the address of an account before deployment. - -[source,solidity] ----- -include::api:example$account/MyFactoryAccountECDSA.sol[] ----- - -You've setup your own account and its corresponding factory. Both are ready to be used with ERC-4337 infrastructure. Customizing the factory to other validation mechanisms must be straightforward. - -== ERC-4337 Overview - -The ERC-4337 is a detailed specification of how to implement the necessary logic to handle operations without making changes to the protocol level (i.e. the rules of the blockchain itself). This specification defines the following components: - -=== UserOperation - -An `UserOperation` is a higher-layer pseudo-transaction object that represents the intent of the account. This shares some similarities with regular EVM transactions like the concept of `gasFees` or `callData` but includes fields that enable new capabilities. - -```solidity -struct PackedUserOperation { - address sender; - uint256 nonce; - bytes initCode; // concatenation of factory address and factoryData (or empty) - bytes callData; - bytes32 accountGasLimits; // concatenation of verificationGas (16 bytes) and callGas (16 bytes) - uint256 preVerificationGas; - bytes32 gasFees; // concatenation of maxPriorityFee (16 bytes) and maxFeePerGas (16 bytes) - bytes paymasterAndData; // concatenation of paymaster fields (or empty) - bytes signature; -} -``` - -=== Entrypoint - -Each `UserOperation` is executed through a contract known as the https://etherscan.io/address/0x0000000071727de22e5e9d8baf0edac6f37da032#code[`EntryPoint`]. This contract is a singleton deployed across multiple networks at the same address although other custom implementations may be used. - -The Entrypoint contracts is considered a trusted entity by the account. - -=== Bundlers - -The bundler is a piece of _offchain_ infrastructure that is in charge of processing an alternative mempool of user operations. Bundlers themselves call the Entrypoint contract's `handleOps` function with an array of UserOperations that are executed and included in a block. - -During the process, the bundler pays for the gas of executing the transaction and gets refunded during the execution phase of the Entrypoint contract. - -=== Account Contract - -The Account Contract is a type of smart contract implements the logic required to validate an `UserOperation` in the context of ERC-4337. Any smart contract account should conform with the `IAccount` interface to validate operations. - -```solidity -interface IAccount { - function validateUserOp(PackedUserOperation calldata, bytes32, uint256) external returns (uint256 validationData); -} -``` - -Similarly, an Account should have a way to execute these operations by either handling arbitrary calldata on its `fallback` or implementing the `IAccountExecute` interface: - -```solidity -interface IAccountExecute { - function executeUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash) external; -} -``` - -To build your own account, see xref:account-abstraction.adoc#smart_accounts[Smart Accounts]. - -=== Factory Contract - -The smart contract accounts are created by a Factory contract defined by the Account developer. This factory receives arbitrary bytes as `initData` and returns an `address` where the logic of the account is deployed. - -To build your own factory, see xref:account-abstraction.adoc#account_factory[Account Factory] - -=== Paymaster Contract - -A Paymaster is an optional entity that can sponsor gas fees for Accounts, or allow them to pay for those fees in ERC-20 instead of native currency. This abstracts gas away of the user experience in the same way that computational costs of cloud servers are abstracted away from end-users. - -== Further notes - -=== EIP712 Typed Signatures - -A common security practice to prevent user operation https://mirror.xyz/curiousapple.eth/pFqAdW2LiJ-6S4sg_u1z08k4vK6BCJ33LcyXpnNb8yU[replayability across smart contract accounts controlled by the same private key] (i.e. multiple accounts for the same signer) is to link the signature to the `address` and `chainId` of the account. This can be done by asking the user to sign the hash of the user operation along with these values. - -The problem with this approach is that the user might be prompted by the wallet provider to sign an https://x.com/howydev/status/1780353754333634738[obfuscated message], which is a phishing vector that may lead to a user losing its assets. - -To prevent this, each account using a signature verification algorithm inherits from xref:api:account#ERC7739Signer[`ERC7739Signer`], a utility that implements xref:api:interfaces#IERC1271[`IERC1271`] for smart contract signatures with a defensive rehashing mechanism based on a https://github.com/frangio/eip712-wrapper-for-eip1271[nested EIP-712 approach] to wrap the signature request in a context where there's clearer information for the end user. - -=== ERC-7562 Validation Rules - -To process a bundle of `UserOperations`, bundlers call xref:api:account.adoc#AccountBase-validateUserOp-struct-PackedUserOperation-bytes32-uint256-[`validateUserOp`] on each operation sender to check whether the operation can be executed. However, the bundler has no guarantee that the state of the blockchain will remain the same after the validation phase. To overcome this problem, https://eips.ethereum.org/EIPS/eip-7562[ERC-7562] proposes a set of limitations to EVM code so that bundlers (or node operators) are protected from unexpected state changes. - -These rules outline the requirements for operations to be processed by the canonical mempool. - -Accounts can access its own storage during the validation phase, they might easily violate ERC-7562 storage access rules in undirect ways. For example, most accounts access their public keys from storage when validating a signature, limiting the ability of having accounts that validate operations for other accounts (e.g. via ERC-1271) - -TIP: Although any Account that breaks such rules may still be processed by a private bundler, developers should keep in mind the centralization tradeoffs of relying on private infrastructure instead of _permissionless_ execution. From 6409cb3745468e464858e69c2436dc8ded62f1b8 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 13 Dec 2024 21:54:59 +0100 Subject: [PATCH 32/51] ERC4337 userOp validation should not be 7739 wrapped --- contracts/account/draft-AccountBase.sol | 11 +++++-- .../{ => extensions}/draft-AccountECDSA.sol | 13 +++----- .../extensions/draft-AccountERC7739.sol | 30 ------------------- .../{ => extensions}/draft-AccountP256.sol | 13 +++----- .../{ => extensions}/draft-AccountRSA.sol | 13 +++----- contracts/mocks/account/AccountBaseMock.sol | 15 ++++------ contracts/mocks/account/AccountECDSAMock.sol | 7 ++--- contracts/mocks/account/AccountP256Mock.sol | 7 ++--- contracts/mocks/account/AccountRSAMock.sol | 7 ++--- .../mocks/docs/account/MyAccountCustom.sol | 14 +-------- .../mocks/docs/account/MyAccountECDSA.sol | 2 +- .../mocks/docs/account/MyAccountP256.sol | 2 +- contracts/mocks/docs/account/MyAccountRSA.sol | 2 +- test/account/draft-AccountBase.test.js | 2 +- 14 files changed, 41 insertions(+), 97 deletions(-) rename contracts/account/{ => extensions}/draft-AccountECDSA.sol (86%) delete mode 100644 contracts/account/extensions/draft-AccountERC7739.sol rename contracts/account/{ => extensions}/draft-AccountP256.sol (87%) rename contracts/account/{ => extensions}/draft-AccountRSA.sol (86%) diff --git a/contracts/account/draft-AccountBase.sol b/contracts/account/draft-AccountBase.sol index 3e29e30d..c70a2c1c 100644 --- a/contracts/account/draft-AccountBase.sol +++ b/contracts/account/draft-AccountBase.sol @@ -3,7 +3,9 @@ pragma solidity ^0.8.20; import {PackedUserOperation, IAccount, IEntryPoint, IAccountExecute} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; +import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol"; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {ERC7739Signer} from "../utils/cryptography/draft-ERC7739Signer.sol"; /** * @dev A simple ERC4337 account implementation. @@ -11,7 +13,7 @@ import {Address} from "@openzeppelin/contracts/utils/Address.sol"; * This base implementation only includes the minimal logic to process user operations. * Developers must implement the {_validateUserOp} function to define the account's validation logic. */ -abstract contract AccountBase is IAccount, IAccountExecute { +abstract contract AccountBase is IAccount, IAccountExecute, ERC7739Signer { /** * @dev Unauthorized call to the account. */ @@ -91,7 +93,12 @@ abstract contract AccountBase is IAccount, IAccountExecute { function _validateUserOp( PackedUserOperation calldata userOp, bytes32 userOpHash - ) internal virtual returns (uint256 validationData); + ) internal virtual returns (uint256 validationData) { + return + _isValidSignature(userOpHash, userOp.signature) + ? ERC4337Utils.SIG_VALIDATION_SUCCESS + : ERC4337Utils.SIG_VALIDATION_FAILED; + } /** * @dev Sends the missing funds for executing the user operation to the {entrypoint}. diff --git a/contracts/account/draft-AccountECDSA.sol b/contracts/account/extensions/draft-AccountECDSA.sol similarity index 86% rename from contracts/account/draft-AccountECDSA.sol rename to contracts/account/extensions/draft-AccountECDSA.sol index 66b22bb9..6dfc328a 100644 --- a/contracts/account/draft-AccountECDSA.sol +++ b/contracts/account/extensions/draft-AccountECDSA.sol @@ -8,10 +8,10 @@ import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; -import {AccountERC7739} from "./extensions/draft-AccountERC7739.sol"; +import {AccountBase} from "../draft-AccountBase.sol"; /** - * @dev Account implementation using {ECDSA} signatures and {AccountERC7739} for replay protection. + * @dev Account implementation using {ECDSA} signatures and {AccountBase} for replay protection. * * An {_initializeSigner} function is provided to set the account's signer address. Doing so it's * easier for a factory, whose likely to use initializable clones of this contract. @@ -32,7 +32,7 @@ import {AccountERC7739} from "./extensions/draft-AccountERC7739.sol"; * IMPORTANT: Avoiding to call {_initializeSigner} either during construction (if used standalone) * or during initialization (if used as a clone) may leave the account either front-runnable or unusable. */ -abstract contract AccountECDSA is AccountERC7739, ERC721Holder, ERC1155Holder { +abstract contract AccountECDSA is AccountBase, ERC721Holder, ERC1155Holder { using MessageHashUtils for bytes32; /** @@ -63,7 +63,7 @@ abstract contract AccountECDSA is AccountERC7739, ERC721Holder, ERC1155Holder { function _validateUserOp( PackedUserOperation calldata userOp, bytes32 userOpHash - ) internal view virtual override returns (uint256) { + ) internal virtual override returns (uint256) { return super._validateUserOp(userOp, userOpHash.toEthSignedMessageHash()); } @@ -77,9 +77,4 @@ abstract contract AccountECDSA is AccountERC7739, ERC721Holder, ERC1155Holder { (address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(hash, signature); return signer() == recovered && err == ECDSA.RecoverError.NoError; } - - /// @inheritdoc ERC165 - function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, ERC1155Holder) returns (bool) { - return super.supportsInterface(interfaceId); - } } diff --git a/contracts/account/extensions/draft-AccountERC7739.sol b/contracts/account/extensions/draft-AccountERC7739.sol deleted file mode 100644 index 202100b0..00000000 --- a/contracts/account/extensions/draft-AccountERC7739.sol +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.20; - -import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; -import {IERC5267} from "@openzeppelin/contracts/interfaces/IERC5267.sol"; -import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; -import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol"; -import {ERC7739Signer} from "../../utils/cryptography/draft-ERC7739Signer.sol"; -import {AccountBase} from "../draft-AccountBase.sol"; - -/** - * @dev An ERC-4337 account implementation that validates domain-specific signatures following ERC-7739. - */ -abstract contract AccountERC7739 is ERC165, IERC5267, ERC7739Signer, AccountBase { - /** - * @dev Internal version of {validateUserOp} that relies on {_validateSignature}. - * - * NOTE: To override the signature functionality, try overriding {_validateSignature} instead. - */ - function _validateUserOp( - PackedUserOperation calldata userOp, - bytes32 userOpHash - ) internal view virtual override returns (uint256) { - return - _isValidSignature(userOpHash, userOp.signature) - ? ERC4337Utils.SIG_VALIDATION_SUCCESS - : ERC4337Utils.SIG_VALIDATION_FAILED; - } -} diff --git a/contracts/account/draft-AccountP256.sol b/contracts/account/extensions/draft-AccountP256.sol similarity index 87% rename from contracts/account/draft-AccountP256.sol rename to contracts/account/extensions/draft-AccountP256.sol index 25c5ab60..3e69cb06 100644 --- a/contracts/account/draft-AccountP256.sol +++ b/contracts/account/extensions/draft-AccountP256.sol @@ -8,10 +8,10 @@ import {P256} from "@openzeppelin/contracts/utils/cryptography/P256.sol"; import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; -import {AccountERC7739} from "./extensions/draft-AccountERC7739.sol"; +import {AccountBase} from "../draft-AccountBase.sol"; /** - * @dev Account implementation using {P256} signatures and {AccountERC7739} for replay protection. + * @dev Account implementation using {P256} signatures and {AccountBase} for replay protection. * * An {_initializeSigner} function is provided to set the account's signer address. Doing so it's * easier for a factory, whose likely to use initializable clones of this contract. @@ -32,7 +32,7 @@ import {AccountERC7739} from "./extensions/draft-AccountERC7739.sol"; * IMPORTANT: Avoiding to call {_initializeSigner} either during construction (if used standalone) * or during initialization (if used as a clone) may leave the account either front-runnable or unusable. */ -abstract contract AccountP256 is AccountERC7739, ERC721Holder, ERC1155Holder { +abstract contract AccountP256 is AccountBase, ERC721Holder, ERC1155Holder { using MessageHashUtils for bytes32; /** @@ -65,7 +65,7 @@ abstract contract AccountP256 is AccountERC7739, ERC721Holder, ERC1155Holder { function _validateUserOp( PackedUserOperation calldata userOp, bytes32 userOpHash - ) internal view virtual override returns (uint256) { + ) internal virtual override returns (uint256) { return super._validateUserOp(userOp, userOpHash.toEthSignedMessageHash()); } @@ -82,9 +82,4 @@ abstract contract AccountP256 is AccountERC7739, ERC721Holder, ERC1155Holder { (bytes32 qx, bytes32 qy) = signer(); return P256.verify(hash, r, s, qx, qy); } - - /// @inheritdoc ERC165 - function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, ERC1155Holder) returns (bool) { - return super.supportsInterface(interfaceId); - } } diff --git a/contracts/account/draft-AccountRSA.sol b/contracts/account/extensions/draft-AccountRSA.sol similarity index 86% rename from contracts/account/draft-AccountRSA.sol rename to contracts/account/extensions/draft-AccountRSA.sol index 3665eaec..5be51b61 100644 --- a/contracts/account/draft-AccountRSA.sol +++ b/contracts/account/extensions/draft-AccountRSA.sol @@ -8,10 +8,10 @@ import {RSA} from "@openzeppelin/contracts/utils/cryptography/RSA.sol"; import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; -import {AccountERC7739} from "./extensions/draft-AccountERC7739.sol"; +import {AccountBase} from "../draft-AccountBase.sol"; /** - * @dev Account implementation using {RSA} signatures and {AccountERC7739} for replay protection. + * @dev Account implementation using {RSA} signatures and {AccountBase} for replay protection. * * An {_initializeSigner} function is provided to set the account's signer address. Doing so it's * easier for a factory, whose likely to use initializable clones of this contract. @@ -32,7 +32,7 @@ import {AccountERC7739} from "./extensions/draft-AccountERC7739.sol"; * IMPORTANT: Avoiding to call {_initializeSigner} either during construction (if used standalone) * or during initialization (if used as a clone) may leave the account either front-runnable or unusable. */ -abstract contract AccountRSA is AccountERC7739, ERC721Holder, ERC1155Holder { +abstract contract AccountRSA is AccountBase, ERC721Holder, ERC1155Holder { using MessageHashUtils for bytes32; /** @@ -65,7 +65,7 @@ abstract contract AccountRSA is AccountERC7739, ERC721Holder, ERC1155Holder { function _validateUserOp( PackedUserOperation calldata userOp, bytes32 userOpHash - ) internal view virtual override returns (uint256) { + ) internal virtual override returns (uint256) { return super._validateUserOp(userOp, userOpHash.toEthSignedMessageHash()); } @@ -79,9 +79,4 @@ abstract contract AccountRSA is AccountERC7739, ERC721Holder, ERC1155Holder { (bytes memory e, bytes memory n) = signer(); return RSA.pkcs1Sha256(abi.encodePacked(hash), signature, e, n); } - - /// @inheritdoc ERC165 - function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, ERC1155Holder) returns (bool) { - return super.supportsInterface(interfaceId); - } } diff --git a/contracts/mocks/account/AccountBaseMock.sol b/contracts/mocks/account/AccountBaseMock.sol index 42098311..8898ef05 100644 --- a/contracts/mocks/account/AccountBaseMock.sol +++ b/contracts/mocks/account/AccountBaseMock.sol @@ -6,15 +6,12 @@ import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol"; import {AccountBase} from "../../account/draft-AccountBase.sol"; -contract AccountBaseMock is AccountBase { +abstract contract AccountBaseMock is AccountBase { /// Validates a user operation with a boolean signature. - function _validateUserOp( - PackedUserOperation calldata userOp, - bytes32 /* userOpHash */ - ) internal pure override returns (uint256 validationData) { - return - bytes1(userOp.signature[0:1]) == bytes1(0x01) - ? ERC4337Utils.SIG_VALIDATION_SUCCESS - : ERC4337Utils.SIG_VALIDATION_FAILED; + function _validateSignature( + bytes32 nestedEIP712Hash, + bytes calldata signature + ) internal pure override returns (bool) { + return bytes1(signature[0:1]) == bytes1(0x01); } } diff --git a/contracts/mocks/account/AccountECDSAMock.sol b/contracts/mocks/account/AccountECDSAMock.sol index 2e22e1e8..cb5a8f6b 100644 --- a/contracts/mocks/account/AccountECDSAMock.sol +++ b/contracts/mocks/account/AccountECDSAMock.sol @@ -2,11 +2,10 @@ pragma solidity ^0.8.20; -import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; -import {AccountECDSA} from "../../account/draft-AccountECDSA.sol"; +import {AccountECDSA} from "../../account/extensions/draft-AccountECDSA.sol"; -contract AccountECDSAMock is AccountECDSA { - constructor(string memory name, string memory version, address signerAddr) EIP712(name, version) { +abstract contract AccountECDSAMock is AccountECDSA { + constructor(address signerAddr) { _initializeSigner(signerAddr); } } diff --git a/contracts/mocks/account/AccountP256Mock.sol b/contracts/mocks/account/AccountP256Mock.sol index 0e23b9c3..55ecc427 100644 --- a/contracts/mocks/account/AccountP256Mock.sol +++ b/contracts/mocks/account/AccountP256Mock.sol @@ -2,11 +2,10 @@ pragma solidity ^0.8.20; -import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; -import {AccountP256} from "../../account/draft-AccountP256.sol"; +import {AccountP256} from "../../account/extensions/draft-AccountP256.sol"; -contract AccountP256Mock is AccountP256 { - constructor(string memory name, string memory version, bytes32 qx, bytes32 qy) EIP712(name, version) { +abstract contract AccountP256Mock is AccountP256 { + constructor(bytes32 qx, bytes32 qy) { _initializeSigner(qx, qy); } } diff --git a/contracts/mocks/account/AccountRSAMock.sol b/contracts/mocks/account/AccountRSAMock.sol index fa4b8410..79d2dd80 100644 --- a/contracts/mocks/account/AccountRSAMock.sol +++ b/contracts/mocks/account/AccountRSAMock.sol @@ -2,11 +2,10 @@ pragma solidity ^0.8.20; -import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; -import {AccountRSA} from "../../account/draft-AccountRSA.sol"; +import {AccountRSA} from "../../account/extensions/draft-AccountRSA.sol"; -contract AccountRSAMock is AccountRSA { - constructor(string memory name, string memory version, bytes memory e, bytes memory n) EIP712(name, version) { +abstract contract AccountRSAMock is AccountRSA { + constructor(bytes memory e, bytes memory n) { _initializeSigner(e, n); } } diff --git a/contracts/mocks/docs/account/MyAccountCustom.sol b/contracts/mocks/docs/account/MyAccountCustom.sol index bf4a8560..a77fe6d8 100644 --- a/contracts/mocks/docs/account/MyAccountCustom.sol +++ b/contracts/mocks/docs/account/MyAccountCustom.sol @@ -7,9 +7,8 @@ import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; import {ERC4337Utils, PackedUserOperation} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol"; import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; import {AccountBase} from "../../../account/draft-AccountBase.sol"; -import {ERC7739Signer} from "../../../utils/cryptography/draft-ERC7739Signer.sol"; -contract MyAccountCustom is ERC7739Signer, AccountBase, Initializable { +contract MyAccountCustom is AccountBase, Initializable { /** * NOTE: EIP-712 domain is set at construction because each account clone * will recalculate its domain separator based on their own address. @@ -23,17 +22,6 @@ contract MyAccountCustom is ERC7739Signer, AccountBase, Initializable { // Custom initialization logic } - /// @inheritdoc AccountBase - function _validateUserOp( - PackedUserOperation calldata userOp, - bytes32 userOpHash - ) internal view virtual override returns (uint256) { - return - _isValidSignature(userOpHash, userOp.signature) - ? ERC4337Utils.SIG_VALIDATION_SUCCESS - : ERC4337Utils.SIG_VALIDATION_FAILED; - } - /// @dev Receives a hash wrapped in an EIP-712 domain separator. function _validateSignature(bytes32 hash, bytes calldata signature) internal view virtual override returns (bool) { // Custom signing logic diff --git a/contracts/mocks/docs/account/MyAccountECDSA.sol b/contracts/mocks/docs/account/MyAccountECDSA.sol index 39bbfb7f..f531c7b4 100644 --- a/contracts/mocks/docs/account/MyAccountECDSA.sol +++ b/contracts/mocks/docs/account/MyAccountECDSA.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.20; import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; -import {AccountECDSA} from "../../../account/draft-AccountECDSA.sol"; +import {AccountECDSA} from "../../../account/extensions/draft-AccountECDSA.sol"; contract MyAccountECDSA is AccountECDSA, Initializable { /** diff --git a/contracts/mocks/docs/account/MyAccountP256.sol b/contracts/mocks/docs/account/MyAccountP256.sol index dc2e5d6e..8fa9547e 100644 --- a/contracts/mocks/docs/account/MyAccountP256.sol +++ b/contracts/mocks/docs/account/MyAccountP256.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.20; import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; -import {AccountP256} from "../../../account/draft-AccountP256.sol"; +import {AccountP256} from "../../../account/extensions/draft-AccountP256.sol"; contract MyAccountP256 is AccountP256, Initializable { /** diff --git a/contracts/mocks/docs/account/MyAccountRSA.sol b/contracts/mocks/docs/account/MyAccountRSA.sol index 0cf88109..eac5d07f 100644 --- a/contracts/mocks/docs/account/MyAccountRSA.sol +++ b/contracts/mocks/docs/account/MyAccountRSA.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.20; import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; -import {AccountRSA} from "../../../account/draft-AccountRSA.sol"; +import {AccountRSA} from "../../../account/extensions/draft-AccountRSA.sol"; contract MyAccountRSA is AccountRSA, Initializable { /** diff --git a/test/account/draft-AccountBase.test.js b/test/account/draft-AccountBase.test.js index 81bc0efd..ab2a1232 100644 --- a/test/account/draft-AccountBase.test.js +++ b/test/account/draft-AccountBase.test.js @@ -9,7 +9,7 @@ async function fixture() { const target = await ethers.deployContract('CallReceiverMockExtended'); const signer = new NonNativeSigner({ sign: () => ({ serialized: '0x01' }) }); const helper = new ERC4337Helper('$AccountBaseMock'); - const smartAccount = await helper.newAccount(); + const smartAccount = await helper.newAccount(['AccountP256', '1']); return { ...helper, mock: smartAccount, signer, target, beneficiary, other }; } From 97b33dfa858386b7e803f6e098d28c99a0a6d4fc Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 13 Dec 2024 22:05:16 +0100 Subject: [PATCH 33/51] documentation --- contracts/account/draft-AccountBase.sol | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/contracts/account/draft-AccountBase.sol b/contracts/account/draft-AccountBase.sol index c70a2c1c..048f449c 100644 --- a/contracts/account/draft-AccountBase.sol +++ b/contracts/account/draft-AccountBase.sol @@ -8,10 +8,14 @@ import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {ERC7739Signer} from "../utils/cryptography/draft-ERC7739Signer.sol"; /** - * @dev A simple ERC4337 account implementation. + * @dev A simple ERC4337 account implementation. This base implementation only includes the minimal logic to process + * user operations. * - * This base implementation only includes the minimal logic to process user operations. - * Developers must implement the {_validateUserOp} function to define the account's validation logic. + * Developers must implement the {_validateSignature} function to define the account's validation logic. + * + * IMPORTANT: Implementing a mechanism to validate signatures is a security-sensitive operation as it may allow an + * attacker to bypass the account's security measures. Check out {AccountECDSA}, {AccountP256}, or {AccountRSA} for + * digital signature validation implementations. */ abstract contract AccountBase is IAccount, IAccountExecute, ERC7739Signer { /** @@ -85,10 +89,6 @@ abstract contract AccountBase is IAccount, IAccountExecute, ERC7739Signer { * * Given the `userOpHash` calculation is defined by ERC-4337, offchain signers * may need to sign this hash by wrapping it in other schemes (e.g. ERC-191) - * - * IMPORTANT: Implementing a mechanism to validate user operations is a security-sensitive operation - * as it may allow an attacker to bypass the account's security measures. Check out {AccountECDSA}, - * {AccountP256}, or {AccountRSA} for digital signature validation implementations. */ function _validateUserOp( PackedUserOperation calldata userOp, From ddd17e924292f6564ebc28fe5696ec319d63e3a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Fri, 13 Dec 2024 16:20:41 -0600 Subject: [PATCH 34/51] Rename `_validateSignature` to `_rawSignatureValidation` and remove _validateUserOp --- contracts/account/draft-AccountBase.sol | 26 +++++++++------- .../account/extensions/draft-AccountECDSA.sol | 18 +++-------- .../account/extensions/draft-AccountP256.sol | 18 +++-------- .../account/extensions/draft-AccountRSA.sol | 18 +++-------- contracts/mocks/account/AccountBaseMock.sol | 4 +-- .../mocks/docs/account/MyAccountCustom.sol | 5 ++- .../cryptography/ERC7739SignerECDSAMock.sol | 5 ++- .../cryptography/ERC7739SignerP256Mock.sol | 5 ++- .../cryptography/ERC7739SignerRSAMock.sol | 5 ++- .../cryptography/draft-ERC7739Signer.sol | 31 ++++++------------- test/account/Account.behavior.js | 12 +++---- test/account/draft-AccountBase.test.js | 2 +- test/helpers/erc4337.js | 8 ++--- 13 files changed, 63 insertions(+), 94 deletions(-) diff --git a/contracts/account/draft-AccountBase.sol b/contracts/account/draft-AccountBase.sol index 048f449c..699a3e23 100644 --- a/contracts/account/draft-AccountBase.sol +++ b/contracts/account/draft-AccountBase.sol @@ -5,19 +5,22 @@ pragma solidity ^0.8.20; import {PackedUserOperation, IAccount, IEntryPoint, IAccountExecute} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol"; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import {ERC7739Signer} from "../utils/cryptography/draft-ERC7739Signer.sol"; /** * @dev A simple ERC4337 account implementation. This base implementation only includes the minimal logic to process * user operations. * - * Developers must implement the {_validateSignature} function to define the account's validation logic. + * Developers must implement the {_rawSignatureValidation} function to define the account's validation logic. * * IMPORTANT: Implementing a mechanism to validate signatures is a security-sensitive operation as it may allow an * attacker to bypass the account's security measures. Check out {AccountECDSA}, {AccountP256}, or {AccountRSA} for * digital signature validation implementations. */ abstract contract AccountBase is IAccount, IAccountExecute, ERC7739Signer { + using MessageHashUtils for bytes32; + /** * @dev Unauthorized call to the account. */ @@ -68,7 +71,9 @@ abstract contract AccountBase is IAccount, IAccountExecute, ERC7739Signer { bytes32 userOpHash, uint256 missingAccountFunds ) public virtual onlyEntryPoint returns (uint256) { - uint256 validationData = _validateUserOp(userOp, userOpHash); + uint256 validationData = _rawSignatureValidation(_signableUserOpHash(userOp, userOpHash), userOp.signature) + ? ERC4337Utils.SIG_VALIDATION_SUCCESS + : ERC4337Utils.SIG_VALIDATION_FAILED; _payPrefund(missingAccountFunds); return validationData; } @@ -85,19 +90,18 @@ abstract contract AccountBase is IAccount, IAccountExecute, ERC7739Signer { } /** - * @dev Validation logic for {validateUserOp}. + * @dev Returns the digest used by an offchain sigenr instead of the opaque `userOpHash`. * * Given the `userOpHash` calculation is defined by ERC-4337, offchain signers - * may need to sign this hash by wrapping it in other schemes (e.g. ERC-191) + * may need to sign again this hash by rehashing it with other schemes (e.g. ERC-191). + * + * Returns the `userOpHash` by default. */ - function _validateUserOp( - PackedUserOperation calldata userOp, + function _signableUserOpHash( + PackedUserOperation calldata /* userOp */, bytes32 userOpHash - ) internal virtual returns (uint256 validationData) { - return - _isValidSignature(userOpHash, userOp.signature) - ? ERC4337Utils.SIG_VALIDATION_SUCCESS - : ERC4337Utils.SIG_VALIDATION_FAILED; + ) internal view virtual returns (bytes32) { + return userOpHash.toEthSignedMessageHash(); } /** diff --git a/contracts/account/extensions/draft-AccountECDSA.sol b/contracts/account/extensions/draft-AccountECDSA.sol index 6dfc328a..3426546c 100644 --- a/contracts/account/extensions/draft-AccountECDSA.sol +++ b/contracts/account/extensions/draft-AccountECDSA.sol @@ -57,23 +57,13 @@ abstract contract AccountECDSA is AccountBase, ERC721Holder, ERC1155Holder { return _signer; } - /** - * @dev Returns the ERC-191 signed `userOpHash` hashed with keccak256 using `personal_sign`. - */ - function _validateUserOp( - PackedUserOperation calldata userOp, - bytes32 userOpHash - ) internal virtual override returns (uint256) { - return super._validateUserOp(userOp, userOpHash.toEthSignedMessageHash()); - } - /** * @dev Validates the signature using the account's signer. - * - * This function provides a nested EIP-712 hash. Developers must override only this - * function to ensure no raw message signing is possible. */ - function _validateSignature(bytes32 hash, bytes calldata signature) internal view virtual override returns (bool) { + function _rawSignatureValidation( + bytes32 hash, + bytes calldata signature + ) internal view virtual override returns (bool) { (address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(hash, signature); return signer() == recovered && err == ECDSA.RecoverError.NoError; } diff --git a/contracts/account/extensions/draft-AccountP256.sol b/contracts/account/extensions/draft-AccountP256.sol index 3e69cb06..11c132f8 100644 --- a/contracts/account/extensions/draft-AccountP256.sol +++ b/contracts/account/extensions/draft-AccountP256.sol @@ -59,23 +59,13 @@ abstract contract AccountP256 is AccountBase, ERC721Holder, ERC1155Holder { return (_qx, _qy); } - /** - * @dev Returns the ERC-191 signed `userOpHash` hashed with keccak256 using `personal_sign`. - */ - function _validateUserOp( - PackedUserOperation calldata userOp, - bytes32 userOpHash - ) internal virtual override returns (uint256) { - return super._validateUserOp(userOp, userOpHash.toEthSignedMessageHash()); - } - /** * @dev Validates the signature using the account's signer. - * - * This function provides a nested EIP-712 hash. Developers must override only this - * function to ensure no raw message signing is possible. */ - function _validateSignature(bytes32 hash, bytes calldata signature) internal view virtual override returns (bool) { + function _rawSignatureValidation( + bytes32 hash, + bytes calldata signature + ) internal view virtual override returns (bool) { if (signature.length < 0x40) return false; bytes32 r = bytes32(signature[0x00:0x20]); bytes32 s = bytes32(signature[0x20:0x40]); diff --git a/contracts/account/extensions/draft-AccountRSA.sol b/contracts/account/extensions/draft-AccountRSA.sol index 5be51b61..09a4f37b 100644 --- a/contracts/account/extensions/draft-AccountRSA.sol +++ b/contracts/account/extensions/draft-AccountRSA.sol @@ -59,23 +59,13 @@ abstract contract AccountRSA is AccountBase, ERC721Holder, ERC1155Holder { return (_e, _n); } - /** - * @dev Returns the ERC-191 signed `userOpHash` hashed with keccak256 using `personal_sign`. - */ - function _validateUserOp( - PackedUserOperation calldata userOp, - bytes32 userOpHash - ) internal virtual override returns (uint256) { - return super._validateUserOp(userOp, userOpHash.toEthSignedMessageHash()); - } - /** * @dev Validates the signature using the account's signer. - * - * This function provides a nested EIP-712 hash. Developers must override only this - * function to ensure no raw message signing is possible. */ - function _validateSignature(bytes32 hash, bytes calldata signature) internal view virtual override returns (bool) { + function _rawSignatureValidation( + bytes32 hash, + bytes calldata signature + ) internal view virtual override returns (bool) { (bytes memory e, bytes memory n) = signer(); return RSA.pkcs1Sha256(abi.encodePacked(hash), signature, e, n); } diff --git a/contracts/mocks/account/AccountBaseMock.sol b/contracts/mocks/account/AccountBaseMock.sol index 8898ef05..15ae6695 100644 --- a/contracts/mocks/account/AccountBaseMock.sol +++ b/contracts/mocks/account/AccountBaseMock.sol @@ -8,8 +8,8 @@ import {AccountBase} from "../../account/draft-AccountBase.sol"; abstract contract AccountBaseMock is AccountBase { /// Validates a user operation with a boolean signature. - function _validateSignature( - bytes32 nestedEIP712Hash, + function _rawSignatureValidation( + bytes32 /* userOpHash */, bytes calldata signature ) internal pure override returns (bool) { return bytes1(signature[0:1]) == bytes1(0x01); diff --git a/contracts/mocks/docs/account/MyAccountCustom.sol b/contracts/mocks/docs/account/MyAccountCustom.sol index a77fe6d8..5f3c9a50 100644 --- a/contracts/mocks/docs/account/MyAccountCustom.sol +++ b/contracts/mocks/docs/account/MyAccountCustom.sol @@ -23,7 +23,10 @@ contract MyAccountCustom is AccountBase, Initializable { } /// @dev Receives a hash wrapped in an EIP-712 domain separator. - function _validateSignature(bytes32 hash, bytes calldata signature) internal view virtual override returns (bool) { + function _rawSignatureValidation( + bytes32 hash, + bytes calldata signature + ) internal view virtual override returns (bool) { // Custom signing logic } } diff --git a/contracts/mocks/docs/utils/cryptography/ERC7739SignerECDSAMock.sol b/contracts/mocks/docs/utils/cryptography/ERC7739SignerECDSAMock.sol index 60e34dea..ea47dc2d 100644 --- a/contracts/mocks/docs/utils/cryptography/ERC7739SignerECDSAMock.sol +++ b/contracts/mocks/docs/utils/cryptography/ERC7739SignerECDSAMock.sol @@ -13,7 +13,10 @@ contract ERC7739SignerECDSAMock is ERC7739Signer { _signer = signerAddr; } - function _validateSignature(bytes32 hash, bytes calldata signature) internal view virtual override returns (bool) { + function _rawSignatureValidation( + bytes32 hash, + bytes calldata signature + ) internal view virtual override returns (bool) { (address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(hash, signature); return _signer == recovered && err == ECDSA.RecoverError.NoError; } diff --git a/contracts/mocks/docs/utils/cryptography/ERC7739SignerP256Mock.sol b/contracts/mocks/docs/utils/cryptography/ERC7739SignerP256Mock.sol index d0782416..e3983002 100644 --- a/contracts/mocks/docs/utils/cryptography/ERC7739SignerP256Mock.sol +++ b/contracts/mocks/docs/utils/cryptography/ERC7739SignerP256Mock.sol @@ -15,7 +15,10 @@ contract ERC7739SignerP256Mock is ERC7739Signer { _qy = qy; } - function _validateSignature(bytes32 hash, bytes calldata signature) internal view virtual override returns (bool) { + function _rawSignatureValidation( + bytes32 hash, + bytes calldata signature + ) internal view virtual override returns (bool) { bytes32 r = bytes32(signature[0x00:0x20]); bytes32 s = bytes32(signature[0x20:0x40]); return P256.verify(hash, r, s, _qx, _qy); diff --git a/contracts/mocks/docs/utils/cryptography/ERC7739SignerRSAMock.sol b/contracts/mocks/docs/utils/cryptography/ERC7739SignerRSAMock.sol index b2d270e6..e6a6184c 100644 --- a/contracts/mocks/docs/utils/cryptography/ERC7739SignerRSAMock.sol +++ b/contracts/mocks/docs/utils/cryptography/ERC7739SignerRSAMock.sol @@ -15,7 +15,10 @@ contract ERC7739SignerRSAMock is ERC7739Signer { _n = n; } - function _validateSignature(bytes32 hash, bytes calldata signature) internal view virtual override returns (bool) { + function _rawSignatureValidation( + bytes32 hash, + bytes calldata signature + ) internal view virtual override returns (bool) { return RSA.pkcs1Sha256(hash, signature, _e, _n); } } diff --git a/contracts/utils/cryptography/draft-ERC7739Signer.sol b/contracts/utils/cryptography/draft-ERC7739Signer.sol index 7682dbe3..c06fc103 100644 --- a/contracts/utils/cryptography/draft-ERC7739Signer.sol +++ b/contracts/utils/cryptography/draft-ERC7739Signer.sol @@ -14,7 +14,7 @@ import {ERC7739Utils} from "./draft-ERC7739Utils.sol"; * Linking the signature to the EIP-712 domain separator is a security measure to prevent signature replay across different * EIP-712 domains (e.g. a single offchain owner of multiple contracts). * - * This contract requires implementing the {_validateSignature} function, which passes the wrapped message hash, + * This contract requires implementing the {_rawSignatureValidation} function, which passes the wrapped message hash, * which may be either an typed data or a personal sign nested type. * * NOTE: {EIP712} uses {ShortStrings} to optimize gas costs for short strings (up to 31 characters). @@ -34,42 +34,36 @@ abstract contract ERC7739Signer is EIP712, IERC1271 { * - As a _personal_ signature (an EIP-712 mimic of the `eth_personalSign` for a smart contract) */ function isValidSignature(bytes32 hash, bytes calldata signature) public view virtual returns (bytes4 result) { + bool valid = _isValidNestedTypedDataSignature(hash, signature) || + _isValidNestedPersonalSignSignature(hash, signature); // For the hash `0x7739773977397739773977397739773977397739773977397739773977397739` and an empty signature, // we return the magic value too as it's assumed impossible to find a preimage for it that can be used maliciously. // Useful for simulation purposes and to validate whether the contract supports ERC-7739. return - _isValidSignature(hash, signature) + valid ? IERC1271.isValidSignature.selector : (hash == 0x7739773977397739773977397739773977397739773977397739773977397739 && signature.length == 0) ? bytes4(0x77390001) : bytes4(0xffffffff); } - /** - * @dev Internal version of {isValidSignature} that returns a boolean. - */ - function _isValidSignature(bytes32 hash, bytes calldata signature) internal view virtual returns (bool) { - return - _isValidNestedTypedDataSignature(hash, signature) || _isValidNestedPersonalSignSignature(hash, signature); - } - /** * @dev Nested personal signature verification. * - * NOTE: Instead of overriding this function, try with {_validateSignature}. It encapsulates + * NOTE: Instead of overriding this function, try with {_rawSignatureValidation}. It encapsulates * nested EIP-712 hashes. */ function _isValidNestedPersonalSignSignature( bytes32 hash, bytes calldata signature ) internal view virtual returns (bool) { - return _validateSignature(_domainSeparatorV4().toTypedDataHash(hash.personalSignStructHash()), signature); + return _rawSignatureValidation(_domainSeparatorV4().toTypedDataHash(hash.personalSignStructHash()), signature); } /** * @dev Nested EIP-712 typed data verification. * - * NOTE: Instead of overriding this function, try with {_validateSignature}. It encapsulates + * NOTE: Instead of overriding this function, try with {_rawSignatureValidation}. It encapsulates * nested EIP-712 hashes. */ function _isValidNestedTypedDataSignature( @@ -99,7 +93,7 @@ abstract contract ERC7739Signer is EIP712, IERC1271 { return hash == appSeparator.toTypedDataHash(contentsHash) && bytes(contentsDescr).length != 0 && - _validateSignature( + _rawSignatureValidation( appSeparator.toTypedDataHash( ERC7739Utils.typedDataSignStructHash( contentsDescr, @@ -114,16 +108,9 @@ abstract contract ERC7739Signer is EIP712, IERC1271 { /** * @dev Signature validation algorithm. * - * To ensure there's no way to inherit from this ERC-7739 signer and still be able to sign raw messages, - * this function provides a nested EIP-712 hash. Developers must implement only this function to ensure - * no raw message signing is possible. - * * WARNING: Implementing a signature validation algorithm is a security-sensitive operation as it involves * cryptographic verification. It is important to review and test thoroughly before deployment. Consider * using one of the signature verification libraries ({ECDSA}, {P256} or {RSA}). */ - function _validateSignature( - bytes32 nestedEIP712Hash, - bytes calldata signature - ) internal view virtual returns (bool); + function _rawSignatureValidation(bytes32 hash, bytes calldata signature) internal view virtual returns (bool); } diff --git a/test/account/Account.behavior.js b/test/account/Account.behavior.js index e076cc03..6a513b53 100644 --- a/test/account/Account.behavior.js +++ b/test/account/Account.behavior.js @@ -36,7 +36,7 @@ function shouldBehaveLikeAnAccountBase() { ), ]), }) - .then(op => op.sign(this.domain, this.signer)); + .then(op => op.sign(this.signer)); await expect(this.mock.connect(this.other).validateUserOp(operation.packed, operation.hash(), 0)) .to.be.revertedWithCustomError(this.mock, 'AccountUnauthorized') .withArgs(this.other); @@ -59,7 +59,7 @@ function shouldBehaveLikeAnAccountBase() { ), ]), }) - .then(op => op.sign(this.domain, this.signer)); + .then(op => op.sign(this.signer)); expect( await this.mock @@ -101,7 +101,7 @@ function shouldBehaveLikeAnAccountBase() { ), ]), }) - .then(op => op.sign(this.domain, this.signer)); + .then(op => op.sign(this.signer)); const prevAccountBalance = await ethers.provider.getBalance(this.mock.target); const prevEntrypointBalance = await ethers.provider.getBalance(this.entrypoint.target); @@ -221,7 +221,7 @@ function shouldBehaveLikeAnAccountBaseExecutor({ deployable = true } = {}) { ), ]), }) - .then(op => op.sign(this.domain, this.signer)); + .then(op => op.sign(this.signer)); await expect(this.mock.connect(this.other).executeUserOp(operation.packed, operation.hash())) .to.be.revertedWithCustomError(this.mock, 'AccountUnauthorized') @@ -243,7 +243,7 @@ function shouldBehaveLikeAnAccountBaseExecutor({ deployable = true } = {}) { ]), }) .then(op => op.addInitCode()) - .then(op => op.sign(this.domain, this.signer)); + .then(op => op.sign(this.signer)); await expect(this.entrypoint.connect(this.entrypointAsSigner).handleOps([operation.packed], this.beneficiary)) .to.emit(this.entrypoint, 'AccountDeployed') @@ -292,7 +292,7 @@ function shouldBehaveLikeAnAccountBaseExecutor({ deployable = true } = {}) { ), ]), }) - .then(op => op.sign(this.domain, this.signer)); + .then(op => op.sign(this.signer)); expect(await this.mock.getNonce()).to.equal(0); await expect(this.entrypoint.connect(this.entrypointAsSigner).handleOps([operation.packed], this.beneficiary)) diff --git a/test/account/draft-AccountBase.test.js b/test/account/draft-AccountBase.test.js index ab2a1232..b8785c18 100644 --- a/test/account/draft-AccountBase.test.js +++ b/test/account/draft-AccountBase.test.js @@ -9,7 +9,7 @@ async function fixture() { const target = await ethers.deployContract('CallReceiverMockExtended'); const signer = new NonNativeSigner({ sign: () => ({ serialized: '0x01' }) }); const helper = new ERC4337Helper('$AccountBaseMock'); - const smartAccount = await helper.newAccount(['AccountP256', '1']); + const smartAccount = await helper.newAccount(['AccountBase', '1']); return { ...helper, mock: smartAccount, signer, target, beneficiary, other }; } diff --git a/test/helpers/erc4337.js b/test/helpers/erc4337.js index 2865b49a..93d302b1 100644 --- a/test/helpers/erc4337.js +++ b/test/helpers/erc4337.js @@ -99,12 +99,8 @@ class UserOperation extends UserOperationVanilla { return super.hash(this.context.entrypoint.target, this.context.chainId); } - async sign(domain, signer) { - this.signature = await PersonalSignHelper.sign( - signer.signTypedData.bind(signer), - ethers.getBytes(this.hash()), - domain, - ); + async sign(signer) { + this.signature = await signer.signMessage(ethers.getBytes(this.hash())); return this; } } From e8ef6d153bf2018457226a11f38c7481f25dcdb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Fri, 13 Dec 2024 16:23:00 -0600 Subject: [PATCH 35/51] errata --- contracts/account/extensions/draft-AccountRSA.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/account/extensions/draft-AccountRSA.sol b/contracts/account/extensions/draft-AccountRSA.sol index 09a4f37b..1aa55000 100644 --- a/contracts/account/extensions/draft-AccountRSA.sol +++ b/contracts/account/extensions/draft-AccountRSA.sol @@ -38,7 +38,7 @@ abstract contract AccountRSA is AccountBase, ERC721Holder, ERC1155Holder { /** * @dev The {signer} is already initialized. */ - error AccountP256UninitializedSigner(bytes e, bytes n); + error AccountRSAUninitializedSigner(bytes e, bytes n); bytes private _e; bytes private _n; @@ -47,7 +47,7 @@ abstract contract AccountRSA is AccountBase, ERC721Holder, ERC1155Holder { * @dev Initializes the account with the RSA public key. This function can be called only once. */ function _initializeSigner(bytes memory e, bytes memory n) internal { - if (_e.length != 0 || _n.length != 0) revert AccountP256UninitializedSigner(e, n); + if (_e.length != 0 || _n.length != 0) revert AccountRSAUninitializedSigner(e, n); _e = e; _n = n; } From d706876d361ea78df427f530db948216da666d0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Fri, 13 Dec 2024 17:41:53 -0600 Subject: [PATCH 36/51] Default _signableUserOpHash to a typed userop signature --- contracts/account/draft-AccountBase.sol | 2 +- .../account/extensions/draft-AccountECDSA.sol | 34 +++++++++++++++++-- .../account/extensions/draft-AccountP256.sol | 34 +++++++++++++++++-- .../account/extensions/draft-AccountRSA.sol | 34 +++++++++++++++++-- test/account/Account.behavior.js | 13 +++---- test/account/draft-AccountBase.test.js | 6 +++- test/account/draft-AccountECDSA.test.js | 19 +++++++++++ test/account/draft-AccountP256.test.js | 20 ++++++++++- test/account/draft-AccountRSA.test.js | 20 ++++++++++- test/helpers/eip712.js | 22 ++++++++++++ test/helpers/erc4337.js | 6 ---- 11 files changed, 188 insertions(+), 22 deletions(-) create mode 100644 test/helpers/eip712.js diff --git a/contracts/account/draft-AccountBase.sol b/contracts/account/draft-AccountBase.sol index 699a3e23..fec36701 100644 --- a/contracts/account/draft-AccountBase.sol +++ b/contracts/account/draft-AccountBase.sol @@ -101,7 +101,7 @@ abstract contract AccountBase is IAccount, IAccountExecute, ERC7739Signer { PackedUserOperation calldata /* userOp */, bytes32 userOpHash ) internal view virtual returns (bytes32) { - return userOpHash.toEthSignedMessageHash(); + return userOpHash; } /** diff --git a/contracts/account/extensions/draft-AccountECDSA.sol b/contracts/account/extensions/draft-AccountECDSA.sol index 3426546c..1ad7e8d6 100644 --- a/contracts/account/extensions/draft-AccountECDSA.sol +++ b/contracts/account/extensions/draft-AccountECDSA.sol @@ -5,7 +5,6 @@ pragma solidity ^0.8.20; import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; import {AccountBase} from "../draft-AccountBase.sol"; @@ -33,7 +32,10 @@ import {AccountBase} from "../draft-AccountBase.sol"; * or during initialization (if used as a clone) may leave the account either front-runnable or unusable. */ abstract contract AccountECDSA is AccountBase, ERC721Holder, ERC1155Holder { - using MessageHashUtils for bytes32; + bytes32 internal constant _PACKED_USER_OPERATION = + keccak256( + "PackedUserOperation(address sender,uint256 nonce,bytes initCode,bytes callData,bytes32 accountGasLimits,uint256 preVerificationGas,bytes32 gasFees,bytes paymasterAndData,address entrypoint)" + ); /** * @dev The {signer} is already initialized. @@ -57,6 +59,34 @@ abstract contract AccountECDSA is AccountBase, ERC721Holder, ERC1155Holder { return _signer; } + /** + * @dev Customize the user operation hash to sign. See {AccountBase-_signableUserOpHash}. + * + * This implementation uses the EIP-712 typed data hashing mechanism for readability. + */ + function _signableUserOpHash( + PackedUserOperation calldata userOp, + bytes32 /* userOpHash */ + ) internal view virtual override returns (bytes32) { + return + _hashTypedDataV4( + keccak256( + abi.encode( + _PACKED_USER_OPERATION, + userOp.sender, + userOp.nonce, + keccak256(userOp.initCode), + keccak256(userOp.callData), + userOp.accountGasLimits, + userOp.preVerificationGas, + userOp.gasFees, + keccak256(userOp.paymasterAndData), + msg.sender + ) + ) + ); + } + /** * @dev Validates the signature using the account's signer. */ diff --git a/contracts/account/extensions/draft-AccountP256.sol b/contracts/account/extensions/draft-AccountP256.sol index 11c132f8..5ef8307a 100644 --- a/contracts/account/extensions/draft-AccountP256.sol +++ b/contracts/account/extensions/draft-AccountP256.sol @@ -5,7 +5,6 @@ pragma solidity ^0.8.20; import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; import {P256} from "@openzeppelin/contracts/utils/cryptography/P256.sol"; -import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; import {AccountBase} from "../draft-AccountBase.sol"; @@ -33,7 +32,10 @@ import {AccountBase} from "../draft-AccountBase.sol"; * or during initialization (if used as a clone) may leave the account either front-runnable or unusable. */ abstract contract AccountP256 is AccountBase, ERC721Holder, ERC1155Holder { - using MessageHashUtils for bytes32; + bytes32 internal constant _PACKED_USER_OPERATION = + keccak256( + "PackedUserOperation(address sender,uint256 nonce,bytes initCode,bytes callData,bytes32 accountGasLimits,uint256 preVerificationGas,bytes32 gasFees,bytes paymasterAndData,address entrypoint)" + ); /** * @dev The {signer} is already initialized. @@ -59,6 +61,34 @@ abstract contract AccountP256 is AccountBase, ERC721Holder, ERC1155Holder { return (_qx, _qy); } + /** + * @dev Customize the user operation hash to sign. See {AccountBase-_signableUserOpHash}. + * + * This implementation uses the EIP-712 typed data hashing mechanism for readability. + */ + function _signableUserOpHash( + PackedUserOperation calldata userOp, + bytes32 /* userOpHash */ + ) internal view virtual override returns (bytes32) { + return + _hashTypedDataV4( + keccak256( + abi.encode( + _PACKED_USER_OPERATION, + userOp.sender, + userOp.nonce, + keccak256(userOp.initCode), + keccak256(userOp.callData), + userOp.accountGasLimits, + userOp.preVerificationGas, + userOp.gasFees, + keccak256(userOp.paymasterAndData), + msg.sender + ) + ) + ); + } + /** * @dev Validates the signature using the account's signer. */ diff --git a/contracts/account/extensions/draft-AccountRSA.sol b/contracts/account/extensions/draft-AccountRSA.sol index 1aa55000..be5a373d 100644 --- a/contracts/account/extensions/draft-AccountRSA.sol +++ b/contracts/account/extensions/draft-AccountRSA.sol @@ -5,7 +5,6 @@ pragma solidity ^0.8.20; import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; import {RSA} from "@openzeppelin/contracts/utils/cryptography/RSA.sol"; -import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; import {AccountBase} from "../draft-AccountBase.sol"; @@ -33,7 +32,10 @@ import {AccountBase} from "../draft-AccountBase.sol"; * or during initialization (if used as a clone) may leave the account either front-runnable or unusable. */ abstract contract AccountRSA is AccountBase, ERC721Holder, ERC1155Holder { - using MessageHashUtils for bytes32; + bytes32 internal constant _PACKED_USER_OPERATION = + keccak256( + "PackedUserOperation(address sender,uint256 nonce,bytes initCode,bytes callData,bytes32 accountGasLimits,uint256 preVerificationGas,bytes32 gasFees,bytes paymasterAndData,address entrypoint)" + ); /** * @dev The {signer} is already initialized. @@ -59,6 +61,34 @@ abstract contract AccountRSA is AccountBase, ERC721Holder, ERC1155Holder { return (_e, _n); } + /** + * @dev Customize the user operation hash to sign. See {AccountBase-_signableUserOpHash}. + * + * This implementation uses the EIP-712 typed data hashing mechanism for readability. + */ + function _signableUserOpHash( + PackedUserOperation calldata userOp, + bytes32 /* userOpHash */ + ) internal view virtual override returns (bytes32) { + return + _hashTypedDataV4( + keccak256( + abi.encode( + _PACKED_USER_OPERATION, + userOp.sender, + userOp.nonce, + keccak256(userOp.initCode), + keccak256(userOp.callData), + userOp.accountGasLimits, + userOp.preVerificationGas, + userOp.gasFees, + keccak256(userOp.paymasterAndData), + msg.sender + ) + ) + ); + } + /** * @dev Validates the signature using the account's signer. */ diff --git a/test/account/Account.behavior.js b/test/account/Account.behavior.js index 6a513b53..8cbad70a 100644 --- a/test/account/Account.behavior.js +++ b/test/account/Account.behavior.js @@ -36,7 +36,8 @@ function shouldBehaveLikeAnAccountBase() { ), ]), }) - .then(op => op.sign(this.signer)); + .then(op => this.signUserOp(op)); + await expect(this.mock.connect(this.other).validateUserOp(operation.packed, operation.hash(), 0)) .to.be.revertedWithCustomError(this.mock, 'AccountUnauthorized') .withArgs(this.other); @@ -59,7 +60,7 @@ function shouldBehaveLikeAnAccountBase() { ), ]), }) - .then(op => op.sign(this.signer)); + .then(op => this.signUserOp(op)); expect( await this.mock @@ -101,7 +102,7 @@ function shouldBehaveLikeAnAccountBase() { ), ]), }) - .then(op => op.sign(this.signer)); + .then(op => this.signUserOp(op)); const prevAccountBalance = await ethers.provider.getBalance(this.mock.target); const prevEntrypointBalance = await ethers.provider.getBalance(this.entrypoint.target); @@ -221,7 +222,7 @@ function shouldBehaveLikeAnAccountBaseExecutor({ deployable = true } = {}) { ), ]), }) - .then(op => op.sign(this.signer)); + .then(op => this.signUserOp(op)); await expect(this.mock.connect(this.other).executeUserOp(operation.packed, operation.hash())) .to.be.revertedWithCustomError(this.mock, 'AccountUnauthorized') @@ -243,7 +244,7 @@ function shouldBehaveLikeAnAccountBaseExecutor({ deployable = true } = {}) { ]), }) .then(op => op.addInitCode()) - .then(op => op.sign(this.signer)); + .then(op => this.signUserOp(op)); await expect(this.entrypoint.connect(this.entrypointAsSigner).handleOps([operation.packed], this.beneficiary)) .to.emit(this.entrypoint, 'AccountDeployed') @@ -292,7 +293,7 @@ function shouldBehaveLikeAnAccountBaseExecutor({ deployable = true } = {}) { ), ]), }) - .then(op => op.sign(this.signer)); + .then(op => this.signUserOp(op)); expect(await this.mock.getNonce()).to.equal(0); await expect(this.entrypoint.connect(this.entrypointAsSigner).handleOps([operation.packed], this.beneficiary)) diff --git a/test/account/draft-AccountBase.test.js b/test/account/draft-AccountBase.test.js index b8785c18..03f748da 100644 --- a/test/account/draft-AccountBase.test.js +++ b/test/account/draft-AccountBase.test.js @@ -10,8 +10,12 @@ async function fixture() { const signer = new NonNativeSigner({ sign: () => ({ serialized: '0x01' }) }); const helper = new ERC4337Helper('$AccountBaseMock'); const smartAccount = await helper.newAccount(['AccountBase', '1']); + const signUserOp = async userOp => { + userOp.signature = await signer.signMessage(userOp.hash()); + return userOp; + }; - return { ...helper, mock: smartAccount, signer, target, beneficiary, other }; + return { ...helper, mock: smartAccount, signer, target, beneficiary, other, signUserOp }; } describe('AccountBase', function () { diff --git a/test/account/draft-AccountECDSA.test.js b/test/account/draft-AccountECDSA.test.js index 2192044e..2e466c2b 100644 --- a/test/account/draft-AccountECDSA.test.js +++ b/test/account/draft-AccountECDSA.test.js @@ -7,6 +7,7 @@ const { const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { ERC4337Helper } = require('../helpers/erc4337'); const { shouldBehaveLikeERC7739Signer } = require('../utils/cryptography/ERC7739Signer.behavior'); +const { PackedUserOperation } = require('../helpers/eip712'); async function fixture() { const [beneficiary, other] = await ethers.getSigners(); @@ -20,6 +21,23 @@ async function fixture() { chainId: helper.chainId, verifyingContract: smartAccount.address, }; + const signUserOp = async userOp => { + const types = { PackedUserOperation }; + const packed = userOp.packed; + const typedOp = { + sender: packed.sender, + nonce: packed.nonce, + initCode: packed.initCode, + callData: packed.callData, + accountGasLimits: packed.accountGasLimits, + preVerificationGas: packed.preVerificationGas, + gasFees: packed.gasFees, + paymasterAndData: packed.paymasterAndData, + entrypoint: userOp.context.entrypoint.target, + }; + userOp.signature = await signer.signTypedData(domain, types, typedOp); + return userOp; + }; return { ...helper, @@ -29,6 +47,7 @@ async function fixture() { target, beneficiary, other, + signUserOp, }; } diff --git a/test/account/draft-AccountP256.test.js b/test/account/draft-AccountP256.test.js index 3fd6e0a1..eecd1c8d 100644 --- a/test/account/draft-AccountP256.test.js +++ b/test/account/draft-AccountP256.test.js @@ -8,6 +8,7 @@ const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { ERC4337Helper } = require('../helpers/erc4337'); const { NonNativeSigner, P256SigningKey } = require('../helpers/signers'); const { shouldBehaveLikeERC7739Signer } = require('../utils/cryptography/ERC7739Signer.behavior'); +const { PackedUserOperation } = require('../helpers/eip712'); async function fixture() { const [beneficiary, other] = await ethers.getSigners(); @@ -26,8 +27,25 @@ async function fixture() { chainId: helper.chainId, verifyingContract: smartAccount.address, }; + const signUserOp = async userOp => { + const types = { PackedUserOperation }; + const packed = userOp.packed; + const typedOp = { + sender: packed.sender, + nonce: packed.nonce, + initCode: packed.initCode, + callData: packed.callData, + accountGasLimits: packed.accountGasLimits, + preVerificationGas: packed.preVerificationGas, + gasFees: packed.gasFees, + paymasterAndData: packed.paymasterAndData, + entrypoint: userOp.context.entrypoint.target, + }; + userOp.signature = await signer.signTypedData(domain, types, typedOp); + return userOp; + }; - return { ...helper, domain, mock: smartAccount, signer, target, beneficiary, other }; + return { ...helper, domain, mock: smartAccount, signer, target, beneficiary, other, signUserOp }; } describe('AccountP256', function () { diff --git a/test/account/draft-AccountRSA.test.js b/test/account/draft-AccountRSA.test.js index dba47506..df8b77db 100644 --- a/test/account/draft-AccountRSA.test.js +++ b/test/account/draft-AccountRSA.test.js @@ -8,6 +8,7 @@ const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { ERC4337Helper } = require('../helpers/erc4337'); const { NonNativeSigner, RSASHA256SigningKey } = require('../helpers/signers'); const { shouldBehaveLikeERC7739Signer } = require('../utils/cryptography/ERC7739Signer.behavior'); +const { PackedUserOperation } = require('../helpers/eip712'); async function fixture() { const [beneficiary, other] = await ethers.getSigners(); @@ -26,8 +27,25 @@ async function fixture() { chainId: helper.chainId, verifyingContract: smartAccount.address, }; + const signUserOp = async userOp => { + const types = { PackedUserOperation }; + const packed = userOp.packed; + const typedOp = { + sender: packed.sender, + nonce: packed.nonce, + initCode: packed.initCode, + callData: packed.callData, + accountGasLimits: packed.accountGasLimits, + preVerificationGas: packed.preVerificationGas, + gasFees: packed.gasFees, + paymasterAndData: packed.paymasterAndData, + entrypoint: userOp.context.entrypoint.target, + }; + userOp.signature = await signer.signTypedData(domain, types, typedOp); + return userOp; + }; - return { ...helper, domain, mock: smartAccount, signer, target, beneficiary, other }; + return { ...helper, domain, mock: smartAccount, signer, target, beneficiary, other, signUserOp }; } describe('AccountRSA', function () { diff --git a/test/helpers/eip712.js b/test/helpers/eip712.js new file mode 100644 index 00000000..30b77f08 --- /dev/null +++ b/test/helpers/eip712.js @@ -0,0 +1,22 @@ +const { types, formatType } = require('../../lib/@openzeppelin-contracts/test/helpers/eip712'); +const { mapValues } = require('../../lib/@openzeppelin-contracts/test/helpers/iterate'); + +module.exports = { + ...types, + ...mapValues( + { + PackedUserOperation: { + sender: 'address', + nonce: 'uint256', + initCode: 'bytes', + callData: 'bytes', + accountGasLimits: 'bytes32', + preVerificationGas: 'uint256', + gasFees: 'bytes32', + paymasterAndData: 'bytes', + entrypoint: 'address', + }, + }, + formatType, + ), +}; diff --git a/test/helpers/erc4337.js b/test/helpers/erc4337.js index 93d302b1..1505a300 100644 --- a/test/helpers/erc4337.js +++ b/test/helpers/erc4337.js @@ -1,7 +1,6 @@ const { setCode } = require('@nomicfoundation/hardhat-network-helpers'); const { ethers } = require('hardhat'); const { UserOperation: UserOperationVanilla } = require('../../lib/@openzeppelin-contracts/test/helpers/erc4337'); -const { PersonalSignHelper } = require('./erc7739'); const CANONICAL_ENTRYPOINT = '0x0000000071727De22E5E9d8BAf0edAc6f37da032'; @@ -98,11 +97,6 @@ class UserOperation extends UserOperationVanilla { hash() { return super.hash(this.context.entrypoint.target, this.context.chainId); } - - async sign(signer) { - this.signature = await signer.signMessage(ethers.getBytes(this.hash())); - return this; - } } module.exports = { From 188e71d7696360cf9356089e4e22f1fe51b5ecae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Fri, 13 Dec 2024 17:45:58 -0600 Subject: [PATCH 37/51] Remove docs mocks --- .../mocks/docs/account/MyAccountCustom.sol | 32 --------------- .../mocks/docs/account/MyAccountECDSA.sol | 22 ----------- .../mocks/docs/account/MyAccountP256.sol | 22 ----------- contracts/mocks/docs/account/MyAccountRSA.sol | 22 ----------- .../docs/account/MyFactoryAccountECDSA.sol | 39 ------------------- 5 files changed, 137 deletions(-) delete mode 100644 contracts/mocks/docs/account/MyAccountCustom.sol delete mode 100644 contracts/mocks/docs/account/MyAccountECDSA.sol delete mode 100644 contracts/mocks/docs/account/MyAccountP256.sol delete mode 100644 contracts/mocks/docs/account/MyAccountRSA.sol delete mode 100644 contracts/mocks/docs/account/MyFactoryAccountECDSA.sol diff --git a/contracts/mocks/docs/account/MyAccountCustom.sol b/contracts/mocks/docs/account/MyAccountCustom.sol deleted file mode 100644 index 5f3c9a50..00000000 --- a/contracts/mocks/docs/account/MyAccountCustom.sol +++ /dev/null @@ -1,32 +0,0 @@ -// contracts/MyAccountCustom.sol -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.20; - -import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; -import {ERC4337Utils, PackedUserOperation} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol"; -import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; -import {AccountBase} from "../../../account/draft-AccountBase.sol"; - -contract MyAccountCustom is AccountBase, Initializable { - /** - * NOTE: EIP-712 domain is set at construction because each account clone - * will recalculate its domain separator based on their own address. - */ - constructor() EIP712("MyAccountCustom", "1") { - _disableInitializers(); - } - - /// @dev Set up the account (e.g. load public keys to storage). - function initialize() public virtual initializer { - // Custom initialization logic - } - - /// @dev Receives a hash wrapped in an EIP-712 domain separator. - function _rawSignatureValidation( - bytes32 hash, - bytes calldata signature - ) internal view virtual override returns (bool) { - // Custom signing logic - } -} diff --git a/contracts/mocks/docs/account/MyAccountECDSA.sol b/contracts/mocks/docs/account/MyAccountECDSA.sol deleted file mode 100644 index f531c7b4..00000000 --- a/contracts/mocks/docs/account/MyAccountECDSA.sol +++ /dev/null @@ -1,22 +0,0 @@ -// contracts/MyAccountECDSA.sol -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.20; - -import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; -import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; -import {AccountECDSA} from "../../../account/extensions/draft-AccountECDSA.sol"; - -contract MyAccountECDSA is AccountECDSA, Initializable { - /** - * NOTE: EIP-712 domain is set at construction because each account clone - * will recalculate its domain separator based on their own address. - */ - constructor() EIP712("MyAccountECDSA", "1") { - _disableInitializers(); - } - - function initializeSigner(address signerAddr) public virtual initializer { - _initializeSigner(signerAddr); - } -} diff --git a/contracts/mocks/docs/account/MyAccountP256.sol b/contracts/mocks/docs/account/MyAccountP256.sol deleted file mode 100644 index 8fa9547e..00000000 --- a/contracts/mocks/docs/account/MyAccountP256.sol +++ /dev/null @@ -1,22 +0,0 @@ -// contracts/MyAccountP256.sol -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.20; - -import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; -import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; -import {AccountP256} from "../../../account/extensions/draft-AccountP256.sol"; - -contract MyAccountP256 is AccountP256, Initializable { - /** - * NOTE: EIP-712 domain is set at construction because each account clone - * will recalculate its domain separator based on their own address. - */ - constructor() EIP712("MyAccountP256", "1") { - _disableInitializers(); - } - - function initializeSigner(bytes32 qx, bytes32 qy) public virtual initializer { - _initializeSigner(qx, qy); - } -} diff --git a/contracts/mocks/docs/account/MyAccountRSA.sol b/contracts/mocks/docs/account/MyAccountRSA.sol deleted file mode 100644 index eac5d07f..00000000 --- a/contracts/mocks/docs/account/MyAccountRSA.sol +++ /dev/null @@ -1,22 +0,0 @@ -// contracts/MyAccountRSA.sol -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.20; - -import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; -import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; -import {AccountRSA} from "../../../account/extensions/draft-AccountRSA.sol"; - -contract MyAccountRSA is AccountRSA, Initializable { - /** - * NOTE: EIP-712 domain is set at construction because each account clone - * will recalculate its domain separator based on their own address. - */ - constructor() EIP712("MyAccountRSA", "1") { - _disableInitializers(); - } - - function initializeSigner(bytes memory e, bytes memory n) public virtual initializer { - _initializeSigner(e, n); - } -} diff --git a/contracts/mocks/docs/account/MyFactoryAccountECDSA.sol b/contracts/mocks/docs/account/MyFactoryAccountECDSA.sol deleted file mode 100644 index 20c095a6..00000000 --- a/contracts/mocks/docs/account/MyFactoryAccountECDSA.sol +++ /dev/null @@ -1,39 +0,0 @@ -// contracts/MyFactoryAccountECDSA.sol -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.20; - -import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; -import {MyAccountECDSA} from "./MyAccountECDSA.sol"; - -/** - * @dev An abstract factory contract to create ECDSA accounts on demand. - */ -contract MyFactoryAccountECDSA { - using Clones for address; - - address private immutable _impl = address(new MyAccountECDSA()); - - /// @dev Predict the address of the account - function predictAddress(bytes32 salt) public view returns (address) { - return _impl.predictDeterministicAddress(salt, address(this)); - } - - /// @dev Create clone accounts on demand - function cloneAndInitialize(bytes32 salt, address signer) public returns (address) { - return _cloneAndInitialize(salt, signer); - } - - /// @dev Create clone accounts on demand and return the address. Uses `signer` to initialize the clone. - function _cloneAndInitialize(bytes32 salt, address signer) internal returns (address) { - // Scope salt to the signer to avoid front-running the salt with a different signer - bytes32 _signerSalt = keccak256(abi.encodePacked(salt, signer)); - - address predicted = predictAddress(_signerSalt); - if (predicted.code.length == 0) { - _impl.cloneDeterministic(_signerSalt); - MyAccountECDSA(payable(predicted)).initializeSigner(signer); - } - return predicted; - } -} From 6ace1a794899a3af518750aca5a34899db493f10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Fri, 13 Dec 2024 18:04:12 -0600 Subject: [PATCH 38/51] Remove ERC7739 from AccountBase --- contracts/account/draft-AccountBase.sol | 8 ++++++-- contracts/account/extensions/draft-AccountECDSA.sol | 11 +++++++++-- contracts/account/extensions/draft-AccountP256.sol | 11 +++++++++-- contracts/account/extensions/draft-AccountRSA.sol | 11 +++++++++-- test/account/draft-AccountBase.test.js | 2 +- 5 files changed, 34 insertions(+), 9 deletions(-) diff --git a/contracts/account/draft-AccountBase.sol b/contracts/account/draft-AccountBase.sol index fec36701..c53594d3 100644 --- a/contracts/account/draft-AccountBase.sol +++ b/contracts/account/draft-AccountBase.sol @@ -6,7 +6,6 @@ import {PackedUserOperation, IAccount, IEntryPoint, IAccountExecute} from "@open import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol"; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; -import {ERC7739Signer} from "../utils/cryptography/draft-ERC7739Signer.sol"; /** * @dev A simple ERC4337 account implementation. This base implementation only includes the minimal logic to process @@ -18,7 +17,7 @@ import {ERC7739Signer} from "../utils/cryptography/draft-ERC7739Signer.sol"; * attacker to bypass the account's security measures. Check out {AccountECDSA}, {AccountP256}, or {AccountRSA} for * digital signature validation implementations. */ -abstract contract AccountBase is IAccount, IAccountExecute, ERC7739Signer { +abstract contract AccountBase is IAccount, IAccountExecute { using MessageHashUtils for bytes32; /** @@ -135,6 +134,11 @@ abstract contract AccountBase is IAccount, IAccountExecute, ERC7739Signer { } } + /** + * @dev Signature validation algorithm. + */ + function _rawSignatureValidation(bytes32 hash, bytes calldata signature) internal view virtual returns (bool); + /** * @dev Receive Ether. */ diff --git a/contracts/account/extensions/draft-AccountECDSA.sol b/contracts/account/extensions/draft-AccountECDSA.sol index 1ad7e8d6..dfd5a6a3 100644 --- a/contracts/account/extensions/draft-AccountECDSA.sol +++ b/contracts/account/extensions/draft-AccountECDSA.sol @@ -2,12 +2,14 @@ pragma solidity ^0.8.20; +import {IERC5267} from "@openzeppelin/contracts/interfaces/IERC5267.sol"; import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; import {AccountBase} from "../draft-AccountBase.sol"; +import {ERC7739Signer} from "../../utils/cryptography/draft-ERC7739Signer.sol"; /** * @dev Account implementation using {ECDSA} signatures and {AccountBase} for replay protection. @@ -31,7 +33,7 @@ import {AccountBase} from "../draft-AccountBase.sol"; * IMPORTANT: Avoiding to call {_initializeSigner} either during construction (if used standalone) * or during initialization (if used as a clone) may leave the account either front-runnable or unusable. */ -abstract contract AccountECDSA is AccountBase, ERC721Holder, ERC1155Holder { +abstract contract AccountECDSA is ERC165, IERC5267, ERC7739Signer, AccountBase, ERC721Holder, ERC1155Holder { bytes32 internal constant _PACKED_USER_OPERATION = keccak256( "PackedUserOperation(address sender,uint256 nonce,bytes initCode,bytes callData,bytes32 accountGasLimits,uint256 preVerificationGas,bytes32 gasFees,bytes paymasterAndData,address entrypoint)" @@ -93,8 +95,13 @@ abstract contract AccountECDSA is AccountBase, ERC721Holder, ERC1155Holder { function _rawSignatureValidation( bytes32 hash, bytes calldata signature - ) internal view virtual override returns (bool) { + ) internal view virtual override(AccountBase, ERC7739Signer) returns (bool) { (address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(hash, signature); return signer() == recovered && err == ECDSA.RecoverError.NoError; } + + // @inheritdoc ERC165 + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, ERC1155Holder) returns (bool) { + return super.supportsInterface(interfaceId); + } } diff --git a/contracts/account/extensions/draft-AccountP256.sol b/contracts/account/extensions/draft-AccountP256.sol index 5ef8307a..bf710ccf 100644 --- a/contracts/account/extensions/draft-AccountP256.sol +++ b/contracts/account/extensions/draft-AccountP256.sol @@ -2,12 +2,14 @@ pragma solidity ^0.8.20; +import {IERC5267} from "@openzeppelin/contracts/interfaces/IERC5267.sol"; import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; import {P256} from "@openzeppelin/contracts/utils/cryptography/P256.sol"; import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; import {AccountBase} from "../draft-AccountBase.sol"; +import {ERC7739Signer} from "../../utils/cryptography/draft-ERC7739Signer.sol"; /** * @dev Account implementation using {P256} signatures and {AccountBase} for replay protection. @@ -31,7 +33,7 @@ import {AccountBase} from "../draft-AccountBase.sol"; * IMPORTANT: Avoiding to call {_initializeSigner} either during construction (if used standalone) * or during initialization (if used as a clone) may leave the account either front-runnable or unusable. */ -abstract contract AccountP256 is AccountBase, ERC721Holder, ERC1155Holder { +abstract contract AccountP256 is ERC165, IERC5267, ERC7739Signer, AccountBase, ERC721Holder, ERC1155Holder { bytes32 internal constant _PACKED_USER_OPERATION = keccak256( "PackedUserOperation(address sender,uint256 nonce,bytes initCode,bytes callData,bytes32 accountGasLimits,uint256 preVerificationGas,bytes32 gasFees,bytes paymasterAndData,address entrypoint)" @@ -95,11 +97,16 @@ abstract contract AccountP256 is AccountBase, ERC721Holder, ERC1155Holder { function _rawSignatureValidation( bytes32 hash, bytes calldata signature - ) internal view virtual override returns (bool) { + ) internal view virtual override(AccountBase, ERC7739Signer) returns (bool) { if (signature.length < 0x40) return false; bytes32 r = bytes32(signature[0x00:0x20]); bytes32 s = bytes32(signature[0x20:0x40]); (bytes32 qx, bytes32 qy) = signer(); return P256.verify(hash, r, s, qx, qy); } + + // @inheritdoc ERC165 + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, ERC1155Holder) returns (bool) { + return super.supportsInterface(interfaceId); + } } diff --git a/contracts/account/extensions/draft-AccountRSA.sol b/contracts/account/extensions/draft-AccountRSA.sol index be5a373d..850b6729 100644 --- a/contracts/account/extensions/draft-AccountRSA.sol +++ b/contracts/account/extensions/draft-AccountRSA.sol @@ -2,12 +2,14 @@ pragma solidity ^0.8.20; +import {IERC5267} from "@openzeppelin/contracts/interfaces/IERC5267.sol"; import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; import {RSA} from "@openzeppelin/contracts/utils/cryptography/RSA.sol"; import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; import {AccountBase} from "../draft-AccountBase.sol"; +import {ERC7739Signer} from "../../utils/cryptography/draft-ERC7739Signer.sol"; /** * @dev Account implementation using {RSA} signatures and {AccountBase} for replay protection. @@ -31,7 +33,7 @@ import {AccountBase} from "../draft-AccountBase.sol"; * IMPORTANT: Avoiding to call {_initializeSigner} either during construction (if used standalone) * or during initialization (if used as a clone) may leave the account either front-runnable or unusable. */ -abstract contract AccountRSA is AccountBase, ERC721Holder, ERC1155Holder { +abstract contract AccountRSA is ERC165, IERC5267, ERC7739Signer, AccountBase, ERC721Holder, ERC1155Holder { bytes32 internal constant _PACKED_USER_OPERATION = keccak256( "PackedUserOperation(address sender,uint256 nonce,bytes initCode,bytes callData,bytes32 accountGasLimits,uint256 preVerificationGas,bytes32 gasFees,bytes paymasterAndData,address entrypoint)" @@ -95,8 +97,13 @@ abstract contract AccountRSA is AccountBase, ERC721Holder, ERC1155Holder { function _rawSignatureValidation( bytes32 hash, bytes calldata signature - ) internal view virtual override returns (bool) { + ) internal view virtual override(AccountBase, ERC7739Signer) returns (bool) { (bytes memory e, bytes memory n) = signer(); return RSA.pkcs1Sha256(abi.encodePacked(hash), signature, e, n); } + + // @inheritdoc ERC165 + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, ERC1155Holder) returns (bool) { + return super.supportsInterface(interfaceId); + } } diff --git a/test/account/draft-AccountBase.test.js b/test/account/draft-AccountBase.test.js index 03f748da..7ce99458 100644 --- a/test/account/draft-AccountBase.test.js +++ b/test/account/draft-AccountBase.test.js @@ -9,7 +9,7 @@ async function fixture() { const target = await ethers.deployContract('CallReceiverMockExtended'); const signer = new NonNativeSigner({ sign: () => ({ serialized: '0x01' }) }); const helper = new ERC4337Helper('$AccountBaseMock'); - const smartAccount = await helper.newAccount(['AccountBase', '1']); + const smartAccount = await helper.newAccount(); const signUserOp = async userOp => { userOp.signature = await signer.signMessage(userOp.hash()); return userOp; From 9793e498d9a02885ac8e4bb1038479160a0804ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Fri, 13 Dec 2024 18:05:28 -0600 Subject: [PATCH 39/51] Make ERC7739Signer validations private --- .../utils/cryptography/draft-ERC7739Signer.sol | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/contracts/utils/cryptography/draft-ERC7739Signer.sol b/contracts/utils/cryptography/draft-ERC7739Signer.sol index c06fc103..3eca321a 100644 --- a/contracts/utils/cryptography/draft-ERC7739Signer.sol +++ b/contracts/utils/cryptography/draft-ERC7739Signer.sol @@ -49,27 +49,18 @@ abstract contract ERC7739Signer is EIP712, IERC1271 { /** * @dev Nested personal signature verification. - * - * NOTE: Instead of overriding this function, try with {_rawSignatureValidation}. It encapsulates - * nested EIP-712 hashes. */ - function _isValidNestedPersonalSignSignature( - bytes32 hash, - bytes calldata signature - ) internal view virtual returns (bool) { + function _isValidNestedPersonalSignSignature(bytes32 hash, bytes calldata signature) private view returns (bool) { return _rawSignatureValidation(_domainSeparatorV4().toTypedDataHash(hash.personalSignStructHash()), signature); } /** * @dev Nested EIP-712 typed data verification. - * - * NOTE: Instead of overriding this function, try with {_rawSignatureValidation}. It encapsulates - * nested EIP-712 hashes. */ function _isValidNestedTypedDataSignature( bytes32 hash, bytes calldata encodedSignature - ) internal view virtual returns (bool) { + ) private view returns (bool) { // decode signature ( bytes calldata signature, From 9e34432cd9324154c9f9f462ac0ea7f47b2a4c8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Sat, 14 Dec 2024 14:05:39 -0600 Subject: [PATCH 40/51] Move EIP712 userop signing to Accountbase --- contracts/account/draft-AccountBase.sol | 32 +++++++++++++--- .../account/extensions/draft-AccountECDSA.sol | 37 +------------------ .../account/extensions/draft-AccountP256.sol | 37 +------------------ .../account/extensions/draft-AccountRSA.sol | 37 +------------------ test/account/draft-AccountBase.test.js | 2 +- 5 files changed, 34 insertions(+), 111 deletions(-) diff --git a/contracts/account/draft-AccountBase.sol b/contracts/account/draft-AccountBase.sol index c53594d3..73bb573d 100644 --- a/contracts/account/draft-AccountBase.sol +++ b/contracts/account/draft-AccountBase.sol @@ -6,6 +6,7 @@ import {PackedUserOperation, IAccount, IEntryPoint, IAccountExecute} from "@open import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol"; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; +import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; /** * @dev A simple ERC4337 account implementation. This base implementation only includes the minimal logic to process @@ -17,9 +18,14 @@ import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/Messa * attacker to bypass the account's security measures. Check out {AccountECDSA}, {AccountP256}, or {AccountRSA} for * digital signature validation implementations. */ -abstract contract AccountBase is IAccount, IAccountExecute { +abstract contract AccountBase is EIP712, IAccount, IAccountExecute { using MessageHashUtils for bytes32; + bytes32 internal constant _PACKED_USER_OPERATION = + keccak256( + "PackedUserOperation(address sender,uint256 nonce,bytes initCode,bytes callData,bytes32 accountGasLimits,uint256 preVerificationGas,bytes32 gasFees,bytes paymasterAndData,address entrypoint)" + ); + /** * @dev Unauthorized call to the account. */ @@ -94,13 +100,29 @@ abstract contract AccountBase is IAccount, IAccountExecute { * Given the `userOpHash` calculation is defined by ERC-4337, offchain signers * may need to sign again this hash by rehashing it with other schemes (e.g. ERC-191). * - * Returns the `userOpHash` by default. + * Returns a typehash following EIP-712 typed data hashing for readability. */ function _signableUserOpHash( - PackedUserOperation calldata /* userOp */, - bytes32 userOpHash + PackedUserOperation calldata userOp, + bytes32 /* userOpHash */ ) internal view virtual returns (bytes32) { - return userOpHash; + return + _hashTypedDataV4( + keccak256( + abi.encode( + _PACKED_USER_OPERATION, + userOp.sender, + userOp.nonce, + keccak256(userOp.initCode), + keccak256(userOp.callData), + userOp.accountGasLimits, + userOp.preVerificationGas, + userOp.gasFees, + keccak256(userOp.paymasterAndData), + msg.sender // entrypoint + ) + ) + ); } /** diff --git a/contracts/account/extensions/draft-AccountECDSA.sol b/contracts/account/extensions/draft-AccountECDSA.sol index dfd5a6a3..7bd7e5a1 100644 --- a/contracts/account/extensions/draft-AccountECDSA.sol +++ b/contracts/account/extensions/draft-AccountECDSA.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.20; import {IERC5267} from "@openzeppelin/contracts/interfaces/IERC5267.sol"; -import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; @@ -12,7 +11,8 @@ import {AccountBase} from "../draft-AccountBase.sol"; import {ERC7739Signer} from "../../utils/cryptography/draft-ERC7739Signer.sol"; /** - * @dev Account implementation using {ECDSA} signatures and {AccountBase} for replay protection. + * @dev Account implementation using {ECDSA} signatures and {ERC7739Signer} for replay protection with + * {ERC721Holder} and {ERC1155Holder} support. * * An {_initializeSigner} function is provided to set the account's signer address. Doing so it's * easier for a factory, whose likely to use initializable clones of this contract. @@ -34,11 +34,6 @@ import {ERC7739Signer} from "../../utils/cryptography/draft-ERC7739Signer.sol"; * or during initialization (if used as a clone) may leave the account either front-runnable or unusable. */ abstract contract AccountECDSA is ERC165, IERC5267, ERC7739Signer, AccountBase, ERC721Holder, ERC1155Holder { - bytes32 internal constant _PACKED_USER_OPERATION = - keccak256( - "PackedUserOperation(address sender,uint256 nonce,bytes initCode,bytes callData,bytes32 accountGasLimits,uint256 preVerificationGas,bytes32 gasFees,bytes paymasterAndData,address entrypoint)" - ); - /** * @dev The {signer} is already initialized. */ @@ -61,34 +56,6 @@ abstract contract AccountECDSA is ERC165, IERC5267, ERC7739Signer, AccountBase, return _signer; } - /** - * @dev Customize the user operation hash to sign. See {AccountBase-_signableUserOpHash}. - * - * This implementation uses the EIP-712 typed data hashing mechanism for readability. - */ - function _signableUserOpHash( - PackedUserOperation calldata userOp, - bytes32 /* userOpHash */ - ) internal view virtual override returns (bytes32) { - return - _hashTypedDataV4( - keccak256( - abi.encode( - _PACKED_USER_OPERATION, - userOp.sender, - userOp.nonce, - keccak256(userOp.initCode), - keccak256(userOp.callData), - userOp.accountGasLimits, - userOp.preVerificationGas, - userOp.gasFees, - keccak256(userOp.paymasterAndData), - msg.sender - ) - ) - ); - } - /** * @dev Validates the signature using the account's signer. */ diff --git a/contracts/account/extensions/draft-AccountP256.sol b/contracts/account/extensions/draft-AccountP256.sol index bf710ccf..e01019a3 100644 --- a/contracts/account/extensions/draft-AccountP256.sol +++ b/contracts/account/extensions/draft-AccountP256.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.20; import {IERC5267} from "@openzeppelin/contracts/interfaces/IERC5267.sol"; -import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; import {P256} from "@openzeppelin/contracts/utils/cryptography/P256.sol"; import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; @@ -12,7 +11,8 @@ import {AccountBase} from "../draft-AccountBase.sol"; import {ERC7739Signer} from "../../utils/cryptography/draft-ERC7739Signer.sol"; /** - * @dev Account implementation using {P256} signatures and {AccountBase} for replay protection. + * @dev Account implementation using {P256} signatures and {ERC7739Signer} for replay protection with + * {ERC721Holder} and {ERC1155Holder} support. * * An {_initializeSigner} function is provided to set the account's signer address. Doing so it's * easier for a factory, whose likely to use initializable clones of this contract. @@ -34,11 +34,6 @@ import {ERC7739Signer} from "../../utils/cryptography/draft-ERC7739Signer.sol"; * or during initialization (if used as a clone) may leave the account either front-runnable or unusable. */ abstract contract AccountP256 is ERC165, IERC5267, ERC7739Signer, AccountBase, ERC721Holder, ERC1155Holder { - bytes32 internal constant _PACKED_USER_OPERATION = - keccak256( - "PackedUserOperation(address sender,uint256 nonce,bytes initCode,bytes callData,bytes32 accountGasLimits,uint256 preVerificationGas,bytes32 gasFees,bytes paymasterAndData,address entrypoint)" - ); - /** * @dev The {signer} is already initialized. */ @@ -63,34 +58,6 @@ abstract contract AccountP256 is ERC165, IERC5267, ERC7739Signer, AccountBase, E return (_qx, _qy); } - /** - * @dev Customize the user operation hash to sign. See {AccountBase-_signableUserOpHash}. - * - * This implementation uses the EIP-712 typed data hashing mechanism for readability. - */ - function _signableUserOpHash( - PackedUserOperation calldata userOp, - bytes32 /* userOpHash */ - ) internal view virtual override returns (bytes32) { - return - _hashTypedDataV4( - keccak256( - abi.encode( - _PACKED_USER_OPERATION, - userOp.sender, - userOp.nonce, - keccak256(userOp.initCode), - keccak256(userOp.callData), - userOp.accountGasLimits, - userOp.preVerificationGas, - userOp.gasFees, - keccak256(userOp.paymasterAndData), - msg.sender - ) - ) - ); - } - /** * @dev Validates the signature using the account's signer. */ diff --git a/contracts/account/extensions/draft-AccountRSA.sol b/contracts/account/extensions/draft-AccountRSA.sol index 850b6729..97050e1e 100644 --- a/contracts/account/extensions/draft-AccountRSA.sol +++ b/contracts/account/extensions/draft-AccountRSA.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.20; import {IERC5267} from "@openzeppelin/contracts/interfaces/IERC5267.sol"; -import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; import {RSA} from "@openzeppelin/contracts/utils/cryptography/RSA.sol"; import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; @@ -12,7 +11,8 @@ import {AccountBase} from "../draft-AccountBase.sol"; import {ERC7739Signer} from "../../utils/cryptography/draft-ERC7739Signer.sol"; /** - * @dev Account implementation using {RSA} signatures and {AccountBase} for replay protection. + * @dev Account implementation using {RSA} signatures and {ERC7739Signer} for replay protection with + * {ERC721Holder} and {ERC1155Holder} support. * * An {_initializeSigner} function is provided to set the account's signer address. Doing so it's * easier for a factory, whose likely to use initializable clones of this contract. @@ -34,11 +34,6 @@ import {ERC7739Signer} from "../../utils/cryptography/draft-ERC7739Signer.sol"; * or during initialization (if used as a clone) may leave the account either front-runnable or unusable. */ abstract contract AccountRSA is ERC165, IERC5267, ERC7739Signer, AccountBase, ERC721Holder, ERC1155Holder { - bytes32 internal constant _PACKED_USER_OPERATION = - keccak256( - "PackedUserOperation(address sender,uint256 nonce,bytes initCode,bytes callData,bytes32 accountGasLimits,uint256 preVerificationGas,bytes32 gasFees,bytes paymasterAndData,address entrypoint)" - ); - /** * @dev The {signer} is already initialized. */ @@ -63,34 +58,6 @@ abstract contract AccountRSA is ERC165, IERC5267, ERC7739Signer, AccountBase, ER return (_e, _n); } - /** - * @dev Customize the user operation hash to sign. See {AccountBase-_signableUserOpHash}. - * - * This implementation uses the EIP-712 typed data hashing mechanism for readability. - */ - function _signableUserOpHash( - PackedUserOperation calldata userOp, - bytes32 /* userOpHash */ - ) internal view virtual override returns (bytes32) { - return - _hashTypedDataV4( - keccak256( - abi.encode( - _PACKED_USER_OPERATION, - userOp.sender, - userOp.nonce, - keccak256(userOp.initCode), - keccak256(userOp.callData), - userOp.accountGasLimits, - userOp.preVerificationGas, - userOp.gasFees, - keccak256(userOp.paymasterAndData), - msg.sender - ) - ) - ); - } - /** * @dev Validates the signature using the account's signer. */ diff --git a/test/account/draft-AccountBase.test.js b/test/account/draft-AccountBase.test.js index 7ce99458..03f748da 100644 --- a/test/account/draft-AccountBase.test.js +++ b/test/account/draft-AccountBase.test.js @@ -9,7 +9,7 @@ async function fixture() { const target = await ethers.deployContract('CallReceiverMockExtended'); const signer = new NonNativeSigner({ sign: () => ({ serialized: '0x01' }) }); const helper = new ERC4337Helper('$AccountBaseMock'); - const smartAccount = await helper.newAccount(); + const smartAccount = await helper.newAccount(['AccountBase', '1']); const signUserOp = async userOp => { userOp.signature = await signer.signMessage(userOp.hash()); return userOp; From ae6a6654ed534cfb44d36506f2af4d72b9a2188c Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 16 Dec 2024 18:13:26 +0100 Subject: [PATCH 41/51] Split AccountCore / Account --- contracts/account/draft-Account.sol | 15 +++++++++++++++ ...aft-AccountBase.sol => draft-AccountCore.sol} | 2 +- .../account/extensions/draft-AccountECDSA.sol | 16 +++------------- .../account/extensions/draft-AccountP256.sol | 16 +++------------- .../account/extensions/draft-AccountRSA.sol | 16 +++------------- contracts/mocks/account/AccountBaseMock.sol | 4 ++-- contracts/mocks/account/AccountECDSAMock.sol | 10 +++++++++- contracts/mocks/account/AccountP256Mock.sol | 10 +++++++++- contracts/mocks/account/AccountRSAMock.sol | 10 +++++++++- 9 files changed, 54 insertions(+), 45 deletions(-) create mode 100644 contracts/account/draft-Account.sol rename contracts/account/{draft-AccountBase.sol => draft-AccountCore.sol} (98%) diff --git a/contracts/account/draft-Account.sol b/contracts/account/draft-Account.sol new file mode 100644 index 00000000..0ec413da --- /dev/null +++ b/contracts/account/draft-Account.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; +import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; +import {ERC7739Signer} from "../utils/cryptography/draft-ERC7739Signer.sol"; +import {AccountCore} from "./draft-AccountCore.sol"; + +abstract contract Account is AccountCore, ERC721Holder, ERC1155Holder, ERC7739Signer { + function _rawSignatureValidation( + bytes32 hash, + bytes calldata signature + ) internal view virtual override(AccountCore, ERC7739Signer) returns (bool); +} diff --git a/contracts/account/draft-AccountBase.sol b/contracts/account/draft-AccountCore.sol similarity index 98% rename from contracts/account/draft-AccountBase.sol rename to contracts/account/draft-AccountCore.sol index 73bb573d..34e88267 100644 --- a/contracts/account/draft-AccountBase.sol +++ b/contracts/account/draft-AccountCore.sol @@ -18,7 +18,7 @@ import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; * attacker to bypass the account's security measures. Check out {AccountECDSA}, {AccountP256}, or {AccountRSA} for * digital signature validation implementations. */ -abstract contract AccountBase is EIP712, IAccount, IAccountExecute { +abstract contract AccountCore is EIP712, IAccount, IAccountExecute { using MessageHashUtils for bytes32; bytes32 internal constant _PACKED_USER_OPERATION = diff --git a/contracts/account/extensions/draft-AccountECDSA.sol b/contracts/account/extensions/draft-AccountECDSA.sol index 7bd7e5a1..55f38c47 100644 --- a/contracts/account/extensions/draft-AccountECDSA.sol +++ b/contracts/account/extensions/draft-AccountECDSA.sol @@ -2,13 +2,8 @@ pragma solidity ^0.8.20; -import {IERC5267} from "@openzeppelin/contracts/interfaces/IERC5267.sol"; -import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; -import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; -import {AccountBase} from "../draft-AccountBase.sol"; -import {ERC7739Signer} from "../../utils/cryptography/draft-ERC7739Signer.sol"; +import {AccountCore} from "../draft-AccountCore.sol"; /** * @dev Account implementation using {ECDSA} signatures and {ERC7739Signer} for replay protection with @@ -33,7 +28,7 @@ import {ERC7739Signer} from "../../utils/cryptography/draft-ERC7739Signer.sol"; * IMPORTANT: Avoiding to call {_initializeSigner} either during construction (if used standalone) * or during initialization (if used as a clone) may leave the account either front-runnable or unusable. */ -abstract contract AccountECDSA is ERC165, IERC5267, ERC7739Signer, AccountBase, ERC721Holder, ERC1155Holder { +abstract contract AccountECDSA is AccountCore { /** * @dev The {signer} is already initialized. */ @@ -62,13 +57,8 @@ abstract contract AccountECDSA is ERC165, IERC5267, ERC7739Signer, AccountBase, function _rawSignatureValidation( bytes32 hash, bytes calldata signature - ) internal view virtual override(AccountBase, ERC7739Signer) returns (bool) { + ) internal view virtual override returns (bool) { (address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(hash, signature); return signer() == recovered && err == ECDSA.RecoverError.NoError; } - - // @inheritdoc ERC165 - function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, ERC1155Holder) returns (bool) { - return super.supportsInterface(interfaceId); - } } diff --git a/contracts/account/extensions/draft-AccountP256.sol b/contracts/account/extensions/draft-AccountP256.sol index e01019a3..10473cf9 100644 --- a/contracts/account/extensions/draft-AccountP256.sol +++ b/contracts/account/extensions/draft-AccountP256.sol @@ -2,13 +2,8 @@ pragma solidity ^0.8.20; -import {IERC5267} from "@openzeppelin/contracts/interfaces/IERC5267.sol"; -import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; import {P256} from "@openzeppelin/contracts/utils/cryptography/P256.sol"; -import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; -import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; -import {AccountBase} from "../draft-AccountBase.sol"; -import {ERC7739Signer} from "../../utils/cryptography/draft-ERC7739Signer.sol"; +import {AccountCore} from "../draft-AccountCore.sol"; /** * @dev Account implementation using {P256} signatures and {ERC7739Signer} for replay protection with @@ -33,7 +28,7 @@ import {ERC7739Signer} from "../../utils/cryptography/draft-ERC7739Signer.sol"; * IMPORTANT: Avoiding to call {_initializeSigner} either during construction (if used standalone) * or during initialization (if used as a clone) may leave the account either front-runnable or unusable. */ -abstract contract AccountP256 is ERC165, IERC5267, ERC7739Signer, AccountBase, ERC721Holder, ERC1155Holder { +abstract contract AccountP256 is AccountCore { /** * @dev The {signer} is already initialized. */ @@ -64,16 +59,11 @@ abstract contract AccountP256 is ERC165, IERC5267, ERC7739Signer, AccountBase, E function _rawSignatureValidation( bytes32 hash, bytes calldata signature - ) internal view virtual override(AccountBase, ERC7739Signer) returns (bool) { + ) internal view virtual override returns (bool) { if (signature.length < 0x40) return false; bytes32 r = bytes32(signature[0x00:0x20]); bytes32 s = bytes32(signature[0x20:0x40]); (bytes32 qx, bytes32 qy) = signer(); return P256.verify(hash, r, s, qx, qy); } - - // @inheritdoc ERC165 - function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, ERC1155Holder) returns (bool) { - return super.supportsInterface(interfaceId); - } } diff --git a/contracts/account/extensions/draft-AccountRSA.sol b/contracts/account/extensions/draft-AccountRSA.sol index 97050e1e..8d203401 100644 --- a/contracts/account/extensions/draft-AccountRSA.sol +++ b/contracts/account/extensions/draft-AccountRSA.sol @@ -2,13 +2,8 @@ pragma solidity ^0.8.20; -import {IERC5267} from "@openzeppelin/contracts/interfaces/IERC5267.sol"; -import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; import {RSA} from "@openzeppelin/contracts/utils/cryptography/RSA.sol"; -import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; -import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; -import {AccountBase} from "../draft-AccountBase.sol"; -import {ERC7739Signer} from "../../utils/cryptography/draft-ERC7739Signer.sol"; +import {AccountCore} from "../draft-AccountCore.sol"; /** * @dev Account implementation using {RSA} signatures and {ERC7739Signer} for replay protection with @@ -33,7 +28,7 @@ import {ERC7739Signer} from "../../utils/cryptography/draft-ERC7739Signer.sol"; * IMPORTANT: Avoiding to call {_initializeSigner} either during construction (if used standalone) * or during initialization (if used as a clone) may leave the account either front-runnable or unusable. */ -abstract contract AccountRSA is ERC165, IERC5267, ERC7739Signer, AccountBase, ERC721Holder, ERC1155Holder { +abstract contract AccountRSA is AccountCore { /** * @dev The {signer} is already initialized. */ @@ -64,13 +59,8 @@ abstract contract AccountRSA is ERC165, IERC5267, ERC7739Signer, AccountBase, ER function _rawSignatureValidation( bytes32 hash, bytes calldata signature - ) internal view virtual override(AccountBase, ERC7739Signer) returns (bool) { + ) internal view virtual override returns (bool) { (bytes memory e, bytes memory n) = signer(); return RSA.pkcs1Sha256(abi.encodePacked(hash), signature, e, n); } - - // @inheritdoc ERC165 - function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, ERC1155Holder) returns (bool) { - return super.supportsInterface(interfaceId); - } } diff --git a/contracts/mocks/account/AccountBaseMock.sol b/contracts/mocks/account/AccountBaseMock.sol index 15ae6695..dd03e69a 100644 --- a/contracts/mocks/account/AccountBaseMock.sol +++ b/contracts/mocks/account/AccountBaseMock.sol @@ -4,9 +4,9 @@ pragma solidity ^0.8.20; import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol"; -import {AccountBase} from "../../account/draft-AccountBase.sol"; +import {Account} from "../../account/draft-Account.sol"; -abstract contract AccountBaseMock is AccountBase { +abstract contract AccountBaseMock is Account { /// Validates a user operation with a boolean signature. function _rawSignatureValidation( bytes32 /* userOpHash */, diff --git a/contracts/mocks/account/AccountECDSAMock.sol b/contracts/mocks/account/AccountECDSAMock.sol index cb5a8f6b..6bfadf26 100644 --- a/contracts/mocks/account/AccountECDSAMock.sol +++ b/contracts/mocks/account/AccountECDSAMock.sol @@ -2,10 +2,18 @@ pragma solidity ^0.8.20; +import {Account} from "../../account/draft-Account.sol"; import {AccountECDSA} from "../../account/extensions/draft-AccountECDSA.sol"; -abstract contract AccountECDSAMock is AccountECDSA { +abstract contract AccountECDSAMock is Account, AccountECDSA { constructor(address signerAddr) { _initializeSigner(signerAddr); } + + function _rawSignatureValidation( + bytes32 hash, + bytes calldata signature + ) internal view virtual override(Account, AccountECDSA) returns (bool) { + return super._rawSignatureValidation(hash, signature); + } } diff --git a/contracts/mocks/account/AccountP256Mock.sol b/contracts/mocks/account/AccountP256Mock.sol index 55ecc427..b5008313 100644 --- a/contracts/mocks/account/AccountP256Mock.sol +++ b/contracts/mocks/account/AccountP256Mock.sol @@ -2,10 +2,18 @@ pragma solidity ^0.8.20; +import {Account} from "../../account/draft-Account.sol"; import {AccountP256} from "../../account/extensions/draft-AccountP256.sol"; -abstract contract AccountP256Mock is AccountP256 { +abstract contract AccountP256Mock is Account, AccountP256 { constructor(bytes32 qx, bytes32 qy) { _initializeSigner(qx, qy); } + + function _rawSignatureValidation( + bytes32 hash, + bytes calldata signature + ) internal view virtual override(Account, AccountP256) returns (bool) { + return super._rawSignatureValidation(hash, signature); + } } diff --git a/contracts/mocks/account/AccountRSAMock.sol b/contracts/mocks/account/AccountRSAMock.sol index 79d2dd80..e2a11af6 100644 --- a/contracts/mocks/account/AccountRSAMock.sol +++ b/contracts/mocks/account/AccountRSAMock.sol @@ -2,10 +2,18 @@ pragma solidity ^0.8.20; +import {Account} from "../../account/draft-Account.sol"; import {AccountRSA} from "../../account/extensions/draft-AccountRSA.sol"; -abstract contract AccountRSAMock is AccountRSA { +abstract contract AccountRSAMock is Account, AccountRSA { constructor(bytes memory e, bytes memory n) { _initializeSigner(e, n); } + + function _rawSignatureValidation( + bytes32 hash, + bytes calldata signature + ) internal view virtual override(Account, AccountRSA) returns (bool) { + return super._rawSignatureValidation(hash, signature); + } } From 07ca06731c1f8ec7fda8c62e588f068b7cef1c85 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 16 Dec 2024 18:15:12 +0100 Subject: [PATCH 42/51] remove intermediary variable --- contracts/account/draft-AccountCore.sol | 4 ++-- contracts/utils/cryptography/draft-ERC7739Signer.sol | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/contracts/account/draft-AccountCore.sol b/contracts/account/draft-AccountCore.sol index 34e88267..b96c9c1d 100644 --- a/contracts/account/draft-AccountCore.sol +++ b/contracts/account/draft-AccountCore.sol @@ -95,7 +95,7 @@ abstract contract AccountCore is EIP712, IAccount, IAccountExecute { } /** - * @dev Returns the digest used by an offchain sigenr instead of the opaque `userOpHash`. + * @dev Returns the digest used by an offchain signer instead of the opaque `userOpHash`. * * Given the `userOpHash` calculation is defined by ERC-4337, offchain signers * may need to sign again this hash by rehashing it with other schemes (e.g. ERC-191). @@ -119,7 +119,7 @@ abstract contract AccountCore is EIP712, IAccount, IAccountExecute { userOp.preVerificationGas, userOp.gasFees, keccak256(userOp.paymasterAndData), - msg.sender // entrypoint + entryPoint() ) ) ); diff --git a/contracts/utils/cryptography/draft-ERC7739Signer.sol b/contracts/utils/cryptography/draft-ERC7739Signer.sol index 3eca321a..52a1c267 100644 --- a/contracts/utils/cryptography/draft-ERC7739Signer.sol +++ b/contracts/utils/cryptography/draft-ERC7739Signer.sol @@ -34,13 +34,11 @@ abstract contract ERC7739Signer is EIP712, IERC1271 { * - As a _personal_ signature (an EIP-712 mimic of the `eth_personalSign` for a smart contract) */ function isValidSignature(bytes32 hash, bytes calldata signature) public view virtual returns (bytes4 result) { - bool valid = _isValidNestedTypedDataSignature(hash, signature) || - _isValidNestedPersonalSignSignature(hash, signature); // For the hash `0x7739773977397739773977397739773977397739773977397739773977397739` and an empty signature, // we return the magic value too as it's assumed impossible to find a preimage for it that can be used maliciously. // Useful for simulation purposes and to validate whether the contract supports ERC-7739. return - valid + (_isValidNestedTypedDataSignature(hash, signature) || _isValidNestedPersonalSignSignature(hash, signature)) ? IERC1271.isValidSignature.selector : (hash == 0x7739773977397739773977397739773977397739773977397739773977397739 && signature.length == 0) ? bytes4(0x77390001) From 6aa55976cd51548bb9ef1359f2cbef0f274c0c2b Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 16 Dec 2024 18:17:08 +0100 Subject: [PATCH 43/51] doc --- contracts/account/draft-Account.sol | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/contracts/account/draft-Account.sol b/contracts/account/draft-Account.sol index 0ec413da..c0cf72cc 100644 --- a/contracts/account/draft-Account.sol +++ b/contracts/account/draft-Account.sol @@ -7,6 +7,13 @@ import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155 import {ERC7739Signer} from "../utils/cryptography/draft-ERC7739Signer.sol"; import {AccountCore} from "./draft-AccountCore.sol"; +/** + * @dev Extention of {AccountCore} with recommanded feature that most account abstraction implementation will want: + * + * * {ERC721Holder} for ERC-721 token handling + * * {ERC1155Holder} for ERC-1155 token handling + * * {ERC7739Signer} for ERC-1271 signature support with ERC-7739 replay protection + */ abstract contract Account is AccountCore, ERC721Holder, ERC1155Holder, ERC7739Signer { function _rawSignatureValidation( bytes32 hash, From 0513f3aba059db569eb5976309c38722910ff10e Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 16 Dec 2024 18:18:51 +0100 Subject: [PATCH 44/51] spelling --- contracts/account/draft-Account.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/account/draft-Account.sol b/contracts/account/draft-Account.sol index c0cf72cc..807d66aa 100644 --- a/contracts/account/draft-Account.sol +++ b/contracts/account/draft-Account.sol @@ -8,7 +8,7 @@ import {ERC7739Signer} from "../utils/cryptography/draft-ERC7739Signer.sol"; import {AccountCore} from "./draft-AccountCore.sol"; /** - * @dev Extention of {AccountCore} with recommanded feature that most account abstraction implementation will want: + * @dev Extension of {AccountCore} with recommended feature that most account abstraction implementation will want: * * * {ERC721Holder} for ERC-721 token handling * * {ERC1155Holder} for ERC-1155 token handling From e3ce6b5057d87c56743bb9f466218c2ff5f0ad52 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 16 Dec 2024 21:25:34 +0100 Subject: [PATCH 45/51] abstract signer --- contracts/account/draft-Account.sol | 9 +++------ contracts/account/draft-AccountCore.sol | 8 ++------ contracts/mocks/account/AccountECDSAMock.sol | 11 ++--------- contracts/mocks/account/AccountP256Mock.sol | 11 ++--------- contracts/mocks/account/AccountRSAMock.sol | 11 ++--------- contracts/utils/cryptography/AbstractSigner.sol | 14 ++++++++++++++ .../cryptography/SignerECDSA.sol} | 4 ++-- .../cryptography/SignerP256.sol} | 4 ++-- .../cryptography/SignerRSA.sol} | 4 ++-- .../utils/cryptography/draft-ERC7739Signer.sol | 12 ++---------- 10 files changed, 33 insertions(+), 55 deletions(-) create mode 100644 contracts/utils/cryptography/AbstractSigner.sol rename contracts/{account/extensions/draft-AccountECDSA.sol => utils/cryptography/SignerECDSA.sol} (95%) rename contracts/{account/extensions/draft-AccountP256.sol => utils/cryptography/SignerP256.sol} (95%) rename contracts/{account/extensions/draft-AccountRSA.sol => utils/cryptography/SignerRSA.sol} (95%) diff --git a/contracts/account/draft-Account.sol b/contracts/account/draft-Account.sol index 807d66aa..3b8a25bb 100644 --- a/contracts/account/draft-Account.sol +++ b/contracts/account/draft-Account.sol @@ -13,10 +13,7 @@ import {AccountCore} from "./draft-AccountCore.sol"; * * {ERC721Holder} for ERC-721 token handling * * {ERC1155Holder} for ERC-1155 token handling * * {ERC7739Signer} for ERC-1271 signature support with ERC-7739 replay protection + * + * NOTE: This needs to be combine with a signer implementation such as {SignerECDSA}, {SignerP256} or {SignerRSA} */ -abstract contract Account is AccountCore, ERC721Holder, ERC1155Holder, ERC7739Signer { - function _rawSignatureValidation( - bytes32 hash, - bytes calldata signature - ) internal view virtual override(AccountCore, ERC7739Signer) returns (bool); -} +abstract contract Account is AccountCore, ERC721Holder, ERC1155Holder, ERC7739Signer {} diff --git a/contracts/account/draft-AccountCore.sol b/contracts/account/draft-AccountCore.sol index b96c9c1d..c4876103 100644 --- a/contracts/account/draft-AccountCore.sol +++ b/contracts/account/draft-AccountCore.sol @@ -7,6 +7,7 @@ import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337U import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; +import {AbstractSigner} from "../utils/cryptography/AbstractSigner.sol"; /** * @dev A simple ERC4337 account implementation. This base implementation only includes the minimal logic to process @@ -18,7 +19,7 @@ import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; * attacker to bypass the account's security measures. Check out {AccountECDSA}, {AccountP256}, or {AccountRSA} for * digital signature validation implementations. */ -abstract contract AccountCore is EIP712, IAccount, IAccountExecute { +abstract contract AccountCore is AbstractSigner, EIP712, IAccount, IAccountExecute { using MessageHashUtils for bytes32; bytes32 internal constant _PACKED_USER_OPERATION = @@ -156,11 +157,6 @@ abstract contract AccountCore is EIP712, IAccount, IAccountExecute { } } - /** - * @dev Signature validation algorithm. - */ - function _rawSignatureValidation(bytes32 hash, bytes calldata signature) internal view virtual returns (bool); - /** * @dev Receive Ether. */ diff --git a/contracts/mocks/account/AccountECDSAMock.sol b/contracts/mocks/account/AccountECDSAMock.sol index 6bfadf26..1e478b57 100644 --- a/contracts/mocks/account/AccountECDSAMock.sol +++ b/contracts/mocks/account/AccountECDSAMock.sol @@ -3,17 +3,10 @@ pragma solidity ^0.8.20; import {Account} from "../../account/draft-Account.sol"; -import {AccountECDSA} from "../../account/extensions/draft-AccountECDSA.sol"; +import {SignerECDSA} from "../../utils/cryptography/SignerECDSA.sol"; -abstract contract AccountECDSAMock is Account, AccountECDSA { +abstract contract AccountECDSAMock is Account, SignerECDSA { constructor(address signerAddr) { _initializeSigner(signerAddr); } - - function _rawSignatureValidation( - bytes32 hash, - bytes calldata signature - ) internal view virtual override(Account, AccountECDSA) returns (bool) { - return super._rawSignatureValidation(hash, signature); - } } diff --git a/contracts/mocks/account/AccountP256Mock.sol b/contracts/mocks/account/AccountP256Mock.sol index b5008313..2bbd20cd 100644 --- a/contracts/mocks/account/AccountP256Mock.sol +++ b/contracts/mocks/account/AccountP256Mock.sol @@ -3,17 +3,10 @@ pragma solidity ^0.8.20; import {Account} from "../../account/draft-Account.sol"; -import {AccountP256} from "../../account/extensions/draft-AccountP256.sol"; +import {SignerP256} from "../../utils/cryptography/SignerP256.sol"; -abstract contract AccountP256Mock is Account, AccountP256 { +abstract contract AccountP256Mock is Account, SignerP256 { constructor(bytes32 qx, bytes32 qy) { _initializeSigner(qx, qy); } - - function _rawSignatureValidation( - bytes32 hash, - bytes calldata signature - ) internal view virtual override(Account, AccountP256) returns (bool) { - return super._rawSignatureValidation(hash, signature); - } } diff --git a/contracts/mocks/account/AccountRSAMock.sol b/contracts/mocks/account/AccountRSAMock.sol index e2a11af6..46c1a708 100644 --- a/contracts/mocks/account/AccountRSAMock.sol +++ b/contracts/mocks/account/AccountRSAMock.sol @@ -3,17 +3,10 @@ pragma solidity ^0.8.20; import {Account} from "../../account/draft-Account.sol"; -import {AccountRSA} from "../../account/extensions/draft-AccountRSA.sol"; +import {SignerRSA} from "../../utils/cryptography/SignerRSA.sol"; -abstract contract AccountRSAMock is Account, AccountRSA { +abstract contract AccountRSAMock is Account, SignerRSA { constructor(bytes memory e, bytes memory n) { _initializeSigner(e, n); } - - function _rawSignatureValidation( - bytes32 hash, - bytes calldata signature - ) internal view virtual override(Account, AccountRSA) returns (bool) { - return super._rawSignatureValidation(hash, signature); - } } diff --git a/contracts/utils/cryptography/AbstractSigner.sol b/contracts/utils/cryptography/AbstractSigner.sol new file mode 100644 index 00000000..db1bfaf3 --- /dev/null +++ b/contracts/utils/cryptography/AbstractSigner.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +abstract contract AbstractSigner { + /** + * @dev Signature validation algorithm. + * + * WARNING: Implementing a signature validation algorithm is a security-sensitive operation as it involves + * cryptographic verification. It is important to review and test thoroughly before deployment. Consider + * using one of the signature verification libraries ({ECDSA}, {P256} or {RSA}). + */ + function _rawSignatureValidation(bytes32 hash, bytes calldata signature) internal view virtual returns (bool); +} diff --git a/contracts/account/extensions/draft-AccountECDSA.sol b/contracts/utils/cryptography/SignerECDSA.sol similarity index 95% rename from contracts/account/extensions/draft-AccountECDSA.sol rename to contracts/utils/cryptography/SignerECDSA.sol index 55f38c47..37bcca57 100644 --- a/contracts/account/extensions/draft-AccountECDSA.sol +++ b/contracts/utils/cryptography/SignerECDSA.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.20; import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import {AccountCore} from "../draft-AccountCore.sol"; +import {AbstractSigner} from "./AbstractSigner.sol"; /** * @dev Account implementation using {ECDSA} signatures and {ERC7739Signer} for replay protection with @@ -28,7 +28,7 @@ import {AccountCore} from "../draft-AccountCore.sol"; * IMPORTANT: Avoiding to call {_initializeSigner} either during construction (if used standalone) * or during initialization (if used as a clone) may leave the account either front-runnable or unusable. */ -abstract contract AccountECDSA is AccountCore { +abstract contract SignerECDSA is AbstractSigner { /** * @dev The {signer} is already initialized. */ diff --git a/contracts/account/extensions/draft-AccountP256.sol b/contracts/utils/cryptography/SignerP256.sol similarity index 95% rename from contracts/account/extensions/draft-AccountP256.sol rename to contracts/utils/cryptography/SignerP256.sol index 10473cf9..9c28f191 100644 --- a/contracts/account/extensions/draft-AccountP256.sol +++ b/contracts/utils/cryptography/SignerP256.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.20; import {P256} from "@openzeppelin/contracts/utils/cryptography/P256.sol"; -import {AccountCore} from "../draft-AccountCore.sol"; +import {AbstractSigner} from "./AbstractSigner.sol"; /** * @dev Account implementation using {P256} signatures and {ERC7739Signer} for replay protection with @@ -28,7 +28,7 @@ import {AccountCore} from "../draft-AccountCore.sol"; * IMPORTANT: Avoiding to call {_initializeSigner} either during construction (if used standalone) * or during initialization (if used as a clone) may leave the account either front-runnable or unusable. */ -abstract contract AccountP256 is AccountCore { +abstract contract SignerP256 is AbstractSigner { /** * @dev The {signer} is already initialized. */ diff --git a/contracts/account/extensions/draft-AccountRSA.sol b/contracts/utils/cryptography/SignerRSA.sol similarity index 95% rename from contracts/account/extensions/draft-AccountRSA.sol rename to contracts/utils/cryptography/SignerRSA.sol index 8d203401..d641a701 100644 --- a/contracts/account/extensions/draft-AccountRSA.sol +++ b/contracts/utils/cryptography/SignerRSA.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.20; import {RSA} from "@openzeppelin/contracts/utils/cryptography/RSA.sol"; -import {AccountCore} from "../draft-AccountCore.sol"; +import {AbstractSigner} from "./AbstractSigner.sol"; /** * @dev Account implementation using {RSA} signatures and {ERC7739Signer} for replay protection with @@ -28,7 +28,7 @@ import {AccountCore} from "../draft-AccountCore.sol"; * IMPORTANT: Avoiding to call {_initializeSigner} either during construction (if used standalone) * or during initialization (if used as a clone) may leave the account either front-runnable or unusable. */ -abstract contract AccountRSA is AccountCore { +abstract contract SignerRSA is AbstractSigner { /** * @dev The {signer} is already initialized. */ diff --git a/contracts/utils/cryptography/draft-ERC7739Signer.sol b/contracts/utils/cryptography/draft-ERC7739Signer.sol index 52a1c267..6775f32f 100644 --- a/contracts/utils/cryptography/draft-ERC7739Signer.sol +++ b/contracts/utils/cryptography/draft-ERC7739Signer.sol @@ -6,6 +6,7 @@ import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import {ShortStrings} from "@openzeppelin/contracts/utils/ShortStrings.sol"; +import {AbstractSigner} from "./AbstractSigner.sol"; import {ERC7739Utils} from "./draft-ERC7739Utils.sol"; /** @@ -21,7 +22,7 @@ import {ERC7739Utils} from "./draft-ERC7739Utils.sol"; * Consider that strings longer than that will use storage, which may limit the ability of the signer to * be used within the ERC-4337 validation phase (due to ERC-7562 storage access rules). */ -abstract contract ERC7739Signer is EIP712, IERC1271 { +abstract contract ERC7739Signer is AbstractSigner, EIP712, IERC1271 { using ERC7739Utils for *; using MessageHashUtils for bytes32; @@ -93,13 +94,4 @@ abstract contract ERC7739Signer is EIP712, IERC1271 { signature ); } - - /** - * @dev Signature validation algorithm. - * - * WARNING: Implementing a signature validation algorithm is a security-sensitive operation as it involves - * cryptographic verification. It is important to review and test thoroughly before deployment. Consider - * using one of the signature verification libraries ({ECDSA}, {P256} or {RSA}). - */ - function _rawSignatureValidation(bytes32 hash, bytes calldata signature) internal view virtual returns (bool); } From 69cc3daca755d4fb186d8bb243f6325d4679226b Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 16 Dec 2024 21:27:20 +0100 Subject: [PATCH 46/51] docs --- contracts/utils/cryptography/SignerECDSA.sol | 5 ++--- contracts/utils/cryptography/SignerP256.sol | 5 ++--- contracts/utils/cryptography/SignerRSA.sol | 5 ++--- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/contracts/utils/cryptography/SignerECDSA.sol b/contracts/utils/cryptography/SignerECDSA.sol index 37bcca57..10ab3607 100644 --- a/contracts/utils/cryptography/SignerECDSA.sol +++ b/contracts/utils/cryptography/SignerECDSA.sol @@ -6,8 +6,7 @@ import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import {AbstractSigner} from "./AbstractSigner.sol"; /** - * @dev Account implementation using {ECDSA} signatures and {ERC7739Signer} for replay protection with - * {ERC721Holder} and {ERC1155Holder} support. + * @dev Implementation of {AbstractSigner} using {ECDSA} signatures. * * An {_initializeSigner} function is provided to set the account's signer address. Doing so it's * easier for a factory, whose likely to use initializable clones of this contract. @@ -15,7 +14,7 @@ import {AbstractSigner} from "./AbstractSigner.sol"; * Example of usage: * * ```solidity - * contract MyAccountECDSA is AccountECDSA { + * contract MyAccountECDSA is Account, SignerECDSA { * constructor() EIP712("MyAccountECDSA", "1") {} * * function initializeSigner(address signerAddr) public virtual initializer { diff --git a/contracts/utils/cryptography/SignerP256.sol b/contracts/utils/cryptography/SignerP256.sol index 9c28f191..2fe004d0 100644 --- a/contracts/utils/cryptography/SignerP256.sol +++ b/contracts/utils/cryptography/SignerP256.sol @@ -6,8 +6,7 @@ import {P256} from "@openzeppelin/contracts/utils/cryptography/P256.sol"; import {AbstractSigner} from "./AbstractSigner.sol"; /** - * @dev Account implementation using {P256} signatures and {ERC7739Signer} for replay protection with - * {ERC721Holder} and {ERC1155Holder} support. + * @dev Implementation of {AbstractSigner} using {P256} signatures. * * An {_initializeSigner} function is provided to set the account's signer address. Doing so it's * easier for a factory, whose likely to use initializable clones of this contract. @@ -15,7 +14,7 @@ import {AbstractSigner} from "./AbstractSigner.sol"; * Example of usage: * * ```solidity - * contract MyAccountP256 is AccountP256 { + * contract MyAccountP256 is Account, SignerP256 { * constructor() EIP712("MyAccountP256", "1") {} * * function initializeSigner(bytes32 qx, bytes32 qy) public virtual initializer { diff --git a/contracts/utils/cryptography/SignerRSA.sol b/contracts/utils/cryptography/SignerRSA.sol index d641a701..03c2cb72 100644 --- a/contracts/utils/cryptography/SignerRSA.sol +++ b/contracts/utils/cryptography/SignerRSA.sol @@ -6,8 +6,7 @@ import {RSA} from "@openzeppelin/contracts/utils/cryptography/RSA.sol"; import {AbstractSigner} from "./AbstractSigner.sol"; /** - * @dev Account implementation using {RSA} signatures and {ERC7739Signer} for replay protection with - * {ERC721Holder} and {ERC1155Holder} support. + * @dev Implementation of {AbstractSigner} using {RSA} signatures. * * An {_initializeSigner} function is provided to set the account's signer address. Doing so it's * easier for a factory, whose likely to use initializable clones of this contract. @@ -15,7 +14,7 @@ import {AbstractSigner} from "./AbstractSigner.sol"; * Example of usage: * * ```solidity - * contract MyAccountRSA is AccountRSA { + * contract MyAccountRSA is Account, SignerRSA { * constructor() EIP712("MyAccountRSA", "1") {} * * function initializeSigner(bytes memory e, bytes memory n) external { From d0bd34f0eeef32036328d314687b479171c8ebdb Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 16 Dec 2024 21:30:56 +0100 Subject: [PATCH 47/51] ERC7702 signer --- .../extensions/AccountSignerERC7702.sol | 19 +++++++++++++++++++ .../mocks/account/AccountERC7702Mock.sol | 8 ++++++++ 2 files changed, 27 insertions(+) create mode 100644 contracts/account/extensions/AccountSignerERC7702.sol create mode 100644 contracts/mocks/account/AccountERC7702Mock.sol diff --git a/contracts/account/extensions/AccountSignerERC7702.sol b/contracts/account/extensions/AccountSignerERC7702.sol new file mode 100644 index 00000000..f74416a4 --- /dev/null +++ b/contracts/account/extensions/AccountSignerERC7702.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {AccountCore} from "../draft-AccountCore.sol"; + +abstract contract AccountSignerERC7702 is AccountCore { + /** + * @dev Validates the signature using the account's address. + */ + function _rawSignatureValidation( + bytes32 hash, + bytes calldata signature + ) internal view virtual override returns (bool) { + (address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(hash, signature); + return address(this) == recovered && err == ECDSA.RecoverError.NoError; + } +} diff --git a/contracts/mocks/account/AccountERC7702Mock.sol b/contracts/mocks/account/AccountERC7702Mock.sol new file mode 100644 index 00000000..a9c30134 --- /dev/null +++ b/contracts/mocks/account/AccountERC7702Mock.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Account} from "../../account/draft-Account.sol"; +import {AccountSignerERC7702} from "../../account/extensions/AccountSignerERC7702.sol"; + +abstract contract AccountECDSAMock is Account, AccountSignerERC7702 {} From 3b1c1f41f0fa5fca9ea884202c6717a02fa17ca9 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 16 Dec 2024 21:53:06 +0100 Subject: [PATCH 48/51] fix --- contracts/mocks/account/AccountERC7702Mock.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/mocks/account/AccountERC7702Mock.sol b/contracts/mocks/account/AccountERC7702Mock.sol index a9c30134..fa17fdcc 100644 --- a/contracts/mocks/account/AccountERC7702Mock.sol +++ b/contracts/mocks/account/AccountERC7702Mock.sol @@ -5,4 +5,4 @@ pragma solidity ^0.8.20; import {Account} from "../../account/draft-Account.sol"; import {AccountSignerERC7702} from "../../account/extensions/AccountSignerERC7702.sol"; -abstract contract AccountECDSAMock is Account, AccountSignerERC7702 {} +abstract contract AccountERC7702Mock is Account, AccountSignerERC7702 {} From 190b5a578a71099a5b41e991774a17c596425da8 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 16 Dec 2024 21:58:20 +0100 Subject: [PATCH 49/51] doc example for ERC7739 use signers --- .../cryptography/ERC7739SignerECDSAMock.sol | 15 +++------------ .../cryptography/ERC7739SignerP256Mock.sol | 19 +++---------------- .../cryptography/ERC7739SignerRSAMock.sol | 17 +++-------------- 3 files changed, 9 insertions(+), 42 deletions(-) diff --git a/contracts/mocks/docs/utils/cryptography/ERC7739SignerECDSAMock.sol b/contracts/mocks/docs/utils/cryptography/ERC7739SignerECDSAMock.sol index ea47dc2d..3cb2dcbb 100644 --- a/contracts/mocks/docs/utils/cryptography/ERC7739SignerECDSAMock.sol +++ b/contracts/mocks/docs/utils/cryptography/ERC7739SignerECDSAMock.sol @@ -5,19 +5,10 @@ pragma solidity ^0.8.20; import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; import {ERC7739Signer} from "../../../../utils/cryptography/draft-ERC7739Signer.sol"; +import {SignerECDSA} from "../../../../utils/cryptography/SignerECDSA.sol"; -contract ERC7739SignerECDSAMock is ERC7739Signer { - address private immutable _signer; - +contract ERC7739SignerECDSAMock is ERC7739Signer, SignerECDSA { constructor(address signerAddr) EIP712("ERC7739SignerECDSA", "1") { - _signer = signerAddr; - } - - function _rawSignatureValidation( - bytes32 hash, - bytes calldata signature - ) internal view virtual override returns (bool) { - (address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(hash, signature); - return _signer == recovered && err == ECDSA.RecoverError.NoError; + _initializeSigner(signerAddr); } } diff --git a/contracts/mocks/docs/utils/cryptography/ERC7739SignerP256Mock.sol b/contracts/mocks/docs/utils/cryptography/ERC7739SignerP256Mock.sol index e3983002..c2adb71f 100644 --- a/contracts/mocks/docs/utils/cryptography/ERC7739SignerP256Mock.sol +++ b/contracts/mocks/docs/utils/cryptography/ERC7739SignerP256Mock.sol @@ -2,25 +2,12 @@ pragma solidity ^0.8.20; -import {P256} from "@openzeppelin/contracts/utils/cryptography/P256.sol"; import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; import {ERC7739Signer} from "../../../../utils/cryptography/draft-ERC7739Signer.sol"; +import {SignerP256} from "../../../../utils/cryptography/SignerP256.sol"; -contract ERC7739SignerP256Mock is ERC7739Signer { - bytes32 private immutable _qx; - bytes32 private immutable _qy; - +contract ERC7739SignerP256Mock is ERC7739Signer, SignerP256 { constructor(bytes32 qx, bytes32 qy) EIP712("ERC7739SignerP256", "1") { - _qx = qx; - _qy = qy; - } - - function _rawSignatureValidation( - bytes32 hash, - bytes calldata signature - ) internal view virtual override returns (bool) { - bytes32 r = bytes32(signature[0x00:0x20]); - bytes32 s = bytes32(signature[0x20:0x40]); - return P256.verify(hash, r, s, _qx, _qy); + _initializeSigner(qx, qy); } } diff --git a/contracts/mocks/docs/utils/cryptography/ERC7739SignerRSAMock.sol b/contracts/mocks/docs/utils/cryptography/ERC7739SignerRSAMock.sol index e6a6184c..5b95834d 100644 --- a/contracts/mocks/docs/utils/cryptography/ERC7739SignerRSAMock.sol +++ b/contracts/mocks/docs/utils/cryptography/ERC7739SignerRSAMock.sol @@ -2,23 +2,12 @@ pragma solidity ^0.8.20; -import {RSA} from "@openzeppelin/contracts/utils/cryptography/RSA.sol"; import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; import {ERC7739Signer} from "../../../../utils/cryptography/draft-ERC7739Signer.sol"; +import {SignerRSA} from "../../../../utils/cryptography/SignerRSA.sol"; -contract ERC7739SignerRSAMock is ERC7739Signer { - bytes private _e; - bytes private _n; - +contract ERC7739SignerRSAMock is ERC7739Signer, SignerRSA { constructor(bytes memory e, bytes memory n) EIP712("ERC7739SignerRSA", "1") { - _e = e; - _n = n; - } - - function _rawSignatureValidation( - bytes32 hash, - bytes calldata signature - ) internal view virtual override returns (bool) { - return RSA.pkcs1Sha256(hash, signature, _e, _n); + _initializeSigner(e, n); } } From a6ab43b222398980a5d1545b7b5e32d3418b1216 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Mon, 16 Dec 2024 23:50:00 -0600 Subject: [PATCH 50/51] Complete minimal documentation --- contracts/account/README.adoc | 15 +++++++++++++ contracts/account/draft-Account.sol | 6 +++--- contracts/account/draft-AccountCore.sol | 4 ++-- .../extensions/AccountSignerERC7702.sol | 5 ++++- contracts/utils/README.adoc | 14 +++++++++++-- .../utils/cryptography/AbstractSigner.sol | 8 ++++++- contracts/utils/cryptography/SignerECDSA.sol | 21 +++++++++---------- contracts/utils/cryptography/SignerP256.sol | 21 +++++++++---------- contracts/utils/cryptography/SignerRSA.sol | 21 +++++++++---------- .../cryptography/draft-ERC7739Signer.sol | 8 ++++--- .../utils/cryptography/draft-ERC7739Utils.sol | 12 ++++++----- 11 files changed, 85 insertions(+), 50 deletions(-) create mode 100644 contracts/account/README.adoc diff --git a/contracts/account/README.adoc b/contracts/account/README.adoc new file mode 100644 index 00000000..c00b9ffb --- /dev/null +++ b/contracts/account/README.adoc @@ -0,0 +1,15 @@ += Account +[.readme-notice] +NOTE: This document is better viewed at https://docs.openzeppelin.com/community-contracts/api/account + +This directory includes contracts to build accounts for ERC-4337. + +== Core + +{{AccountCore}} + +{{Account}} + +== Extensions + +{{AccountSignerERC7702}} diff --git a/contracts/account/draft-Account.sol b/contracts/account/draft-Account.sol index 3b8a25bb..b849032f 100644 --- a/contracts/account/draft-Account.sol +++ b/contracts/account/draft-Account.sol @@ -10,10 +10,10 @@ import {AccountCore} from "./draft-AccountCore.sol"; /** * @dev Extension of {AccountCore} with recommended feature that most account abstraction implementation will want: * - * * {ERC721Holder} for ERC-721 token handling - * * {ERC1155Holder} for ERC-1155 token handling + * * {ERC721Holder} and {ERC1155Holder} to accept ERC-712 and ERC-1155 token transfers transfers. * * {ERC7739Signer} for ERC-1271 signature support with ERC-7739 replay protection * - * NOTE: This needs to be combine with a signer implementation such as {SignerECDSA}, {SignerP256} or {SignerRSA} + * NOTE: To use this contract, the {ERC7739Signer-_rawSignatureValidation} function must be + * implemented using a specific signature verification algorithm. See {SignerECDSA}, {SignerP256} or {SignerRSA}. */ abstract contract Account is AccountCore, ERC721Holder, ERC1155Holder, ERC7739Signer {} diff --git a/contracts/account/draft-AccountCore.sol b/contracts/account/draft-AccountCore.sol index c4876103..3ac42550 100644 --- a/contracts/account/draft-AccountCore.sol +++ b/contracts/account/draft-AccountCore.sol @@ -13,10 +13,10 @@ import {AbstractSigner} from "../utils/cryptography/AbstractSigner.sol"; * @dev A simple ERC4337 account implementation. This base implementation only includes the minimal logic to process * user operations. * - * Developers must implement the {_rawSignatureValidation} function to define the account's validation logic. + * Developers must implement the {AbstractSigner-_rawSignatureValidation} function to define the account's validation logic. * * IMPORTANT: Implementing a mechanism to validate signatures is a security-sensitive operation as it may allow an - * attacker to bypass the account's security measures. Check out {AccountECDSA}, {AccountP256}, or {AccountRSA} for + * attacker to bypass the account's security measures. Check out {SignerECDSA}, {SignerP256}, or {SignerRSA} for * digital signature validation implementations. */ abstract contract AccountCore is AbstractSigner, EIP712, IAccount, IAccountExecute { diff --git a/contracts/account/extensions/AccountSignerERC7702.sol b/contracts/account/extensions/AccountSignerERC7702.sol index f74416a4..879939cd 100644 --- a/contracts/account/extensions/AccountSignerERC7702.sol +++ b/contracts/account/extensions/AccountSignerERC7702.sol @@ -5,9 +5,12 @@ pragma solidity ^0.8.20; import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import {AccountCore} from "../draft-AccountCore.sol"; +/** + * @dev {Account} implementation whose low-level signature validation is done by an EOA. + */ abstract contract AccountSignerERC7702 is AccountCore { /** - * @dev Validates the signature using the account's address. + * @dev Validates the signature using the EOA's address (ie. `address(this)`). */ function _rawSignatureValidation( bytes32 hash, diff --git a/contracts/utils/README.adoc b/contracts/utils/README.adoc index d6e6a0f0..e5649388 100644 --- a/contracts/utils/README.adoc +++ b/contracts/utils/README.adoc @@ -5,16 +5,26 @@ NOTE: This document is better viewed at https://docs.openzeppelin.com/community- Miscellaneous contracts and libraries containing utility functions you can use to improve security, work with new data types, or safely use low-level primitives. - * {Masks}: Library to handle `bytes32` masks. - * {ERC7739Utils}: Utilities library that implements a defensive rehashing mechanism to prevent replayability of smart contract signatures based on ERC-7739. + * {AbstractSigner}: Abstract contract for internal signature validation in smart contracts. * {ERC7739Signer}: An abstract contract to validate signatures following the rehashing scheme from `ERC7739Utils`. + * {ERC7739Utils}: Utilities library that implements a defensive rehashing mechanism to prevent replayability of smart contract signatures based on ERC-7739. + * {SignerECDSA}, {SignerP256}, {SignerRSA}: Implementations of an {AbstractSigner} with specific signature validation algorithms. + * {Masks}: Library to handle `bytes32` masks. == Cryptography +{{AbstractSigner}} + {{ERC7739Signer}} {{ERC7739Utils}} +{{SignerECDSA}} + +{{SignerP256}} + +{{SignerRSA}} + == Libraries {{Masks}} diff --git a/contracts/utils/cryptography/AbstractSigner.sol b/contracts/utils/cryptography/AbstractSigner.sol index db1bfaf3..6f109f39 100644 --- a/contracts/utils/cryptography/AbstractSigner.sol +++ b/contracts/utils/cryptography/AbstractSigner.sol @@ -2,13 +2,19 @@ pragma solidity ^0.8.20; +/** + * @dev Abstract contract for signature validation. + * + * Developers must implement {_rawSignatureValidation} and use it as the lowest-level signature validation mechanism. + */ abstract contract AbstractSigner { /** * @dev Signature validation algorithm. * * WARNING: Implementing a signature validation algorithm is a security-sensitive operation as it involves * cryptographic verification. It is important to review and test thoroughly before deployment. Consider - * using one of the signature verification libraries ({ECDSA}, {P256} or {RSA}). + * using one of the signature verification libraries (https://docs.openzeppelin.com/contracts/api/utils#ECDSA[ECDSA], + * https://docs.openzeppelin.com/contracts/api/utils#P256[P256] or https://docs.openzeppelin.com/contracts/api/utils#RSA[RSA]). */ function _rawSignatureValidation(bytes32 hash, bytes calldata signature) internal view virtual returns (bool); } diff --git a/contracts/utils/cryptography/SignerECDSA.sol b/contracts/utils/cryptography/SignerECDSA.sol index 10ab3607..d877b3f2 100644 --- a/contracts/utils/cryptography/SignerECDSA.sol +++ b/contracts/utils/cryptography/SignerECDSA.sol @@ -6,10 +6,11 @@ import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import {AbstractSigner} from "./AbstractSigner.sol"; /** - * @dev Implementation of {AbstractSigner} using {ECDSA} signatures. + * @dev Implementation of {AbstractSigner} using + * https://docs.openzeppelin.com/contracts/api/utils#ECDSA[ECDSA] signatures. * - * An {_initializeSigner} function is provided to set the account's signer address. Doing so it's - * easier for a factory, whose likely to use initializable clones of this contract. + * For {Account} usage, an {_initializeSigner} function is provided to set the {signer} address. + * Doing so it's easier for a factory, whose likely to use initializable clones of this contract. * * Example of usage: * @@ -25,34 +26,32 @@ import {AbstractSigner} from "./AbstractSigner.sol"; * ``` * * IMPORTANT: Avoiding to call {_initializeSigner} either during construction (if used standalone) - * or during initialization (if used as a clone) may leave the account either front-runnable or unusable. + * or during initialization (if used as a clone) may leave the signer either front-runnable or unusable. */ abstract contract SignerECDSA is AbstractSigner { /** * @dev The {signer} is already initialized. */ - error AccountECDSAUninitializedSigner(address signer); + error SignerECDSAUninitializedSigner(address signer); address private _signer; /** - * @dev Initializes the account with the address of the native signer. This function can be called only once. + * @dev Initializes the signer with the address of the native signer. This function can be called only once. */ function _initializeSigner(address signerAddr) internal { - if (_signer != address(0)) revert AccountECDSAUninitializedSigner(signerAddr); + if (_signer != address(0)) revert SignerECDSAUninitializedSigner(signerAddr); _signer = signerAddr; } /** - * @dev Return the account's signer address. + * @dev Return the signer's address. */ function signer() public view virtual returns (address) { return _signer; } - /** - * @dev Validates the signature using the account's signer. - */ + // @inheritdoc AbstractSigner function _rawSignatureValidation( bytes32 hash, bytes calldata signature diff --git a/contracts/utils/cryptography/SignerP256.sol b/contracts/utils/cryptography/SignerP256.sol index 2fe004d0..175c79ab 100644 --- a/contracts/utils/cryptography/SignerP256.sol +++ b/contracts/utils/cryptography/SignerP256.sol @@ -6,10 +6,11 @@ import {P256} from "@openzeppelin/contracts/utils/cryptography/P256.sol"; import {AbstractSigner} from "./AbstractSigner.sol"; /** - * @dev Implementation of {AbstractSigner} using {P256} signatures. + * @dev Implementation of {AbstractSigner} using + * https://docs.openzeppelin.com/contracts/api/utils#P256[P256] signatures. * - * An {_initializeSigner} function is provided to set the account's signer address. Doing so it's - * easier for a factory, whose likely to use initializable clones of this contract. + * For {Account} usage, an {_initializeSigner} function is provided to set the {signer} public key. + * Doing so it's easier for a factory, whose likely to use initializable clones of this contract. * * Example of usage: * @@ -25,36 +26,34 @@ import {AbstractSigner} from "./AbstractSigner.sol"; * ``` * * IMPORTANT: Avoiding to call {_initializeSigner} either during construction (if used standalone) - * or during initialization (if used as a clone) may leave the account either front-runnable or unusable. + * or during initialization (if used as a clone) may leave the signer either front-runnable or unusable. */ abstract contract SignerP256 is AbstractSigner { /** * @dev The {signer} is already initialized. */ - error AccountP256UninitializedSigner(bytes32 qx, bytes32 qy); + error SignerP256UninitializedSigner(bytes32 qx, bytes32 qy); bytes32 private _qx; bytes32 private _qy; /** - * @dev Initializes the account with the P256 public key. This function can be called only once. + * @dev Initializes the signer with the P256 public key. This function can be called only once. */ function _initializeSigner(bytes32 qx, bytes32 qy) internal { - if (_qx != 0 || _qy != 0) revert AccountP256UninitializedSigner(qx, qy); + if (_qx != 0 || _qy != 0) revert SignerP256UninitializedSigner(qx, qy); _qx = qx; _qy = qy; } /** - * @dev Return the account's signer P256 public key. + * @dev Return the signer's P256 public key. */ function signer() public view virtual returns (bytes32 qx, bytes32 qy) { return (_qx, _qy); } - /** - * @dev Validates the signature using the account's signer. - */ + /// @inheritdoc AbstractSigner function _rawSignatureValidation( bytes32 hash, bytes calldata signature diff --git a/contracts/utils/cryptography/SignerRSA.sol b/contracts/utils/cryptography/SignerRSA.sol index 03c2cb72..c93f22c4 100644 --- a/contracts/utils/cryptography/SignerRSA.sol +++ b/contracts/utils/cryptography/SignerRSA.sol @@ -6,10 +6,11 @@ import {RSA} from "@openzeppelin/contracts/utils/cryptography/RSA.sol"; import {AbstractSigner} from "./AbstractSigner.sol"; /** - * @dev Implementation of {AbstractSigner} using {RSA} signatures. + * @dev Implementation of {AbstractSigner} using + * https://docs.openzeppelin.com/contracts/api/utils#RSA[RSA] signatures. * - * An {_initializeSigner} function is provided to set the account's signer address. Doing so it's - * easier for a factory, whose likely to use initializable clones of this contract. + * For {Account} usage, an {_initializeSigner} function is provided to set the {signer} public key. + * Doing so it's easier for a factory, whose likely to use initializable clones of this contract. * * Example of usage: * @@ -25,36 +26,34 @@ import {AbstractSigner} from "./AbstractSigner.sol"; * ``` * * IMPORTANT: Avoiding to call {_initializeSigner} either during construction (if used standalone) - * or during initialization (if used as a clone) may leave the account either front-runnable or unusable. + * or during initialization (if used as a clone) may leave the signer either front-runnable or unusable. */ abstract contract SignerRSA is AbstractSigner { /** * @dev The {signer} is already initialized. */ - error AccountRSAUninitializedSigner(bytes e, bytes n); + error SignerRSAUninitializedSigner(bytes e, bytes n); bytes private _e; bytes private _n; /** - * @dev Initializes the account with the RSA public key. This function can be called only once. + * @dev Initializes the signer with the RSA public key. This function can be called only once. */ function _initializeSigner(bytes memory e, bytes memory n) internal { - if (_e.length != 0 || _n.length != 0) revert AccountRSAUninitializedSigner(e, n); + if (_e.length != 0 || _n.length != 0) revert SignerRSAUninitializedSigner(e, n); _e = e; _n = n; } /** - * @dev Return the account's signer RSA public key. + * @dev Return the signer's RSA public key. */ function signer() public view virtual returns (bytes memory e, bytes memory n) { return (_e, _n); } - /** - * @dev Validates the signature using the account's signer. - */ + /// @inheritdoc AbstractSigner function _rawSignatureValidation( bytes32 hash, bytes calldata signature diff --git a/contracts/utils/cryptography/draft-ERC7739Signer.sol b/contracts/utils/cryptography/draft-ERC7739Signer.sol index 6775f32f..61e820ca 100644 --- a/contracts/utils/cryptography/draft-ERC7739Signer.sol +++ b/contracts/utils/cryptography/draft-ERC7739Signer.sol @@ -18,9 +18,11 @@ import {ERC7739Utils} from "./draft-ERC7739Utils.sol"; * This contract requires implementing the {_rawSignatureValidation} function, which passes the wrapped message hash, * which may be either an typed data or a personal sign nested type. * - * NOTE: {EIP712} uses {ShortStrings} to optimize gas costs for short strings (up to 31 characters). - * Consider that strings longer than that will use storage, which may limit the ability of the signer to - * be used within the ERC-4337 validation phase (due to ERC-7562 storage access rules). + * NOTE: https://docs.openzeppelin.com/contracts/api/utils#EIP712[EIP-712] uses + * https://docs.openzeppelin.com/contracts/api/utils#ShortStrings[ShortStrings] to optimize gas costs for + * short strings (up to 31 characters). Consider that strings longer than that will use storage, which + * may limit the ability of the signer to be used within the ERC-4337 validation phase (due to + * https://eips.ethereum.org/EIPS/eip-7562#storage-rules[ERC-7562 storage access rules]). */ abstract contract ERC7739Signer is AbstractSigner, EIP712, IERC1271 { using ERC7739Utils for *; diff --git a/contracts/utils/cryptography/draft-ERC7739Utils.sol b/contracts/utils/cryptography/draft-ERC7739Utils.sol index 320ac4bd..eed6acd2 100644 --- a/contracts/utils/cryptography/draft-ERC7739Utils.sol +++ b/contracts/utils/cryptography/draft-ERC7739Utils.sol @@ -7,18 +7,20 @@ pragma solidity ^0.8.20; * that are specific to an EIP-712 domain. * * This library provides methods to wrap, unwrap and operate over typed data signatures with a defensive - * rehashing mechanism that includes the application's {EIP712-_domainSeparatorV4} and preserves - * readability of the signed content using an EIP-712 nested approach. + * rehashing mechanism that includes the application's + * https://docs.openzeppelin.com/contracts/api/utils#EIP712-_domainSeparatorV4[EIP-712] + * and preserves readability of the signed content using an EIP-712 nested approach. * * A smart contract domain can validate a signature for a typed data structure in two ways: * - * - As an application validating a typed data signature. See {toNestedTypedDataHash}. - * - As a smart contract validating a raw message signature. See {toNestedPersonalSignHash}. + * - As an application validating a typed data signature. See {typedDataSignStructHash}. + * - As a smart contract validating a raw message signature. See {personalSignStructHash}. * * NOTE: A provider for a smart contract wallet would need to return this signature as the * result of a call to `personal_sign` or `eth_signTypedData`, and this may be unsupported by * API clients that expect a return value of 129 bytes, or specifically the `r,s,v` parameters - * of an {ECDSA} signature, as is for example specified for {EIP712}. + * of an https://docs.openzeppelin.com/contracts/api/utils#ECDSA[ECDSA] signature, as is for + * example specified for https://docs.openzeppelin.com/contracts/api/utils#EIP712[EIP-712]. */ library ERC7739Utils { /** From 5c210cf8078faab86ca32f45cb5e7952ba7c3815 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Mon, 16 Dec 2024 23:50:18 -0600 Subject: [PATCH 51/51] Update CHANGELOG.md Co-authored-by: Hadrien Croubois --- CHANGELOG.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f998e75d..504b6880 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,9 @@ ## XX-XX-2024 -- `AccountECDSA`, `AccountP256` and `AccountRSA`: Add implementations of `AccountBase` based on the available signature schemes (i.e. ECDSA, P256, RSA). -- `AccountBase`: Added a simple ERC-4337 account implementation with the minimal logic to process user operations. +- `AccountCore`: Added a simple ERC-4337 account implementation with minimal logic to process user operations. +- `Account`: Extensions of {AccountCore} with recommended features that most accounts should have. +- `AbstractSigner`, `SignerECDSA`, `SignerP256`, and `SignerRSA`: Add an abstract contract, and various implementations, for contracts that deal with signature verification. Used by {AccountCore} and {ERC7739Utils}. +- `AccountSignerERC7702`: Implementation of `AbstractSigner` for ERC-7702 compatible accounts. ## 06-11-2024