FeeSwapRouter

0x8acc0be5e946812542c562395ef6f9cf792d83da

Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
3,666 bytes
ABI entries
34
7 read · 8 write
License
none

Contract information

Address
0x8acc0be5e946812542c562395ef6f9cf792d83da
Chain
Robinhood Chain (4663)
Compiler
v0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0x7e33b9e0c6…f517a86f2e

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (7)

MAX_BPS()uint16
MAX_FEE_BPS()uint16
feeBps()uint16
feeRecipient()address
owner()address
router()address
weth()address

Events (7)

BoughtEthRescuedFeeBpsUpdatedFeeRecipientUpdatedOwnershipTransferredSoldTokenRescued

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "router_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "weth_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "feeRecipient_",
        "type": "address"
      },
      {
        "internalType": "uint16",
        "name": "feeBps_",
        "type": "uint16"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "EthTransferFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidBps",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotWeth",
    "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": "Slippage",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddress",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAmount",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "buyer",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethIn",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "fee",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokensOut",
        "type": "uint256"
      }
    ],
    "name": "Bought",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "EthRescued",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint16",
        "name": "feeBps",
        "type": "uint16"
      }
    ],
    "name": "FeeBpsUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "address",
        "name": "feeRecipient",
        "type": "address"
      }
    ],
    "name": "FeeRecipientUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferred",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "seller",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokensIn",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "fee",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethOut",
        "type": "uint256"
      }
    ],
    "name": "Sold",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "TokenRescued",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "MAX_BPS",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_FEE_BPS",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint24",
        "name": "poolFee",
        "type": "uint24"
      },
      {
        "internalType": "uint256",
        "name": "amountOutMinimum",
        "type": "uint256"
      }
    ],
    "name": "buy",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "tokensOut",
        "type": "uint256"
      }
    ],
    "stateMutability": "payable",
    "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": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      }
    ],
    "name": "rescueEth",
    "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": "rescueToken",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "router",
    "outputs": [
      {
        "internalType": "contract ISwapRouter02",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint24",
        "name": "poolFee",
        "type": "uint24"
      },
      {
        "internalType": "uint256",
        "name": "amountIn",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "amountOutMinimum",
        "type": "uint256"
      }
    ],
    "name": "sell",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "ethOut",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "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": [],
    "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.26;

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
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";

interface ISwapRouter02 {
    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) external;
}

