FujinRobinhoodSwapRouter

0x5c14466d977bf8a5477eb61e9628a5fe4318a177

Verification
Verified
v0.8.28+commit.7893614a
Type
Contract
10,609 bytes
ABI entries
45
11 read · 11 write
License
none

Contract information

Address
0x5c14466d977bf8a5477eb61e9628a5fe4318a177
Chain
Robinhood Chain (4663)
Compiler
v0.8.28+commit.7893614a
Optimization
Enabled
Creation tx
0xe1ad3bf214…1f60455462

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (11)

BPS()uint256
MAX_FEE_BPS()uint256
accruedEthFees()uint256
feeBps()uint256
feeRecipient()address
owner()address
paused()bool
pendingOwner()address
swapRouter()address
v2Router()address
wNative()address

Events (8)

FeeBpsUpdatedFeeRecipientUpdatedFeesWithdrawnNormalBuyNormalSellOwnershipTransferStartedOwnershipTransferredPausedUpdated

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "swapRouter_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "v2Router_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "wNative_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "feeRecipient_",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "feeBps_",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "DeadlineExpired",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "EthTransferFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "FeeTooHigh",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidPath",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidRecipient",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NoFees",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotOwner",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "Paused",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "Reentrancy",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "SlippageExceeded",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TokenTransferFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddress",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAmount",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "oldFeeBps",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "newFeeBps",
        "type": "uint256"
      }
    ],
    "name": "FeeBpsUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "oldRecipient",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newRecipient",
        "type": "address"
      }
    ],
    "name": "FeeRecipientUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "FeesWithdrawn",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "buyer",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethIn",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "fujinFee",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokensOut",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "bytes32",
        "name": "quoteId",
        "type": "bytes32"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "route",
        "type": "string"
      }
    ],
    "name": "NormalBuy",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "seller",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenAmount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethOut",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "fujinFee",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "bytes32",
        "name": "quoteId",
        "type": "bytes32"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "route",
        "type": "string"
      }
    ],
    "name": "NormalSell",
    "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": "bool",
        "name": "paused",
        "type": "bool"
      }
    ],
    "name": "PausedUpdated",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_FEE_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "acceptOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "accruedEthFees",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address[]",
        "name": "path",
        "type": "address[]"
      },
      {
        "internalType": "uint256",
        "name": "minTokensOut",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      },
      {
        "internalType": "bytes32",
        "name": "quoteId",
        "type": "bytes32"
      }
    ],
    "name": "buyV2",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "tokensOut",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "fujinFee",
        "type": "uint256"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes",
        "name": "path",
        "type": "bytes"
      },
      {
        "internalType": "address",
        "name": "tokenOut",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "minTokensOut",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      },
      {
        "internalType": "bytes32",
        "name": "quoteId",
        "type": "bytes32"
      }
    ],
    "name": "buyV3Path",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "tokensOut",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "fujinFee",
        "type": "uint256"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint24",
        "name": "poolFee",
        "type": "uint24"
      },
      {
        "internalType": "uint256",
        "name": "minTokensOut",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      },
      {
        "internalType": "bytes32",
        "name": "quoteId",
        "type": "bytes32"
      }
    ],
    "name": "buyV3Single",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "tokensOut",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "fujinFee",
        "type": "uint256"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "feeBps",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "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": [],
    "name": "paused",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pendingOwner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokenAmount",
        "type": "uint256"
      },
      {
        "internalType": "address[]",
        "name": "path",
        "type": "address[]"
      },
      {
        "internalType": "uint256",
        "name": "minSwapEthOut",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "minRecipientEthOut",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      },
      {
        "internalType": "bytes32",
        "name": "quoteId",
        "type": "bytes32"
      }
    ],
    "name": "sellV2",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "recipientEthOut",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "fujinFee",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokenAmount",
        "type": "uint256"
      },
      {
        "internalType": "bytes",
        "name": "path",
        "type": "bytes"
      },
      {
        "internalType": "uint256",
        "name": "minSwapEthOut",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "minRecipientEthOut",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      },
      {
        "internalType": "bytes32",
        "name": "quoteId",
        "type": "bytes32"
      }
    ],
    "name": "sellV3Path",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "recipientEthOut",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "fujinFee",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokenAmount",
        "type": "uint256"
      },
      {
        "internalType": "uint24",
        "name": "poolFee",
        "type": "uint24"
      },
      {
        "internalType": "uint256",
        "name": "minSwapEthOut",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "minRecipientEthOut",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      },
      {
        "internalType": "bytes32",
        "name": "quoteId",
        "type": "bytes32"
      }
    ],
    "name": "sellV3Single",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "recipientEthOut",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "fujinFee",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newFeeRecipient",
        "type": "address"
      }
    ],
    "name": "setFeeRecipient",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bool",
        "name": "newPaused",
        "type": "bool"
      }
    ],
    "name": "setPaused",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "swapRouter",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "v2Router",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "wNative",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "withdrawFees",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "stateMutability": "payable",
    "type": "receive"
  }
]

