RobinPumpFeeRouter

0xcc30e6ffd47f34154cf863e5b4ce01e161c221ab

Verification
Verified
v0.8.35+commit.47b9dedd
Type
Contract
6,796 bytes
ABI entries
22
7 read · 9 write
License
none

Contract information

Address
0xcc30e6ffd47f34154cf863e5b4ce01e161c221ab
Chain
Robinhood Chain (4663)
Compiler
v0.8.35+commit.47b9dedd
Optimization
Enabled
Creation tx
0xf92c772248…4ac4dd3ee7

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (7)

MAX_FEE_BPS()uint16
feeBps()uint16
feeRecipient()address
owner()address
v2Router()address
v3Router()address
weth()address

Events (4)

FeeBpsSetFeeRecipientSetFeeRoutedOwnershipTransferred

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_v2Router",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_v3Router",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_weth",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_feeRecipient",
        "type": "address"
      },
      {
        "internalType": "uint16",
        "name": "_feeBps",
        "type": "uint16"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint16",
        "name": "oldBps",
        "type": "uint16"
      },
      {
        "indexed": false,
        "internalType": "uint16",
        "name": "newBps",
        "type": "uint16"
      }
    ],
    "name": "FeeBpsSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "address",
        "name": "oldRecipient",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "newRecipient",
        "type": "address"
      }
    ],
    "name": "FeeRecipientSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "trader",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "isBuy",
        "type": "bool"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "feeEth",
        "type": "uint256"
      }
    ],
    "name": "FeeRouted",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "oldOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferred",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "MAX_FEE_BPS",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "feeBps",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "feeRecipient",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint16",
        "name": "_feeBps",
        "type": "uint16"
      }
    ],
    "name": "setFeeBps",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_feeRecipient",
        "type": "address"
      }
    ],
    "name": "setFeeRecipient",
    "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": "uint256",
        "name": "amountOutMin",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      }
    ],
    "name": "v2Buy",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amountOutMin",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      }
    ],
    "name": "v2BuyFeeOnTransfer",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "v2Router",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amountIn",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "minEthOut",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      }
    ],
    "name": "v2Sell",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amountIn",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "minEthOut",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      }
    ],
    "name": "v2SellFeeOnTransfer",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "tokenOut",
        "type": "address"
      },
      {
        "internalType": "uint24",
        "name": "poolFee",
        "type": "uint24"
      },
      {
        "internalType": "uint256",
        "name": "amountOutMin",
        "type": "uint256"
      }
    ],
    "name": "v3Buy",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "v3Router",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "tokenIn",
        "type": "address"
      },
      {
        "internalType": "uint24",
        "name": "poolFee",
        "type": "uint24"
      },
      {
        "internalType": "uint256",
        "name": "amountIn",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "minEthOut",
        "type": "uint256"
      }
    ],
    "name": "v3Sell",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "weth",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "stateMutability": "payable",
    "type": "receive"
  }
]

Source code

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

/**
 * ███ RobinPumpFeeRouter — transparent platform-fee router for external Top Memes swaps ███
 *
 * Sits between the user and Uniswap V2/V3 for external meme-coin swaps only.
 * RobinPump bonding-curve tokens go through the factory directly and are
 * NEVER routed through this contract.
 *
 * Fee model:
 *  - On ETH-in (buy): fee = ethIn * feeBps / 10000 sent to feeRecipient,
 *    remaining ETH forwarded to Uniswap. Single user transaction.
 *  - On token-in (sell): full tokenIn amount swapped, fee deducted from ETH
 *    output, remainder sent to caller. Single user transaction.
 *
 * Owner powers are intentionally narrow:
 *  - setFeeBps: adjust fee rate, hard-capped at MAX_FEE_BPS (1%).
 *  - setFeeRecipient: change treasury address.
 *  - transferOwnership: hand off the role.
 *  Owner has NO power over any token, the bonding curve, or user funds beyond
 *  the routing flow described above.
 *
 * Safety properties:
 *  - Reentrancy-guarded on all external entry points.
 *  - Fee hard-capped at 1% (100 bps) in the contract — not just a convention.
 *  - No upgradability; no proxy.
 *  - V2/V3 router addresses are immutable after deployment.
 *
 * ⚠ UNAUDITED. Test on Robinhood Chain testnet (46630) before mainnet use.
 */

