HoodxTreasury

0x940030aa7e3c53f68783a9f73311c34dcbdafb01

Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
5,644 bytes
ABI entries
46
13 read · 11 write
License
none

Contract information

Address
0x940030aa7e3c53f68783a9f73311c34dcbdafb01
Chain
Robinhood Chain (4663)
Compiler
v0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0x5a2ac6a431…5a926ca2f2

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (13)

DISTRIBUTION_SHARE_BPS()uint256
approvedStockCount()uint256
approvedStockList(uint256)address
approvedStocks(address)bool
approvedSwapTargets(address)bool
availableForBuybacks(address)uint256
distributor()address
keepers(address)bool
owner()address
pendingOwner()address
spentOnBuybacks(address)uint256
withdrawableSurplus(address)uint256
withdrawnFees(address)uint256

Events (7)

BuybackExecutedDistributorSetKeeperSetOwnershipTransferStartedOwnershipTransferredStockApprovedSwapTargetSet

ABI

[
  {
    "inputs": [],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "AlreadyApproved",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "requested",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "available",
        "type": "uint256"
      }
    ],
    "name": "ExceedsDistributionShare",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "requested",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "surplus",
        "type": "uint256"
      }
    ],
    "name": "ExceedsSurplus",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotKeeper",
    "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": [
      {
        "internalType": "uint256",
        "name": "received",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "minOut",
        "type": "uint256"
      }
    ],
    "name": "SlippageExceeded",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "StockNotApproved",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "SwapFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "SwapTargetNotApproved",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddress",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "stock",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "tokenIn",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amountIn",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "stockOut",
        "type": "uint256"
      }
    ],
    "name": "BuybackExecuted",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "distributor",
        "type": "address"
      }
    ],
    "name": "DistributorSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "keeper",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "allowed",
        "type": "bool"
      }
    ],
    "name": "KeeperSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferStarted",
    "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": true,
        "internalType": "address",
        "name": "stock",
        "type": "address"
      }
    ],
    "name": "StockApproved",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "target",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "allowed",
        "type": "bool"
      }
    ],
    "name": "SwapTargetSet",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "DISTRIBUTION_SHARE_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "acceptOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "stock",
        "type": "address"
      }
    ],
    "name": "approveStock",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "approvedStockCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "approvedStockList",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "approvedStocks",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "approvedSwapTargets",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "tokenIn",
        "type": "address"
      }
    ],
    "name": "availableForBuybacks",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "stock",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "tokenIn",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amountIn",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "minStockOut",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "swapTarget",
        "type": "address"
      },
      {
        "internalType": "bytes",
        "name": "swapData",
        "type": "bytes"
      }
    ],
    "name": "buybackAndDistribute",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "distributor",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "keepers",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pendingOwner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "rescue",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_distributor",
        "type": "address"
      }
    ],
    "name": "setDistributor",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "keeper",
        "type": "address"
      },
      {
        "internalType": "bool",
        "name": "allowed",
        "type": "bool"
      }
    ],
    "name": "setKeeper",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "target",
        "type": "address"
      },
      {
        "internalType": "bool",
        "name": "allowed",
        "type": "bool"
      }
    ],
    "name": "setSwapTarget",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "spentOnBuybacks",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "syncDistributor",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "withdrawSurplus",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "withdrawableSurplus",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "withdrawnFees",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "stateMutability": "payable",
    "type": "receive"
  }
]

Source code

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

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Ownable2Step, Ownable} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

interface IHoodxDistributor {
    function addRewardToken(address token) external;
    function notifyReward(address token, uint256 amount) external;
    function isRewardToken(address token) external view returns (bool);
}

