diff --git a/contracts/token-bound-account/TokenBoundAccount.sol b/contracts/token-bound-account/TokenBoundAccount.sol index d748d1aa8..7cb09a300 100644 --- a/contracts/token-bound-account/TokenBoundAccount.sol +++ b/contracts/token-bound-account/TokenBoundAccount.sol @@ -1,13 +1,42 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0; +/* solhint-disable avoid-low-level-calls */ +/* solhint-disable no-inline-assembly */ +/* solhint-disable reason-string */ + +// Base +import "../smart-wallet/utils/BaseAccount.sol"; + +// Extensions +import "../extension/Multicall.sol"; +import "../dynamic-contracts/extension/Initializable.sol"; +import "../dynamic-contracts/extension/ContractMetadata.sol"; +import "../openzeppelin-presets/token/ERC721/utils/ERC721Holder.sol"; +import "../openzeppelin-presets/token/ERC1155/utils/ERC1155Holder.sol"; +import "../eip/ERC1271.sol"; + +// Utils +import "../openzeppelin-presets/utils/cryptography/ECDSA.sol"; +import "../smart-wallet/utils/BaseAccountFactory.sol"; + import "./erc6551-utils/ERC6551AccountLib.sol"; import "./erc6551-utils/IERC6551Account.sol"; import "../eip/interface/IERC721.sol"; -import "../smart-wallet/non-upgradeable/Account.sol"; +import "../openzeppelin-presets/utils/cryptography/EIP712.sol"; -contract TokenBoundAccount is Account, IERC6551Account { +contract TokenBoundAccount is + Initializable, + ERC1271, + Multicall, + BaseAccount, + ContractMetadata, + ERC721Holder, + ERC1155Holder, + IERC6551Account, + EIP712 +{ using ECDSA for bytes32; /*/////////////////////////////////////////////////////////////// @@ -17,7 +46,17 @@ contract TokenBoundAccount is Account, IERC6551Account { event TokenBoundAccountCreated(address indexed account, bytes indexed data); /*/////////////////////////////////////////////////////////////// - Constructor + State + //////////////////////////////////////////////////////////////*/ + + /// @notice EIP 4337 factory for this contract. + address public immutable factory; + + /// @notice EIP 4337 Entrypoint contract. + IEntryPoint private immutable entrypointContract; + + /*/////////////////////////////////////////////////////////////// + Constructor, Initializer, Modifiers //////////////////////////////////////////////////////////////*/ /** @@ -27,14 +66,22 @@ contract TokenBoundAccount is Account, IERC6551Account { * @param _factory - The factory contract address to issue token Bound accounts * */ - constructor(IEntryPoint _entrypoint, address _factory) Account(_entrypoint, _factory) { + constructor(IEntryPoint _entrypoint, address _factory) EIP712("TokenBoundAccount", "1") { _disableInitializers(); + factory = _factory; + entrypointContract = _entrypoint; } - receive() external payable override(IERC6551Account, Account) {} + // solhint-disable-next-line no-empty-blocks + receive() external payable virtual {} + + /// @notice Initializes the smart contract wallet. + function initialize(address _defaultAdmin, bytes calldata _data) public virtual initializer { + emit TokenBoundAccountCreated(_defaultAdmin, _data); + } /// @notice Returns whether a signer is authorized to perform transactions using the wallet. - function isValidSigner(address _signer, UserOperation calldata) public view override returns (bool) { + function isValidSigner(address _signer, UserOperation calldata) public view returns (bool) { return (owner() == _signer); } @@ -70,7 +117,7 @@ contract TokenBoundAccount is Account, IERC6551Account { } /// @notice Withdraw funds for this account from Entrypoint. - function withdrawDepositTo(address payable withdrawAddress, uint256 amount) public virtual override { + function withdrawDepositTo(address payable withdrawAddress, uint256 amount) public virtual { require(owner() == msg.sender, "Account: not NFT owner"); entryPoint().withdrawTo(withdrawAddress, amount); } @@ -91,6 +138,50 @@ contract TokenBoundAccount is Account, IERC6551Account { return getNonce(); } + /// @notice See {IERC165-supportsInterface}. + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC1155Receiver) returns (bool) { + return + interfaceId == type(IERC1155Receiver).interfaceId || + interfaceId == type(IERC721Receiver).interfaceId || + super.supportsInterface(interfaceId); + } + + /// @notice Returns the EIP 4337 entrypoint contract. + function entryPoint() public view virtual override returns (IEntryPoint) { + return entrypointContract; + } + + /// @notice Returns the balance of the account in Entrypoint. + function getDeposit() public view virtual returns (uint256) { + return entryPoint().balanceOf(address(this)); + } + + /// @notice Executes a transaction (called directly from an admin, or by entryPoint) + function execute( + address _target, + uint256 _value, + bytes calldata _calldata + ) external virtual onlyAdminOrEntrypoint { + _call(_target, _value, _calldata); + } + + /// @notice Executes a sequence transaction (called directly from an admin, or by entryPoint) + function executeBatch( + address[] calldata _target, + uint256[] calldata _value, + bytes[] calldata _calldata + ) external virtual onlyAdminOrEntrypoint { + require(_target.length == _calldata.length && _target.length == _value.length, "Account: wrong array lengths."); + for (uint256 i = 0; i < _target.length; i++) { + _call(_target[i], _value[i], _calldata[i]); + } + } + + /// @notice Deposit funds for this account in Entrypoint. + function addDeposit() public payable virtual { + entryPoint().depositTo{ value: msg.value }(address(this)); + } + /*/////////////////////////////////////////////////////////////// Internal Functions //////////////////////////////////////////////////////////////*/ @@ -99,7 +190,7 @@ contract TokenBoundAccount is Account, IERC6551Account { address _target, uint256 value, bytes memory _calldata - ) internal virtual override returns (bytes memory result) { + ) internal virtual returns (bytes memory result) { bool success; (success, result) = _target.call{ value: value }(_calldata); if (!success) { @@ -109,12 +200,65 @@ contract TokenBoundAccount is Account, IERC6551Account { } } + /// @notice Validates the signature of a user operation. + function _validateSignature(UserOperation calldata userOp, bytes32 userOpHash) + internal + virtual + override + returns (uint256 validationData) + { + bytes32 hash = userOpHash.toEthSignedMessageHash(); + address signer = hash.recover(userOp.signature); + + if (!isValidSigner(signer, userOp)) return SIG_VALIDATION_FAILED; + return 0; + } + + function getFunctionSignature(bytes calldata data) internal pure returns (bytes4 functionSelector) { + require(data.length >= 4, "Data too short"); + return bytes4(data[:4]); + } + + function decodeExecuteCalldata(bytes calldata data) internal pure returns (address _target, uint256 _value) { + require(data.length >= 4 + 32 + 32, "Data too short"); + + // Decode the address, which is bytes 4 to 35 + _target = abi.decode(data[4:36], (address)); + + // Decode the value, which is bytes 36 to 68 + _value = abi.decode(data[36:68], (uint256)); + } + + function decodeExecuteBatchCalldata(bytes calldata data) + internal + pure + returns ( + address[] memory _targets, + uint256[] memory _values, + bytes[] memory _callData + ) + { + require(data.length >= 4 + 32 + 32 + 32, "Data too short"); + + (_targets, _values, _callData) = abi.decode(data[4:], (address[], uint256[], bytes[])); + } + + /// @dev Returns whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view virtual override returns (bool) { + return msg.sender == owner(); + } + /*/////////////////////////////////////////////////////////////// Modifiers //////////////////////////////////////////////////////////////*/ - modifier onlyAdminOrEntrypoint() override { + modifier onlyAdminOrEntrypoint() { require(msg.sender == address(entryPoint()) || msg.sender == owner(), "Account: not admin or EntryPoint."); _; } + + modifier onlyAdmin() { + require(msg.sender == owner(), "Account: not admin."); + _; + } }