aere-contracts/contracts/tokenbound/AereTokenBoundAccount.sol
Aere Network a13a649b77
Some checks are pending
contracts-ci / Install (lockfile) → compile → full test suite (push) Waiting to run
contracts-ci / PQC known-answer tests (NIST vectors) (push) Waiting to run
contracts-ci / Coverage (scoped, with artifacts) (push) Waiting to run
Initial public release
Aere Network public source. Everything here can be checked against the live
chain (chain id 2800, https://rpc.aere.network).

Scope note, stated up front rather than buried: consensus on chain 2800 is
classical secp256k1 ECDSA QBFT. The post-quantum work in this repository is at
the signature, precompile, account and transport layers. Nothing here makes the
consensus post-quantum, and no document in it should be read as claiming so.
2026-07-20 01:02:37 +03:00

230 lines
9.0 KiB
Solidity

// 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;
}
}