SwapFeeRouter

0x8ed5b76780a1bbc0598aea0875edd47ccde7b42c

Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
6,454 bytes
ABI entries
30
8 read · 8 write
License
none

Contract information

Address
0x8ed5b76780a1bbc0598aea0875edd47ccde7b42c
Chain
Robinhood Chain (4663)
Compiler
v0.8.24+commit.e11b9ed9
Optimization
Enabled
Creation tx
0x9f29f0c958…55dc48626a

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (8)

BPS_DENOM()uint256
FEE_BPS()uint256
V2_ROUTER()address
V3_ROUTER()address
WETH()address
feeRecipient()address
owner()address
quoteSwapAmount(uint256)uint256, uint256

Events (3)

FeeRecipientUpdatedFeeTakenOwnershipTransferred

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "weth_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "v2Router_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "v3Router_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "feeRecipient_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "owner_",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "BadPath",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "EthTransferFailed",
    "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": [],
    "name": "RouterCallFailed",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "SafeERC20FailedOperation",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddress",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAmount",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previous",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "next",
        "type": "address"
      }
    ],
    "name": "FeeRecipientUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "FeeTaken",
    "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"
  },
  {
    "inputs": [],
    "name": "BPS_DENOM",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "FEE_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "V2_ROUTER",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "V3_ROUTER",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "WETH",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "amountOutMin",
        "type": "uint256"
      },
      {
        "internalType": "address[]",
        "name": "path",
        "type": "address[]"
      },
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      }
    ],
    "name": "buyV2",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "tokenOut",
        "type": "address"
      },
      {
        "internalType": "uint24",
        "name": "poolFee",
        "type": "uint24"
      },
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amountOutMinimum",
        "type": "uint256"
      },
      {
        "internalType": "uint160",
        "name": "sqrtPriceLimitX96",
        "type": "uint160"
      }
    ],
    "name": "buyV3",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "target",
        "type": "address"
      },
      {
        "internalType": "bytes",
        "name": "data",
        "type": "bytes"
      }
    ],
    "name": "buyViaRouter",
    "outputs": [],
    "stateMutability": "payable",
    "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": "uint256",
        "name": "amountIn",
        "type": "uint256"
      }
    ],
    "name": "quoteSwapAmount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "fee",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "swapAmount",
        "type": "uint256"
      }
    ],
    "stateMutability": "pure",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "amountIn",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "amountOutMin",
        "type": "uint256"
      },
      {
        "internalType": "address[]",
        "name": "path",
        "type": "address[]"
      },
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      }
    ],
    "name": "sellV2",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "tokenIn",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amountIn",
        "type": "uint256"
      },
      {
        "internalType": "uint24",
        "name": "poolFee",
        "type": "uint24"
      },
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amountOutMinimum",
        "type": "uint256"
      },
      {
        "internalType": "uint160",
        "name": "sqrtPriceLimitX96",
        "type": "uint160"
      }
    ],
    "name": "sellV3",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "next",
        "type": "address"
      }
    ],
    "name": "setFeeRecipient",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "stateMutability": "payable",
    "type": "receive"
  }
]

Source code

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

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

interface IUniswapV2Router02 {
    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 IUniswapV3SwapRouter {
    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);

    function multicall(bytes[] calldata data) external payable returns (bytes[] memory results);

    function unwrapWETH9(uint256 amountMinimum, address recipient) external payable;
}

/**
 * @title SwapFeeRouter
 * @notice Single-tx wrapper around Uniswap V2/V3 (and arbitrary buy routers) that
 *         skims a fixed platform fee before swapping. Used for tokens NOT launched
 *         on Robinlaunch so external volume on robinlaunch.fun still pays protocol.
 *
 * Fee: 0.5% (50 bps) of the input notional, paid in the same transaction:
 *   • Buys  (ETH → token): fee in ETH to `feeRecipient`, remainder swapped
 *   • Sells (token → ETH): fee in the input token to `feeRecipient`, remainder swapped
 *
 * Security:
 *   • No custody of user funds beyond the atomic swap
 *   • ReentrancyGuard on all entrypoints
 *   • Owner can only update `feeRecipient` (not the fee bps)
 */
