EquifoldV4DexAdapter
0x64aa9cbe29edd1b047988eebba7fcd3c82add693
Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
12,643 bytes
ABI entries
43
10 read · 8 write
License
none
Contract information
- Address
- 0x64aa9cbe29edd1b047988eebba7fcd3c82add693
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.24+commit.e11b9ed9
- Optimization
- Enabled
- Creator
- 0xd03543d97B…F446Cd1418
- Creation tx
- 0x9183764e3f…14b89a2427
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (10)
feeCollector() → address
graduationFeeBps() → uint16
graduationManager() → address
hook() → address
liquidityCount() → uint256
liquidityLocked(uint256) → bool
owner() → address
pendingOwner() → address
poolManager() → address
treasury() → address
Events (7)
FeeConfiguredFeesCollectedGraduationFeeTakenGraduationManagerConfiguredLiquidityCreatedOwnershipTransferStartedOwnershipTransferred
ABI
[
{
"inputs": [
{
"internalType": "contract IPoolManager",
"name": "poolManager_",
"type": "address"
},
{
"internalType": "contract EquifoldV4Hook",
"name": "hook_",
"type": "address"
},
{
"internalType": "address",
"name": "initialOwner",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "AlreadyConfigured",
"type": "error"
},
{
"inputs": [],
"name": "FeeCapExceeded",
"type": "error"
},
{
"inputs": [],
"name": "FeeConfigNotSet",
"type": "error"
},
{
"inputs": [],
"name": "IncorrectCaller",
"type": "error"
},
{
"inputs": [],
"name": "InvalidAddress",
"type": "error"
},
{
"inputs": [],
"name": "InvalidData",
"type": "error"
},
{
"inputs": [],
"name": "InvalidProject",
"type": "error"
},
{
"inputs": [],
"name": "InvalidSqrtPrice",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "OwnableInvalidOwner",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "OwnableUnauthorizedAccount",
"type": "error"
},
{
"inputs": [],
"name": "ReentrancyGuardReentrantCall",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "SafeERC20FailedOperation",
"type": "error"
},
{
"inputs": [],
"name": "SlippageExceeded",
"type": "error"
},
{
"inputs": [],
"name": "TransferFailed",
"type": "error"
},
{
"inputs": [],
"name": "UnknownLiquidity",
"type": "error"
},
{
"inputs": [],
"name": "ZeroLiquidity",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint16",
"name": "graduationFeeBps",
"type": "uint16"
},
{
"indexed": true,
"internalType": "address",
"name": "treasury",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "feeCollector",
"type": "address"
}
],
"name": "FeeConfigured",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "liquidityId",
"type": "uint256"
},
{
"indexed": true,
"internalType": "uint256",
"name": "projectNumber",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "creatorQuote",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "protocolQuote",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokenDonated",
"type": "uint256"
}
],
"name": "FeesCollected",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "liquidityId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "platformFee",
"type": "uint256"
}
],
"name": "GraduationFeeTaken",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "graduationManager",
"type": "address"
}
],
"name": "GraduationManagerConfigured",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "quoteAsset",
"type": "address"
},
{
"indexed": true,
"internalType": "uint256",
"name": "liquidityId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint160",
"name": "sqrtPriceX96",
"type": "uint160"
},
{
"indexed": false,
"internalType": "uint128",
"name": "liquidity",
"type": "uint128"
}
],
"name": "LiquidityCreated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferStarted",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferred",
"type": "event"
},
{
"inputs": [],
"name": "acceptOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "liquidityId",
"type": "uint256"
}
],
"name": "collectFees",
"outputs": [
{
"internalType": "uint256",
"name": "creatorQuote",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "protocolQuote",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "tokenDonated",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "quoteAsset",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenAmount",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "quoteAmount",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "minTokenAmount",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "minQuoteAmount",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "createLiquidity",
"outputs": [
{
"internalType": "address",
"name": "pool",
"type": "address"
},
{
"internalType": "uint256",
"name": "liquidityId",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "feeCollector",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "graduationFeeBps",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "graduationManager",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "hook",
"outputs": [
{
"internalType": "contract EquifoldV4Hook",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "liquidityCount",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "liquidityId",
"type": "uint256"
}
],
"name": "liquidityLocked",
"outputs": [
{
"internalType": "bool",
"name": "locked",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "pendingOwner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "poolManager",
"outputs": [
{
"internalType": "contract IPoolManager",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint16",
"name": "graduationFeeBps_",
"type": "uint16"
},
{
"internalType": "address",
"name": "treasury_",
"type": "address"
},
{
"internalType": "address",
"name": "feeCollector_",
"type": "address"
}
],
"name": "setFeeConfig",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "graduationManager_",
"type": "address"
}
],
"name": "setGraduationManager",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "treasury",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes",
"name": "rawData",
"type": "bytes"
}
],
"name": "unlockCallback",
"outputs": [
{
"internalType": "bytes",
"name": "",
"type": "bytes"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"stateMutability": "payable",
"type": "receive"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { Math } from "@openzeppelin/contracts/utils/math/Math.sol";
import { Ownable, Ownable2Step } from "@openzeppelin/contracts/access/Ownable2Step.sol";
import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import { IPoolManager } from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import { IUnlockCallback } from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol";
import { IHooks } from "@uniswap/v4-core/src/interfaces/IHooks.sol";
import { PoolKey } from "@uniswap/v4-core/src/types/PoolKey.sol";
import { PoolIdLibrary } from "@uniswap/v4-core/src/types/PoolId.sol";
import { Currency } from "@uniswap/v4-core/src/types/Currency.sol";
import { BalanceDelta, BalanceDeltaLibrary } from "@uniswap/v4-core/src/types/BalanceDelta.sol";
import { ModifyLiquidityParams } from "@uniswap/v4-core/src/types/PoolOperation.sol";
import { TickMath } from "@uniswap/v4-core/src/libraries/TickMath.sol";
import { FullMath } from "@uniswap/v4-core/src/libraries/FullMath.sol";
import { LPFeeLibrary } from "@uniswap/v4-core/src/libraries/LPFeeLibrary.sol";
import { IDexAdapter } from "../interfaces/IDexAdapter.sol";
import { IWrappedNative } from "../interfaces/IWrappedNative.sol";
import { EquifoldV4Hook } from "./EquifoldV4Hook.sol";
import { LiquidityAmounts } from "./LiquidityAmounts.sol";
/// @dev Minimal call surface for {LaunchpadFeeCollector.accrueCreatorFees}. Declared locally
/// instead of importing the concrete contract — which self-pins to exactly 0.8.24 for
/// bytecode stability — so this file's flexible ^0.8.24 pragma can still compile at 0.8.26
/// alongside the v4-core test suite (which needs an exactly-0.8.26 `PoolManager`). The two
/// pragma pins are mutually exclusive within one compilation unit; a bare interface has no
/// transitive dependency on the concrete file, so it sidesteps the conflict entirely.
interface ILaunchpadFeeAccruer {
function accrueCreatorFees(
uint256 projectNumber
) external payable;
}
/// @title Equifold Uniswap v4 Graduation Adapter
/// @author Equifold
/// @notice Turns a graduated bonding curve into a Uniswap v4 pool with a dynamic (market-cap
/// tiered) fee and PERMANENTLY LOCKED full-range liquidity. Implements {IDexAdapter} so the
/// GraduationManager can call it exactly like the mock: it pulls the released token + WETH,
/// skims an optional graduation platform fee, initializes the pool at the graduation price,
/// adds full-range liquidity through the unlock/settle flow, and keeps the position forever
/// (there is no withdrawal path). Anyone can later crank {collectFees} to route the pool's
/// accrued dynamic-fee share to the project's creator and the protocol treasury.
/// @dev Consumes EXACTLY the amounts the manager approved (it pulls the full token+quote so the
/// manager returns to its pre-release baseline); any liquidity-math dust stays locked here.
contract EquifoldV4DexAdapter is IDexAdapter, IUnlockCallback, Ownable2Step, ReentrancyGuard {
using SafeERC20 for IERC20;
using BalanceDeltaLibrary for BalanceDelta;
using PoolIdLibrary for PoolKey;
error AlreadyConfigured();
error FeeCapExceeded();
error FeeConfigNotSet();
error IncorrectCaller();
error InvalidAddress();
error InvalidData();
error InvalidProject();
error InvalidSqrtPrice();
error SlippageExceeded();
error TransferFailed();
error UnknownLiquidity();
error ZeroLiquidity();
uint256 private constant Q192 = 0x1000000000000000000000000000000000000000000000000; // 2**192
int24 private constant TICK_SPACING = 60;
uint16 private constant MAX_GRADUATION_FEE_BPS = 500;
uint16 private constant MAX_BPS = 10_000;
/// @dev Discriminates the two operations {unlockCallback} can be asked to perform.
enum CallbackKind {
AddLiquidity,
Collect
}
struct AddLiquidityCallback {
PoolKey key;
int24 tickLower;
int24 tickUpper;
uint128 liquidity;
}
/// @notice Everything needed to crank fees for a graduated pool later.
struct Position {
PoolKey key;
address token;
address quoteAsset;
uint256 projectNumber;
}
IPoolManager public immutable poolManager;
EquifoldV4Hook public immutable hook;
address public graduationManager;
uint256 public liquidityCount;
mapping(uint256 liquidityId => bool locked) public liquidityLocked;
mapping(uint256 liquidityId => Position) private _positions;
/// @dev Quote assets this adapter has ever built a pool against; gates {receive}.
mapping(address quoteAsset => bool known) private _knownQuoteAssets;
/// @notice Basis points of each graduation's quote amount skimmed to {treasury} (0 = disabled).
uint16 public graduationFeeBps;
/// @notice Protocol fee destination for both the graduation skim and the collect-fee split.
address public treasury;
/// @notice The {LaunchpadFeeCollector} that receives and splits each project's creator share.
address public feeCollector;
event GraduationManagerConfigured(address indexed graduationManager);
event FeeConfigured(
uint16 graduationFeeBps, address indexed treasury, address indexed feeCollector
);
event LiquidityCreated(
address indexed token,
address indexed quoteAsset,
uint256 indexed liquidityId,
uint160 sqrtPriceX96,
uint128 liquidity
);
event GraduationFeeTaken(uint256 indexed liquidityId, uint256 platformFee);
event FeesCollected(
uint256 indexed liquidityId,
uint256 indexed projectNumber,
uint256 creatorQuote,
uint256 protocolQuote,
uint256 tokenDonated
);
constructor(
IPoolManager poolManager_,
EquifoldV4Hook hook_,
address initialOwner
) Ownable(initialOwner) {
if (
address(poolManager_) == address(0) || address(hook_) == address(0)
|| initialOwner == address(0)
) revert InvalidAddress();
poolManager = poolManager_;
hook = hook_;
}
/// @notice One-time wiring of the GraduationManager allowed to call `createLiquidity`.
function setGraduationManager(
address graduationManager_
) external onlyOwner {
if (graduationManager != address(0)) revert AlreadyConfigured();
if (graduationManager_ == address(0) || graduationManager_.code.length == 0) {
revert InvalidAddress();
}
graduationManager = graduationManager_;
emit GraduationManagerConfigured(graduationManager_);
}
/// @notice Configures the graduation platform fee and where every {collectFees} payout is
/// routed (creator share via {feeCollector}, protocol remainder via {treasury}). The
/// creator/protocol split itself is no longer a ratio set here — it's the hook's exact
/// per-swap bookkeeping, see {EquifoldV4Hook.consumeCreatorOwed}. Owner-gated; may be
/// re-run to retune. Until this is called the graduation fee defaults to zero (fully
/// backward compatible with pools graduated before it is set).
function setFeeConfig(
uint16 graduationFeeBps_,
address treasury_,
address feeCollector_
) external onlyOwner {
if (graduationFeeBps_ > MAX_GRADUATION_FEE_BPS) {
revert FeeCapExceeded();
}
if (
treasury_ == address(0) || feeCollector_ == address(0) || feeCollector_.code.length == 0
) {
revert InvalidAddress();
}
graduationFeeBps = graduationFeeBps_;
treasury = treasury_;
feeCollector = feeCollector_;
emit FeeConfigured(graduationFeeBps_, treasury_, feeCollector_);
}
/// @inheritdoc IDexAdapter
function createLiquidity(
address token,
address quoteAsset,
uint256 tokenAmount,
uint256 quoteAmount,
uint256 minTokenAmount,
uint256 minQuoteAmount,
bytes calldata data
) external nonReentrant returns (address pool, uint256 liquidityId) {
uint256 projectNumber = _validateCreateLiquidity(
token, quoteAsset, tokenAmount, quoteAmount, minTokenAmount, minQuoteAmount, data
);
// Pull the full released amounts so the manager returns to its pre-release baseline.
IERC20(token).safeTransferFrom(msg.sender, address(this), tokenAmount);
IERC20(quoteAsset).safeTransferFrom(msg.sender, address(this), quoteAmount);
liquidityId =
_buildAndStoreLiquidity(token, quoteAsset, tokenAmount, quoteAmount, projectNumber);
// The adapter itself is the permanent locker: it holds the position and has no exit path.
return (address(this), liquidityId);
}
/// @dev Validates a `createLiquidity` call and returns the decoded project number. Split out of
/// `createLiquidity` to keep that function's stack shallow.
function _validateCreateLiquidity(
address token,
address quoteAsset,
uint256 tokenAmount,
uint256 quoteAmount,
uint256 minTokenAmount,
uint256 minQuoteAmount,
bytes calldata data
) private view returns (uint256 projectNumber) {
if (msg.sender != graduationManager) revert IncorrectCaller();
if (data.length != 32) revert InvalidData();
projectNumber = abi.decode(data, (uint256));
if (projectNumber == 0) revert InvalidProject();
if (token == address(0) || quoteAsset == address(0)) revert InvalidAddress();
if (tokenAmount < minTokenAmount || quoteAmount < minQuoteAmount) {
revert SlippageExceeded();
}
}
/// @dev Skims the graduation platform fee, builds the pool with the net quote amount, and
/// records the position. Split out of `createLiquidity` to keep that function's stack
/// shallow (assumes the full token+quote amounts were already pulled from the caller).
function _buildAndStoreLiquidity(
address token,
address quoteAsset,
uint256 tokenAmount,
uint256 quoteAmount,
uint256 projectNumber
) private returns (uint256 liquidityId) {
// Graduation platform fee: skimmed from the quote side before the pool is seeded. Zero
// (the default until {setFeeConfig} is called) skims nothing, so this is backward compatible.
(uint256 netQuoteAmount, uint256 platformFee) = _takeGraduationFee(quoteAsset, quoteAmount);
(uint160 sqrtPriceX96, uint128 liquidity, PoolKey memory key) =
_createPool(token, quoteAsset, tokenAmount, netQuoteAmount);
liquidityId = ++liquidityCount;
liquidityLocked[liquidityId] = true;
_knownQuoteAssets[quoteAsset] = true;
_positions[liquidityId] = Position({
key: key, token: token, quoteAsset: quoteAsset, projectNumber: projectNumber
});
if (platformFee > 0) emit GraduationFeeTaken(liquidityId, platformFee);
emit LiquidityCreated(token, quoteAsset, liquidityId, sqrtPriceX96, liquidity);
}
/// @dev Transfers `graduationFeeBps` of `quoteAmount` to {treasury} (no-op while unset) and
/// returns the remaining net amount the pool should be seeded with.
function _takeGraduationFee(
address quoteAsset,
uint256 quoteAmount
) private returns (uint256 netQuoteAmount, uint256 platformFee) {
platformFee = (quoteAmount * graduationFeeBps) / MAX_BPS;
netQuoteAmount = quoteAmount - platformFee;
if (platformFee > 0) {
IERC20(quoteAsset).safeTransfer(treasury, platformFee);
}
}
/// @notice Permissionlessly cranks a graduated pool's accrued dynamic-fee share. Pokes the
/// permanently-locked position, donates the token-side amount straight back into the
/// pool (it never leaves the locked liquidity), and splits the quote-side amount taken
/// out between the project's creator (via {LaunchpadFeeCollector.accrueCreatorFees}) and
/// the protocol {treasury}.
/// @dev Reverts if `liquidityId` is unknown or fee config was never set; returns zeros without
/// reverting when nothing has accrued since the last crank.
function collectFees(
uint256 liquidityId
)
external
nonReentrant
returns (uint256 creatorQuote, uint256 protocolQuote, uint256 tokenDonated)
{
Position memory position = _requirePosition(liquidityId);
if (treasury == address(0) || feeCollector == address(0)) revert FeeConfigNotSet();
bytes memory result =
poolManager.unlock(abi.encode(CallbackKind.Collect, abi.encode(liquidityId)));
uint256 quoteCollected;
(quoteCollected, tokenDonated) = abi.decode(result, (uint256, uint256));
if (quoteCollected > 0) {
(creatorQuote, protocolQuote) = _splitAndRoute(position, quoteCollected);
}
emit FeesCollected(
liquidityId, position.projectNumber, creatorQuote, protocolQuote, tokenDonated
);
}
/// @dev Pulls the hook's exact per-swap creator bookkeeping for this pool (atomically zeroing
/// it), clamps it to what was actually collected (bookkeeping can round up by a few wei
/// relative to the position's own fee-growth accounting), and routes each share; unwraps
/// the quote asset (assumed WETH-like) to ETH for both the creator accrual and the
/// treasury transfer.
function _splitAndRoute(
Position memory position,
uint256 quoteCollected
) private returns (uint256 creatorQuote, uint256 protocolQuote) {
uint256 owed = hook.consumeCreatorOwed(position.key.toId());
creatorQuote = owed > quoteCollected ? quoteCollected : owed;
protocolQuote = quoteCollected - creatorQuote;
if (creatorQuote > 0) {
IWrappedNative(position.quoteAsset).withdraw(creatorQuote);
ILaunchpadFeeAccruer(feeCollector).accrueCreatorFees{ value: creatorQuote }(
position.projectNumber
);
}
if (protocolQuote > 0) {
IWrappedNative(position.quoteAsset).withdraw(protocolQuote);
(bool success,) = payable(treasury).call{ value: protocolQuote }("");
if (!success) revert TransferFailed();
}
}
/// @dev Initializes the v4 pool at the graduation price, registers it with the hook, and adds
/// full-range liquidity through the unlock/settle flow. Returns the price, liquidity, and
/// key (the latter stored per-liquidityId for later {collectFees} cranks).
function _createPool(
address token,
address quoteAsset,
uint256 tokenAmount,
uint256 quoteAmount
) private returns (uint160 sqrtPriceX96, uint128 liquidity, PoolKey memory key) {
key = _buildKey(token, quoteAsset);
int24 tickLower;
int24 tickUpper;
(sqrtPriceX96, liquidity, tickLower, tickUpper) =
_priceAndLiquidity(token < quoteAsset, tokenAmount, quoteAmount);
if (liquidity == 0) revert ZeroLiquidity();
poolManager.initialize(key, sqrtPriceX96);
hook.registerPool(key, token);
poolManager.unlock(
abi.encode(
CallbackKind.AddLiquidity,
abi.encode(
AddLiquidityCallback({
key: key, tickLower: tickLower, tickUpper: tickUpper, liquidity: liquidity
})
)
)
);
}
/// @dev Builds the pool key with the canonical currency ordering (currency0 < currency1).
function _buildKey(
address token,
address quoteAsset
) private view returns (PoolKey memory key) {
(address currency0, address currency1) =
token < quoteAsset ? (token, quoteAsset) : (quoteAsset, token);
key = PoolKey({
currency0: Currency.wrap(currency0),
currency1: Currency.wrap(currency1),
fee: LPFeeLibrary.DYNAMIC_FEE_FLAG,
tickSpacing: TICK_SPACING,
hooks: IHooks(address(hook))
});
}
/// @dev The graduation-price sqrt (`sqrt(amount1/amount0) * 2^96`) and the full-range liquidity.
function _priceAndLiquidity(
bool tokenIsCurrency0,
uint256 tokenAmount,
uint256 quoteAmount
)
private
pure
returns (uint160 sqrtPriceX96, uint128 liquidity, int24 tickLower, int24 tickUpper)
{
(uint256 amount0, uint256 amount1) =
tokenIsCurrency0 ? (tokenAmount, quoteAmount) : (quoteAmount, tokenAmount);
uint256 sqrtPrice = Math.sqrt(FullMath.mulDiv(amount1, Q192, amount0));
// Reject any price v4 itself would reject; this also makes the uint160 cast non-truncating.
if (sqrtPrice < TickMath.MIN_SQRT_PRICE || sqrtPrice > TickMath.MAX_SQRT_PRICE) {
revert InvalidSqrtPrice();
}
// forge-lint: disable-next-line(unsafe-typecast)
sqrtPriceX96 = uint160(sqrtPrice); // bounded above by MAX_SQRT_PRICE < 2**160
(tickLower, tickUpper) = _fullRangeTicks();
liquidity = LiquidityAmounts.getLiquidityForAmounts(
sqrtPriceX96,
TickMath.getSqrtPriceAtTick(tickLower),
TickMath.getSqrtPriceAtTick(tickUpper),
amount0,
amount1
);
}
/// @dev The full-range tick bounds every Equifold graduation position uses.
function _fullRangeTicks() private pure returns (int24 tickLower, int24 tickUpper) {
tickLower = TickMath.minUsableTick(TICK_SPACING);
tickUpper = TickMath.maxUsableTick(TICK_SPACING);
}
/// @dev Reverts with {UnknownLiquidity} unless `liquidityId` has a stored position.
function _requirePosition(
uint256 liquidityId
) private view returns (Position memory position) {
position = _positions[liquidityId];
if (position.token == address(0)) revert UnknownLiquidity();
}
/// @notice v4 unlock callback: dispatches to the add-liquidity or fee-collection handler.
function unlockCallback(
bytes calldata rawData
) external returns (bytes memory) {
if (msg.sender != address(poolManager)) revert IncorrectCaller();
(CallbackKind kind, bytes memory payload) = abi.decode(rawData, (CallbackKind, bytes));
if (kind == CallbackKind.AddLiquidity) return _handleAddLiquidity(payload);
return _handleCollect(payload);
}
/// @dev Adds the full-range liquidity for a fresh graduation and settles what the pool is owed.
function _handleAddLiquidity(
bytes memory payload
) private returns (bytes memory) {
AddLiquidityCallback memory data = abi.decode(payload, (AddLiquidityCallback));
// uint128 liquidity always fits int256; positive delta adds liquidity.
// forge-lint: disable-next-line(unsafe-typecast)
int256 liquidityDelta = int256(uint256(data.liquidity));
(BalanceDelta delta,) = poolManager.modifyLiquidity(
data.key,
ModifyLiquidityParams({
tickLower: data.tickLower,
tickUpper: data.tickUpper,
liquidityDelta: liquidityDelta,
salt: bytes32(0)
}),
""
);
_settle(data.key.currency0, delta.amount0());
_settle(data.key.currency1, delta.amount1());
return "";
}
/// @dev Pokes the permanently-locked position for accrued fees (a zero-liquidity-delta
/// `modifyLiquidity` call), donates the token-side amount straight back to the pool so it
/// neither needs taking nor settling, and takes the quote-side amount to this adapter for
/// {collectFees} to split. Returns `abi.encode(quoteOwed, tokenOwed)`.
function _handleCollect(
bytes memory payload
) private returns (bytes memory) {
uint256 liquidityId = abi.decode(payload, (uint256));
Position storage position = _positions[liquidityId];
(int24 tickLower, int24 tickUpper) = _fullRangeTicks();
(BalanceDelta delta,) = poolManager.modifyLiquidity(
position.key,
ModifyLiquidityParams({
tickLower: tickLower, tickUpper: tickUpper, liquidityDelta: 0, salt: bytes32(0)
}),
""
);
bool tokenIsCurrency0 = Currency.unwrap(position.key.currency0) == position.token;
(uint256 tokenOwed, uint256 quoteOwed) = _splitByToken(delta, tokenIsCurrency0);
if (tokenOwed > 0) {
(uint256 donate0, uint256 donate1) =
tokenIsCurrency0 ? (tokenOwed, uint256(0)) : (uint256(0), tokenOwed);
poolManager.donate(position.key, donate0, donate1, "");
}
if (quoteOwed > 0) {
Currency quoteCurrency =
tokenIsCurrency0 ? position.key.currency1 : position.key.currency0;
poolManager.take(quoteCurrency, address(this), quoteOwed);
}
return abi.encode(quoteOwed, tokenOwed);
}
/// @dev Maps a poke's raw currency0/currency1 delta onto (tokenOwed, quoteOwed), clamped to 0.
function _splitByToken(
BalanceDelta delta,
bool tokenIsCurrency0
) private pure returns (uint256 tokenOwed, uint256 quoteOwed) {
uint256 amount0 = _positiveAmount(delta.amount0());
uint256 amount1 = _positiveAmount(delta.amount1());
(tokenOwed, quoteOwed) = tokenIsCurrency0 ? (amount0, amount1) : (amount1, amount0);
}
/// @dev Returns `amount` as a uint256 when positive (owed to this adapter), else 0.
function _positiveAmount(
int128 amount
) private pure returns (uint256) {
if (amount <= 0) return 0;
// forge-lint: disable-next-line(unsafe-typecast)
return uint256(uint128(amount));
}
/// @dev Pays the pool manager any currency this adapter owes (negative delta).
function _settle(
Currency currency,
int128 amount
) private {
if (amount >= 0) return; // nothing owed on this side
// amount < 0 here, so -amount is a positive int128 that fits uint128.
// forge-lint: disable-next-line(unsafe-typecast)
uint256 owed = uint256(uint128(-amount));
poolManager.sync(currency);
IERC20(Currency.unwrap(currency)).safeTransfer(address(poolManager), owed);
poolManager.settle();
}
/// @notice Accepts ETH only when unwrapping a known quote asset during {collectFees}.
receive() external payable {
if (!_knownQuoteAssets[msg.sender]) revert IncorrectCaller();
}
}
Chain explorer1829msChain node83ms