// SPDX-License-Identifier: MIT pragma solidity 0.8.23; import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import "./interfaces/ISeason1.sol"; /** * @title AereQuestCheckIn — "First Flight" soulbound attestation * @notice A SOULBOUND (non-transferable) ERC-721-shaped attestation minted * exactly once per account. It is the on-chain "First Flight" marker of * Season 1 onboarding, intended to be minted via an * AereOnboardingPaymaster-sponsored UserOp so the user pays no gas. * * Soulbound: tokenId is deterministically uint256(uint160(account)), so an * account can hold at most one, and it can never be transferred — every * transfer/approval entrypoint HARD-REVERTS (Soulbound). * * ERC-721 read surface (ownerOf/balanceOf/tokenURI/supportsInterface) and the * mint Transfer event are provided so wallets and the AERE indexer can render * the badge. There is no burn (the attestation is permanent). * * No owner / no admin: the only state transition is a one-shot self check-in. * The referral registry reads `hasCheckedIn` as one of its activation gates. */ contract AereQuestCheckIn is IERC165, IAereQuestCheckIn { string public constant name = "AERE First Flight"; string public constant symbol = "FLIGHT"; /// @notice account => minted. mapping(address => bool) private _minted; uint256 public totalCheckedIn; /// @dev ERC-721 events (mint only; no transfers ever happen). event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); event CheckedIn(address indexed account, uint256 tokenId, uint256 timestamp); error Soulbound(); error AlreadyCheckedIn(); error NotMinted(); error ZeroAccount(); /* ------------------------------- check-in ------------------------------- */ /// @notice Mint the caller's First-Flight attestation. One per account. /// In production this is called by the user's (passkey) smart /// account inside a paymaster-sponsored UserOp, so msg.sender is /// that account and the badge is bound to it. function checkIn() external returns (uint256 tokenId) { return _checkIn(msg.sender); } function _checkIn(address account) internal returns (uint256 tokenId) { if (account == address(0)) revert ZeroAccount(); if (_minted[account]) revert AlreadyCheckedIn(); _minted[account] = true; totalCheckedIn += 1; tokenId = uint256(uint160(account)); emit Transfer(address(0), account, tokenId); emit CheckedIn(account, tokenId, block.timestamp); } /* --------------------------------- views -------------------------------- */ function hasCheckedIn(address account) external view returns (bool) { return _minted[account]; } function balanceOf(address account) external view returns (uint256) { if (account == address(0)) revert ZeroAccount(); return _minted[account] ? 1 : 0; } function ownerOf(uint256 tokenId) public view returns (address) { address account = address(uint160(tokenId)); if (!_minted[account]) revert NotMinted(); return account; } function tokenURI(uint256 tokenId) external view returns (string memory) { ownerOf(tokenId); // reverts NotMinted if absent // Static on-chain metadata; no external hosting dependency. return "data:application/json;base64,eyJuYW1lIjoiQUVSRSBGaXJzdCBGbGlnaHQiLCJkZXNjcmlwdGlvbiI6IlNvdWxib3VuZCBTZWFzb24gMSBBbHRpdHVkZSBjaGVjay1pbi4gUG9pbnRzIGFyZSBub3QgYSB0b2tlbiBhbmQgaGF2ZSBubyBndWFyYW50ZWVkIHZhbHVlLiJ9"; } function supportsInterface(bytes4 interfaceId) external pure returns (bool) { return interfaceId == type(IERC165).interfaceId || interfaceId == 0x80ac58cd || // ERC-721 interfaceId == 0x5b5e139f; // ERC-721 Metadata } /* --------------------------- bricked ERC-721 write ---------------------- */ function transferFrom(address, address, uint256) external pure { revert Soulbound(); } function safeTransferFrom(address, address, uint256) external pure { revert Soulbound(); } function safeTransferFrom(address, address, uint256, bytes calldata) external pure { revert Soulbound(); } function approve(address, uint256) external pure { revert Soulbound(); } function setApprovalForAll(address, bool) external pure { revert Soulbound(); } function getApproved(uint256) external pure returns (address) { return address(0); } function isApprovedForAll(address, address) external pure returns (bool) { return false; } }