FeeRouter

0xba3a36b648bce86c62a82134c38fbfc51a174c8e

Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
6,385 bytes
ABI entries
51
17 read · 12 write
License
none

Contract information

Address
0xba3a36b648bce86c62a82134c38fbfc51a174c8e
Chain
Robinhood Chain (4663)
Compiler
v0.8.24+commit.e11b9ed9
Optimization
Enabled
Creation tx
0xbbaa3a8b16…d3c217a576

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (17)

BPS()uint16
DEAD()address
burnBps()uint16
buybackBps()uint16
indexBps()uint16
indexManager()address
isKeeper(address)bool
maxSlippageBps()uint16
memedex()address
opsBps()uint16
opsWallet()address
owner()address
pendingOwner()address
router()address
treasury()address
treasuryBps()uint16
weth()address

Events (11)

BurnBpsSetBuybackDistributedKeeperSetMaxSlippageSetMemedexSetOwnershipTransferStartedOwnershipTransferredRouterSetSplitsSetTargetsSet

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "initialOwner",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "weth_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "router_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "indexManager_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "treasury_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "opsWallet_",
        "type": "address"
      },
      {
        "internalType": "uint16",
        "name": "indexBps_",
        "type": "uint16"
      },
      {
        "internalType": "uint16",
        "name": "buybackBps_",
        "type": "uint16"
      },
      {
        "internalType": "uint16",
        "name": "treasuryBps_",
        "type": "uint16"
      },
      {
        "internalType": "uint16",
        "name": "opsBps_",
        "type": "uint16"
      },
      {
        "internalType": "uint16",
        "name": "maxSlippageBps_",
        "type": "uint16"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "BadSplit",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "MemedexAlreadySet",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotKeeper",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NothingToDistribute",
    "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": "ZeroAddress",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint16",
        "name": "bps",
        "type": "uint16"
      }
    ],
    "name": "BurnBpsSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "wethIn",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "memedexBought",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "burned",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "toTreasury",
        "type": "uint256"
      }
    ],
    "name": "Buyback",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "wethTotal",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "toIndex",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "buyback",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "toTreasury",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "toOps",
        "type": "uint256"
      }
    ],
    "name": "Distributed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "keeper",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "enabled",
        "type": "bool"
      }
    ],
    "name": "KeeperSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint16",
        "name": "bps",
        "type": "uint16"
      }
    ],
    "name": "MaxSlippageSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "address",
        "name": "memedex",
        "type": "address"
      }
    ],
    "name": "MemedexSet",
    "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": false,
        "internalType": "address",
        "name": "router",
        "type": "address"
      }
    ],
    "name": "RouterSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint16",
        "name": "indexBps",
        "type": "uint16"
      },
      {
        "indexed": false,
        "internalType": "uint16",
        "name": "buybackBps",
        "type": "uint16"
      },
      {
        "indexed": false,
        "internalType": "uint16",
        "name": "treasuryBps",
        "type": "uint16"
      },
      {
        "indexed": false,
        "internalType": "uint16",
        "name": "opsBps",
        "type": "uint16"
      }
    ],
    "name": "SplitsSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "address",
        "name": "indexManager",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "treasury",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "opsWallet",
        "type": "address"
      }
    ],
    "name": "TargetsSet",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "BPS",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "DEAD",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "acceptOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "burnBps",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "buybackBps",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "distribute",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "indexBps",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "indexManager",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "isKeeper",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "maxSlippageBps",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "memedex",
    "outputs": [
      {
        "internalType": "contract IERC20",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "opsBps",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "opsWallet",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "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": [],
    "name": "router",
    "outputs": [
      {
        "internalType": "contract IUniswapV2Router",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint16",
        "name": "bps",
        "type": "uint16"
      }
    ],
    "name": "setBurnBps",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "keeper",
        "type": "address"
      },
      {
        "internalType": "bool",
        "name": "enabled",
        "type": "bool"
      }
    ],
    "name": "setKeeper",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint16",
        "name": "bps",
        "type": "uint16"
      }
    ],
    "name": "setMaxSlippage",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "memedex_",
        "type": "address"
      }
    ],
    "name": "setMemedex",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "router_",
        "type": "address"
      }
    ],
    "name": "setRouter",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint16",
        "name": "indexBps_",
        "type": "uint16"
      },
      {
        "internalType": "uint16",
        "name": "buybackBps_",
        "type": "uint16"
      },
      {
        "internalType": "uint16",
        "name": "treasuryBps_",
        "type": "uint16"
      },
      {
        "internalType": "uint16",
        "name": "opsBps_",
        "type": "uint16"
      }
    ],
    "name": "setSplits",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "indexManager_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "treasury_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "opsWallet_",
        "type": "address"
      }
    ],
    "name": "setTargets",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "treasury",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "treasuryBps",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "weth",
    "outputs": [
      {
        "internalType": "contract IERC20",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "stateMutability": "payable",
    "type": "receive"
  }
]

