ActivationManager

0xacd5ae3c060c1137fe2ee86b0ab2ef697456f664

Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
4,573 bytes
ABI entries
48
19 read · 8 write
License
none

Contract information

Address
0xacd5ae3c060c1137fe2ee86b0ab2ef697456f664
Chain
Robinhood Chain (4663)
Compiler
v0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0x3ed05353d3…f1142f2efb

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (19)

BPS()uint16
TIER_COUNT()uint8
activationOf(uint256)bool, uint8
activeCount()uint256
activeTokenAt(uint256)uint256
activeWeight(uint256)uint256
burnBps()uint16
nft()address
owner()address
paused()bool
protocolBps()uint16
protocolTreasury()address
quoteActivation(uint256, uint8)uint256
quoteActivationBatch(uint256[], uint8[])uint256
redirectBps()uint16
redirectReceiver()address
stonkBroker()address
tiers(uint256)uint256, uint16
totalActiveWeight()uint256

Events (7)

ActivatedActivationClearedActivationFeeSplitUpdatedActivationUpgradedOwnershipTransferredPausedUnpaused

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "nft_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "stonkBroker_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "protocolTreasury_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "owner_",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "EnforcedPause",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ExpectedPause",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidConfig",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidFeeSplit",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidTier",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotTokenOwner",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "OwnableInvalidOwner",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "OwnableUnauthorizedAccount",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "SafeERC20FailedOperation",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TierNotUpgrade",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "Unauthorized",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddress",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "owner",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint8",
        "name": "tier",
        "type": "uint8"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "feePaid",
        "type": "uint256"
      }
    ],
    "name": "Activated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "ActivationCleared",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint16",
        "name": "burnBps",
        "type": "uint16"
      },
      {
        "indexed": false,
        "internalType": "uint16",
        "name": "protocolBps",
        "type": "uint16"
      },
      {
        "indexed": false,
        "internalType": "uint16",
        "name": "redirectBps",
        "type": "uint16"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "redirectReceiver",
        "type": "address"
      }
    ],
    "name": "ActivationFeeSplitUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "owner",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint8",
        "name": "fromTier",
        "type": "uint8"
      },
      {
        "indexed": false,
        "internalType": "uint8",
        "name": "toTier",
        "type": "uint8"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "feePaid",
        "type": "uint256"
      }
    ],
    "name": "ActivationUpgraded",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferred",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "Paused",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "Unpaused",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "BPS",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TIER_COUNT",
    "outputs": [
      {
        "internalType": "uint8",
        "name": "",
        "type": "uint8"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "internalType": "uint8",
        "name": "tier",
        "type": "uint8"
      }
    ],
    "name": "activate",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256[]",
        "name": "tokenIds",
        "type": "uint256[]"
      },
      {
        "internalType": "uint8[]",
        "name": "tiersWanted",
        "type": "uint8[]"
      }
    ],
    "name": "activateBatch",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "activationOf",
    "outputs": [
      {
        "internalType": "bool",
        "name": "activated",
        "type": "bool"
      },
      {
        "internalType": "uint8",
        "name": "tier",
        "type": "uint8"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "activeCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "index",
        "type": "uint256"
      }
    ],
    "name": "activeTokenAt",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "activeWeight",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "burnBps",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "clearActivation",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "nft",
    "outputs": [
      {
        "internalType": "contract IERC721",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pause",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "paused",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "protocolBps",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "protocolTreasury",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "internalType": "uint8",
        "name": "tier",
        "type": "uint8"
      }
    ],
    "name": "quoteActivation",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "feeDue",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256[]",
        "name": "tokenIds",
        "type": "uint256[]"
      },
      {
        "internalType": "uint8[]",
        "name": "tiersWanted",
        "type": "uint8[]"
      }
    ],
    "name": "quoteActivationBatch",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "total",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "redirectBps",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "redirectReceiver",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint16",
        "name": "burnBps_",
        "type": "uint16"
      },
      {
        "internalType": "uint16",
        "name": "protocolBps_",
        "type": "uint16"
      },
      {
        "internalType": "uint16",
        "name": "redirectBps_",
        "type": "uint16"
      },
      {
        "internalType": "address",
        "name": "redirectReceiver_",
        "type": "address"
      }
    ],
    "name": "setFeeSplit",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "stonkBroker",
    "outputs": [
      {
        "internalType": "contract IERC20",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "tiers",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "fee",
        "type": "uint256"
      },
      {
        "internalType": "uint16",
        "name": "multiplierBps",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "totalActiveWeight",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "unpause",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  }
]

