// SPDX-License-Identifier: MIT pragma solidity 0.8.23; import "@openzeppelin/contracts/access/Ownable.sol"; /** * @title AerePointsLedger — AERE Season 1 "Altitude" non-transferable points * @notice A CREDIT-ONLY, NON-TRANSFERABLE ledger of "Altitude Points" (AP). * * HARD SAFETY DESIGN — AP is NOT a token and can never become one: * - transfer / transferFrom / approve HARD-REVERT (NotTransferable). AP can * never move between accounts, so it can never trade as a shadow token. * - There is NO burn, NO redeem, NO conversion, NO "claim your tokens" * path anywhere in this contract. AP is a scoreboard, nothing else. * - Only Foundation-authorized attestors may `credit`. There is no mint() * callable by users and no way for AP to leave an account. * * HONESTY / DISCLAIMER (must accompany any user-facing surfacing of AP): * Altitude Points are NOT a token, carry NO guaranteed value, and confer * NO right to returns. Any future reward depends on the network growing * and is not promised. There is no listing. * * ERC-20 read shape (name/symbol/decimals/balanceOf/totalSupply) plus a * mint-style Transfer(address(0), account, amount) event are emitted purely * so block explorers / the AERE indexer can display balances. The WRITE side * of ERC-20 is deliberately bricked. * * Owner = Foundation multisig. Ownable only manages the attestor allowlist; * the owner cannot move, mint to itself for transfer, or reduce balances. */ contract AerePointsLedger is Ownable { string public constant name = "AERE Altitude Points"; string public constant symbol = "AP"; uint8 public constant decimals = 0; // whole points, not a token uint256 public totalSupply; mapping(address => uint256) public balanceOf; /// @notice Foundation-authorized credit sources (the quest attestor and the /// referral registry are added here post-deploy). mapping(address => bool) public isAttestor; /* --------------------------------- events ------------------------------- */ /// @dev ERC-20 mint-style event for indexers. address(0) => account only. event Transfer(address indexed from, address indexed to, uint256 value); /// @dev Rich credit event carrying the quest attribution for the indexer. event PointsCredited(address indexed account, uint256 amount, bytes32 indexed questId, uint256 newBalance); event AttestorSet(address indexed attestor, bool allowed); /* --------------------------------- errors ------------------------------- */ error NotTransferable(); error NotAttestor(); error ZeroAccount(); error ZeroAmount(); /* ------------------------------- attestors ------------------------------ */ function setAttestor(address attestor, bool allowed) external onlyOwner { if (attestor == address(0)) revert ZeroAccount(); isAttestor[attestor] = allowed; emit AttestorSet(attestor, allowed); } modifier onlyAttestor() { if (!isAttestor[msg.sender]) revert NotAttestor(); _; } /* -------------------------------- credit -------------------------------- */ /// @notice Credit `amount` AP to `account`, attributed to `questId`. /// Credit-only: balances can only ever go up. function credit(address account, uint256 amount, bytes32 questId) external onlyAttestor { if (account == address(0)) revert ZeroAccount(); if (amount == 0) revert ZeroAmount(); balanceOf[account] += amount; totalSupply += amount; emit Transfer(address(0), account, amount); emit PointsCredited(account, amount, questId, balanceOf[account]); } /* --------------------------- bricked ERC-20 write ----------------------- */ function transfer(address, uint256) external pure returns (bool) { revert NotTransferable(); } function transferFrom(address, address, uint256) external pure returns (bool) { revert NotTransferable(); } function approve(address, uint256) external pure returns (bool) { revert NotTransferable(); } /// @notice Always zero — AP has no allowance concept. function allowance(address, address) external pure returns (uint256) { return 0; } }