Address
0x0f8c0ff59a5e8aebc8589958b28b5ebcd24bf392Current Holdings
$0.00692900
TXs sent
not counted
First Active
2026-06-05
block 26,712,222
Last Active
95 days ago
block 26,793,467
Funded By
not identified
Net worth historyi
3 snapshots · to block 27,463,673coverage change 26 Augcoverage change 27 Augcoverage change 27 Augcoverage change 27 Augcoverage change 27 Aug
exact matchKEYStakingV4solc 0.8.21+commit.d9974bedruntime exact · creation exact
// SPDX-License-Identifier: MIT
pragma solidity 0.8.21;
// evmVersion: paris (PulseChain does not support Cancun MCOPY opcode)
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/Pausable.sol";
// ================================================================
// KEYStakingV4.sol -- MeFi Entertainment LLC
// https://mefi.stream
//
// KEY staking contract governing listener tier access, streaming
// boost multipliers, HEX dividend distribution, and MeFi Card
// qualification on the MeFi platform.
//
// TIERS
// ------
// Collector / 90d lock / 1.35x streaming boost / 0.36% supply
// Curator / 180d lock / 1.72x streaming boost / 0.72% supply
// Legend / 365d lock / 2.45x streaming boost / 1.45% supply
//
// Tier index is 0-based: Collector=0, Curator=1, Legend=2.
// 255 is the sentinel value returned by getTierId() for non-stakers.
//
// ARCHITECTURE
// -------------
// Stakers lock KEY to access listener tiers. The boost multiplier
// is read by ArtistTokenFactory to scale streaming reward output.
//
// Legend stakers receive a configurable share of HEX dividends via
// a Synthetix rewards-per-token accumulator -- proportional, gas-
// efficient, and requiring no iteration over the staker list.
//
// claimAndBurn() is permissionless and rate-limited. It claims KEY
// token dividends, diverts a Legend HEX share, swaps the remainder
// to KEY via BuybackHelper (PulseX), burns 10%, and sends 90% to
// the MysteryBox contract as its prize pool.
//
// stakeForCard() creates a permanent lock (lockExpiry = max uint256)
// for MeFi Mastercard qualification. Card benefits persist as long
// as KEY remains locked.
//
// DEPLOYMENT
// ----------
// 1. Deploy KEYStakingV4 with keyToken, dividendToken (HEX), mysteryBox.
// 2. setOracleAddress(oracle)
// 3. setStreamingRewards(streamingRewardsV8)
// 4. Notify ArtistFactoryV8: setKeyStaking(address(this))
// 5. Notify StreamingRewardsV8: setKeyStaking(address(this))
// 6. Notify MysteryBoxV4: setKeyStaking(address(this))
// ================================================================
interface IERC20 {
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
}
interface IKeyToken is IERC20 {
function claimDividends() external;
}
interface IDexRouter {
function swapExactTokensForETHSupportingFeeOnTransferTokens(
uint256 amountIn, uint256 amountOutMin,
address[] calldata path, address to, uint256 deadline
) external;
function swapExactETHForTokensSupportingFeeOnTransferTokens(
uint256 amountOutMin,
address[] calldata path, address to, uint256 deadline
) external payable;
}
// ================================================================
// BuybackHelper -- Executes PulseX swaps on behalf of KEYStakingV4.
// Isolated in a separate contract so the router approval never
// touches the main staking contract's token balance.
// ================================================================
contract BuybackHelper {
address public immutable stakingContract;
address public immutable keyToken;
address public constant PULSEX_ROUTER = 0x165C3410fC91EF562C50559f7d2289fEbed552d9;
address public constant WPLS = 0xA1077a294dDE1B09bB078844df40758a5D0f9a27;
event BuybackExecuted(uint256 dividendSpent, uint256 plsReceived, uint256 keyBought);
event BuybackFailed(string reason);
constructor(address _stakingContract, address _keyToken) {
require(_stakingContract != address(0) && _keyToken != address(0), "Invalid");
stakingContract = _stakingContract;
keyToken = _keyToken;
}
function buybackAndReturn(address dividendTokenAddr) external {
require(msg.sender == stakingContract, "Only staking");
IERC20 divToken = IERC20(dividendTokenAddr);
if (divToken.allowance(address(this), PULSEX_ROUTER) == 0)
divToken.approve(PULSEX_ROUTER, type(uint256).max);
uint256 divBalance = divToken.balanceOf(address(this));
require(divBalance > 0, "No balance");
address[] memory path1 = new address[](2);
path1[0] = dividendTokenAddr; path1[1] = WPLS;
uint256 plsBefore = address(this).balance;
try IDexRouter(PULSEX_ROUTER).swapExactTokensForETHSupportingFeeOnTransferTokens(
divBalance, 0, path1, address(this), block.timestamp + 300
) {
uint256 plsReceived = address(this).balance - plsBefore;
if (plsReceived == 0) {
divToken.transfer(stakingContract, divToken.balanceOf(address(this)));
emit BuybackFailed("Div->PLS yielded 0"); return;
}
address[] memory path2 = new address[](2);
path2[0] = WPLS; path2[1] = keyToken;
uint256 keyBefore = IERC20(keyToken).balanceOf(address(this));
try IDexRouter(PULSEX_ROUTER).swapExactETHForTokensSupportingFeeOnTransferTokens{value: plsReceived}(
0, path2, address(this), block.timestamp + 300
) {
uint256 keyBought = IERC20(keyToken).balanceOf(address(this)) - keyBefore;
if (keyBought > 0) {
require(IERC20(keyToken).transfer(stakingContract, keyBought), "Transfer failed");
emit BuybackExecuted(divBalance, plsReceived, keyBought);
} else { emit BuybackFailed("PLS->KEY yielded 0"); }
} catch {
(bool ok,) = stakingContract.call{value: address(this).balance}("");
require(ok, "PLS refund failed");
emit BuybackFailed("PLS->KEY reverted");
}
} catch {
divToken.transfer(stakingContract, divBalance);
emit BuybackFailed("Div->PLS reverted");
}
}
function rescueToken(address token) external {
require(msg.sender == stakingContract, "Only staking");
uint256 bal = IERC20(token).balanceOf(address(this));
require(bal > 0, "Nothing");
require(IERC20(token).transfer(stakingContract, bal), "Transfer failed");
}
receive() external payable {}
}
// ================================================================
// KEYStakingV4
// ================================================================
contract KEYStakingV4 is Ownable, ReentrancyGuard, Pausable {
// -- Constants -------------------------------------------------
address public constant DEAD = 0x000000000000000000000000000000000000dEaD;
uint256 public constant TIER_COUNT = 3;
uint256 public constant MAX_HOUSE_TAX_BPS = 1500;
uint8 public constant LEGEND_TIER_ID = 2;
uint256 public constant PERMANENT_LOCK = type(uint256).max;
uint256 public constant HEX_PRECISION = 1e18;
// -- Tier Config -----------------------------------------------
struct TierConfig {
uint256 supplyBps;
uint256 lockDays;
uint256 boostBps;
bool active;
}
TierConfig[3] public tiers;
// -- HEX Dividend Distribution for Legend ----------------------
// Synthetix rewards-per-token accumulator pattern.
// earnedHex(user) = (hexPerKeyStored - hexRewardDebt[user])
// * userKeyAmount / HEX_PRECISION + pendingHex[user]
uint256 public legendHexShareBps = 2000; // 20% of HEX dividends -> Legend stakers
uint256 public hexPerKeyStored;
uint256 public totalLegendStaked;
mapping(address => uint256) public hexRewardDebt;
mapping(address => uint256) public pendingHex;
// -- originalStakedAt ------------------------------------------
// Set ONCE on first stake/stakeForCard. Resets to 0 on unstake.
mapping(address => uint256) public originalStakedAt;
// -- claimAndBurn cooldown -------------------------------------
uint256 public lastClaimTime;
uint256 public claimCooldown = 6 hours;
// -- Immutables ------------------------------------------------
IKeyToken public immutable keyToken;
BuybackHelper public immutable buybackHelper;
// -- Config ----------------------------------------------------
IERC20 public dividendToken;
address public mysteryBoxAddress;
address public oracleAddress;
address public streamingRewards;
uint256 public burnShareBps = 1000; // 10% of buyback KEY burned
uint256 public minClaimThreshold = 0;
uint256 public minBuybackOutput = 0;
uint256 public downgradeHouseTaxBps = 500; // 5%
// -- Stake State -----------------------------------------------
struct StakeInfo {
uint256 amount;
uint256 lockExpiry; // PERMANENT_LOCK for card stakes
uint8 tierId;
uint256 stakedAt;
}
mapping(address => StakeInfo) public stakes;
// -- Primary Artist --------------------------------------------
mapping(address => address) public primaryArtist;
// -- DedicatedListener -----------------------------------------
mapping(address => bool) public dedicatedListener;
// -- Global Stats ----------------------------------------------
uint256 public totalStaked;
uint256 public totalKeyBurned;
uint256 public totalKeyToMysteryBox;
uint256 public totalDividendsClaimed;
uint256 public totalSurplusReturned;
uint256 public totalHouseTaxHeld;
uint256 public totalHexToLegend;
// -- Events ----------------------------------------------------
event Staked(address indexed user, uint8 tierId, uint256 amount, uint256 lockExpiry);
event CardLocked(address indexed user, uint8 tierId, uint256 amount);
event Restaked(address indexed user, uint8 oldTier, uint8 newTier, uint256 newAmount, uint256 lockExpiry);
event Unstaked(address indexed user, uint8 tierId, uint256 returned, uint256 houseTax);
event SurplusReturned(address indexed user, uint256 netSurplus, uint256 houseTaxHeld);
event HouseTaxHeld(address indexed user, uint256 keyAmount);
event PrimaryArtistSet(address indexed listener, address indexed creator);
event LegendStatusAchieved(address indexed user);
event DedicatedListenerSet(address indexed user);
event TierUpdated(uint8 tierId, uint256 supplyBps, uint256 lockDays, uint256 boostBps, bool active);
event BuybackToMysteryBox(uint256 dividendSpent, uint256 keyBurned, uint256 keyToMysteryBox);
event HexRewardDistributed(uint256 hexAmount, uint256 totalLegendKey);
event HexRewardClaimed(address indexed user, uint256 hexAmount);
event PLSReceived(uint256 amount);
event StreamingRewardsUpdated(address indexed streamingRewards);
// -- Modifiers -------------------------------------------------
modifier onlyOracle() {
require(msg.sender == oracleAddress || msg.sender == owner(), "Not oracle");
_;
}
modifier onlyAuthorized() {
require(
msg.sender == oracleAddress ||
(streamingRewards != address(0) && msg.sender == streamingRewards) ||
msg.sender == owner(),
"Not authorized"
);
_;
}
// -- Constructor -----------------------------------------------
constructor(
address _keyToken,
address _dividendToken,
address _mysteryBox
) Ownable(msg.sender) {
require(_keyToken != address(0), "Invalid KEY");
require(_dividendToken != address(0), "Invalid dividend");
require(_mysteryBox != address(0), "Invalid mysteryBox");
keyToken = IKeyToken(_keyToken);
dividendToken = IERC20(_dividendToken);
mysteryBoxAddress = _mysteryBox;
buybackHelper = new BuybackHelper(address(this), _keyToken);
// Collector: 0.36% / 90d / 1.35x
tiers[0] = TierConfig({ supplyBps: 36, lockDays: 90, boostBps: 3500, active: true });
// Curator: 0.72% / 180d / 1.72x
tiers[1] = TierConfig({ supplyBps: 72, lockDays: 180, boostBps: 7200, active: true });
// Legend: 1.45% / 365d / 2.45x
tiers[2] = TierConfig({ supplyBps: 145, lockDays: 365, boostBps: 14500, active: true });
}
// -- Internal helpers ------------------------------------------
function _requiredForTier(uint8 tierId) internal view returns (uint256) {
uint256 supply = keyToken.totalSupply();
require(supply > 0, "No KEY supply");
return (supply * tiers[tierId].supplyBps) / 10000 + 1;
}
function _updateHexReward(address user) internal {
StakeInfo memory s = stakes[user];
if (s.amount > 0 && s.tierId == LEGEND_TIER_ID && block.timestamp <= s.lockExpiry) {
pendingHex[user] = _earnedHex(user);
}
hexRewardDebt[user] = hexPerKeyStored;
}
function _earnedHex(address user) internal view returns (uint256) {
StakeInfo memory s = stakes[user];
if (s.amount == 0 || s.tierId != LEGEND_TIER_ID) return pendingHex[user];
uint256 accrued = (hexPerKeyStored - hexRewardDebt[user]) * s.amount / HEX_PRECISION;
return pendingHex[user] + accrued;
}
function _distributeHexToLegend(uint256 hexAmount) internal {
if (hexAmount == 0 || totalLegendStaked == 0) return;
hexPerKeyStored += (hexAmount * HEX_PRECISION) / totalLegendStaked;
totalHexToLegend += hexAmount;
emit HexRewardDistributed(hexAmount, totalLegendStaked);
}
function _applyHouseTax(address user, uint256 surplus) internal returns (uint256 netSurplus) {
if (surplus == 0 || downgradeHouseTaxBps == 0) return surplus;
uint256 tax = (surplus * downgradeHouseTaxBps) / 10000;
netSurplus = surplus - tax;
// Tax KEY stays in contract -- earns HEX reflections alongside staked KEY,
// flows through claimAndBurn -> 90% MysteryBox + 10% burn.
// primaryArtist mapping is preserved for future StreamingRewards / Factory mechanics.
totalHouseTaxHeld += tax;
emit HouseTaxHeld(user, tax);
}
// -- Stake (timed lock) ----------------------------------------
/// @notice Stake KEY at the specified tier with a time-based lock.
/// For MeFi Card qualification use stakeForCard() (permanent lock).
/// @param tierId Tier index: 0=Collector, 1=Curator, 2=Legend.
function stake(uint8 tierId) external nonReentrant whenNotPaused {
require(tierId < TIER_COUNT, "Invalid tier");
require(stakes[msg.sender].amount == 0, "Already staking -- use restake");
TierConfig memory t = tiers[tierId];
require(t.active, "Tier not active");
uint256 required = _requiredForTier(tierId);
require(keyToken.transferFrom(msg.sender, address(this), required), "Transfer failed");
_updateHexReward(msg.sender);
uint256 expiry = block.timestamp + (t.lockDays * 1 days);
stakes[msg.sender] = StakeInfo({
amount: required,
lockExpiry: expiry,
tierId: tierId,
stakedAt: block.timestamp
});
totalStaked += required;
if (originalStakedAt[msg.sender] == 0) {
originalStakedAt[msg.sender] = block.timestamp;
}
if (tierId == LEGEND_TIER_ID) {
totalLegendStaked += required;
emit LegendStatusAchieved(msg.sender);
}
emit Staked(msg.sender, tierId, required, expiry);
}
// -- stakeForCard (permanent lock) -----------------------------
/// @notice Stake KEY with a permanent lock for MeFi Card qualification.
/// Lock never expires while KEY remains staked.
/// Re-qualifying after unstake uses current supply % (may be higher).
/// @param tierId Tier index: 0=Collector, 1=Curator, 2=Legend.
function stakeForCard(uint8 tierId) external nonReentrant whenNotPaused {
require(tierId < TIER_COUNT, "Invalid tier");
require(stakes[msg.sender].amount == 0, "Already staking -- use restake");
TierConfig memory t = tiers[tierId];
require(t.active, "Tier not active");
uint256 required = _requiredForTier(tierId);
require(keyToken.transferFrom(msg.sender, address(this), required), "Transfer failed");
_updateHexReward(msg.sender);
stakes[msg.sender] = StakeInfo({
amount: required,
lockExpiry: PERMANENT_LOCK,
tierId: tierId,
stakedAt: block.timestamp
});
totalStaked += required;
if (originalStakedAt[msg.sender] == 0) {
originalStakedAt[msg.sender] = block.timestamp;
}
if (tierId == LEGEND_TIER_ID) {
totalLegendStaked += required;
emit LegendStatusAchieved(msg.sender);
}
emit CardLocked(msg.sender, tierId, required);
emit Staked(msg.sender, tierId, required, PERMANENT_LOCK);
}
// -- Restake ---------------------------------------------------
/// @notice Change tier or renew lock. Upgrade pulls deficit; downgrade returns
/// surplus minus house tax. Card locks remain permanent after restake.
/// @param newTierId Target tier: 0=Collector, 1=Curator, 2=Legend.
function restake(uint8 newTierId) external nonReentrant whenNotPaused {
StakeInfo storage s = stakes[msg.sender];
require(s.amount > 0, "No active stake");
require(newTierId < TIER_COUNT, "Invalid tier");
TierConfig memory t = tiers[newTierId];
require(t.active, "Tier not active");
_updateHexReward(msg.sender);
uint8 oldTierId = s.tierId;
bool wasLegend = (oldTierId == LEGEND_TIER_ID);
bool isNowLegend = (newTierId == LEGEND_TIER_ID);
uint256 required = _requiredForTier(newTierId);
uint256 current = s.amount;
if (wasLegend) totalLegendStaked = totalLegendStaked > current ? totalLegendStaked - current : 0;
if (required > current) {
uint256 deficit = required - current;
require(keyToken.transferFrom(msg.sender, address(this), deficit), "Transfer failed");
totalStaked += deficit;
s.amount = required;
} else if (required < current) {
uint256 surplus = current - required;
uint256 netSurplus = _applyHouseTax(msg.sender, surplus);
totalStaked -= surplus;
s.amount = required;
if (netSurplus > 0) {
require(keyToken.transfer(msg.sender, netSurplus), "Return failed");
totalSurplusReturned += netSurplus;
}
emit SurplusReturned(msg.sender, netSurplus, surplus - netSurplus);
}
if (isNowLegend) totalLegendStaked += s.amount;
// -- Auto-claim HEX on Legend -> lower tier downgrade ---------------
// After _updateHexReward(), pendingHex holds all earned HEX.
// claimHexReward() will revert once tierId is no longer LEGEND_TIER_ID,
// so we transfer here before the tier change commits.
if (wasLegend && !isNowLegend) {
uint256 hexOwed = pendingHex[msg.sender];
if (hexOwed > 0) {
pendingHex[msg.sender] = 0;
bool hexOk = dividendToken.transfer(msg.sender, hexOwed);
if (hexOk) emit HexRewardClaimed(msg.sender, hexOwed);
}
}
bool isPermanent = (s.lockExpiry == PERMANENT_LOCK);
s.tierId = newTierId;
s.lockExpiry = isPermanent ? PERMANENT_LOCK : block.timestamp + (t.lockDays * 1 days);
s.stakedAt = block.timestamp;
// First-stake timestamp preserved across restake calls.
if (isNowLegend && !wasLegend) emit LegendStatusAchieved(msg.sender);
emit Restaked(msg.sender, oldTierId, newTierId, s.amount, s.lockExpiry);
}
// -- Unstake ---------------------------------------------------
/// @notice Withdraw full principal after lock expires.
/// CAUTION: Unstaking forfeits originalStakedAt date.
/// Card lock unstake breaks permanent card status.
/// Accumulated HEX is auto-claimed before exit.
function unstake() external nonReentrant whenNotPaused {
StakeInfo storage s = stakes[msg.sender];
require(s.amount > 0, "No active stake");
require(
s.lockExpiry == PERMANENT_LOCK || block.timestamp >= s.lockExpiry,
"Still locked"
);
_updateHexReward(msg.sender);
uint256 amount = s.amount;
uint8 tierId = s.tierId;
// -- Auto-claim accumulated HEX before state deletion --------------
// Without this, pendingHex[user] is preserved in storage but
// claimHexReward() would revert (stake deleted, s.amount == 0).
if (tierId == LEGEND_TIER_ID) {
uint256 hexOwed = pendingHex[msg.sender];
if (hexOwed > 0) {
pendingHex[msg.sender] = 0;
bool hexOk = dividendToken.transfer(msg.sender, hexOwed);
if (hexOk) emit HexRewardClaimed(msg.sender, hexOwed);
}
totalLegendStaked = totalLegendStaked > amount ? totalLegendStaked - amount : 0;
}
totalStaked -= amount;
originalStakedAt[msg.sender] = 0; // loses founding date on exit
delete stakes[msg.sender];
require(keyToken.transfer(msg.sender, amount), "Transfer failed");
emit Unstaked(msg.sender, tierId, amount, 0);
}
// -- Claim HEX reward (Legend stakers only) --------------------
/// @notice Claim accumulated HEX dividend share (Legend stakers only).
/// HEX is distributed proportionally to KEY staked at Legend tier
/// via the Synthetix rewards-per-token accumulator pattern.
function claimHexReward() external nonReentrant whenNotPaused {
StakeInfo memory s = stakes[msg.sender];
require(s.amount > 0 && s.tierId == LEGEND_TIER_ID, "Legend stake required");
require(block.timestamp <= s.lockExpiry, "Lock expired");
_updateHexReward(msg.sender);
uint256 reward = pendingHex[msg.sender];
require(reward > 0, "No HEX reward");
pendingHex[msg.sender] = 0;
hexRewardDebt[msg.sender] = hexPerKeyStored;
require(dividendToken.transfer(msg.sender, reward), "HEX transfer failed");
emit HexRewardClaimed(msg.sender, reward);
}
// -- claimAndBurn ----------------------------------------------
/// @notice Claim KEY dividends, divert 20% to Legend stakers as HEX,
/// swap remainder to KEY via BuybackHelper, burn 10%, send 90% to MysteryBox.
/// 6-hour cooldown prevents MEV sandwich attacks.
/// Callable by anyone -- permissionless but rate-limited.
function claimAndBurn() external nonReentrant whenNotPaused {
require(totalStaked > 0, "No stakers");
require(block.timestamp >= lastClaimTime + claimCooldown, "Cooldown active");
lastClaimTime = block.timestamp;
try keyToken.claimDividends() {} catch {}
uint256 divBalance = dividendToken.balanceOf(address(this));
require(divBalance >= minClaimThreshold, "Below threshold");
require(divBalance > 0, "No dividends");
// Divert Legend share before buyback
uint256 legendShare = 0;
if (legendHexShareBps > 0 && totalLegendStaked > 0) {
legendShare = (divBalance * legendHexShareBps) / 10000;
if (legendShare > 0) {
_distributeHexToLegend(legendShare);
divBalance -= legendShare;
}
}
if (divBalance == 0) return;
require(dividendToken.transfer(address(buybackHelper), divBalance), "Transfer failed");
uint256 keyBefore = keyToken.balanceOf(address(this));
buybackHelper.buybackAndReturn(address(dividendToken));
uint256 keyReceived = keyToken.balanceOf(address(this)) - keyBefore;
require(keyReceived >= minBuybackOutput, "Slippage exceeded");
if (keyReceived > 0) {
uint256 burnAmount = (keyReceived * burnShareBps) / 10000;
uint256 boxAmount = keyReceived - burnAmount;
if (burnAmount > 0) {
require(keyToken.transfer(DEAD, burnAmount), "Burn failed");
totalKeyBurned += burnAmount;
}
if (boxAmount > 0) {
if (mysteryBoxAddress != address(0)) {
require(keyToken.transfer(mysteryBoxAddress, boxAmount), "MysteryBox failed");
totalKeyToMysteryBox += boxAmount;
} else {
totalHouseTaxHeld += boxAmount;
}
}
totalDividendsClaimed += divBalance;
emit BuybackToMysteryBox(divBalance, burnAmount, boxAmount);
}
}
// -- Primary Artist --------------------------------------------
/// @notice Designate a primary artist. Stored for future use in
/// StreamingRewards (jackpot routing) and ArtistFactory (graduation bonus).
/// Has no on-chain effect in the current staking contract.
/// @param creator Artist wallet address.
function setPrimaryArtist(address creator) external {
require(creator != address(0), "Invalid creator");
primaryArtist[msg.sender] = creator;
emit PrimaryArtistSet(msg.sender, creator);
}
// -- DedicatedListener -----------------------------------------
function isDedicatedListener(address user) external view returns (bool) {
return dedicatedListener[user];
}
function setDedicatedListener(address user) external onlyAuthorized {
dedicatedListener[user] = true;
emit DedicatedListenerSet(user);
}
// -- Views -----------------------------------------------------
/// @notice Streaming boost bps for an active stake. 0 if unstaked/expired.
function getBoostBps(address user) external view returns (uint256) {
StakeInfo memory s = stakes[user];
if (s.amount == 0 || block.timestamp > s.lockExpiry) return 0;
return tiers[s.tierId].boostBps;
}
function isEligibleStaker(address user) external view returns (bool) {
StakeInfo memory s = stakes[user];
return s.amount > 0 && block.timestamp <= s.lockExpiry;
}
function getTierId(address user) external view returns (uint8) {
StakeInfo memory s = stakes[user];
if (s.amount == 0 || block.timestamp > s.lockExpiry) return 255;
return s.tierId;
}
function isCardLocked(address user) external view returns (bool) {
return stakes[user].lockExpiry == PERMANENT_LOCK;
}
function earnedHex(address user) external view returns (uint256) {
return _earnedHex(user);
}
function getStakeInfo(address user) external view returns (
uint256 amount,
uint8 tierId,
uint256 lockExpiry,
uint256 stakedAt,
bool isLocked,
uint256 boostBps,
uint256 multiplierBps,
uint256 timeRemaining
) {
StakeInfo memory s = stakes[user];
bool locked = s.amount > 0 && block.timestamp <= s.lockExpiry;
uint256 boost = locked ? tiers[s.tierId].boostBps : 0;
uint256 remaining = (locked && s.lockExpiry != PERMANENT_LOCK)
? s.lockExpiry - block.timestamp : 0;
return (
s.amount, s.tierId, s.lockExpiry, s.stakedAt,
locked, boost, 10000 + boost, remaining
);
}
/// @notice V4 extended info -- card lock, HEX rewards, founding date.
function getStakeInfoV4(address user) external view returns (
bool isPermanent,
uint256 hexClaimable,
uint256 originalStakedAtTs
) {
StakeInfo memory s = stakes[user];
return (
s.lockExpiry == PERMANENT_LOCK,
_earnedHex(user),
originalStakedAt[user]
);
}
function previewRestake(address user, uint8 newTierId) external view returns (
uint256 required, uint256 pulled, uint256 returned, uint256 houseTax
) {
StakeInfo memory s = stakes[user];
if (s.amount == 0 || newTierId >= TIER_COUNT) return (0,0,0,0);
required = _requiredForTier(newTierId);
uint256 current = s.amount;
if (required > current) {
pulled = required - current;
} else if (required < current) {
uint256 surplus = current - required;
houseTax = (surplus * downgradeHouseTaxBps) / 10000;
returned = surplus - houseTax;
}
}
function previewTier(uint8 tierId) external view returns (
uint256 requiredKey, uint256 lockDays, uint256 boostBps,
uint256 multiplierBps, bool active
) {
require(tierId < TIER_COUNT, "Invalid tier");
TierConfig memory t = tiers[tierId];
return (
_requiredForTier(tierId), t.lockDays, t.boostBps,
10000 + t.boostBps, t.active
);
}
function getRequiredAmount(uint8 tierId) external view returns (uint256) {
require(tierId < TIER_COUNT, "Invalid tier");
return _requiredForTier(tierId);
}
function getAllTiers() external view returns (TierConfig[3] memory) { return tiers; }
function getGlobalStats() external view returns (
uint256 _totalStaked,
uint256 _totalKeyBurned,
uint256 _totalKeyToMysteryBox,
uint256 _totalDividendsClaimed,
uint256 _totalSurplusReturned,
uint256 _totalHouseTaxHeld
) {
return (
totalStaked, totalKeyBurned, totalKeyToMysteryBox,
totalDividendsClaimed, totalSurplusReturned, totalHouseTaxHeld
);
}
function getGlobalStatsV4() external view returns (
uint256 _totalHexToLegend,
uint256 _pendingDividends,
uint256 _totalLegendStaked,
address _buybackHelper
) {
return (
totalHexToLegend,
dividendToken.balanceOf(address(this)),
totalLegendStaked,
address(buybackHelper)
);
}
// -- Admin -----------------------------------------------------
function setTier(uint8 tierId, uint256 supplyBps, uint256 lockDays, uint256 boostBps, bool active)
external onlyOwner {
require(tierId < TIER_COUNT && supplyBps > 0 && lockDays > 0, "Invalid params");
require(boostBps <= 14500, "Boost exceeds Legend max");
tiers[tierId] = TierConfig({ supplyBps: supplyBps, lockDays: lockDays, boostBps: boostBps, active: active });
emit TierUpdated(tierId, supplyBps, lockDays, boostBps, active);
}
function setLegendHexShareBps(uint256 bps) external onlyOwner {
require(bps <= 5000, "Max 50%");
legendHexShareBps = bps;
}
function setClaimCooldown(uint256 seconds_) external onlyOwner {
require(seconds_ >= 1 hours && seconds_ <= 24 hours, "Invalid cooldown");
claimCooldown = seconds_;
}
function setOracleAddress(address _o) external onlyOwner { require(_o != address(0), "Zero address"); oracleAddress = _o; }
function setStreamingRewards(address _sr) external onlyOwner { streamingRewards = _sr; emit StreamingRewardsUpdated(_sr); }
function setDowngradeHouseTaxBps(uint256 _bps) external onlyOwner { require(_bps <= MAX_HOUSE_TAX_BPS, "Exceeds cap"); downgradeHouseTaxBps = _bps; }
function setMysteryBoxAddress(address _a) external onlyOwner { mysteryBoxAddress = _a; }
function setBurnShareBps(uint256 _bps) external onlyOwner { require(_bps <= 10000); burnShareBps = _bps; }
function setMinClaimThreshold(uint256 _t) external onlyOwner { minClaimThreshold = _t; }
function setMinBuybackOutput(uint256 _min) external onlyOwner { minBuybackOutput = _min; }
function setDividendToken(address _t) external onlyOwner { require(_t != address(0)); dividendToken = IERC20(_t); }
function pause() external onlyOwner { _pause(); }
function unpause() external onlyOwner { _unpause(); }
function rescueToken(address token) external onlyOwner {
require(token != address(keyToken), "Cannot rescue KEY");
if (token == address(dividendToken)) {
require(totalLegendStaked == 0, "Legend HEX pending");
}
uint256 bal = IERC20(token).balanceOf(address(this));
require(bal > 0, "Nothing");
require(IERC20(token).transfer(owner(), bal), "Transfer failed");
}
function rescueFromHelper(address token) external onlyOwner { buybackHelper.rescueToken(token); }
function rescuePLS() external onlyOwner {
uint256 bal = address(this).balance;
require(bal > 0, "No PLS");
(bool _ok,) = payable(owner()).call{value: bal}("");
require(_ok, "PLS transfer failed");
}
function emergencyReturn(address user) external onlyOwner whenPaused {
StakeInfo storage s = stakes[user];
require(s.amount > 0, "No stake");
_updateHexReward(user);
uint256 amount = s.amount;
uint8 tierId = s.tierId;
if (tierId == LEGEND_TIER_ID) {
uint256 hexOwed = pendingHex[user];
if (hexOwed > 0) {
pendingHex[user] = 0;
bool hexOk = dividendToken.transfer(user, hexOwed);
if (hexOk) emit HexRewardClaimed(user, hexOwed);
}
totalLegendStaked = totalLegendStaked > amount ? totalLegendStaked - amount : 0;
}
totalStaked -= amount;
originalStakedAt[user] = 0;
delete stakes[user];
require(keyToken.transfer(user, amount), "Transfer failed");
}
receive() external payable { emit PLSReceived(msg.value); }
}