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
- Creator
- 0x31098c0E25…659D3783Fe
- 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