RadarDEX Docs
What RadarDex is and how launching a token through it works.
About RadarDex
RadarDex (previously ArcDEXScan) is a DEX screener and trading hub for Arc Mainnet — the USDC-native network by Circle. It watches the chain directly and aggregates every trading pool into one interface:
Launch a token — how it works
A launch is one transaction to the LaunchFactory. It creates everything at once:
Logo, website and socials are stored off-chain in the RadarDex API and can be edited later by signing a message from the deployer wallet.
Two launch modes: Classic and Reflection
Every launch is one of two types, chosen at creation. Both share the same trustless core — fixed 1B supply, 100% single-sided liquidity, LP locked forever, no owner or mint — and differ only in what happens to the pool's trading fees.
FeeSplitLocker. Trading fees accrue to the locked position and are split 70% to the creator / 30% to the platform. collectFees is permissionless — anyone can trigger a collection, but recipients are fixed in the lock record, so nothing can be redirected.ReflectionLaunchFactory into the ReflectionLocker. Here the pool's USDC fees are split 50% to holders / 50% to the creator, and any token-side fees are burned. The holder half is distributed pro-rata across every holder by balance — you earn USDC just for holding, with no staking or lockup. Selling or transferring auto-claims what you're owed; pure holders claim any time with a single claim() call that never expires. On RadarDEX these tokens carry a metallic R badge, and there's a filter to show only them.How reflection works under the hood: collectFees(tokenId) on the locker pulls the position's accrued fees, sends the creator's USDC share directly, and calls distribute() on the token with the holders' share — which credits every holder proportionally via an on-chain accumulator. Holders read withdrawableDividendOf(address) to see what's claimable and call claim() to withdraw. The 50/50 split and the burn are fixed constants in the contract — not adjustable by anyone.
Liquidity & fees
The FeeSplitLocker holds the LP position and deliberately cannot release it: no decreaseLiquidity, no burn, no NFT transfer or approve, no delegatecall, no admin backdoor. Not the deployer, not the treasury, not the factory — nobody can pull the liquidity.
The only thing that moves is the 1% swap fee the pool earns. How it's split depends on the launch type:
Classic launch — 70 / 30
collectFees(tokenId)is permissionless — anyone can call it, but recipients come from the lock record, so nothing can be redirected.- Fees accrue in both assets (USDC and the token); both are split 70/30.
- Deployers claim from their token page on RadarDex — the more the token trades, the more they earn.
Reflection launch — 50 / 50, holders earn
- The USDC side of the fee is split 50/50; the holder half is distributed pro-rata across every holder by balance — you earn USDC just for holding, no staking or lockup.
- The token side of the fee is burned, permanently reducing supply.
- Holders
claim()accrued USDC any time (it never expires); selling or transferring auto-claims what you're owed. - The 50/50 split and the burn are fixed constants in the contract — not adjustable by anyone.
Launchpad integration
RadarDex automatically pulls token metadata (logo, website, X, Telegram) from partner launchpads and shows a launchpad badge on their tokens. Integration is for launchpads whose tokens trade in Uniswap pools directly — no bonding curve stage. Tokens with pools on Uniswap are already indexed by RadarDex on-chain; the integration only enriches them with your metadata and badge.
To get integrated, a launchpad provides three things:
address, name, symbol, image URL, and optional website / twitter / telegram. Image can be any https URL (or a base64 data-URI). Pagination is fine. No auth, no rate-limit surprises — we poll it roughly every 2 hours.How the sync behaves: metadata from your API only fills blanks — if a token's deployer already set their own logo or links on RadarDex, we never overwrite them. The sync re-runs on a schedule, so tokens you launch later are picked up automatically with no further action on your side.
Index RadarDEX tokens
For screeners, aggregators and analytics platforms (GMGN, DexScreener, GeckoTerminal, …) that want to tag tokens launched through RadarDEX on Arc Mainnet (chain id 5042). Everything needed for detection and metadata is below.
Factory contracts — four factories, one launchpad. The V3 factory adds a quote field to the deploy event (any pair asset), the others use the 6-field form:
V2 added Community Takeover support; the Reflection factory launches tokens whose holders earn USDC from LP fees. Factory V3 lets a token pair against any whitelisted quote asset (USDC, EURC, COOL, cirBTC…), not just USDC; the token side of every LP fee is burned (supply only shrinks) and the quote side is split creator / treasury. Token detection: subscribe to the deploy event on all four factory addresses.
Deploy event (emitted by every factory on each launch):
TokenLaunched(address indexed token, address indexed deployer, address pool, uint256 lpTokenId, string name, string symbol)
topic0: 0x851d681a32f0efba577c4a1bd412f74b575764a6b91e499a05a48a23f3821d66
// Factory V3 — adds the pair asset as a 7th field:
TokenLaunched(address indexed token, address indexed deployer, address pool, uint256 lpTokenId, string name, string symbol, address quote)
topic0: 0xa9d57fe9b64954c79fe599f5ef63ca270fe2e55cafaa7830d92bb1051a598089
Mechanics: no bonding curve — every token deploys straight into a Uniswap V3 pool paired with its quote asset — native USDC (0x3600000000000000000000000000000000000000) by default, or any whitelisted quote on the V3 factory — fee tier 10000 (1%), full 1B supply as single-sided liquidity. LP NFTs are locked forever in the lockers:
Example tokens:
Metadata API — public, no auth: token icons, socials and the full list of launches:
https://api.radardex.pro/tokenlist.json
https://api.radardex.pro/launches
https://api.radardex.pro/token/{address}
Launchpad logo (for the badge): radarlogonew.png
Token Locker
RadarDEX Token Locker is an on-chain vault for locking any ERC-20 on Arc Mainnet — team allocations, marketing budgets, V2-style LP tokens — with optional linear vesting. It is fully trustless: no one can touch locked tokens, not the team, not RadarDEX. Locked tokens can only ever go to the lock's beneficiary, only per its schedule.
How a lock works. One lock = {token, beneficiary, amount, tgeTime, tgeBps, vestEnd, title}:
- Before
tgeTimenothing is claimable. - At
tgeTime,amount × tgeBps / 10000unlocks instantly. - The remainder vests linearly (second by second) from
tgeTimetovestEnd. - A simple "everything at date X" lock is
tgeBps = 10000withvestEnd == tgeTime.
Unlocked tokens are not pushed automatically — the beneficiary calls claim(id) (or presses Claim in the Locker tab) and receives everything unlocked so far. Claiming any time, any number of times.
Creating a lock on the site (Locker → + New lock): paste the token address (symbol, decimals and your balance load automatically; the balance is clickable to lock everything), enter the amount, an optional label shown on the token page ("Team", "Marketing", "LP"), pick the unlock date & time (quick presets: 30/60/90/180/360 days), set the % unlocked at TGE — 100% means a simple lock, anything less reveals the vesting end date — and optionally a beneficiary (defaults to your connected wallet; use your team multisig if claiming should be separate from locking).
Fee. Creating a lock costs a flat fee in native USDC (currently 1 USDC), shown on the button and read live from the contract. Claiming, extending and transferring the beneficiary are free. The fee is hard-capped at 50 USDC in the contract — it can never exceed that.
Where locks show up: the Locker tab (searchable, sortable, live countdowns), the token page under the burn line — "Locked (Team): 10.00% of supply · unlocks in 29d 14h" — and the Holders list, where the locker contract is badged 🔒 with the lock's label instead of looking like a whale wallet.
Contract functions:
lock(token, beneficiary, amount, tgeTime, tgeBps, vestEnd, title) payable → id
creates a lock; msg.value must equal lockFee(); returns the lock id
claim(id)
sends everything unlocked-but-unclaimed to the beneficiary
claimable(id) view → uint256
unlockedAt(id, timestamp) view → uint256
extendLock(id, newTgeTime, newVestEnd)
beneficiary only; dates can only move FORWARD, never shorter
transferBeneficiary(id, to)
lockCount() / locks(id) / lockInfo(id) / locksOfToken(token) / locksOfBeneficiary(who)
Trust model. There are no owner powers over locked funds — no rescue, no pause, no upgrade path. The only admin surface is the flat creation fee (capped at MAX_LOCK_FEE = 50 native USDC) and its recipient. Locks are irrevocable and can only be extended forward. Fee-on-transfer tokens are supported: the lock records the amount actually received.
Full source (verified on arc-scan; solc 0.8.20+, optimizer 200 runs):
pragma solidity ^0.8.20;
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
}
contract RadarLocker {
struct Lock {
address token;
address beneficiary;
uint128 amount;
uint128 claimed;
uint64 tgeTime;
uint16 tgeBps;
uint64 vestEnd;
bool revokedNever;
string title;
}
Lock[] public locks;
mapping(address => uint256[]) private _byToken;
mapping(address => uint256[]) private _byBeneficiary;
address public feeAdmin;
address public feeRecipient;
uint256 public lockFee;
uint256 public constant MAX_LOCK_FEE = 50e18;
event Locked(uint256 indexed id, address indexed token, address indexed beneficiary,
uint256 amount, uint64 tgeTime, uint16 tgeBps, uint64 vestEnd, string title);
event Claimed(uint256 indexed id, address indexed beneficiary, uint256 amount);
event LockExtended(uint256 indexed id, uint64 newTgeTime, uint64 newVestEnd);
event BeneficiaryTransferred(uint256 indexed id, address indexed from, address indexed to);
event FeeUpdated(uint256 lockFee, address feeRecipient, address feeAdmin);
uint256 private _lockGuard = 1;
modifier nonReentrant() { require(_lockGuard == 1, "reentrant"); _lockGuard = 2; _; _lockGuard = 1; }
constructor(address _feeAdmin, address _feeRecipient, uint256 _lockFee) {
require(_lockFee <= MAX_LOCK_FEE, "fee too high");
feeAdmin = _feeAdmin;
feeRecipient = _feeRecipient == address(0) ? _feeAdmin : _feeRecipient;
lockFee = _lockFee;
emit FeeUpdated(_lockFee, feeRecipient, _feeAdmin);
}
function lock(
address token,
address beneficiary,
uint256 amount,
uint64 tgeTime,
uint16 tgeBps,
uint64 vestEnd,
string calldata title
) external payable nonReentrant returns (uint256 id) {
require(token != address(0) && beneficiary != address(0), "zero addr");
require(amount > 0, "amount=0");
require(tgeTime > block.timestamp, "tge in past");
require(tgeBps <= 10000, "bps>100%");
require(vestEnd >= tgeTime, "vest before tge");
require(tgeBps == 10000 ? vestEnd == tgeTime : vestEnd > tgeTime, "bad schedule");
require(msg.value == lockFee, "wrong fee");
require(bytes(title).length <= 48, "title too long");
if (lockFee > 0) {
(bool ok, ) = feeRecipient.call{value: lockFee}("");
require(ok, "fee send failed");
}
uint256 balBefore = IERC20(token).balanceOf(address(this));
require(IERC20(token).transferFrom(msg.sender, address(this), amount), "transferFrom failed");
uint256 received = IERC20(token).balanceOf(address(this)) - balBefore;
require(received > 0, "nothing received");
require(received <= type(uint128).max, "amount too large");
id = locks.length;
locks.push(Lock({
token: token,
beneficiary: beneficiary,
amount: uint128(received),
claimed: 0,
tgeTime: tgeTime,
tgeBps: tgeBps,
vestEnd: vestEnd,
revokedNever: false,
title: title
}));
_byToken[token].push(id);
_byBeneficiary[beneficiary].push(id);
emit Locked(id, token, beneficiary, received, tgeTime, tgeBps, vestEnd, title);
}
function unlockedAt(uint256 id, uint256 t) public view returns (uint256) {
Lock storage L = locks[id];
if (t < L.tgeTime) return 0;
uint256 tge = (uint256(L.amount) * L.tgeBps) / 10000;
if (t >= L.vestEnd) return L.amount;
uint256 vestable = L.amount - tge;
uint256 elapsed = t - L.tgeTime;
uint256 span = L.vestEnd - L.tgeTime;
return tge + (vestable * elapsed) / span;
}
function claimable(uint256 id) public view returns (uint256) {
Lock storage L = locks[id];
return unlockedAt(id, block.timestamp) - L.claimed;
}
function claim(uint256 id) external nonReentrant {
Lock storage L = locks[id];
require(msg.sender == L.beneficiary, "not beneficiary");
uint256 amt = unlockedAt(id, block.timestamp) - L.claimed;
require(amt > 0, "nothing claimable");
L.claimed += uint128(amt);
require(IERC20(L.token).transfer(L.beneficiary, amt), "transfer failed");
emit Claimed(id, L.beneficiary, amt);
}
function extendLock(uint256 id, uint64 newTgeTime, uint64 newVestEnd) external {
Lock storage L = locks[id];
require(msg.sender == L.beneficiary, "not beneficiary");
require(L.claimed == 0, "already vesting");
require(newTgeTime >= L.tgeTime && newVestEnd >= L.vestEnd, "only forward");
require(newVestEnd >= newTgeTime, "vest before tge");
require(L.tgeBps == 10000 ? newVestEnd == newTgeTime : newVestEnd > newTgeTime, "bad schedule");
L.tgeTime = newTgeTime;
L.vestEnd = newVestEnd;
emit LockExtended(id, newTgeTime, newVestEnd);
}
function transferBeneficiary(uint256 id, address to) external {
Lock storage L = locks[id];
require(msg.sender == L.beneficiary, "not beneficiary");
require(to != address(0), "zero addr");
address from = L.beneficiary;
L.beneficiary = to;
_byBeneficiary[to].push(id);
emit BeneficiaryTransferred(id, from, to);
}
function setFee(uint256 _lockFee, address _feeRecipient) external {
require(msg.sender == feeAdmin, "not admin");
require(_lockFee <= MAX_LOCK_FEE, "fee too high");
require(_feeRecipient != address(0), "recipient=0");
lockFee = _lockFee;
feeRecipient = _feeRecipient;
emit FeeUpdated(_lockFee, _feeRecipient, feeAdmin);
}
function setFeeAdmin(address _admin) external {
require(msg.sender == feeAdmin, "not admin");
feeAdmin = _admin;
emit FeeUpdated(lockFee, feeRecipient, _admin);
}
function lockCount() external view returns (uint256) { return locks.length; }
function locksOfToken(address token) external view returns (uint256[] memory) { return _byToken[token]; }
function locksOfBeneficiary(address who) external view returns (uint256[] memory) { return _byBeneficiary[who]; }
function lockInfo(uint256 id) external view returns (
address token, address beneficiary, uint256 amount, uint256 claimed,
uint64 tgeTime, uint16 tgeBps, uint64 vestEnd, string memory title,
uint256 unlockedNow, uint256 claimableNow
) {
Lock storage L = locks[id];
uint256 u = unlockedAt(id, block.timestamp);
return (L.token, L.beneficiary, L.amount, L.claimed, L.tgeTime, L.tgeBps, L.vestEnd, L.title, u, u - L.claimed);
}
}
Contracts & Source
Every RadarDEX contract on Arc Mainnet (chain id 5042), with full Solidity source (comments stripped). Swaps route through fee-router contracts that take a 0.3% fee and forward the trade to the underlying DEX in one atomic transaction — they hold no user funds between trades. All routers are immutable (no proxy, no upgrade path); the fee is capped at 1% by an immutable constant.
Addresses
Swap fee-routers
Locker
Launchpad factories
Launchpad fee-lockers
Source code
Swap fee-routers
FeeSwapRouter V3 · FeeSwapRouter.sol
pragma solidity ^0.8.20;
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
function approve(address spender, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
}
interface IV3SwapRouter {
struct ExactInputParams {
bytes path;
address recipient;
uint256 amountIn;
uint256 amountOutMinimum;
}
function exactInput(ExactInputParams calldata params)
external payable returns (uint256 amountOut);
}
contract FeeSwapRouter {
address public owner;
uint16 public feeBps;
address public feeRecipient;
uint16 public constant MAX_FEE_BPS = 100;
mapping(address => bool) public approvedRouter;
event Swapped(address indexed user, address indexed router, address indexed tokenIn, address tokenOut, uint256 amountInTotal, uint256 feeAmount, uint256 amountOut);
event FeeUpdated(uint16 feeBps, address feeRecipient);
event RouterSet(address router, bool approved);
event OwnerChanged(address newOwner);
modifier onlyOwner() { require(msg.sender == owner, "not owner"); _; }
constructor(address _feeRecipient, uint16 _feeBps, address _initialRouter) {
require(_feeRecipient != address(0), "recipient=0");
require(_feeBps <= MAX_FEE_BPS, "fee too high");
owner = msg.sender;
feeRecipient = _feeRecipient;
feeBps = _feeBps;
if (_initialRouter != address(0)) {
approvedRouter[_initialRouter] = true;
emit RouterSet(_initialRouter, true);
}
emit OwnerChanged(msg.sender);
emit FeeUpdated(_feeBps, _feeRecipient);
}
function swapExactInput(
address router,
address tokenIn,
address tokenOut,
uint24 poolFee,
uint256 amountIn,
uint256 amountOutMinimum
) external returns (uint256 amountOut) {
require(approvedRouter[router], "router not approved");
require(amountIn > 0, "amountIn=0");
_safeTransferFrom(tokenIn, msg.sender, address(this), amountIn);
uint256 fee = (amountIn * feeBps) / 10000;
if (fee > 0) _safeTransfer(tokenIn, feeRecipient, fee);
uint256 swapAmount = amountIn - fee;
_safeApprove(tokenIn, router, 0);
_safeApprove(tokenIn, router, swapAmount);
bytes memory path = abi.encodePacked(tokenIn, poolFee, tokenOut);
amountOut = IV3SwapRouter(router).exactInput(
IV3SwapRouter.ExactInputParams({
path: path,
recipient: msg.sender,
amountIn: swapAmount,
amountOutMinimum: amountOutMinimum
})
);
emit Swapped(msg.sender, router, tokenIn, tokenOut, amountIn, fee, amountOut);
}
function setFee(uint16 _feeBps) external onlyOwner {
require(_feeBps <= MAX_FEE_BPS, "fee too high");
feeBps = _feeBps;
emit FeeUpdated(_feeBps, feeRecipient);
}
function setFeeRecipient(address _recipient) external onlyOwner {
require(_recipient != address(0), "recipient=0");
feeRecipient = _recipient;
emit FeeUpdated(feeBps, _recipient);
}
function setRouter(address router, bool approved) external onlyOwner {
require(router != address(0), "router=0");
approvedRouter[router] = approved;
emit RouterSet(router, approved);
}
function transferOwnership(address newOwner) external onlyOwner {
require(newOwner != address(0), "owner=0");
owner = newOwner;
emit OwnerChanged(newOwner);
}
function rescue(address token, address to, uint256 amount) external onlyOwner {
_safeTransfer(token, to, amount);
}
function _safeTransfer(address token, address to, uint256 amount) internal {
(bool ok, bytes memory data) = token.call(abi.encodeWithSelector(IERC20.transfer.selector, to, amount));
require(ok && (data.length == 0 || abi.decode(data, (bool))), "transfer failed");
}
function _safeTransferFrom(address token, address from, address to, uint256 amount) internal {
(bool ok, bytes memory data) = token.call(abi.encodeWithSelector(IERC20.transferFrom.selector, from, to, amount));
require(ok && (data.length == 0 || abi.decode(data, (bool))), "transferFrom failed");
}
function _safeApprove(address token, address spender, uint256 amount) internal {
(bool ok, bytes memory data) = token.call(abi.encodeWithSelector(IERC20.approve.selector, spender, amount));
require(ok && (data.length == 0 || abi.decode(data, (bool))), "approve failed");
}
}
FeeSwapRouterV2 · FeeSwapRouterV2.sol
pragma solidity ^0.8.20;
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
function approve(address spender, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
}
interface IV2Router {
function swapExactTokensForTokensSupportingFeeOnTransferTokens(
uint256 amountIn,
uint256 amountOutMin,
address[] calldata path,
address to,
uint256 deadline
) external;
}
contract FeeSwapRouterV2 {
address public owner;
uint16 public feeBps;
address public feeRecipient;
uint16 public constant MAX_FEE_BPS = 100;
mapping(address => bool) public approvedRouter;
event Swapped(address indexed user, address indexed router, address indexed tokenIn, address tokenOut, uint256 amountInTotal, uint256 feeAmount, uint256 amountOut);
event FeeUpdated(uint16 feeBps, address feeRecipient);
event RouterSet(address router, bool approved);
event OwnerChanged(address newOwner);
modifier onlyOwner() { require(msg.sender == owner, "not owner"); _; }
constructor(address _feeRecipient, uint16 _feeBps, address _initialRouter) {
require(_feeRecipient != address(0), "recipient=0");
require(_feeBps <= MAX_FEE_BPS, "fee too high");
owner = msg.sender;
feeRecipient = _feeRecipient;
feeBps = _feeBps;
if (_initialRouter != address(0)) {
approvedRouter[_initialRouter] = true;
emit RouterSet(_initialRouter, true);
}
emit OwnerChanged(msg.sender);
emit FeeUpdated(_feeBps, _feeRecipient);
}
function swapExactInputV2(
address router,
address tokenIn,
address tokenOut,
uint256 amountIn,
uint256 amountOutMinimum
) external returns (uint256 amountOut) {
require(approvedRouter[router], "router not approved");
require(amountIn > 0, "amountIn=0");
require(tokenIn != tokenOut, "same token");
_safeTransferFrom(tokenIn, msg.sender, address(this), amountIn);
uint256 fee = (amountIn * feeBps) / 10000;
if (fee > 0) _safeTransfer(tokenIn, feeRecipient, fee);
uint256 swapAmount = amountIn - fee;
_safeApprove(tokenIn, router, 0);
_safeApprove(tokenIn, router, swapAmount);
address[] memory path = new address[](2);
path[0] = tokenIn;
path[1] = tokenOut;
uint256 balBefore = IERC20(tokenOut).balanceOf(msg.sender);
IV2Router(router).swapExactTokensForTokensSupportingFeeOnTransferTokens(
swapAmount,
amountOutMinimum,
path,
msg.sender,
block.timestamp
);
amountOut = IERC20(tokenOut).balanceOf(msg.sender) - balBefore;
require(amountOut >= amountOutMinimum, "slippage");
emit Swapped(msg.sender, router, tokenIn, tokenOut, amountIn, fee, amountOut);
}
function setFee(uint16 _feeBps) external onlyOwner {
require(_feeBps <= MAX_FEE_BPS, "fee too high");
feeBps = _feeBps;
emit FeeUpdated(_feeBps, feeRecipient);
}
function setFeeRecipient(address _recipient) external onlyOwner {
require(_recipient != address(0), "recipient=0");
feeRecipient = _recipient;
emit FeeUpdated(feeBps, _recipient);
}
function setRouter(address router, bool approved) external onlyOwner {
require(router != address(0), "router=0");
approvedRouter[router] = approved;
emit RouterSet(router, approved);
}
function transferOwnership(address newOwner) external onlyOwner {
require(newOwner != address(0), "owner=0");
owner = newOwner;
emit OwnerChanged(newOwner);
}
function rescue(address token, address to, uint256 amount) external onlyOwner {
_safeTransfer(token, to, amount);
}
function _safeTransfer(address token, address to, uint256 amount) internal {
(bool ok, bytes memory data) = token.call(abi.encodeWithSelector(IERC20.transfer.selector, to, amount));
require(ok && (data.length == 0 || abi.decode(data, (bool))), "transfer failed");
}
function _safeTransferFrom(address token, address from, address to, uint256 amount) internal {
(bool ok, bytes memory data) = token.call(abi.encodeWithSelector(IERC20.transferFrom.selector, from, to, amount));
require(ok && (data.length == 0 || abi.decode(data, (bool))), "transferFrom failed");
}
function _safeApprove(address token, address spender, uint256 amount) internal {
(bool ok, bytes memory data) = token.call(abi.encodeWithSelector(IERC20.approve.selector, spender, amount));
require(ok && (data.length == 0 || abi.decode(data, (bool))), "approve failed");
}
}
FeeSwapRouter Multihop · FeeSwapRouterV3-multihop.sol
pragma solidity ^0.8.20;
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
function approve(address spender, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
}
interface IV3SwapRouter {
struct ExactInputParams {
bytes path;
address recipient;
uint256 amountIn;
uint256 amountOutMinimum;
}
function exactInput(ExactInputParams calldata params)
external payable returns (uint256 amountOut);
}
interface IUniversalRouter {
function execute(bytes calldata commands, bytes[] calldata inputs, uint256 deadline)
external payable;
}
interface IPermit2 {
function approve(address token, address spender, uint160 amount, uint48 expiration) external;
function allowance(address user, address token, address spender)
external view returns (uint160 amount, uint48 expiration, uint48 nonce);
}
contract FeeSwapRouter {
address public owner;
uint16 public feeBps;
address public feeRecipient;
uint16 public constant MAX_FEE_BPS = 100;
mapping(address => bool) public approvedRouter;
address public constant PERMIT2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3;
struct PoolKey {
address currency0;
address currency1;
uint24 fee;
int24 tickSpacing;
address hooks;
}
struct ExactInputSingleParams {
PoolKey poolKey;
bool zeroForOne;
uint128 amountIn;
uint128 amountOutMinimum;
bytes hookData;
}
uint8 private constant CMD_V4_SWAP = 0x10;
uint8 private constant A_SWAP_EXACT_IN_SINGLE = 0x06;
uint8 private constant A_SETTLE_ALL = 0x0c;
uint8 private constant A_TAKE = 0x0e;
event Swapped(address indexed user, address indexed router, address indexed tokenIn, address tokenOut, uint256 amountInTotal, uint256 feeAmount, uint256 amountOut);
event FeeUpdated(uint16 feeBps, address feeRecipient);
event RouterSet(address router, bool approved);
event OwnerChanged(address newOwner);
modifier onlyOwner() { require(msg.sender == owner, "not owner"); _; }
constructor(address _feeRecipient, uint16 _feeBps, address _initialRouter) {
require(_feeRecipient != address(0), "recipient=0");
require(_feeBps <= MAX_FEE_BPS, "fee too high");
owner = msg.sender;
feeRecipient = _feeRecipient;
feeBps = _feeBps;
if (_initialRouter != address(0)) {
approvedRouter[_initialRouter] = true;
emit RouterSet(_initialRouter, true);
}
emit OwnerChanged(msg.sender);
emit FeeUpdated(_feeBps, _feeRecipient);
}
function swapExactInput(
address router,
address tokenIn,
address tokenOut,
uint24 poolFee,
uint256 amountIn,
uint256 amountOutMinimum
) external returns (uint256 amountOut) {
require(approvedRouter[router], "router not approved");
require(amountIn > 0, "amountIn=0");
_safeTransferFrom(tokenIn, msg.sender, address(this), amountIn);
uint256 fee = (amountIn * feeBps) / 10000;
if (fee > 0) _safeTransfer(tokenIn, feeRecipient, fee);
uint256 swapAmount = amountIn - fee;
_safeApprove(tokenIn, router, 0);
_safeApprove(tokenIn, router, swapAmount);
bytes memory path = abi.encodePacked(tokenIn, poolFee, tokenOut);
amountOut = IV3SwapRouter(router).exactInput(
IV3SwapRouter.ExactInputParams({
path: path,
recipient: msg.sender,
amountIn: swapAmount,
amountOutMinimum: amountOutMinimum
})
);
emit Swapped(msg.sender, router, tokenIn, tokenOut, amountIn, fee, amountOut);
}
function swapExactInputPath(
address router,
bytes calldata path,
uint256 amountIn,
uint256 amountOutMinimum
) external returns (uint256 amountOut) {
require(approvedRouter[router], "router not approved");
require(amountIn > 0, "amountIn=0");
require(path.length >= 43, "path too short");
address tokenIn = address(bytes20(path[0:20]));
_safeTransferFrom(tokenIn, msg.sender, address(this), amountIn);
uint256 fee = (amountIn * feeBps) / 10000;
if (fee > 0) _safeTransfer(tokenIn, feeRecipient, fee);
uint256 swapAmount = amountIn - fee;
_safeApprove(tokenIn, router, 0);
_safeApprove(tokenIn, router, swapAmount);
amountOut = IV3SwapRouter(router).exactInput(
IV3SwapRouter.ExactInputParams({
path: path,
recipient: msg.sender,
amountIn: swapAmount,
amountOutMinimum: amountOutMinimum
})
);
emit Swapped(msg.sender, router, tokenIn, address(0), amountIn, fee, amountOut);
}
function swapExactInputV4(
address router,
address tokenIn,
address tokenOut,
uint24 poolFee,
int24 tickSpacing,
address hooks,
uint256 amountIn,
uint256 amountOutMinimum
) external returns (uint256 amountOut) {
require(approvedRouter[router], "router not approved");
require(amountIn > 0, "amountIn=0");
require(tokenIn != tokenOut, "same token");
_safeTransferFrom(tokenIn, msg.sender, address(this), amountIn);
uint256 swapAmount;
{
uint256 fee = (amountIn * feeBps) / 10000;
if (fee > 0) _safeTransfer(tokenIn, feeRecipient, fee);
swapAmount = amountIn - fee;
}
require(swapAmount <= type(uint128).max, "amount too big");
_ensurePermit2(tokenIn, router, swapAmount);
uint256 balBefore = IERC20(tokenOut).balanceOf(msg.sender);
_execV4(router, tokenIn, tokenOut, poolFee, tickSpacing, hooks, uint128(swapAmount), uint128(amountOutMinimum));
amountOut = IERC20(tokenOut).balanceOf(msg.sender) - balBefore;
require(amountOut >= amountOutMinimum, "slippage");
emit Swapped(msg.sender, router, tokenIn, tokenOut, amountIn, amountIn - swapAmount, amountOut);
}
function _execV4(
address router,
address tokenIn,
address tokenOut,
uint24 poolFee,
int24 tickSpacing,
address hooks,
uint128 swapAmount,
uint128 minOut
) internal {
bytes[] memory inputs = new bytes[](1);
inputs[0] = abi.encode(
abi.encodePacked(A_SWAP_EXACT_IN_SINGLE, A_SETTLE_ALL, A_TAKE),
_v4Params(tokenIn, tokenOut, poolFee, tickSpacing, hooks, swapAmount, minOut)
);
IUniversalRouter(router).execute(abi.encodePacked(CMD_V4_SWAP), inputs, block.timestamp);
}
function _v4Params(
address tokenIn,
address tokenOut,
uint24 poolFee,
int24 tickSpacing,
address hooks,
uint128 swapAmount,
uint128 minOut
) internal view returns (bytes[] memory params) {
bool zeroForOne = tokenIn < tokenOut;
params = new bytes[](3);
params[0] = abi.encode(ExactInputSingleParams({
poolKey: _poolKey(tokenIn, tokenOut, poolFee, tickSpacing, hooks),
zeroForOne: zeroForOne,
amountIn: swapAmount,
amountOutMinimum: minOut,
hookData: ""
}));
params[1] = abi.encode(tokenIn, uint256(swapAmount));
params[2] = abi.encode(tokenOut, msg.sender, uint256(0));
}
function _poolKey(
address tokenIn,
address tokenOut,
uint24 poolFee,
int24 tickSpacing,
address hooks
) internal pure returns (PoolKey memory) {
(address c0, address c1) = tokenIn < tokenOut ? (tokenIn, tokenOut) : (tokenOut, tokenIn);
return PoolKey(c0, c1, poolFee, tickSpacing, hooks);
}
function _ensurePermit2(address token, address router, uint256 needed) internal {
if (IERC20(token).allowance(address(this), PERMIT2) < needed) {
_safeApprove(token, PERMIT2, 0);
_safeApprove(token, PERMIT2, type(uint256).max);
}
(uint160 p2Amt, uint48 p2Exp,) = IPermit2(PERMIT2).allowance(address(this), token, router);
if (p2Amt < needed || p2Exp <= block.timestamp) {
IPermit2(PERMIT2).approve(token, router, type(uint160).max, type(uint48).max);
}
}
function setFee(uint16 _feeBps) external onlyOwner {
require(_feeBps <= MAX_FEE_BPS, "fee too high");
feeBps = _feeBps;
emit FeeUpdated(_feeBps, feeRecipient);
}
function setFeeRecipient(address _recipient) external onlyOwner {
require(_recipient != address(0), "recipient=0");
feeRecipient = _recipient;
emit FeeUpdated(feeBps, _recipient);
}
function setRouter(address router, bool approved) external onlyOwner {
require(router != address(0), "router=0");
approvedRouter[router] = approved;
emit RouterSet(router, approved);
}
function transferOwnership(address newOwner) external onlyOwner {
require(newOwner != address(0), "owner=0");
owner = newOwner;
emit OwnerChanged(newOwner);
}
function rescue(address token, address to, uint256 amount) external onlyOwner {
_safeTransfer(token, to, amount);
}
function _safeTransfer(address token, address to, uint256 amount) internal {
(bool ok, bytes memory data) = token.call(abi.encodeWithSelector(IERC20.transfer.selector, to, amount));
require(ok && (data.length == 0 || abi.decode(data, (bool))), "transfer failed");
}
function _safeTransferFrom(address token, address from, address to, uint256 amount) internal {
(bool ok, bytes memory data) = token.call(abi.encodeWithSelector(IERC20.transferFrom.selector, from, to, amount));
require(ok && (data.length == 0 || abi.decode(data, (bool))), "transferFrom failed");
}
function _safeApprove(address token, address spender, uint256 amount) internal {
(bool ok, bytes memory data) = token.call(abi.encodeWithSelector(IERC20.approve.selector, spender, amount));
require(ok && (data.length == 0 || abi.decode(data, (bool))), "approve failed");
}
}
FeeSwapRouter V4 · FeeSwapRouterV4Direct.sol
pragma solidity ^0.8.20;
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
function approve(address spender, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
}
interface IV3SwapRouter {
struct ExactInputParams {
bytes path;
address recipient;
uint256 amountIn;
uint256 amountOutMinimum;
}
function exactInput(ExactInputParams calldata params)
external payable returns (uint256 amountOut);
}
interface IUniversalRouter {
function execute(bytes calldata commands, bytes[] calldata inputs, uint256 deadline)
external payable;
}
interface IPoolManager {
function unlock(bytes calldata data) external returns (bytes memory);
function swap(
FeeSwapRouter.PoolKey calldata key,
FeeSwapRouter.SwapParams calldata params,
bytes calldata hookData
) external returns (int256 swapDelta);
function sync(address currency) external;
function settle() external payable returns (uint256 paid);
function take(address currency, address to, uint256 amount) external;
}
interface IPermit2 {
function approve(address token, address spender, uint160 amount, uint48 expiration) external;
function allowance(address user, address token, address spender)
external view returns (uint160 amount, uint48 expiration, uint48 nonce);
}
contract FeeSwapRouter {
address public owner;
uint16 public feeBps;
address public feeRecipient;
uint16 public constant MAX_FEE_BPS = 100;
mapping(address => bool) public approvedRouter;
address public constant PERMIT2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3;
address public poolManager = 0x8366a39CC670B4001A1121B8F6A443A643e40951;
struct PoolKey {
address currency0;
address currency1;
uint24 fee;
int24 tickSpacing;
address hooks;
}
struct ExactInputSingleParams {
PoolKey poolKey;
bool zeroForOne;
uint128 amountIn;
uint128 amountOutMinimum;
bytes hookData;
}
struct SwapParams {
bool zeroForOne;
int256 amountSpecified;
uint160 sqrtPriceLimitX96;
}
struct V4CallbackData {
address user;
address tokenIn;
address tokenOut;
uint128 amountIn;
uint128 amountOutMinimum;
PoolKey key;
bool zeroForOne;
}
uint8 private constant CMD_V4_SWAP = 0x10;
uint8 private constant A_SWAP_EXACT_IN_SINGLE = 0x06;
uint8 private constant A_SETTLE_ALL = 0x0c;
uint8 private constant A_TAKE = 0x0e;
uint160 private constant MIN_SQRT_PRICE = 4295128739 + 1;
uint160 private constant MAX_SQRT_PRICE = 1461446703485210103287273052203988822378723970342 - 1;
event Swapped(address indexed user, address indexed router, address indexed tokenIn, address tokenOut, uint256 amountInTotal, uint256 feeAmount, uint256 amountOut);
event FeeUpdated(uint16 feeBps, address feeRecipient);
event RouterSet(address router, bool approved);
event OwnerChanged(address newOwner);
modifier onlyOwner() { require(msg.sender == owner, "not owner"); _; }
constructor(address _feeRecipient, uint16 _feeBps, address _initialRouter) {
require(_feeRecipient != address(0), "recipient=0");
require(_feeBps <= MAX_FEE_BPS, "fee too high");
owner = msg.sender;
feeRecipient = _feeRecipient;
feeBps = _feeBps;
if (_initialRouter != address(0)) {
approvedRouter[_initialRouter] = true;
emit RouterSet(_initialRouter, true);
}
emit OwnerChanged(msg.sender);
emit FeeUpdated(_feeBps, _feeRecipient);
}
function swapExactInput(
address router,
address tokenIn,
address tokenOut,
uint24 poolFee,
uint256 amountIn,
uint256 amountOutMinimum
) external returns (uint256 amountOut) {
require(approvedRouter[router], "router not approved");
require(amountIn > 0, "amountIn=0");
_safeTransferFrom(tokenIn, msg.sender, address(this), amountIn);
uint256 fee = (amountIn * feeBps) / 10000;
if (fee > 0) _safeTransfer(tokenIn, feeRecipient, fee);
uint256 swapAmount = amountIn - fee;
_safeApprove(tokenIn, router, 0);
_safeApprove(tokenIn, router, swapAmount);
bytes memory path = abi.encodePacked(tokenIn, poolFee, tokenOut);
amountOut = IV3SwapRouter(router).exactInput(
IV3SwapRouter.ExactInputParams({
path: path,
recipient: msg.sender,
amountIn: swapAmount,
amountOutMinimum: amountOutMinimum
})
);
emit Swapped(msg.sender, router, tokenIn, tokenOut, amountIn, fee, amountOut);
}
function swapExactInputV4(
address router,
address tokenIn,
address tokenOut,
uint24 poolFee,
int24 tickSpacing,
address hooks,
uint256 amountIn,
uint256 amountOutMinimum
) external returns (uint256 amountOut) {
require(amountIn > 0, "amountIn=0");
require(tokenIn != tokenOut, "same token");
_safeTransferFrom(tokenIn, msg.sender, address(this), amountIn);
uint256 swapAmount;
{
uint256 fee = (amountIn * feeBps) / 10000;
if (fee > 0) _safeTransfer(tokenIn, feeRecipient, fee);
swapAmount = amountIn - fee;
}
require(swapAmount <= type(uint128).max, "amount too big");
PoolKey memory key = _poolKey(tokenIn, tokenOut, poolFee, tickSpacing, hooks);
bool zeroForOne = tokenIn < tokenOut;
uint256 balBefore = IERC20(tokenOut).balanceOf(msg.sender);
IPoolManager(poolManager).unlock(abi.encode(V4CallbackData({
user: msg.sender,
tokenIn: tokenIn,
tokenOut: tokenOut,
amountIn: uint128(swapAmount),
amountOutMinimum: uint128(amountOutMinimum),
key: key,
zeroForOne: zeroForOne
})));
amountOut = IERC20(tokenOut).balanceOf(msg.sender) - balBefore;
require(amountOut >= amountOutMinimum, "slippage");
emit Swapped(msg.sender, router, tokenIn, tokenOut, amountIn, amountIn - swapAmount, amountOut);
}
function unlockCallback(bytes calldata rawData) external returns (bytes memory) {
require(msg.sender == poolManager, "not pool manager");
V4CallbackData memory d = abi.decode(rawData, (V4CallbackData));
int256 delta = IPoolManager(poolManager).swap(
d.key,
SwapParams({
zeroForOne: d.zeroForOne,
amountSpecified: -int256(uint256(d.amountIn)),
sqrtPriceLimitX96: d.zeroForOne ? MIN_SQRT_PRICE : MAX_SQRT_PRICE
}),
""
);
int128 amount0 = int128(delta >> 128);
int128 amount1 = int128(delta);
int128 outDelta = d.zeroForOne ? amount1 : amount0;
require(outDelta > 0, "no output");
uint256 outAmount = uint256(uint128(outDelta));
IPoolManager(poolManager).sync(d.tokenIn);
_safeTransfer(d.tokenIn, poolManager, d.amountIn);
IPoolManager(poolManager).settle();
IPoolManager(poolManager).take(d.tokenOut, d.user, outAmount);
return "";
}
function _poolKey(
address tokenIn,
address tokenOut,
uint24 poolFee,
int24 tickSpacing,
address hooks
) internal pure returns (PoolKey memory) {
(address c0, address c1) = tokenIn < tokenOut ? (tokenIn, tokenOut) : (tokenOut, tokenIn);
return PoolKey(c0, c1, poolFee, tickSpacing, hooks);
}
function _ensurePermit2(address token, address router, uint256 needed) internal {
if (IERC20(token).allowance(address(this), PERMIT2) < needed) {
_safeApprove(token, PERMIT2, 0);
_safeApprove(token, PERMIT2, type(uint256).max);
}
(uint160 p2Amt, uint48 p2Exp,) = IPermit2(PERMIT2).allowance(address(this), token, router);
if (p2Amt < needed || p2Exp <= block.timestamp) {
IPermit2(PERMIT2).approve(token, router, type(uint160).max, type(uint48).max);
}
}
function setFee(uint16 _feeBps) external onlyOwner {
require(_feeBps <= MAX_FEE_BPS, "fee too high");
feeBps = _feeBps;
emit FeeUpdated(_feeBps, feeRecipient);
}
function setFeeRecipient(address _recipient) external onlyOwner {
require(_recipient != address(0), "recipient=0");
feeRecipient = _recipient;
emit FeeUpdated(feeBps, _recipient);
}
function setPoolManager(address pm) external onlyOwner {
require(pm != address(0), "pm=0");
poolManager = pm;
}
function setRouter(address router, bool approved) external onlyOwner {
require(router != address(0), "router=0");
approvedRouter[router] = approved;
emit RouterSet(router, approved);
}
function transferOwnership(address newOwner) external onlyOwner {
require(newOwner != address(0), "owner=0");
owner = newOwner;
emit OwnerChanged(newOwner);
}
function rescue(address token, address to, uint256 amount) external onlyOwner {
_safeTransfer(token, to, amount);
}
function _safeTransfer(address token, address to, uint256 amount) internal {
(bool ok, bytes memory data) = token.call(abi.encodeWithSelector(IERC20.transfer.selector, to, amount));
require(ok && (data.length == 0 || abi.decode(data, (bool))), "transfer failed");
}
function _safeTransferFrom(address token, address from, address to, uint256 amount) internal {
(bool ok, bytes memory data) = token.call(abi.encodeWithSelector(IERC20.transferFrom.selector, from, to, amount));
require(ok && (data.length == 0 || abi.decode(data, (bool))), "transferFrom failed");
}
function _safeApprove(address token, address spender, uint256 amount) internal {
(bool ok, bytes memory data) = token.call(abi.encodeWithSelector(IERC20.approve.selector, spender, amount));
require(ok && (data.length == 0 || abi.decode(data, (bool))), "approve failed");
}
}
FeeSwapRouter V4 Native / Multi · FeeSwapRouterV4-multihop.sol
pragma solidity ^0.8.20;
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
function approve(address spender, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
}
interface IV3SwapRouter {
struct ExactInputParams {
bytes path;
address recipient;
uint256 amountIn;
uint256 amountOutMinimum;
}
function exactInput(ExactInputParams calldata params)
external payable returns (uint256 amountOut);
}
interface IUniversalRouter {
function execute(bytes calldata commands, bytes[] calldata inputs, uint256 deadline)
external payable;
}
interface IPoolManager {
function unlock(bytes calldata data) external returns (bytes memory);
function swap(
FeeSwapRouter.PoolKey calldata key,
FeeSwapRouter.SwapParams calldata params,
bytes calldata hookData
) external returns (int256 swapDelta);
function sync(address currency) external;
function settle() external payable returns (uint256 paid);
function take(address currency, address to, uint256 amount) external;
}
interface IPermit2 {
function approve(address token, address spender, uint160 amount, uint48 expiration) external;
function allowance(address user, address token, address spender)
external view returns (uint160 amount, uint48 expiration, uint48 nonce);
}
contract FeeSwapRouter {
address public owner;
uint16 public feeBps;
address public feeRecipient;
uint16 public constant MAX_FEE_BPS = 100;
mapping(address => bool) public approvedRouter;
address public constant PERMIT2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3;
address public poolManager = 0x8366a39CC670B4001A1121B8F6A443A643e40951;
struct PoolKey {
address currency0;
address currency1;
uint24 fee;
int24 tickSpacing;
address hooks;
}
struct ExactInputSingleParams {
PoolKey poolKey;
bool zeroForOne;
uint128 amountIn;
uint128 amountOutMinimum;
bytes hookData;
}
struct SwapParams {
bool zeroForOne;
int256 amountSpecified;
uint160 sqrtPriceLimitX96;
}
struct V4CallbackData {
address user;
address tokenIn;
address tokenOut;
uint128 amountIn;
uint128 amountOutMinimum;
PoolKey key;
bool zeroForOne;
}
struct V4MultihopData {
address user;
address tokenIn;
address hub;
address tokenOut;
uint128 amountIn;
uint128 amountOutMinimum;
PoolKey key1;
bool zeroForOne1;
PoolKey key2;
bool zeroForOne2;
bool isMultihop;
}
uint256 private constant _MULTIHOP_MIN_LEN = 500;
uint8 private constant CMD_V4_SWAP = 0x10;
uint8 private constant A_SWAP_EXACT_IN_SINGLE = 0x06;
uint8 private constant A_SETTLE_ALL = 0x0c;
uint8 private constant A_TAKE = 0x0e;
uint160 private constant MIN_SQRT_PRICE = 4295128739 + 1;
uint160 private constant MAX_SQRT_PRICE = 1461446703485210103287273052203988822378723970342 - 1;
event Swapped(address indexed user, address indexed router, address indexed tokenIn, address tokenOut, uint256 amountInTotal, uint256 feeAmount, uint256 amountOut);
event FeeUpdated(uint16 feeBps, address feeRecipient);
event RouterSet(address router, bool approved);
event OwnerChanged(address newOwner);
modifier onlyOwner() { require(msg.sender == owner, "not owner"); _; }
constructor(address _feeRecipient, uint16 _feeBps, address _initialRouter) {
require(_feeRecipient != address(0), "recipient=0");
require(_feeBps <= MAX_FEE_BPS, "fee too high");
owner = msg.sender;
feeRecipient = _feeRecipient;
feeBps = _feeBps;
if (_initialRouter != address(0)) {
approvedRouter[_initialRouter] = true;
emit RouterSet(_initialRouter, true);
}
emit OwnerChanged(msg.sender);
emit FeeUpdated(_feeBps, _feeRecipient);
}
function swapExactInput(
address router,
address tokenIn,
address tokenOut,
uint24 poolFee,
uint256 amountIn,
uint256 amountOutMinimum
) external returns (uint256 amountOut) {
require(approvedRouter[router], "router not approved");
require(amountIn > 0, "amountIn=0");
_safeTransferFrom(tokenIn, msg.sender, address(this), amountIn);
uint256 fee = (amountIn * feeBps) / 10000;
if (fee > 0) _safeTransfer(tokenIn, feeRecipient, fee);
uint256 swapAmount = amountIn - fee;
_safeApprove(tokenIn, router, 0);
_safeApprove(tokenIn, router, swapAmount);
bytes memory path = abi.encodePacked(tokenIn, poolFee, tokenOut);
amountOut = IV3SwapRouter(router).exactInput(
IV3SwapRouter.ExactInputParams({
path: path,
recipient: msg.sender,
amountIn: swapAmount,
amountOutMinimum: amountOutMinimum
})
);
emit Swapped(msg.sender, router, tokenIn, tokenOut, amountIn, fee, amountOut);
}
function swapExactInputV4(
address router,
address tokenIn,
address tokenOut,
uint24 poolFee,
int24 tickSpacing,
address hooks,
uint256 amountIn,
uint256 amountOutMinimum
) external returns (uint256 amountOut) {
require(amountIn > 0, "amountIn=0");
require(tokenIn != tokenOut, "same token");
_safeTransferFrom(tokenIn, msg.sender, address(this), amountIn);
uint256 swapAmount;
{
uint256 fee = (amountIn * feeBps) / 10000;
if (fee > 0) _safeTransfer(tokenIn, feeRecipient, fee);
swapAmount = amountIn - fee;
}
require(swapAmount <= type(uint128).max, "amount too big");
PoolKey memory key = _poolKey(tokenIn, tokenOut, poolFee, tickSpacing, hooks);
bool zeroForOne = tokenIn < tokenOut;
uint256 balBefore = IERC20(tokenOut).balanceOf(msg.sender);
IPoolManager(poolManager).unlock(abi.encode(V4CallbackData({
user: msg.sender,
tokenIn: tokenIn,
tokenOut: tokenOut,
amountIn: uint128(swapAmount),
amountOutMinimum: uint128(amountOutMinimum),
key: key,
zeroForOne: zeroForOne
})));
amountOut = IERC20(tokenOut).balanceOf(msg.sender) - balBefore;
require(amountOut >= amountOutMinimum, "slippage");
emit Swapped(msg.sender, router, tokenIn, tokenOut, amountIn, amountIn - swapAmount, amountOut);
}
function swapNativeForToken(
address token,
uint24 poolFee,
int24 tickSpacing,
address hooks,
uint256 amountOutMinimum
) external payable returns (uint256 amountOut) {
require(msg.value > 0, "value=0");
require(token != address(0), "token=0");
uint256 fee = (msg.value * feeBps) / 10000;
if (fee > 0) _sendNative(feeRecipient, fee);
uint256 swapAmount = msg.value - fee;
require(swapAmount <= type(uint128).max, "amount too big");
bytes memory res = IPoolManager(poolManager).unlock(abi.encode(V4CallbackData({
user: msg.sender,
tokenIn: address(0),
tokenOut: token,
amountIn: uint128(swapAmount),
amountOutMinimum: uint128(amountOutMinimum),
key: PoolKey(address(0), token, poolFee, tickSpacing, hooks),
zeroForOne: true
})));
amountOut = abi.decode(res, (uint256));
require(amountOut >= amountOutMinimum, "slippage");
emit Swapped(msg.sender, poolManager, address(0), token, msg.value, fee, amountOut);
}
function swapTokenForNative(
address token,
uint24 poolFee,
int24 tickSpacing,
address hooks,
uint256 amountIn,
uint256 amountOutMinimum
) external returns (uint256 amountOut) {
require(amountIn > 0, "amountIn=0");
require(token != address(0), "token=0");
_safeTransferFrom(token, msg.sender, address(this), amountIn);
uint256 fee = (amountIn * feeBps) / 10000;
if (fee > 0) _safeTransfer(token, feeRecipient, fee);
uint256 swapAmount = amountIn - fee;
require(swapAmount <= type(uint128).max, "amount too big");
bytes memory res = IPoolManager(poolManager).unlock(abi.encode(V4CallbackData({
user: msg.sender,
tokenIn: token,
tokenOut: address(0),
amountIn: uint128(swapAmount),
amountOutMinimum: uint128(amountOutMinimum),
key: PoolKey(address(0), token, poolFee, tickSpacing, hooks),
zeroForOne: false
})));
amountOut = abi.decode(res, (uint256));
require(amountOut >= amountOutMinimum, "slippage");
emit Swapped(msg.sender, poolManager, token, address(0), amountIn, fee, amountOut);
}
function swapV4Multihop(
address tokenIn,
address hub,
address tokenOut,
uint256 amountIn,
uint256 amountOutMinimum,
PoolKey calldata key1, bool zeroForOne1,
PoolKey calldata key2, bool zeroForOne2
) external payable returns (uint256 amountOut) {
uint128 inAmt = _takeInput(tokenIn, amountIn);
bytes memory res = IPoolManager(poolManager).unlock(abi.encode(V4MultihopData({
user: msg.sender,
tokenIn: tokenIn,
hub: hub,
tokenOut: tokenOut,
amountIn: inAmt,
amountOutMinimum: uint128(amountOutMinimum),
key1: key1, zeroForOne1: zeroForOne1,
key2: key2, zeroForOne2: zeroForOne2,
isMultihop: true
})));
amountOut = abi.decode(res, (uint256));
require(amountOut >= amountOutMinimum, "slippage");
emit Swapped(msg.sender, poolManager, tokenIn, tokenOut, inAmt, 0, amountOut);
}
function _takeInput(address tokenIn, uint256 amountIn) private returns (uint128) {
uint256 inAmt;
if (tokenIn == address(0)) {
require(msg.value > 0, "value=0");
uint256 f = (msg.value * feeBps) / 10000;
if (f > 0) _sendNative(feeRecipient, f);
inAmt = msg.value - f;
} else {
require(amountIn > 0, "amountIn=0");
_safeTransferFrom(tokenIn, msg.sender, address(this), amountIn);
uint256 f = (amountIn * feeBps) / 10000;
if (f > 0) _safeTransfer(tokenIn, feeRecipient, f);
inAmt = amountIn - f;
}
require(inAmt <= type(uint128).max, "amount too big");
return uint128(inAmt);
}
function unlockCallback(bytes calldata rawData) external returns (bytes memory) {
require(msg.sender == poolManager, "not pool manager");
if (rawData.length >= _MULTIHOP_MIN_LEN) return _multihopCallback(rawData);
V4CallbackData memory d = abi.decode(rawData, (V4CallbackData));
int256 delta = IPoolManager(poolManager).swap(
d.key,
SwapParams({
zeroForOne: d.zeroForOne,
amountSpecified: -int256(uint256(d.amountIn)),
sqrtPriceLimitX96: d.zeroForOne ? MIN_SQRT_PRICE : MAX_SQRT_PRICE
}),
""
);
int128 amount0 = int128(delta >> 128);
int128 amount1 = int128(delta);
int128 outDelta = d.zeroForOne ? amount1 : amount0;
require(outDelta > 0, "no output");
uint256 outAmount = uint256(uint128(outDelta));
if (d.tokenIn == address(0)) {
IPoolManager(poolManager).settle{value: d.amountIn}();
} else {
IPoolManager(poolManager).sync(d.tokenIn);
_safeTransfer(d.tokenIn, poolManager, d.amountIn);
IPoolManager(poolManager).settle();
}
IPoolManager(poolManager).take(d.tokenOut, d.user, outAmount);
return abi.encode(outAmount);
}
function _multihopCallback(bytes calldata raw) private returns (bytes memory) {
V4MultihopData memory d = abi.decode(raw, (V4MultihopData));
uint128 hubAmt = _swapOut(d.key1, d.zeroForOne1, d.amountIn);
if (d.tokenIn == address(0)) {
IPoolManager(poolManager).settle{value: d.amountIn}();
} else {
IPoolManager(poolManager).sync(d.tokenIn);
_safeTransfer(d.tokenIn, poolManager, d.amountIn);
IPoolManager(poolManager).settle();
}
IPoolManager(poolManager).take(d.hub, address(this), hubAmt);
uint128 outAmt = _swapOut(d.key2, d.zeroForOne2, hubAmt);
IPoolManager(poolManager).sync(d.hub);
_safeTransfer(d.hub, poolManager, hubAmt);
IPoolManager(poolManager).settle();
IPoolManager(poolManager).take(d.tokenOut, d.user, outAmt);
return abi.encode(uint256(outAmt));
}
function _swapOut(PoolKey memory key, bool zeroForOne, uint128 amtIn) private returns (uint128) {
int256 delta = IPoolManager(poolManager).swap(
key,
SwapParams({ zeroForOne: zeroForOne, amountSpecified: -int256(uint256(amtIn)),
sqrtPriceLimitX96: zeroForOne ? MIN_SQRT_PRICE : MAX_SQRT_PRICE }),
"");
int128 out = zeroForOne ? int128(delta) : int128(delta >> 128);
require(out > 0, "no output");
return uint128(out);
}
function _sendNative(address to, uint256 amount) private {
(bool ok, ) = to.call{value: amount}("");
require(ok, "native send");
}
receive() external payable {}
function _poolKey(
address tokenIn,
address tokenOut,
uint24 poolFee,
int24 tickSpacing,
address hooks
) internal pure returns (PoolKey memory) {
(address c0, address c1) = tokenIn < tokenOut ? (tokenIn, tokenOut) : (tokenOut, tokenIn);
return PoolKey(c0, c1, poolFee, tickSpacing, hooks);
}
function _ensurePermit2(address token, address router, uint256 needed) internal {
if (IERC20(token).allowance(address(this), PERMIT2) < needed) {
_safeApprove(token, PERMIT2, 0);
_safeApprove(token, PERMIT2, type(uint256).max);
}
(uint160 p2Amt, uint48 p2Exp,) = IPermit2(PERMIT2).allowance(address(this), token, router);
if (p2Amt < needed || p2Exp <= block.timestamp) {
IPermit2(PERMIT2).approve(token, router, type(uint160).max, type(uint48).max);
}
}
function setFee(uint16 _feeBps) external onlyOwner {
require(_feeBps <= MAX_FEE_BPS, "fee too high");
feeBps = _feeBps;
emit FeeUpdated(_feeBps, feeRecipient);
}
function setFeeRecipient(address _recipient) external onlyOwner {
require(_recipient != address(0), "recipient=0");
feeRecipient = _recipient;
emit FeeUpdated(feeBps, _recipient);
}
function setPoolManager(address pm) external onlyOwner {
require(pm != address(0), "pm=0");
poolManager = pm;
}
function setRouter(address router, bool approved) external onlyOwner {
require(router != address(0), "router=0");
approvedRouter[router] = approved;
emit RouterSet(router, approved);
}
function transferOwnership(address newOwner) external onlyOwner {
require(newOwner != address(0), "owner=0");
owner = newOwner;
emit OwnerChanged(newOwner);
}
function rescue(address token, address to, uint256 amount) external onlyOwner {
_safeTransfer(token, to, amount);
}
function _safeTransfer(address token, address to, uint256 amount) internal {
(bool ok, bytes memory data) = token.call(abi.encodeWithSelector(IERC20.transfer.selector, to, amount));
require(ok && (data.length == 0 || abi.decode(data, (bool))), "transfer failed");
}
function _safeTransferFrom(address token, address from, address to, uint256 amount) internal {
(bool ok, bytes memory data) = token.call(abi.encodeWithSelector(IERC20.transferFrom.selector, from, to, amount));
require(ok && (data.length == 0 || abi.decode(data, (bool))), "transferFrom failed");
}
function _safeApprove(address token, address spender, uint256 amount) internal {
(bool ok, bytes memory data) = token.call(abi.encodeWithSelector(IERC20.approve.selector, spender, amount));
require(ok && (data.length == 0 || abi.decode(data, (bool))), "approve failed");
}
}
Curve Fee Router · FeeSwapRouterCurve.sol
pragma solidity ^0.8.20;
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
function approve(address spender, uint256 amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
}
interface ICurveFactory {
function curveOf(address token) external view returns (address);
}
interface ICurveNative {
function buyFor(address recipient, uint256 minTokensOut) external payable returns (uint256);
function sell(uint256 tokenAmount, uint256 minUsdcOut) external returns (uint256);
}
interface ICurveErc20 {
function buyExactIn(uint256 usdcIn, uint256 minimumTokensOut, address recipient, uint256 deadline) external returns (uint256);
function sellExactIn(uint256 tokensIn, uint256 minimumUsdcOut, address recipient, uint256 deadline) external returns (uint256);
}
contract FeeSwapRouterCurve {
address public owner;
uint16 public feeBps;
address public feeRecipient;
uint16 public constant MAX_FEE_BPS = 100;
address public immutable usdc;
mapping(address => bool) public approvedFactory;
mapping(address => bool) public approvedCurve;
event Swapped(address indexed user, address indexed router, address indexed tokenIn, address tokenOut, uint256 amountInTotal, uint256 feeAmount, uint256 amountOut);
event FeeUpdated(uint16 feeBps, address feeRecipient);
event FactorySet(address factory, bool approved);
event CurveSet(address curve, bool approved);
event OwnerChanged(address newOwner);
modifier onlyOwner() { require(msg.sender == owner, "not owner"); _; }
uint256 private _lock = 1;
modifier nonReentrant() { require(_lock == 1, "reentrant"); _lock = 2; _; _lock = 1; }
constructor(address _usdc, address _feeRecipient, uint16 _feeBps, address _initialFactory) {
require(_usdc != address(0), "usdc=0");
require(_feeRecipient != address(0), "recipient=0");
require(_feeBps <= MAX_FEE_BPS, "fee too high");
owner = msg.sender;
usdc = _usdc;
feeRecipient = _feeRecipient;
feeBps = _feeBps;
if (_initialFactory != address(0)) {
approvedFactory[_initialFactory] = true;
emit FactorySet(_initialFactory, true);
}
emit OwnerChanged(msg.sender);
emit FeeUpdated(_feeBps, _feeRecipient);
}
receive() external payable {}
function _checkCurve(address factory, address token, address curve) internal view {
if (approvedCurve[curve]) return;
require(approvedFactory[factory], "factory not approved");
require(ICurveFactory(factory).curveOf(token) == curve, "curve mismatch");
}
function buyCurveNative(
address factory,
address token,
address curve,
uint256 minTokensOut
) external payable nonReentrant returns (uint256 amountOut) {
require(msg.value > 0, "value=0");
_checkCurve(factory, token, curve);
uint256 fee = (msg.value * feeBps) / 10000;
if (fee > 0) _sendNative(feeRecipient, fee);
amountOut = ICurveNative(curve).buyFor{value: msg.value - fee}(msg.sender, minTokensOut);
require(amountOut >= minTokensOut, "slippage");
emit Swapped(msg.sender, curve, address(0), token, msg.value, fee, amountOut);
}
function sellCurveNative(
address factory,
address token,
address curve,
uint256 tokenAmount,
uint256 minUsdcOut
) external nonReentrant returns (uint256 userAmount) {
require(tokenAmount > 0, "amount=0");
_checkCurve(factory, token, curve);
_safeTransferFrom(token, msg.sender, address(this), tokenAmount);
_safeApprove(token, curve, 0);
_safeApprove(token, curve, tokenAmount);
uint256 balBefore = address(this).balance;
ICurveNative(curve).sell(tokenAmount, minUsdcOut);
uint256 got = address(this).balance - balBefore;
uint256 fee = (got * feeBps) / 10000;
userAmount = got - fee;
require(userAmount >= minUsdcOut, "slippage");
if (fee > 0) _sendNative(feeRecipient, fee);
_sendNative(msg.sender, userAmount);
emit Swapped(msg.sender, curve, token, address(0), tokenAmount, fee, userAmount);
}
function buyCurveErc20(
address factory,
address token,
address curve,
uint256 usdcIn,
uint256 minTokensOut,
uint256 deadline
) external nonReentrant returns (uint256 amountOut) {
require(usdcIn > 0, "amountIn=0");
_checkCurve(factory, token, curve);
_safeTransferFrom(usdc, msg.sender, address(this), usdcIn);
uint256 fee = (usdcIn * feeBps) / 10000;
if (fee > 0) _safeTransfer(usdc, feeRecipient, fee);
uint256 swapAmount = usdcIn - fee;
_safeApprove(usdc, curve, 0);
_safeApprove(usdc, curve, swapAmount);
amountOut = ICurveErc20(curve).buyExactIn(swapAmount, minTokensOut, msg.sender, deadline);
require(amountOut >= minTokensOut, "slippage");
emit Swapped(msg.sender, curve, usdc, token, usdcIn, fee, amountOut);
}
function sellCurveErc20(
address factory,
address token,
address curve,
uint256 tokensIn,
uint256 minUsdcOut,
uint256 deadline
) external nonReentrant returns (uint256 userAmount) {
require(tokensIn > 0, "amount=0");
_checkCurve(factory, token, curve);
_safeTransferFrom(token, msg.sender, address(this), tokensIn);
_safeApprove(token, curve, 0);
_safeApprove(token, curve, tokensIn);
uint256 balBefore = IERC20(usdc).balanceOf(address(this));
ICurveErc20(curve).sellExactIn(tokensIn, minUsdcOut, address(this), deadline);
uint256 got = IERC20(usdc).balanceOf(address(this)) - balBefore;
uint256 fee = (got * feeBps) / 10000;
userAmount = got - fee;
require(userAmount >= minUsdcOut, "slippage");
if (fee > 0) _safeTransfer(usdc, feeRecipient, fee);
_safeTransfer(usdc, msg.sender, userAmount);
emit Swapped(msg.sender, curve, token, usdc, tokensIn, fee, userAmount);
}
function setFee(uint16 _feeBps) external onlyOwner {
require(_feeBps <= MAX_FEE_BPS, "fee too high");
feeBps = _feeBps;
emit FeeUpdated(_feeBps, feeRecipient);
}
function setFeeRecipient(address _recipient) external onlyOwner {
require(_recipient != address(0), "recipient=0");
feeRecipient = _recipient;
emit FeeUpdated(feeBps, _recipient);
}
function setFactory(address factory, bool approved) external onlyOwner {
approvedFactory[factory] = approved;
emit FactorySet(factory, approved);
}
function setCurve(address curve, bool approved) external onlyOwner {
approvedCurve[curve] = approved;
emit CurveSet(curve, approved);
}
function setOwner(address _owner) external onlyOwner {
require(_owner != address(0), "owner=0");
owner = _owner;
emit OwnerChanged(_owner);
}
function rescue(address token, uint256 amount) external onlyOwner {
if (token == address(0)) _sendNative(owner, amount);
else _safeTransfer(token, owner, amount);
}
function _sendNative(address to, uint256 amount) internal {
(bool ok, ) = to.call{value: amount}("");
require(ok, "native send failed");
}
function _safeTransfer(address token, address to, uint256 amount) internal {
(bool ok, bytes memory data) = token.call(abi.encodeWithSelector(IERC20.transfer.selector, to, amount));
require(ok && (data.length == 0 || abi.decode(data, (bool))), "transfer failed");
}
function _safeTransferFrom(address token, address from, address to, uint256 amount) internal {
(bool ok, bytes memory data) = token.call(abi.encodeWithSelector(IERC20.transferFrom.selector, from, to, amount));
require(ok && (data.length == 0 || abi.decode(data, (bool))), "transferFrom failed");
}
function _safeApprove(address token, address spender, uint256 amount) internal {
(bool ok, bytes memory data) = token.call(abi.encodeWithSelector(IERC20.approve.selector, spender, amount));
require(ok && (data.length == 0 || abi.decode(data, (bool))), "approve failed");
}
}
Curve Fee Router (central) · FeeSwapRouterCurveCentral.sol
pragma solidity ^0.8.20;
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
function approve(address spender, uint256 amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
}
interface ICentralCurve {
function buy(address token, uint256 usdcIn, uint256 minTokensOut, uint256 deadline) external returns (uint256);
function sell(address token, uint256 tokensIn, uint256 minUsdcOut, uint256 deadline) external returns (uint256);
}
contract FeeSwapRouterCurveCentral {
address public owner;
uint16 public feeBps;
address public feeRecipient;
uint16 public constant MAX_FEE_BPS = 100;
address public immutable usdc;
mapping(address => bool) public approvedCentral;
event Swapped(address indexed user, address indexed router, address indexed tokenIn, address tokenOut, uint256 amountInTotal, uint256 feeAmount, uint256 amountOut);
event FeeUpdated(uint16 feeBps, address feeRecipient);
event CentralSet(address central, bool approved);
event OwnerChanged(address newOwner);
modifier onlyOwner() { require(msg.sender == owner, "not owner"); _; }
uint256 private _lock = 1;
modifier nonReentrant() { require(_lock == 1, "reentrant"); _lock = 2; _; _lock = 1; }
constructor(address _usdc, address _feeRecipient, uint16 _feeBps, address _initialCentral) {
require(_usdc != address(0), "usdc=0");
require(_feeRecipient != address(0), "recipient=0");
require(_feeBps <= MAX_FEE_BPS, "fee too high");
owner = msg.sender;
usdc = _usdc;
feeRecipient = _feeRecipient;
feeBps = _feeBps;
if (_initialCentral != address(0)) {
approvedCentral[_initialCentral] = true;
emit CentralSet(_initialCentral, true);
}
emit OwnerChanged(msg.sender);
emit FeeUpdated(_feeBps, _feeRecipient);
}
function buyCentral(
address central,
address token,
uint256 usdcIn,
uint256 minTokensOut,
uint256 deadline
) external nonReentrant returns (uint256 amountOut) {
require(approvedCentral[central], "central not approved");
require(usdcIn > 0, "amountIn=0");
_safeTransferFrom(usdc, msg.sender, address(this), usdcIn);
uint256 fee = (usdcIn * feeBps) / 10000;
if (fee > 0) _safeTransfer(usdc, feeRecipient, fee);
uint256 swapAmount = usdcIn - fee;
_safeApprove(usdc, central, 0);
_safeApprove(usdc, central, swapAmount);
uint256 balBefore = IERC20(token).balanceOf(address(this));
ICentralCurve(central).buy(token, swapAmount, minTokensOut, deadline);
amountOut = IERC20(token).balanceOf(address(this)) - balBefore;
require(amountOut >= minTokensOut, "slippage");
_safeTransfer(token, msg.sender, amountOut);
emit Swapped(msg.sender, central, usdc, token, usdcIn, fee, amountOut);
}
function sellCentral(
address central,
address token,
uint256 tokensIn,
uint256 minUsdcOut,
uint256 deadline
) external nonReentrant returns (uint256 userAmount) {
require(approvedCentral[central], "central not approved");
require(tokensIn > 0, "amount=0");
_safeTransferFrom(token, msg.sender, address(this), tokensIn);
_safeApprove(token, central, 0);
_safeApprove(token, central, tokensIn);
uint256 balBefore = IERC20(usdc).balanceOf(address(this));
ICentralCurve(central).sell(token, tokensIn, minUsdcOut, deadline);
uint256 got = IERC20(usdc).balanceOf(address(this)) - balBefore;
uint256 fee = (got * feeBps) / 10000;
userAmount = got - fee;
require(userAmount >= minUsdcOut, "slippage");
if (fee > 0) _safeTransfer(usdc, feeRecipient, fee);
_safeTransfer(usdc, msg.sender, userAmount);
emit Swapped(msg.sender, central, token, usdc, tokensIn, fee, userAmount);
}
function setFee(uint16 _feeBps) external onlyOwner {
require(_feeBps <= MAX_FEE_BPS, "fee too high");
feeBps = _feeBps;
emit FeeUpdated(_feeBps, feeRecipient);
}
function setFeeRecipient(address _recipient) external onlyOwner {
require(_recipient != address(0), "recipient=0");
feeRecipient = _recipient;
emit FeeUpdated(feeBps, _recipient);
}
function setCentral(address central, bool approved) external onlyOwner {
approvedCentral[central] = approved;
emit CentralSet(central, approved);
}
function setOwner(address _owner) external onlyOwner {
require(_owner != address(0), "owner=0");
owner = _owner;
emit OwnerChanged(_owner);
}
function rescue(address token, uint256 amount) external onlyOwner {
_safeTransfer(token, owner, amount);
}
function _safeTransfer(address token, address to, uint256 amount) internal {
(bool ok, bytes memory data) = token.call(abi.encodeWithSelector(IERC20.transfer.selector, to, amount));
require(ok && (data.length == 0 || abi.decode(data, (bool))), "transfer failed");
}
function _safeTransferFrom(address token, address from, address to, uint256 amount) internal {
(bool ok, bytes memory data) = token.call(abi.encodeWithSelector(IERC20.transferFrom.selector, from, to, amount));
require(ok && (data.length == 0 || abi.decode(data, (bool))), "transferFrom failed");
}
function _safeApprove(address token, address spender, uint256 amount) internal {
(bool ok, bytes memory data) = token.call(abi.encodeWithSelector(IERC20.approve.selector, spender, amount));
require(ok && (data.length == 0 || abi.decode(data, (bool))), "approve failed");
}
}
DYOR v1 Operator Router · RadarV1OperatorRouter.sol
pragma solidity ^0.8.20;
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
function balanceOf(address a) external view returns (uint256);
function approve(address s, uint256 a) external returns (bool);
function allowance(address o, address s) external view returns (uint256);
}
contract RadarV1OperatorRouter {
address public immutable USDC;
address public immutable OPERATOR;
uint16 public immutable feeBps;
address public feeRecipient;
address public feeAdmin;
bytes4 private constant OP_SWAP = 0x53a0e063;
event Swapped(address indexed user, address indexed router, address tokenIn,
address tokenOut, uint256 amountInTotal, uint256 feeAmount, uint256 amountOut);
error Expired();
error ZeroAmount();
error NotAdmin();
error OperatorCallFailed();
constructor(address usdc_, address operator_, uint16 feeBps_, address feeRecipient_, address feeAdmin_) {
USDC = usdc_; OPERATOR = operator_; feeBps = feeBps_;
feeRecipient = feeRecipient_; feeAdmin = feeAdmin_;
}
function setFeeRecipient(address r) external {
if (msg.sender != feeAdmin) revert NotAdmin();
feeRecipient = r;
}
function _approveOp(address token) internal {
if (IERC20(token).allowance(address(this), OPERATOR) < type(uint128).max) {
IERC20(token).approve(OPERATOR, type(uint256).max);
}
}
function _opSwap(uint256 amountIn, uint256 minOut, address a, address b, address to, uint256 deadline) internal {
address[] memory path = new address[](2);
path[0] = a; path[1] = b;
(bool ok, ) = OPERATOR.call(abi.encodeWithSelector(
OP_SWAP, amountIn, minOut, path, to, uint256(0), uint256(0), deadline));
if (!ok) revert OperatorCallFailed();
}
function buy(address token, uint256 usdcIn, uint256 minOut, address to, uint256 deadline)
external returns (uint256 out)
{
if (block.timestamp > deadline) revert Expired();
if (usdcIn == 0) revert ZeroAmount();
IERC20(USDC).transferFrom(msg.sender, address(this), usdcIn);
uint256 fee = (usdcIn * feeBps) / 10000;
if (fee > 0) IERC20(USDC).transfer(feeRecipient, fee);
_approveOp(USDC);
uint256 balBefore = IERC20(token).balanceOf(to);
_opSwap(usdcIn - fee, minOut, USDC, token, to, deadline);
out = IERC20(token).balanceOf(to) - balBefore;
emit Swapped(msg.sender, address(this), USDC, token, usdcIn, fee, out);
}
function sell(address token, uint256 tokenIn, uint256 minUsdcOut, address to, uint256 deadline)
external returns (uint256 out)
{
if (block.timestamp > deadline) revert Expired();
if (tokenIn == 0) revert ZeroAmount();
IERC20(token).transferFrom(msg.sender, address(this), tokenIn);
_approveOp(token);
uint256 balBefore = IERC20(USDC).balanceOf(address(this));
uint256 minGross = feeBps < 10000 ? (minUsdcOut * 10000) / (10000 - feeBps) : minUsdcOut;
_opSwap(tokenIn, minGross, token, USDC, address(this), deadline);
uint256 gross = IERC20(USDC).balanceOf(address(this)) - balBefore;
uint256 fee = (gross * feeBps) / 10000;
if (fee > 0) IERC20(USDC).transfer(feeRecipient, fee);
out = gross - fee;
IERC20(USDC).transfer(to, out);
emit Swapped(msg.sender, address(this), token, USDC, tokenIn, fee, out);
}
}
Locker
RadarLocker · RadarLocker.sol
pragma solidity ^0.8.20;
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
}
contract RadarLocker {
struct Lock {
address token;
address beneficiary;
uint128 amount;
uint128 claimed;
uint64 tgeTime;
uint16 tgeBps;
uint64 vestEnd;
bool revokedNever;
string title;
}
Lock[] public locks;
mapping(address => uint256[]) private _byToken;
mapping(address => uint256[]) private _byBeneficiary;
address public feeAdmin;
address public feeRecipient;
uint256 public lockFee;
uint256 public constant MAX_LOCK_FEE = 50e18;
event Locked(uint256 indexed id, address indexed token, address indexed beneficiary,
uint256 amount, uint64 tgeTime, uint16 tgeBps, uint64 vestEnd, string title);
event Claimed(uint256 indexed id, address indexed beneficiary, uint256 amount);
event LockExtended(uint256 indexed id, uint64 newTgeTime, uint64 newVestEnd);
event BeneficiaryTransferred(uint256 indexed id, address indexed from, address indexed to);
event FeeUpdated(uint256 lockFee, address feeRecipient, address feeAdmin);
uint256 private _lockGuard = 1;
modifier nonReentrant() { require(_lockGuard == 1, "reentrant"); _lockGuard = 2; _; _lockGuard = 1; }
constructor(address _feeAdmin, address _feeRecipient, uint256 _lockFee) {
require(_lockFee <= MAX_LOCK_FEE, "fee too high");
feeAdmin = _feeAdmin;
feeRecipient = _feeRecipient == address(0) ? _feeAdmin : _feeRecipient;
lockFee = _lockFee;
emit FeeUpdated(_lockFee, feeRecipient, _feeAdmin);
}
function lock(
address token,
address beneficiary,
uint256 amount,
uint64 tgeTime,
uint16 tgeBps,
uint64 vestEnd,
string calldata title
) external payable nonReentrant returns (uint256 id) {
require(token != address(0) && beneficiary != address(0), "zero addr");
require(amount > 0, "amount=0");
require(tgeTime > block.timestamp, "tge in past");
require(tgeBps <= 10000, "bps>100%");
require(vestEnd >= tgeTime, "vest before tge");
require(tgeBps == 10000 ? vestEnd == tgeTime : vestEnd > tgeTime, "bad schedule");
require(msg.value == lockFee, "wrong fee");
require(bytes(title).length <= 48, "title too long");
if (lockFee > 0) {
(bool ok, ) = feeRecipient.call{value: lockFee}("");
require(ok, "fee send failed");
}
uint256 balBefore = IERC20(token).balanceOf(address(this));
require(IERC20(token).transferFrom(msg.sender, address(this), amount), "transferFrom failed");
uint256 received = IERC20(token).balanceOf(address(this)) - balBefore;
require(received > 0, "nothing received");
require(received <= type(uint128).max, "amount too large");
id = locks.length;
locks.push(Lock({
token: token,
beneficiary: beneficiary,
amount: uint128(received),
claimed: 0,
tgeTime: tgeTime,
tgeBps: tgeBps,
vestEnd: vestEnd,
revokedNever: false,
title: title
}));
_byToken[token].push(id);
_byBeneficiary[beneficiary].push(id);
emit Locked(id, token, beneficiary, received, tgeTime, tgeBps, vestEnd, title);
}
function unlockedAt(uint256 id, uint256 t) public view returns (uint256) {
Lock storage L = locks[id];
if (t < L.tgeTime) return 0;
uint256 tge = (uint256(L.amount) * L.tgeBps) / 10000;
if (t >= L.vestEnd) return L.amount;
uint256 vestable = L.amount - tge;
uint256 elapsed = t - L.tgeTime;
uint256 span = L.vestEnd - L.tgeTime;
return tge + (vestable * elapsed) / span;
}
function claimable(uint256 id) public view returns (uint256) {
Lock storage L = locks[id];
return unlockedAt(id, block.timestamp) - L.claimed;
}
function claim(uint256 id) external nonReentrant {
Lock storage L = locks[id];
require(msg.sender == L.beneficiary, "not beneficiary");
uint256 amt = unlockedAt(id, block.timestamp) - L.claimed;
require(amt > 0, "nothing claimable");
L.claimed += uint128(amt);
require(IERC20(L.token).transfer(L.beneficiary, amt), "transfer failed");
emit Claimed(id, L.beneficiary, amt);
}
function extendLock(uint256 id, uint64 newTgeTime, uint64 newVestEnd) external {
Lock storage L = locks[id];
require(msg.sender == L.beneficiary, "not beneficiary");
require(L.claimed == 0, "already vesting");
require(newTgeTime >= L.tgeTime && newVestEnd >= L.vestEnd, "only forward");
require(newVestEnd >= newTgeTime, "vest before tge");
require(L.tgeBps == 10000 ? newVestEnd == newTgeTime : newVestEnd > newTgeTime, "bad schedule");
L.tgeTime = newTgeTime;
L.vestEnd = newVestEnd;
emit LockExtended(id, newTgeTime, newVestEnd);
}
function transferBeneficiary(uint256 id, address to) external {
Lock storage L = locks[id];
require(msg.sender == L.beneficiary, "not beneficiary");
require(to != address(0), "zero addr");
address from = L.beneficiary;
L.beneficiary = to;
_byBeneficiary[to].push(id);
emit BeneficiaryTransferred(id, from, to);
}
function setFee(uint256 _lockFee, address _feeRecipient) external {
require(msg.sender == feeAdmin, "not admin");
require(_lockFee <= MAX_LOCK_FEE, "fee too high");
require(_feeRecipient != address(0), "recipient=0");
lockFee = _lockFee;
feeRecipient = _feeRecipient;
emit FeeUpdated(_lockFee, _feeRecipient, feeAdmin);
}
function setFeeAdmin(address _admin) external {
require(msg.sender == feeAdmin, "not admin");
feeAdmin = _admin;
emit FeeUpdated(lockFee, feeRecipient, _admin);
}
function lockCount() external view returns (uint256) { return locks.length; }
function locksOfToken(address token) external view returns (uint256[] memory) { return _byToken[token]; }
function locksOfBeneficiary(address who) external view returns (uint256[] memory) { return _byBeneficiary[who]; }
function lockInfo(uint256 id) external view returns (
address token, address beneficiary, uint256 amount, uint256 claimed,
uint64 tgeTime, uint16 tgeBps, uint64 vestEnd, string memory title,
uint256 unlockedNow, uint256 claimableNow
) {
Lock storage L = locks[id];
uint256 u = unlockedAt(id, block.timestamp);
return (L.token, L.beneficiary, L.amount, L.claimed, L.tgeTime, L.tgeBps, L.vestEnd, L.title, u, u - L.claimed);
}
}
Launchpad factories
LaunchFactory · LaunchFactory.sol
pragma solidity ^0.8.24;
import {FairToken} from "./FairToken.sol";
interface IUniswapV3Factory {
function createPool(address tokenA, address tokenB, uint24 fee) external returns (address pool);
function getPool(address tokenA, address tokenB, uint24 fee) external view returns (address pool);
}
interface IUniswapV3Pool {
function initialize(uint160 sqrtPriceX96) external;
}
interface INonfungiblePositionManager {
struct MintParams {
address token0;
address token1;
uint24 fee;
int24 tickLower;
int24 tickUpper;
uint256 amount0Desired;
uint256 amount1Desired;
uint256 amount0Min;
uint256 amount1Min;
address recipient;
uint256 deadline;
}
function mint(MintParams calldata params)
external
payable
returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1);
}
interface ISwapRouter {
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 IFeeSplitLocker {
function registerLock(uint256 tokenId, address deployer, address token) external;
}
interface IERC20 {
function approve(address spender, uint256 amount) external returns (bool);
function transfer(address to, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
}
contract LaunchFactory {
IUniswapV3Factory public immutable V3_FACTORY;
INonfungiblePositionManager public immutable POSITION_MANAGER;
ISwapRouter public immutable SWAP_ROUTER;
IFeeSplitLocker public immutable LOCKER;
address public immutable USDC;
uint256 public constant SUPPLY = 1_000_000_000;
uint24 public constant POOL_FEE = 10_000;
int24 public constant TICK_LOWER = 198_000;
int24 public constant TICK_UPPER = 398_000;
int24 public constant TICK_LOWER_FLIP = -398_000;
int24 public constant TICK_UPPER_FLIP = -198_000;
uint160 public immutable INITIAL_SQRT_PRICE;
uint160 public immutable INITIAL_SQRT_PRICE_FLIP;
struct Launch {
address token;
address pool;
address deployer;
uint256 lpTokenId;
uint64 timestamp;
}
Launch[] public launches;
mapping(address => uint256) public launchIndexOf;
event TokenLaunched(
address indexed token,
address indexed deployer,
address pool,
uint256 lpTokenId,
string name,
string symbol
);
event InitialBuy(address indexed token, address indexed buyer, uint256 usdcIn, uint256 tokensOut);
error PoolExists();
error TokenEqualsUSDC();
error InitialBuyFailed();
constructor(
address v3Factory,
address positionManager,
address swapRouter,
address locker,
address usdc,
uint160 initialSqrtPrice
) {
V3_FACTORY = IUniswapV3Factory(v3Factory);
POSITION_MANAGER = INonfungiblePositionManager(positionManager);
SWAP_ROUTER = ISwapRouter(swapRouter);
LOCKER = IFeeSplitLocker(locker);
USDC = usdc;
INITIAL_SQRT_PRICE = initialSqrtPrice;
INITIAL_SQRT_PRICE_FLIP = uint160((uint256(1) << 192) / initialSqrtPrice);
}
function launch(string calldata name_, string calldata symbol_, uint256 initialBuy)
external
returns (address token, address pool, uint256 lpTokenId)
{
token = address(new FairToken(name_, symbol_, SUPPLY, address(this)));
if (token == USDC) revert TokenEqualsUSDC();
bool tokenIsToken0 = token < USDC;
if (V3_FACTORY.getPool(USDC, token, POOL_FEE) != address(0)) revert PoolExists();
pool = V3_FACTORY.createPool(USDC, token, POOL_FEE);
IUniswapV3Pool(pool).initialize(
tokenIsToken0 ? INITIAL_SQRT_PRICE_FLIP : INITIAL_SQRT_PRICE
);
uint256 amount = SUPPLY * 10 ** 18;
IERC20(token).approve(address(POSITION_MANAGER), amount);
INonfungiblePositionManager.MintParams memory mp;
if (tokenIsToken0) {
mp = INonfungiblePositionManager.MintParams({
token0: token,
token1: USDC,
fee: POOL_FEE,
tickLower: TICK_LOWER_FLIP,
tickUpper: TICK_UPPER_FLIP,
amount0Desired: amount,
amount1Desired: 0,
amount0Min: 0,
amount1Min: 0,
recipient: address(LOCKER),
deadline: block.timestamp
});
} else {
mp = INonfungiblePositionManager.MintParams({
token0: USDC,
token1: token,
fee: POOL_FEE,
tickLower: TICK_LOWER,
tickUpper: TICK_UPPER,
amount0Desired: 0,
amount1Desired: amount,
amount0Min: 0,
amount1Min: 0,
recipient: address(LOCKER),
deadline: block.timestamp
});
}
(lpTokenId, , , ) = POSITION_MANAGER.mint(mp);
LOCKER.registerLock(lpTokenId, msg.sender, token);
launches.push(
Launch({
token: token,
pool: pool,
deployer: msg.sender,
lpTokenId: lpTokenId,
timestamp: uint64(block.timestamp)
})
);
launchIndexOf[token] = launches.length;
emit TokenLaunched(token, msg.sender, pool, lpTokenId, name_, symbol_);
if (initialBuy > 0) {
_initialBuy(token, initialBuy);
}
}
function _initialBuy(address token, uint256 usdcIn) internal {
IERC20(USDC).transferFrom(msg.sender, address(this), usdcIn);
IERC20(USDC).approve(address(SWAP_ROUTER), usdcIn);
uint256 out = SWAP_ROUTER.exactInputSingle(
ISwapRouter.ExactInputSingleParams({
tokenIn: USDC,
tokenOut: token,
fee: POOL_FEE,
recipient: msg.sender,
amountIn: usdcIn,
amountOutMinimum: 0,
sqrtPriceLimitX96: 0
})
);
if (out == 0) revert InitialBuyFailed();
emit InitialBuy(token, msg.sender, usdcIn, out);
}
function launchCount() external view returns (uint256) {
return launches.length;
}
function launchOf(address token) external view returns (Launch memory) {
uint256 idx = launchIndexOf[token];
require(idx != 0, "unknown token");
return launches[idx - 1];
}
}
Reflection Launch Factory · ReflectionLaunchFactory.sol
pragma solidity ^0.8.24;
import {ReflectionToken} from "./ReflectionToken.sol";
interface IUniswapV3Factory {
function createPool(address tokenA, address tokenB, uint24 fee) external returns (address pool);
function getPool(address tokenA, address tokenB, uint24 fee) external view returns (address pool);
}
interface IUniswapV3Pool {
function initialize(uint160 sqrtPriceX96) external;
}
interface INonfungiblePositionManager {
struct MintParams {
address token0;
address token1;
uint24 fee;
int24 tickLower;
int24 tickUpper;
uint256 amount0Desired;
uint256 amount1Desired;
uint256 amount0Min;
uint256 amount1Min;
address recipient;
uint256 deadline;
}
function mint(MintParams calldata params)
external payable
returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1);
}
interface ISwapRouter {
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 IFeeSplitLocker {
function registerLock(uint256 tokenId, address deployer, address token) external;
}
interface IERC20 {
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
}
contract ReflectionLaunchFactory {
IUniswapV3Factory public immutable V3_FACTORY;
INonfungiblePositionManager public immutable POSITION_MANAGER;
ISwapRouter public immutable SWAP_ROUTER;
IFeeSplitLocker public immutable LOCKER;
address public immutable USDC;
address public immutable PLATFORM_ADMIN;
uint256 public constant SUPPLY = 1_000_000_000;
uint24 public constant POOL_FEE = 10_000;
int24 public constant TICK_LOWER = 198_000;
int24 public constant TICK_UPPER = 398_000;
int24 public constant TICK_LOWER_FLIP = -398_000;
int24 public constant TICK_UPPER_FLIP = -198_000;
uint160 public immutable INITIAL_SQRT_PRICE;
uint160 public immutable INITIAL_SQRT_PRICE_FLIP;
struct Launch {
address token;
address pool;
address deployer;
uint256 lpTokenId;
uint64 timestamp;
}
Launch[] public launches;
mapping(address => uint256) public launchIndexOf;
event TokenLaunched(
address indexed token,
address indexed deployer,
address pool,
uint256 lpTokenId,
string name,
string symbol
);
event InitialBuy(address indexed token, address indexed buyer, uint256 usdcIn, uint256 tokensOut);
error PoolExists();
error TokenEqualsUSDC();
error InitialBuyFailed();
error ZeroAddress();
constructor(
address v3Factory,
address positionManager,
address swapRouter,
address locker,
address usdc,
uint160 initialSqrtPrice,
address platformAdmin
) {
if (platformAdmin == address(0)) revert ZeroAddress();
V3_FACTORY = IUniswapV3Factory(v3Factory);
POSITION_MANAGER = INonfungiblePositionManager(positionManager);
SWAP_ROUTER = ISwapRouter(swapRouter);
LOCKER = IFeeSplitLocker(locker);
USDC = usdc;
PLATFORM_ADMIN = platformAdmin;
INITIAL_SQRT_PRICE = initialSqrtPrice;
INITIAL_SQRT_PRICE_FLIP = uint160((uint256(1) << 192) / initialSqrtPrice);
}
function launch(string calldata name_, string calldata symbol_, uint256 initialBuy)
external
returns (address token, address pool, uint256 lpTokenId)
{
ReflectionToken t = new ReflectionToken(name_, symbol_, SUPPLY, address(this), USDC, PLATFORM_ADMIN);
token = address(t);
if (token == USDC) revert TokenEqualsUSDC();
bool tokenIsToken0 = token < USDC;
if (V3_FACTORY.getPool(USDC, token, POOL_FEE) != address(0)) revert PoolExists();
pool = V3_FACTORY.createPool(USDC, token, POOL_FEE);
IUniswapV3Pool(pool).initialize(
tokenIsToken0 ? INITIAL_SQRT_PRICE_FLIP : INITIAL_SQRT_PRICE
);
t.setDividendExempt(pool, true);
t.setDividendExempt(address(LOCKER), true);
uint256 amount = SUPPLY * 10 ** 18;
t.approve(address(POSITION_MANAGER), amount);
INonfungiblePositionManager.MintParams memory mp;
if (tokenIsToken0) {
mp = INonfungiblePositionManager.MintParams({
token0: token,
token1: USDC,
fee: POOL_FEE,
tickLower: TICK_LOWER_FLIP,
tickUpper: TICK_UPPER_FLIP,
amount0Desired: amount,
amount1Desired: 0,
amount0Min: 0,
amount1Min: 0,
recipient: address(LOCKER),
deadline: block.timestamp
});
} else {
mp = INonfungiblePositionManager.MintParams({
token0: USDC,
token1: token,
fee: POOL_FEE,
tickLower: TICK_LOWER,
tickUpper: TICK_UPPER,
amount0Desired: 0,
amount1Desired: amount,
amount0Min: 0,
amount1Min: 0,
recipient: address(LOCKER),
deadline: block.timestamp
});
}
(lpTokenId, , , ) = POSITION_MANAGER.mint(mp);
LOCKER.registerLock(lpTokenId, msg.sender, token);
launches.push(Launch({
token: token,
pool: pool,
deployer: msg.sender,
lpTokenId: lpTokenId,
timestamp: uint64(block.timestamp)
}));
launchIndexOf[token] = launches.length;
emit TokenLaunched(token, msg.sender, pool, lpTokenId, name_, symbol_);
if (initialBuy > 0) {
_initialBuy(token, initialBuy);
}
}
function _initialBuy(address token, uint256 usdcIn) internal {
IERC20(USDC).transferFrom(msg.sender, address(this), usdcIn);
IERC20(USDC).approve(address(SWAP_ROUTER), usdcIn);
uint256 out = SWAP_ROUTER.exactInputSingle(
ISwapRouter.ExactInputSingleParams({
tokenIn: USDC,
tokenOut: token,
fee: POOL_FEE,
recipient: msg.sender,
amountIn: usdcIn,
amountOutMinimum: 0,
sqrtPriceLimitX96: 0
})
);
if (out == 0) revert InitialBuyFailed();
emit InitialBuy(token, msg.sender, usdcIn, out);
}
function launchCount() external view returns (uint256) {
return launches.length;
}
function launchOf(address token) external view returns (Launch memory) {
uint256 idx = launchIndexOf[token];
require(idx != 0, "unknown token");
return launches[idx - 1];
}
}
Launchpad fee-lockers
FeeSplitLocker · FeeSplitLocker.sol
pragma solidity ^0.8.24;
interface INonfungiblePositionManager {
struct CollectParams {
uint256 tokenId;
address recipient;
uint128 amount0Max;
uint128 amount1Max;
}
function collect(CollectParams calldata params)
external
payable
returns (uint256 amount0, uint256 amount1);
function positions(uint256 tokenId)
external
view
returns (
uint96 nonce,
address operator,
address token0,
address token1,
uint24 fee,
int24 tickLower,
int24 tickUpper,
uint128 liquidity,
uint256 feeGrowthInside0LastX128,
uint256 feeGrowthInside1LastX128,
uint128 tokensOwed0,
uint128 tokensOwed1
);
function ownerOf(uint256 tokenId) external view returns (address);
}
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
}
contract FeeSplitLocker {
INonfungiblePositionManager public immutable POSITION_MANAGER;
address public factory;
address public treasury;
uint16 public constant DEPLOYER_BPS = 7000;
uint16 public constant BPS_DENOM = 10_000;
struct Lock {
address deployer;
address token;
bool exists;
}
mapping(uint256 => Lock) public locks;
mapping(address => uint256[]) public positionsOf;
event PositionLocked(uint256 indexed tokenId, address indexed deployer, address indexed token);
event FeesCollected(
uint256 indexed tokenId,
address indexed deployer,
uint256 amount0Deployer,
uint256 amount1Deployer,
uint256 amount0Treasury,
uint256 amount1Treasury
);
event TreasuryUpdated(address indexed previous, address indexed next);
event FactoryUpdated(address indexed previous, address indexed next);
error NotFactory();
error NotTreasury();
error UnknownPosition();
error ZeroAddress();
error AlreadyLocked();
constructor(address positionManager, address treasury_) {
if (positionManager == address(0) || treasury_ == address(0)) revert ZeroAddress();
POSITION_MANAGER = INonfungiblePositionManager(positionManager);
treasury = treasury_;
factory = msg.sender;
}
function setFactory(address next) external {
if (msg.sender != treasury) revert NotTreasury();
if (next == address(0)) revert ZeroAddress();
emit FactoryUpdated(factory, next);
factory = next;
}
function setTreasury(address next) external {
if (msg.sender != treasury) revert NotTreasury();
if (next == address(0)) revert ZeroAddress();
emit TreasuryUpdated(treasury, next);
treasury = next;
}
function registerLock(uint256 tokenId, address deployer, address token) external {
if (msg.sender != factory) revert NotFactory();
if (deployer == address(0)) revert ZeroAddress();
if (locks[tokenId].exists) revert AlreadyLocked();
locks[tokenId] = Lock({deployer: deployer, token: token, exists: true});
positionsOf[deployer].push(tokenId);
emit PositionLocked(tokenId, deployer, token);
}
function collectFees(uint256 tokenId) external returns (uint256 amount0, uint256 amount1) {
Lock memory lock = locks[tokenId];
if (!lock.exists) revert UnknownPosition();
(amount0, amount1) = POSITION_MANAGER.collect(
INonfungiblePositionManager.CollectParams({
tokenId: tokenId,
recipient: address(this),
amount0Max: type(uint128).max,
amount1Max: type(uint128).max
})
);
(, , address token0, address token1, , , , , , , , ) = POSITION_MANAGER.positions(tokenId);
uint256 dep0 = (amount0 * DEPLOYER_BPS) / BPS_DENOM;
uint256 dep1 = (amount1 * DEPLOYER_BPS) / BPS_DENOM;
uint256 tre0 = amount0 - dep0;
uint256 tre1 = amount1 - dep1;
address deployer = lock.deployer;
address treasury_ = treasury;
if (dep0 > 0) IERC20(token0).transfer(deployer, dep0);
if (tre0 > 0) IERC20(token0).transfer(treasury_, tre0);
if (dep1 > 0) IERC20(token1).transfer(deployer, dep1);
if (tre1 > 0) IERC20(token1).transfer(treasury_, tre1);
emit FeesCollected(tokenId, deployer, dep0, dep1, tre0, tre1);
}
function pendingFees(uint256 tokenId) external view returns (uint128 owed0, uint128 owed1) {
(, , , , , , , , , , owed0, owed1) = POSITION_MANAGER.positions(tokenId);
}
function positionCount(address deployer) external view returns (uint256) {
return positionsOf[deployer].length;
}
function onERC721Received(address, address, uint256, bytes calldata)
external
pure
returns (bytes4)
{
return this.onERC721Received.selector;
}
}
FeeSplitLocker V2 · FeeSplitLockerV2.sol
pragma solidity ^0.8.24;
interface INonfungiblePositionManager {
struct CollectParams {
uint256 tokenId;
address recipient;
uint128 amount0Max;
uint128 amount1Max;
}
function collect(CollectParams calldata params)
external
payable
returns (uint256 amount0, uint256 amount1);
function positions(uint256 tokenId)
external
view
returns (
uint96 nonce,
address operator,
address token0,
address token1,
uint24 fee,
int24 tickLower,
int24 tickUpper,
uint128 liquidity,
uint256 feeGrowthInside0LastX128,
uint256 feeGrowthInside1LastX128,
uint128 tokensOwed0,
uint128 tokensOwed1
);
function ownerOf(uint256 tokenId) external view returns (address);
}
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
}
contract FeeSplitLockerV2 {
INonfungiblePositionManager public immutable POSITION_MANAGER;
address public factory;
address public treasury;
uint16 public constant DEFAULT_DEPLOYER_BPS = 7000;
uint16 public constant BPS_DENOM = 10_000;
struct Lock {
address deployer;
address token;
uint16 deployerBps;
bool exists;
}
mapping(uint256 => Lock) public locks;
mapping(address => uint256[]) public positionsOf;
event PositionLocked(uint256 indexed tokenId, address indexed deployer, address indexed token);
event FeesCollected(
uint256 indexed tokenId,
address indexed deployer,
uint256 amount0Deployer,
uint256 amount1Deployer,
uint256 amount0Treasury,
uint256 amount1Treasury
);
event DeployerReassigned(uint256 indexed tokenId, address indexed previous, address indexed next);
event DeployerBpsRaised(uint256 indexed tokenId, uint16 previous, uint16 next);
event TreasuryUpdated(address indexed previous, address indexed next);
event FactoryUpdated(address indexed previous, address indexed next);
error NotFactory();
error NotTreasury();
error UnknownPosition();
error ZeroAddress();
error AlreadyLocked();
error BpsTooLow();
error BpsTooHigh();
error SameDeployer();
modifier onlyTreasury() {
if (msg.sender != treasury) revert NotTreasury();
_;
}
constructor(address positionManager, address treasury_) {
if (positionManager == address(0) || treasury_ == address(0)) revert ZeroAddress();
POSITION_MANAGER = INonfungiblePositionManager(positionManager);
treasury = treasury_;
factory = msg.sender;
}
function setFactory(address next) external onlyTreasury {
if (next == address(0)) revert ZeroAddress();
emit FactoryUpdated(factory, next);
factory = next;
}
function setTreasury(address next) external onlyTreasury {
if (next == address(0)) revert ZeroAddress();
emit TreasuryUpdated(treasury, next);
treasury = next;
}
function reassignDeployer(uint256 tokenId, address newDeployer) external onlyTreasury {
Lock storage lock = locks[tokenId];
if (!lock.exists) revert UnknownPosition();
if (newDeployer == address(0)) revert ZeroAddress();
if (newDeployer == lock.deployer) revert SameDeployer();
address previous = lock.deployer;
lock.deployer = newDeployer;
positionsOf[newDeployer].push(tokenId);
emit DeployerReassigned(tokenId, previous, newDeployer);
}
function raiseDeployerBps(uint256 tokenId, uint16 newBps) external onlyTreasury {
Lock storage lock = locks[tokenId];
if (!lock.exists) revert UnknownPosition();
if (newBps > BPS_DENOM) revert BpsTooHigh();
if (newBps < lock.deployerBps) revert BpsTooLow();
uint16 previous = lock.deployerBps;
lock.deployerBps = newBps;
emit DeployerBpsRaised(tokenId, previous, newBps);
}
function registerLock(uint256 tokenId, address deployer, address token) external {
if (msg.sender != factory) revert NotFactory();
if (deployer == address(0)) revert ZeroAddress();
if (locks[tokenId].exists) revert AlreadyLocked();
locks[tokenId] = Lock({
deployer: deployer,
token: token,
deployerBps: DEFAULT_DEPLOYER_BPS,
exists: true
});
positionsOf[deployer].push(tokenId);
emit PositionLocked(tokenId, deployer, token);
}
function collectFees(uint256 tokenId) external returns (uint256 amount0, uint256 amount1) {
Lock memory lock = locks[tokenId];
if (!lock.exists) revert UnknownPosition();
(amount0, amount1) = POSITION_MANAGER.collect(
INonfungiblePositionManager.CollectParams({
tokenId: tokenId,
recipient: address(this),
amount0Max: type(uint128).max,
amount1Max: type(uint128).max
})
);
(, , address token0, address token1, , , , , , , , ) = POSITION_MANAGER.positions(tokenId);
uint256 dep0 = (amount0 * lock.deployerBps) / BPS_DENOM;
uint256 dep1 = (amount1 * lock.deployerBps) / BPS_DENOM;
uint256 tre0 = amount0 - dep0;
uint256 tre1 = amount1 - dep1;
address deployer = lock.deployer;
address treasury_ = treasury;
if (dep0 > 0) IERC20(token0).transfer(deployer, dep0);
if (tre0 > 0) IERC20(token0).transfer(treasury_, tre0);
if (dep1 > 0) IERC20(token1).transfer(deployer, dep1);
if (tre1 > 0) IERC20(token1).transfer(treasury_, tre1);
emit FeesCollected(tokenId, deployer, dep0, dep1, tre0, tre1);
}
function pendingFees(uint256 tokenId) external view returns (uint128 owed0, uint128 owed1) {
(, , , , , , , , , , owed0, owed1) = POSITION_MANAGER.positions(tokenId);
}
function positionCount(address deployer) external view returns (uint256) {
return positionsOf[deployer].length;
}
function onERC721Received(address, address, uint256, bytes calldata)
external
pure
returns (bytes4)
{
return this.onERC721Received.selector;
}
}
Brand
Assets for partners, listings and integrations. Use the name and logo as-is; don't recolor, stretch or add effects.
Socials: x.com/ArcDEXScan · t.me/RadarDEXScan
