-
Notifications
You must be signed in to change notification settings - Fork 25
Feat/multicurve rehype #445
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
kinrezC
wants to merge
15
commits into
main
Choose a base branch
from
feat/multicurve-rehype
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
44ea1f4
feat(rehype): scaffold rehype multicurve hook
kinrezC a7252de
poc
kinrezC acd756b
wip
kinrezC f80994d
wip
kinrezC 183ea6b
working example
kinrezC 2f56706
fix: rehype accounting to not use balances
kinrezC 6007d05
fix(logs): remove spurious logs
kinrezC d3ce7e4
minor fixes
kinrezC 1ba6582
add asset swapping test
kinrezC 7a8031a
add rehype initializer, add buyback/beneficiary/lp percentWad model
kinrezC 0760d5a
wip integration tests
kinrezC ab7fd04
fix: fees net out on collectFees test
kinrezC a54b850
chore: cleanup types + interfaces
kinrezC 1e65371
feat: add numeraire buyback
kinrezC 802221c
chore: inline docs explanation on poolManager balance checks
kinrezC File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| { | ||
| "lib/forge-std": { | ||
| "rev": "d3db4ef90a72b7d24aa5a2e5c649593eaef7801d" | ||
| }, | ||
| "lib/solady": { | ||
| "rev": "513f581675374706dbe947284d6b12d19ce35a2a" | ||
| }, | ||
| "lib/universal-router": { | ||
| "rev": "3fc2b48b66a43d7da649809b301dfb271779b0b5" | ||
| }, | ||
| "lib/v3-core": { | ||
| "rev": "e3589b192d0be27e100cd0daaf6c97204fdb1899" | ||
| }, | ||
| "lib/v3-periphery": { | ||
| "rev": "80f26c86c57b8a5e4b913f42844d4c8bd274d058" | ||
| }, | ||
| "lib/v4-core": { | ||
| "rev": "80311e34080fee64b6fc6c916e9a51a437d0e482" | ||
| }, | ||
| "lib/v4-periphery": { | ||
| "rev": "9628c36b4f5083d19606e63224e4041fe748edae" | ||
| } | ||
| } |
Submodule view-quoter-v4
added at
f05058
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,304 @@ | ||
| // SPDX-License-Identifier: BUSL-1.1 | ||
| pragma solidity ^0.8.13; | ||
|
|
||
| import { TickMath } from "@v4-core/libraries/TickMath.sol"; | ||
| import { StateLibrary } from "@v4-core/libraries/StateLibrary.sol"; | ||
| import { IPoolManager } from "@v4-core/interfaces/IPoolManager.sol"; | ||
| import { PoolId, PoolIdLibrary } from "@v4-core/types/PoolId.sol"; | ||
| import { PoolKey } from "@v4-core/types/PoolKey.sol"; | ||
| import { Currency, CurrencyLibrary } from "@v4-core/types/Currency.sol"; | ||
| import { IHooks } from "@v4-core/interfaces/IHooks.sol"; | ||
| import { BalanceDelta, BalanceDeltaLibrary } from "@v4-core/types/BalanceDelta.sol"; | ||
| import { SafeTransferLib } from "@solady/utils/SafeTransferLib.sol"; | ||
|
|
||
| import { FeesManager } from "src/base/FeesManager.sol"; | ||
| import { Position } from "src/types/Position.sol"; | ||
| import { MiniV4Manager } from "src/base/MiniV4Manager.sol"; | ||
| import { IPoolInitializer } from "src/interfaces/IPoolInitializer.sol"; | ||
| import { ImmutableAirlock } from "src/base/ImmutableAirlock.sol"; | ||
| import { BeneficiaryData, MIN_PROTOCOL_OWNER_SHARES } from "src/types/BeneficiaryData.sol"; | ||
| import { calculatePositions, adjustCurves, Curve } from "src/libraries/Multicurve.sol"; | ||
| import { IRehypeHook } from "src/interfaces/IRehypeHook.sol"; | ||
|
|
||
| /** | ||
| * @notice Emitted when a new pool is locked | ||
| * @param pool Address of the Uniswap V4 pool key | ||
| * @param beneficiaries Array of beneficiaries with their shares | ||
| */ | ||
| event Lock(address indexed pool, BeneficiaryData[] beneficiaries); | ||
|
|
||
| /// @notice Thrown when the pool is already initialized | ||
| error PoolAlreadyInitialized(); | ||
|
|
||
| /// @notice Thrown when the pool is already exited | ||
| error PoolAlreadyExited(); | ||
|
|
||
| /// @notice Thrown when the pool is not locked but collect is called | ||
| error PoolNotLocked(); | ||
|
|
||
| /// @notice Thrown when the current tick is not sufficient to migrate | ||
| error CannotMigrateInsufficientTick(int24 targetTick, int24 currentTick); | ||
|
|
||
| /** | ||
| * @notice Data used to initialize the Uniswap V4 pool | ||
| * @param fee Fee of the Uniswap V4 pool (capped at 1_000_000) | ||
| * @param tickSpacing Tick spacing for the Uniswap V4 pool | ||
| * @param curves Array of curves to distribute liquidity across | ||
| * @param beneficiaries Array of beneficiaries with their shares | ||
| */ | ||
| struct InitData { | ||
| uint24 fee; | ||
| int24 tickSpacing; | ||
| Curve[] curves; | ||
| BeneficiaryData[] beneficiaries; | ||
| uint24 customFee; | ||
| uint96 assetBuybackPercentWad; | ||
| uint96 numeraireBuybackPercentWad; | ||
| uint96 beneficiaryPercentWad; | ||
| uint96 lpPercentWad; | ||
| } | ||
|
|
||
| /// @notice Possible status of a pool, note a locked pool cannot be exited | ||
| enum PoolStatus { | ||
| Uninitialized, | ||
| Initialized, | ||
| Locked, | ||
| Exited | ||
| } | ||
|
|
||
| /** | ||
| * @notice State of a pool | ||
| * @param numeraire Address of the numeraire currency | ||
| * @param beneficiaries Array of beneficiaries with their shares | ||
| * @param positions Array of positions held in the pool | ||
| * @param status Current status of the pool | ||
| * @param poolKey Key of the Uniswap V4 pool | ||
| * @param farTick The farthest tick that must be reached to allow exiting liquidity | ||
| */ | ||
| struct PoolState { | ||
| address numeraire; | ||
| BeneficiaryData[] beneficiaries; | ||
| Position[] positions; | ||
| PoolStatus status; | ||
| PoolKey poolKey; | ||
| int24 farTick; | ||
| } | ||
|
|
||
| /** | ||
| * @title Doppler Uniswap V4 Multicurve Initializer | ||
| * @author Whetstone Research | ||
| * @custom:security-contact [email protected] | ||
| * @notice Initializes a fresh Uniswap V4 pool and distributes liquidity across multiple positions, as | ||
| * described in the Doppler Multicurve whitepaper (https://www.doppler.lol/multicurve.pdf). | ||
| * | ||
| * Liquidity pools can be initialized with two different flows: | ||
| * | ||
| * A. No beneficiaries (possible migration) | ||
| * | ||
| * ┌─────────────┐┌───────────┐ ┌──────┐ | ||
| * │Uninitialized││Initialized│ │Exited│ | ||
| * └──────┬──────┘└─────┬─────┘ └──┬───┘ | ||
| * │ │ │ | ||
| * │initialize() │ │ | ||
| * │────────────>│ │ | ||
| * │ │ │ | ||
| * │ │exitLiquidity()│ | ||
| * │ │──────────────>│ | ||
| * ┌──────┴──────┐┌─────┴─────┐ ┌──┴───┐ | ||
| * │Uninitialized││Initialized│ │Exited│ | ||
| * └─────────────┘└───────────┘ └──────┘ | ||
| * | ||
| * | ||
| * B. With beneficiaries (locked pool, no migration) | ||
| * | ||
| * ┌─────────────┐ ┌──────┐ | ||
| * │Uninitialized│ │Locked│ | ||
| * └──────┬──────┘ └──┬───┘ | ||
| * │ │ | ||
| * │initialize()│ | ||
| * │───────────>│ | ||
| * ┌──────┴──────┐ ┌──┴───┐ | ||
| * │Uninitialized│ │Locked│ | ||
| * └─────────────┘ └──────┘ | ||
| * | ||
| * Passing beneficiaries during the initialization will "lock" the pool, preventing any future migration. However | ||
| * this will allow the collection of fees by the designed beneficiaries. If no beneficiaries are passed, the pool | ||
| * can be migrated later if the conditions are met. | ||
| */ | ||
| contract UniswapV4MulticurveRehypeInitializer is IPoolInitializer, FeesManager, ImmutableAirlock, MiniV4Manager { | ||
| using StateLibrary for IPoolManager; | ||
| using PoolIdLibrary for PoolKey; | ||
| using CurrencyLibrary for Currency; | ||
| using BalanceDeltaLibrary for BalanceDelta; | ||
|
|
||
| /// @notice Address of the Uniswap V4 Multicurve hook | ||
| IHooks public immutable HOOK; | ||
|
|
||
| /// @notice Returns the state of a pool | ||
| mapping(address asset => PoolState state) public getState; | ||
|
|
||
| /// @notice Maps a Uniswap V4 poolId to its associated asset | ||
| mapping(PoolId poolId => address asset) internal getAsset; | ||
|
|
||
| /** | ||
| * @param airlock_ Address of the Airlock contract | ||
| * @param poolManager_ Address of the Uniswap V4 pool manager | ||
| * @param hook_ Address of the UniswapV4MulticurveInitializerHook | ||
| */ | ||
| constructor( | ||
| address airlock_, | ||
| IPoolManager poolManager_, | ||
| IHooks hook_ | ||
| ) ImmutableAirlock(airlock_) MiniV4Manager(poolManager_) { | ||
| HOOK = hook_; | ||
| } | ||
|
|
||
| /// @inheritdoc IPoolInitializer | ||
| function initialize( | ||
| address asset, | ||
| address numeraire, | ||
| uint256 totalTokensOnBondingCurve, | ||
| bytes32, | ||
| bytes calldata data | ||
| ) external onlyAirlock returns (address) { | ||
| require(getState[asset].status == PoolStatus.Uninitialized, PoolAlreadyInitialized()); | ||
|
|
||
| InitData memory initData = abi.decode(data, (InitData)); | ||
|
|
||
| (uint24 fee, int24 tickSpacing, Curve[] memory curves, BeneficiaryData[] memory beneficiaries) = | ||
| (initData.fee, initData.tickSpacing, initData.curves, initData.beneficiaries); | ||
|
|
||
| PoolKey memory poolKey = PoolKey({ | ||
| currency0: asset < numeraire ? Currency.wrap(asset) : Currency.wrap(numeraire), | ||
| currency1: asset < numeraire ? Currency.wrap(numeraire) : Currency.wrap(asset), | ||
| hooks: HOOK, | ||
| fee: fee, | ||
| tickSpacing: tickSpacing | ||
| }); | ||
| bool isToken0 = asset == Currency.unwrap(poolKey.currency0); | ||
|
|
||
| (Curve[] memory adjustedCurves, int24 tickLower, int24 tickUpper) = | ||
| adjustCurves(curves, 0, tickSpacing, isToken0); | ||
|
|
||
| int24 startTick = isToken0 ? tickLower : tickUpper; | ||
| uint160 sqrtPriceX96 = TickMath.getSqrtPriceAtTick(startTick); | ||
| poolManager.initialize(poolKey, sqrtPriceX96); | ||
|
|
||
| Position[] memory positions = | ||
| calculatePositions(adjustedCurves, tickSpacing, totalTokensOnBondingCurve, 0, isToken0); | ||
|
|
||
| PoolState memory state = PoolState({ | ||
| numeraire: numeraire, | ||
| beneficiaries: beneficiaries, | ||
| positions: positions, | ||
| status: beneficiaries.length != 0 ? PoolStatus.Locked : PoolStatus.Initialized, | ||
| poolKey: poolKey, | ||
| farTick: isToken0 ? tickUpper : tickLower | ||
| }); | ||
|
|
||
| getState[asset] = state; | ||
| getAsset[poolKey.toId()] = asset; | ||
|
|
||
| SafeTransferLib.safeTransferFrom(asset, address(airlock), address(this), totalTokensOnBondingCurve); | ||
| _mint(poolKey, positions); | ||
|
|
||
| emit Create(address(poolManager), asset, numeraire); | ||
|
|
||
| IRehypeHook(address(HOOK)) | ||
| .setFeeDistributionForPool( | ||
| poolKey.toId(), | ||
| asset, | ||
| numeraire, | ||
| address(1), // temp hardcode to address(1) for testing | ||
| initData.customFee, | ||
| initData.assetBuybackPercentWad, | ||
| initData.numeraireBuybackPercentWad, | ||
| initData.beneficiaryPercentWad, | ||
| initData.lpPercentWad | ||
| ); | ||
|
|
||
| if (beneficiaries.length != 0) { | ||
| _storeBeneficiaries(poolKey, beneficiaries, airlock.owner(), MIN_PROTOCOL_OWNER_SHARES); | ||
| emit Lock(asset, beneficiaries); | ||
| } | ||
|
|
||
| // If any dust asset tokens are left in this contract after providing liquidity, we send them | ||
| // back to the Airlock so they'll be transferred to the associated governance or burnt | ||
| if (Currency.wrap(asset).balanceOfSelf() > 0) { | ||
| Currency.wrap(asset).transfer(address(airlock), Currency.wrap(asset).balanceOfSelf()); | ||
| } | ||
|
|
||
| // Uniswap V4 pools don't have addresses, so we are returning the asset address | ||
| // instead to retrieve the associated state later during the `exitLiquidity` call | ||
| return asset; | ||
| } | ||
|
|
||
| /// @inheritdoc IPoolInitializer | ||
| function exitLiquidity( | ||
| address asset | ||
| ) | ||
| external | ||
| onlyAirlock | ||
| returns ( | ||
| uint160 sqrtPriceX96, | ||
| address token0, | ||
| uint128 fees0, | ||
| uint128 balance0, | ||
| address token1, | ||
| uint128 fees1, | ||
| uint128 balance1 | ||
| ) | ||
| { | ||
| PoolState memory state = getState[asset]; | ||
| require(state.status == PoolStatus.Initialized, PoolAlreadyExited()); | ||
| getState[asset].status = PoolStatus.Exited; | ||
|
|
||
| token0 = Currency.unwrap(state.poolKey.currency0); | ||
| token1 = Currency.unwrap(state.poolKey.currency1); | ||
|
|
||
| (, int24 tick,,) = poolManager.getSlot0(state.poolKey.toId()); | ||
| int24 farTick = state.farTick; | ||
| require(asset == token0 ? tick >= farTick : tick <= farTick, CannotMigrateInsufficientTick(farTick, tick)); | ||
| sqrtPriceX96 = TickMath.getSqrtPriceAtTick(farTick); | ||
|
|
||
| (BalanceDelta balanceDelta, BalanceDelta feesAccrued) = _burn(state.poolKey, state.positions); | ||
| balance0 = uint128(balanceDelta.amount0()); | ||
| balance1 = uint128(balanceDelta.amount1()); | ||
| fees0 = uint128(feesAccrued.amount0()); | ||
| fees1 = uint128(feesAccrued.amount1()); | ||
|
|
||
| state.poolKey.currency0.transfer(msg.sender, balance0); | ||
| state.poolKey.currency1.transfer(msg.sender, balance1); | ||
| } | ||
|
|
||
| /** | ||
| * @notice Returns the positions currently held in the Uniswap V4 pool for the given `asset` | ||
| * @param asset Address of the asset used for the Uniswap V4 pool | ||
| * @return Array of positions currently held in the Uniswap V4 pool | ||
| */ | ||
| function getPositions( | ||
| address asset | ||
| ) external view returns (Position[] memory) { | ||
| return getState[asset].positions; | ||
| } | ||
|
|
||
| /** | ||
| * @notice Returns the beneficiaries and their shares for the given `asset` | ||
| * @param asset Address of the asset used for the Uniswap V4 pool | ||
| * @return Array of beneficiaries with their shares | ||
| */ | ||
| function getBeneficiaries( | ||
| address asset | ||
| ) external view returns (BeneficiaryData[] memory) { | ||
| return getState[asset].beneficiaries; | ||
| } | ||
|
|
||
| /// @inheritdoc FeesManager | ||
| function _collectFees( | ||
| PoolId poolId | ||
| ) internal override returns (BalanceDelta fees) { | ||
| PoolState memory state = getState[getAsset[poolId]]; | ||
| require(state.status == PoolStatus.Locked, PoolNotLocked()); | ||
| fees = IRehypeHook(address(HOOK)).collectFees(poolId); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a reminder to update this before finalizing