FeeRouter
0x28a5328b61e00dd75f1d0c8a1831a65890e42d36
Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
3,451 bytes
ABI entries
34
12 read · 5 write
License
none
Contract information
- Address
- 0x28a5328b61e00dd75f1d0c8a1831a65890e42d36
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.26+commit.8a97fa7a
- Optimization
- Enabled
- Creator
- 0x407Fe47FA0…AB006322d8
- Creation tx
- 0x0abd644aa8…3ce6141e4b
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (12)
DEFAULT_BURNER_SHARE_BPS() → uint16
DEFAULT_CTO_SHARE_BPS() → uint16
DEFAULT_PROTOCOL_SHARE_BPS() → uint16
MAX_BPS() → uint16
burnerRecipient() → address
burnerShareBps() → uint16
ctoShareBps() → uint16
locker() → address
owner() → address
protocolRecipient() → address
protocolRecipientIsSplitter() → bool
protocolShareBps() → uint16
Events (6)
FeesDistributedLockerUpdatedOwnershipTransferredProtocolSplitterModeUpdatedRecipientsUpdatedSharesUpdated
ABI
[
{
"inputs": [],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "AlreadyInitialized",
"type": "error"
},
{
"inputs": [],
"name": "InvalidBasisPoints",
"type": "error"
},
{
"inputs": [],
"name": "InvalidRecipient",
"type": "error"
},
{
"inputs": [],
"name": "NotConfigured",
"type": "error"
},
{
"inputs": [],
"name": "NotLocker",
"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": "ZeroAddress",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "ctoRecipient",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "ctoTokenAmount",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "ctoPairAmount",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "protocolTokenAmount",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "protocolPairAmount",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "burnerTokenAmount",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "burnerPairAmount",
"type": "uint256"
}
],
"name": "FeesDistributed",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "locker",
"type": "address"
}
],
"name": "LockerUpdated",
"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": false,
"internalType": "bool",
"name": "enabled",
"type": "bool"
}
],
"name": "ProtocolSplitterModeUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "protocolRecipient",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "burnerRecipient",
"type": "address"
}
],
"name": "RecipientsUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint16",
"name": "protocolShareBps",
"type": "uint16"
},
{
"indexed": false,
"internalType": "uint16",
"name": "burnerShareBps",
"type": "uint16"
},
{
"indexed": false,
"internalType": "uint16",
"name": "ctoShareBps",
"type": "uint16"
}
],
"name": "SharesUpdated",
"type": "event"
},
{
"inputs": [],
"name": "DEFAULT_BURNER_SHARE_BPS",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "DEFAULT_CTO_SHARE_BPS",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "DEFAULT_PROTOCOL_SHARE_BPS",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MAX_BPS",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "burnerRecipient",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "burnerShareBps",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "ctoShareBps",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "pairToken",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenAmount",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "pairAmount",
"type": "uint256"
},
{
"internalType": "address",
"name": "ctoRecipient",
"type": "address"
}
],
"name": "distribute",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "locker",
"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": "protocolRecipient",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "protocolRecipientIsSplitter",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "protocolShareBps",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "protocolRecipient_",
"type": "address"
},
{
"internalType": "address",
"name": "burnerRecipient_",
"type": "address"
},
{
"internalType": "uint16",
"name": "protocolShareBps_",
"type": "uint16"
},
{
"internalType": "uint16",
"name": "burnerShareBps_",
"type": "uint16"
},
{
"internalType": "uint16",
"name": "ctoShareBps_",
"type": "uint16"
}
],
"name": "setFeeConfig",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "locker_",
"type": "address"
}
],
"name": "setLocker",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
interface IFeeSplitterRecipient {
function feeRouter() external view returns (address);
function currentEpoch() external view returns (uint256);
function deposit(address token, uint256 amount) external;
}
/// @notice Splits both ERC20 assets collected by the LauncherLocker among the
/// protocol, the buy-burner, and the token's current CTO fee vault.
contract FeeRouter is Ownable, ReentrancyGuard {
using SafeERC20 for IERC20;
error InvalidBasisPoints();
error ZeroAddress();
error InvalidRecipient();
error NotConfigured();
error NotLocker();
error AlreadyInitialized();
event FeesDistributed(
address indexed token,
address indexed ctoRecipient,
uint256 ctoTokenAmount,
uint256 ctoPairAmount,
uint256 protocolTokenAmount,
uint256 protocolPairAmount,
uint256 burnerTokenAmount,
uint256 burnerPairAmount
);
event RecipientsUpdated(address indexed protocolRecipient, address indexed burnerRecipient);
event ProtocolSplitterModeUpdated(bool enabled);
event SharesUpdated(uint16 protocolShareBps, uint16 burnerShareBps, uint16 ctoShareBps);
event LockerUpdated(address locker);
uint16 public constant MAX_BPS = 10_000;
uint16 public constant DEFAULT_PROTOCOL_SHARE_BPS = 3_333;
uint16 public constant DEFAULT_BURNER_SHARE_BPS = 3_333;
uint16 public constant DEFAULT_CTO_SHARE_BPS = 3_334;
address public protocolRecipient;
address public burnerRecipient;
address public locker;
bool public protocolRecipientIsSplitter;
/// @notice Share paid to the fixed protocol recipient for each fee asset.
uint16 public protocolShareBps = DEFAULT_PROTOCOL_SHARE_BPS;
/// @notice Share paid to the fixed buy-burner recipient for each fee asset.
uint16 public burnerShareBps = DEFAULT_BURNER_SHARE_BPS;
/// @notice Share paid to the per-token CTO vault for each fee asset.
uint16 public ctoShareBps = DEFAULT_CTO_SHARE_BPS;
constructor() Ownable(msg.sender) {}
/// @notice Atomically updates the two fixed recipients and the split applied
/// identically to both ERC20 fee assets. The per-token CTO recipient
/// continues to be supplied by the LauncherLocker.
function setFeeConfig(
address protocolRecipient_,
address burnerRecipient_,
uint16 protocolShareBps_,
uint16 burnerShareBps_,
uint16 ctoShareBps_
) external onlyOwner {
if (protocolRecipient_ == address(0) || burnerRecipient_ == address(0)) {
revert ZeroAddress();
}
if (
protocolRecipient_ == burnerRecipient_ || protocolRecipient_ == address(this)
|| burnerRecipient_ == address(this)
) revert InvalidRecipient();
(bool protocolIsSplitter, address protocolSplitterSource) = _splitterBinding(protocolRecipient_);
if (protocolIsSplitter && protocolSplitterSource != address(this)) revert InvalidRecipient();
// The factory sends launch-fee WETH directly to this semantic slot.
// A FeeSplitter intentionally ignores raw transfers, so accepting one
// here would make every launch fee unallocated and unclaimable.
(bool burnerIsSplitter,) = _splitterBinding(burnerRecipient_);
if (burnerIsSplitter) revert InvalidRecipient();
if (uint256(protocolShareBps_) + burnerShareBps_ + ctoShareBps_ != MAX_BPS) {
revert InvalidBasisPoints();
}
protocolRecipient = protocolRecipient_;
burnerRecipient = burnerRecipient_;
protocolShareBps = protocolShareBps_;
burnerShareBps = burnerShareBps_;
ctoShareBps = ctoShareBps_;
protocolRecipientIsSplitter = protocolIsSplitter;
emit RecipientsUpdated(protocolRecipient_, burnerRecipient_);
emit ProtocolSplitterModeUpdated(protocolRecipientIsSplitter);
emit SharesUpdated(protocolShareBps_, burnerShareBps_, ctoShareBps_);
}
/// @notice One-time wiring of the locker (set at deployment).
function setLocker(address locker_) external onlyOwner {
if (locker_ == address(0)) revert ZeroAddress();
if (locker != address(0)) revert AlreadyInitialized();
locker = locker_;
emit LockerUpdated(locker_);
}
/// @notice Distributes fees previously transferred to this contract by the locker.
/// @dev Only callable by the locker, within the same tx as the collect.
function distribute(address token, address pairToken, uint256 tokenAmount, uint256 pairAmount, address ctoRecipient)
external
nonReentrant
{
if (msg.sender != locker) revert NotLocker();
if (protocolRecipient == address(0) || burnerRecipient == address(0)) revert NotConfigured();
if (ctoRecipient == address(0)) revert ZeroAddress();
uint16 fixedRecipientShareBps = uint16(uint256(protocolShareBps) + burnerShareBps);
uint256 protocolTokenAmount = _mulBps(tokenAmount, protocolShareBps);
uint256 fixedRecipientTokenAmount = _mulBps(tokenAmount, fixedRecipientShareBps);
uint256 burnerTokenAmount = fixedRecipientTokenAmount - protocolTokenAmount;
uint256 ctoTokenAmount = tokenAmount - fixedRecipientTokenAmount;
uint256 protocolPairAmount = _mulBps(pairAmount, protocolShareBps);
uint256 fixedRecipientPairAmount = _mulBps(pairAmount, fixedRecipientShareBps);
uint256 burnerPairAmount = fixedRecipientPairAmount - protocolPairAmount;
uint256 ctoPairAmount = pairAmount - fixedRecipientPairAmount;
_payFixedRecipient(token, protocolRecipient, protocolTokenAmount, protocolRecipientIsSplitter);
_payFixedRecipient(token, burnerRecipient, burnerTokenAmount, false);
if (ctoTokenAmount != 0) IERC20(token).safeTransfer(ctoRecipient, ctoTokenAmount);
_payFixedRecipient(pairToken, protocolRecipient, protocolPairAmount, protocolRecipientIsSplitter);
_payFixedRecipient(pairToken, burnerRecipient, burnerPairAmount, false);
if (ctoPairAmount != 0) IERC20(pairToken).safeTransfer(ctoRecipient, ctoPairAmount);
emit FeesDistributed(
token,
ctoRecipient,
ctoTokenAmount,
ctoPairAmount,
protocolTokenAmount,
protocolPairAmount,
burnerTokenAmount,
burnerPairAmount
);
}
function _payFixedRecipient(address token, address recipient, uint256 amount, bool isSplitter) private {
if (amount == 0) return;
IERC20 asset = IERC20(token);
if (!isSplitter) {
asset.safeTransfer(recipient, amount);
return;
}
asset.forceApprove(recipient, amount);
IFeeSplitterRecipient(recipient).deposit(token, amount);
asset.forceApprove(recipient, 0);
}
function _splitterBinding(address recipient) private view returns (bool, address) {
if (recipient.code.length == 0) return (false, address(0));
try IFeeSplitterRecipient(recipient).feeRouter() returns (address source) {
try IFeeSplitterRecipient(recipient).currentEpoch() returns (uint256 epoch) {
return (epoch != 0, source);
} catch {
return (false, address(0));
}
} catch {
return (false, address(0));
}
}
/// @dev Equivalent to floor(amount * bps / MAX_BPS) without risking
/// overflow for arbitrary uint256 amounts.
function _mulBps(uint256 amount, uint16 bps) private pure returns (uint256) {
return (amount / MAX_BPS) * bps + ((amount % MAX_BPS) * bps) / MAX_BPS;
}
}
Chain explorer5107msChain node85ms