// SPDX-License-Identifier: MIT pragma solidity 0.8.23; import "@openzeppelin/contracts/access/Ownable.sol"; import "./interfaces/ISeason1.sol"; /** * @title AereQuestRegistry — AERE Season 1 "Altitude" quest catalogue * @notice Foundation-owned registry describing every quest: its point weight, * active window, per-user cap, active flag, and whether it requires a * verified human. Fully TUNABLE without redeploying the ledger or the * attestor — those read quest config live from here. * * A quest is keyed by a bytes32 questId (e.g. keccak256("create-passkey")). * * This contract holds NO funds and issues NO credits. It is pure config that * the attestor and referral registry consult. Owner = Foundation multisig. */ contract AereQuestRegistry is Ownable, IAereQuestRegistry { mapping(bytes32 => Quest) private _quests; bytes32[] public questIds; event QuestCreated( bytes32 indexed questId, uint256 weight, uint64 startTime, uint64 endTime, uint256 perUserCap, bool requiresVerifiedHuman ); event QuestUpdated( bytes32 indexed questId, uint256 weight, uint64 startTime, uint64 endTime, uint256 perUserCap, bool requiresVerifiedHuman ); event QuestActiveSet(bytes32 indexed questId, bool active); error QuestExists(); error UnknownQuest(); error BadWindow(); error BadWeight(); /* ------------------------------- mutations ------------------------------ */ function createQuest( bytes32 questId, uint256 weight, uint64 startTime, uint64 endTime, uint256 perUserCap, bool requiresVerifiedHuman ) external onlyOwner { if (questId == bytes32(0)) revert UnknownQuest(); if (_quests[questId].exists) revert QuestExists(); _validate(weight, startTime, endTime, perUserCap); _quests[questId] = Quest({ weight: weight, startTime: startTime, endTime: endTime, perUserCap: perUserCap, active: true, requiresVerifiedHuman: requiresVerifiedHuman, exists: true }); questIds.push(questId); emit QuestCreated(questId, weight, startTime, endTime, perUserCap, requiresVerifiedHuman); } function updateQuest( bytes32 questId, uint256 weight, uint64 startTime, uint64 endTime, uint256 perUserCap, bool requiresVerifiedHuman ) external onlyOwner { Quest storage q = _quests[questId]; if (!q.exists) revert UnknownQuest(); _validate(weight, startTime, endTime, perUserCap); q.weight = weight; q.startTime = startTime; q.endTime = endTime; q.perUserCap = perUserCap; q.requiresVerifiedHuman = requiresVerifiedHuman; emit QuestUpdated(questId, weight, startTime, endTime, perUserCap, requiresVerifiedHuman); } function setQuestActive(bytes32 questId, bool active) external onlyOwner { if (!_quests[questId].exists) revert UnknownQuest(); _quests[questId].active = active; emit QuestActiveSet(questId, active); } function _validate(uint256 weight, uint64 startTime, uint64 endTime, uint256 perUserCap) internal pure { if (weight == 0) revert BadWeight(); // perUserCap must admit at least one completion. if (perUserCap < weight) revert BadWeight(); // endTime 0 == open-ended; otherwise must be after start. if (endTime != 0 && endTime <= startTime) revert BadWindow(); } /* --------------------------------- views -------------------------------- */ function getQuest(bytes32 questId) external view returns (Quest memory) { Quest memory q = _quests[questId]; if (!q.exists) revert UnknownQuest(); return q; } /// @notice True iff the quest exists, is active, and `atTime` is within its /// window. endTime == 0 means open-ended. function isOpen(bytes32 questId, uint256 atTime) external view returns (bool) { Quest memory q = _quests[questId]; if (!q.exists || !q.active) return false; if (atTime < q.startTime) return false; if (q.endTime != 0 && atTime > q.endTime) return false; return true; } function questCount() external view returns (uint256) { return questIds.length; } }