interface IUniswapV2Router {
    function swapExactETHForTokens(
        uint256 amountOutMin,
        address[] calldata path,
        address to,
        uint256 deadline
    ) external payable returns (uint256[] memory amounts);

    function swapExactTokensForETH(
        uint256 amountIn,
        uint256 amountOutMin,
        address[] calldata path,
        address to,
        uint256 deadline
    ) external returns (uint256[] memory amounts);

    function swapExactETHForTokensSupportingFeeOnTransferTokens(
        uint256 amountOutMin,
        address[] calldata path,
        address to,
        uint256 deadline
    ) external payable;

    function swapExactTokensForETHSupportingFeeOnTransferTokens(
        uint256 amountIn,
        uint256 amountOutMin,
        address[] calldata path,
        address to,
        uint256 deadline
    ) external;
}

interface IUniswapV3Router {
    struct ExactInputSingleParams {
        address tokenIn;
        address tokenOut;
        uint24 fee;
        address recipient;
        uint256 amountIn;
        uint256 amountOutMinimum;
        uint160 sqrtPriceLimitX96;
    }

    function exactInputSingle(ExactInputSingleParams calldata params)
        external
        payable
        returns (uint256 amountOut);
}

interface IWETH9 {
    function withdraw(uint256 amount) external;
}

interface IERC20Minimal {
    function allowance(address owner, address spender) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
}

