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
- Creator
- 0x76053CcE1B…c8949D3385
- 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