Source code

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

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

interface IHoodLaunchpad {
    function buy(address token, uint256 minTokensOut) external payable;
    function sell(address token, uint256 tokenAmount, uint256 minEthOut) external;
    function isHoodToken(address token) external view returns (bool);
    function quoteBuy(address token, uint256 ethIn)
        external
        view
        returns (uint256 tokensOut, uint256 fee, uint256 ethUsed);
    function quoteSell(address token, uint256 tokenAmount) external view returns (uint256 ethOut, uint256 fee);
}

interface IWETH is IERC20 {
    function withdraw(uint256 amount) external;
}

interface IUniV2Router {
    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 IUniV3SwapRouter {
    struct ExactInputSingleParams {
        address tokenIn;
        address tokenOut;
        uint24 fee;
        address recipient;
        uint256 amountIn;
        uint256 amountOutMinimum;
        uint160 sqrtPriceLimitX96;
    }

    struct ExactInputParams {
        bytes path;
        address recipient;
        uint256 amountIn;
        uint256 amountOutMinimum;
    }

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

    function exactInput(ExactInputParams calldata params)
        external
        payable
        returns (uint256 amountOut);
}

/// @notice Minimal non-custodial router for hood.fun curve trades through Fujin.
/// @dev HoodLaunchpad.buy() sends bought tokens to msg.sender, so this router
///      receives buy output first, then forwards it to the user atomically.
///      Consequence: Hood's Trade event and guard accounting see this router as
///      the direct trader. Keep that in mind for early-block/anti-snipe limits.
contract FujinHoodRouter {
    uint256 public constant BPS = 10_000;
    uint256 public constant MAX_FEE_BPS = 1_000; // 10%

    address public immutable hoodLaunchpad;
    address public owner;
    address public pendingOwner;
    address public feeRecipient;
    uint256 public immutable feeBps;
    uint256 public accruedEthFees;
    bool public paused;

    uint256 private locked = 1;

    event HoodBuy(
        address indexed token,
        address indexed buyer,
        address indexed recipient,
        uint256 ethIn,
        uint256 fujinFee,
        uint256 refund,
        uint256 tokensOut,
        bytes32 quoteId
    );
    event HoodSell(
        address indexed token,
        address indexed seller,
        address indexed recipient,
        uint256 tokenAmount,
        uint256 ethOut,
        uint256 fujinFee,
        bytes32 quoteId
    );
    event FeeBpsUpdated(uint256 oldFeeBps, uint256 newFeeBps);
    event FeeRecipientUpdated(address indexed oldRecipient, address indexed newRecipient);
    event FeesWithdrawn(address indexed to, uint256 amount);
    event PausedUpdated(bool paused);
    event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner);
    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    error DeadlineExpired();
    error EthTransferFailed();
    error FeeTooHigh();
    error InvalidRecipient();
    error InvalidToken();
    error NoFees();
    error NotOwner();
    error Paused();
    error Reentrancy();
    error SlippageExceeded();
    error TokenTransferFailed();
    error ZeroAddress();
    error ZeroAmount();