/// @notice Interface-fee wrapper around SwapRouter02 for the robinfun frontend.
/// Buys skim `feeBps` of the incoming ETH before swapping; sells swap first and
/// skim `feeBps` of the ETH proceeds. The pool's own fee is unchanged — this is
/// an additional frontend fee, collected only from trades routed through this
/// contract. Slippage bounds apply to what the USER receives (net of the fee).
contract FeeSwapRouter is Ownable, ReentrancyGuard {
    using SafeERC20 for IERC20;

    error ZeroAddress();
    error InvalidBps();
    error ZeroAmount();
    error Slippage();
    error EthTransferFailed();
    error NotWeth();

    event Bought(address indexed buyer, address indexed token, uint256 ethIn, uint256 fee, uint256 tokensOut);
    event Sold(address indexed seller, address indexed token, uint256 tokensIn, uint256 fee, uint256 ethOut);
    event FeeBpsUpdated(uint16 feeBps);
    event FeeRecipientUpdated(address feeRecipient);
    event TokenRescued(address indexed token, address indexed to, uint256 amount);
    event EthRescued(address indexed to, uint256 amount);

    uint16 public constant MAX_BPS = 10_000;
    /// @notice Hard cap on the interface fee (2%).
    uint16 public constant MAX_FEE_BPS = 200;

    ISwapRouter02 public immutable router;
    address public immutable weth;

    uint16 public feeBps;
    address public feeRecipient;

    constructor(address router_, address weth_, address feeRecipient_, uint16 feeBps_) Ownable(msg.sender) {
        if (router_ == address(0) || weth_ == address(0) || feeRecipient_ == address(0)) revert ZeroAddress();
        if (feeBps_ > MAX_FEE_BPS) revert InvalidBps();
        router = ISwapRouter02(router_);
        weth = weth_;
        feeRecipient = feeRecipient_;
        feeBps = feeBps_;
    }

    /// @notice Buy `token` with the attached ETH. `feeBps` of the ETH goes to
    /// `feeRecipient`; the remainder swaps through the pool straight to the
    /// caller. `amountOutMinimum` bounds the tokens the caller receives.
    function buy(address token, uint24 poolFee, uint256 amountOutMinimum)
        external
        payable
        nonReentrant
        returns (uint256 tokensOut)
    {
        if (msg.value == 0) revert ZeroAmount();
        uint256 fee = (msg.value * feeBps) / MAX_BPS;
        if (fee > 0) _sendEth(feeRecipient, fee);

        tokensOut = router.exactInputSingle{value: msg.value - fee}(
            ISwapRouter02.ExactInputSingleParams({
                tokenIn: weth,
                tokenOut: token,
                fee: poolFee,
                recipient: msg.sender,
                amountIn: msg.value - fee,
                amountOutMinimum: amountOutMinimum,
                sqrtPriceLimitX96: 0
            })
        );
        emit Bought(msg.sender, token, msg.value, fee, tokensOut);
    }

    /// @notice Sell `amountIn` of `token` for native ETH. The swap's WETH
    /// proceeds are unwrapped, `feeBps` goes to `feeRecipient`, and the rest is
    /// sent to the caller. `amountOutMinimum` bounds the NET ETH the caller
    /// receives (after the fee). Requires prior approval of this contract.
    function sell(address token, uint24 poolFee, uint256 amountIn, uint256 amountOutMinimum)
        external
        nonReentrant
        returns (uint256 ethOut)
    {
        if (amountIn == 0) revert ZeroAmount();
        uint256 balBefore = IERC20(token).balanceOf(address(this));
        IERC20(token).safeTransferFrom(msg.sender, address(this), amountIn);
        uint256 received = IERC20(token).balanceOf(address(this)) - balBefore;
        IERC20(token).forceApprove(address(router), received);

        uint256 wethOut = router.exactInputSingle(
            ISwapRouter02.ExactInputSingleParams({
                tokenIn: token,
                tokenOut: weth,
                fee: poolFee,
                recipient: address(this),
                amountIn: received,
                amountOutMinimum: 0, // net amount enforced below
                sqrtPriceLimitX96: 0
            })
        );
        IWETH9(weth).withdraw(wethOut);

        uint256 fee = (wethOut * feeBps) / MAX_BPS;
        ethOut = wethOut - fee;
        if (ethOut < amountOutMinimum) revert Slippage();
        if (fee > 0) _sendEth(feeRecipient, fee);
        _sendEth(msg.sender, ethOut);
        emit Sold(msg.sender, token, amountIn, fee, ethOut);
    }

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

    function setFeeBps(uint16 feeBps_) external onlyOwner {
        if (feeBps_ > MAX_FEE_BPS) revert InvalidBps();
        feeBps = feeBps_;
        emit FeeBpsUpdated(feeBps_);
    }

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

    /// @notice Escape hatch for tokens stranded by a partial failure; this
    /// contract is not meant to hold balances between transactions.
    function rescueToken(address token, address to, uint256 amount) external onlyOwner {
        if (to == address(0)) revert ZeroAddress();
        IERC20(token).safeTransfer(to, amount);
        emit TokenRescued(token, to, amount);
    }

    function rescueEth(address to) external onlyOwner {
        if (to == address(0)) revert ZeroAddress();
        uint256 amount = address(this).balance;
        (bool ok,) = to.call{value: amount}("");
        if (!ok) revert EthTransferFailed();
        emit EthRescued(to, amount);
    }

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

    /// @dev Only WETH may push ETH here (unwrap during sells).
    receive() external payable {
        if (msg.sender != weth) revert NotWeth();
    }
}
Chain explorer2549msChain node103ms