FeeSwapRouterV3

0xac33d51cf764859a678bcc9b255edbde69956a0b

Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
5,274 bytes
ABI entries
36
11 read · 9 write
License
none

Contract information

Address
0xac33d51cf764859a678bcc9b255edbde69956a0b
Chain
Robinhood Chain (4663)
Compiler
v0.8.24+commit.e11b9ed9
Optimization
Enabled
Creation tx
0x98f9f7fa4b…d246b6043d

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (11)

FEE_TIMELOCK()uint256
MAX_FEE_BPS()uint16
feeBps()uint16
feeRecipient()address
feeRecipientEta()uint256
owner()address
pendingFeeRecipient()address
pendingOwner()address
poolFee()uint24
router()address
weth()address

Events (7)

FeeBpsUpdatedFeeRecipientCancelledFeeRecipientProposedFeeRecipientUpdatedFeeSwapOwnershipTransferStartedOwnershipTransferred

ABI

[
  {
    "inputs": [
      {
        "internalType": "contract ISwapRouterV3",
        "name": "router_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "weth_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "feeRecipient_",
        "type": "address"
      },
      {
        "internalType": "uint24",
        "name": "poolFee_",
        "type": "uint24"
      },
      {
        "internalType": "uint16",
        "name": "feeBps_",
        "type": "uint16"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "target",
        "type": "address"
      }
    ],
    "name": "AddressEmptyCode",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "AddressInsufficientBalance",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "FailedInnerCall",
    "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"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint16",
        "name": "newFeeBps",
        "type": "uint16"
      }
    ],
    "name": "FeeBpsUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "cancelled",
        "type": "address"
      }
    ],
    "name": "FeeRecipientCancelled",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "next",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "eta",
        "type": "uint256"
      }
    ],
    "name": "FeeRecipientProposed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "prev",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "next",
        "type": "address"
      }
    ],
    "name": "FeeRecipientUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "trader",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "buy",
        "type": "bool"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethAmount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "fee",
        "type": "uint256"
      }
    ],
    "name": "FeeSwap",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferStarted",
    "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": "FEE_TIMELOCK",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_FEE_BPS",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "acceptOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "applyFeeRecipient",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "minTokensOut",
        "type": "uint256"
      }
    ],
    "name": "buyWithFee",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "tokensOut",
        "type": "uint256"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "cancelFeeRecipient",
    "outputs": [],
    "stateMutability": "nonpayable",
    "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": "feeRecipientEta",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pendingFeeRecipient",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pendingOwner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "poolFee",
    "outputs": [
      {
        "internalType": "uint24",
        "name": "",
        "type": "uint24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "r",
        "type": "address"
      }
    ],
    "name": "proposeFeeRecipient",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "router",
    "outputs": [
      {
        "internalType": "contract ISwapRouterV3",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amountIn",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "minEthOut",
        "type": "uint256"
      }
    ],
    "name": "sellWithFee",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "ethOut",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint16",
        "name": "newFeeBps",
        "type": "uint16"
      }
    ],
    "name": "setFeeBps",
    "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.24;

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