    modifier onlyOwner() {
        if (msg.sender != owner) revert NotOwner();
        _;
    }

    modifier nonReentrant() {
        if (locked != 1) revert Reentrancy();
        locked = 2;
        _;
        locked = 1;
    }

    modifier whenNotPaused() {
        if (paused) revert Paused();
        _;
    }

    constructor(address hoodLaunchpad_, address feeRecipient_, uint256 feeBps_) {
        if (hoodLaunchpad_ == address(0) || feeRecipient_ == address(0)) revert ZeroAddress();
        if (feeBps_ > MAX_FEE_BPS) revert FeeTooHigh();
        hoodLaunchpad = hoodLaunchpad_;
        owner = msg.sender;
        feeRecipient = feeRecipient_;
        feeBps = feeBps_;
        emit OwnershipTransferred(address(0), msg.sender);
        emit FeeRecipientUpdated(address(0), feeRecipient_);
        emit FeeBpsUpdated(0, feeBps_);
    }

    receive() external payable {
        if (msg.sender != hoodLaunchpad) revert InvalidRecipient();
    }

    function quoteBuyAfterFujinFee(address token, uint256 ethIn)
        external
        view
        returns (uint256 tokensOut, uint256 hoodFee, uint256 ethUsed, uint256 fujinFee, uint256 ethToCurve)
    {
        if (ethIn == 0) return (0, 0, 0, 0, 0);
        fujinFee = (ethIn * feeBps) / BPS;
        ethToCurve = ethIn - fujinFee;
        (tokensOut, hoodFee, ethUsed) = IHoodLaunchpad(hoodLaunchpad).quoteBuy(token, ethToCurve);
    }

    function quoteSellAfterFujinFee(address token, uint256 tokenAmount)
        external
        view
        returns (uint256 recipientEthOut, uint256 hoodFee, uint256 fujinFee, uint256 launchpadEthOut)
    {
        if (tokenAmount == 0) return (0, 0, 0, 0);
        (launchpadEthOut, hoodFee) = IHoodLaunchpad(hoodLaunchpad).quoteSell(token, tokenAmount);
        fujinFee = (launchpadEthOut * feeBps) / BPS;
        recipientEthOut = launchpadEthOut - fujinFee;
    }

    function buyHoodCurve(
        address token,
        uint256 minTokensOut,
        address recipient,
        uint256 deadline,
        bytes32 quoteId
    ) external payable nonReentrant whenNotPaused returns (uint256 tokensOut, uint256 refund) {
        if (block.timestamp > deadline) revert DeadlineExpired();
        if (msg.value == 0) revert ZeroAmount();
        if (recipient == address(0)) revert ZeroAddress();
        if (!IHoodLaunchpad(hoodLaunchpad).isHoodToken(token)) revert InvalidToken();

        uint256 balanceBefore = address(this).balance - msg.value;
        uint256 tokenBefore = IERC20(token).balanceOf(address(this));
        uint256 fujinFee = (msg.value * feeBps) / BPS;
        uint256 ethToCurve = msg.value - fujinFee;
        if (ethToCurve == 0) revert ZeroAmount();

        IHoodLaunchpad(hoodLaunchpad).buy{value: ethToCurve}(token, minTokensOut);

        tokensOut = IERC20(token).balanceOf(address(this)) - tokenBefore;
        if (tokensOut < minTokensOut || tokensOut == 0) revert SlippageExceeded();

        uint256 expectedRouterBalance = balanceBefore + fujinFee;
        if (address(this).balance > expectedRouterBalance) {
            refund = address(this).balance - expectedRouterBalance;
        }
        accruedEthFees += fujinFee;

        _safeTransfer(token, recipient, tokensOut);
        if (refund != 0) _sendEth(recipient, refund);

        emit HoodBuy(token, msg.sender, recipient, msg.value, fujinFee, refund, tokensOut, quoteId);
    }

