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
- Creator
- 0x4e3C4f0bF9…16FE22298c
- 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