contract RobinPumpFeeRouter {
    /// @notice Hard cap: owner can never set feeBps above 1%.
    uint16 public constant MAX_FEE_BPS = 100;

    address public immutable v2Router;
    address public immutable v3Router;
    address public immutable weth;

    address public owner;
    address public feeRecipient;
    uint16  public feeBps;

    uint256 private _lock = 1;

    modifier nonReentrant() {
        require(_lock == 1, "reentrant");
        _lock = 2;
        _;
        _lock = 1;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "not owner");
        _;
    }

    // ── Events ────────────────────────────────────────────────────────

    event FeeRouted(address indexed trader, bool isBuy, uint256 feeEth);
    event FeeBpsSet(uint16 oldBps, uint16 newBps);
    event FeeRecipientSet(address oldRecipient, address newRecipient);
    event OwnershipTransferred(address indexed oldOwner, address indexed newOwner);

    // ── Constructor ───────────────────────────────────────────────────

    constructor(
        address _v2Router,
        address _v3Router,
        address _weth,
        address _feeRecipient,
        uint16  _feeBps
    ) {
        require(_v2Router     != address(0), "v2Router zero");
        require(_v3Router     != address(0), "v3Router zero");
        require(_weth         != address(0), "weth zero");
        require(_feeRecipient != address(0), "feeRecipient zero");
        require(_feeBps <= MAX_FEE_BPS,      "feeBps > cap");
        v2Router     = _v2Router;
        v3Router     = _v3Router;
        weth         = _weth;
        feeRecipient = _feeRecipient;
        feeBps       = _feeBps;
        owner        = msg.sender;
    }

    // ── Owner setters ─────────────────────────────────────────────────

    function setFeeBps(uint16 _feeBps) external onlyOwner {
        require(_feeBps <= MAX_FEE_BPS, "feeBps > cap");
        emit FeeBpsSet(feeBps, _feeBps);
        feeBps = _feeBps;
    }

    function setFeeRecipient(address _feeRecipient) external onlyOwner {
        require(_feeRecipient != address(0), "feeRecipient zero");
        emit FeeRecipientSet(feeRecipient, _feeRecipient);
        feeRecipient = _feeRecipient;
    }

    function transferOwnership(address newOwner) external onlyOwner {
        require(newOwner != address(0), "newOwner zero");
        emit OwnershipTransferred(owner, newOwner);
        owner = newOwner;
    }

    // ── Internal helpers ──────────────────────────────────────────────

    /// @dev Deducts platform fee from `amount`, sends to feeRecipient.
    ///      Returns (fee, remainder). If feeBps == 0, fee == 0 and the call
    ///      to feeRecipient is skipped (no gas wasted).
    function _takeFeeEth(uint256 amount) internal returns (uint256 fee, uint256 rest) {
        fee  = (amount * feeBps) / 10000;
        rest = amount - fee;
        if (fee > 0) {
            (bool ok, ) = feeRecipient.call{value: fee}("");
            require(ok, "fee transfer failed");
        }
    }

    /// @dev Approves `spender` for `token` to spend up to uint256.max if the
    ///      current allowance is less than `minAmount`. Uses max-approve to
    ///      avoid repeated approval transactions.
    function _approveIfNeeded(address token, address spender, uint256 minAmount) internal {
        if (IERC20Minimal(token).allowance(address(this), spender) < minAmount) {
            require(IERC20Minimal(token).approve(spender, type(uint256).max), "approve failed");
        }
    }

    // ── V2 Buy: ETH → token ───────────────────────────────────────────

    /**
     * @notice Swap ETH for tokens via Uniswap V2. Platform fee is taken from
     *         the incoming ETH; the remainder is forwarded to the V2 router.
     *         The Uniswap amountOutMin is enforced on the post-fee ETH, so
     *         the frontend must quote on post-fee ETH (ethIn * (10000 - feeBps)
     *         / 10000) for the displayed output to match what is received.
     *
     * @param token        ERC-20 output token address.
     * @param amountOutMin Minimum tokens to receive (Uniswap-enforced on
     *                     post-fee ETH — set from the post-fee quote).
     * @param deadline     Uniswap deadline (unix seconds).
     */
    function v2Buy(
        address token,
        uint256 amountOutMin,
        uint256 deadline
    ) external payable nonReentrant {
        require(msg.value > 0, "no eth");
        (uint256 fee, uint256 ethForSwap) = _takeFeeEth(msg.value);

        address[] memory path = new address[](2);
        path[0] = weth;
        path[1] = token;

        IUniswapV2Router(v2Router).swapExactETHForTokens{value: ethForSwap}(
            amountOutMin, path, msg.sender, deadline
        );
        emit FeeRouted(msg.sender, true, fee);
    }

    /**
     * @notice Same as v2Buy but uses swapExactETHForTokensSupportingFeeOnTransferTokens.
     *         Use this for tokens with transfer taxes so the swap does not revert
     *         on the exact-amount check that the standard variant enforces.
     */
    function v2BuyFeeOnTransfer(
        address token,
        uint256 amountOutMin,
        uint256 deadline
    ) external payable nonReentrant {
        require(msg.value > 0, "no eth");
        (uint256 fee, uint256 ethForSwap) = _takeFeeEth(msg.value);

        address[] memory path = new address[](2);
        path[0] = weth;
        path[1] = token;

        IUniswapV2Router(v2Router).swapExactETHForTokensSupportingFeeOnTransferTokens{value: ethForSwap}(
            amountOutMin, path, msg.sender, deadline
        );
        emit FeeRouted(msg.sender, true, fee);
    }

    // ── V2 Sell: token → ETH ──────────────────────────────────────────

    /**
     * @notice Swap tokens for ETH via Uniswap V2. Full tokenIn is swapped;
     *         platform fee is deducted from the ETH output.
     *
     * @param token      ERC-20 input token (caller must have approved this contract).
     * @param amountIn   Token amount to sell.
     * @param minEthOut  Minimum ETH the CALLER receives AFTER the platform fee.
     *                   Set this from postFeeQuote * (10000 - slippageBps) / 10000.
     * @param deadline   Uniswap deadline (unix seconds).
     */
    function v2Sell(
        address token,
        uint256 amountIn,
        uint256 minEthOut,
        uint256 deadline
    ) external nonReentrant {
        require(amountIn > 0, "no tokens");
        require(
            IERC20Minimal(token).transferFrom(msg.sender, address(this), amountIn),
            "transferFrom failed"
        );
        _approveIfNeeded(token, v2Router, amountIn);

        address[] memory path = new address[](2);
        path[0] = token;
        path[1] = weth;

        uint256[] memory amounts = IUniswapV2Router(v2Router).swapExactTokensForETH(
            amountIn, 0, path, address(this), deadline
        );
        uint256 ethOut = amounts[amounts.length - 1];

        (uint256 fee, uint256 ethToUser) = _takeFeeEth(ethOut);
        require(ethToUser >= minEthOut, "slippage: eth out too low");

        (bool ok, ) = msg.sender.call{value: ethToUser}("");
        require(ok, "eth transfer failed");
        emit FeeRouted(msg.sender, false, fee);
    }

    /**
     * @notice Same as v2Sell but uses swapExactTokensForETHSupportingFeeOnTransferTokens.
     *         Use this for tokens with transfer taxes.
     */
    function v2SellFeeOnTransfer(
        address token,
        uint256 amountIn,
        uint256 minEthOut,
        uint256 deadline
    ) external nonReentrant {
        require(amountIn > 0, "no tokens");
        require(
            IERC20Minimal(token).transferFrom(msg.sender, address(this), amountIn),
            "transferFrom failed"
        );
        _approveIfNeeded(token, v2Router, amountIn);

        address[] memory path = new address[](2);
        path[0] = token;
        path[1] = weth;

        uint256 ethBefore = address(this).balance;
        IUniswapV2Router(v2Router).swapExactTokensForETHSupportingFeeOnTransferTokens(
            amountIn, 0, path, address(this), deadline
        );
        uint256 ethOut = address(this).balance - ethBefore;

        (uint256 fee, uint256 ethToUser) = _takeFeeEth(ethOut);
        require(ethToUser >= minEthOut, "slippage: eth out too low");

        (bool ok, ) = msg.sender.call{value: ethToUser}("");
        require(ok, "eth transfer failed");
        emit FeeRouted(msg.sender, false, fee);
    }

    // ── V3 Buy: ETH → token ───────────────────────────────────────────

    /**
     * @notice Swap ETH for tokens via Uniswap V3 (SwapRouter02 exactInputSingle).
     *         Platform fee taken from incoming ETH; remainder forwarded to V3 router
     *         which wraps it to WETH and swaps. Output tokens go directly to the caller.
     *
     * @param tokenOut     ERC-20 output token.
     * @param poolFee      V3 pool fee tier (e.g. 500, 3000, 10000).
     * @param amountOutMin Minimum tokens out (Uniswap-enforced on post-fee ETH).
     */
    function v3Buy(
        address tokenOut,
        uint24  poolFee,
        uint256 amountOutMin
    ) external payable nonReentrant {
        require(msg.value > 0, "no eth");
        (uint256 fee, uint256 ethForSwap) = _takeFeeEth(msg.value);

        IUniswapV3Router(v3Router).exactInputSingle{value: ethForSwap}(
            IUniswapV3Router.ExactInputSingleParams({
                tokenIn:           weth,
                tokenOut:          tokenOut,
                fee:               poolFee,
                recipient:         msg.sender,
                amountIn:          ethForSwap,
                amountOutMinimum:  amountOutMin,
                sqrtPriceLimitX96: 0
            })
        );
        emit FeeRouted(msg.sender, true, fee);
    }

    // ── V3 Sell: token → ETH ──────────────────────────────────────────

    /**
     * @notice Swap tokens for ETH via Uniswap V3. Internally does:
     *           exactInputSingle(token → WETH, recipient = this)
     *           + WETH.withdraw()
     *           + platform fee deduction
     *           + ETH sent to caller.
     *         This replaces the frontend multicall(exactInputSingle + unwrapWETH9)
     *         pattern with a single contract call.
     *
     * @param tokenIn    ERC-20 input token (caller must have approved this contract).
     * @param poolFee    V3 pool fee tier.
     * @param amountIn   Token amount to sell.
     * @param minEthOut  Minimum ETH the CALLER receives AFTER the platform fee.
     */
    function v3Sell(
        address tokenIn,
        uint24  poolFee,
        uint256 amountIn,
        uint256 minEthOut
    ) external nonReentrant {
        require(amountIn > 0, "no tokens");
        require(
            IERC20Minimal(tokenIn).transferFrom(msg.sender, address(this), amountIn),
            "transferFrom failed"
        );
        _approveIfNeeded(tokenIn, v3Router, amountIn);

        uint256 wethOut = IUniswapV3Router(v3Router).exactInputSingle(
            IUniswapV3Router.ExactInputSingleParams({
                tokenIn:           tokenIn,
                tokenOut:          weth,
                fee:               poolFee,
                recipient:         address(this),
                amountIn:          amountIn,
                amountOutMinimum:  0,
                sqrtPriceLimitX96: 0
            })
        );

        IWETH9(weth).withdraw(wethOut);

        (uint256 fee, uint256 ethToUser) = _takeFeeEth(wethOut);
        require(ethToUser >= minEthOut, "slippage: eth out too low");

        (bool ok, ) = msg.sender.call{value: ethToUser}("");
        require(ok, "eth transfer failed");
        emit FeeRouted(msg.sender, false, fee);
    }

    /// @dev Accepts ETH from WETH.withdraw() during v3Sell, and any other
    ///      incidental ETH sends. The contract is designed to hold zero ETH
    ///      between transactions — any stray balance is swept on the next sell.
    receive() external payable {}
}
Chain explorer1839msChain node78ms