Skip to main content
PulseScanner.io

Address

0x0f8c0ff59a5e8aebc8589958b28b5ebcd24bf392
Current 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); }
}