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.
254 lines
11 KiB
Solidity
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);
|
|
}
|
|
}
|