aere-contracts/contracts/season1/AereReferralRegistry.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

254 lines
11 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "./interfaces/ISeason1.sol";
/**
* @title AereReferralRegistry — AERE Season 1 "Altitude" referrals
* @notice Referral codes bound to accounts. A referrer is credited AP ONLY
* once their invitee reaches ACTIVATION, defined as ALL of:
*
* 1. a passkey smart account was created for the invitee
* (checked via a Foundation-settable `passkeyFlag` oracle over the
* live passkey-account source), AND
* 2. the invitee completed First Flight check-in
* (AereQuestCheckIn.hasCheckedIn), AND
* 3. the invitee has been credited for >= MIN_DISTINCT_QUESTS
* distinct on-chain quests (AereQuestAttestor.distinctQuestCount).
*
* This "activation, not signup" rule is the core anti-farm defense:
* a referrer earns nothing for a wallet that just binds a code, only
* for one that actually onboards and uses the chain.
*
* REWARD SHAPE (all Foundation-tunable):
* - DIMINISHING RETURNS: per-referral reward halves every `halvingEvery`
* activated referrals, floored at `minReward`.
* - CAPPED ONGOING SHARE: cumulative referral AP per referrer is capped at
* cap(tier); rewards clamp to the remaining headroom.
* - PER-ACCOUNT CAP RAISED BY TIER: tier = activatedReferrals / tierSize,
* and cap(tier) = tierCapBase + tier * tierCapStep, so a proven referrer
* unlocks more headroom over time instead of an unlimited faucet.
*
* This contract is authorized as an attestor on the ledger by the Foundation
* and credits referrers directly via LEDGER.credit(...). It holds no funds.
* Owner = Foundation multisig.
*
* DISCLAIMER: AP is not a token, has no guaranteed value, and confers no
* right to returns. Rewards depend on the network growing and are not
* promised. There is no listing.
*/
contract AereReferralRegistry is Ownable, ReentrancyGuard {
bytes32 public constant REFERRAL_QUEST_ID = keccak256("season1-referral");
IAerePointsLedger public immutable LEDGER;
IAereQuestCheckIn public immutable CHECKIN;
IAereQuestAttestor public immutable ATTESTOR;
/// @notice Settable "has a passkey account" flag oracle for activation.
IAccountFlag public passkeyFlag;
/* -------------------------------- config -------------------------------- */
uint256 public minDistinctQuests = 2;
uint256 public baseReward = 50; // AP for a referral in the first bracket
uint256 public halvingEvery = 10; // halve per-referral reward every N referrals
uint256 public minReward = 5; // floor
uint256 public tierSize = 10; // referrals per tier step
uint256 public tierCapBase = 250; // cap at tier 0
uint256 public tierCapStep = 250; // extra cap per tier
/* --------------------------------- state -------------------------------- */
mapping(bytes32 => address) public codeOwner; // code => referrer
mapping(address => bytes32) public accountCode; // referrer => code
mapping(address => address) public referredBy; // invitee => referrer
mapping(address => bool) public referralCredited; // invitee => paid out
mapping(address => uint256) public activatedReferrals; // referrer => count
mapping(address => uint256) public referralPointsEarned; // referrer => AP
/* --------------------------------- events ------------------------------- */
event PasskeyFlagSet(address indexed oracle);
event ReferralParamsSet(
uint256 minDistinctQuests,
uint256 baseReward,
uint256 halvingEvery,
uint256 minReward,
uint256 tierSize,
uint256 tierCapBase,
uint256 tierCapStep
);
event CodeRegistered(address indexed referrer, bytes32 indexed code);
event ReferrerBound(address indexed invitee, address indexed referrer, bytes32 indexed code);
event ReferralCredited(
address indexed referrer,
address indexed invitee,
uint256 reward,
uint256 activatedReferralCount
);
/* --------------------------------- errors ------------------------------- */
error ZeroCode();
error CodeTaken();
error AlreadyHasCode();
error UnknownCode();
error SelfReferral();
error AlreadyBound();
error NoReferrer();
error AlreadyCredited();
error NotActivated();
error BadParams();
constructor(address ledger, address checkIn, address attestor) {
require(ledger != address(0) && checkIn != address(0) && attestor != address(0), "zero-ref");
LEDGER = IAerePointsLedger(ledger);
CHECKIN = IAereQuestCheckIn(checkIn);
ATTESTOR = IAereQuestAttestor(attestor);
}
/* --------------------------------- admin -------------------------------- */
function setPasskeyFlag(address oracle) external onlyOwner {
passkeyFlag = IAccountFlag(oracle);
emit PasskeyFlagSet(oracle);
}
function setReferralParams(
uint256 _minDistinctQuests,
uint256 _baseReward,
uint256 _halvingEvery,
uint256 _minReward,
uint256 _tierSize,
uint256 _tierCapBase,
uint256 _tierCapStep
) external onlyOwner {
if (_halvingEvery == 0 || _tierSize == 0) revert BadParams();
if (_minReward > _baseReward) revert BadParams();
minDistinctQuests = _minDistinctQuests;
baseReward = _baseReward;
halvingEvery = _halvingEvery;
minReward = _minReward;
tierSize = _tierSize;
tierCapBase = _tierCapBase;
tierCapStep = _tierCapStep;
emit ReferralParamsSet(
_minDistinctQuests, _baseReward, _halvingEvery, _minReward,
_tierSize, _tierCapBase, _tierCapStep
);
}
/* ------------------------------- codes ---------------------------------- */
/// @notice Claim a unique referral code for the caller. One code per account.
/// @dev CODES ARE NON-SEMANTIC AND FIRST-COME. The contract attaches no meaning
/// to the bytes32: any caller may claim any currently-unused value, and the
/// claim is therefore front-runnable. A "branded" string (for example a
/// company or handle) is NOT reserved for anyone and can be squatted by the
/// first sender to register it. This is acceptable for this version because
/// a code carries no monetary value: AP is non-transferable, has no listing,
/// and a squatted code only routes activation credit to whoever holds it, it
/// cannot take funds or another account's AP. Branded-code reservation is
/// intentionally OUT OF SCOPE for this version. If branded codes ever matter,
/// a future version should add an owner-gated reservation list or derive the
/// code deterministically from msg.sender instead of accepting arbitrary bytes.
function registerCode(bytes32 code) external {
if (code == bytes32(0)) revert ZeroCode();
if (codeOwner[code] != address(0)) revert CodeTaken();
if (accountCode[msg.sender] != bytes32(0)) revert AlreadyHasCode();
codeOwner[code] = msg.sender;
accountCode[msg.sender] = code;
emit CodeRegistered(msg.sender, code);
}
/// @notice Bind the caller (invitee) to the referrer who owns `code`.
/// One-shot and immutable; cannot self-refer.
function bindReferrer(bytes32 code) external {
if (referredBy[msg.sender] != address(0)) revert AlreadyBound();
address referrer = codeOwner[code];
if (referrer == address(0)) revert UnknownCode();
if (referrer == msg.sender) revert SelfReferral();
referredBy[msg.sender] = referrer;
emit ReferrerBound(msg.sender, referrer, code);
}
/* ----------------------------- activation ------------------------------- */
/// @notice True iff `invitee` satisfies all three activation gates.
function isActivated(address invitee) public view returns (bool) {
if (address(passkeyFlag) == address(0)) return false;
if (!passkeyFlag.isFlagged(invitee)) return false;
if (!CHECKIN.hasCheckedIn(invitee)) return false;
if (ATTESTOR.distinctQuestCount(invitee) < minDistinctQuests) return false;
return true;
}
/// @notice Credit the referrer of `invitee` once the invitee has activated.
/// Permissionless: anyone may trigger (referrer or a relayer).
function claimReferral(address invitee) external nonReentrant {
address referrer = referredBy[invitee];
if (referrer == address(0)) revert NoReferrer();
if (referralCredited[invitee]) revert AlreadyCredited();
if (!isActivated(invitee)) revert NotActivated();
uint256 n = activatedReferrals[referrer];
uint256 reward = _diminishingReward(n);
uint256 cap = _capForCount(n);
uint256 already = referralPointsEarned[referrer];
uint256 remaining = cap > already ? cap - already : 0;
uint256 actual = reward < remaining ? reward : remaining;
// Effects first (reentrancy-safe; also guarded).
referralCredited[invitee] = true;
activatedReferrals[referrer] = n + 1;
if (actual > 0) {
referralPointsEarned[referrer] = already + actual;
}
// Interaction: credit the ledger only when there is headroom.
if (actual > 0) {
LEDGER.credit(referrer, actual, REFERRAL_QUEST_ID);
}
emit ReferralCredited(referrer, invitee, actual, n + 1);
}
/* ------------------------------- reward math ---------------------------- */
function _diminishingReward(uint256 n) internal view returns (uint256) {
uint256 shifts = n / halvingEvery;
if (shifts > 255) shifts = 255;
uint256 r = baseReward >> shifts;
return r > minReward ? r : minReward;
}
function _capForCount(uint256 n) internal view returns (uint256) {
uint256 tier = n / tierSize;
return tierCapBase + tier * tierCapStep;
}
/* --------------------------------- views -------------------------------- */
function nextReferralReward(address referrer) external view returns (uint256) {
uint256 n = activatedReferrals[referrer];
uint256 reward = _diminishingReward(n);
uint256 cap = _capForCount(n);
uint256 already = referralPointsEarned[referrer];
uint256 remaining = cap > already ? cap - already : 0;
return reward < remaining ? reward : remaining;
}
function referralStats(address referrer)
external
view
returns (uint256 activated, uint256 pointsEarned, uint256 currentCap)
{
activated = activatedReferrals[referrer];
pointsEarned = referralPointsEarned[referrer];
currentCap = _capForCount(activated);
}
}