    function sellHoodCurve(
        address token,
        uint256 tokenAmount,
        uint256 minLaunchpadEthOut,
        uint256 minRecipientEthOut,
        address recipient,
        uint256 deadline,
        bytes32 quoteId
    ) external nonReentrant whenNotPaused returns (uint256 recipientEthOut, uint256 fujinFee) {
        if (block.timestamp > deadline) revert DeadlineExpired();
        if (tokenAmount == 0) revert ZeroAmount();
        if (recipient == address(0)) revert ZeroAddress();
        if (!IHoodLaunchpad(hoodLaunchpad).isHoodToken(token)) revert InvalidToken();

        uint256 balanceBefore = address(this).balance;
        _safeTransferFrom(token, msg.sender, address(this), tokenAmount);
        _forceApprove(token, hoodLaunchpad, tokenAmount);

        IHoodLaunchpad(hoodLaunchpad).sell(token, tokenAmount, minLaunchpadEthOut);

        uint256 launchpadEthOut = address(this).balance - balanceBefore;
        fujinFee = (launchpadEthOut * feeBps) / BPS;
        recipientEthOut = launchpadEthOut - fujinFee;
        if (recipientEthOut < minRecipientEthOut || recipientEthOut == 0) revert SlippageExceeded();

        accruedEthFees += fujinFee;
        _sendEth(recipient, recipientEthOut);

        emit HoodSell(token, msg.sender, recipient, tokenAmount, launchpadEthOut, fujinFee, quoteId);
    }

    function withdrawFees(uint256 amount) external onlyOwner nonReentrant {
        uint256 toWithdraw = amount == 0 ? accruedEthFees : amount;
        if (toWithdraw == 0 || toWithdraw > accruedEthFees) revert NoFees();
        accruedEthFees -= toWithdraw;
        _sendEth(feeRecipient, toWithdraw);
        emit FeesWithdrawn(feeRecipient, toWithdraw);
    }

    function setFeeRecipient(address newFeeRecipient) external onlyOwner {
        if (newFeeRecipient == address(0)) revert ZeroAddress();
        emit FeeRecipientUpdated(feeRecipient, newFeeRecipient);
        feeRecipient = newFeeRecipient;
    }

    function setPaused(bool newPaused) external onlyOwner {
        paused = newPaused;
        emit PausedUpdated(newPaused);
    }

    function transferOwnership(address newOwner) external onlyOwner {
        if (newOwner == address(0)) revert ZeroAddress();
        pendingOwner = newOwner;
        emit OwnershipTransferStarted(owner, newOwner);
    }

    function acceptOwnership() external {
        if (msg.sender != pendingOwner) revert NotOwner();
        address oldOwner = owner;
        owner = msg.sender;
        pendingOwner = address(0);
        emit OwnershipTransferred(oldOwner, msg.sender);
    }

    function _sendEth(address to, uint256 amount) internal {
        (bool ok,) = to.call{value: amount}("");
        if (!ok) revert EthTransferFailed();
    }

    function _safeTransfer(address token, address to, uint256 amount) internal {
        (bool ok, bytes memory data) = token.call(abi.encodeCall(IERC20.transfer, (to, amount)));
        if (!ok || (data.length != 0 && !abi.decode(data, (bool)))) revert TokenTransferFailed();
    }

    function _safeTransferFrom(address token, address from, address to, uint256 amount) internal {
        (bool ok, bytes memory data) = token.call(abi.encodeCall(IERC20.transferFrom, (from, to, amount)));
        if (!ok || (data.length != 0 && !abi.decode(data, (bool)))) revert TokenTransferFailed();
    }

    function _forceApprove(address token, address spender, uint256 amount) internal {
        if (IERC20(token).allowance(address(this), spender) >= amount) return;
        _callApprove(token, spender, 0);
        _callApprove(token, spender, amount);
    }

