diff --git a/contracts/ClaimIssuer.sol b/contracts/ClaimIssuer.sol index 4238ec3..8b6ed35 100644 --- a/contracts/ClaimIssuer.sol +++ b/contracts/ClaimIssuer.sol @@ -144,6 +144,9 @@ contract ClaimIssuer is IClaimIssuer, Identity { // Initialize UUPS upgradeability __UUPSUpgradeable_init(); + // Initialize IdentitySmartAccount functionality + __IdentitySmartAccount_init(); + // Initialize Identity functionality __Identity_init(initialManagementKey); diff --git a/contracts/Identity.sol b/contracts/Identity.sol index 8180355..b46ad67 100644 --- a/contracts/Identity.sol +++ b/contracts/Identity.sol @@ -5,6 +5,15 @@ import { MulticallUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { IdentitySmartAccount } from "./IdentitySmartAccount.sol"; +import { + SIG_VALIDATION_FAILED, + SIG_VALIDATION_SUCCESS +} from "@account-abstraction/contracts/core/Helpers.sol"; +import { PackedUserOperation } from "@account-abstraction/contracts/interfaces/PackedUserOperation.sol"; +import { IEntryPoint } from "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { Exec } from "@account-abstraction/contracts/utils/Exec.sol"; import { IIdentity } from "./interface/IIdentity.sol"; import { IClaimIssuer } from "./interface/IClaimIssuer.sol"; import { IERC734 } from "./interface/IERC734.sol"; @@ -12,7 +21,6 @@ import { IERC735 } from "./interface/IERC735.sol"; import { Version } from "./version/Version.sol"; import { Errors } from "./libraries/Errors.sol"; import { KeyPurposes } from "./libraries/KeyPurposes.sol"; -import { KeyTypes } from "./libraries/KeyTypes.sol"; import { Structs } from "./storage/Structs.sol"; import { KeyManager } from "./KeyManager.sol"; @@ -41,6 +49,7 @@ contract Identity is Initializable, UUPSUpgradeable, IIdentity, + IdentitySmartAccount, Version, KeyManager, MulticallUpgradeable @@ -112,6 +121,44 @@ contract Identity is } } + /** + * @dev See {IERC734-execute}. + * @notice Executes a single call from the account (ERC-734 compatible) + */ + function execute( + address _to, + uint256 _value, + bytes calldata _data + ) + external + payable + virtual + override(IERC734, KeyManager) + returns (uint256 executionId) + { + // Allow entry point calls + if (msg.sender == address(entryPoint())) { + // For entry point calls, use direct execution + _executeDirect(_to, _value, _data); + return 0; // Return 0 for entry point calls + } + + // For regular calls, use KeyManager's execution logic + KeyStorage storage ks = _getKeyStorage(); + executionId = ks.executionNonce; + ks.executions[executionId].to = _to; + ks.executions[executionId].value = _value; + ks.executions[executionId].data = _data; + ks.executionNonce++; + + emit ExecutionRequested(executionId, _to, _value, _data); + + // Check if execution can be auto-approved + if (_canAutoApproveExecution(_to)) { + _approve(executionId, true); + } + } + /** * @notice When using this contract as an implementation for a proxy, call this initializer with a delegatecall. * @dev This function initializes the upgradeable contract and sets up the initial management key. @@ -123,6 +170,7 @@ contract Identity is ) external virtual initializer { require(initialManagementKey != address(0), Errors.ZeroAddress()); __UUPSUpgradeable_init(); + __IdentitySmartAccount_init(); __Identity_init(initialManagementKey); __Version_init("2.2.2"); } @@ -438,7 +486,12 @@ contract Identity is */ function _authorizeUpgrade( address newImplementation - ) internal virtual override onlyManager { + ) + internal + virtual + override(IdentitySmartAccount, UUPSUpgradeable) + onlyManager + { // Only management keys can authorize upgrades // This prevents unauthorized upgrades and potential rug pulls } @@ -554,6 +607,88 @@ contract Identity is } } + /** + * @dev Internal function for direct execution (used by entry point) + * @param _to The target address + * @param _value The value to send + * @param _data The calldata + * + * @notice Uses the professional Exec library pattern from BaseAccount for consistent + * and gas-optimized execution with proper error handling. + */ + function _executeDirect( + address _to, + uint256 _value, + bytes calldata _data + ) internal { + bool ok = Exec.call(_to, _value, _data, gasleft()); + if (!ok) { + Exec.revertWithReturnData(); + } + } + + /** + * @dev See {IdentitySmartAccount-_requireManager}. + * @notice Requires the caller to have management permissions + */ + function _requireManager() internal view override onlyManager { + // The onlyManager modifier handles the access control + } + + /** + * @dev See {IdentitySmartAccount-_validateSignature}. + * @notice Validates the signature of a UserOperation and the signer's permissions + * This function performs complete validation: + * 1. Recovers the signer address from the signature + * 2. Validates that the signature is valid (not address(0)) + * 3. Validates that the signer has required permissions (ERC4337_SIGNER or MANAGEMENT) + * @param userOp The UserOperation to validate + * @param userOpHash The hash of the UserOperation + * @return validationData Packed validation data (0 for success, 1 for signature/permission failure) + */ + function _validateSignature( + PackedUserOperation calldata userOp, + bytes32 userOpHash + ) internal view override returns (uint256 validationData) { + // 1. Recover the signer address from the signature + address signer = ECDSA.recover(userOpHash, userOp.signature); + + // 2. Validate that the signature is valid + if (signer == address(0)) { + return SIG_VALIDATION_FAILED; + } + + // 3. Validate that the signer has required permissions + if ( + !keyHasPurpose( + keccak256(abi.encode(signer)), + KeyPurposes.ERC4337_SIGNER + ) && + !keyHasPurpose( + keccak256(abi.encode(signer)), + KeyPurposes.MANAGEMENT + ) + ) { + return SIG_VALIDATION_FAILED; + } + + return SIG_VALIDATION_SUCCESS; + } + + /** + * @dev See {IdentitySmartAccount-_requireForExecute}. + * @notice Requires the caller to be authorized for execution + */ + function _requireForExecute() internal view override { + // Allow entry point calls + if (msg.sender == address(entryPoint())) { + return; + } + + // For all other calls, require management permissions + _requireManager(); + } + /** * @dev Internal helper to validate claim with external issuer. * diff --git a/contracts/IdentitySmartAccount.sol b/contracts/IdentitySmartAccount.sol new file mode 100644 index 0000000..bda5e63 --- /dev/null +++ b/contracts/IdentitySmartAccount.sol @@ -0,0 +1,318 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.27; + +import { IAccount } from "@account-abstraction/contracts/interfaces/IAccount.sol"; +import { IEntryPoint } from "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; +import { PackedUserOperation } from "@account-abstraction/contracts/interfaces/PackedUserOperation.sol"; +import { SIG_VALIDATION_SUCCESS } from "@account-abstraction/contracts/core/Helpers.sol"; +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { Exec } from "@account-abstraction/contracts/utils/Exec.sol"; + +/** + * @title IdentitySmartAccount + * @author OnChainID Team + * @notice Abstract contract providing ERC-4337 Account Abstraction functionality for Identity contracts + * @dev Abstract contract providing ERC-4337 Account Abstraction functionality for Identity contracts + * + * This contract handles: + * - UserOperation validation and execution + * - Entry point management + * - Nonce management for UserOperations + * - Missing funds handling + * - Replay attack prevention + * + * @custom:security This contract uses ERC-7201 storage slots to prevent storage collision attacks + * in upgradeable contracts. + */ +abstract contract IdentitySmartAccount is + IAccount, + Initializable, + UUPSUpgradeable +{ + struct Call { + address target; + uint256 value; + bytes data; + } + + /** + * @dev Storage struct for ERC-4337 Account Abstraction data + * @custom:storage-location erc7201:onchainid.identity.smartaccount.storage + */ + struct SmartAccountStorage { + /// @dev Entry point contract address for UserOperations + IEntryPoint entryPoint; + } + + /** + * @dev Hardcoded Entry Point addresses per network + * Official ERC-4337 Entry Point v0.6: 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789 + */ + IEntryPoint internal constant _DEFAULT_ENTRY_POINT = + IEntryPoint(0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789); + + /** + * @dev ERC-7201 Storage Slot for ERC-4337 Account Abstraction data + * This slot ensures no storage collision between different versions of the contract + * + * Formula: keccak256(abi.encode(uint256(keccak256(bytes(id))) - 1)) & ~bytes32(uint256(0xff)) + * where id is the namespace identifier + */ + bytes32 internal constant _ENTRYPOINT_STORAGE_SLOT = + keccak256( + abi.encode( + uint256( + keccak256(bytes("onchainid.identity.smartaccount.storage")) + ) - 1 + ) + ) & ~bytes32(uint256(0xff)); + + // ========= Events ========= + + event EntryPointSet(address indexed entryPoint); + + // ========= Errors ========= + + error ExecuteError(uint256 index, bytes error); + + // ========= Modifiers ========= + + /** + * @notice Requires the caller to be the entry point + */ + modifier onlyEntryPoint() { + require( + msg.sender == address(entryPoint()), + "IdentitySmartAccount: not from EntryPoint" + ); + _; + } + + /** + * @dev Execute a batch of calls from the account (ERC-4337 standard) + * @param calls Array of calls to execute + */ + function executeBatch(Call[] calldata calls) external virtual { + _requireForExecute(); + + uint256 callsLength = calls.length; + for (uint256 i = 0; i < callsLength; ++i) { + Call calldata call = calls[i]; + bool ok = Exec.call(call.target, call.value, call.data, gasleft()); + if (!ok) { + if (callsLength == 1) { + Exec.revertWithReturnData(); + } else { + revert ExecuteError(i, Exec.getReturnData(0)); + } + } + } + } + + // ========= Public Functions ========= + + /** + * @dev See {IAccount-validateUserOp}. + * @notice Validates a UserOperation for ERC-4337 Account Abstraction + * + * This function validates: + * 1. The caller is the entry point + * 2. The UserOperation signature is valid + * 3. The signer has appropriate permissions (must be implemented by inheriting contract) + * 4. The nonce is correct + * 5. Handles missing account funds + * + * @param userOp The UserOperation to validate + * @param userOpHash The hash of the UserOperation + * @param missingAccountFunds Missing funds that need to be deposited + * @return validationData Packed validation data (0 for success, 1 for signature failure) + */ + function validateUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash, + uint256 missingAccountFunds + ) + external + virtual + override(IAccount) + onlyEntryPoint + returns (uint256 validationData) + { + // 1. Validate signature and permissions in one step + validationData = _validateSignature(userOp, userOpHash); + if (validationData != SIG_VALIDATION_SUCCESS) { + return validationData; + } + + // 2. Validate nonce + _validateNonce(userOp.nonce); + + // 3. Handle missing funds + _payPrefund(missingAccountFunds); + + return SIG_VALIDATION_SUCCESS; + } + + /** + * @dev Sets the entry point contract address + * @param _entryPoint The new entry point contract address + */ + function setEntryPoint(IEntryPoint _entryPoint) external virtual { + _requireManager(); + _setEntryPoint(_entryPoint); + } + + /** + * @dev Deposit more funds for this account in the entryPoint + */ + function addDeposit() public payable virtual { + _requireManager(); + entryPoint().depositTo{ value: msg.value }(address(this)); + } + + /** + * @dev Withdraw value from the account's deposit + * @param withdrawAddress target to send to + * @param amount to withdraw + */ + function withdrawDepositTo( + address payable withdrawAddress, + uint256 amount + ) public virtual { + _requireManager(); + entryPoint().withdrawTo(withdrawAddress, amount); + } + + /** + * @dev Check current account deposit in the entryPoint + * @return The current deposit amount + */ + function getDeposit() public view virtual returns (uint256) { + return entryPoint().balanceOf(address(this)); + } + + /** + * @dev Returns the entry point contract address + * @return The entry point contract address + */ + function entryPoint() public view virtual returns (IEntryPoint) { + return _getSmartAccountStorage().entryPoint; + } + + /** + * @dev Returns the current UserOperation nonce from the entry point + * @return The current UserOperation nonce + */ + function getNonce() public view virtual returns (uint256) { + return entryPoint().getNonce(address(this), 0); + } + + /** + * @dev Validates the signature of a UserOperation and the signer's permissions + * Must be implemented by inheriting contract + * This function should: + * 1. Recover the signer address from the signature + * 2. Validate that the signature is valid (not address(0)) + * 3. Validate that the signer has required permissions + * @param userOp The UserOperation to validate + * @param userOpHash The hash of the UserOperation + * @return validationData Packed validation data (0 for success, 1 for signature/permission failure) + */ + function _validateSignature( + PackedUserOperation calldata userOp, + bytes32 userOpHash + ) internal virtual returns (uint256 validationData); + + /** + * @dev Handles missing account funds by depositing to entry point + * @param missingAccountFunds The amount of missing funds + */ + function _payPrefund(uint256 missingAccountFunds) internal virtual { + if (missingAccountFunds > 0) { + entryPoint().depositTo{ value: missingAccountFunds }(address(this)); + } + } + + /** + * @dev Sets the entry point contract address + * @param _entryPoint The new entry point contract address + */ + function _setEntryPoint(IEntryPoint _entryPoint) internal virtual { + require( + address(_entryPoint) != address(0), + "IdentitySmartAccount: zero address" + ); + _getSmartAccountStorage().entryPoint = _entryPoint; + emit EntryPointSet(address(_entryPoint)); + } + + // ========= Initialization ========= + + /** + * @dev Initializes the ERC-4337 functionality with default entry point + */ + // solhint-disable-next-line func-name-mixedcase + function __IdentitySmartAccount_init() internal onlyInitializing { + __UUPSUpgradeable_init(); + _getSmartAccountStorage().entryPoint = _DEFAULT_ENTRY_POINT; + emit EntryPointSet(address(_DEFAULT_ENTRY_POINT)); + } + + /** + * @dev Reinitializes the ERC-4337 functionality for upgrades + * @param versionNumber The version number for the reinitializer modifier + */ + // solhint-disable-next-line func-name-mixedcase + function __IdentitySmartAccount_init_unchained( + uint8 versionNumber + ) internal reinitializer(versionNumber) { + _getSmartAccountStorage().entryPoint = _DEFAULT_ENTRY_POINT; + emit EntryPointSet(address(_DEFAULT_ENTRY_POINT)); + } + + /** + * @dev Internal function to authorize the upgrade of the contract. + * This function is required by UUPSUpgradeable. + * @param newImplementation The address of the new implementation. + */ + function _authorizeUpgrade( + address newImplementation + ) internal virtual override {} + + /** + * @dev Requires the caller to have management permissions + * Must be implemented by inheriting contract + */ + function _requireManager() internal view virtual; + + /** + * @dev Requires the caller to be authorized for execution + * Must be implemented by inheriting contract for custom execution requirements + */ + function _requireForExecute() internal view virtual; + + /** + * @dev Validates the nonce of the UserOperation + * Can be overridden by inheriting contract for custom nonce validation + * @param nonce The nonce to validate + */ + function _validateNonce(uint256 nonce) internal view virtual { + require(nonce == getNonce(), "Invalid nonce"); + } + + /** + * @dev Returns the SmartAccount storage struct at the specified ERC-7201 slot + * @return s The SmartAccountStorage struct pointer for the smart account management slot + */ + function _getSmartAccountStorage() + internal + pure + returns (SmartAccountStorage storage s) + { + bytes32 slot = _ENTRYPOINT_STORAGE_SLOT; + assembly { + s.slot := slot + } + } +} diff --git a/contracts/libraries/KeyPurposes.sol b/contracts/libraries/KeyPurposes.sol index 60b779d..5cf347c 100644 --- a/contracts/libraries/KeyPurposes.sol +++ b/contracts/libraries/KeyPurposes.sol @@ -15,4 +15,7 @@ library KeyPurposes { /// @dev 4: ENCRYPTION keys, used to encrypt data e.g. hold in claims. uint256 internal constant ENCRYPTION = 4; + + /// @dev 5: ERC4337_SIGNER keys, used to sign UserOperations for ERC-4337 Account Abstraction + uint256 internal constant ERC4337_SIGNER = 5; } diff --git a/hardhat.config.ts b/hardhat.config.ts index 2ab69b8..60d4bb6 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -13,13 +13,26 @@ import "./tasks/revoke.task"; const config: HardhatUserConfig = { solidity: { - version: "0.8.27", - settings: { - optimizer: { - enabled: true, - runs: 200, + compilers: [ + { + version: "0.8.27", + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + }, }, - }, + { + version: "0.8.28", + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + }, + }, + ], }, networks: { mumbai: { diff --git a/package-lock.json b/package-lock.json index bbc77b1..8d12614 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "2.2.2-beta3", "license": "ISC", "dependencies": { + "@account-abstraction/contracts": "^0.8.0", "@openzeppelin/contracts-upgradeable": "^4.9.6", "eslint-plugin-import": "^2.32.0", "eslint-plugin-node": "^11.1.0" @@ -32,6 +33,16 @@ "solidity-coverage": "^0.8.14" } }, + "node_modules/@account-abstraction/contracts": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@account-abstraction/contracts/-/contracts-0.8.0.tgz", + "integrity": "sha512-8krPx/gpnoT+5xAroagVCbeA7FbUigMZWXFKKPm+oghyr29Dksssdx5sI7xGv9212i4JPaDDUGFk58dpuwVgHA==", + "license": "MIT", + "dependencies": { + "@openzeppelin/contracts": "^5.1.0", + "@uniswap/v3-periphery": "^1.4.3" + } + }, "node_modules/@adraffy/ens-normalize": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", @@ -1619,7 +1630,6 @@ "version": "5.4.0", "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-5.4.0.tgz", "integrity": "sha512-eCYgWnLg6WO+X52I16TZt8uEjbtdkgLC0SUX/xnAksjjrQI4Xfn4iBRoI5j55dmlOhDv1Y7BoR3cU7e3WWhC6A==", - "dev": true, "license": "MIT" }, "node_modules/@openzeppelin/contracts-upgradeable": { @@ -2178,6 +2188,55 @@ "@types/node": "*" } }, + "node_modules/@uniswap/lib": { + "version": "4.0.1-alpha", + "resolved": "https://registry.npmjs.org/@uniswap/lib/-/lib-4.0.1-alpha.tgz", + "integrity": "sha512-f6UIliwBbRsgVLxIaBANF6w09tYqc6Y/qXdsrbEmXHyFA7ILiKrIwRFXe1yOg8M3cksgVsO9N7yuL2DdCGQKBA==", + "license": "GPL-3.0-or-later", + "engines": { + "node": ">=10" + } + }, + "node_modules/@uniswap/v2-core": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@uniswap/v2-core/-/v2-core-1.0.1.tgz", + "integrity": "sha512-MtybtkUPSyysqLY2U210NBDeCHX+ltHt3oADGdjqoThZaFRDKwM6k1Nb3F0A3hk5hwuQvytFWhrWHOEq6nVJ8Q==", + "license": "GPL-3.0-or-later", + "engines": { + "node": ">=10" + } + }, + "node_modules/@uniswap/v3-core": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@uniswap/v3-core/-/v3-core-1.0.1.tgz", + "integrity": "sha512-7pVk4hEm00j9tc71Y9+ssYpO6ytkeI0y7WE9P6UcmNzhxPePwyAxImuhVsTqWK9YFvzgtvzJHi64pBl4jUzKMQ==", + "license": "BUSL-1.1", + "engines": { + "node": ">=10" + } + }, + "node_modules/@uniswap/v3-periphery": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@uniswap/v3-periphery/-/v3-periphery-1.4.4.tgz", + "integrity": "sha512-S4+m+wh8HbWSO3DKk4LwUCPZJTpCugIsHrWR86m/OrUyvSqGDTXKFfc2sMuGXCZrD1ZqO3rhQsKgdWg3Hbb2Kw==", + "license": "GPL-2.0-or-later", + "dependencies": { + "@openzeppelin/contracts": "3.4.2-solc-0.7", + "@uniswap/lib": "^4.0.1-alpha", + "@uniswap/v2-core": "^1.0.1", + "@uniswap/v3-core": "^1.0.0", + "base64-sol": "1.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@uniswap/v3-periphery/node_modules/@openzeppelin/contracts": { + "version": "3.4.2-solc-0.7", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-3.4.2-solc-0.7.tgz", + "integrity": "sha512-W6QmqgkADuFcTLzHL8vVoNBtkwjvQRpYIAom7KiUNoLKghyx3FgH0GBjt8NRvigV1ZmMOBllvE1By1C+bi8WpA==", + "license": "MIT" + }, "node_modules/abbrev": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", @@ -2689,6 +2748,12 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/base64-sol": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/base64-sol/-/base64-sol-1.0.1.tgz", + "integrity": "sha512-ld3cCNMeXt4uJXmLZBHFGMvVpK9KsLVEhPpFRXnvSVAqABKbuNZg/+dsq3NuM+wxFLb/UrVkz7m1ciWmkMfTbg==", + "license": "MIT" + }, "node_modules/better-ajv-errors": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/better-ajv-errors/-/better-ajv-errors-2.0.2.tgz", diff --git a/package.json b/package.json index a869da9..705f6b1 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "solidity-coverage": "^0.8.14" }, "dependencies": { + "@account-abstraction/contracts": "^0.8.0", "@openzeppelin/contracts-upgradeable": "^4.9.6", "eslint-plugin-import": "^2.32.0", "eslint-plugin-node": "^11.1.0"