/// @title HoodxTreasury
/// @notice Mottar 4 %-fees fra HoodxFeeHook. Eier godkjenner Robinhood
///         Stock Tokens én og én (`approveStock`) etter hvert som dere
///         legger dem til på plattformen. En keeper trigger buybacks.
///
/// @dev Fordeling: Av 4 %-feen brukes maks HALVPARTEN (= 2 % av volum) til
///      buybacks og distribusjon. Resten forblir i treasury. Dette håndheves
///      on-chain per fee-token via `availableForBuybacks` — keeper/owner kan
///      aldri bruke mer enn 50 %% av all fee som noen gang er mottatt.
///
/// @dev Swap-vei: Stock Tokens trades via RFQ (0x) ved launch, ikke
///      nødvendigvis via Uniswap AMM-pools. Derfor bruker treasury en
///      WHITELIST av swap-targets (Universal Router, 0x Exchange Proxy,
///      osv.) i stedet for én hardkodet router. Sikkerheten ligger i:
///      (1) kun owner kan godkjenne swap-targets,
///      (2) balance-diff + minStockOut gjør at vilkårlig calldata ikke
///          kan stjele midler — enten kommer nok aksjer inn, eller så
///          reverteres hele transaksjonen.
contract HoodxTreasury is Ownable2Step, ReentrancyGuard {
    using SafeERC20 for IERC20;

    address public distributor;

    /// @notice Godkjente Robinhood Stock Token-kontrakter (eksisterende
    ///         kontrakter på Robinhood Chain — vi deployer ingen aksjer selv).
    mapping(address => bool) public approvedStocks;
    address[] public approvedStockList;

    /// @notice Godkjente swap-targets (Universal Router, 0x, ...).
    mapping(address => bool) public approvedSwapTargets;

    /// @notice Andel av mottatte fees som kan brukes til buybacks/distribusjon.
    ///         5000 bps = halvparten av 4 %-feen = 2 % av trading-volum.
    uint256 public constant DISTRIBUTION_SHARE_BPS = 5000;

    /// @notice Livstidsregnskap per fee-token (address(0) = native ETH).
    mapping(address => uint256) public spentOnBuybacks;
    mapping(address => uint256) public withdrawnFees;

    mapping(address => bool) public keepers;

    event StockApproved(address indexed stock);
    event SwapTargetSet(address indexed target, bool allowed);
    event KeeperSet(address indexed keeper, bool allowed);
    event DistributorSet(address indexed distributor);
    event BuybackExecuted(address indexed stock, address tokenIn, uint256 amountIn, uint256 stockOut);

    error NotKeeper();
    error StockNotApproved();
    error SwapTargetNotApproved();
    error AlreadyApproved();
    error SwapFailed();
    error ExceedsDistributionShare(uint256 requested, uint256 available);
    error SlippageExceeded(uint256 received, uint256 minOut);
    error ZeroAddress();

    modifier onlyKeeper() {
        if (!keepers[msg.sender] && msg.sender != owner()) revert NotKeeper();
        _;
    }

    constructor() Ownable(msg.sender) {}

    receive() external payable {} // fees kan komme som native ETH

    function setDistributor(address _distributor) external onlyOwner {
        if (_distributor == address(0)) revert ZeroAddress();
        distributor = _distributor;
        emit DistributorSet(_distributor);
    }

    /// @notice AUDIT-fix M-2: ved distributor-migrering (setDistributor etter
    ///         at aksjer er godkjent) manglet den nye distributoren reward-
    ///         registreringene — og approveStock kan ikke kjoeres om
    ///         (AlreadyApproved) — saa buybacks ville revertert permanent.
    ///         Denne synker alle godkjente aksjer inn i gjeldende distributor.
    function syncDistributor() external onlyOwner {
        IHoodxDistributor d = IHoodxDistributor(distributor);
        uint256 len = approvedStockList.length;
        for (uint256 i; i < len; ++i) {
            address stock = approvedStockList[i];
            if (!d.isRewardToken(stock)) d.addRewardToken(stock);
        }
    }

    function setKeeper(address keeper, bool allowed) external onlyOwner {
        keepers[keeper] = allowed;
        emit KeeperSet(keeper, allowed);
    }

    /// @notice Godkjenn/fjern en swap-target (Universal Router, 0x Exchange
    ///         Proxy osv.). Bruk KUN offisielt verifiserte kontrakter.
    function setSwapTarget(address target, bool allowed) external onlyOwner {
        if (target == address(0)) revert ZeroAddress();
        approvedSwapTargets[target] = allowed;
        emit SwapTargetSet(target, allowed);
    }

    /// @notice Godkjenn en ny Robinhood Stock Token for buybacks/distribusjon.
    ///         Registrerer den samtidig som reward-token i distributoren.
    ///         Godkjenning kan ikke reverseres (holdere skal kunne stole på
    ///         at opptjente aksjer aldri fryses).
    function approveStock(address stock) external onlyOwner {
        if (stock == address(0)) revert ZeroAddress();
        if (approvedStocks[stock]) revert AlreadyApproved();
        approvedStocks[stock] = true;
        approvedStockList.push(stock);
        IHoodxDistributor(distributor).addRewardToken(stock);
        emit StockApproved(stock);
    }

    function approvedStockCount() external view returns (uint256) {
        return approvedStockList.length;
    }

    /// @notice Hvor mye av en fee-token som fortsatt kan brukes til buybacks.
    ///         Livstidsinnflyt = nåværende balanse + brukt + uttatt.
    ///         Tilgjengelig = 50 %% av livstidsinnflyt − allerede brukt.
    function availableForBuybacks(address tokenIn) public view returns (uint256) {
        uint256 balance = tokenIn == address(0)
            ? address(this).balance
            : IERC20(tokenIn).balanceOf(address(this));
        uint256 lifetimeInflow = balance + spentOnBuybacks[tokenIn] + withdrawnFees[tokenIn];
        uint256 cap = (lifetimeInflow * DISTRIBUTION_SHARE_BPS) / 10_000;
        uint256 spent = spentOnBuybacks[tokenIn];
        return cap > spent ? cap - spent : 0;
    }

    /// @notice Kjøp en godkjent aksje for oppsamlede fees og distribuer.
    /// @param stock       Godkjent Stock Token som skal kjøpes.
    /// @param tokenIn     Fee-token som brukes (address(0) = native ETH).
    /// @param amountIn    Hvor mye av tokenIn som brukes.
    /// @param minStockOut Minimum aksjer mottatt — beregnes off-chain fra
    ///                    Chainlink-feeden til aksjen med slippage-toleranse.
    /// @param swapTarget  Godkjent swap-kontrakt (Universal Router / 0x).
    /// @param swapData    Calldata for swappen, bygget off-chain av keeper.
    function buybackAndDistribute(
        address stock,
        address tokenIn,
        uint256 amountIn,
        uint256 minStockOut,
        address swapTarget,
        bytes calldata swapData
    ) external onlyKeeper nonReentrant {
        if (!approvedStocks[stock]) revert StockNotApproved();
        if (!approvedSwapTargets[swapTarget]) revert SwapTargetNotApproved();
        // AUDIT-fix L-1: minStockOut == 0 ville latt en kompromittert keeper
        // godta en vilkaarlig daarlig kurs (sandwich). Naa kreves et reelt
        // prisanker fra keeperens Chainlink-berekning.
        if (minStockOut == 0) revert SlippageExceeded(0, 0);

        // Håndhev 2 %-regelen: aldri bruk mer enn halvparten av mottatte fees.
        uint256 available = availableForBuybacks(tokenIn);
        if (amountIn > available) revert ExceedsDistributionShare(amountIn, available);
        spentOnBuybacks[tokenIn] += amountIn;

        uint256 stockBefore = IERC20(stock).balanceOf(address(this));

        bool ok;
        if (tokenIn == address(0)) {
            (ok,) = swapTarget.call{value: amountIn}(swapData);
        } else {
            IERC20(tokenIn).forceApprove(swapTarget, amountIn);
            (ok,) = swapTarget.call(swapData);
            IERC20(tokenIn).forceApprove(swapTarget, 0);
        }
        if (!ok) revert SwapFailed();

        uint256 received = IERC20(stock).balanceOf(address(this)) - stockBefore;
        if (received < minStockOut || received == 0) {
            revert SlippageExceeded(received, minStockOut);
        }
        emit BuybackExecuted(stock, tokenIn, amountIn, received);

        IERC20(stock).safeTransfer(distributor, received);
        IHoodxDistributor(distributor).notifyReward(stock, received);
    }

    error ExceedsSurplus(uint256 requested, uint256 surplus);

    /// @notice Hvor mye owner kan ta ut som overskudd: alt som IKKE er
    ///         oeremerket buybacks. Buyback-halvparten kan owner ALDRI roere
    ///         — haandhevet her, ikke lovet.
    function withdrawableSurplus(address token) public view returns (uint256) {
        uint256 balance = token == address(0)
            ? address(this).balance
            : IERC20(token).balanceOf(address(this));
        uint256 reserved = availableForBuybacks(token);
        return balance > reserved ? balance - reserved : 0;
    }

    /// @notice Ta ut overskudd (den beholdte 50 %-andelen av fees).
    ///         Reverterer hvis beloepet ville spist av buyback-reserven.
    function withdrawSurplus(address token, address to, uint256 amount) external onlyOwner {
        uint256 surplus = withdrawableSurplus(token);
        if (amount > surplus) revert ExceedsSurplus(amount, surplus);
        withdrawnFees[token] += amount; // holder 50/50-regnskapet korrekt
        if (token == address(0)) {
            (bool okEth,) = to.call{value: amount}("");
            require(okEth, "ETH withdraw failed");
        } else {
            IERC20(token).safeTransfer(to, amount);
        }
    }

    /// @notice Nødluke for FREMMEDE tokens som sitter fast. Kan IKKE brukes
    ///         på godkjente aksjer (tilhoerer distribusjonen) og IKKE paa
    ///         ETH/fee-midler (bruk withdrawSurplus, som respekterer
    ///         buyback-reserven).
    function rescue(address token, address to, uint256 amount) external onlyOwner {
        if (approvedStocks[token]) revert StockNotApproved();
        if (token == address(0)) revert ZeroAddress();
        withdrawnFees[token] += amount; // holder 50/50-regnskapet korrekt
        IERC20(token).safeTransfer(to, amount);
    }
}
Chain explorer3020msChain node73ms