// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; 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 IBondingCurveMin { function buy(uint256 minTokensOut) external payable; function sell(uint256 tokenAmount, uint256 minEthOut) external payable; } interface IV2RouterMin { 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 IWETHMin { function withdraw(uint256) external; } interface IV3RouterMin { struct ExactInputSingleParams { address tokenIn; address tokenOut; uint24 fee; address recipient; uint256 deadline; uint256 amountIn; uint256 amountOutMinimum; uint160 sqrtPriceLimitX96; } function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut); } interface IV3RouterLegacyMin { 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); } /// @title DegenZFeeRouter — collects a bot trade fee atomically in a single transaction. /// @notice Wraps bonding-curve, Uniswap V2 and Uniswap V3 (standard + legacy) trades. /// A configurable fee (default 1%) is skimmed to the DegenZ Fun treasury: /// from the native input on buys, and from the native proceeds on sells. /// @dev Purpose-built (no arbitrary call target) so approvals granted to this router /// can only ever move the caller's own funds for the trade they requested. contract DegenZFeeRouter is Ownable, ReentrancyGuard { using SafeERC20 for IERC20; uint256 public constant MAX_FEE_BPS = 300; // hard cap 3% address public treasury; uint256 public feeBps; error ZeroAddress(); error ZeroAmount(); error FeeTooHigh(); error SlippageExceeded(); error NativeTransferFailed(); event TreasuryUpdated(address indexed treasury); event FeeUpdated(uint256 feeBps); event Bought( address indexed user, address indexed token, uint8 venue, uint256 nativeIn, uint256 fee, uint256 tokensOut ); event Sold( address indexed user, address indexed token, uint8 venue, uint256 tokensIn, uint256 grossNative, uint256 fee, uint256 netNative ); // Venue codes for events: 0 = bonding curve, 1 = Uniswap V2, 2 = Uniswap V3. uint8 private constant VENUE_CURVE = 0; uint8 private constant VENUE_V2 = 1; uint8 private constant VENUE_V3 = 2; constructor(address treasury_, uint256 feeBps_) Ownable(msg.sender) { if (treasury_ == address(0)) revert ZeroAddress(); if (feeBps_ > MAX_FEE_BPS) revert FeeTooHigh(); treasury = treasury_; feeBps = feeBps_; } // ---------------------------------------------------------------- admin function setTreasury(address treasury_) external onlyOwner { if (treasury_ == address(0)) revert ZeroAddress(); treasury = treasury_; emit TreasuryUpdated(treasury_); } function setFeeBps(uint256 feeBps_) external onlyOwner { if (feeBps_ > MAX_FEE_BPS) revert FeeTooHigh(); feeBps = feeBps_; emit FeeUpdated(feeBps_); } // ------------------------------------------------------------------ buys /// @notice Buy a pre-graduation token on its bonding curve. function buyCurve(address curve, address token, uint256 minTokensOut) external payable nonReentrant returns (uint256 tokensOut) { uint256 net = _takeBuyFee(); uint256 balBefore = IERC20(token).balanceOf(address(this)); IBondingCurveMin(curve).buy{value: net}(minTokensOut); tokensOut = IERC20(token).balanceOf(address(this)) - balBefore; if (tokensOut < minTokensOut) revert SlippageExceeded(); IERC20(token).safeTransfer(msg.sender, tokensOut); emit Bought(msg.sender, token, VENUE_CURVE, msg.value, msg.value - net, tokensOut); } /// @notice Buy a token on a Uniswap V2 router. path = [weth, token]. /// @dev Uses SupportingFeeOnTransfer so Tax/Reward Fun coins work post-graduation. function buyV2(address router, address weth, address token, uint256 minTokensOut, uint256 deadline) external payable nonReentrant returns (uint256 tokensOut) { uint256 net = _takeBuyFee(); address[] memory path = new address[](2); path[0] = weth; path[1] = token; uint256 balBefore = IERC20(token).balanceOf(msg.sender); IV2RouterMin(router).swapExactETHForTokensSupportingFeeOnTransferTokens{value: net}( minTokensOut, path, msg.sender, deadline ); tokensOut = IERC20(token).balanceOf(msg.sender) - balBefore; if (tokensOut < minTokensOut) revert SlippageExceeded(); emit Bought(msg.sender, token, VENUE_V2, msg.value, msg.value - net, tokensOut); } /// @notice Buy a token on a Uniswap V3 router (standard or legacy param layout). function buyV3( address router, address weth, address token, uint24 poolFee, uint256 minTokensOut, uint256 deadline, bool legacy ) external payable nonReentrant returns (uint256 tokensOut) { uint256 net = _takeBuyFee(); if (legacy) { tokensOut = IV3RouterLegacyMin(router).exactInputSingle{value: net}( IV3RouterLegacyMin.ExactInputSingleParams({ tokenIn: weth, tokenOut: token, fee: poolFee, recipient: msg.sender, amountIn: net, amountOutMinimum: minTokensOut, sqrtPriceLimitX96: 0 }) ); } else { tokensOut = IV3RouterMin(router).exactInputSingle{value: net}( IV3RouterMin.ExactInputSingleParams({ tokenIn: weth, tokenOut: token, fee: poolFee, recipient: msg.sender, deadline: deadline, amountIn: net, amountOutMinimum: minTokensOut, sqrtPriceLimitX96: 0 }) ); } emit Bought(msg.sender, token, VENUE_V3, msg.value, msg.value - net, tokensOut); } // ----------------------------------------------------------------- sells /// @notice Sell a pre-graduation token back to its bonding curve. function sellCurve(address curve, address token, uint256 amountIn, uint256 minEthOut) external nonReentrant returns (uint256 netNative) { _pullTokens(token, amountIn); IERC20(token).forceApprove(curve, amountIn); uint256 balBefore = address(this).balance; IBondingCurveMin(curve).sell(amountIn, 0); uint256 gross = address(this).balance - balBefore; netNative = _settleSell(token, VENUE_CURVE, amountIn, gross, minEthOut); } /// @notice Sell a token for native on a Uniswap V2 router. path = [token, weth]. function sellV2( address router, address weth, address token, uint256 amountIn, uint256 minEthOut, uint256 deadline ) external nonReentrant returns (uint256 netNative) { _pullTokens(token, amountIn); IERC20(token).forceApprove(router, amountIn); address[] memory path = new address[](2); path[0] = token; path[1] = weth; uint256 balBefore = address(this).balance; IV2RouterMin(router).swapExactTokensForETHSupportingFeeOnTransferTokens( amountIn, 0, path, address(this), deadline ); uint256 gross = address(this).balance - balBefore; netNative = _settleSell(token, VENUE_V2, amountIn, gross, minEthOut); } /// @notice Sell a token for native on a Uniswap V3 router (standard or legacy layout). function sellV3( address router, address weth, address token, uint24 poolFee, uint256 amountIn, uint256 minEthOut, uint256 deadline, bool legacy ) external nonReentrant returns (uint256 netNative) { _pullTokens(token, amountIn); IERC20(token).forceApprove(router, amountIn); uint256 wethBefore = IERC20(weth).balanceOf(address(this)); if (legacy) { IV3RouterLegacyMin(router).exactInputSingle( IV3RouterLegacyMin.ExactInputSingleParams({ tokenIn: token, tokenOut: weth, fee: poolFee, recipient: address(this), amountIn: amountIn, amountOutMinimum: 0, sqrtPriceLimitX96: 0 }) ); } else { IV3RouterMin(router).exactInputSingle( IV3RouterMin.ExactInputSingleParams({ tokenIn: token, tokenOut: weth, fee: poolFee, recipient: address(this), deadline: deadline, amountIn: amountIn, amountOutMinimum: 0, sqrtPriceLimitX96: 0 }) ); } uint256 wethOut = IERC20(weth).balanceOf(address(this)) - wethBefore; if (wethOut > 0) IWETHMin(weth).withdraw(wethOut); netNative = _settleSell(token, VENUE_V3, amountIn, wethOut, minEthOut); } // --------------------------------------------------------------- internal function _takeBuyFee() internal returns (uint256 net) { if (msg.value == 0) revert ZeroAmount(); uint256 fee = (msg.value * feeBps) / 10_000; net = msg.value - fee; if (net == 0) revert ZeroAmount(); if (fee > 0) _sendNative(treasury, fee); } function _pullTokens(address token, uint256 amountIn) internal { if (amountIn == 0) revert ZeroAmount(); IERC20(token).safeTransferFrom(msg.sender, address(this), amountIn); } function _settleSell(address token, uint8 venue, uint256 amountIn, uint256 gross, uint256 minEthOut) internal returns (uint256 net) { uint256 fee = (gross * feeBps) / 10_000; net = gross - fee; if (net < minEthOut) revert SlippageExceeded(); if (fee > 0) _sendNative(treasury, fee); _sendNative(msg.sender, net); emit Sold(msg.sender, token, venue, amountIn, gross, fee, net); } function _sendNative(address to, uint256 amount) internal { (bool ok,) = to.call{value: amount}(""); if (!ok) revert NativeTransferFailed(); } /// @notice Rescue tokens accidentally left in the router (dust from fee-on-transfer, etc.). function sweepToken(address token, address to) external onlyOwner { IERC20(token).safeTransfer(to, IERC20(token).balanceOf(address(this))); } /// @notice Rescue native dust. function sweepNative(address to) external onlyOwner { _sendNative(to, address(this).balance); } receive() external payable {} }