Source code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
import {IERC721} from "openzeppelin-contracts/contracts/token/ERC721/IERC721.sol";
import {ERC20Burnable} from "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol";
import {Pausable} from "openzeppelin-contracts/contracts/utils/Pausable.sol";
import {ReentrancyGuard} from "openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol";
import {
    ZeroAddress,
    Unauthorized,
    NotTokenOwner,
    InvalidTier,
    TierNotUpgrade,
    InvalidFeeSplit,
    InvalidConfig
} from "../libs/Errors.sol";
import {Events} from "../libs/Events.sol";

/// @title ActivationManager
/// @notice Per-NFT paid activation for StonkBrokers. Holders pay a non-refundable
/// $STONKBROKER fee to activate a broker at a tier; only activated brokers receive
/// StockBooster airdrops, weighted by their tier multiplier.
///
/// Tier schedule (fees in whole $STONKBROKER, 18 decimals; multipliers in bps):
///   Base   66,666       1.00x (10000)  — Floor Trader
///   T1     166,666      1.25x (12500)  — Analyst
///   T2     366,666      1.60x (16000)  — Portfolio Manager
///   T3     666,666      2.00x (20000)  — Managing Director
///   T4     1,666,666    3.33x (33300)  — Partner
///
/// Ladder economics (why these numbers): the benchmark for "one more unit of drop weight"
/// is buying a second broker from the AMM — 666,666 tokensPerNFT capital + the 10% ETH
/// swap fee + the 66,666 base activation for the new broker ≈ 800k all-in for +10000
/// weight (~80 per weight point). The anchor rung (per launch decision) is T3: reaching
/// 2.00x from scratch costs exactly the broker's 666,666 sticker price but skips the 10%
/// ETH fee and the second activation fee, needs no AMM inventory, and lives on one NFT —
/// a slightly better deal than buying one more broker. Marginal pricing per rung: 40 / 57
/// / 75 / 75 per weight point, so every upgrade is at-or-slightly-better than the broker
/// route, with the earliest rungs the best value. Earlier drafts priced rungs at up to
/// 60,000 per weight point (750x the broker route), making every tier above Base
/// economically irrational and starving the burn sink.
///
/// Upgrades are allowed at any time and cost only the DIFFERENCE between the new tier's fee
/// and the already-paid tier's fee (per launch decision — no full re-pay).
///
/// Activation clears on EVERY true ownership transfer of the NFT, with no approved-custody
/// exceptions (per launch decision): selling into the Anvil AMM, escrowing as loan collateral,
/// or plain wallet-to-wallet transfers all reset it. The NFT contract calls clearActivation()
/// from its transfer hook. Stock already delivered to the token-bound wallet stays with the
/// NFT — clearing only stops future accrual.
///
/// Fee destination is a configurable bps split across three sinks (must sum to 10000):
///   burnBps     — burned via ERC20Burnable (default 50%)
///   protocolBps — protocol treasury (default 50%)
///   redirectBps — optional redirect receiver, e.g. a distributor that pushes $STONKBROKER
///                 back to broker token-bound wallets (default 0%)
contract ActivationManager is Ownable, Pausable, ReentrancyGuard {
    using SafeERC20 for IERC20;

    struct Tier {
        uint256 fee;
        uint16 multiplierBps;
    }

    uint8 public constant TIER_COUNT = 5;
    uint16 public constant BPS = 10_000;

    IERC721 public immutable nft;
    IERC20 public immutable stonkBroker;
    address public immutable protocolTreasury;

    /// @notice Fee split, in bps of every activation fee collected. Sums to 10000.
    uint16 public burnBps = 5000;
    uint16 public protocolBps = 5000;
    uint16 public redirectBps = 0;
    /// @notice Optional third sink for the redirectBps share (e.g. a $STONKBROKER
    /// back-to-wallets distributor). Must be set before redirectBps can be non-zero.
    address public redirectReceiver;

    /// @notice tier index + 1 for activated tokens; 0 = not activated.
    mapping(uint256 => uint8) private _tierPlusOne;
    /// @notice Multiplier bps for activated tokens; 0 when inactive.
    mapping(uint256 => uint256) public activeWeight;
    uint256 public totalActiveWeight;

    /// @dev Swap-and-pop enumerable set of activated tokenIds, so the StockBooster can page
    /// through ONLY activated brokers (never all 4,444) when pushing airdrops.
    uint256[] private _activeTokens;
    mapping(uint256 => uint256) private _activeIndexPlusOne;

    Tier[TIER_COUNT] public tiers;

    constructor(address nft_, address stonkBroker_, address protocolTreasury_, address owner_) Ownable(owner_) {
        if (nft_ == address(0) || stonkBroker_ == address(0) || protocolTreasury_ == address(0)) revert ZeroAddress();
        nft = IERC721(nft_);
        stonkBroker = IERC20(stonkBroker_);
        protocolTreasury = protocolTreasury_;

        tiers[0] = Tier({fee: 66_666e18, multiplierBps: 10_000});
        tiers[1] = Tier({fee: 166_666e18, multiplierBps: 12_500});
        tiers[2] = Tier({fee: 366_666e18, multiplierBps: 16_000});
        tiers[3] = Tier({fee: 666_666e18, multiplierBps: 20_000});
        tiers[4] = Tier({fee: 1_666_666e18, multiplierBps: 33_300});
    }

    // ------------------------------------------------------------------
    // User actions
    // ------------------------------------------------------------------

    /// @notice Activates `tokenId` at `tier`, or upgrades it if already activated at a lower
    /// tier. First activation pays the full tier fee; upgrades pay only the difference between
    /// the new and current tier fees. Downgrades and same-tier re-activation revert.
    function activate(uint256 tokenId, uint8 tier) external nonReentrant whenNotPaused {
        _collectFee(msg.sender, _activateOne(tokenId, tier));
    }

    /// @notice Batch form of activate(): activates/upgrades many brokers in ONE transaction
    /// with a SINGLE fee pull for the summed total (one transferFrom + one burn + one treasury
    /// transfer instead of one set per broker). Semantics per entry are identical to sequential
    /// activate() calls — including atomicity: any invalid entry (not owner, bad tier,
    /// non-upgrade, duplicate at the same tier) reverts the whole batch.
    function activateBatch(uint256[] calldata tokenIds, uint8[] calldata tiersWanted)
        external
        nonReentrant
        whenNotPaused
    {
        uint256 n = tokenIds.length;
        if (n == 0 || n != tiersWanted.length) revert InvalidConfig();

        uint256 totalFee;
        for (uint256 i = 0; i < n; i++) {
            totalFee += _activateOne(tokenIds[i], tiersWanted[i]);
        }
        _collectFee(msg.sender, totalFee);
    }

    /// @dev Validation + state update + event for one activation/upgrade; returns the fee due.
    /// Fee COLLECTION is hoisted to the external entry points so a batch can pull once for the
    /// sum. State-before-collection is safe here: both entry points are nonReentrant and the
    /// whole transaction reverts (state included) if the pull fails.
    function _activateOne(uint256 tokenId, uint8 tier) internal returns (uint256 feeDue) {
        if (tier >= TIER_COUNT) revert InvalidTier();
        if (nft.ownerOf(tokenId) != msg.sender) revert NotTokenOwner();

        uint8 currentPlusOne = _tierPlusOne[tokenId];

        if (currentPlusOne == 0) {
            feeDue = tiers[tier].fee;
        } else {
            uint8 currentTier = currentPlusOne - 1;
            if (tier <= currentTier) revert TierNotUpgrade();
            feeDue = tiers[tier].fee - tiers[currentTier].fee;
        }

        uint256 newWeight = tiers[tier].multiplierBps;
        uint256 oldWeight = activeWeight[tokenId];

        _tierPlusOne[tokenId] = tier + 1;
        activeWeight[tokenId] = newWeight;
        totalActiveWeight = totalActiveWeight - oldWeight + newWeight;

        if (_activeIndexPlusOne[tokenId] == 0) {
            _activeTokens.push(tokenId);
            _activeIndexPlusOne[tokenId] = _activeTokens.length;
        }

        if (currentPlusOne == 0) {
            emit Events.Activated(tokenId, msg.sender, tier, feeDue);
        } else {
            emit Events.ActivationUpgraded(tokenId, msg.sender, currentPlusOne - 1, tier, feeDue);
        }
    }

    // ------------------------------------------------------------------
    // NFT transfer hook
    // ------------------------------------------------------------------

    /// @notice Clears activation for `tokenId`. Only the NFT contract may call this, from its
    /// ERC-721 transfer hook, so every true ownership change resets activation exactly once.
    /// Idempotent: clearing an inactive token is a no-op (transfer hooks must never revert).
    /// Deliberately NOT pausable — pausing it would let activation survive a transfer, and NFT
    /// transfers must never depend on this contract's admin state.
    function clearActivation(uint256 tokenId) external {
        if (msg.sender != address(nft)) revert Unauthorized();
        if (_tierPlusOne[tokenId] == 0) return;

        totalActiveWeight -= activeWeight[tokenId];
        delete activeWeight[tokenId];
        delete _tierPlusOne[tokenId];

        uint256 idxPlusOne = _activeIndexPlusOne[tokenId];
        uint256 lastIdx = _activeTokens.length - 1;
        uint256 idx = idxPlusOne - 1;
        if (idx != lastIdx) {
            uint256 movedToken = _activeTokens[lastIdx];
            _activeTokens[idx] = movedToken;
            _activeIndexPlusOne[movedToken] = idxPlusOne;
        }
        _activeTokens.pop();
        delete _activeIndexPlusOne[tokenId];

        emit Events.ActivationCleared(tokenId);
    }

    // ------------------------------------------------------------------
    // Owner config
    // ------------------------------------------------------------------

    /// @notice Updates the activation fee split. Shares must sum to exactly 10000 bps; a
    /// non-zero redirect share requires a non-zero receiver. This is the "optionally redirect
    /// activation fees to StonkBroker wallets" switch — point redirectReceiver at a distributor
    /// and shift bps into redirectBps.
    function setFeeSplit(uint16 burnBps_, uint16 protocolBps_, uint16 redirectBps_, address redirectReceiver_)
        external
        onlyOwner
    {
        if (uint256(burnBps_) + protocolBps_ + redirectBps_ != BPS) revert InvalidFeeSplit();
        if (redirectBps_ > 0 && redirectReceiver_ == address(0)) revert ZeroAddress();

        burnBps = burnBps_;
        protocolBps = protocolBps_;
        redirectBps = redirectBps_;
        redirectReceiver = redirectReceiver_;

        emit Events.ActivationFeeSplitUpdated(burnBps_, protocolBps_, redirectBps_, redirectReceiver_);
    }

    /// @notice Emergency stop for new activations/upgrades (fees are burned and non-refundable,
    /// so a wrong config must be haltable). Clearing stays live — see clearActivation.
    function pause() external onlyOwner {
        _pause();
    }

    function unpause() external onlyOwner {
        _unpause();
    }

    // ------------------------------------------------------------------
    // Views
    // ------------------------------------------------------------------

    /// @notice Current tier of `tokenId`, or (false, 0) when not activated.
    function activationOf(uint256 tokenId) external view returns (bool activated, uint8 tier) {
        uint8 plusOne = _tierPlusOne[tokenId];
        return plusOne == 0 ? (false, 0) : (true, plusOne - 1);
    }

    /// @notice Fee owed to move `tokenId` to `tier` right now (full fee if inactive,
    /// difference if upgrading). Reverts on non-upgrades, mirroring activate().
    function quoteActivation(uint256 tokenId, uint8 tier) public view returns (uint256 feeDue) {
        if (tier >= TIER_COUNT) revert InvalidTier();
        uint8 currentPlusOne = _tierPlusOne[tokenId];
        if (currentPlusOne == 0) return tiers[tier].fee;
        uint8 currentTier = currentPlusOne - 1;
        if (tier <= currentTier) revert TierNotUpgrade();
        return tiers[tier].fee - tiers[currentTier].fee;
    }

    /// @notice Total fee a matching activateBatch() call would pull right now — the exact
    /// amount to approve. Assumes distinct tokenIds (duplicates would revert in the batch
    /// itself anyway, since the second occurrence is not an upgrade).
    function quoteActivationBatch(uint256[] calldata tokenIds, uint8[] calldata tiersWanted)
        external
        view
        returns (uint256 total)
    {
        uint256 n = tokenIds.length;
        if (n == 0 || n != tiersWanted.length) revert InvalidConfig();
        for (uint256 i = 0; i < n; i++) {
            total += quoteActivation(tokenIds[i], tiersWanted[i]);
        }
    }

    function activeCount() external view returns (uint256) {
        return _activeTokens.length;
    }

    function activeTokenAt(uint256 index) external view returns (uint256) {
        return _activeTokens[index];
    }

    // ------------------------------------------------------------------
    // Internal
    // ------------------------------------------------------------------

    /// @dev Pulls the fee then routes it per the configured split: burn share is burned from
    /// this contract's balance ($STONKBROKER is ERC20Burnable), protocol share to treasury,
    /// redirect share to the optional receiver. Remainder-from-rounding lands in the burn
    /// share (computed by subtraction) so no dust is stranded here.
    function _collectFee(address payer, uint256 amount) internal {
        if (amount == 0) return;
        stonkBroker.safeTransferFrom(payer, address(this), amount);

        uint256 protocolShare = (amount * protocolBps) / BPS;
        uint256 redirectShare = (amount * redirectBps) / BPS;
        uint256 burnShare = amount - protocolShare - redirectShare;

        if (protocolShare > 0) stonkBroker.safeTransfer(protocolTreasury, protocolShare);
        if (redirectShare > 0) stonkBroker.safeTransfer(redirectReceiver, redirectShare);
        if (burnShare > 0) ERC20Burnable(address(stonkBroker)).burn(burnShare);
    }
}
Chain explorer3127msChain node76ms