Robinhood MainnetChain ID: 4663
Contract detail

RoboSalaryPool

RoboSalaryPool is a verified Robinhood Chain smart contract at 0x2eaf5736…964da360 with 1 indexed transactions.

Address type
Smart contract
Transactions
1
First indexed block
#28275052
Latest indexed block
#28275052
Emitted events
7
Verification
Verified
Compiler
v0.8.28+commit.7893614a
Verification match
full
Creation transaction
0xda39efb6…8a4898aa
Created at block
#28274551
Language
solidity
EVM version
paris
License
none
Compiler optimization
Enabled · 200 runs
Related activity

Transactions

Verified contract

Contract source code

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

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
import {IActivationTransferHook, IRewardPool} from "./interfaces/IRoboSalaryPool.sol";

interface IERC20BurnFrom is IERC20 {
    function burnFrom(address account, uint256 amount) external;
}

/// @title RoboBrokers Salary Pool
/// @notice Splits ROBO activation fees 50/50 between burn and an immutable
///         protocol treasury, then distributes the immutable NVDA/INTC/TSLA
///         robotics basket by activation weight.
contract RoboSalaryPool is IActivationTransferHook, IRewardPool, Ownable2Step, ReentrancyGuard {
    using SafeERC20 for IERC20;

    uint256 private constant ACCURACY = 1e24;
    uint8 public constant REWARD_COUNT = 3;
    uint8 public constant NVDA_REWARD = 0;
    uint8 public constant INTC_REWARD = 1;
    uint8 public constant TSLA_REWARD = 2;
    uint8 public constant TIER_COUNT = 5;

    uint8 public constant JUNIOR_BROKER = 0;
    uint8 public constant MARKET_ANALYST = 1;
    uint8 public constant PORTFOLIO_MANAGER = 2;
    uint8 public constant MANAGING_DIRECTOR = 3;
    uint8 public constant PARTNER = 4;

    struct Tier {
        uint128 activationCost;
        uint32 weight100;
    }

    struct Activation {
        address owner;
        uint32 weight100;
        uint8 tier;
        bool active;
    }

    IERC20BurnFrom public immutable robo;
    IERC721 public immutable nft;
    address public immutable activationTreasury;
    IERC20[3] public rewardTokens;

    bool public tierConfigurationFrozen;
    uint256 public activeRobotCount;
    uint256 public override totalActiveWeight100;

    mapping(uint8 rewardIndex => uint256 amount) public accumulatedRewardPerWeight;
    mapping(uint8 tier => Tier config) public tiers;
    mapping(uint256 tokenId => Activation activation) public activations;
    mapping(uint256 tokenId => mapping(uint8 rewardIndex => uint256 amount)) public rewardDebt;
    mapping(address account => mapping(uint8 rewardIndex => uint256 amount)) public storedRewards;

    error ZeroAddress();
    error InvalidContract();
    error IdenticalTokens();
    error NotNFTOwner();
    error AlreadyActive();
    error NotNFTContract();
    error InvalidTransferOwner();
    error InvalidReward();
    error InvalidTier();
    error InvalidTierConfiguration();
    error InvalidTierOrder();
    error InvalidUpgrade();
    error TierConfigurationIsFrozen();
    error TierConfigurationNotFrozen();
    error NoActiveRobots();
    error NoRewardsProvided();

    event RobotActivated(
        uint256 indexed tokenId,
        address indexed owner,
        uint8 indexed tier,
        uint256 roboBurned,
        uint256 roboToTreasury,
        uint32 weight100
    );
    event RobotDeactivated(
        uint256 indexed tokenId,
        address indexed previousOwner,
        uint256 nvdaStored,
        uint256 intcStored,
        uint256 tslaStored
    );
    event RobotUpgraded(
        uint256 indexed tokenId,
        address indexed owner,
        uint8 indexed previousTier,
        uint8 newTier,
        uint256 roboBurned,
        uint256 roboToTreasury,
        uint32 newWeight100,
        uint256 nvdaStored,
        uint256 intcStored,
        uint256 tslaStored
    );
    event RewardsFunded(
        uint8 indexed rewardIndex,
        address indexed rewardToken,
        address indexed funder,
        uint256 amount
    );
    event RewardsClaimed(
        uint8 indexed rewardIndex,
        address indexed rewardToken,
        address indexed account,
        uint256 amount
    );
    event TierUpdated(uint8 indexed tier, uint256 activationCost, uint32 weight100);
    event TierConfigurationFrozen();

    constructor(
        address initialOwner,
        address roboToken,
        address nvdaToken,
        address intcToken,
        address tslaToken,
        address nftContract,
        address protocolTreasury
    ) Ownable(initialOwner) {
        if (
            initialOwner == address(0) ||
            roboToken == address(0) ||
            nvdaToken == address(0) ||
            intcToken == address(0) ||
            tslaToken == address(0) ||
            nftContract == address(0) ||
            protocolTreasury == address(0)
        ) revert ZeroAddress();
        if (
            roboToken == nvdaToken || roboToken == intcToken || roboToken == tslaToken ||
            nvdaToken == intcToken || nvdaToken == tslaToken || intcToken == tslaToken
        ) {
            revert IdenticalTokens();
        }
        if (
            roboToken.code.length == 0 ||
            nvdaToken.code.length == 0 ||
            intcToken.code.length == 0 ||
            tslaToken.code.length == 0 ||
            nftContract.code.length == 0
        ) revert InvalidContract();

        robo = IERC20BurnFrom(roboToken);
        rewardTokens[NVDA_REWARD] = IERC20(nvdaToken);
        rewardTokens[INTC_REWARD] = IERC20(intcToken);
        rewardTokens[TSLA_REWARD] = IERC20(tslaToken);
        nft = IERC721(nftContract);
        activationTreasury = protocolTreasury;

        _setTier(JUNIOR_BROKER, 55_555 ether, 100);
        _setTier(MARKET_ANALYST, 144_444 ether, 120);
        _setTier(PORTFOLIO_MANAGER, 333_333 ether, 150);
        _setTier(MANAGING_DIRECTOR, 555_555 ether, 200);
        _setTier(PARTNER, 1_444_444 ether, 300);
    }

    function activate(uint256 tokenId, uint8 tier) external nonReentrant {
        if (!tierConfigurationFrozen) revert TierConfigurationNotFrozen();
        if (tier >= TIER_COUNT) revert InvalidTier();
        if (nft.ownerOf(tokenId) != msg.sender) revert NotNFTOwner();
        if (activations[tokenId].active) revert AlreadyActive();

        Tier memory config = tiers[tier];
        (uint256 burnAmount, uint256 treasuryAmount) =
            _collectActivationFee(msg.sender, config.activationCost);

        activations[tokenId] = Activation({
            owner: msg.sender,
            weight100: config.weight100,
            tier: tier,
            active: true
        });
        for (uint8 rewardIndex; rewardIndex < REWARD_COUNT; ++rewardIndex) {
            rewardDebt[tokenId][rewardIndex] = _accumulated(config.weight100, rewardIndex);
        }
        ++activeRobotCount;
        totalActiveWeight100 += config.weight100;

        emit RobotActivated(
            tokenId,
            msg.sender,
            tier,
            burnAmount,
            treasuryAmount,
            config.weight100
        );
    }

    function upgrade(uint256 tokenId, uint8 newTier) external nonReentrant {
        if (!tierConfigurationFrozen) revert TierConfigurationNotFrozen();
        if (newTier >= TIER_COUNT) revert InvalidTier();

        Activation storage activation = activations[tokenId];
        if (!activation.active || activation.owner != msg.sender || nft.ownerOf(tokenId) != msg.sender) {
            revert NotNFTOwner();
        }
        uint8 previousTier = activation.tier;
        if (newTier <= previousTier) revert InvalidUpgrade();

        Tier memory previousConfig = tiers[previousTier];
        Tier memory newConfig = tiers[newTier];
        uint256 upgradeCost = uint256(newConfig.activationCost) - previousConfig.activationCost;
        uint256[3] memory amountsStored;
        for (uint8 rewardIndex; rewardIndex < REWARD_COUNT; ++rewardIndex) {
            amountsStored[rewardIndex] = _storePending(tokenId, activation, rewardIndex);
        }

        (uint256 burnAmount, uint256 treasuryAmount) =
            _collectActivationFee(msg.sender, upgradeCost);

        totalActiveWeight100 = totalActiveWeight100 - activation.weight100 + newConfig.weight100;
        activation.weight100 = newConfig.weight100;
        activation.tier = newTier;
        for (uint8 rewardIndex; rewardIndex < REWARD_COUNT; ++rewardIndex) {
            rewardDebt[tokenId][rewardIndex] = _accumulated(newConfig.weight100, rewardIndex);
        }

        emit RobotUpgraded(
            tokenId,
            msg.sender,
            previousTier,
            newTier,
            burnAmount,
            treasuryAmount,
            newConfig.weight100,
            amountsStored[NVDA_REWARD],
            amountsStored[INTC_REWARD],
            amountsStored[TSLA_REWARD]
        );
    }

    function _collectActivationFee(address payer, uint256 amount)
        private
        returns (uint256 burnAmount, uint256 treasuryAmount)
    {
        burnAmount = amount / 2;
        treasuryAmount = amount - burnAmount;
        robo.burnFrom(payer, burnAmount);
        IERC20(address(robo)).safeTransferFrom(payer, activationTreasury, treasuryAmount);
    }

    function onNFTTransfer(uint256 tokenId, address previousOwner, address) external override nonReentrant {
        if (msg.sender != address(nft)) revert NotNFTContract();
        Activation memory activation = activations[tokenId];
        if (!activation.active) return;
        if (activation.owner != previousOwner) revert InvalidTransferOwner();

        uint256[3] memory amountsStored;
        for (uint8 rewardIndex; rewardIndex < REWARD_COUNT; ++rewardIndex) {
            amountsStored[rewardIndex] = _storePending(tokenId, activation, rewardIndex);
            delete rewardDebt[tokenId][rewardIndex];
        }

        delete activations[tokenId];
        --activeRobotCount;
        totalActiveWeight100 -= activation.weight100;

        emit RobotDeactivated(
            tokenId,
            previousOwner,
            amountsStored[NVDA_REWARD],
            amountsStored[INTC_REWARD],
            amountsStored[TSLA_REWARD]
        );
    }

    function fundRewards(uint8 rewardIndex, uint256 amount)
        external
        override
        nonReentrant
        returns (uint256 received)
    {
        IERC20 rewardToken = _rewardToken(rewardIndex);
        if (totalActiveWeight100 == 0) revert NoActiveRobots();
        uint256 balanceBefore = rewardToken.balanceOf(address(this));
        rewardToken.safeTransferFrom(msg.sender, address(this), amount);
        received = rewardToken.balanceOf(address(this)) - balanceBefore;
        if (received == 0) revert NoRewardsProvided();
        accumulatedRewardPerWeight[rewardIndex] += Math.mulDiv(
            received,
            ACCURACY,
            totalActiveWeight100
        );
        emit RewardsFunded(rewardIndex, address(rewardToken), msg.sender, received);
    }

    /// @notice Claims one reward asset at a time so a problem with one stock
    ///         token cannot block claims of the other components.
    function claim(uint8 rewardIndex, uint256[] calldata activeTokenIds)
        external
        nonReentrant
        returns (uint256 amount)
    {
        amount = _claim(rewardIndex, msg.sender, activeTokenIds);
    }

    /// @notice Convenience path for the UI. This is atomic; users can fall
    ///         back to claim(index, ids) if any individual stock token pauses.
    function claimAll(uint256[] calldata activeTokenIds)
        external
        nonReentrant
        returns (uint256[3] memory amounts)
    {
        for (uint8 rewardIndex; rewardIndex < REWARD_COUNT; ++rewardIndex) {
            amounts[rewardIndex] = _claim(rewardIndex, msg.sender, activeTokenIds);
        }
    }

    function pendingRewards(uint256 tokenId, uint8 rewardIndex) external view returns (uint256) {
        if (rewardIndex >= REWARD_COUNT) revert InvalidReward();
        Activation memory activation = activations[tokenId];
        return activation.active ? _pending(tokenId, activation, rewardIndex) : 0;
    }

    function setTier(uint8 tier, uint128 activationCost, uint32 weight100) external onlyOwner {
        if (tierConfigurationFrozen) revert TierConfigurationIsFrozen();
        _setTier(tier, activationCost, weight100);
    }

    function freezeTierConfiguration() external onlyOwner {
        for (uint8 tier = 1; tier < TIER_COUNT; ++tier) {
            Tier memory previous = tiers[tier - 1];
            Tier memory current = tiers[tier];
            if (
                current.activationCost <= previous.activationCost ||
                current.weight100 <= previous.weight100
            ) revert InvalidTierOrder();
        }
        tierConfigurationFrozen = true;
        emit TierConfigurationFrozen();
    }

    function _setTier(uint8 tier, uint128 activationCost, uint32 weight100) private {
        if (tier >= TIER_COUNT) revert InvalidTier();
        if (activationCost == 0 || weight100 == 0) revert InvalidTierConfiguration();
        tiers[tier] = Tier(activationCost, weight100);
        emit TierUpdated(tier, activationCost, weight100);
    }

    function _rewardToken(uint8 rewardIndex) private view returns (IERC20) {
        if (rewardIndex >= REWARD_COUNT) revert InvalidReward();
        return rewardTokens[rewardIndex];
    }

    function _claim(uint8 rewardIndex, address account, uint256[] calldata activeTokenIds)
        private
        returns (uint256 amount)
    {
        IERC20 rewardToken = _rewardToken(rewardIndex);
        amount = storedRewards[account][rewardIndex];
        storedRewards[account][rewardIndex] = 0;

        for (uint256 i; i < activeTokenIds.length; ++i) {
            uint256 tokenId = activeTokenIds[i];
            Activation storage activation = activations[tokenId];
            if (!activation.active || activation.owner != account) revert NotNFTOwner();
            amount += _pending(tokenId, activation, rewardIndex);
            rewardDebt[tokenId][rewardIndex] = _accumulated(activation.weight100, rewardIndex);
        }

        if (amount != 0) rewardToken.safeTransfer(account, amount);
        emit RewardsClaimed(rewardIndex, address(rewardToken), account, amount);
    }

    function _accumulated(uint32 weight100, uint8 rewardIndex) private view returns (uint256) {
        return Math.mulDiv(weight100, accumulatedRewardPerWeight[rewardIndex], ACCURACY);
    }

    function _pending(uint256 tokenId, Activation memory activation, uint8 rewardIndex)
        private
        view
        returns (uint256)
    {
        return _accumulated(activation.weight100, rewardIndex) - rewardDebt[tokenId][rewardIndex];
    }

    function _storePending(uint256 tokenId, Activation memory activation, uint8 rewardIndex)
        private
        returns (uint256 amount)
    {
        amount = _pending(tokenId, activation, rewardIndex);
        if (amount != 0) storedRewards[activation.owner][rewardIndex] += amount;
    }
}