-
Notifications
You must be signed in to change notification settings - Fork 510
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* in progress full range * rename * in progress * compiling * basic tests passing * erc20 contract deploy * initial liquidity deposit working * removeLiquidity tests * withdrawing liquidity works * merge with main * in progress * compiling code with rebalancing * basic liquidity add tests with fee working * in progress add liq swap tests * debug add liquidity rebalance * simple rebalance working * removing liquidity tests passing but check implementation * add gas snapshots * add blockNumber and liquidity token addr to poolinfo struct, update gas snapshots * update erc20 contract, rerun gas snapshots, remove create2 * readd create2, rerun gas snapshots with create2 * increase amounts used in tests * update tests and debug rebalance * fixed rebalancing dust issue * working tests and fixed rebalancing * fix dependencies * debug lib * some code cleanup and v4-core update * some more cleanup * some more code cleanup * Optimize v2 hook (#49) * sload and swrite on every swap * sload on every swap, only write if need to * remove console.logs * rebalance and reinvest dust with donate * remove no rebalance test * some code cleanup * test cleanup and store poolId * remove liquidity in pool info struct * comment + test cleanup * deploy erc20 with custom liquidity token name * cleanup and add custom tick spacing error * add minimum liquidity * make rebalance public * fuzz testing * clean up tests * in progress pr comment addressing * fix burning liquidity, some code cleanup * uncached storage slot remove liquidity gas test * use safe cast library * fix minimum liquidity error, add price slippage test * address pr comments * fix remove liquidity fuzz test * add fuzz test for swapping and removing all liquidity * add univ4 to erc20 token names * update init snapshot * fix max tick liquidity * custom errors
- Loading branch information
Showing
17 changed files
with
1,394 additions
and
2 deletions.
There are no files selected for viewing
This file contains 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 @@ | ||
412012 |
This file contains 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 @@ | ||
206278 |
This file contains 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 @@ | ||
152057 |
This file contains 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 @@ | ||
878152 |
This file contains 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 @@ | ||
199416 |
This file contains 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 @@ | ||
375677 |
This file contains 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 @@ | ||
109596 |
This file contains 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 @@ | ||
150064 |
This file contains 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 |
---|---|---|
|
@@ -10,3 +10,6 @@ | |
[submodule "lib/v4-core"] | ||
path = lib/v4-core | ||
url = [email protected]:Uniswap/v4-core.git | ||
[submodule "lib/solmate"] | ||
path = lib/solmate | ||
url = https://github.com/transmissions11/solmate |
This file contains 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,362 @@ | ||
// SPDX-License-Identifier: UNLICENSED | ||
pragma solidity ^0.8.19; | ||
|
||
import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; | ||
import {PoolManager} from "@uniswap/v4-core/contracts/PoolManager.sol"; | ||
import {Hooks} from "@uniswap/v4-core/contracts/libraries/Hooks.sol"; | ||
import {BaseHook} from "../../BaseHook.sol"; | ||
import {SafeCast} from "@uniswap/v4-core/contracts/libraries/SafeCast.sol"; | ||
import {IHooks} from "@uniswap/v4-core/contracts/interfaces/IHooks.sol"; | ||
import {CurrencyLibrary, Currency} from "@uniswap/v4-core/contracts/types/Currency.sol"; | ||
import {TickMath} from "@uniswap/v4-core/contracts/libraries/TickMath.sol"; | ||
import {BalanceDelta} from "@uniswap/v4-core/contracts/types/BalanceDelta.sol"; | ||
import {IERC20Minimal} from "@uniswap/v4-core/contracts/interfaces/external/IERC20Minimal.sol"; | ||
import {ILockCallback} from "@uniswap/v4-core/contracts/interfaces/callback/ILockCallback.sol"; | ||
import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/contracts/types/PoolId.sol"; | ||
import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; | ||
import {FullMath} from "@uniswap/v4-core/contracts/libraries/FullMath.sol"; | ||
import {UniswapV4ERC20} from "../../libraries/UniswapV4ERC20.sol"; | ||
import {FixedPoint96} from "@uniswap/v4-core/contracts/libraries/FixedPoint96.sol"; | ||
import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; | ||
import {IERC20Metadata} from "@openzeppelin/contracts/interfaces/IERC20Metadata.sol"; | ||
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; | ||
|
||
import "../../libraries/LiquidityAmounts.sol"; | ||
|
||
contract FullRange is BaseHook, ILockCallback { | ||
using CurrencyLibrary for Currency; | ||
using PoolIdLibrary for PoolKey; | ||
using SafeCast for uint256; | ||
using SafeCast for uint128; | ||
|
||
/// @notice Thrown when trying to interact with a non-initialized pool | ||
error PoolNotInitialized(); | ||
error TickSpacingNotDefault(); | ||
error LiquidityDoesntMeetMinimum(); | ||
error SenderMustBeHook(); | ||
error ExpiredPastDeadline(); | ||
error TooMuchSlippage(); | ||
|
||
/// @dev Min tick for full range with tick spacing of 60 | ||
int24 internal constant MIN_TICK = -887220; | ||
/// @dev Max tick for full range with tick spacing of 60 | ||
int24 internal constant MAX_TICK = -MIN_TICK; | ||
|
||
int256 internal constant MAX_INT = type(int256).max; | ||
uint16 internal constant MINIMUM_LIQUIDITY = 1000; | ||
|
||
struct CallbackData { | ||
address sender; | ||
PoolKey key; | ||
IPoolManager.ModifyPositionParams params; | ||
} | ||
|
||
struct PoolInfo { | ||
bool hasAccruedFees; | ||
address liquidityToken; | ||
} | ||
|
||
struct AddLiquidityParams { | ||
address token0; | ||
address token1; | ||
uint24 fee; | ||
uint256 amount0Desired; | ||
uint256 amount1Desired; | ||
uint256 amount0Min; | ||
uint256 amount1Min; | ||
address to; | ||
uint256 deadline; | ||
} | ||
|
||
struct RemoveLiquidityParams { | ||
address token0; | ||
address token1; | ||
uint24 fee; | ||
uint256 liquidity; | ||
uint256 deadline; | ||
} | ||
|
||
mapping(PoolId => PoolInfo) public poolInfo; | ||
|
||
constructor(IPoolManager _poolManager) BaseHook(_poolManager) {} | ||
|
||
modifier ensure(uint256 deadline) { | ||
if (deadline < block.timestamp) revert ExpiredPastDeadline(); | ||
_; | ||
} | ||
|
||
function getHooksCalls() public pure override returns (Hooks.Calls memory) { | ||
return Hooks.Calls({ | ||
beforeInitialize: true, | ||
afterInitialize: false, | ||
beforeModifyPosition: true, | ||
afterModifyPosition: false, | ||
beforeSwap: true, | ||
afterSwap: false, | ||
beforeDonate: false, | ||
afterDonate: false | ||
}); | ||
} | ||
|
||
function addLiquidity(AddLiquidityParams calldata params) | ||
external | ||
ensure(params.deadline) | ||
returns (uint128 liquidity) | ||
{ | ||
PoolKey memory key = PoolKey({ | ||
currency0: Currency.wrap(params.token0), | ||
currency1: Currency.wrap(params.token1), | ||
fee: params.fee, | ||
tickSpacing: 60, | ||
hooks: IHooks(address(this)) | ||
}); | ||
|
||
PoolId poolId = key.toId(); | ||
|
||
(uint160 sqrtPriceX96,,,,,) = poolManager.getSlot0(poolId); | ||
|
||
if (sqrtPriceX96 == 0) revert PoolNotInitialized(); | ||
|
||
PoolInfo storage pool = poolInfo[poolId]; | ||
|
||
uint128 poolLiquidity = poolManager.getLiquidity(poolId); | ||
|
||
liquidity = LiquidityAmounts.getLiquidityForAmounts( | ||
sqrtPriceX96, | ||
TickMath.getSqrtRatioAtTick(MIN_TICK), | ||
TickMath.getSqrtRatioAtTick(MAX_TICK), | ||
params.amount0Desired, | ||
params.amount1Desired | ||
); | ||
|
||
if (poolLiquidity == 0 && liquidity <= MINIMUM_LIQUIDITY) { | ||
revert LiquidityDoesntMeetMinimum(); | ||
} | ||
BalanceDelta addedDelta = modifyPosition( | ||
key, | ||
IPoolManager.ModifyPositionParams({ | ||
tickLower: MIN_TICK, | ||
tickUpper: MAX_TICK, | ||
liquidityDelta: liquidity.toInt256() | ||
}) | ||
); | ||
|
||
if (poolLiquidity == 0) { | ||
// permanently lock the first MINIMUM_LIQUIDITY tokens | ||
liquidity -= MINIMUM_LIQUIDITY; | ||
UniswapV4ERC20(pool.liquidityToken).mint(address(0), MINIMUM_LIQUIDITY); | ||
} | ||
|
||
UniswapV4ERC20(pool.liquidityToken).mint(params.to, liquidity); | ||
|
||
if (uint128(addedDelta.amount0()) < params.amount0Min || uint128(addedDelta.amount1()) < params.amount1Min) { | ||
revert TooMuchSlippage(); | ||
} | ||
} | ||
|
||
function removeLiquidity(RemoveLiquidityParams calldata params) | ||
public | ||
virtual | ||
ensure(params.deadline) | ||
returns (BalanceDelta delta) | ||
{ | ||
PoolKey memory key = PoolKey({ | ||
currency0: Currency.wrap(params.token0), | ||
currency1: Currency.wrap(params.token1), | ||
fee: params.fee, | ||
tickSpacing: 60, | ||
hooks: IHooks(address(this)) | ||
}); | ||
|
||
PoolId poolId = key.toId(); | ||
|
||
(uint160 sqrtPriceX96,,,,,) = poolManager.getSlot0(poolId); | ||
|
||
if (sqrtPriceX96 == 0) revert PoolNotInitialized(); | ||
|
||
UniswapV4ERC20 erc20 = UniswapV4ERC20(poolInfo[poolId].liquidityToken); | ||
|
||
delta = modifyPosition( | ||
key, | ||
IPoolManager.ModifyPositionParams({ | ||
tickLower: MIN_TICK, | ||
tickUpper: MAX_TICK, | ||
liquidityDelta: -(params.liquidity.toInt256()) | ||
}) | ||
); | ||
|
||
erc20.burn(msg.sender, params.liquidity); | ||
} | ||
|
||
function beforeInitialize(address, PoolKey calldata key, uint160) external override returns (bytes4) { | ||
if (key.tickSpacing != 60) revert TickSpacingNotDefault(); | ||
|
||
PoolId poolId = key.toId(); | ||
|
||
string memory tokenSymbol = string( | ||
abi.encodePacked( | ||
"UniV4", | ||
"-", | ||
IERC20Metadata(Currency.unwrap(key.currency0)).symbol(), | ||
"-", | ||
IERC20Metadata(Currency.unwrap(key.currency1)).symbol(), | ||
"-", | ||
Strings.toString(uint256(key.fee)) | ||
) | ||
); | ||
address poolToken = address(new UniswapV4ERC20(tokenSymbol, tokenSymbol)); | ||
|
||
poolInfo[poolId] = PoolInfo({hasAccruedFees: false, liquidityToken: poolToken}); | ||
|
||
return FullRange.beforeInitialize.selector; | ||
} | ||
|
||
function beforeModifyPosition(address sender, PoolKey calldata, IPoolManager.ModifyPositionParams calldata) | ||
external | ||
view | ||
override | ||
returns (bytes4) | ||
{ | ||
if (sender != address(this)) revert SenderMustBeHook(); | ||
|
||
return FullRange.beforeModifyPosition.selector; | ||
} | ||
|
||
function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata) | ||
external | ||
override | ||
returns (bytes4) | ||
{ | ||
PoolId poolId = key.toId(); | ||
|
||
if (!poolInfo[poolId].hasAccruedFees) { | ||
PoolInfo storage pool = poolInfo[poolId]; | ||
pool.hasAccruedFees = true; | ||
} | ||
|
||
return IHooks.beforeSwap.selector; | ||
} | ||
|
||
function modifyPosition(PoolKey memory key, IPoolManager.ModifyPositionParams memory params) | ||
internal | ||
returns (BalanceDelta delta) | ||
{ | ||
delta = abi.decode(poolManager.lock(abi.encode(CallbackData(msg.sender, key, params))), (BalanceDelta)); | ||
} | ||
|
||
function _settleDeltas(address sender, PoolKey memory key, BalanceDelta delta) internal { | ||
_settleDelta(sender, key.currency0, uint128(delta.amount0())); | ||
_settleDelta(sender, key.currency1, uint128(delta.amount1())); | ||
} | ||
|
||
function _settleDelta(address sender, Currency currency, uint128 amount) internal { | ||
if (currency.isNative()) { | ||
poolManager.settle{value: amount}(currency); | ||
} else { | ||
if (sender == address(this)) { | ||
currency.transfer(address(poolManager), amount); | ||
} else { | ||
IERC20Minimal(Currency.unwrap(currency)).transferFrom(sender, address(poolManager), amount); | ||
} | ||
poolManager.settle(currency); | ||
} | ||
} | ||
|
||
function _takeDeltas(address sender, PoolKey memory key, BalanceDelta delta) internal { | ||
poolManager.take(key.currency0, sender, uint256(uint128(-delta.amount0()))); | ||
poolManager.take(key.currency1, sender, uint256(uint128(-delta.amount1()))); | ||
} | ||
|
||
function _removeLiquidity(PoolKey memory key, IPoolManager.ModifyPositionParams memory params) | ||
internal | ||
returns (BalanceDelta delta) | ||
{ | ||
PoolId poolId = key.toId(); | ||
PoolInfo storage pool = poolInfo[poolId]; | ||
|
||
if (pool.hasAccruedFees) { | ||
_rebalance(key); | ||
} | ||
|
||
uint256 liquidityToRemove = FullMath.mulDiv( | ||
uint256(-params.liquidityDelta), | ||
poolManager.getLiquidity(poolId), | ||
UniswapV4ERC20(pool.liquidityToken).totalSupply() | ||
); | ||
|
||
params.liquidityDelta = -(liquidityToRemove.toInt256()); | ||
delta = poolManager.modifyPosition(key, params); | ||
pool.hasAccruedFees = false; | ||
} | ||
|
||
function lockAcquired(bytes calldata rawData) | ||
external | ||
override(ILockCallback, BaseHook) | ||
poolManagerOnly | ||
returns (bytes memory) | ||
{ | ||
CallbackData memory data = abi.decode(rawData, (CallbackData)); | ||
BalanceDelta delta; | ||
|
||
if (data.params.liquidityDelta < 0) { | ||
delta = _removeLiquidity(data.key, data.params); | ||
_takeDeltas(data.sender, data.key, delta); | ||
} else { | ||
delta = poolManager.modifyPosition(data.key, data.params); | ||
_settleDeltas(data.sender, data.key, delta); | ||
} | ||
return abi.encode(delta); | ||
} | ||
|
||
function _rebalance(PoolKey memory key) public { | ||
PoolId poolId = key.toId(); | ||
BalanceDelta balanceDelta = poolManager.modifyPosition( | ||
key, | ||
IPoolManager.ModifyPositionParams({ | ||
tickLower: MIN_TICK, | ||
tickUpper: MAX_TICK, | ||
liquidityDelta: -(poolManager.getLiquidity(poolId).toInt256()) | ||
}) | ||
); | ||
|
||
uint160 newSqrtPriceX96 = ( | ||
FixedPointMathLib.sqrt( | ||
FullMath.mulDiv(uint128(-balanceDelta.amount1()), FixedPoint96.Q96, uint128(-balanceDelta.amount0())) | ||
) * FixedPointMathLib.sqrt(FixedPoint96.Q96) | ||
).toUint160(); | ||
|
||
(uint160 sqrtPriceX96,,,,,) = poolManager.getSlot0(poolId); | ||
|
||
poolManager.swap( | ||
key, | ||
IPoolManager.SwapParams({ | ||
zeroForOne: newSqrtPriceX96 < sqrtPriceX96, | ||
amountSpecified: MAX_INT, | ||
sqrtPriceLimitX96: newSqrtPriceX96 | ||
}) | ||
); | ||
|
||
uint128 liquidity = LiquidityAmounts.getLiquidityForAmounts( | ||
newSqrtPriceX96, | ||
TickMath.getSqrtRatioAtTick(MIN_TICK), | ||
TickMath.getSqrtRatioAtTick(MAX_TICK), | ||
uint256(uint128(-balanceDelta.amount0())), | ||
uint256(uint128(-balanceDelta.amount1())) | ||
); | ||
|
||
BalanceDelta balanceDeltaAfter = poolManager.modifyPosition( | ||
key, | ||
IPoolManager.ModifyPositionParams({ | ||
tickLower: MIN_TICK, | ||
tickUpper: MAX_TICK, | ||
liquidityDelta: liquidity.toInt256() | ||
}) | ||
); | ||
|
||
// Donate any "dust" from the sqrtRatio change as fees | ||
uint128 donateAmount0 = uint128(-balanceDelta.amount0() - balanceDeltaAfter.amount0()); | ||
uint128 donateAmount1 = uint128(-balanceDelta.amount1() - balanceDeltaAfter.amount1()); | ||
|
||
poolManager.donate(key, donateAmount0, donateAmount1); | ||
} | ||
} |
Oops, something went wrong.