    function _callApprove(address token, address spender, uint256 amount) internal {
        (bool ok, bytes memory data) = token.call(abi.encodeCall(IERC20.approve, (spender, amount)));
        if (!ok || (data.length != 0 && !abi.decode(data, (bool)))) revert TokenTransferFailed();
    }
}

/// @notice Non-custodial Fujin router for normal/graduated Robinhood token swaps.
/// @dev Deliberately narrow: only configured Robinhood V2/V3 routers are callable.
contract FujinRobinhoodSwapRouter {
    uint256 public constant BPS = 10_000;
    uint256 public constant MAX_FEE_BPS = 1_000; // 10%

    address public immutable swapRouter;
    address public immutable v2Router;
    address public immutable wNative;
    address public owner;
    address public pendingOwner;
    address public feeRecipient;
    uint256 public immutable feeBps;
    uint256 public accruedEthFees;
    bool public paused;

    uint256 private locked = 1;

    event NormalBuy(
        address indexed token,
        address indexed buyer,
        address indexed recipient,
        uint256 ethIn,
        uint256 fujinFee,
        uint256 tokensOut,
        bytes32 quoteId,
        string route
    );
    event NormalSell(
        address indexed token,
        address indexed seller,
        address indexed recipient,
        uint256 tokenAmount,
        uint256 ethOut,
        uint256 fujinFee,
        bytes32 quoteId,
        string route
    );
    event FeeBpsUpdated(uint256 oldFeeBps, uint256 newFeeBps);
    event FeeRecipientUpdated(address indexed oldRecipient, address indexed newRecipient);
    event FeesWithdrawn(address indexed to, uint256 amount);
    event PausedUpdated(bool paused);
    event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner);
    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    error DeadlineExpired();
    error EthTransferFailed();
    error FeeTooHigh();
    error InvalidPath();
    error InvalidRecipient();
    error NoFees();
    error NotOwner();
    error Paused();
    error Reentrancy();
    error SlippageExceeded();
    error TokenTransferFailed();
    error ZeroAddress();
    error ZeroAmount();

    modifier onlyOwner() {
        if (msg.sender != owner) revert NotOwner();
        _;
    }

    modifier nonReentrant() {
        if (locked != 1) revert Reentrancy();
        locked = 2;
        _;
        locked = 1;
    }

    modifier whenNotPaused() {
        if (paused) revert Paused();
        _;
    }

    constructor(
        address swapRouter_,
        address v2Router_,
        address wNative_,
        address feeRecipient_,
        uint256 feeBps_
    ) {
        if (swapRouter_ == address(0) || v2Router_ == address(0) || wNative_ == address(0) || feeRecipient_ == address(0)) {
            revert ZeroAddress();
        }
        if (feeBps_ > MAX_FEE_BPS) revert FeeTooHigh();
        swapRouter = swapRouter_;
        v2Router = v2Router_;
        wNative = wNative_;
        feeRecipient = feeRecipient_;
        feeBps = feeBps_;
        owner = msg.sender;
        emit OwnershipTransferred(address(0), msg.sender);
        emit FeeRecipientUpdated(address(0), feeRecipient_);
        emit FeeBpsUpdated(0, feeBps_);
    }

    receive() external payable {
        if (msg.sender != wNative && msg.sender != v2Router && msg.sender != swapRouter) {
            revert InvalidRecipient();
        }
    }

    function buyV3Single(
        address token,
        uint24 poolFee,
        uint256 minTokensOut,
        address recipient,
        uint256 deadline,
        bytes32 quoteId
    ) external payable nonReentrant whenNotPaused returns (uint256 tokensOut, uint256 fujinFee) {
        _checkTrade(recipient, deadline);
        if (token == address(0) || token == wNative) revert InvalidPath();

        uint256 tokenBefore = IERC20(token).balanceOf(recipient);
        uint256 ethToSwap = _takeBuyFee(msg.value);
        fujinFee = msg.value - ethToSwap;

        IUniV3SwapRouter(swapRouter).exactInputSingle{value: ethToSwap}(
            IUniV3SwapRouter.ExactInputSingleParams({
                tokenIn: wNative,
                tokenOut: token,
                fee: poolFee,
                recipient: recipient,
                amountIn: ethToSwap,
                amountOutMinimum: minTokensOut,
                sqrtPriceLimitX96: 0
            })
        );

        tokensOut = IERC20(token).balanceOf(recipient) - tokenBefore;
        if (tokensOut < minTokensOut || tokensOut == 0) revert SlippageExceeded();
        emit NormalBuy(token, msg.sender, recipient, msg.value, fujinFee, tokensOut, quoteId, "v3");
    }

    function buyV3Path(
        bytes calldata path,
        address tokenOut,
        uint256 minTokensOut,
        address recipient,
        uint256 deadline,
        bytes32 quoteId
    ) external payable nonReentrant whenNotPaused returns (uint256 tokensOut, uint256 fujinFee) {
        _checkTrade(recipient, deadline);
        _requireV3Path(path, wNative, tokenOut);
        if (tokenOut == address(0) || tokenOut == wNative) revert InvalidPath();

        uint256 tokenBefore = IERC20(tokenOut).balanceOf(recipient);
        uint256 ethToSwap = _takeBuyFee(msg.value);
        fujinFee = msg.value - ethToSwap;

        IUniV3SwapRouter(swapRouter).exactInput{value: ethToSwap}(
            IUniV3SwapRouter.ExactInputParams({
                path: path,
                recipient: recipient,
                amountIn: ethToSwap,
                amountOutMinimum: minTokensOut
            })
        );

        tokensOut = IERC20(tokenOut).balanceOf(recipient) - tokenBefore;
        if (tokensOut < minTokensOut || tokensOut == 0) revert SlippageExceeded();
        emit NormalBuy(tokenOut, msg.sender, recipient, msg.value, fujinFee, tokensOut, quoteId, "v3-path");
    }

    function buyV2(
        address[] calldata path,
        uint256 minTokensOut,
        address recipient,
        uint256 deadline,
        bytes32 quoteId
    ) external payable nonReentrant whenNotPaused returns (uint256 tokensOut, uint256 fujinFee) {
        _checkTrade(recipient, deadline);
        if (path.length < 2) revert InvalidPath();
        address tokenOut = path[path.length - 1];
        _requireV2Path(path, wNative, tokenOut);
        if (tokenOut == wNative) revert InvalidPath();

        uint256 tokenBefore = IERC20(tokenOut).balanceOf(recipient);
        uint256 ethToSwap = _takeBuyFee(msg.value);
        fujinFee = msg.value - ethToSwap;

        IUniV2Router(v2Router).swapExactETHForTokensSupportingFeeOnTransferTokens{value: ethToSwap}(
            minTokensOut,
            path,
            recipient,
            deadline
        );

        tokensOut = IERC20(tokenOut).balanceOf(recipient) - tokenBefore;
        if (tokensOut < minTokensOut || tokensOut == 0) revert SlippageExceeded();
        emit NormalBuy(tokenOut, msg.sender, recipient, msg.value, fujinFee, tokensOut, quoteId, "v2");
    }

    function sellV3Single(
        address token,
        uint256 tokenAmount,
        uint24 poolFee,
        uint256 minSwapEthOut,
        uint256 minRecipientEthOut,
        address recipient,
        uint256 deadline,
        bytes32 quoteId
    ) external nonReentrant whenNotPaused returns (uint256 recipientEthOut, uint256 fujinFee) {
        _checkTrade(recipient, deadline);
        if (token == address(0) || token == wNative || tokenAmount == 0) revert InvalidPath();

        _safeTransferFrom(token, msg.sender, address(this), tokenAmount);
        _forceApprove(token, swapRouter, tokenAmount);
        uint256 wethBefore = IERC20(wNative).balanceOf(address(this));

        IUniV3SwapRouter(swapRouter).exactInputSingle(
            IUniV3SwapRouter.ExactInputSingleParams({
                tokenIn: token,
                tokenOut: wNative,
                fee: poolFee,
                recipient: address(this),
                amountIn: tokenAmount,
                amountOutMinimum: minSwapEthOut,
                sqrtPriceLimitX96: 0
            })
        );

        uint256 wethOut = IERC20(wNative).balanceOf(address(this)) - wethBefore;
        (recipientEthOut, fujinFee) = _unwrapAndPay(recipient, wethOut, minRecipientEthOut);
        emit NormalSell(token, msg.sender, recipient, tokenAmount, wethOut, fujinFee, quoteId, "v3");
    }

    function sellV3Path(
        address token,
        uint256 tokenAmount,
        bytes calldata path,
        uint256 minSwapEthOut,
        uint256 minRecipientEthOut,
        address recipient,
        uint256 deadline,
        bytes32 quoteId
    ) external nonReentrant whenNotPaused returns (uint256 recipientEthOut, uint256 fujinFee) {
        _checkTrade(recipient, deadline);
        if (token == address(0) || token == wNative || tokenAmount == 0) revert InvalidPath();
        _requireV3Path(path, token, wNative);

        _safeTransferFrom(token, msg.sender, address(this), tokenAmount);
        _forceApprove(token, swapRouter, tokenAmount);
        uint256 wethBefore = IERC20(wNative).balanceOf(address(this));

        IUniV3SwapRouter(swapRouter).exactInput(
            IUniV3SwapRouter.ExactInputParams({
                path: path,
                recipient: address(this),
                amountIn: tokenAmount,
                amountOutMinimum: minSwapEthOut
            })
        );

        uint256 wethOut = IERC20(wNative).balanceOf(address(this)) - wethBefore;
        (recipientEthOut, fujinFee) = _unwrapAndPay(recipient, wethOut, minRecipientEthOut);
        emit NormalSell(token, msg.sender, recipient, tokenAmount, wethOut, fujinFee, quoteId, "v3-path");
    }

    function sellV2(
        address token,
        uint256 tokenAmount,
        address[] calldata path,
        uint256 minSwapEthOut,
        uint256 minRecipientEthOut,
        address recipient,
        uint256 deadline,
        bytes32 quoteId
    ) external nonReentrant whenNotPaused returns (uint256 recipientEthOut, uint256 fujinFee) {
        _checkTrade(recipient, deadline);
        if (token == address(0) || token == wNative || tokenAmount == 0) revert InvalidPath();
        _requireV2Path(path, token, wNative);

        uint256 ethBefore = address(this).balance;
        _safeTransferFrom(token, msg.sender, address(this), tokenAmount);
        _forceApprove(token, v2Router, tokenAmount);

        IUniV2Router(v2Router).swapExactTokensForETHSupportingFeeOnTransferTokens(
            tokenAmount,
            minSwapEthOut,
            path,
            address(this),
            deadline
        );

        uint256 ethOut = address(this).balance - ethBefore;
        (recipientEthOut, fujinFee) = _payEthAfterFee(recipient, ethOut, minRecipientEthOut);
        emit NormalSell(token, msg.sender, recipient, tokenAmount, ethOut, fujinFee, quoteId, "v2");
    }

    function withdrawFees(uint256 amount) external onlyOwner nonReentrant {
        uint256 toWithdraw = amount == 0 ? accruedEthFees : amount;
        if (toWithdraw == 0 || toWithdraw > accruedEthFees) revert NoFees();
        accruedEthFees -= toWithdraw;
        _sendEth(feeRecipient, toWithdraw);
        emit FeesWithdrawn(feeRecipient, toWithdraw);
    }

    function setFeeRecipient(address newFeeRecipient) external onlyOwner {
        if (newFeeRecipient == address(0)) revert ZeroAddress();
        emit FeeRecipientUpdated(feeRecipient, newFeeRecipient);
        feeRecipient = newFeeRecipient;
    }

    function setPaused(bool newPaused) external onlyOwner {
        paused = newPaused;
        emit PausedUpdated(newPaused);
    }

    function transferOwnership(address newOwner) external onlyOwner {
        if (newOwner == address(0)) revert ZeroAddress();
        pendingOwner = newOwner;
        emit OwnershipTransferStarted(owner, newOwner);
    }

    function acceptOwnership() external {
        if (msg.sender != pendingOwner) revert NotOwner();
        address oldOwner = owner;
        owner = msg.sender;
        pendingOwner = address(0);
        emit OwnershipTransferred(oldOwner, msg.sender);
    }

    function _checkTrade(address recipient, uint256 deadline) internal view {
        if (block.timestamp > deadline) revert DeadlineExpired();
        if (recipient == address(0)) revert ZeroAddress();
    }

    function _takeBuyFee(uint256 ethIn) internal returns (uint256 ethToSwap) {
        if (ethIn == 0) revert ZeroAmount();
        uint256 fujinFee = (ethIn * feeBps) / BPS;
        ethToSwap = ethIn - fujinFee;
        if (ethToSwap == 0) revert ZeroAmount();
        accruedEthFees += fujinFee;
    }

    function _unwrapAndPay(address recipient, uint256 wethOut, uint256 minRecipientEthOut)
        internal
        returns (uint256 recipientEthOut, uint256 fujinFee)
    {
        if (wethOut == 0) revert SlippageExceeded();
        uint256 ethBefore = address(this).balance;
        IWETH(wNative).withdraw(wethOut);
        uint256 ethOut = address(this).balance - ethBefore;
        return _payEthAfterFee(recipient, ethOut, minRecipientEthOut);
    }

    function _payEthAfterFee(address recipient, uint256 ethOut, uint256 minRecipientEthOut)
        internal
        returns (uint256 recipientEthOut, uint256 fujinFee)
    {
        if (ethOut == 0) revert SlippageExceeded();
        fujinFee = (ethOut * feeBps) / BPS;
        recipientEthOut = ethOut - fujinFee;
        if (recipientEthOut < minRecipientEthOut || recipientEthOut == 0) revert SlippageExceeded();
        accruedEthFees += fujinFee;
        _sendEth(recipient, recipientEthOut);
    }

    function _requireV2Path(address[] calldata path, address expectedIn, address expectedOut) internal pure {
        if (path.length < 2) revert InvalidPath();
        if (path[0] != expectedIn || path[path.length - 1] != expectedOut) revert InvalidPath();
    }

    function _requireV3Path(bytes calldata path, address expectedIn, address expectedOut) internal pure {
        if (path.length < 43 || (path.length - 20) % 23 != 0) revert InvalidPath();
        if (_pathAddress(path, 0) != expectedIn) revert InvalidPath();
        if (_pathAddress(path, path.length - 20) != expectedOut) revert InvalidPath();
    }

    function _pathAddress(bytes calldata path, uint256 offset) internal pure returns (address token) {
        assembly {
            token := shr(96, calldataload(add(path.offset, offset)))
        }
    }

    function _sendEth(address to, uint256 amount) internal {
        (bool ok,) = to.call{value: amount}("");
        if (!ok) revert EthTransferFailed();
    }

    function _safeTransferFrom(address token, address from, address to, uint256 amount) internal {
        (bool ok, bytes memory data) = token.call(abi.encodeCall(IERC20.transferFrom, (from, to, amount)));
        if (!ok || (data.length != 0 && !abi.decode(data, (bool)))) revert TokenTransferFailed();
    }

    function _forceApprove(address token, address spender, uint256 amount) internal {
        if (IERC20(token).allowance(address(this), spender) >= amount) return;
        _callApprove(token, spender, 0);
        _callApprove(token, spender, amount);
    }

    function _callApprove(address token, address spender, uint256 amount) internal {
        (bool ok, bytes memory data) = token.call(abi.encodeCall(IERC20.approve, (spender, amount)));
        if (!ok || (data.length != 0 && !abi.decode(data, (bool)))) revert TokenTransferFailed();
    }
}
Chain explorer3013msChain node90ms