/// @dev Swap subset of the canonical Uniswap V3 SwapRouter02 on Robinhood Chain.
///      SwapRouter02's exactInputSingle has no deadline field.
interface ISwapRouterV3 {
    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 FeeSwapRouterV3
/// @custom:mission We're building a transparent launchpad. Verified contracts, no
///  hidden owner over tokens, no token tax, liquidity you can never rug. Read the
///  code and verify everything yourself. Every launch is a drop. — rawbin
/// @notice Thin wrapper over Uniswap V3's SwapRouter02 that takes the Rawbin platform
///         fee on swaps made through the site (both Instant-DEX launches and migrated
///         tokens trade on V3 pools). Completely optional: anyone can still trade the
///         pool directly on Uniswap and pay no platform fee — this only prices in the
///         convenience of the UI. The token side always goes straight to the trader;
///         the fee is only ever taken from the ETH leg.
contract FeeSwapRouterV3 is Ownable2Step, ReentrancyGuard {
    using SafeERC20 for IERC20;

    ISwapRouterV3 public immutable router;
    address public immutable weth;
    uint24 public immutable poolFee;   // Uniswap fee tier used for our pools (1% = 10000)

    /// @notice Platform fee in basis points. Owner-adjustable, hard-capped at 3% — a
    ///         trader can verify the maximum they could ever be charged.
    uint16 public feeBps;
    uint16 public constant MAX_FEE_BPS = 300; // hard cap 3%

    /// @notice Where the platform fee goes. Mutable behind a 24h timelock (propose ->
    ///         apply), so it can move to a multisig later without a redeploy.
    address public feeRecipient;
    address public pendingFeeRecipient;
    uint256 public feeRecipientEta;
    uint256 public constant FEE_TIMELOCK = 24 hours;

    event FeeSwap(address indexed trader, address indexed token, bool buy, uint256 ethAmount, uint256 fee);
    event FeeBpsUpdated(uint16 newFeeBps);
    event FeeRecipientProposed(address indexed next, uint256 eta);
    event FeeRecipientUpdated(address indexed prev, address indexed next);
    event FeeRecipientCancelled(address indexed cancelled);

    constructor(
        ISwapRouterV3 router_,
        address weth_,
        address feeRecipient_,
        uint24 poolFee_,
        uint16 feeBps_
    ) Ownable(feeRecipient_) {
        require(feeRecipient_ != address(0), "feeRecipient=0");
        require(weth_ != address(0), "weth=0");
        require(feeBps_ <= MAX_FEE_BPS, "fee too high");
        router = router_;
        weth = weth_;
        feeRecipient = feeRecipient_;
        poolFee = poolFee_;
        feeBps = feeBps_;
    }

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

    /// @notice Adjust the platform fee (immediate, capped). No fee can exceed 3%.
    function setFeeBps(uint16 newFeeBps) external onlyOwner {
        require(newFeeBps <= MAX_FEE_BPS, "fee too high");
        feeBps = newFeeBps;
        emit FeeBpsUpdated(newFeeBps);
    }

    function proposeFeeRecipient(address r) external onlyOwner {
        require(r != address(0), "feeRecipient=0");
        pendingFeeRecipient = r;
        feeRecipientEta = block.timestamp + FEE_TIMELOCK;
        emit FeeRecipientProposed(r, feeRecipientEta);
    }

    function applyFeeRecipient() external onlyOwner {
        // forge-lint: disable-next-line(block-timestamp)
        require(feeRecipientEta != 0 && block.timestamp >= feeRecipientEta, "timelock");
        emit FeeRecipientUpdated(feeRecipient, pendingFeeRecipient);
        feeRecipient = pendingFeeRecipient;
        pendingFeeRecipient = address(0);
        feeRecipientEta = 0;
    }

    function cancelFeeRecipient() external onlyOwner {
        emit FeeRecipientCancelled(pendingFeeRecipient);
        pendingFeeRecipient = address(0);
        feeRecipientEta = 0;
    }

    /// @notice Buy `token` with ETH: platform fee off the top, remainder swapped via the pool.
    function buyWithFee(address token, uint256 minTokensOut)
        external payable nonReentrant returns (uint256 tokensOut)
    {
        require(msg.value > 0, "eth=0");
        uint256 fee = (msg.value * feeBps) / 10_000;
        uint256 swapAmount = msg.value - fee;

        IWETH9(weth).deposit{value: swapAmount}();
        IERC20(weth).forceApprove(address(router), swapAmount);
        tokensOut = router.exactInputSingle(
            ISwapRouterV3.ExactInputSingleParams({
                tokenIn: weth,
                tokenOut: token,
                fee: poolFee,
                recipient: msg.sender,
                amountIn: swapAmount,
                amountOutMinimum: minTokensOut,
                sqrtPriceLimitX96: 0
            })
        );

        if (fee > 0) {
            (bool ok, ) = feeRecipient.call{value: fee}("");
            require(ok, "fee transfer failed");
        }
        emit FeeSwap(msg.sender, token, true, msg.value, fee);
    }

    /// @notice Sell `token` for ETH via the pool; platform fee taken from the ETH proceeds.
    function sellWithFee(address token, uint256 amountIn, uint256 minEthOut)
        external nonReentrant returns (uint256 ethOut)
    {
        require(amountIn > 0, "amount=0");
        IERC20(token).safeTransferFrom(msg.sender, address(this), amountIn);
        IERC20(token).forceApprove(address(router), amountIn);

        uint256 gross = router.exactInputSingle(
            ISwapRouterV3.ExactInputSingleParams({
                tokenIn: token,
                tokenOut: weth,
                fee: poolFee,
                recipient: address(this),
                amountIn: amountIn,
                amountOutMinimum: 0, // slippage enforced on the NET amount below
                sqrtPriceLimitX96: 0
            })
        );

        IWETH9(weth).withdraw(gross);
        uint256 fee = (gross * feeBps) / 10_000;
        ethOut = gross - fee;
        require(ethOut >= minEthOut, "slippage");

        if (fee > 0) {
            (bool okFee, ) = feeRecipient.call{value: fee}("");
            require(okFee, "fee transfer failed");
        }
        (bool okUser, ) = msg.sender.call{value: ethOut}("");
        require(okUser, "eth transfer failed");
        emit FeeSwap(msg.sender, token, false, gross, fee);
    }

    /// @dev Receives ETH from unwrapping WETH during sellWithFee.
    receive() external payable {}
}
Chain explorer5325msChain node87ms