SushiV3FeeManager
0x2bbdf7cb9e4e361006708693795b89cb1a48b1b2
Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
12,455 bytes
ABI entries
41
17 read · 9 write
License
none
Contract information
- Address
- 0x2bbdf7cb9e4e361006708693795b89cb1a48b1b2
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.26+commit.8a97fa7a
- Optimization
- Enabled
- Creator
- 0x7B7DE29bc1…C6b0DaB694
- Creation tx
- 0x922e92ea69…16ecdd990c
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (17)
BPS() → uint256
MAX_CONVERSION_SLIPPAGE_BPS() → uint256
POOL_FEE() → uint24
TICK_SPACING() → int24
TOTAL_SUPPLY() → uint256
claimable(bytes32, address) → uint256, uint256
config() → address
factory() → address
getPositionTokenId(bytes32, uint256) → uint256
getPositionTokenIds(bytes32) → uint256[5]
getPositionsMinted(bytes32) → bool[5]
owner() → address
pendingETH(bytes32, address) → uint256
pendingToken(bytes32, address) → uint256
positionManager() → address
positionMinted(bytes32, uint256) → bool
weth() → address
Events (6)
FactorySetFeesAccruedFeesClaimedFeesCollectedOwnershipTransferredPositionMinted
ABI
[
{
"inputs": [
{
"internalType": "contract SushiConfig",
"name": "_config",
"type": "address"
},
{
"internalType": "contract INonfungiblePositionManager",
"name": "_positionManager",
"type": "address"
},
{
"internalType": "contract IWETH9",
"name": "_weth",
"type": "address"
},
{
"internalType": "address",
"name": "_owner",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "AlreadyRegistered",
"type": "error"
},
{
"inputs": [],
"name": "FactoryAlreadySet",
"type": "error"
},
{
"inputs": [],
"name": "OnlyFactory",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "OwnableInvalidOwner",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "OwnableUnauthorizedAccount",
"type": "error"
},
{
"inputs": [],
"name": "ReentrancyGuardReentrantCall",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "SafeCastOverflowedUintToInt",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "factory",
"type": "address"
}
],
"name": "FactorySet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "poolId",
"type": "bytes32"
},
{
"indexed": true,
"internalType": "address",
"name": "recipient",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "ethAmount",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokenAmount",
"type": "uint256"
}
],
"name": "FeesAccrued",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "poolId",
"type": "bytes32"
},
{
"indexed": true,
"internalType": "address",
"name": "recipient",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "ethAmount",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokenAmount",
"type": "uint256"
}
],
"name": "FeesClaimed",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "poolId",
"type": "bytes32"
},
{
"indexed": false,
"internalType": "uint256",
"name": "positionIndex",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "ethGained",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokenGained",
"type": "uint256"
}
],
"name": "FeesCollected",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "poolId",
"type": "bytes32"
},
{
"indexed": false,
"internalType": "uint256",
"name": "index",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint128",
"name": "liquidity",
"type": "uint128"
}
],
"name": "PositionMinted",
"type": "event"
},
{
"inputs": [],
"name": "BPS",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MAX_CONVERSION_SLIPPAGE_BPS",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "POOL_FEE",
"outputs": [
{
"internalType": "uint24",
"name": "",
"type": "uint24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "TICK_SPACING",
"outputs": [
{
"internalType": "int24",
"name": "",
"type": "int24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "TOTAL_SUPPLY",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "poolId",
"type": "bytes32"
}
],
"name": "claim",
"outputs": [
{
"internalType": "uint256",
"name": "ethAmount",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "tokenAmount",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "poolId",
"type": "bytes32"
},
{
"internalType": "address",
"name": "recipient",
"type": "address"
}
],
"name": "claimable",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "poolId",
"type": "bytes32"
},
{
"internalType": "uint256",
"name": "positionIndex",
"type": "uint256"
}
],
"name": "collectFeesFromPosition",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "config",
"outputs": [
{
"internalType": "contract SushiConfig",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "poolId",
"type": "bytes32"
},
{
"internalType": "address",
"name": "recipient",
"type": "address"
},
{
"internalType": "uint256",
"name": "ethIn",
"type": "uint256"
}
],
"name": "devBuy",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "factory",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "poolId",
"type": "bytes32"
},
{
"internalType": "uint256",
"name": "index",
"type": "uint256"
}
],
"name": "getPositionTokenId",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "poolId",
"type": "bytes32"
}
],
"name": "getPositionTokenIds",
"outputs": [
{
"internalType": "uint256[5]",
"name": "",
"type": "uint256[5]"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "poolId",
"type": "bytes32"
}
],
"name": "getPositionsMinted",
"outputs": [
{
"internalType": "bool[5]",
"name": "",
"type": "bool[5]"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
},
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "pendingETH",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
},
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "pendingToken",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "positionManager",
"outputs": [
{
"internalType": "contract INonfungiblePositionManager",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "poolId",
"type": "bytes32"
},
{
"internalType": "uint256",
"name": "index",
"type": "uint256"
}
],
"name": "positionMinted",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"components": [
{
"internalType": "bytes32",
"name": "poolId",
"type": "bytes32"
},
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "pool",
"type": "address"
},
{
"internalType": "address",
"name": "beneficiary",
"type": "address"
},
{
"internalType": "uint256",
"name": "targetMcapETH",
"type": "uint256"
},
{
"internalType": "enum SushiV3FeeManager.FeeClaimMode",
"name": "feeClaimMode",
"type": "uint8"
},
{
"components": [
{
"internalType": "address[5]",
"name": "wallets",
"type": "address[5]"
},
{
"internalType": "uint16[5]",
"name": "percentages",
"type": "uint16[5]"
},
{
"internalType": "uint8",
"name": "count",
"type": "uint8"
}
],
"internalType": "struct SushiV3FeeManager.FeeSplit",
"name": "feeSplit",
"type": "tuple"
},
{
"internalType": "uint256",
"name": "bootstrapETH",
"type": "uint256"
}
],
"internalType": "struct SushiV3FeeManager.RegisterParams",
"name": "p",
"type": "tuple"
}
],
"name": "registerAndMint",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "rescueETH",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_factory",
"type": "address"
}
],
"name": "setFactory",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "int256",
"name": "amount0Delta",
"type": "int256"
},
{
"internalType": "int256",
"name": "amount1Delta",
"type": "int256"
},
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "uniswapV3SwapCallback",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "weth",
"outputs": [
{
"internalType": "contract IWETH9",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"stateMutability": "payable",
"type": "receive"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {
IUniswapV3Pool,
INonfungiblePositionManager,
IUniswapV3SwapCallback,
IWETH9
} from "../interfaces/ISushiV3.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SushiConfig} from "./SushiConfig.sol";
import {PriceMath} from "../libraries/PriceMath.sol";
import {LiquidityAmounts} from "../libraries/math/LiquidityAmounts.sol";
import {TickMath} from "../libraries/math/TickMath.sol";
import {FullMath} from "../libraries/math/FullMath.sol";
import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";
/**
* @title SushiV3FeeManager
* @notice Position + swap + fee half of the Sushi V3 launcher (the counterpart of
* the original Uniswap-V4 Core+Helper split, resurrected here purely to
* keep each contract under the 24KB EIP-170 limit).
*
* The SushiV3Factory orchestrates launches (token deploy, pool create/init,
* registry) and, per launch, hands this contract the full token supply plus
* the launch parameters via `registerAndMint`. From that point on this
* contract:
* - holds the 5 concentrated NFT positions (it is the NPM `recipient`),
* - is the pool `swap` caller for the atomic dev-buy and fee conversions,
* - collects + splits LP fees (20% platform / 80% beneficiary),
* - exposes the ProofOfSteak fee-source interface
* (collectFeesFromPosition / claim / claimable) unchanged.
*
* ONLY the factory may call `registerAndMint` and `devBuy`. Fee collection
* and claims are permissionless (interface-identical to the DIRECT stack so
* ProofOfSteak's harvest loop works verbatim).
*/
contract SushiV3FeeManager is Ownable, ReentrancyGuard, IUniswapV3SwapCallback {
using SafeCast for uint256;
uint256 public constant TOTAL_SUPPLY = 1_000_000_000 * 1e18;
uint256 public constant BPS = 10000;
uint24 public constant POOL_FEE = 10000;
int24 public constant TICK_SPACING = 200;
/// @notice Max slippage (BPS vs pre-swap spot) tolerated on fee-conversion
/// swaps (ETH_ONLY / TOKEN_ONLY). Loose (50%) — a thin launch pool
/// legitimately loses a lot to pool fee + impact; the guard only blocks
/// an egregious sandwich.
uint256 public constant MAX_CONVERSION_SLIPPAGE_BPS = 5000;
enum FeeClaimMode { BOTH, ETH_ONLY, TOKEN_ONLY }
struct FeeSplit {
address[5] wallets;
uint16[5] percentages;
uint8 count;
}
/// @notice Everything this contract needs per pool. Populated once by the
/// factory's `registerAndMint` call during a launch.
struct PoolRecord {
address token;
address pool;
address beneficiary;
uint256 targetMcapETH;
FeeClaimMode feeClaimMode;
FeeSplit feeSplit;
uint256[5] positionTokenIds;
bool[5] positionMinted;
bool registered;
}
/// @notice Launch parameters passed by the factory in one struct (avoids a
/// stack-too-deep argument list).
struct RegisterParams {
bytes32 poolId;
address token;
address pool;
address beneficiary;
uint256 targetMcapETH;
FeeClaimMode feeClaimMode;
FeeSplit feeSplit;
uint256 bootstrapETH;
}
SushiConfig public immutable config;
INonfungiblePositionManager public immutable positionManager;
IWETH9 public immutable weth;
/// @notice The one factory allowed to register launches + trigger dev-buys.
address public factory;
mapping(bytes32 => PoolRecord) internal _records;
/// @notice Unclaimed ETH / token fee share per (poolId, recipient).
mapping(bytes32 => mapping(address => uint256)) public pendingETH;
mapping(bytes32 => mapping(address => uint256)) public pendingToken;
/// @dev Transient guard so uniswapV3SwapCallback only trusts calls from the
/// pool we are actively swapping against in this tx.
address private _activeSwapPool;
event FactorySet(address indexed factory);
event PositionMinted(bytes32 indexed poolId, uint256 index, uint256 tokenId, uint128 liquidity);
event FeesCollected(bytes32 indexed poolId, uint256 positionIndex, uint256 ethGained, uint256 tokenGained);
event FeesAccrued(bytes32 indexed poolId, address indexed recipient, uint256 ethAmount, uint256 tokenAmount);
event FeesClaimed(bytes32 indexed poolId, address indexed recipient, uint256 ethAmount, uint256 tokenAmount);
error OnlyFactory();
error FactoryAlreadySet();
error AlreadyRegistered();
constructor(
SushiConfig _config,
INonfungiblePositionManager _positionManager,
IWETH9 _weth,
address _owner
) Ownable(_owner) {
config = _config;
positionManager = _positionManager;
weth = _weth;
}
modifier onlyFactory() {
if (msg.sender != factory) revert OnlyFactory();
_;
}
/// @notice One-time wiring of the factory allowed to register launches. Owner
/// gated; can only be set once (immutability after deploy wiring).
function setFactory(address _factory) external onlyOwner {
require(_factory != address(0), "Zero factory");
if (factory != address(0)) revert FactoryAlreadySet();
factory = _factory;
emit FactorySet(_factory);
}
// ------------------------------------------------------------------
// REGISTRATION + LIQUIDITY (factory-only)
// ------------------------------------------------------------------
/// @notice Register a launch and mint its 5 bootstrap positions. The factory
/// MUST have transferred the full token supply to this contract and
/// exempted this contract from token limits before calling. The
/// `bootstrapETH` is forwarded as msg.value and wrapped to WETH here.
function registerAndMint(RegisterParams calldata p) external payable onlyFactory nonReentrant {
PoolRecord storage r = _records[p.poolId];
if (r.registered) revert AlreadyRegistered();
r.token = p.token;
r.pool = p.pool;
r.beneficiary = p.beneficiary;
r.targetMcapETH = p.targetMcapETH;
r.feeClaimMode = p.feeClaimMode;
r.feeSplit = p.feeSplit;
r.registered = true;
require(msg.value == p.bootstrapETH, "Bootstrap mismatch");
_mintPositions(p.poolId, p.bootstrapETH);
}
/// @notice Atomic dev-buy (WETH -> token) delivered to `recipient` (the
/// creator). Factory-only; forwards `ethIn` as msg.value.
function devBuy(bytes32 poolId, address recipient, uint256 ethIn)
external
payable
onlyFactory
nonReentrant
{
require(msg.value == ethIn, "DevBuy mismatch");
PoolRecord storage r = _records[poolId];
address token = r.token;
// Wrap ETH; the callback will pay WETH to the pool.
weth.deposit{value: ethIn}();
// WETH(token0) -> token(token1): zeroForOne = true, exact-input.
uint256 tokensOut = _poolSwap(r.pool, token, true, ethIn, 0);
if (tokensOut > 0) {
IERC20(token).transfer(recipient, tokensOut);
}
}
function _mintPositions(bytes32 poolId, uint256 bootstrapETH) internal {
PoolRecord storage r = _records[poolId];
address token = r.token;
(int24[5] memory tickLowers, int24[5] memory tickUppers, uint256[5] memory allocations) =
PriceMath.calculatePositionRanges(r.targetMcapETH, TOTAL_SUPPLY, TICK_SPACING);
(uint160 curSqrtPrice,,,,,,) = IUniswapV3Pool(r.pool).slot0();
// Wrap the whole bootstrap into WETH once and approve NPM for both assets.
if (bootstrapETH > 0) {
weth.deposit{value: bootstrapETH}();
weth.approve(address(positionManager), bootstrapETH);
}
// Approve the NPM to pull the token allocations.
IERC20(token).approve(address(positionManager), TOTAL_SUPPLY);
uint256 remainingWeth = bootstrapETH;
for (uint256 i = 0; i < 5; i++) {
uint160 sqrtLower = TickMath.getSqrtPriceAtTick(tickLowers[i]);
uint160 sqrtUpper = TickMath.getSqrtPriceAtTick(tickUppers[i]);
// WETH = token0. A position needs WETH only while the current price is
// below its upper bound; token-only (out-of-range above) positions take
// no WETH. At launch only P1 is in range, so it receives all the WETH.
uint256 wethForPosition = curSqrtPrice < sqrtUpper ? remainingWeth : 0;
uint128 liq = LiquidityAmounts.getLiquidityForAmounts(
curSqrtPrice, sqrtLower, sqrtUpper, wethForPosition, allocations[i]
);
if (liq == 0) continue;
// token0 = WETH, token1 = token (orientation enforced by the factory).
INonfungiblePositionManager.MintParams memory mp = INonfungiblePositionManager.MintParams({
token0: address(weth),
token1: token,
fee: POOL_FEE,
tickLower: tickLowers[i],
tickUpper: tickUppers[i],
amount0Desired: wethForPosition,
amount1Desired: allocations[i],
amount0Min: 0,
amount1Min: 0,
recipient: address(this),
deadline: block.timestamp + 1 hours
});
(uint256 tokenId, uint128 mintedLiq, uint256 usedWeth,) = positionManager.mint(mp);
r.positionTokenIds[i] = tokenId;
r.positionMinted[i] = true;
if (usedWeth <= remainingWeth) remainingWeth -= usedWeth;
emit PositionMinted(poolId, i, tokenId, mintedLiq);
}
// Any WETH the positions didn't consume: unwrap and park with this contract
// (kept as ETH; recyclable / withdrawable by owner via rescue).
uint256 leftover = weth.balanceOf(address(this));
if (leftover > 0) {
weth.withdraw(leftover);
}
}
// ------------------------------------------------------------------
// DEV-BUY & SWAPS (direct V3 pool.swap + uniswapV3SwapCallback)
// ------------------------------------------------------------------
/// @dev Exact-input swap directly against the pool, with a spot-price min-out
/// guard (fee-conversion for ETH_ONLY / TOKEN_ONLY). `wethToToken`:
/// WETH->token. Input (WETH or token) is already held by this contract.
function _swapExactIn(bytes32 poolId, bool wethToToken, uint256 amountIn)
internal
returns (uint256 amountOut)
{
if (amountIn == 0) return 0;
PoolRecord storage r = _records[poolId];
// spot: sqrtP^2 / 2^192 = token1/token0 = token per WETH (WETH=token0).
(uint160 sqrtP,,,,,,) = IUniswapV3Pool(r.pool).slot0();
uint256 priceX192 = uint256(sqrtP) * uint256(sqrtP);
uint256 expectedOut = wethToToken
? FullMath.mulDiv(amountIn, priceX192, 1 << 192)
: FullMath.mulDiv(amountIn, 1 << 192, priceX192);
uint256 minOut = (expectedOut * (BPS - MAX_CONVERSION_SLIPPAGE_BPS)) / BPS;
amountOut = _poolSwap(r.pool, r.token, wethToToken, amountIn, minOut);
}
/// @dev Core exact-input pool swap. `zeroForOne == wethToToken` (WETH=token0).
/// Returns the output amount and enforces `minOut`.
function _poolSwap(address pool, address token, bool wethToToken, uint256 amountIn, uint256 minOut)
internal
returns (uint256 amountOut)
{
_activeSwapPool = pool;
// Encode which token this contract must pay in the callback.
bytes memory cb = abi.encode(pool, token);
uint160 limit = wethToToken ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1;
(int256 amount0, int256 amount1) = IUniswapV3Pool(pool).swap(
address(this), wethToToken, amountIn.toInt256(), limit, cb
);
// Output is the negative delta on the OUT side. WETH=token0, token=token1.
// wethToToken: pay token0 (positive amount0), receive token1 (negative amount1).
amountOut = wethToToken ? uint256(-amount1) : uint256(-amount0);
_activeSwapPool = address(0);
require(amountOut >= minOut, "Conversion slippage");
}
/// @notice V3 swap callback — pay the pool the input token it is owed.
function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata data)
external
override
{
require(msg.sender == _activeSwapPool && msg.sender != address(0), "Unexpected callback");
(address pool, address token) = abi.decode(data, (address, address));
require(pool == msg.sender, "Bad pool");
// Positive delta = amount this contract owes the pool. token0 = WETH,
// token1 = the launch token.
if (amount0Delta > 0) {
// owe WETH
weth.transfer(msg.sender, uint256(amount0Delta));
}
if (amount1Delta > 0) {
// owe launch token
IERC20(token).transfer(msg.sender, uint256(amount1Delta));
}
}
// ------------------------------------------------------------------
// FEE COLLECTION (ProofOfSteak-compatible interface)
// ------------------------------------------------------------------
/// @notice Collect fees from position `positionIndex` and split platform /
/// beneficiary. ProofOfSteak calls this (interface-identical to DIRECT).
function collectFeesFromPosition(bytes32 poolId, uint256 positionIndex) external nonReentrant {
_collect(poolId, positionIndex);
}
function _collect(bytes32 poolId, uint256 positionIndex) internal {
PoolRecord storage r = _records[poolId];
require(r.positionMinted[positionIndex], "Position not minted");
uint256 tokenId = r.positionTokenIds[positionIndex];
require(tokenId != 0, "No position");
address token = r.token;
FeeClaimMode mode = r.feeClaimMode;
// Collect all owed fees (WETH + token) to this contract.
(uint256 wethGained, uint256 tokenGained) = positionManager.collect(
INonfungiblePositionManager.CollectParams({
tokenId: tokenId,
recipient: address(this),
amount0Max: type(uint128).max,
amount1Max: type(uint128).max
})
);
// token0 = WETH -> unwrap to native ETH so downstream stays ETH-denominated.
uint256 ethGained = wethGained;
if (wethGained > 0) {
weth.withdraw(wethGained);
}
// Convert the unwanted side per claim mode.
if (mode == FeeClaimMode.ETH_ONLY && tokenGained > 0) {
uint256 extraWeth = _swapExactIn(poolId, false, tokenGained); // token -> WETH
if (extraWeth > 0) {
weth.withdraw(extraWeth);
ethGained += extraWeth;
}
tokenGained = 0;
} else if (mode == FeeClaimMode.TOKEN_ONLY && ethGained > 0) {
// wrap the ETH we just unwrapped, then WETH -> token
weth.deposit{value: ethGained}();
uint256 extraToken = _swapExactIn(poolId, true, ethGained); // WETH -> token
tokenGained += extraToken;
ethGained = 0;
}
uint256 platformBps = config.platformShareBps();
if (ethGained > 0) {
uint256 beneficiaryETH = (ethGained * (BPS - platformBps)) / BPS;
uint256 platformETH = ethGained - beneficiaryETH;
if (platformETH > 0) {
(bool ok,) = config.treasury().call{value: platformETH}("");
require(ok, "Platform ETH transfer failed");
}
if (beneficiaryETH > 0) _accrueETH(poolId, r, beneficiaryETH);
}
if (tokenGained > 0) {
uint256 beneficiaryTokens = (tokenGained * (BPS - platformBps)) / BPS;
uint256 platformTokens = tokenGained - beneficiaryTokens;
if (platformTokens > 0) {
IERC20(token).transfer(config.treasury(), platformTokens);
}
if (beneficiaryTokens > 0) _accrueToken(poolId, r, beneficiaryTokens);
}
emit FeesCollected(poolId, positionIndex, ethGained, tokenGained);
}
/// @dev Accrue ETH across the fee-split wallets (or the sole beneficiary).
function _accrueETH(bytes32 poolId, PoolRecord storage r, uint256 amount) internal {
if (r.feeSplit.count == 0) {
pendingETH[poolId][r.beneficiary] += amount;
emit FeesAccrued(poolId, r.beneficiary, amount, 0);
return;
}
uint256 distributed;
for (uint256 i = 0; i < r.feeSplit.count; i++) {
uint256 share = (amount * r.feeSplit.percentages[i]) / BPS;
if (i == r.feeSplit.count - 1) share = amount - distributed; // dust to last
distributed += share;
pendingETH[poolId][r.feeSplit.wallets[i]] += share;
emit FeesAccrued(poolId, r.feeSplit.wallets[i], share, 0);
}
}
function _accrueToken(bytes32 poolId, PoolRecord storage r, uint256 amount) internal {
if (r.feeSplit.count == 0) {
pendingToken[poolId][r.beneficiary] += amount;
emit FeesAccrued(poolId, r.beneficiary, 0, amount);
return;
}
uint256 distributed;
for (uint256 i = 0; i < r.feeSplit.count; i++) {
uint256 share = (amount * r.feeSplit.percentages[i]) / BPS;
if (i == r.feeSplit.count - 1) share = amount - distributed;
distributed += share;
pendingToken[poolId][r.feeSplit.wallets[i]] += share;
emit FeesAccrued(poolId, r.feeSplit.wallets[i], 0, share);
}
}
/// @notice Claim caller's accrued share. Interface-identical to DIRECT so
/// ProofOfSteak's harvest loop (collectFeesFromPosition + claim) works.
function claim(bytes32 poolId) external nonReentrant returns (uint256 ethAmount, uint256 tokenAmount) {
PoolRecord storage r = _records[poolId];
address token = r.token;
ethAmount = pendingETH[poolId][msg.sender];
tokenAmount = pendingToken[poolId][msg.sender];
require(ethAmount > 0 || tokenAmount > 0, "Nothing to claim");
if (ethAmount > 0) {
pendingETH[poolId][msg.sender] = 0;
(bool ok,) = msg.sender.call{value: ethAmount}("");
require(ok, "Claim ETH transfer failed");
}
if (tokenAmount > 0) {
pendingToken[poolId][msg.sender] = 0;
IERC20(token).transfer(msg.sender, tokenAmount);
}
emit FeesClaimed(poolId, msg.sender, ethAmount, tokenAmount);
}
function claimable(bytes32 poolId, address recipient) external view returns (uint256, uint256) {
return (pendingETH[poolId][recipient], pendingToken[poolId][recipient]);
}
// ------------------------------------------------------------------
// VIEWS
// ------------------------------------------------------------------
function getPositionTokenId(bytes32 poolId, uint256 index) external view returns (uint256) {
return _records[poolId].positionTokenIds[index];
}
function positionMinted(bytes32 poolId, uint256 index) external view returns (bool) {
return _records[poolId].positionMinted[index];
}
function getPositionTokenIds(bytes32 poolId) external view returns (uint256[5] memory) {
return _records[poolId].positionTokenIds;
}
function getPositionsMinted(bytes32 poolId) external view returns (bool[5] memory) {
return _records[poolId].positionMinted;
}
/// @notice Rescue parked ETH (leftover unwrapped bootstrap) to treasury.
function rescueETH() external onlyOwner {
uint256 bal = address(this).balance;
(bool ok,) = config.treasury().call{value: bal}("");
require(ok, "rescue failed");
}
receive() external payable {}
}
Chain explorer3356msChain node99ms