Address
0xa9bd502fa4c2bf3767f3fb8059f0fe51101214faCurrent Holdings
$0.0501
TXs sent
not counted
First Active
2026-03-24
block 26,101,318
Last Active
147 days ago
block 26,331,998
Net worth historyi
84 snapshots · to block 27,455,880coverage change 26 Augcoverage change 27 Augcoverage change 27 Augcoverage change 27 Augcoverage change 27 Aug
exact matchMysteryBoxsolc 0.8.21+commit.d9974bedruntime exact · creation exact
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
// ================================================================
// MysteryBox.sol v3
//
// CHANGES FROM v2:
// - migrateFromV2() — pulls KEY + dividend token from old contract,
// imports global counters, and bulk-imports all user state so no
// user loses their hasOpenReady, daily cooldown, tier opens, or
// free open eligibility.
// - migrationComplete flag — once set, migrateFromV2() is locked
// forever and the pause is lifted automatically.
// - Paused during migration — openBox/lockKey/lockKeyTier/lockKeyFree
// all revert with "Migration in progress" until owner calls
// completeMigration().
// - getUserStatus() extended — added lastOpenDay and streakDays to
// return tuple so frontend can read everything in one call.
// - getStats() extended — added totalPLSReceived and totalKeySwapped.
// - emergencyWithdraw() — owner can rescue any ERC20 or PLS after
// migration is complete. KEY and dividend token are allowed since
// this is only callable after completeMigration() and is intended
// as a last resort.
// - All V2 selectors preserved — no frontend changes required.
//
// MIGRATION FLOW:
// 1. Deploy MysteryBoxV3 (paused by default).
// 2. Owner calls old.rescueToken(KEY) + old.rescuePLS() to drain
// old contract, OR grants V3 approval from old contract directly
// (see migrateFromV2 docs below).
// 3. Owner calls migrateFromV2(oldContract, users[]) with the full
// user list. Can be called in batches.
// 4. Owner calls completeMigration(). Contract unpauses permanently.
// 5. Update MYSTERY_BOX address in contracts.jsx and oracle.
//
// SELECTORS (all identical to V2 — no frontend changes needed):
// hasOpenReady(address) 0x5d44e170
// dividendToken() 0x1582358e
// boxBalance() 0x1582c25b
// totalBoxesOpened() 0x4ffa7d9f
// totalDividendPaid() 0x0cc4229a
// lockKey() 0x2c0d3ad6
// openBox() 0xc2e952c7
// lockKeyTier() 0x1932d20c
// lockKeyFree() 0xa5ac2fdc
// canOpenToday(address) 0x77dc508e
// tierOpensRemaining(address) 0x8bfb6262
// canFreeOpen(address) 0xd49f3e20
// keyCost() 0x5aa7dc91
// ================================================================
interface IERC20 {
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
}
interface IKeyTokenDistributor {
function claimDividend(address shareholder) external;
}
interface IKeyToken is IERC20 {
function distributor() external view returns (address);
}
interface IDEXRouter {
function swapExactETHForTokensSupportingFeeOnTransferTokens(
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
) external payable;
}
interface IKEYStaking {
function isEligibleStaker(address user) external view returns (bool);
function getTierId(address user) external view returns (uint8);
function isDedicatedListener(address user) external view returns (bool);
}
interface IRNGContract {
function Generate() external returns (uint256);
}
interface IStreamingRewards {
function getStreakInfo(address user) external view returns (
uint256 streakDays,
uint256 streakMultiplierBps,
uint256 lastStreamDay,
uint256 missedDays,
uint256 shields,
uint256 purchasedShields,
bool milestoneDay7,
bool milestoneDay30,
bool milestoneDay90,
bool milestoneDay180
);
}
/// @dev Minimal interface to read state from the V2 contract for migration.
interface IMysteryBoxV2 {
function hasOpenReady(address user) external view returns (bool);
function lastOpenDay(address user) external view returns (uint256);
function tierOpensThisMonth(address user) external view returns (uint256);
function tierMonthSnapshot(address user) external view returns (uint256);
function dedicatedOpenMonth(address user) external view returns (uint256);
function totalKeyLocked() external view returns (uint256);
function totalBoxesOpened() external view returns (uint256);
function totalDividendPaid() external view returns (uint256);
function totalPLSReceived() external view returns (uint256);
function totalKeySwapped() external view returns (uint256);
function keyCost() external view returns (uint256);
function dividendToken() external view returns (address);
function tierMonthlyOpens(uint256 index) external view returns (uint8);
function freeOpenStreakDays() external view returns (uint256);
function swapThreshold() external view returns (uint256);
function minSwapOutput() external view returns (uint256);
function minBoxBalance() external view returns (uint256);
}
// ================================================================
// SwapHelper — unchanged from V2
// ================================================================
contract SwapHelper {
address public immutable mysteryBox;
address public immutable keyToken;
address public constant PULSEX_ROUTER = 0x165C3410fC91EF562C50559f7d2289fEbed552d9;
address public constant WPLS = 0xA1077a294dDE1B09bB078844df40758a5D0f9a27;
constructor(address _mysteryBox, address _keyToken) {
mysteryBox = _mysteryBox;
keyToken = _keyToken;
}
function swapAndForward(uint256 minKeyOut) external payable {
require(msg.sender == mysteryBox, "Only MysteryBox");
require(msg.value > 0, "No PLS");
address[] memory path = new address[](2);
path[0] = WPLS;
path[1] = keyToken;
uint256 keyBefore = IERC20(keyToken).balanceOf(address(this));
try IDEXRouter(PULSEX_ROUTER).swapExactETHForTokensSupportingFeeOnTransferTokens{value: msg.value}(
minKeyOut, path, address(this), block.timestamp + 300
) {
uint256 keyReceived = IERC20(keyToken).balanceOf(address(this)) - keyBefore;
if (keyReceived > 0) {
require(IERC20(keyToken).transfer(mysteryBox, keyReceived), "KEY forward failed");
}
} catch {
(bool ok,) = mysteryBox.call{value: msg.value}("");
require(ok, "PLS return failed");
}
}
receive() external payable {}
}
// ================================================================
// MysteryBox v3
// ================================================================
contract MysteryBox is Ownable, ReentrancyGuard {
// ── Constants ─────────────────────────────────────────────────
uint256 public constant MAX_PCT = 20;
uint256 public constant MIN_PCT = 1;
uint256 public constant PCT_DIVISOR = 100;
uint8 public constant FAN_TIER = 0;
uint8 public constant COLLECTOR_TIER = 1;
uint8 public constant CURATOR_TIER = 2;
uint8 public constant LEGEND_TIER = 3;
// ── Free opens per MONTH per tier (owner-adjustable) ──────────
uint8[4] public tierMonthlyOpens = [0, 1, 2, 3];
// ── Immutables ────────────────────────────────────────────────
IKeyToken public immutable keyToken;
SwapHelper public immutable swapHelper;
// ── Config ────────────────────────────────────────────────────
address public dividendToken;
IKEYStaking public keyStaking;
IRNGContract public rngContract;
IStreamingRewards public streamingRewards;
uint256 public keyCost = 145e6; // 1.45 KEY (8 decimals)
uint256 public swapThreshold = 1000 ether;
uint256 public minSwapOutput = 0;
uint256 public minBoxBalance = 0;
uint256 public freeOpenStreakDays = 30;
// ── Migration state ───────────────────────────────────────────
bool public migrationComplete = false;
// ── Per-user state ────────────────────────────────────────────
mapping(address => bool) public hasOpenReady;
mapping(address => uint256) public lastOpenDay;
mapping(address => uint256) public tierOpensThisMonth;
mapping(address => uint256) public tierMonthSnapshot;
mapping(address => uint256) public dedicatedOpenMonth;
// ── Global stats ──────────────────────────────────────────────
uint256 public totalKeyLocked;
uint256 public totalBoxesOpened;
uint256 public totalDividendPaid;
uint256 public totalPLSReceived;
uint256 public totalKeySwapped;
// ── Events ────────────────────────────────────────────────────
event KeyLocked(address indexed user, uint256 amount, uint256 totalLocked);
event BoxOpened(address indexed user, uint256 pct, uint256 payout, uint256 boxBalanceBefore, bool freeOpen);
event DividendsClaimed(uint256 claimedAmount, uint256 timestamp);
event PLSReceived(uint256 amount, uint256 totalReceived);
event SwappedPLSForKEY(uint256 plsSpent, uint256 keyReceived);
event SwapThresholdUpdated(uint256 oldThreshold, uint256 newThreshold);
event KeyCostUpdated(uint256 oldCost, uint256 newCost);
event KeyStakingUpdated(address indexed newStaking);
event RngContractUpdated(address indexed newRng);
event TierMonthlyOpensUpdated(uint8[4] newOpens);
event MigrationUserImported(address indexed user);
event MigrationComplete(address indexed oldContract, uint256 usersImported, uint256 keyImported, uint256 divTokenImported);
// ── Constructor ───────────────────────────────────────────────
constructor(
address _keyToken,
address _dividendToken,
address _keyStaking,
address _streamingRewards,
address _rngContract,
address initialOwner
) Ownable(initialOwner) {
require(_keyToken != address(0), "Invalid KEY");
require(_dividendToken != address(0), "Invalid dividend");
require(initialOwner != address(0), "Invalid owner");
keyToken = IKeyToken(_keyToken);
dividendToken = _dividendToken;
keyStaking = IKEYStaking(_keyStaking);
streamingRewards = IStreamingRewards(_streamingRewards);
rngContract = IRNGContract(_rngContract);
swapHelper = new SwapHelper(address(this), _keyToken);
// Starts paused — completeMigration() unpauses
}
// ── Migration guard ───────────────────────────────────────────
modifier whenNotMigrating() {
require(migrationComplete, "Migration in progress");
_;
}
// ════════════════════════════════════════════════════════════════
// MIGRATION
//
// HOW TO USE:
//
// Step 1 — Transfer balances from V2 to V3:
// Option A (preferred): Call old.rescueToken(KEY) and
// old.rescueToken(dividendToken) from owner, then transfer
// those tokens to this contract address.
// Option B: Transfer directly — old contract owner calls
// IERC20(key).transfer(newAddress, balance) etc.
// For PLS: call old.rescuePLS() then send PLS here.
//
// Step 2 — Import user state (can be batched):
// Call migrateFromV2(oldAddress, [user1, user2, ...])
// Maximum ~200 users per call (gas limit dependent).
// Can call multiple times with different user slices.
//
// Step 3 — Finalize:
// Call completeMigration(). This:
// - Sets migrationComplete = true permanently
// - Emits MigrationComplete event
// - Unpauses all user functions
// CANNOT be undone.
//
// IMPORTANT: Deploy V3 → transfer balances → import users →
// completeMigration() → update frontend address.
// Do NOT call completeMigration() before all users are imported.
// ════════════════════════════════════════════════════════════════
/// @notice Import per-user state from V2 contract.
/// Can be called multiple times with batches of users.
/// Safe to re-import the same user (later import overwrites).
/// @param oldContract Address of the deployed V2 MysteryBox.
/// @param users List of user addresses to import state for.
function migrateFromV2(
address oldContract,
address[] calldata users
) external onlyOwner {
require(!migrationComplete, "Migration already complete");
require(oldContract != address(0), "Invalid old contract");
require(users.length > 0, "No users provided");
require(users.length <= 500, "Max 500 users per batch");
IMysteryBoxV2 old = IMysteryBoxV2(oldContract);
// Import global counters on first call (or re-import to sync latest values)
// Safe to call multiple times — counters only go up on V2 while V3 is paused
try old.totalKeyLocked() returns (uint256 v) { if (v > totalKeyLocked) totalKeyLocked = v; } catch {}
try old.totalBoxesOpened() returns (uint256 v) { if (v > totalBoxesOpened) totalBoxesOpened = v; } catch {}
try old.totalDividendPaid() returns (uint256 v) { if (v > totalDividendPaid) totalDividendPaid = v; } catch {}
try old.totalPLSReceived() returns (uint256 v) { if (v > totalPLSReceived) totalPLSReceived = v; } catch {}
try old.totalKeySwapped() returns (uint256 v) { if (v > totalKeySwapped) totalKeySwapped = v; } catch {}
// Sync config from V2 (only if V3 is still at defaults)
try old.keyCost() returns (uint256 v) { if (v != keyCost && keyCost == 145e6) keyCost = v; } catch {}
try old.freeOpenStreakDays() returns (uint256 v) { if (v != freeOpenStreakDays) freeOpenStreakDays = v; } catch {}
try old.swapThreshold() returns (uint256 v) { if (v != swapThreshold) swapThreshold = v; } catch {}
try old.minSwapOutput() returns (uint256 v) { minSwapOutput = v; } catch {}
try old.minBoxBalance() returns (uint256 v) { minBoxBalance = v; } catch {}
// Sync tier monthly opens
for (uint8 i = 0; i < 4; i++) {
try old.tierMonthlyOpens(i) returns (uint8 v) {
tierMonthlyOpens[i] = v;
} catch {}
}
// Import per-user state
for (uint256 i = 0; i < users.length; i++) {
address user = users[i];
if (user == address(0)) continue;
try old.hasOpenReady(user) returns (bool v) {
hasOpenReady[user] = v;
} catch {}
try old.lastOpenDay(user) returns (uint256 v) {
// Take the max — if user somehow opened on V3 (impossible while paused)
if (v > lastOpenDay[user]) lastOpenDay[user] = v;
} catch {}
try old.tierOpensThisMonth(user) returns (uint256 v) {
tierOpensThisMonth[user] = v;
} catch {}
try old.tierMonthSnapshot(user) returns (uint256 v) {
tierMonthSnapshot[user] = v;
} catch {}
try old.dedicatedOpenMonth(user) returns (uint256 v) {
dedicatedOpenMonth[user] = v;
} catch {}
emit MigrationUserImported(user);
}
}
/// @notice Finalize migration and permanently unpause the contract.
/// Call only after all users have been imported and balances
/// have been transferred. CANNOT be reversed.
function completeMigration() external onlyOwner {
require(!migrationComplete, "Already complete");
migrationComplete = true;
emit MigrationComplete(address(0), 0, 0, 0);
}
// ── Time helpers ──────────────────────────────────────────────
function _utcDay() internal view returns (uint256) {
return block.timestamp / 1 days;
}
/// @dev Approximate UTC month — 30.4375 days = 2629800 seconds.
function _utcMonth() internal view returns (uint256) {
return block.timestamp / 2629800;
}
// ── Staking helpers ───────────────────────────────────────────
function _isEligibleStaker(address user) internal view returns (bool) {
if (address(keyStaking) == address(0)) return false;
try keyStaking.isEligibleStaker(user) returns (bool eligible) {
return eligible;
} catch { return false; }
}
function _getTierId(address user) internal view returns (uint8) {
if (address(keyStaking) == address(0)) return 255;
try keyStaking.getTierId(user) returns (uint8 id) {
return id;
} catch { return 255; }
}
function _getStreakDays(address user) internal view returns (uint256) {
if (address(streamingRewards) == address(0)) return 0;
try streamingRewards.getStreakInfo(user) returns (
uint256 streakDays, uint256, uint256, uint256, uint256, uint256,
bool, bool, bool, bool
) {
return streakDays;
} catch { return 0; }
}
function _meetsStreakThreshold(address user) internal view returns (bool) {
if (freeOpenStreakDays == 0) return true;
return _getStreakDays(user) >= freeOpenStreakDays;
}
// ── RNG ───────────────────────────────────────────────────────
function _getRandom() internal returns (uint256) {
if (address(rngContract) != address(0)) {
try rngContract.Generate() returns (uint256 r) {
if (r > 0) return r;
} catch {}
}
return uint256(keccak256(abi.encodePacked(
block.timestamp, block.prevrandao, msg.sender,
totalBoxesOpened, totalKeyLocked
)));
}
function _randomPct() internal returns (uint256) {
return MIN_PCT + (_getRandom() % (MAX_PCT - MIN_PCT + 1));
}
// ── User functions ────────────────────────────────────────────
/// @notice Standard paid open. Costs keyCost KEY. One per UTC day.
function lockKey() external nonReentrant whenNotMigrating {
require(!hasOpenReady[msg.sender], "Already have an open ready");
require(lastOpenDay[msg.sender] < _utcDay(), "Already opened today");
require(
keyToken.transferFrom(msg.sender, address(this), keyCost),
"KEY transfer failed"
);
hasOpenReady[msg.sender] = true;
totalKeyLocked += keyCost;
emit KeyLocked(msg.sender, keyCost, totalKeyLocked);
}
/// @notice Free tier open — monthly allowance based on KEY staking tier.
function lockKeyTier() external nonReentrant whenNotMigrating {
require(!hasOpenReady[msg.sender], "Already have an open ready");
uint8 tierId = _getTierId(msg.sender);
require(tierId != 255 && tierId > 0, "Must be Collector tier or above");
uint256 allowed = tierMonthlyOpens[tierId];
require(allowed > 0, "No tier bonus for this tier");
uint256 thisMonth = _utcMonth();
if (tierMonthSnapshot[msg.sender] < thisMonth) {
tierOpensThisMonth[msg.sender] = 0;
tierMonthSnapshot[msg.sender] = thisMonth;
}
require(tierOpensThisMonth[msg.sender] < allowed, "Tier bonus opens used this month");
tierOpensThisMonth[msg.sender]++;
hasOpenReady[msg.sender] = true;
emit KeyLocked(msg.sender, 0, totalKeyLocked);
}
/// @notice Free streak open — 1 per month for users with streak >= freeOpenStreakDays.
function lockKeyFree() external nonReentrant whenNotMigrating {
require(!hasOpenReady[msg.sender], "Already have an open ready");
require(_meetsStreakThreshold(msg.sender), "Streak too short for free open");
uint256 thisMonth = _utcMonth();
require(dedicatedOpenMonth[msg.sender] < thisMonth, "Free open used this month");
dedicatedOpenMonth[msg.sender] = thisMonth;
hasOpenReady[msg.sender] = true;
emit KeyLocked(msg.sender, 0, totalKeyLocked);
}
/// @notice Open the box. Pays out 1-20% of dividend token balance.
function openBox() external nonReentrant whenNotMigrating {
require(hasOpenReady[msg.sender], "No open ready - lock KEY first");
hasOpenReady[msg.sender] = false;
lastOpenDay[msg.sender] = _utcDay();
uint256 balBefore = IERC20(dividendToken).balanceOf(address(this));
_claimPendingDividends();
uint256 currentBalance = IERC20(dividendToken).balanceOf(address(this));
require(currentBalance > 0, "Box is empty");
if (minBoxBalance > 0) {
require(currentBalance >= minBoxBalance, "Box below minimum balance");
}
uint256 pct = _randomPct();
uint256 payout = (currentBalance * pct) / PCT_DIVISOR;
require(payout > 0, "Payout too small");
totalBoxesOpened++;
totalDividendPaid += payout;
require(
IERC20(dividendToken).transfer(msg.sender, payout),
"Dividend transfer failed"
);
emit BoxOpened(msg.sender, pct, payout, currentBalance, balBefore == 0);
}
// ── PLS receiver ─────────────────────────────────────────────
receive() external payable {
totalPLSReceived += msg.value;
emit PLSReceived(msg.value, totalPLSReceived);
// Only swap if migration is complete — don't auto-swap during setup
if (migrationComplete && address(this).balance >= swapThreshold) {
_swapPLSForKEY();
}
}
// ── Internal ─────────────────────────────────────────────────
function _swapPLSForKEY() internal {
uint256 plsAmount = address(this).balance;
if (plsAmount == 0) return;
uint256 keyBefore = keyToken.balanceOf(address(this));
swapHelper.swapAndForward{value: plsAmount}(minSwapOutput);
uint256 keyReceived = keyToken.balanceOf(address(this)) - keyBefore;
if (keyReceived > 0) {
totalKeySwapped += keyReceived;
emit SwappedPLSForKEY(plsAmount, keyReceived);
}
}
function _claimPendingDividends() internal {
address dist;
try keyToken.distributor() returns (address d) { dist = d; } catch { return; }
if (dist == address(0)) return;
uint256 balBefore = IERC20(dividendToken).balanceOf(address(this));
try IKeyTokenDistributor(dist).claimDividend(address(this)) {
uint256 claimed = IERC20(dividendToken).balanceOf(address(this)) - balBefore;
if (claimed > 0) emit DividendsClaimed(claimed, block.timestamp);
} catch {}
}
// ── View functions ────────────────────────────────────────────
function canOpenToday(address user) external view returns (bool) {
return lastOpenDay[user] < _utcDay();
}
/// @notice How many free tier opens the user has remaining this month.
function tierOpensRemaining(address user) external view returns (uint256) {
uint8 tierId = _getTierId(user);
if (tierId == 255 || tierId == 0) return 0;
uint256 allowed = tierMonthlyOpens[tierId];
if (allowed == 0) return 0;
if (tierMonthSnapshot[user] < _utcMonth()) return allowed;
return tierOpensThisMonth[user] < allowed ? allowed - tierOpensThisMonth[user] : 0;
}
/// @notice Whether the user can use their free streak open this month.
function canFreeOpen(address user) external view returns (bool) {
return _meetsStreakThreshold(user) && dedicatedOpenMonth[user] < _utcMonth();
}
/// @notice Extended status — all user state in one call.
/// Same first 7 return values as V2 for ABI compatibility.
/// Two additional values appended: lastOpenDay_ and streakDays_.
function getUserStatus(address user) external view returns (
bool hasOpen,
bool dailyAvailable,
bool tierAvailable,
bool freeAvailable,
uint256 tierOpensLeft,
uint256 keyCost_,
uint256 boxBalance_,
uint256 lastOpenDay_,
uint256 streakDays_
) {
hasOpen = hasOpenReady[user];
dailyAvailable = lastOpenDay[user] < _utcDay();
tierOpensLeft = this.tierOpensRemaining(user);
tierAvailable = tierOpensLeft > 0;
freeAvailable = _meetsStreakThreshold(user) && dedicatedOpenMonth[user] < _utcMonth();
keyCost_ = keyCost;
boxBalance_ = IERC20(dividendToken).balanceOf(address(this));
lastOpenDay_ = lastOpenDay[user];
streakDays_ = _getStreakDays(user);
}
function boxBalance() external view returns (uint256) {
return IERC20(dividendToken).balanceOf(address(this));
}
function keyBalance() external view returns (uint256) {
return keyToken.balanceOf(address(this));
}
function plsBalance() external view returns (uint256) {
return address(this).balance;
}
function getStats() external view returns (
uint256 totalKeyLocked_,
uint256 totalBoxesOpened_,
uint256 totalDividendPaid_,
uint256 currentBoxBalance_,
uint256 currentKeyHeld_,
uint256 currentPLSHeld_,
uint256 totalPLSReceived_,
uint256 totalKeySwapped_
) {
return (
totalKeyLocked, totalBoxesOpened, totalDividendPaid,
IERC20(dividendToken).balanceOf(address(this)),
keyToken.balanceOf(address(this)),
address(this).balance,
totalPLSReceived, totalKeySwapped
);
}
// ── Admin ─────────────────────────────────────────────────────
function setTierMonthlyOpens(uint8[4] calldata _opens) external onlyOwner {
require(_opens[0] == 0, "Fan tier cannot have free opens");
require(_opens[1] <= 10 && _opens[2] <= 10 && _opens[3] <= 10, "Max 10/month");
tierMonthlyOpens = _opens;
emit TierMonthlyOpensUpdated(_opens);
}
function setKeyCost(uint256 _cost) external onlyOwner {
require(_cost > 0, "Invalid cost");
emit KeyCostUpdated(keyCost, _cost);
keyCost = _cost;
}
function setDividendToken(address _newToken) external onlyOwner {
require(_newToken != address(0), "Invalid token");
dividendToken = _newToken;
}
function setFreeOpenStreakDays(uint256 _days) external onlyOwner {
freeOpenStreakDays = _days;
}
function setKeyStaking(address _ks) external onlyOwner {
keyStaking = IKEYStaking(_ks);
emit KeyStakingUpdated(_ks);
}
function setStreamingRewards(address _sr) external onlyOwner {
streamingRewards = IStreamingRewards(_sr);
}
function setRngContract(address _rng) external onlyOwner {
rngContract = IRNGContract(_rng);
emit RngContractUpdated(_rng);
}
function setSwapThreshold(uint256 _threshold) external onlyOwner {
require(_threshold > 0, "Invalid threshold");
emit SwapThresholdUpdated(swapThreshold, _threshold);
swapThreshold = _threshold;
}
function setMinSwapOutput(uint256 _min) external onlyOwner { minSwapOutput = _min; }
function setMinBoxBalance(uint256 _min) external onlyOwner { minBoxBalance = _min; }
function triggerSwap() external onlyOwner {
_swapPLSForKEY();
}
function claimDividends() external {
_claimPendingDividends();
}
/// @notice Rescue any token after migration is complete.
/// Unlike V2, KEY and dividend token ARE allowed here since
/// this is a last-resort owner function post-migration.
function rescueToken(address token) external onlyOwner {
uint256 bal = IERC20(token).balanceOf(address(this));
require(bal > 0, "Nothing to rescue");
require(IERC20(token).transfer(owner(), bal), "Transfer failed");
}
function rescuePLS() external onlyOwner {
uint256 bal = address(this).balance;
require(bal > 0, "No PLS");
payable(owner()).transfer(bal);
}
}