diff --git a/src/LightAccount.sol b/src/LightAccount.sol index 9e4b2e6..c06366c 100644 --- a/src/LightAccount.sol +++ b/src/LightAccount.sol @@ -6,18 +6,15 @@ pragma solidity ^0.8.23; /* solhint-disable reason-string */ import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; -import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import {SignatureChecker} from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; - -import {BaseAccount} from "account-abstraction/core/BaseAccount.sol"; import {SIG_VALIDATION_FAILED} from "account-abstraction/core/Helpers.sol"; import {IEntryPoint} from "account-abstraction/interfaces/IEntryPoint.sol"; import {PackedUserOperation} from "account-abstraction/interfaces/PackedUserOperation.sol"; -import {TokenCallbackHandler} from "account-abstraction/samples/callback/TokenCallbackHandler.sol"; -import {CustomSlotInitializable} from "./CustomSlotInitializable.sol"; +import {BaseLightAccount} from "./common/BaseLightAccount.sol"; +import {CustomSlotInitializable} from "./common/CustomSlotInitializable.sol"; /** * @title A simple ERC-4337 compatible smart contract account with a designated owner account @@ -49,7 +46,7 @@ import {CustomSlotInitializable} from "./CustomSlotInitializable.sol"; * * 5. Uses custom errors. */ -contract LightAccount is BaseAccount, TokenCallbackHandler, UUPSUpgradeable, CustomSlotInitializable, IERC1271 { +contract LightAccount is BaseLightAccount, CustomSlotInitializable { using ECDSA for bytes32; using MessageHashUtils for bytes32; @@ -58,14 +55,11 @@ contract LightAccount is BaseAccount, TokenCallbackHandler, UUPSUpgradeable, Cus // keccak256(abi.encode(uint256(keccak256("light_account_v1.initializable")) - 1)) & ~bytes32(uint256(0xff)); bytes32 internal constant _INITIALIZABLE_STORAGE_POSITION = 0x33e4b41198cc5b8053630ed667ea7c0c4c873f7fc8d9a478b5d7259cec0a4a00; - // bytes4(keccak256("isValidSignature(bytes32,bytes)")) - bytes4 internal constant _1271_MAGIC_VALUE = 0x1626ba7e; - IEntryPoint private immutable _ENTRY_POINT; - bytes32 private constant _DOMAIN_SEPARATOR_TYPEHASH = + bytes32 internal constant _DOMAIN_SEPARATOR_TYPEHASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); - bytes32 private constant _LA_MSG_TYPEHASH = keccak256("LightAccountMessage(bytes message)"); - bytes32 private constant _NAME_HASH = keccak256("LightAccount"); - bytes32 private constant _VERSION_HASH = keccak256("1"); + bytes32 internal constant _LA_MSG_TYPEHASH = keccak256("LightAccountMessage(bytes message)"); + bytes32 internal constant _NAME_HASH = keccak256("LightAccount"); + bytes32 internal constant _VERSION_HASH = keccak256("1"); struct LightAccountStorage { address owner; @@ -86,88 +80,27 @@ contract LightAccount is BaseAccount, TokenCallbackHandler, UUPSUpgradeable, Cus */ event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); - /** - * @dev The length of the array does not match the expected length. - */ - error ArrayLengthMismatch(); - /** * @dev The new owner is not a valid owner (e.g., `address(0)`, the * account itself, or the current owner). */ error InvalidOwner(address owner); - /** - * @dev The caller is not authorized. - */ - error NotAuthorized(address caller); - - modifier onlyOwner() { - _onlyOwner(); - _; - } - constructor(IEntryPoint anEntryPoint) CustomSlotInitializable(_INITIALIZABLE_STORAGE_POSITION) { _ENTRY_POINT = anEntryPoint; _disableInitializers(); } - // solhint-disable-next-line no-empty-blocks - receive() external payable {} - - /** - * @notice Execute a transaction. This may only be called directly by the - * owner or by the entry point via a user operation signed by the owner. - * @param dest The target of the transaction - * @param value The amount of wei sent in the transaction - * @param func The transaction's calldata - */ - function execute(address dest, uint256 value, bytes calldata func) external { - _requireFromEntryPointOrOwner(); - _call(dest, value, func); - } - - /** - * @notice Execute a sequence of transactions - * @param dest An array of the targets for each transaction in the sequence - * @param func An array of calldata for each transaction in the sequence. - * Must be the same length as dest, with corresponding elements representing - * the parameters for each transaction. - */ - function executeBatch(address[] calldata dest, bytes[] calldata func) external { - _requireFromEntryPointOrOwner(); - if (dest.length != func.length) { - revert ArrayLengthMismatch(); - } - uint256 length = dest.length; - for (uint256 i = 0; i < length;) { - _call(dest[i], 0, func[i]); - unchecked { - ++i; - } - } - } - /** - * @notice Execute a sequence of transactions - * @param dest An array of the targets for each transaction in the sequence - * @param value An array of value for each transaction in the sequence - * @param func An array of calldata for each transaction in the sequence. - * Must be the same length as dest, with corresponding elements representing - * the parameters for each transaction. + * @notice Called once as part of initialization, either during initial deployment or when first upgrading to + * this contract. + * @dev The _ENTRY_POINT member is immutable, to reduce gas consumption. To upgrade EntryPoint, + * a new implementation of LightAccount must be deployed with the new EntryPoint address, then upgrading + * the implementation by calling `upgradeTo()` + * @param anOwner The initial owner of the account */ - function executeBatch(address[] calldata dest, uint256[] calldata value, bytes[] calldata func) external { - _requireFromEntryPointOrOwner(); - if (dest.length != func.length || dest.length != value.length) { - revert ArrayLengthMismatch(); - } - uint256 length = dest.length; - for (uint256 i = 0; i < length;) { - _call(dest[i], value[i], func[i]); - unchecked { - ++i; - } - } + function initialize(address anOwner) public virtual initializer { + _initialize(anOwner); } /** @@ -183,39 +116,6 @@ contract LightAccount is BaseAccount, TokenCallbackHandler, UUPSUpgradeable, Cus _transferOwnership(newOwner); } - /** - * @notice Called once as part of initialization, either during initial deployment or when first upgrading to - * this contract. - * @dev The _ENTRY_POINT member is immutable, to reduce gas consumption. To upgrade EntryPoint, - * a new implementation of LightAccount must be deployed with the new EntryPoint address, then upgrading - * the implementation by calling `upgradeTo()` - * @param anOwner The initial owner of the account - */ - function initialize(address anOwner) public virtual initializer { - _initialize(anOwner); - } - - /** - * @notice Deposit more funds for this account in the entryPoint - */ - function addDeposit() public payable { - entryPoint().depositTo{value: msg.value}(address(this)); - } - - /** - * @notice Withdraw value from the account's deposit - * @param withdrawAddress Target to send to - * @param amount Amount to withdraw - */ - function withdrawDepositTo(address payable withdrawAddress, uint256 amount) public onlyOwner { - entryPoint().withdrawTo(withdrawAddress, amount); - } - - /// @inheritdoc BaseAccount - function entryPoint() public view virtual override returns (IEntryPoint) { - return _ENTRY_POINT; - } - /** * @notice Return the current owner of this account * @return The current owner @@ -224,14 +124,6 @@ contract LightAccount is BaseAccount, TokenCallbackHandler, UUPSUpgradeable, Cus return _getStorage().owner; } - /** - * @notice Check current account deposit in the entryPoint - * @return The current account deposit - */ - function getDeposit() public view returns (uint256) { - return entryPoint().balanceOf(address(this)); - } - /** * @notice Returns the domain separator for this contract, as defined in the EIP-712 standard. * @return bytes32 The domain separator hash. @@ -268,16 +160,16 @@ contract LightAccount is BaseAccount, TokenCallbackHandler, UUPSUpgradeable, Cus } /** + * @inheritdoc IERC1271 * @dev The signature is valid if it is signed by the owner's private key * (if the owner is an EOA) or if it is a valid ERC-1271 signature from the * owner (if the owner is a contract). Note that unlike the signature * validation used in `validateUserOp`, this does **not** wrap the digest in * an "Ethereum Signed Message" envelope before checking the signature in * the EOA-owner case. - * @inheritdoc IERC1271 */ - function isValidSignature(bytes32 digest, bytes memory signature) public view override returns (bytes4) { - bytes32 messageHash = getMessageHash(abi.encode(digest)); + function isValidSignature(bytes32 hash, bytes memory signature) public view override returns (bytes4) { + bytes32 messageHash = getMessageHash(abi.encode(hash)); if (SignatureChecker.isValidSignatureNow(owner(), messageHash, signature)) { return _1271_MAGIC_VALUE; } @@ -333,32 +225,8 @@ contract LightAccount is BaseAccount, TokenCallbackHandler, UUPSUpgradeable, Cus return SIG_VALIDATION_FAILED; } - function _onlyOwner() internal view { - //directly from EOA owner, or through the account itself (which gets redirected through execute()) - if (msg.sender != address(this) && msg.sender != owner()) { - revert NotAuthorized(msg.sender); - } - } - - // Require the function call went through EntryPoint or owner - function _requireFromEntryPointOrOwner() internal view { - if (msg.sender != address(entryPoint()) && msg.sender != owner()) { - revert NotAuthorized(msg.sender); - } - } - - function _call(address target, uint256 value, bytes memory data) internal { - (bool success, bytes memory result) = target.call{value: value}(data); - if (!success) { - assembly { - revert(add(result, 32), mload(result)) - } - } - } - - function _authorizeUpgrade(address newImplementation) internal view override { - (newImplementation); - _onlyOwner(); + function _isFromOwner() internal view virtual override returns (bool) { + return msg.sender == owner(); } function _getStorage() internal pure returns (LightAccountStorage storage storageStruct) { diff --git a/src/MultiOwnerLightAccount.sol b/src/MultiOwnerLightAccount.sol index da1345e..d660eec 100644 --- a/src/MultiOwnerLightAccount.sol +++ b/src/MultiOwnerLightAccount.sol @@ -2,31 +2,23 @@ pragma solidity ^0.8.23; import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; -import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import {SignatureChecker} from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; -import {BaseAccount} from "account-abstraction/core/BaseAccount.sol"; import {SIG_VALIDATION_FAILED} from "account-abstraction/core/Helpers.sol"; import {IEntryPoint} from "account-abstraction/interfaces/IEntryPoint.sol"; import {PackedUserOperation} from "account-abstraction/interfaces/PackedUserOperation.sol"; -import {TokenCallbackHandler} from "account-abstraction/samples/callback/TokenCallbackHandler.sol"; import {CastLib} from "modular-account/helpers/CastLib.sol"; import {SetValue} from "modular-account/libraries/Constants.sol"; import {LinkedListSet, LinkedListSetLib} from "modular-account/libraries/LinkedListSetLib.sol"; -import {CustomSlotInitializable} from "./CustomSlotInitializable.sol"; +import {BaseLightAccount} from "./common/BaseLightAccount.sol"; +import {CustomSlotInitializable} from "./common/CustomSlotInitializable.sol"; /// @title A simple ERC-4337 compatible smart contract account with one or more designated owner accounts. /// @dev Like LightAccount, but multiple owners are supported. The account is initialized with a list of owners, /// and the `updateOwners` method can be used to add or remove owners. -contract MultiOwnerLightAccount is - BaseAccount, - TokenCallbackHandler, - UUPSUpgradeable, - CustomSlotInitializable, - IERC1271 -{ +contract MultiOwnerLightAccount is BaseLightAccount, CustomSlotInitializable { using ECDSA for bytes32; using MessageHashUtils for bytes32; using LinkedListSetLib for LinkedListSet; @@ -38,14 +30,11 @@ contract MultiOwnerLightAccount is // keccak256(abi.encode(uint256(keccak256("multi_owner_light_account_v1.initializable")) - 1)) & ~bytes32(uint256(0xff)); bytes32 internal constant _INITIALIZABLE_STORAGE_POSITION = 0xaa296a366a62f6551d3ddfceae892d1791068a359a0d3461aab99dfc6c5fd700; - // bytes4(keccak256("isValidSignature(bytes32,bytes)")) - bytes4 internal constant _1271_MAGIC_VALUE = 0x1626ba7e; - IEntryPoint private immutable _ENTRY_POINT; - bytes32 private constant _DOMAIN_SEPARATOR_TYPEHASH = + bytes32 internal constant _DOMAIN_SEPARATOR_TYPEHASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); - bytes32 private constant _LA_MSG_TYPEHASH = keccak256("MultiOwnerLightAccountMessage(bytes message)"); - bytes32 private constant _NAME_HASH = keccak256("MultiOwnerLightAccount"); - bytes32 private constant _VERSION_HASH = keccak256("1"); + bytes32 internal constant _LA_MSG_TYPEHASH = keccak256("MultiOwnerLightAccountMessage(bytes message)"); + bytes32 internal constant _NAME_HASH = keccak256("MultiOwnerLightAccount"); + bytes32 internal constant _VERSION_HASH = keccak256("1"); struct LightAccountStorage { LinkedListSet owners; @@ -62,85 +51,20 @@ contract MultiOwnerLightAccount is /// @param removedOwners The address array of removed owners. event OwnersUpdated(address[] addedOwners, address[] removedOwners); - /// @dev The length of the array does not match the expected length. - error ArrayLengthMismatch(); - /// @dev The account is not allowed to have 0 owners. error EmptyOwnersNotAllowed(); /// @dev The owner to be added is not valid (e.g., `address(0)`, the account itself, or a current owner). error InvalidOwner(address owner); - /// @dev The caller is not authorized. - error NotAuthorized(address caller); - /// @dev The owner to be removed does not exist. error OwnerDoesNotExist(address owner); - modifier onlyOwners() { - _onlyOwners(); - _; - } - constructor(IEntryPoint entryPoint_) CustomSlotInitializable(_INITIALIZABLE_STORAGE_POSITION) { _ENTRY_POINT = entryPoint_; _disableInitializers(); } - // solhint-disable-next-line no-empty-blocks - receive() external payable {} - - /// @notice Execute a transaction. This may only be called directly by an owner or by the entry point via a user - /// operation signed by an owner. - /// @param dest The target of the transaction. - /// @param value The amount of wei sent in the transaction. - /// @param func The transaction's calldata. - function execute(address dest, uint256 value, bytes calldata func) external { - _requireFromEntryPointOrOwner(); - _call(dest, value, func); - } - - /// @notice Execute a sequence of transactions. - /// @param dest An array of the targets for each transaction in the sequence. - /// @param func An array of calldata for each transaction in the sequence. Must be the same length as `dest`, with - /// corresponding elements representing the parameters for each transaction. - function executeBatch(address[] calldata dest, bytes[] calldata func) external { - _requireFromEntryPointOrOwner(); - if (dest.length != func.length) { - revert ArrayLengthMismatch(); - } - uint256 length = dest.length; - for (uint256 i = 0; i < length; ++i) { - _call(dest[i], 0, func[i]); - } - } - - /// @notice Execute a sequence of transactions. - /// @param dest An array of the targets for each transaction in the sequence. - /// @param value An array of value for each transaction in the sequence. - /// @param func An array of calldata for each transaction in the sequence. Must be the same length as `dest`, with - /// corresponding elements representing the parameters for each transaction. - function executeBatch(address[] calldata dest, uint256[] calldata value, bytes[] calldata func) external { - _requireFromEntryPointOrOwner(); - if (dest.length != func.length || dest.length != value.length) { - revert ArrayLengthMismatch(); - } - uint256 length = dest.length; - for (uint256 i = 0; i < length; ++i) { - _call(dest[i], value[i], func[i]); - } - } - - /// @notice Update owners of the account. Can only be called by a current owner or from the entry point via - /// a user operation signed by a current owner. - /// @dev If an owner is present in both `ownersToAdd` and `ownersToRemove`, it will be added as owner. The owner - /// array cannot have 0 or duplicate addresses. - /// @param ownersToAdd The address array of owners to be added. - /// @param ownersToRemove The address array of owners to be removed. - function updateOwners(address[] memory ownersToAdd, address[] memory ownersToRemove) external virtual onlyOwners { - _updateOwners(ownersToAdd, ownersToRemove); - } - /// @notice Called once as part of initialization, either during initial deployment or when first upgrading to /// this contract. /// @dev The `_ENTRY_POINT` member is immutable, to reduce gas consumption. To update the entry point address, a new @@ -151,21 +75,14 @@ contract MultiOwnerLightAccount is _initialize(owners_); } - /// @notice Deposit more funds for this account in the entry point. - function addDeposit() public payable { - entryPoint().depositTo{value: msg.value}(address(this)); - } - - /// @notice Withdraw value from the account's deposit. - /// @param withdrawAddress Target to send to. - /// @param amount Amount to withdraw. - function withdrawDepositTo(address payable withdrawAddress, uint256 amount) public onlyOwners { - entryPoint().withdrawTo(withdrawAddress, amount); - } - - /// @inheritdoc BaseAccount - function entryPoint() public view virtual override returns (IEntryPoint) { - return _ENTRY_POINT; + /// @notice Update owners of the account. Can only be called by a current owner or from the entry point via + /// a user operation signed by a current owner. + /// @dev If an owner is present in both `ownersToAdd` and `ownersToRemove`, it will be added as owner. The owner + /// array cannot have 0 or duplicate addresses. + /// @param ownersToAdd The address array of owners to be added. + /// @param ownersToRemove The address array of owners to be removed. + function updateOwners(address[] memory ownersToAdd, address[] memory ownersToRemove) external virtual onlyOwner { + _updateOwners(ownersToAdd, ownersToRemove); } ///@notice Return the owners of this account. @@ -174,12 +91,6 @@ contract MultiOwnerLightAccount is return _getStorage().owners.getAll().toAddressArray(); } - /// @notice Check current account deposit in the entry point. - /// @return The current account deposit. - function getDeposit() public view returns (uint256) { - return entryPoint().balanceOf(address(this)); - } - /// @notice Returns the domain separator for this contract, as defined in the EIP-712 standard. /// @return bytes32 The domain separator hash. function domainSeparator() public view returns (bytes32) { @@ -209,13 +120,13 @@ contract MultiOwnerLightAccount is return keccak256(encodeMessageData(message)); } + /// @inheritdoc IERC1271 /// @dev The signature is valid if it is signed by the owner's private key (if the owner is an EOA) or if it is a /// valid ERC-1271 signature from the owner (if the owner is a contract). Note that unlike the signature validation /// used in `validateUserOp`, this does **not** wrap the digest in an "Ethereum Signed Message" envelope before /// checking the signature in the EOA-owner case. - /// @inheritdoc IERC1271 - function isValidSignature(bytes32 digest, bytes memory signature) public view override returns (bytes4) { - bytes32 messageHash = getMessageHash(abi.encode(digest)); + function isValidSignature(bytes32 hash, bytes memory signature) public view override returns (bytes4) { + bytes32 messageHash = getMessageHash(abi.encode(hash)); (address recovered, ECDSA.RecoverError error,) = messageHash.tryRecover(signature); if (error == ECDSA.RecoverError.NoError && _getStorage().owners.contains(CastLib.toSetValue(recovered))) { return _1271_MAGIC_VALUE; @@ -299,32 +210,8 @@ contract MultiOwnerLightAccount is return false; } - /// @dev Revert if the caller is not one of the owners or the account itself (when redirected through `execute`). - function _onlyOwners() internal view { - if (msg.sender != address(this) && !_getStorage().owners.contains(msg.sender.toSetValue())) { - revert NotAuthorized(msg.sender); - } - } - - /// @dev Require that the call is from the entry point or an owner. - function _requireFromEntryPointOrOwner() internal view { - if (msg.sender != address(entryPoint()) && !_getStorage().owners.contains(msg.sender.toSetValue())) { - revert NotAuthorized(msg.sender); - } - } - - function _call(address target, uint256 value, bytes memory data) internal { - (bool success, bytes memory result) = target.call{value: value}(data); - if (!success) { - assembly { - revert(add(result, 32), mload(result)) - } - } - } - - function _authorizeUpgrade(address newImplementation) internal view override { - (newImplementation); - _onlyOwners(); + function _isFromOwner() internal view virtual override returns (bool) { + return _getStorage().owners.contains(msg.sender.toSetValue()); } function _getStorage() internal pure returns (LightAccountStorage storage storageStruct) { diff --git a/src/common/BaseLightAccount.sol b/src/common/BaseLightAccount.sol new file mode 100644 index 0000000..8e70da8 --- /dev/null +++ b/src/common/BaseLightAccount.sol @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.23; + +import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; +import {BaseAccount} from "account-abstraction/core/BaseAccount.sol"; +import {SIG_VALIDATION_FAILED} from "account-abstraction/core/Helpers.sol"; +import {IEntryPoint} from "account-abstraction/interfaces/IEntryPoint.sol"; +import {PackedUserOperation} from "account-abstraction/interfaces/PackedUserOperation.sol"; +import {TokenCallbackHandler} from "account-abstraction/samples/callback/TokenCallbackHandler.sol"; + +abstract contract BaseLightAccount is BaseAccount, TokenCallbackHandler, UUPSUpgradeable, IERC1271 { + bytes4 internal constant _1271_MAGIC_VALUE = bytes4(keccak256("isValidSignature(bytes32,bytes)")); // 0x1626ba7e + IEntryPoint internal immutable _ENTRY_POINT; + + /// @dev The length of the array does not match the expected length. + error ArrayLengthMismatch(); + + /// @dev The caller is not authorized. + error NotAuthorized(address caller); + + modifier onlyOwner() { + _onlyOwner(); + _; + } + + // solhint-disable-next-line no-empty-blocks + receive() external payable virtual {} + + /// @notice Execute a transaction. This may only be called directly by an owner or by the entry point via a user + /// operation signed by an owner. + /// @param dest The target of the transaction. + /// @param value The amount of wei sent in the transaction. + /// @param func The transaction's calldata. + function execute(address dest, uint256 value, bytes calldata func) external virtual { + _onlyOwnerOrEntryPoint(); + _call(dest, value, func); + } + + /// @notice Execute a sequence of transactions. + /// @param dest An array of the targets for each transaction in the sequence. + /// @param func An array of calldata for each transaction in the sequence. Must be the same length as `dest`, with + /// corresponding elements representing the parameters for each transaction. + function executeBatch(address[] calldata dest, bytes[] calldata func) external virtual { + _onlyOwnerOrEntryPoint(); + if (dest.length != func.length) { + revert ArrayLengthMismatch(); + } + uint256 length = dest.length; + for (uint256 i = 0; i < length; ++i) { + _call(dest[i], 0, func[i]); + } + } + + /// @notice Execute a sequence of transactions. + /// @param dest An array of the targets for each transaction in the sequence. + /// @param value An array of value for each transaction in the sequence. + /// @param func An array of calldata for each transaction in the sequence. Must be the same length as `dest`, with + /// corresponding elements representing the parameters for each transaction. + function executeBatch(address[] calldata dest, uint256[] calldata value, bytes[] calldata func) external virtual { + _onlyOwnerOrEntryPoint(); + if (dest.length != func.length || dest.length != value.length) { + revert ArrayLengthMismatch(); + } + uint256 length = dest.length; + for (uint256 i = 0; i < length; ++i) { + _call(dest[i], value[i], func[i]); + } + } + + /// @notice Deposit more funds for this account in the entry point. + function addDeposit() public payable { + entryPoint().depositTo{value: msg.value}(address(this)); + } + + /// @notice Withdraw value from the account's deposit. + /// @param withdrawAddress Target to send to. + /// @param amount Amount to withdraw. + function withdrawDepositTo(address payable withdrawAddress, uint256 amount) public onlyOwner { + entryPoint().withdrawTo(withdrawAddress, amount); + } + + /// @inheritdoc BaseAccount + function entryPoint() public view virtual override returns (IEntryPoint) { + return _ENTRY_POINT; + } + + /// @notice Check current account deposit in the entry point. + /// @return The current account deposit. + function getDeposit() public view returns (uint256) { + return entryPoint().balanceOf(address(this)); + } + + /// @inheritdoc IERC1271 + /// @dev Must override to support ERC-1271 signature validation. + function isValidSignature(bytes32 hash, bytes memory signature) public view virtual override returns (bytes4); + + /// @dev Must override to support user op validation. + function _validateSignature(PackedUserOperation calldata userOp, bytes32 userOpHash) + internal + virtual + override + returns (uint256); + + /// @dev Must override to allow calls to protected functions. + function _isFromOwner() internal view virtual returns (bool); + + function _isFromEntryPoint() internal view returns (bool) { + return msg.sender == address(entryPoint()); + } + + /// @dev Revert if the caller is not an owner or the account itself (when redirected through `execute`). + function _onlyOwner() internal view { + if (msg.sender != address(this) && !_isFromOwner()) { + revert NotAuthorized(msg.sender); + } + } + + /// @dev Require that the call is from the entry point or an owner. + function _onlyOwnerOrEntryPoint() internal view { + if (!_isFromEntryPoint() && !_isFromOwner()) { + revert NotAuthorized(msg.sender); + } + } + + function _call(address target, uint256 value, bytes memory data) internal { + (bool success, bytes memory result) = target.call{value: value}(data); + if (!success) { + assembly { + revert(add(result, 32), mload(result)) + } + } + } + + function _authorizeUpgrade(address newImplementation) internal view override { + (newImplementation); + _onlyOwner(); + } +} diff --git a/src/CustomSlotInitializable.sol b/src/common/CustomSlotInitializable.sol similarity index 100% rename from src/CustomSlotInitializable.sol rename to src/common/CustomSlotInitializable.sol diff --git a/test/CustomSlotInitializable.t.sol b/test/CustomSlotInitializable.t.sol index b74db2f..f7165c9 100644 --- a/test/CustomSlotInitializable.t.sol +++ b/test/CustomSlotInitializable.t.sol @@ -6,7 +6,7 @@ import "forge-std/Test.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; -import {CustomSlotInitializable} from "../src/CustomSlotInitializable.sol"; +import {CustomSlotInitializable} from "../src/common/CustomSlotInitializable.sol"; contract CustomSlotInitializableTest is Test { using stdStorage for StdStorage; diff --git a/test/LightAccount.t.sol b/test/LightAccount.t.sol index dd9e7f7..981e2aa 100644 --- a/test/LightAccount.t.sol +++ b/test/LightAccount.t.sol @@ -12,6 +12,7 @@ import {IEntryPoint} from "account-abstraction/interfaces/IEntryPoint.sol"; import {PackedUserOperation} from "account-abstraction/interfaces/PackedUserOperation.sol"; import {SimpleAccount} from "account-abstraction/samples/SimpleAccount.sol"; +import {BaseLightAccount} from "../src/common/BaseLightAccount.sol"; import {LightAccount} from "../src/LightAccount.sol"; import {LightAccountFactory} from "../src/LightAccountFactory.sol"; @@ -84,7 +85,7 @@ contract LightAccountTest is Test { } function testExecuteCannotBeCalledByRandos() public { - vm.expectRevert(abi.encodeWithSelector(LightAccount.NotAuthorized.selector, (address(this)))); + vm.expectRevert(abi.encodeWithSelector(BaseLightAccount.NotAuthorized.selector, (address(this)))); account.execute(address(lightSwitch), 0, abi.encodeCall(LightSwitch.turnOn, ())); } @@ -112,7 +113,7 @@ contract LightAccountTest is Test { dest[1] = address(lightSwitch); bytes[] memory func = new bytes[](1); func[0] = abi.encodeCall(LightSwitch.turnOn, ()); - vm.expectRevert(LightAccount.ArrayLengthMismatch.selector); + vm.expectRevert(BaseLightAccount.ArrayLengthMismatch.selector); account.executeBatch(dest, func); } @@ -138,7 +139,7 @@ contract LightAccountTest is Test { value[1] = uint256(1 ether); bytes[] memory func = new bytes[](1); func[0] = abi.encodeCall(LightSwitch.turnOn, ()); - vm.expectRevert(LightAccount.ArrayLengthMismatch.selector); + vm.expectRevert(BaseLightAccount.ArrayLengthMismatch.selector); account.executeBatch(dest, value, func); } @@ -171,7 +172,7 @@ contract LightAccountTest is Test { function testWithdrawDepositToCannotBeCalledByRandos() public { account.addDeposit{value: 10}(); - vm.expectRevert(abi.encodeWithSelector(LightAccount.NotAuthorized.selector, (address(this)))); + vm.expectRevert(abi.encodeWithSelector(BaseLightAccount.NotAuthorized.selector, (address(this)))); account.withdrawDepositTo(BENEFICIARY, 5); } @@ -197,7 +198,7 @@ contract LightAccountTest is Test { } function testRandosCannotTransferOwnership() public { - vm.expectRevert(abi.encodeWithSelector(LightAccount.NotAuthorized.selector, (address(this)))); + vm.expectRevert(abi.encodeWithSelector(BaseLightAccount.NotAuthorized.selector, (address(this)))); account.transferOwnership(address(0x100)); } @@ -258,7 +259,7 @@ contract LightAccountTest is Test { // Try to upgrade to a normal SimpleAccount with a different entry point. IEntryPoint newEntryPoint = IEntryPoint(address(0x2000)); SimpleAccount newImplementation = new SimpleAccount(newEntryPoint); - vm.expectRevert(abi.encodeWithSelector(LightAccount.NotAuthorized.selector, (address(this)))); + vm.expectRevert(abi.encodeWithSelector(BaseLightAccount.NotAuthorized.selector, (address(this)))); account.upgradeToAndCall(address(newImplementation), abi.encodeCall(SimpleAccount.initialize, (address(this)))); } @@ -287,7 +288,7 @@ contract LightAccountTest is Test { bytes32(uint256(uint160(0x0000000071727De22E5E9d8BAf0edAc6f37da032))) ) ), - 0x3bc154d32c096215e957ca99af52e83275464261e8cbe90d8da1df052c89947a + 0x366526c31a9f3ae63f66515d13f8e6888fe6edac9bc95296cb53e68006ad9888 ); } @@ -309,7 +310,7 @@ contract LightAccountTest is Test { sender: address(account), nonce: 0, initCode: "", - callData: abi.encodeCall(LightAccount.execute, (target, 0, innerCallData)), + callData: abi.encodeCall(BaseLightAccount.execute, (target, 0, innerCallData)), accountGasLimits: bytes32(uint256(verificationGasLimit) << 128 | callGasLimit), preVerificationGas: 1 << 24, gasFees: bytes32(uint256(maxPriorityFeePerGas) << 128 | maxFeePerGas), diff --git a/test/MultiOwnerLightAccount.t.sol b/test/MultiOwnerLightAccount.t.sol index de7780c..8f5572c 100644 --- a/test/MultiOwnerLightAccount.t.sol +++ b/test/MultiOwnerLightAccount.t.sol @@ -13,6 +13,7 @@ import {SimpleAccount} from "account-abstraction/samples/SimpleAccount.sol"; import {SENTINEL_VALUE} from "modular-account/libraries/Constants.sol"; import {LinkedListSet, LinkedListSetLib} from "modular-account/libraries/LinkedListSetLib.sol"; +import {BaseLightAccount} from "../src/common/BaseLightAccount.sol"; import {MultiOwnerLightAccount} from "../src/MultiOwnerLightAccount.sol"; import {MultiOwnerLightAccountFactory} from "../src/MultiOwnerLightAccountFactory.sol"; @@ -86,7 +87,7 @@ contract MultiOwnerLightAccountTest is Test { } function testExecuteCannotBeCalledByRandos() public { - vm.expectRevert(abi.encodeWithSelector(MultiOwnerLightAccount.NotAuthorized.selector, (address(this)))); + vm.expectRevert(abi.encodeWithSelector(BaseLightAccount.NotAuthorized.selector, (address(this)))); account.execute(address(lightSwitch), 0, abi.encodeCall(LightSwitch.turnOn, ())); } @@ -114,7 +115,7 @@ contract MultiOwnerLightAccountTest is Test { dest[1] = address(lightSwitch); bytes[] memory func = new bytes[](1); func[0] = abi.encodeCall(LightSwitch.turnOn, ()); - vm.expectRevert(MultiOwnerLightAccount.ArrayLengthMismatch.selector); + vm.expectRevert(BaseLightAccount.ArrayLengthMismatch.selector); account.executeBatch(dest, func); } @@ -140,7 +141,7 @@ contract MultiOwnerLightAccountTest is Test { value[1] = uint256(1 ether); bytes[] memory func = new bytes[](1); func[0] = abi.encodeCall(LightSwitch.turnOn, ()); - vm.expectRevert(MultiOwnerLightAccount.ArrayLengthMismatch.selector); + vm.expectRevert(BaseLightAccount.ArrayLengthMismatch.selector); account.executeBatch(dest, value, func); } @@ -173,7 +174,7 @@ contract MultiOwnerLightAccountTest is Test { function testWithdrawDepositToCannotBeCalledByRandos() public { account.addDeposit{value: 10}(); - vm.expectRevert(abi.encodeWithSelector(MultiOwnerLightAccount.NotAuthorized.selector, (address(this)))); + vm.expectRevert(abi.encodeWithSelector(BaseLightAccount.NotAuthorized.selector, (address(this)))); account.withdrawDepositTo(BENEFICIARY, 5); } @@ -211,7 +212,7 @@ contract MultiOwnerLightAccountTest is Test { function testRandosCannotUpdateOwners() public { address[] memory ownersToAdd = new address[](1); ownersToAdd[0] = address(0x100); - vm.expectRevert(abi.encodeWithSelector(MultiOwnerLightAccount.NotAuthorized.selector, (address(this)))); + vm.expectRevert(abi.encodeWithSelector(BaseLightAccount.NotAuthorized.selector, (address(this)))); account.updateOwners(ownersToAdd, new address[](0)); } @@ -299,7 +300,7 @@ contract MultiOwnerLightAccountTest is Test { // Try to upgrade to a normal SimpleAccount with a different entry point. IEntryPoint newEntryPoint = IEntryPoint(address(0x2000)); SimpleAccount newImplementation = new SimpleAccount(newEntryPoint); - vm.expectRevert(abi.encodeWithSelector(MultiOwnerLightAccount.NotAuthorized.selector, (address(this)))); + vm.expectRevert(abi.encodeWithSelector(BaseLightAccount.NotAuthorized.selector, (address(this)))); account.upgradeToAndCall(address(newImplementation), abi.encodeCall(SimpleAccount.initialize, (address(this)))); } @@ -329,7 +330,7 @@ contract MultiOwnerLightAccountTest is Test { bytes32(uint256(uint160(0x0000000071727De22E5E9d8BAf0edAc6f37da032))) ) ), - 0xc08a03589978f2f668b59c037f95687ffb9826f79ffe8c8287f524f1423b9be8 + 0x67239bd017c8365ae0798c8cc56c5b0e63b931855b19fd3433433ab6a49b5b8d ); } @@ -355,7 +356,7 @@ contract MultiOwnerLightAccountTest is Test { sender: address(account), nonce: 0, initCode: "", - callData: abi.encodeCall(MultiOwnerLightAccount.execute, (target, 0, innerCallData)), + callData: abi.encodeCall(BaseLightAccount.execute, (target, 0, innerCallData)), accountGasLimits: bytes32(uint256(verificationGasLimit) << 128 | callGasLimit), preVerificationGas: 1 << 24, gasFees: bytes32(uint256(maxPriorityFeePerGas) << 128 | maxFeePerGas),