// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "./IReferralRewards.sol";
/// @notice Referral reward engine adapted from ThunderCore's referral library idea,
/// but designed to be paid in an ERC20 token by the caller (EventManager).
///
/// Key points:
/// - 3 levels maximum.
/// - Bonus depends on (a) referral bonus %, (b) level split %, (c) upline referee-count rate.
/// - "Active" gating: when enabled, only uplines active within `secondsUntilInactive` earn.
/// - This contract does not transfer tokens; it returns computed payouts and updates state.
/// The operator (EventManager) performs ERC20 transfers from its own balance.
contract ReferralRewards is AccessControl, IReferralRewards {
bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE");
struct RateStep {
uint32 refereeCount; // minimum direct referees required
uint32 rate; // [0..decimals] where decimals is typically 10000 (bps-style)
}
IERC20 public immutable rewardToken;
uint32 public immutable decimals;
uint32 public referralBonus; // [0..decimals]
uint32 public secondsUntilInactive;
bool public onlyRewardActiveReferrers;
// levelRates[0] = level-1 share; [1] level-2; [2] level-3. Each [0..decimals].
uint32[3] public levelRates;
RateStep[] private _rateSteps;
mapping(address => address) public referrerOf;
mapping(address => uint32) public directRefereeCount;
mapping(address => uint256) public lastActiveAt;
event ReferrerSet(address indexed referee, address indexed referrer);
event ActiveUpdated(address indexed user, uint256 timestamp);
event ConfigUpdated(
uint32 referralBonus,
uint32 secondsUntilInactive,
bool onlyRewardActiveReferrers,
uint32 level1,
uint32 level2,
uint32 level3
);
error InvalidConfig();
constructor(
IERC20 _rewardToken,
address admin,
address operator,
uint32 _decimals,
uint32 _referralBonus,
uint32 _secondsUntilInactive,
bool _onlyRewardActiveReferrers,
uint32[3] memory _levelRates,
RateStep[] memory rateSteps
) {
rewardToken = _rewardToken;
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(OPERATOR_ROLE, operator);
if (_decimals == 0) revert InvalidConfig();
decimals = _decimals;
_setConfig(
_referralBonus,
_secondsUntilInactive,
_onlyRewardActiveReferrers,
_levelRates
);
_setRateSteps(rateSteps);
}
function setOperator(address operator) external onlyRole(DEFAULT_ADMIN_ROLE) {
_grantRole(OPERATOR_ROLE, operator);
}
function setConfig(
uint32 _referralBonus,
uint32 _secondsUntilInactive,
bool _onlyRewardActiveReferrers,
uint32[3] memory _levelRates
) external onlyRole(DEFAULT_ADMIN_ROLE) {
_setConfig(
_referralBonus,
_secondsUntilInactive,
_onlyRewardActiveReferrers,
_levelRates
);
}
function setRateSteps(
RateStep[] calldata rateSteps
) external onlyRole(DEFAULT_ADMIN_ROLE) {
_setRateSteps(rateSteps);
}
function rateStepsLength() external view returns (uint256) {
return _rateSteps.length;
}
function rateStepAt(uint256 idx) external view returns (RateStep memory) {
return _rateSteps[idx];
}
function isActive(address user) public view returns (bool) {
uint256 ts = lastActiveAt[user];
if (ts == 0) return false;
// secondsUntilInactive == 0 means "always active"
if (secondsUntilInactive == 0) return true;
return block.timestamp <= ts + secondsUntilInactive;
}
function processPurchase(
address referee,
address referrer,
uint256 amount
)
external
override
onlyRole(OPERATOR_ROLE)
returns (
address[3] memory recipients,
uint256[3] memory bonuses,
uint256 totalPaid
)
{
// Mark the buyer active even if they have no referrer.
lastActiveAt[referee] = block.timestamp;
emit ActiveUpdated(referee, block.timestamp);
// Bind referrer on first purchase if valid hint is provided.
if (referrer != address(0)) {
_trySetReferrer(referee, referrer);
}
if (amount == 0 || referralBonus == 0) {
return (recipients, bonuses, 0);
}
uint256 referralBase = (amount * uint256(referralBonus)) / uint256(decimals);
address current = referee;
for (uint256 level = 0; level < 3; level++) {
address upline = referrerOf[current];
if (upline == address(0)) break;
recipients[level] = upline;
if (!onlyRewardActiveReferrers || isActive(upline)) {
uint256 refereeRate = uint256(_refereeCountRate(upline));
uint256 levelRate = uint256(levelRates[level]);
uint256 bonus = referralBase;
bonus = (bonus * refereeRate) / uint256(decimals);
bonus = (bonus * levelRate) / uint256(decimals);
bonuses[level] = bonus;
totalPaid += bonus;
}
current = upline;
}
return (recipients, bonuses, totalPaid);
}
function _trySetReferrer(address referee, address referrer) internal {
if (referee == address(0) || referrer == address(0)) return;
if (referee == referrer) return;
if (referrerOf[referee] != address(0)) return;
// Prevent short cycles within the 3-level depth this system uses.
address cur = referrer;
for (uint256 i = 0; i < 3; i++) {
if (cur == address(0)) break;
if (cur == referee) return;
cur = referrerOf[cur];
}
referrerOf[referee] = referrer;
directRefereeCount[referrer] += 1;
emit ReferrerSet(referee, referrer);
}
function _refereeCountRate(address user) internal view returns (uint32) {
// Default: 100% (no reduction).
if (_rateSteps.length == 0) return decimals;
uint32 cnt = directRefereeCount[user];
uint32 best = 0;
// Steps must be sorted ascending by refereeCount.
for (uint256 i = 0; i < _rateSteps.length; i++) {
if (cnt < _rateSteps[i].refereeCount) break;
best = _rateSteps[i].rate;
}
// If user has fewer referees than the first threshold, best will be 0.
return best;
}
function _setConfig(
uint32 _referralBonus,
uint32 _secondsUntilInactive,
bool _onlyRewardActiveReferrers,
uint32[3] memory _levelRates
) internal {
if (_referralBonus > decimals) revert InvalidConfig();
// Require the level split to be <= 100%.
uint256 sum = uint256(_levelRates[0]) +
uint256(_levelRates[1]) +
uint256(_levelRates[2]);
if (sum > uint256(decimals)) revert InvalidConfig();
referralBonus = _referralBonus;
secondsUntilInactive = _secondsUntilInactive;
onlyRewardActiveReferrers = _onlyRewardActiveReferrers;
levelRates = _levelRates;
emit ConfigUpdated(
referralBonus,
secondsUntilInactive,
onlyRewardActiveReferrers,
levelRates[0],
levelRates[1],
levelRates[2]
);
}
function _setRateSteps(RateStep[] memory rateSteps) internal {
delete _rateSteps;
uint32 prev = 0;
for (uint256 i = 0; i < rateSteps.length; i++) {
if (rateSteps[i].rate > decimals) revert InvalidConfig();
if (i > 0 && rateSteps[i].refereeCount <= prev) revert InvalidConfig();
prev = rateSteps[i].refereeCount;
_rateSteps.push(rateSteps[i]);
}
}
}
Homonyms
cyberia/research/events/backend/ethereum_artifacts/contracts/ReferralRewards.sol
ReferralRewards.sol