Source code

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

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {Governed} from "./access/Governed.sol";
import {IUniswapV2Router} from "./interfaces/IUniswapV2Router.sol";
import {IWETH} from "./interfaces/IWETH.sol";

/// @title  FeeRouter
/// @notice The flap.sh vault beneficiary: receives the trade tax (as native ETH on RH Chain, or WETH) and
///         splits it four ways — memecoin index buys, MEMEDEX buyback (burn + Treasury), WETH reserve, ops.
///         Splits are bps that must sum to 10000. Any native ETH received is auto-wrapped to WETH on distribute.
contract FeeRouter is Governed, ReentrancyGuard {
    using SafeERC20 for IERC20;

    uint16 public constant BPS = 10_000;

    IERC20 public immutable weth;
    IERC20 public memedex; // set once AFTER the flap token is created (its address isn't known at deploy)
    IUniswapV2Router public router;

    address public indexManager;
    address public treasury;
    address public opsWallet;

    uint16 public indexBps;
    uint16 public buybackBps;
    uint16 public treasuryBps;
    uint16 public opsBps;
    uint16 public maxSlippageBps; // guard on the buyback swap
    uint16 public burnBps = 6600; // portion of each MEMEDEX buyback that is BURNED (rest → treasury). Default 66%.

    address public constant DEAD = 0x000000000000000000000000000000000000dEaD;

    event SplitsSet(uint16 indexBps, uint16 buybackBps, uint16 treasuryBps, uint16 opsBps);
    event TargetsSet(address indexManager, address treasury, address opsWallet);
    event Distributed(uint256 wethTotal, uint256 toIndex, uint256 buyback, uint256 toTreasury, uint256 toOps);
    event RouterSet(address router);
    event MemedexSet(address memedex);
    event MaxSlippageSet(uint16 bps);
    event BurnBpsSet(uint16 bps);
    event Buyback(uint256 wethIn, uint256 memedexBought, uint256 burned, uint256 toTreasury);

    error BadSplit();
    error ZeroAddress();
    error NothingToDistribute();
    error MemedexAlreadySet();

    constructor(
        address initialOwner,
        address weth_,
        address router_,
        address indexManager_,
        address treasury_,
        address opsWallet_,
        uint16 indexBps_,
        uint16 buybackBps_,
        uint16 treasuryBps_,
        uint16 opsBps_,
        uint16 maxSlippageBps_
    ) Governed(initialOwner) {
        if (
            weth_ == address(0) || router_ == address(0) || indexManager_ == address(0)
                || treasury_ == address(0) || opsWallet_ == address(0)
        ) revert ZeroAddress();
        if (uint256(indexBps_) + buybackBps_ + treasuryBps_ + opsBps_ != BPS) revert BadSplit();

        weth = IERC20(weth_);
        router = IUniswapV2Router(router_);
        indexManager = indexManager_;
        treasury = treasury_;
        opsWallet = opsWallet_;
        indexBps = indexBps_;
        buybackBps = buybackBps_;
        treasuryBps = treasuryBps_;
        opsBps = opsBps_;
        maxSlippageBps = maxSlippageBps_;
    }

    // --- admin -------------------------------------------------------------

    function setSplits(uint16 indexBps_, uint16 buybackBps_, uint16 treasuryBps_, uint16 opsBps_)
        external
        onlyOwner
    {
        if (uint256(indexBps_) + buybackBps_ + treasuryBps_ + opsBps_ != BPS) revert BadSplit();
        indexBps = indexBps_;
        buybackBps = buybackBps_;
        treasuryBps = treasuryBps_;
        opsBps = opsBps_;
        emit SplitsSet(indexBps_, buybackBps_, treasuryBps_, opsBps_);
    }

    function setTargets(address indexManager_, address treasury_, address opsWallet_) external onlyOwner {
        if (indexManager_ == address(0) || treasury_ == address(0) || opsWallet_ == address(0)) {
            revert ZeroAddress();
        }
        indexManager = indexManager_;
        treasury = treasury_;
        opsWallet = opsWallet_;
        emit TargetsSet(indexManager_, treasury_, opsWallet_);
    }

    function setRouter(address router_) external onlyOwner {
        if (router_ == address(0)) revert ZeroAddress();
        router = IUniswapV2Router(router_);
        emit RouterSet(router_);
    }

    /// @notice Wire the MEMEDEX token address once the flap token exists. Set-once. Until it is set, the
    ///         buyback slice is routed to the Treasury as WETH instead of buying MEMEDEX.
    function setMemedex(address memedex_) external onlyOwner {
        if (memedex_ == address(0)) revert ZeroAddress();
        if (address(memedex) != address(0)) revert MemedexAlreadySet();
        memedex = IERC20(memedex_);
        emit MemedexSet(memedex_);
    }

    function setMaxSlippage(uint16 bps) external onlyOwner {
        if (bps > BPS) revert BadSplit();
        maxSlippageBps = bps;
        emit MaxSlippageSet(bps);
    }

    /// @notice Portion (bps) of each MEMEDEX buyback that is burned (sent to dead); the rest goes to Treasury.
    function setBurnBps(uint16 bps) external onlyOwner {
        if (bps > BPS) revert BadSplit();
        burnBps = bps;
        emit BurnBpsSet(bps);
    }

    // --- core --------------------------------------------------------------

    /// @notice Accept native ETH from flap's Tax Splitter (RH Chain pays in ETH). Wrapped on distribute().
    receive() external payable {}

    /// @notice Split the contract's entire WETH balance per the configured bps. Keeper-callable.
    function distribute() external onlyKeeper nonReentrant {
        // flap delivers the tax in the payment token (ETH on RH Chain). Wrap any native ETH to WETH first.
        uint256 nativeBal = address(this).balance;
        if (nativeBal > 0) IWETH(address(weth)).deposit{value: nativeBal}();

        uint256 bal = weth.balanceOf(address(this));
        if (bal == 0) revert NothingToDistribute();

        uint256 toIndex = (bal * indexBps) / BPS;
        uint256 buyback = (bal * buybackBps) / BPS;
        uint256 toTreasury = (bal * treasuryBps) / BPS;
        // ops gets the remainder so the sum is always exactly `bal` (no dust stranded).
        uint256 toOps = bal - toIndex - buyback - toTreasury;

        if (toIndex > 0) weth.safeTransfer(indexManager, toIndex);
        if (toTreasury > 0) weth.safeTransfer(treasury, toTreasury);
        if (toOps > 0) weth.safeTransfer(opsWallet, toOps);
        if (buyback > 0) {
            // Before the flap token is wired, park the buyback slice in the Treasury (as WETH) rather than
            // reverting — so fee routing works from the moment the token trades.
            if (address(memedex) == address(0)) {
                weth.safeTransfer(treasury, buyback);
            } else {
                _buyback(buyback);
            }
        }

        emit Distributed(bal, toIndex, buyback, toTreasury, toOps);
    }

    /// @dev Buy MEMEDEX with WETH, burn `burnBps` of it (to dead) and send the rest to the Treasury.
    ///      Slippage-guarded. NOTE: the FeeRouter should be tax-exempt on the flap MEMEDEX token, else the buy
    ///      pays MEMEDEX's own buy tax (it recycles back via the Tax Splitter, but wastes gas/slippage).
    function _buyback(uint256 wethIn) private {
        address[] memory path = new address[](2);
        path[0] = address(weth);
        path[1] = address(memedex);

        uint256[] memory quote = router.getAmountsOut(wethIn, path);
        uint256 minOut = (quote[1] * (BPS - maxSlippageBps)) / BPS;

        uint256 beforeBal = memedex.balanceOf(address(this));
        IERC20(address(weth)).forceApprove(address(router), wethIn);
        router.swapExactTokensForTokensSupportingFeeOnTransferTokens(
            wethIn, minOut, path, address(this), block.timestamp
        );
        uint256 bought = memedex.balanceOf(address(this)) - beforeBal;

        uint256 toBurn = (bought * burnBps) / BPS;
        uint256 toTreasury = bought - toBurn;
        if (toBurn > 0) memedex.safeTransfer(DEAD, toBurn); // permanent burn → deflationary
        if (toTreasury > 0) memedex.safeTransfer(treasury, toTreasury); // dry powder
        emit Buyback(wethIn, bought, toBurn, toTreasury);
    }

    /// @notice Recover tokens accidentally sent here (not WETH, which is the working balance).
    function rescue(address token, address to, uint256 amount) external onlyOwner {
        if (to == address(0)) revert ZeroAddress();
        IERC20(token).safeTransfer(to, amount);
    }
}
Chain explorer2977msChain node73ms