From 03a730c2c054d1880f4990b31e9c219660f913ff Mon Sep 17 00:00:00 2001 From: Jayden Windle Date: Tue, 11 Apr 2023 14:23:26 -0400 Subject: [PATCH] Macro Audit Fixes (#19) * [H-3]: Added max lock time (#8) * Added max lock time of 1 year * Fixed tests + covered max unlock exceeded scenario * Removed payable castings (#9) * [L-2]: Added event coverage (#10) * [Q-1]: Added onlyUnlocked modifier (#11) * [Q-2] Removed unused imports (#12) * [Q-3]: Added ERC165 support (#14) * [G-3] Made vaultImplementation immutable (#15) * Added chainid to vault salt + context (#16) * Added chainid to vault salt + context to mitigate cross-chain attacks * Added support for deploying cross-chain vaults to VaultRegistry * Updated natspec docs * Added cross-chain executor (#17) * Added support for cross-chain executors and increased test coverage * added natspec coverage * Apply EIP naming conventions (#18) * migrated vault to account in accordance with eip naming * renamed VaultRegistry to AccountRegistry, updated naming in tests * split registry and cross chain executor list, renamed methods * finished migrating to eip naming conventions, split out tests, removed proxy size test * removed chainid from interface * remove console * removed unused imports * fix spelling * fixed registry interface to match eip * fix event * fixed event test * added event indexing --- src/Account.sol | 336 +++++++++++++++++ src/AccountRegistry.sol | 128 +++++++ src/CrossChainExecutorList.sol | 23 ++ src/Vault.sol | 199 ---------- src/VaultRegistry.sol | 79 ---- src/interfaces/IAccount.sol | 17 + src/interfaces/IRegistry.sol | 19 + src/interfaces/IVault.sol | 22 -- test/Account.t.sol | 434 ++++++++++++++++++++++ test/AccountERC1155.t.sol | 111 ++++++ test/AccountERC20.t.sol | 97 +++++ test/AccountERC721.t.sol | 110 ++++++ test/AccountETH.t.sol | 99 +++++ test/AccountRegistry.t.sol | 53 +++ test/CrossChainExecutorList.t.sol | 53 +++ test/MinimalProxyStore.t.sol | 12 - test/Vault.t.sol | 589 ------------------------------ test/VaultRegistry.t.sol | 37 -- test/mocks/MockExecutor.sol | 8 + 19 files changed, 1488 insertions(+), 938 deletions(-) create mode 100644 src/Account.sol create mode 100644 src/AccountRegistry.sol create mode 100644 src/CrossChainExecutorList.sol delete mode 100644 src/Vault.sol delete mode 100644 src/VaultRegistry.sol create mode 100644 src/interfaces/IAccount.sol create mode 100644 src/interfaces/IRegistry.sol delete mode 100644 src/interfaces/IVault.sol create mode 100644 test/Account.t.sol create mode 100644 test/AccountERC1155.t.sol create mode 100644 test/AccountERC20.t.sol create mode 100644 test/AccountERC721.t.sol create mode 100644 test/AccountETH.t.sol create mode 100644 test/AccountRegistry.t.sol create mode 100644 test/CrossChainExecutorList.t.sol delete mode 100644 test/Vault.t.sol delete mode 100644 test/VaultRegistry.t.sol diff --git a/src/Account.sol b/src/Account.sol new file mode 100644 index 0000000..3e17786 --- /dev/null +++ b/src/Account.sol @@ -0,0 +1,336 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "openzeppelin-contracts/token/ERC721/IERC721.sol"; +import "openzeppelin-contracts/interfaces/IERC1271.sol"; +import "openzeppelin-contracts/utils/cryptography/SignatureChecker.sol"; + +import "openzeppelin-contracts/utils/introspection/IERC165.sol"; +import "openzeppelin-contracts/token/ERC1155/IERC1155Receiver.sol"; + +import "./CrossChainExecutorList.sol"; +import "./MinimalReceiver.sol"; +import "./interfaces/IAccount.sol"; +import "./lib/MinimalProxyStore.sol"; + +/** + * @title A smart contract wallet owned by a single ERC721 token + * @author Jayden Windle (jaydenwindle) + */ +contract Account is IERC165, IERC1271, IAccount, MinimalReceiver { + error NotAuthorized(); + error AccountLocked(); + error ExceedsMaxLockTime(); + + CrossChainExecutorList public immutable crossChainExecutorList; + + /** + * @dev Timestamp at which Account will unlock + */ + uint256 public unlockTimestamp; + + /** + * @dev Mapping from owner address to executor address + */ + mapping(address => address) public executor; + + /** + * @dev Emitted whenever the lock status of a account is updated + */ + event LockUpdated(uint256 timestamp); + + /** + * @dev Emitted whenever the executor for a account is updated + */ + event ExecutorUpdated(address owner, address executor); + + constructor(address _crossChainExecutorList) { + crossChainExecutorList = CrossChainExecutorList( + _crossChainExecutorList + ); + } + + /** + * @dev Ensures execution can only continue if the account is not locked + */ + modifier onlyUnlocked() { + if (unlockTimestamp > block.timestamp) revert AccountLocked(); + _; + } + + /** + * @dev If account is unlocked and an executor is set, pass call to executor + */ + fallback(bytes calldata data) + external + payable + onlyUnlocked + returns (bytes memory result) + { + address _owner = owner(); + address _executor = executor[_owner]; + + // accept funds if executor is undefined or cannot be called + if (_executor.code.length == 0) return ""; + + return _call(_executor, 0, data); + } + + /** + * @dev Executes a transaction from the Account. Must be called by an account owner. + * + * @param to Destination address of the transaction + * @param value Ether value of the transaction + * @param data Encoded payload of the transaction + */ + function executeCall( + address to, + uint256 value, + bytes calldata data + ) external payable onlyUnlocked returns (bytes memory result) { + address _owner = owner(); + if (msg.sender != _owner) revert NotAuthorized(); + + return _call(to, value, data); + } + + /** + * @dev Executes a transaction from the Account. Must be called by an authorized executor. + * + * @param to Destination address of the transaction + * @param value Ether value of the transaction + * @param data Encoded payload of the transaction + */ + function executeTrustedCall( + address to, + uint256 value, + bytes calldata data + ) external payable onlyUnlocked returns (bytes memory result) { + address _executor = executor[owner()]; + if (msg.sender != _executor) revert NotAuthorized(); + + return _call(to, value, data); + } + + /** + * @dev Executes a transaction from the Account. Must be called by a trusted cross-chain executor. + * Can only be called if account is owned by a token on another chain. + * + * @param to Destination address of the transaction + * @param value Ether value of the transaction + * @param data Encoded payload of the transaction + */ + function executeCrossChainCall( + address to, + uint256 value, + bytes calldata data + ) external payable onlyUnlocked returns (bytes memory result) { + (uint256 chainId, , ) = context(); + + if (chainId == block.chainid) { + revert NotAuthorized(); + } + + if (!crossChainExecutorList.isCrossChainExecutor(chainId, msg.sender)) { + revert NotAuthorized(); + } + + return _call(to, value, data); + } + + /** + * @dev Sets executor address for Account, allowing owner to use a custom implementation if they choose to. + * When the token controlling the account is transferred, the implementation address will reset + * + * @param _executionModule the address of the execution module + */ + function setExecutor(address _executionModule) external onlyUnlocked { + address _owner = owner(); + if (_owner != msg.sender) revert NotAuthorized(); + + executor[_owner] = _executionModule; + + emit ExecutorUpdated(_owner, _executionModule); + } + + /** + * @dev Locks Account, preventing transactions from being executed until a certain time + * + * @param _unlockTimestamp timestamp when the account will become unlocked + */ + function lock(uint256 _unlockTimestamp) external onlyUnlocked { + if (_unlockTimestamp > block.timestamp + 365 days) + revert ExceedsMaxLockTime(); + + address _owner = owner(); + if (_owner != msg.sender) revert NotAuthorized(); + + unlockTimestamp = _unlockTimestamp; + + emit LockUpdated(_unlockTimestamp); + } + + /** + * @dev Returns Account lock status + * + * @return true if Account is locked, false otherwise + */ + function isLocked() external view returns (bool) { + return unlockTimestamp > block.timestamp; + } + + /** + * @dev Returns true if caller is authorized to execute actions on this account + * + * @param caller the address to query authorization for + * @return true if caller is authorized, false otherwise + */ + function isAuthorized(address caller) external view returns (bool) { + (uint256 chainId, address tokenCollection, uint256 tokenId) = context(); + + if (chainId != block.chainid) { + return crossChainExecutorList.isCrossChainExecutor(chainId, caller); + } + + address _owner = IERC721(tokenCollection).ownerOf(tokenId); + if (caller == _owner) return true; + + address _executor = executor[_owner]; + if (caller == _executor) return true; + + return false; + } + + /** + * @dev Implements EIP-1271 signature validation + * + * @param hash Hash of the signed data + * @param signature Signature to validate + */ + function isValidSignature(bytes32 hash, bytes memory signature) + external + view + returns (bytes4 magicValue) + { + // If account is locked, disable signing + if (unlockTimestamp > block.timestamp) return ""; + + // If account has an executor, check if executor signature is valid + address _owner = owner(); + address _executor = executor[_owner]; + + if ( + _executor != address(0) && + SignatureChecker.isValidSignatureNow(_executor, hash, signature) + ) { + return IERC1271.isValidSignature.selector; + } + + // Default - check if signature is valid for account owner + if (SignatureChecker.isValidSignatureNow(_owner, hash, signature)) { + return IERC1271.isValidSignature.selector; + } + + return ""; + } + + /** + * @dev Implements EIP-165 standard interface detection + * + * @param interfaceId the interfaceId to check support for + * @return true if the interface is supported, false otherwise + */ + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(IERC165, ERC1155Receiver) + returns (bool) + { + // default interface support + if ( + interfaceId == type(IAccount).interfaceId || + interfaceId == type(IERC1155Receiver).interfaceId || + interfaceId == type(IERC165).interfaceId + ) { + return true; + } + + address _executor = executor[owner()]; + + if (_executor == address(0) || _executor.code.length == 0) { + return false; + } + + // if interface is not supported by default, check executor + try IERC165(_executor).supportsInterface(interfaceId) returns ( + bool _supportsInterface + ) { + return _supportsInterface; + } catch { + return false; + } + } + + /** + * @dev Returns the owner of the token that controls this Account (public for Ownable compatibility) + * + * @return the address of the Account owner + */ + function owner() public view returns (address) { + (uint256 chainId, address tokenCollection, uint256 tokenId) = context(); + + if (chainId != block.chainid) { + return address(0); + } + + return IERC721(tokenCollection).ownerOf(tokenId); + } + + /** + * @dev Returns information about the token that owns this account + * + * @return tokenCollection the contract address of the ERC721 token which owns this account + * @return tokenId the tokenId of the ERC721 token which owns this account + */ + function token() + public + view + returns (address tokenCollection, uint256 tokenId) + { + (, tokenCollection, tokenId) = context(); + } + + function context() + internal + view + returns ( + uint256, + address, + uint256 + ) + { + bytes memory rawContext = MinimalProxyStore.getContext(address(this)); + if (rawContext.length == 0) return (0, address(0), 0); + + return abi.decode(rawContext, (uint256, address, uint256)); + } + + /** + * @dev Executes a low-level call + */ + function _call( + address to, + uint256 value, + bytes calldata data + ) internal returns (bytes memory result) { + bool success; + (success, result) = to.call{value: value}(data); + + if (!success) { + assembly { + revert(add(result, 32), mload(result)) + } + } + } +} diff --git a/src/AccountRegistry.sol b/src/AccountRegistry.sol new file mode 100644 index 0000000..3ab284f --- /dev/null +++ b/src/AccountRegistry.sol @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "./interfaces/IRegistry.sol"; +import "./lib/MinimalProxyStore.sol"; +import "./Account.sol"; + +/** + * @title A registry for token bound accounts + * @dev Determines the address for each token bound account and performs deployment of accounts + * @author Jayden Windle (jaydenwindle) + */ +contract AccountRegistry is IRegistry { + /** + * @dev Address of the account implementation + */ + address public immutable implementation; + + constructor(address _implementation) { + implementation = _implementation; + } + + /** + * @dev Creates the account for an ERC721 token. Will revert if account has already been deployed + * + * @param chainId the chainid of the network the ERC721 token exists on + * @param tokenCollection the contract address of the ERC721 token which will control the deployed account + * @param tokenId the token ID of the ERC721 token which will control the deployed account + * @return The address of the deployed ccount + */ + function createAccount( + uint256 chainId, + address tokenCollection, + uint256 tokenId + ) external returns (address) { + return _createAccount(chainId, tokenCollection, tokenId); + } + + /** + * @dev Deploys the account for an ERC721 token. Will revert if account has already been deployed + * + * @param tokenCollection the contract address of the ERC721 token which will control the deployed account + * @param tokenId the token ID of the ERC721 token which will control the deployed account + * @return The address of the deployed account + */ + function createAccount(address tokenCollection, uint256 tokenId) + external + returns (address) + { + return _createAccount(block.chainid, tokenCollection, tokenId); + } + + /** + * @dev Gets the address of the account for an ERC721 token. If account is + * not yet deployed, returns the address it will be deployed to + * + * @param chainId the chainid of the network the ERC721 token exists on + * @param tokenCollection the address of the ERC721 token contract + * @param tokenId the tokenId of the ERC721 token that controls the account + * @return The account address + */ + function account( + uint256 chainId, + address tokenCollection, + uint256 tokenId + ) external view returns (address) { + return _account(chainId, tokenCollection, tokenId); + } + + /** + * @dev Gets the address of the account for an ERC721 token. If account is + * not yet deployed, returns the address it will be deployed to + * + * @param tokenCollection the address of the ERC721 token contract + * @param tokenId the tokenId of the ERC721 token that controls the account + * @return The account address + */ + function account(address tokenCollection, uint256 tokenId) + external + view + returns (address) + { + return _account(block.chainid, tokenCollection, tokenId); + } + + function _createAccount( + uint256 chainId, + address tokenCollection, + uint256 tokenId + ) internal returns (address) { + bytes memory encodedTokenData = abi.encode( + chainId, + tokenCollection, + tokenId + ); + bytes32 salt = keccak256(encodedTokenData); + address accountProxy = MinimalProxyStore.cloneDeterministic( + implementation, + encodedTokenData, + salt + ); + + emit AccountCreated(accountProxy, tokenCollection, tokenId); + + return accountProxy; + } + + function _account( + uint256 chainId, + address tokenCollection, + uint256 tokenId + ) internal view returns (address) { + bytes memory encodedTokenData = abi.encode( + chainId, + tokenCollection, + tokenId + ); + bytes32 salt = keccak256(encodedTokenData); + + address accountProxy = MinimalProxyStore.predictDeterministicAddress( + implementation, + encodedTokenData, + salt + ); + + return accountProxy; + } +} diff --git a/src/CrossChainExecutorList.sol b/src/CrossChainExecutorList.sol new file mode 100644 index 0000000..927d33a --- /dev/null +++ b/src/CrossChainExecutorList.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "openzeppelin-contracts/access/Ownable2Step.sol"; + +contract CrossChainExecutorList is Ownable2Step { + mapping(uint256 => mapping(address => bool)) public isCrossChainExecutor; + + /** + * @dev Enables or disables a trusted cross-chain executor. + * + * @param chainId the chainid of the network the executor exists on + * @param executor the address of the executor + * @param enabled true if executor should be enabled, false otherwise + */ + function setCrossChainExecutor( + uint256 chainId, + address executor, + bool enabled + ) external onlyOwner { + isCrossChainExecutor[chainId][executor] = enabled; + } +} diff --git a/src/Vault.sol b/src/Vault.sol deleted file mode 100644 index ba18817..0000000 --- a/src/Vault.sol +++ /dev/null @@ -1,199 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import "openzeppelin-contracts/token/ERC721/IERC721.sol"; -import "openzeppelin-contracts/token/ERC721/IERC721Receiver.sol"; -import "openzeppelin-contracts/token/ERC1155/IERC1155Receiver.sol"; -import "openzeppelin-contracts/interfaces/IERC1271.sol"; -import "openzeppelin-contracts/utils/cryptography/SignatureChecker.sol"; -import "openzeppelin-contracts/utils/Address.sol"; - -import "./MinimalReceiver.sol"; -import "./interfaces/IVault.sol"; -import "./lib/MinimalProxyStore.sol"; - -/** - * @title A smart contract wallet owned by a single ERC721 token - * @author Jayden Windle (jaydenwindle) - */ -contract Vault is IVault, MinimalReceiver { - error NotAuthorized(); - error VaultLocked(); - - /** - * @dev Timestamp at which Vault will unlock - */ - uint256 public unlockTimestamp; - - /** - * @dev Mapping from owner address to executor address - */ - mapping(address => address) public executor; - - /** - * @dev If vault is unlocked and an executor is set, pass call to executor - */ - fallback(bytes calldata data) - external - payable - returns (bytes memory result) - { - if (unlockTimestamp > block.timestamp) revert VaultLocked(); - - address _owner = owner(); - address _executor = executor[_owner]; - - // accept funds if executor is undefined or cannot be called - if (_executor == address(0)) return ""; - if (_executor.code.length == 0) return ""; - - bool success; - (success, result) = _executor.call(data); - - if (!success) { - assembly { - revert(add(result, 32), mload(result)) - } - } - } - - /** - * @dev Executes a transaction from the Vault. Must be called by an authorized sender. - * - * @param to Destination address of the transaction - * @param value Ether value of the transaction - * @param data Encoded payload of the transaction - */ - function executeCall( - address payable to, - uint256 value, - bytes calldata data - ) external payable returns (bytes memory result) { - if (unlockTimestamp > block.timestamp) revert VaultLocked(); - if (!isOwnerOrExecutor(msg.sender)) revert NotAuthorized(); - - bool success; - (success, result) = to.call{value: value}(data); - - if (!success) { - assembly { - revert(add(result, 32), mload(result)) - } - } - } - - /** - * @dev Sets executior address for Vault, allowing owner to use a custom implementation if they choose to. - * When the token controlling the vault is transferred, the implementation address will reset - * - * @param _executionModule the address of the execution module - */ - function setExecutor(address _executionModule) external { - if (unlockTimestamp > block.timestamp) revert VaultLocked(); - - address _owner = owner(); - if (_owner != msg.sender) revert NotAuthorized(); - - executor[_owner] = _executionModule; - } - - /** - * @dev Locks Vault, preventing transactions from being executed until a certain time - * - * @param _unlockTimestamp timestamp when the vault will become unlocked - */ - function lock(uint256 _unlockTimestamp) external { - if (unlockTimestamp > block.timestamp) revert VaultLocked(); - - address _owner = owner(); - if (_owner != msg.sender) revert NotAuthorized(); - - unlockTimestamp = _unlockTimestamp; - } - - /** - * @dev Returns Vault lock status - * - * @return true if Vault is locked, false otherwise - */ - function isLocked() external view returns (bool) { - return unlockTimestamp > block.timestamp; - } - - /** - * @dev Returns true if caller is authorized to execute actions on this vault - * - * @param caller the address to query authorization for - * @return true if caller is authorized, false otherwise - */ - function isAuthorized(address caller) external view returns (bool) { - return isOwnerOrExecutor(caller); - } - - /** - * @dev Implements EIP-1271 signature validation - * - * @param hash Hash of the signed data - * @param signature Signature to validate - */ - function isValidSignature(bytes32 hash, bytes memory signature) - external - view - returns (bytes4 magicValue) - { - // If vault is locked, disable signing - if (unlockTimestamp > block.timestamp) return ""; - - // If vault has an executor, check if executor signature is valid - address _owner = owner(); - address _executor = executor[_owner]; - - if ( - _executor != address(0) && - SignatureChecker.isValidSignatureNow(_executor, hash, signature) - ) { - return IERC1271.isValidSignature.selector; - } - - // Default - check if signature is valid for vault owner - if (SignatureChecker.isValidSignatureNow(_owner, hash, signature)) { - return IERC1271.isValidSignature.selector; - } - - return ""; - } - - /** - * @dev Returns the owner of the token that controls this Vault (public for Ownable compatibility) - * - * @return the address of the Vault owner - */ - function owner() public view returns (address) { - bytes memory context = MinimalProxyStore.getContext(address(this)); - - if (context.length == 0) return address(0); - - (address tokenCollection, uint256 tokenId) = abi.decode( - context, - (address, uint256) - ); - - return IERC721(tokenCollection).ownerOf(tokenId); - } - - /** - * @dev Returns true if caller is owner or ececutor - * - * @param caller the address to query for - * @return true if caller is owner or executor, false otherwise - */ - function isOwnerOrExecutor(address caller) internal view returns (bool) { - address _owner = owner(); - if (caller == _owner) return true; - - address _executor = executor[_owner]; - if (caller == _executor) return true; - - return false; - } -} diff --git a/src/VaultRegistry.sol b/src/VaultRegistry.sol deleted file mode 100644 index e294e5a..0000000 --- a/src/VaultRegistry.sol +++ /dev/null @@ -1,79 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import "openzeppelin-contracts/proxy/Clones.sol"; -import "openzeppelin-contracts/utils/Create2.sol"; -import "openzeppelin-contracts/token/ERC721/IERC721.sol"; -import "openzeppelin-contracts/utils/cryptography/SignatureChecker.sol"; - -import "./Vault.sol"; -import "./lib/MinimalProxyStore.sol"; - -/** - * @title A registry for tokenbound Vaults - * @dev Determines the address for each tokenbound Vault and performs deployment of vault instances - * @author Jayden Windle (jaydenwindle) - */ -contract VaultRegistry { - error NotAuthorized(); - error VaultLocked(); - - /** - * @dev Address of the default vault implementation - */ - address public vaultImplementation; - - /** - * @dev Deploys the default Vault implementation - */ - constructor() { - vaultImplementation = address(new Vault()); - } - - /** - * @dev Deploys the Vault instance for an ERC721 token. Will revert if Vault has already been deployed - * - * @param tokenCollection the contract address of the ERC721 token which will control the deployed Vault - * @param tokenId the token ID of the ERC721 token which will control the deployed Vault - * @return The address of the deployed Vault - */ - function deployVault(address tokenCollection, uint256 tokenId) - external - returns (address payable) - { - bytes memory encodedTokenData = abi.encode(tokenCollection, tokenId); - bytes32 salt = keccak256(encodedTokenData); - address vaultProxy = MinimalProxyStore.cloneDeterministic( - vaultImplementation, - encodedTokenData, - salt - ); - - return payable(vaultProxy); - } - - /** - * @dev Gets the address of the VaultProxy for an ERC721 token. If VaultProxy is - * not yet deployed, returns the address it will be deployed to - * - * @param tokenCollection the address of the ERC721 token contract - * @param tokenId the tokenId of the ERC721 token that controls the vault - * @return The VaultProxy address - */ - function vaultAddress(address tokenCollection, uint256 tokenId) - external - view - returns (address payable) - { - bytes memory encodedTokenData = abi.encode(tokenCollection, tokenId); - bytes32 salt = keccak256(encodedTokenData); - - address vaultProxy = MinimalProxyStore.predictDeterministicAddress( - vaultImplementation, - encodedTokenData, - salt - ); - - return payable(vaultProxy); - } -} diff --git a/src/interfaces/IAccount.sol b/src/interfaces/IAccount.sol new file mode 100644 index 0000000..da027ae --- /dev/null +++ b/src/interfaces/IAccount.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +interface IAccount { + function owner() external view returns (address); + + function token() + external + view + returns (address tokenContract, uint256 tokenId); + + function executeCall( + address to, + uint256 value, + bytes calldata data + ) external payable returns (bytes memory); +} diff --git a/src/interfaces/IRegistry.sol b/src/interfaces/IRegistry.sol new file mode 100644 index 0000000..c36cc34 --- /dev/null +++ b/src/interfaces/IRegistry.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +interface IRegistry { + event AccountCreated( + address account, + address indexed tokenContract, + uint256 indexed tokenId + ); + + function createAccount(address tokenContract, uint256 tokenId) + external + returns (address); + + function account(address tokenContract, uint256 tokenId) + external + view + returns (address); +} diff --git a/src/interfaces/IVault.sol b/src/interfaces/IVault.sol deleted file mode 100644 index 4b7600f..0000000 --- a/src/interfaces/IVault.sol +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import "openzeppelin-contracts/interfaces/IERC1271.sol"; - -interface IVault is IERC1271 { - function executeCall( - address payable to, - uint256 value, - bytes calldata data - ) external payable returns (bytes memory); - - function executor(address owner) external view returns (address); - function setExecutor(address _executionModule) external; - - function isLocked() external view returns (bool); - function lock(uint256 _unlockTimestamp) external; - - function isAuthorized(address caller) external view returns (bool); - - function owner() external view returns (address); -} diff --git a/test/Account.t.sol b/test/Account.t.sol new file mode 100644 index 0000000..bfaf951 --- /dev/null +++ b/test/Account.t.sol @@ -0,0 +1,434 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; + +import "openzeppelin-contracts/token/ERC20/ERC20.sol"; +import "openzeppelin-contracts/proxy/Clones.sol"; + +import "../src/CrossChainExecutorList.sol"; +import "../src/Account.sol"; +import "../src/AccountRegistry.sol"; + +import "./mocks/MockERC721.sol"; +import "./mocks/MockExecutor.sol"; +import "./mocks/MockReverter.sol"; + +contract AccountTest is Test { + CrossChainExecutorList ccExecutorList; + Account implementation; + AccountRegistry public accountRegistry; + + MockERC721 public tokenCollection; + + function setUp() public { + ccExecutorList = new CrossChainExecutorList(); + implementation = new Account(address(ccExecutorList)); + accountRegistry = new AccountRegistry(address(implementation)); + + tokenCollection = new MockERC721(); + } + + function testNonOwnerCallsFail(uint256 tokenId) public { + address user1 = vm.addr(1); + address user2 = vm.addr(2); + + tokenCollection.mint(user1, tokenId); + assertEq(tokenCollection.ownerOf(tokenId), user1); + + address accountAddress = accountRegistry.createAccount( + address(tokenCollection), + tokenId + ); + + vm.deal(accountAddress, 1 ether); + + Account account = Account(payable(accountAddress)); + + // should fail if user2 tries to use account + vm.prank(user2); + vm.expectRevert(Account.NotAuthorized.selector); + account.executeCall(payable(user2), 0.1 ether, ""); + + // should fail if user2 tries to set executor + vm.prank(user2); + vm.expectRevert(Account.NotAuthorized.selector); + account.setExecutor(vm.addr(1337)); + + // should fail if user2 tries to lock account + vm.prank(user2); + vm.expectRevert(Account.NotAuthorized.selector); + account.lock(364 days); + } + + function testAccountOwnershipTransfer(uint256 tokenId) public { + address user1 = vm.addr(1); + address user2 = vm.addr(2); + + tokenCollection.mint(user1, tokenId); + assertEq(tokenCollection.ownerOf(tokenId), user1); + + address accountAddress = accountRegistry.createAccount( + address(tokenCollection), + tokenId + ); + + vm.deal(accountAddress, 1 ether); + + Account account = Account(payable(accountAddress)); + + // should fail if user2 tries to use account + vm.prank(user2); + vm.expectRevert(Account.NotAuthorized.selector); + account.executeCall(payable(user2), 0.1 ether, ""); + + vm.prank(user1); + tokenCollection.safeTransferFrom(user1, user2, tokenId); + + // should succeed now that user2 is owner + vm.prank(user2); + account.executeCall(payable(user2), 0.1 ether, ""); + + assertEq(user2.balance, 0.1 ether); + } + + function testMessageVerification(uint256 tokenId) public { + address user1 = vm.addr(1); + + tokenCollection.mint(user1, tokenId); + assertEq(tokenCollection.ownerOf(tokenId), user1); + + address accountAddress = accountRegistry.createAccount( + address(tokenCollection), + tokenId + ); + + Account account = Account(payable(accountAddress)); + + bytes32 hash = keccak256("This is a signed message"); + (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(1, hash); + + bytes memory signature1 = abi.encodePacked(r1, s1, v1); + + bytes4 returnValue1 = account.isValidSignature(hash, signature1); + + assertEq(returnValue1, IERC1271.isValidSignature.selector); + } + + function testMessageVerificationForUnauthorizedUser(uint256 tokenId) + public + { + address user1 = vm.addr(1); + + tokenCollection.mint(user1, tokenId); + assertEq(tokenCollection.ownerOf(tokenId), user1); + + address accountAddress = accountRegistry.createAccount( + address(tokenCollection), + tokenId + ); + + Account account = Account(payable(accountAddress)); + + bytes32 hash = keccak256("This is a signed message"); + + (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign(2, hash); + bytes memory signature2 = abi.encodePacked(r2, s2, v2); + + bytes4 returnValue2 = account.isValidSignature(hash, signature2); + + assertEq(returnValue2, 0); + } + + function testAccountLocksAndUnlocks(uint256 tokenId) public { + address user1 = vm.addr(1); + + tokenCollection.mint(user1, tokenId); + assertEq(tokenCollection.ownerOf(tokenId), user1); + + address accountAddress = accountRegistry.createAccount( + address(tokenCollection), + tokenId + ); + + vm.deal(accountAddress, 1 ether); + + Account account = Account(payable(accountAddress)); + + // cannot be locked for more than 365 days + vm.prank(user1); + vm.expectRevert(Account.ExceedsMaxLockTime.selector); + account.lock(366 days); + + // lock account for 10 days + uint256 unlockTimestamp = block.timestamp + 10 days; + vm.prank(user1); + account.lock(unlockTimestamp); + + assertEq(account.isLocked(), true); + + // transaction should revert if account is locked + vm.prank(user1); + vm.expectRevert(Account.AccountLocked.selector); + account.executeCall(payable(user1), 1 ether, ""); + + // fallback calls should revert if account is locked + vm.prank(user1); + vm.expectRevert(Account.AccountLocked.selector); + (bool success, bytes memory result) = accountAddress.call( + abi.encodeWithSignature("customFunction()") + ); + + // silence unused variable compiler warnings + success; + result; + + // setExecutor calls should revert if account is locked + vm.prank(user1); + vm.expectRevert(Account.AccountLocked.selector); + account.setExecutor(vm.addr(1337)); + + // lock calls should revert if account is locked + vm.prank(user1); + vm.expectRevert(Account.AccountLocked.selector); + account.lock(0); + + // signing should fail if account is locked + bytes32 hash = keccak256("This is a signed message"); + (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(2, hash); + bytes memory signature1 = abi.encodePacked(r1, s1, v1); + bytes4 returnValue = account.isValidSignature(hash, signature1); + assertEq(returnValue, 0); + + // warp to timestamp after account is unlocked + vm.warp(unlockTimestamp + 1 days); + + // transaction succeed now that account lock has expired + vm.prank(user1); + account.executeCall(payable(user1), 1 ether, ""); + assertEq(user1.balance, 1 ether); + + // signing should now that account lock has expired + bytes32 hashAfterUnlock = keccak256("This is a signed message"); + (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign(1, hashAfterUnlock); + bytes memory signature2 = abi.encodePacked(r2, s2, v2); + bytes4 returnValue1 = account.isValidSignature( + hashAfterUnlock, + signature2 + ); + assertEq(returnValue1, IERC1271.isValidSignature.selector); + } + + function testCustomExecutorFallback(uint256 tokenId) public { + address user1 = vm.addr(1); + + tokenCollection.mint(user1, tokenId); + assertEq(tokenCollection.ownerOf(tokenId), user1); + + address accountAddress = accountRegistry.createAccount( + address(tokenCollection), + tokenId + ); + + vm.deal(accountAddress, 1 ether); + + Account account = Account(payable(accountAddress)); + + MockExecutor mockExecutor = new MockExecutor(); + + // calls succeed with noop if executor is undefined + (bool success, bytes memory result) = accountAddress.call( + abi.encodeWithSignature("customFunction()") + ); + assertEq(success, true); + assertEq(result, ""); + + // calls succeed with noop if executor is EOA + vm.prank(user1); + account.setExecutor(vm.addr(1337)); + (bool success1, bytes memory result1) = accountAddress.call( + abi.encodeWithSignature("customFunction()") + ); + assertEq(success1, true); + assertEq(result1, ""); + + assertEq(account.isAuthorized(user1), true); + assertEq(account.isAuthorized(address(mockExecutor)), false); + + vm.prank(user1); + account.setExecutor(address(mockExecutor)); + + assertEq(account.isAuthorized(user1), true); + assertEq(account.isAuthorized(address(mockExecutor)), true); + + assertEq( + account.isValidSignature(bytes32(0), ""), + IERC1271.isValidSignature.selector + ); + + // execution module handles fallback calls + assertEq(MockExecutor(accountAddress).customFunction(), 12345); + + // execution bubbles up errors on revert + vm.expectRevert(MockReverter.MockError.selector); + MockExecutor(accountAddress).fail(); + } + + function testCustomExecutorCalls(uint256 tokenId) public { + address user1 = vm.addr(1); + address user2 = vm.addr(2); + + tokenCollection.mint(user1, tokenId); + assertEq(tokenCollection.ownerOf(tokenId), user1); + + address accountAddress = accountRegistry.createAccount( + address(tokenCollection), + tokenId + ); + + vm.deal(accountAddress, 1 ether); + + Account account = Account(payable(accountAddress)); + + assertEq(account.isAuthorized(user2), false); + + vm.prank(user1); + account.setExecutor(user2); + + assertEq(account.isAuthorized(user2), true); + + vm.prank(user2); + account.executeTrustedCall(user2, 0.1 ether, ""); + + assertEq(user2.balance, 0.1 ether); + } + + function testCrossChainCalls() public { + uint256 tokenId = 1; + address user1 = vm.addr(1); + address crossChainExecutor = vm.addr(2); + + uint256 chainId = block.chainid + 1; + + tokenCollection.mint(user1, tokenId); + assertEq(tokenCollection.ownerOf(tokenId), user1); + + address accountAddress = accountRegistry.createAccount( + chainId, + address(tokenCollection), + tokenId + ); + + vm.deal(accountAddress, 1 ether); + + Account account = Account(payable(accountAddress)); + + assertEq(account.isAuthorized(crossChainExecutor), false); + + CrossChainExecutorList(ccExecutorList).setCrossChainExecutor( + chainId, + crossChainExecutor, + true + ); + + assertEq(account.isAuthorized(crossChainExecutor), true); + + vm.prank(crossChainExecutor); + account.executeCrossChainCall(user1, 0.1 ether, ""); + + assertEq(user1.balance, 0.1 ether); + + address notCrossChainExecutor = vm.addr(3); + vm.prank(notCrossChainExecutor); + vm.expectRevert(Account.NotAuthorized.selector); + Account(payable(account)).executeCrossChainCall(user1, 0.1 ether, ""); + + assertEq(user1.balance, 0.1 ether); + + address nativeAccountAddress = accountRegistry.createAccount( + block.chainid, + address(tokenCollection), + tokenId + ); + + vm.prank(crossChainExecutor); + vm.expectRevert(Account.NotAuthorized.selector); + Account(payable(nativeAccountAddress)).executeCrossChainCall( + user1, + 0.1 ether, + "" + ); + + assertEq(user1.balance, 0.1 ether); + } + + function testExecuteCallRevert(uint256 tokenId) public { + address user1 = vm.addr(1); + + tokenCollection.mint(user1, tokenId); + assertEq(tokenCollection.ownerOf(tokenId), user1); + + address accountAddress = accountRegistry.createAccount( + address(tokenCollection), + tokenId + ); + + vm.deal(accountAddress, 1 ether); + + Account account = Account(payable(accountAddress)); + + MockReverter mockReverter = new MockReverter(); + + vm.prank(user1); + vm.expectRevert(MockReverter.MockError.selector); + account.executeCall( + payable(address(mockReverter)), + 0, + abi.encodeWithSignature("fail()") + ); + } + + function testAccountOwnerIsNullIfContextNotSet() public { + address accountClone = Clones.clone(accountRegistry.implementation()); + + assertEq(Account(payable(accountClone)).owner(), address(0)); + } + + function testEIP165Support() public { + uint256 tokenId = 1; + address user1 = vm.addr(1); + + tokenCollection.mint(user1, tokenId); + assertEq(tokenCollection.ownerOf(tokenId), user1); + + address accountAddress = accountRegistry.createAccount( + address(tokenCollection), + tokenId + ); + + vm.deal(accountAddress, 1 ether); + + Account account = Account(payable(accountAddress)); + + assertEq(account.supportsInterface(type(IAccount).interfaceId), true); + assertEq( + account.supportsInterface(type(IERC1155Receiver).interfaceId), + true + ); + assertEq(account.supportsInterface(type(IERC165).interfaceId), true); + assertEq( + account.supportsInterface(IERC1271.isValidSignature.selector), + false + ); + + MockExecutor mockExecutor = new MockExecutor(); + + vm.prank(user1); + account.setExecutor(address(mockExecutor)); + + assertEq( + account.supportsInterface(IERC1271.isValidSignature.selector), + true + ); + } +} diff --git a/test/AccountERC1155.t.sol b/test/AccountERC1155.t.sol new file mode 100644 index 0000000..3f7a9cc --- /dev/null +++ b/test/AccountERC1155.t.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; + +import "openzeppelin-contracts/token/ERC20/ERC20.sol"; +import "openzeppelin-contracts/proxy/Clones.sol"; + +import "../src/CrossChainExecutorList.sol"; +import "../src/Account.sol"; +import "../src/AccountRegistry.sol"; + +import "./mocks/MockERC721.sol"; +import "./mocks/MockERC1155.sol"; + +contract AccountTest is Test { + MockERC1155 public dummyERC1155; + + CrossChainExecutorList ccExecutorList; + Account implementation; + AccountRegistry public accountRegistry; + + MockERC721 public tokenCollection; + + function setUp() public { + dummyERC1155 = new MockERC1155(); + + ccExecutorList = new CrossChainExecutorList(); + implementation = new Account(address(ccExecutorList)); + accountRegistry = new AccountRegistry(address(implementation)); + + tokenCollection = new MockERC721(); + } + + function testTransferERC1155PreDeploy(uint256 tokenId) public { + address user1 = vm.addr(1); + + address computedAccountInstance = accountRegistry.account( + address(tokenCollection), + tokenId + ); + + tokenCollection.mint(user1, tokenId); + assertEq(tokenCollection.ownerOf(tokenId), user1); + + dummyERC1155.mint(computedAccountInstance, 1, 10); + + assertEq(dummyERC1155.balanceOf(computedAccountInstance, 1), 10); + + address accountAddress = accountRegistry.createAccount( + address(tokenCollection), + tokenId + ); + + Account account = Account(payable(accountAddress)); + + bytes memory erc1155TransferCall = abi.encodeWithSignature( + "safeTransferFrom(address,address,uint256,uint256,bytes)", + account, + user1, + 1, + 10, + "" + ); + vm.prank(user1); + account.executeCall( + payable(address(dummyERC1155)), + 0, + erc1155TransferCall + ); + + assertEq(dummyERC1155.balanceOf(accountAddress, 1), 0); + assertEq(dummyERC1155.balanceOf(user1, 1), 10); + } + + function testTransferERC1155PostDeploy(uint256 tokenId) public { + address user1 = vm.addr(1); + + address accountAddress = accountRegistry.createAccount( + address(tokenCollection), + tokenId + ); + + tokenCollection.mint(user1, tokenId); + assertEq(tokenCollection.ownerOf(tokenId), user1); + + dummyERC1155.mint(accountAddress, 1, 10); + + assertEq(dummyERC1155.balanceOf(accountAddress, 1), 10); + + Account account = Account(payable(accountAddress)); + + bytes memory erc1155TransferCall = abi.encodeWithSignature( + "safeTransferFrom(address,address,uint256,uint256,bytes)", + account, + user1, + 1, + 10, + "" + ); + vm.prank(user1); + account.executeCall( + payable(address(dummyERC1155)), + 0, + erc1155TransferCall + ); + + assertEq(dummyERC1155.balanceOf(accountAddress, 1), 0); + assertEq(dummyERC1155.balanceOf(user1, 1), 10); + } +} diff --git a/test/AccountERC20.t.sol b/test/AccountERC20.t.sol new file mode 100644 index 0000000..0cd0c2c --- /dev/null +++ b/test/AccountERC20.t.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; + +import "openzeppelin-contracts/token/ERC20/ERC20.sol"; +import "openzeppelin-contracts/proxy/Clones.sol"; + +import "../src/CrossChainExecutorList.sol"; +import "../src/Account.sol"; +import "../src/AccountRegistry.sol"; + +import "./mocks/MockERC721.sol"; +import "./mocks/MockERC20.sol"; + +contract AccountTest is Test { + MockERC20 public dummyERC20; + + CrossChainExecutorList ccExecutorList; + Account implementation; + AccountRegistry public accountRegistry; + + MockERC721 public tokenCollection; + + function setUp() public { + dummyERC20 = new MockERC20(); + + ccExecutorList = new CrossChainExecutorList(); + implementation = new Account(address(ccExecutorList)); + accountRegistry = new AccountRegistry(address(implementation)); + + tokenCollection = new MockERC721(); + } + + function testTransferERC20PreDeploy(uint256 tokenId) public { + address user1 = vm.addr(1); + + address computedAccountInstance = accountRegistry.account( + address(tokenCollection), + tokenId + ); + + tokenCollection.mint(user1, tokenId); + assertEq(tokenCollection.ownerOf(tokenId), user1); + + dummyERC20.mint(computedAccountInstance, 1 ether); + + assertEq(dummyERC20.balanceOf(computedAccountInstance), 1 ether); + + address accountAddress = accountRegistry.createAccount( + address(tokenCollection), + tokenId + ); + + Account account = Account(payable(accountAddress)); + + bytes memory erc20TransferCall = abi.encodeWithSignature( + "transfer(address,uint256)", + user1, + 1 ether + ); + vm.prank(user1); + account.executeCall(payable(address(dummyERC20)), 0, erc20TransferCall); + + assertEq(dummyERC20.balanceOf(accountAddress), 0); + assertEq(dummyERC20.balanceOf(user1), 1 ether); + } + + function testTransferERC20PostDeploy(uint256 tokenId) public { + address user1 = vm.addr(1); + + address accountAddress = accountRegistry.createAccount( + address(tokenCollection), + tokenId + ); + + tokenCollection.mint(user1, tokenId); + assertEq(tokenCollection.ownerOf(tokenId), user1); + + dummyERC20.mint(accountAddress, 1 ether); + + assertEq(dummyERC20.balanceOf(accountAddress), 1 ether); + + Account account = Account(payable(accountAddress)); + + bytes memory erc20TransferCall = abi.encodeWithSignature( + "transfer(address,uint256)", + user1, + 1 ether + ); + vm.prank(user1); + account.executeCall(payable(address(dummyERC20)), 0, erc20TransferCall); + + assertEq(dummyERC20.balanceOf(accountAddress), 0); + assertEq(dummyERC20.balanceOf(user1), 1 ether); + } +} diff --git a/test/AccountERC721.t.sol b/test/AccountERC721.t.sol new file mode 100644 index 0000000..b08175c --- /dev/null +++ b/test/AccountERC721.t.sol @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; + +import "openzeppelin-contracts/token/ERC20/ERC20.sol"; +import "openzeppelin-contracts/proxy/Clones.sol"; + +import "../src/CrossChainExecutorList.sol"; +import "../src/Account.sol"; +import "../src/AccountRegistry.sol"; + +import "./mocks/MockERC721.sol"; + +contract AccountTest is Test { + MockERC721 public dummyERC721; + + CrossChainExecutorList ccExecutorList; + Account implementation; + AccountRegistry public accountRegistry; + + MockERC721 public tokenCollection; + + function setUp() public { + dummyERC721 = new MockERC721(); + + ccExecutorList = new CrossChainExecutorList(); + implementation = new Account(address(ccExecutorList)); + accountRegistry = new AccountRegistry(address(implementation)); + + tokenCollection = new MockERC721(); + } + + function testTransferERC721PreDeploy(uint256 tokenId) public { + address user1 = vm.addr(1); + + address computedAccountInstance = accountRegistry.account( + address(tokenCollection), + tokenId + ); + + tokenCollection.mint(user1, tokenId); + assertEq(tokenCollection.ownerOf(tokenId), user1); + + dummyERC721.mint(computedAccountInstance, 1); + + assertEq(dummyERC721.balanceOf(computedAccountInstance), 1); + assertEq(dummyERC721.ownerOf(1), computedAccountInstance); + + address accountAddress = accountRegistry.createAccount( + address(tokenCollection), + tokenId + ); + + Account account = Account(payable(accountAddress)); + + bytes memory erc721TransferCall = abi.encodeWithSignature( + "safeTransferFrom(address,address,uint256)", + accountAddress, + user1, + 1 + ); + vm.prank(user1); + account.executeCall( + payable(address(dummyERC721)), + 0, + erc721TransferCall + ); + + assertEq(dummyERC721.balanceOf(address(account)), 0); + assertEq(dummyERC721.balanceOf(user1), 1); + assertEq(dummyERC721.ownerOf(1), user1); + } + + function testTransferERC721PostDeploy(uint256 tokenId) public { + address user1 = vm.addr(1); + + address accountAddress = accountRegistry.createAccount( + address(tokenCollection), + tokenId + ); + + tokenCollection.mint(user1, tokenId); + assertEq(tokenCollection.ownerOf(tokenId), user1); + + dummyERC721.mint(accountAddress, 1); + + assertEq(dummyERC721.balanceOf(accountAddress), 1); + assertEq(dummyERC721.ownerOf(1), accountAddress); + + Account account = Account(payable(accountAddress)); + + bytes memory erc721TransferCall = abi.encodeWithSignature( + "safeTransferFrom(address,address,uint256)", + account, + user1, + 1 + ); + vm.prank(user1); + account.executeCall( + payable(address(dummyERC721)), + 0, + erc721TransferCall + ); + + assertEq(dummyERC721.balanceOf(accountAddress), 0); + assertEq(dummyERC721.balanceOf(user1), 1); + assertEq(dummyERC721.ownerOf(1), user1); + } +} diff --git a/test/AccountETH.t.sol b/test/AccountETH.t.sol new file mode 100644 index 0000000..6dd255a --- /dev/null +++ b/test/AccountETH.t.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; + +import "openzeppelin-contracts/token/ERC20/ERC20.sol"; +import "openzeppelin-contracts/proxy/Clones.sol"; + +import "../src/CrossChainExecutorList.sol"; +import "../src/Account.sol"; +import "../src/AccountRegistry.sol"; + +import "./mocks/MockERC721.sol"; + +contract AccountTest is Test { + CrossChainExecutorList ccExecutorList; + Account implementation; + AccountRegistry public accountRegistry; + + MockERC721 public tokenCollection; + + function setUp() public { + ccExecutorList = new CrossChainExecutorList(); + implementation = new Account(address(ccExecutorList)); + accountRegistry = new AccountRegistry(address(implementation)); + + tokenCollection = new MockERC721(); + } + + function testTransferETHPreDeploy() public { + uint256 tokenId = 1; + address user1 = vm.addr(1); + vm.deal(user1, 0.2 ether); + + // get address that account will be deployed to (before token is minted) + address accountAddress = accountRegistry.account( + address(tokenCollection), + tokenId + ); + + // mint token for account to user1 + tokenCollection.mint(user1, tokenId); + + assertEq(tokenCollection.ownerOf(tokenId), user1); + + // send ETH from user1 to account (prior to account deployment) + vm.prank(user1); + (bool sent, ) = accountAddress.call{value: 0.2 ether}(""); + assertTrue(sent); + + assertEq(accountAddress.balance, 0.2 ether); + + // deploy account contract (from a different wallet) + address createdAccountInstance = accountRegistry.createAccount( + address(tokenCollection), + tokenId + ); + + assertEq(accountAddress, createdAccountInstance); + + Account account = Account(payable(accountAddress)); + + // user1 executes transaction to send ETH from account + vm.prank(user1); + account.executeCall(payable(user1), 0.1 ether, ""); + + // success! + assertEq(accountAddress.balance, 0.1 ether); + assertEq(user1.balance, 0.1 ether); + } + + function testTransferETHPostDeploy(uint256 tokenId) public { + address user1 = vm.addr(1); + vm.deal(user1, 0.2 ether); + + address accountAddress = accountRegistry.createAccount( + address(tokenCollection), + tokenId + ); + + tokenCollection.mint(user1, tokenId); + + assertEq(tokenCollection.ownerOf(tokenId), user1); + + vm.prank(user1); + (bool sent, ) = accountAddress.call{value: 0.2 ether}(""); + assertTrue(sent); + + assertEq(accountAddress.balance, 0.2 ether); + + Account account = Account(payable(accountAddress)); + + vm.prank(user1); + account.executeCall(payable(user1), 0.1 ether, ""); + + assertEq(accountAddress.balance, 0.1 ether); + assertEq(user1.balance, 0.1 ether); + } +} diff --git a/test/AccountRegistry.t.sol b/test/AccountRegistry.t.sol new file mode 100644 index 0000000..83310e9 --- /dev/null +++ b/test/AccountRegistry.t.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; + +import "../src/interfaces/IRegistry.sol"; +import "../src/lib/MinimalProxyStore.sol"; +import "../src/CrossChainExecutorList.sol"; +import "../src/Account.sol"; +import "../src/AccountRegistry.sol"; + +contract AccountRegistryTest is Test { + CrossChainExecutorList ccExecutorList; + Account implementation; + AccountRegistry public accountRegistry; + + event AccountCreated( + address account, + address indexed tokenContract, + uint256 indexed tokenId + ); + + function setUp() public { + ccExecutorList = new CrossChainExecutorList(); + implementation = new Account(address(ccExecutorList)); + accountRegistry = new AccountRegistry(address(implementation)); + } + + function testDeployAccount(address tokenCollection, uint256 tokenId) + public + { + assertTrue(address(accountRegistry) != address(0)); + + address predictedAccountAddress = accountRegistry.account( + tokenCollection, + tokenId + ); + + vm.expectEmit(true, true, true, true); + emit AccountCreated(predictedAccountAddress, tokenCollection, tokenId); + address accountAddress = accountRegistry.createAccount( + tokenCollection, + tokenId + ); + + assertTrue(accountAddress != address(0)); + assertTrue(accountAddress == predictedAccountAddress); + assertEq( + MinimalProxyStore.getContext(accountAddress), + abi.encode(block.chainid, tokenCollection, tokenId) + ); + } +} diff --git a/test/CrossChainExecutorList.t.sol b/test/CrossChainExecutorList.t.sol new file mode 100644 index 0000000..c49ec56 --- /dev/null +++ b/test/CrossChainExecutorList.t.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; + +import "../src/CrossChainExecutorList.sol"; +import "../src/lib/MinimalProxyStore.sol"; + +contract AccountRegistryTest is Test { + CrossChainExecutorList public crossChainExecutorList; + + function setUp() public { + crossChainExecutorList = new CrossChainExecutorList(); + } + + function testSetCrossChainExecutor() public { + address crossChainExecutor = vm.addr(1); + address notCrossChainExecutor = vm.addr(2); + + crossChainExecutorList.setCrossChainExecutor( + block.chainid, + crossChainExecutor, + true + ); + + assertTrue( + crossChainExecutorList.isCrossChainExecutor( + block.chainid, + crossChainExecutor + ) + ); + assertEq( + crossChainExecutorList.isCrossChainExecutor( + block.chainid, + notCrossChainExecutor + ), + false + ); + + crossChainExecutorList.setCrossChainExecutor( + block.chainid, + crossChainExecutor, + false + ); + assertEq( + crossChainExecutorList.isCrossChainExecutor( + block.chainid, + crossChainExecutor + ), + false + ); + } +} diff --git a/test/MinimalProxyStore.t.sol b/test/MinimalProxyStore.t.sol index eec6e93..03d800e 100644 --- a/test/MinimalProxyStore.t.sol +++ b/test/MinimalProxyStore.t.sol @@ -103,16 +103,4 @@ contract MinimalProxyStoreTest is Test { keccak256("hello") ); } - - // must run with --code-size-limit 24576 - function testCannotOverflowContext() public { - uint256 maxSize = 0x6000 - 46; - bytes memory maxSizeContext = new bytes(maxSize); - bytes memory overflowContext = new bytes(maxSize + 1); - - MinimalProxyStore.clone(address(this), maxSizeContext); - - vm.expectRevert(MinimalProxyStore.CreateError.selector); - MinimalProxyStore.clone(address(this), overflowContext); - } } diff --git a/test/Vault.t.sol b/test/Vault.t.sol deleted file mode 100644 index a5c6f4a..0000000 --- a/test/Vault.t.sol +++ /dev/null @@ -1,589 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import "forge-std/Test.sol"; - -import "openzeppelin-contracts/token/ERC20/ERC20.sol"; -import "openzeppelin-contracts/proxy/Clones.sol"; - -import "../src/Vault.sol"; -import "../src/VaultRegistry.sol"; - -import "./mocks/MockERC721.sol"; -import "./mocks/MockERC1155.sol"; -import "./mocks/MockERC20.sol"; -import "./mocks/MockExecutor.sol"; -import "./mocks/MockReverter.sol"; - -contract VaultTest is Test { - MockERC721 public dummyERC721; - MockERC1155 public dummyERC1155; - MockERC20 public dummyERC20; - - VaultRegistry public vaultRegistry; - - MockERC721 public tokenCollection; - - function setUp() public { - dummyERC721 = new MockERC721(); - dummyERC1155 = new MockERC1155(); - dummyERC20 = new MockERC20(); - - vaultRegistry = new VaultRegistry(); - - tokenCollection = new MockERC721(); - } - - function testTransferETHPreDeploy() public { - uint256 tokenId = 1; - address user1 = vm.addr(1); - vm.deal(user1, 0.2 ether); - - // get address that vault will be deployed to (before token is minted) - address payable vaultAddress = vaultRegistry.vaultAddress( - address(tokenCollection), - tokenId - ); - - // mint token for vault to user1 - tokenCollection.mint(user1, tokenId); - - assertEq(tokenCollection.ownerOf(tokenId), user1); - - // send ETH from user1 to vault (prior to vault deployment) - vm.prank(user1); - (bool sent, ) = vaultAddress.call{value: 0.2 ether}(""); - assertTrue(sent); - - assertEq(vaultAddress.balance, 0.2 ether); - - // deploy vault contract (from a different wallet) - address payable createdVaultInstance = vaultRegistry.deployVault( - address(tokenCollection), - tokenId - ); - - assertEq(vaultAddress, createdVaultInstance); - - Vault vault = Vault(vaultAddress); - - // user1 executes transaction to send ETH from vault - vm.prank(user1); - vault.executeCall(payable(user1), 0.1 ether, ""); - - // success! - assertEq(vaultAddress.balance, 0.1 ether); - assertEq(user1.balance, 0.1 ether); - } - - function testTransferETHPostDeploy(uint256 tokenId) public { - address user1 = vm.addr(1); - vm.deal(user1, 0.2 ether); - - address payable vaultAddress = vaultRegistry.deployVault( - address(tokenCollection), - tokenId - ); - - tokenCollection.mint(user1, tokenId); - - assertEq(tokenCollection.ownerOf(tokenId), user1); - - vm.prank(user1); - (bool sent, ) = vaultAddress.call{value: 0.2 ether}(""); - assertTrue(sent); - - assertEq(vaultAddress.balance, 0.2 ether); - - Vault vault = Vault(vaultAddress); - - vm.prank(user1); - vault.executeCall(payable(user1), 0.1 ether, ""); - - assertEq(vaultAddress.balance, 0.1 ether); - assertEq(user1.balance, 0.1 ether); - } - - function testTransferERC20PreDeploy(uint256 tokenId) public { - address user1 = vm.addr(1); - - address payable computedVaultInstance = vaultRegistry.vaultAddress( - address(tokenCollection), - tokenId - ); - - tokenCollection.mint(user1, tokenId); - assertEq(tokenCollection.ownerOf(tokenId), user1); - - dummyERC20.mint(computedVaultInstance, 1 ether); - - assertEq(dummyERC20.balanceOf(computedVaultInstance), 1 ether); - - address payable vaultAddress = vaultRegistry.deployVault( - address(tokenCollection), - tokenId - ); - - Vault vault = Vault(vaultAddress); - - bytes memory erc20TransferCall = abi.encodeWithSignature( - "transfer(address,uint256)", - user1, - 1 ether - ); - vm.prank(user1); - vault.executeCall(payable(address(dummyERC20)), 0, erc20TransferCall); - - assertEq(dummyERC20.balanceOf(vaultAddress), 0); - assertEq(dummyERC20.balanceOf(user1), 1 ether); - } - - function testTransferERC20PostDeploy(uint256 tokenId) public { - address user1 = vm.addr(1); - - address payable vaultAddress = vaultRegistry.deployVault( - address(tokenCollection), - tokenId - ); - - tokenCollection.mint(user1, tokenId); - assertEq(tokenCollection.ownerOf(tokenId), user1); - - dummyERC20.mint(vaultAddress, 1 ether); - - assertEq(dummyERC20.balanceOf(vaultAddress), 1 ether); - - Vault vault = Vault(vaultAddress); - - bytes memory erc20TransferCall = abi.encodeWithSignature( - "transfer(address,uint256)", - user1, - 1 ether - ); - vm.prank(user1); - vault.executeCall(payable(address(dummyERC20)), 0, erc20TransferCall); - - assertEq(dummyERC20.balanceOf(vaultAddress), 0); - assertEq(dummyERC20.balanceOf(user1), 1 ether); - } - - function testTransferERC1155PreDeploy(uint256 tokenId) public { - address user1 = vm.addr(1); - - address payable computedVaultInstance = vaultRegistry.vaultAddress( - address(tokenCollection), - tokenId - ); - - tokenCollection.mint(user1, tokenId); - assertEq(tokenCollection.ownerOf(tokenId), user1); - - dummyERC1155.mint(computedVaultInstance, 1, 10); - - assertEq(dummyERC1155.balanceOf(computedVaultInstance, 1), 10); - - address payable vaultAddress = vaultRegistry.deployVault( - address(tokenCollection), - tokenId - ); - - Vault vault = Vault(vaultAddress); - - bytes memory erc1155TransferCall = abi.encodeWithSignature( - "safeTransferFrom(address,address,uint256,uint256,bytes)", - vaultAddress, - user1, - 1, - 10, - "" - ); - vm.prank(user1); - vault.executeCall( - payable(address(dummyERC1155)), - 0, - erc1155TransferCall - ); - - assertEq(dummyERC1155.balanceOf(vaultAddress, 1), 0); - assertEq(dummyERC1155.balanceOf(user1, 1), 10); - } - - function testTransferERC1155PostDeploy(uint256 tokenId) public { - address user1 = vm.addr(1); - - address payable vaultAddress = vaultRegistry.deployVault( - address(tokenCollection), - tokenId - ); - - tokenCollection.mint(user1, tokenId); - assertEq(tokenCollection.ownerOf(tokenId), user1); - - dummyERC1155.mint(vaultAddress, 1, 10); - - assertEq(dummyERC1155.balanceOf(vaultAddress, 1), 10); - - Vault vault = Vault(vaultAddress); - - bytes memory erc1155TransferCall = abi.encodeWithSignature( - "safeTransferFrom(address,address,uint256,uint256,bytes)", - vaultAddress, - user1, - 1, - 10, - "" - ); - vm.prank(user1); - vault.executeCall( - payable(address(dummyERC1155)), - 0, - erc1155TransferCall - ); - - assertEq(dummyERC1155.balanceOf(vaultAddress, 1), 0); - assertEq(dummyERC1155.balanceOf(user1, 1), 10); - } - - function testTransferERC721PreDeploy(uint256 tokenId) public { - address user1 = vm.addr(1); - - address payable computedVaultInstance = vaultRegistry.vaultAddress( - address(tokenCollection), - tokenId - ); - - tokenCollection.mint(user1, tokenId); - assertEq(tokenCollection.ownerOf(tokenId), user1); - - dummyERC721.mint(computedVaultInstance, 1); - - assertEq(dummyERC721.balanceOf(computedVaultInstance), 1); - assertEq(dummyERC721.ownerOf(1), computedVaultInstance); - - address payable vaultAddress = vaultRegistry.deployVault( - address(tokenCollection), - tokenId - ); - - Vault vault = Vault(vaultAddress); - - bytes memory erc721TransferCall = abi.encodeWithSignature( - "safeTransferFrom(address,address,uint256)", - address(vaultAddress), - user1, - 1 - ); - vm.prank(user1); - vault.executeCall(payable(address(dummyERC721)), 0, erc721TransferCall); - - assertEq(dummyERC721.balanceOf(address(vaultAddress)), 0); - assertEq(dummyERC721.balanceOf(user1), 1); - assertEq(dummyERC721.ownerOf(1), user1); - } - - function testTransferERC721PostDeploy(uint256 tokenId) public { - address user1 = vm.addr(1); - - address payable vaultAddress = vaultRegistry.deployVault( - address(tokenCollection), - tokenId - ); - - tokenCollection.mint(user1, tokenId); - assertEq(tokenCollection.ownerOf(tokenId), user1); - - dummyERC721.mint(vaultAddress, 1); - - assertEq(dummyERC721.balanceOf(vaultAddress), 1); - assertEq(dummyERC721.ownerOf(1), vaultAddress); - - Vault vault = Vault(vaultAddress); - - bytes memory erc721TransferCall = abi.encodeWithSignature( - "safeTransferFrom(address,address,uint256)", - vaultAddress, - user1, - 1 - ); - vm.prank(user1); - vault.executeCall(payable(address(dummyERC721)), 0, erc721TransferCall); - - assertEq(dummyERC721.balanceOf(vaultAddress), 0); - assertEq(dummyERC721.balanceOf(user1), 1); - assertEq(dummyERC721.ownerOf(1), user1); - } - - function testNonOwnerCallsFail(uint256 tokenId) public { - address user1 = vm.addr(1); - address user2 = vm.addr(2); - - tokenCollection.mint(user1, tokenId); - assertEq(tokenCollection.ownerOf(tokenId), user1); - - address payable vaultAddress = vaultRegistry.deployVault( - address(tokenCollection), - tokenId - ); - - vm.deal(vaultAddress, 1 ether); - - Vault vault = Vault(vaultAddress); - - // should fail if user2 tries to use vault - vm.prank(user2); - vm.expectRevert(Vault.NotAuthorized.selector); - vault.executeCall(payable(user2), 0.1 ether, ""); - - // should fail if user2 tries to set executor - vm.prank(user2); - vm.expectRevert(Vault.NotAuthorized.selector); - vault.setExecutor(vm.addr(1337)); - - // should fail if user2 tries to lock vault - vm.prank(user2); - vm.expectRevert(Vault.NotAuthorized.selector); - vault.lock(type(uint256).max); - } - - function testVaultOwnershipTransfer(uint256 tokenId) public { - address user1 = vm.addr(1); - address user2 = vm.addr(2); - - tokenCollection.mint(user1, tokenId); - assertEq(tokenCollection.ownerOf(tokenId), user1); - - address payable vaultAddress = vaultRegistry.deployVault( - address(tokenCollection), - tokenId - ); - - vm.deal(vaultAddress, 1 ether); - - Vault vault = Vault(vaultAddress); - - // should fail if user2 tries to use vault - vm.prank(user2); - vm.expectRevert(Vault.NotAuthorized.selector); - vault.executeCall(payable(user2), 0.1 ether, ""); - - vm.prank(user1); - tokenCollection.safeTransferFrom(user1, user2, tokenId); - - // should succeed now that user2 is owner - vm.prank(user2); - vault.executeCall(payable(user2), 0.1 ether, ""); - - assertEq(user2.balance, 0.1 ether); - } - - function testMessageSigningAndVerificationForAuthorizedUser(uint256 tokenId) - public - { - address user1 = vm.addr(1); - - tokenCollection.mint(user1, tokenId); - assertEq(tokenCollection.ownerOf(tokenId), user1); - - address payable vaultAddress = vaultRegistry.deployVault( - address(tokenCollection), - tokenId - ); - - Vault vault = Vault(vaultAddress); - - bytes32 hash = keccak256("This is a signed message"); - (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(1, hash); - - bytes memory signature1 = abi.encodePacked(r1, s1, v1); - - bytes4 returnValue1 = vault.isValidSignature(hash, signature1); - - assertEq(returnValue1, IERC1271.isValidSignature.selector); - } - - function testMessageSigningAndVerificationForUnauthorizedUser( - uint256 tokenId - ) public { - address user1 = vm.addr(1); - - tokenCollection.mint(user1, tokenId); - assertEq(tokenCollection.ownerOf(tokenId), user1); - - address payable vaultAddress = vaultRegistry.deployVault( - address(tokenCollection), - tokenId - ); - - Vault vault = Vault(vaultAddress); - - bytes32 hash = keccak256("This is a signed message"); - - (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign(2, hash); - bytes memory signature2 = abi.encodePacked(r2, s2, v2); - - bytes4 returnValue2 = vault.isValidSignature(hash, signature2); - - assertEq(returnValue2, 0); - } - - function testVaultLocksAndUnlocks(uint256 tokenId) public { - address user1 = vm.addr(1); - - tokenCollection.mint(user1, tokenId); - assertEq(tokenCollection.ownerOf(tokenId), user1); - - address payable vaultAddress = vaultRegistry.deployVault( - address(tokenCollection), - tokenId - ); - - vm.deal(vaultAddress, 1 ether); - - Vault vault = Vault(vaultAddress); - - // lock vault for 10 days - uint256 unlockTimestamp = block.timestamp + 10 days; - vm.prank(user1); - vault.lock(unlockTimestamp); - - assertEq(vault.isLocked(), true); - - // transaction should revert if vault is locked - vm.prank(user1); - vm.expectRevert(Vault.VaultLocked.selector); - vault.executeCall(payable(user1), 1 ether, ""); - - // fallback calls should revert if vault is locked - vm.prank(user1); - vm.expectRevert(Vault.VaultLocked.selector); - (bool success, bytes memory result) = vaultAddress.call( - abi.encodeWithSignature("customFunction()") - ); - - // silence unused variable compiler warnings - success; - result; - - // setExecutor calls should revert if vault is locked - vm.prank(user1); - vm.expectRevert(Vault.VaultLocked.selector); - vault.setExecutor(vm.addr(1337)); - - // lock calls should revert if vault is locked - vm.prank(user1); - vm.expectRevert(Vault.VaultLocked.selector); - vault.lock(0); - - // signing should fail if vault is locked - bytes32 hash = keccak256("This is a signed message"); - (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(2, hash); - bytes memory signature1 = abi.encodePacked(r1, s1, v1); - bytes4 returnValue = vault.isValidSignature(hash, signature1); - assertEq(returnValue, 0); - - // warp to timestamp after vault is unlocked - vm.warp(unlockTimestamp + 1 days); - - // transaction succeed now that vault lock has expired - vm.prank(user1); - vault.executeCall(payable(user1), 1 ether, ""); - assertEq(user1.balance, 1 ether); - - // signing should now that vault lock has expired - bytes32 hashAfterUnlock = keccak256("This is a signed message"); - (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign(1, hashAfterUnlock); - bytes memory signature2 = abi.encodePacked(r2, s2, v2); - bytes4 returnValue1 = vault.isValidSignature( - hashAfterUnlock, - signature2 - ); - assertEq(returnValue1, IERC1271.isValidSignature.selector); - } - - function testCustomExecutionModule(uint256 tokenId) public { - address user1 = vm.addr(1); - - tokenCollection.mint(user1, tokenId); - assertEq(tokenCollection.ownerOf(tokenId), user1); - - address payable vaultAddress = vaultRegistry.deployVault( - address(tokenCollection), - tokenId - ); - - vm.deal(vaultAddress, 1 ether); - - Vault vault = Vault(vaultAddress); - - MockExecutor mockExecutor = new MockExecutor(); - - // calls succeed with noop if executor is undefined - (bool success, bytes memory result) = vaultAddress.call( - abi.encodeWithSignature("customFunction()") - ); - assertEq(success, true); - assertEq(result, ""); - - // calls succeed with noop if executor is EOA - vm.prank(user1); - vault.setExecutor(vm.addr(1337)); - (bool success1, bytes memory result1) = vaultAddress.call( - abi.encodeWithSignature("customFunction()") - ); - assertEq(success1, true); - assertEq(result1, ""); - - assertEq(vault.isAuthorized(user1), true); - assertEq(vault.isAuthorized(address(mockExecutor)), false); - - vm.prank(user1); - vault.setExecutor(address(mockExecutor)); - - assertEq(vault.isAuthorized(user1), true); - assertEq(vault.isAuthorized(address(mockExecutor)), true); - - assertEq( - vault.isValidSignature(bytes32(0), ""), - IERC1271.isValidSignature.selector - ); - - // execution module handles fallback calls - assertEq(MockExecutor(vaultAddress).customFunction(), 12345); - - // execution bubbles up errors on revert - vm.expectRevert(MockReverter.MockError.selector); - MockExecutor(vaultAddress).fail(); - } - - function testExecuteCallRevert(uint256 tokenId) public { - address user1 = vm.addr(1); - - tokenCollection.mint(user1, tokenId); - assertEq(tokenCollection.ownerOf(tokenId), user1); - - address payable vaultAddress = vaultRegistry.deployVault( - address(tokenCollection), - tokenId - ); - - vm.deal(vaultAddress, 1 ether); - - Vault vault = Vault(vaultAddress); - - MockReverter mockReverter = new MockReverter(); - - vm.prank(user1); - vm.expectRevert(MockReverter.MockError.selector); - vault.executeCall( - payable(address(mockReverter)), - 0, - abi.encodeWithSignature("fail()") - ); - } - - function testVaultOwnerIsNullIfContextNotSet() public { - address vaultClone = Clones.clone(vaultRegistry.vaultImplementation()); - - assertEq(Vault(payable(vaultClone)).owner(), address(0)); - } -} diff --git a/test/VaultRegistry.t.sol b/test/VaultRegistry.t.sol deleted file mode 100644 index 31ca4f5..0000000 --- a/test/VaultRegistry.t.sol +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import "forge-std/Test.sol"; - -import "../src/Vault.sol"; -import "../src/VaultRegistry.sol"; -import "../src/lib/MinimalProxyStore.sol"; - -contract VaultRegistryTest is Test { - VaultRegistry public vaultRegistry; - - function setUp() public { - vaultRegistry = new VaultRegistry(); - } - - function testDeployVault(address tokenCollection, uint256 tokenId) public { - assertTrue(address(vaultRegistry) != address(0)); - - address predictedVaultAddress = vaultRegistry.vaultAddress( - tokenCollection, - tokenId - ); - - address vaultAddress = vaultRegistry.deployVault( - tokenCollection, - tokenId - ); - - assertTrue(vaultAddress != address(0)); - assertTrue(vaultAddress == predictedVaultAddress); - assertEq( - MinimalProxyStore.getContext(vaultAddress), - abi.encode(tokenCollection, tokenId) - ); - } -} diff --git a/test/mocks/MockExecutor.sol b/test/mocks/MockExecutor.sol index 64b1c71..b866cbb 100644 --- a/test/mocks/MockExecutor.sol +++ b/test/mocks/MockExecutor.sol @@ -16,4 +16,12 @@ contract MockExecutor is MockReverter { function customFunction() external pure returns (uint256) { return 12345; } + + function supportsInterface(bytes4 interfaceId) + external + pure + returns (bool) + { + return interfaceId == IERC1271.isValidSignature.selector; + } }