contract SwapFeeRouter is Ownable, ReentrancyGuard {
    using SafeERC20 for IERC20;

    uint256 public constant FEE_BPS = 50; // 0.5 %
    uint256 public constant BPS_DENOM = 10_000;

    address public immutable WETH;
    address public immutable V2_ROUTER;
    address public immutable V3_ROUTER;

    address public feeRecipient;

    event FeeTaken(address indexed token, address indexed recipient, uint256 amount);
    event FeeRecipientUpdated(address indexed previous, address indexed next);

    error ZeroAddress();
    error ZeroAmount();
    error BadPath();
    error EthTransferFailed();
    error RouterCallFailed();

    constructor(
        address weth_,
        address v2Router_,
        address v3Router_,
        address feeRecipient_,
        address owner_
    ) Ownable(owner_) {
        if (
            weth_ == address(0) || v2Router_ == address(0) || v3Router_ == address(0)
                || feeRecipient_ == address(0) || owner_ == address(0)
        ) {
            revert ZeroAddress();
        }
        WETH = weth_;
        V2_ROUTER = v2Router_;
        V3_ROUTER = v3Router_;
        feeRecipient = feeRecipient_;
    }

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

    /// @notice Split an input amount into (fee, swapAmount).
    function quoteSwapAmount(uint256 amountIn)
        public
        pure
        returns (uint256 fee, uint256 swapAmount)
    {
        fee = (amountIn * FEE_BPS) / BPS_DENOM;
        swapAmount = amountIn - fee;
    }

    // ─── Buys ────────────────────────────────────────────────────────────────

    function buyV2(
        uint256 amountOutMin,
        address[] calldata path,
        address to,
        uint256 deadline
    ) external payable nonReentrant {
        if (msg.value == 0) revert ZeroAmount();
        if (to == address(0)) revert ZeroAddress();
        if (path.length < 2 || path[0] != WETH) revert BadPath();

        (uint256 fee, uint256 swapAmount) = quoteSwapAmount(msg.value);
        _payEthFee(fee);

        IUniswapV2Router02(V2_ROUTER).swapExactETHForTokensSupportingFeeOnTransferTokens{
            value: swapAmount
        }(amountOutMin, path, to, deadline);
    }

    function buyV3(
        address tokenOut,
        uint24 poolFee,
        address to,
        uint256 amountOutMinimum,
        uint160 sqrtPriceLimitX96
    ) external payable nonReentrant {
        if (msg.value == 0) revert ZeroAmount();
        if (tokenOut == address(0) || to == address(0)) revert ZeroAddress();

        (uint256 fee, uint256 swapAmount) = quoteSwapAmount(msg.value);
        _payEthFee(fee);

        IUniswapV3SwapRouter(V3_ROUTER).exactInputSingle{value: swapAmount}(
            IUniswapV3SwapRouter.ExactInputSingleParams({
                tokenIn: WETH,
                tokenOut: tokenOut,
                fee: poolFee,
                recipient: to,
                amountIn: swapAmount,
                amountOutMinimum: amountOutMinimum,
                sqrtPriceLimitX96: sqrtPriceLimitX96
            })
        );
    }

    /**
     * @notice ETH buy via an arbitrary router (e.g. Uniswap Trading API calldata).
     *         Caller must quote the router for `swapAmount = msg.value - fee`.
     */
    function buyViaRouter(address target, bytes calldata data) external payable nonReentrant {
        if (msg.value == 0) revert ZeroAmount();
        if (target == address(0)) revert ZeroAddress();

        (uint256 fee, uint256 swapAmount) = quoteSwapAmount(msg.value);
        _payEthFee(fee);

        (bool ok, bytes memory ret) = target.call{value: swapAmount}(data);
        if (!ok) {
            if (ret.length > 0) {
                assembly {
                    revert(add(ret, 0x20), mload(ret))
                }
            }
            revert RouterCallFailed();
        }
    }

    // ─── Sells ───────────────────────────────────────────────────────────────

    function sellV2(
        uint256 amountIn,
        uint256 amountOutMin,
        address[] calldata path,
        address to,
        uint256 deadline
    ) external nonReentrant {
        if (amountIn == 0) revert ZeroAmount();
        if (to == address(0)) revert ZeroAddress();
        if (path.length < 2 || path[path.length - 1] != WETH) revert BadPath();

        address tokenIn = path[0];
        IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn);

        (uint256 fee, uint256 swapAmount) = quoteSwapAmount(amountIn);
        if (fee > 0) {
            IERC20(tokenIn).safeTransfer(feeRecipient, fee);
            emit FeeTaken(tokenIn, feeRecipient, fee);
        }

        IERC20(tokenIn).forceApprove(V2_ROUTER, swapAmount);
        IUniswapV2Router02(V2_ROUTER).swapExactTokensForETHSupportingFeeOnTransferTokens(
            swapAmount, amountOutMin, path, to, deadline
        );
        IERC20(tokenIn).forceApprove(V2_ROUTER, 0);
    }

    function sellV3(
        address tokenIn,
        uint256 amountIn,
        uint24 poolFee,
        address to,
        uint256 amountOutMinimum,
        uint160 sqrtPriceLimitX96
    ) external nonReentrant {
        if (amountIn == 0) revert ZeroAmount();
        if (tokenIn == address(0) || to == address(0)) revert ZeroAddress();

        IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn);

        (uint256 fee, uint256 swapAmount) = quoteSwapAmount(amountIn);
        if (fee > 0) {
            IERC20(tokenIn).safeTransfer(feeRecipient, fee);
            emit FeeTaken(tokenIn, feeRecipient, fee);
        }

        IERC20(tokenIn).forceApprove(V3_ROUTER, swapAmount);

        bytes[] memory calls = new bytes[](2);
        calls[0] = abi.encodeCall(
            IUniswapV3SwapRouter.exactInputSingle,
            (
                IUniswapV3SwapRouter.ExactInputSingleParams({
                    tokenIn: tokenIn,
                    tokenOut: WETH,
                    fee: poolFee,
                    recipient: V3_ROUTER,
                    amountIn: swapAmount,
                    amountOutMinimum: amountOutMinimum,
                    sqrtPriceLimitX96: sqrtPriceLimitX96
                })
            )
        );
        calls[1] = abi.encodeCall(IUniswapV3SwapRouter.unwrapWETH9, (amountOutMinimum, to));

        IUniswapV3SwapRouter(V3_ROUTER).multicall(calls);
        IERC20(tokenIn).forceApprove(V3_ROUTER, 0);
    }

    // ─── Internal ────────────────────────────────────────────────────────────

    function _payEthFee(uint256 fee) internal {
        if (fee == 0) return;
        (bool ok,) = feeRecipient.call{value: fee}("");
        if (!ok) revert EthTransferFailed();
        emit FeeTaken(address(0), feeRecipient, fee);
    }

    receive() external payable {}
}
Chain explorer1749msChain node77ms