// SPDX-License-Identifier: MIT pragma solidity 0.8.23; import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import "@openzeppelin/contracts/interfaces/IERC1271.sol"; import "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; /// @dev ERC-6551 account interface (ERC-165 id 0x6faff5f1). interface IERC6551Account { receive() external payable; function token() external view returns (uint256 chainId, address tokenContract, uint256 tokenId); function state() external view returns (uint256); function isValidSigner(address signer, bytes calldata context) external view returns (bytes4 magicValue); } /// @dev ERC-6551 executable interface (ERC-165 id 0x51945447). interface IERC6551Executable { function execute(address to, uint256 value, bytes calldata data, uint8 operation) external payable returns (bytes memory); } /** * @title AereTokenBoundAccount — ERC-6551 token-bound smart account for AERE. * * @notice A wallet OWNED BY AN NFT. Deployed as a deterministic ERC-1167 proxy * by the canonical ERC6551Registry and bound at creation to a specific * (chainId, tokenContract, tokenId). Whoever currently holds that NFT * controls this account: they can execute arbitrary calls, and the * account produces EIP-1271 signatures on their behalf. The account can * hold native AERE and any tokens/NFTs like any other address. * * This is an ACCOUNT PRIMITIVE, not a fungible/currency/governance token. * It mints nothing, has no supply, and issues no transferable claim. Its * only authority source is `ownerOf(tokenId)` on the bound ERC-721. * * EXECUTION + SIGNING (reuses AERE's 4337 / EIP-1271 pattern from * AerePasskeyAccountV2): the same "owner authenticates, then call the * target" shape, and the same EIP-1271 `isValidSignature` shape, but the * owner set is derived from NFT ownership rather than a stored key set. * * Two authorization paths: * 1. execute(...) — msg.sender must be the current NFT holder. * 2. validateUserOp / executeFromEntryPoint — ERC-4337 path through the * deployed AereEntryPointV2 (0x8D6f…aF770); the owner signs the * userOpHash and the EntryPoint relays. Same 4337 shape as the * passkey account, letting a bundler/paymaster sponsor the NFT * wallet's gas. * * Only CALL (operation 0) is supported; DELEGATECALL/CREATE are rejected * so the bound account can never be hijacked into changing its own logic. */ contract AereTokenBoundAccount is IERC165, IERC1271, IERC6551Account, IERC6551Executable { /// @notice Monotonic counter bumped on every successful execute — ERC-6551 `state`. uint256 public state; /// @notice Deployed AERE ERC-4337 EntryPoint (AereEntryPointV2). Fixed constant: /// ERC-6551 proxies share one implementation and take no constructor args. address public constant ENTRY_POINT = 0x8D6f40598d552fF0Cb358b6012cF4227B86aF770; /// @notice EIP-1271 magic value on success. bytes4 internal constant EIP1271_MAGIC = 0x1626ba7e; /// @notice ERC-4337 v0.7 PackedUserOperation — identical shape to AereEntryPointV2. struct PackedUserOperation { address sender; uint256 nonce; bytes initCode; bytes callData; bytes32 accountGasLimits; uint256 preVerificationGas; bytes32 gasFees; bytes paymasterAndData; bytes signature; } event Executed(uint256 indexed state, address indexed to, uint256 value, bytes data); error InvalidSigner(); error UnsupportedOperation(uint8 operation); error NotFromEntryPoint(); receive() external payable {} // ─── ERC-6551 execution (direct owner path) ─────────────────────────────── /// @notice Execute a call authorized by the current NFT holder. /// @param operation must be 0 (CALL); anything else reverts. function execute(address to, uint256 value, bytes calldata data, uint8 operation) external payable override returns (bytes memory result) { if (!_isValidSigner(msg.sender)) revert InvalidSigner(); if (operation != 0) revert UnsupportedOperation(operation); ++state; bool success; (success, result) = to.call{value: value}(data); if (!success) { // bubble up the revert reason assembly { revert(add(result, 32), mload(result)) } } emit Executed(state, to, value, data); } // ─── ERC-4337 path (through AereEntryPointV2) ───────────────────────────── /// @notice ERC-4337 validation. Returns 0 on success, 1 on bad signature /// (per spec — does not revert on signature failure). function validateUserOp( PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds ) external returns (uint256 validationData) { if (msg.sender != ENTRY_POINT) revert NotFromEntryPoint(); // Owner (the NFT holder) must have signed the userOpHash. SignatureChecker // supports both EOA holders and smart-contract (EIP-1271) holders. bool ok = SignatureChecker.isValidSignatureNow(owner(), userOpHash, userOp.signature); if (!ok) return 1; // SIG_VALIDATION_FAILED if (missingAccountFunds > 0) { (bool sent, ) = payable(msg.sender).call{value: missingAccountFunds}(""); (sent); // EntryPoint reverts if underfunded } return 0; } /// @notice Post-validation execution dispatched by the EntryPoint. function executeFromEntryPoint(address to, uint256 value, bytes calldata data) external returns (bytes memory result) { if (msg.sender != ENTRY_POINT) revert NotFromEntryPoint(); ++state; bool success; (success, result) = to.call{value: value}(data); if (!success) { assembly { revert(add(result, 32), mload(result)) } } emit Executed(state, to, value, data); } // ─── ERC-6551 signer / EIP-1271 ─────────────────────────────────────────── function isValidSigner(address signer, bytes calldata) external view override returns (bytes4) { if (_isValidSigner(signer)) return IERC6551Account.isValidSigner.selector; return bytes4(0); } /// @notice EIP-1271: an off-chain signature is valid iff it is a valid /// signature by the current NFT holder over `hash`. function isValidSignature(bytes32 hash, bytes memory signature) external view override returns (bytes4 magicValue) { return SignatureChecker.isValidSignatureNow(owner(), hash, signature) ? EIP1271_MAGIC : bytes4(0); } // ─── ERC-6551 binding / ownership ───────────────────────────────────────── /// @notice The (chainId, tokenContract, tokenId) this account is bound to, /// read from the ERC-1167 footer appended by the registry. function token() public view override returns (uint256 chainId, address tokenContract, uint256 tokenId) { bytes memory footer = new bytes(0x60); assembly { // proxy runtime is 45 bytes; the 3 immutable words start at 0x4d. extcodecopy(address(), add(footer, 0x20), 0x4d, 0x60) } return abi.decode(footer, (uint256, address, uint256)); } /// @notice The current controller = holder of the bound NFT. function owner() public view returns (address) { (uint256 chainId, address tokenContract, uint256 tokenId) = token(); if (chainId != block.chainid) return address(0); return IERC721(tokenContract).ownerOf(tokenId); } function _isValidSigner(address signer) internal view returns (bool) { return signer != address(0) && signer == owner(); } // ─── ERC-165 ────────────────────────────────────────────────────────────── function supportsInterface(bytes4 interfaceId) external pure override returns (bool) { return interfaceId == type(IERC165).interfaceId || interfaceId == type(IERC6551Account).interfaceId || interfaceId == type(IERC6551Executable).interfaceId || interfaceId == type(IERC1271).interfaceId; } }