-
Notifications
You must be signed in to change notification settings - Fork 504
Commit
* initial thoughts lock and batch * update safecallback with constructor * simple batch under lock * oops * misc version bump; will conflict but can resolve later * defining types and different levels of abstractions * merge in main; resolve conflicts * wip * misc fixes with main:latest * basic mint * begin moving tests to fuzz * test for slippage * burning * decrease liquidity * mint transfer burn, liquidityOf accounting * wip * refactor to use CurrencySettleTake * basic fee collection * wip * misc fix * fee collection for independent same-range parties * LiquidityPosition -> LiquidityRange * erc20 fee collection * decrease liquidity with fee collection * wip test decrease liquidity on same range * reworked fuzzers; more testing on fee claims for liquidity decreasing * forge fmt * test fixes for flipped deltas * wip * test coverage for increase liquidity cases * preliminary gas benchmarks * Position manager refactor (#2) * chore: update v4-core:latest (#105) * update core * rename lockAcquired to unlockCallback * update core; temporary path hack in remappings * update v4-core; remove remapping * wip: fix compatibility * update core; fix renaming of swap fee to lp fee * update core; fix events * update core; address liquidity salt and modify liquidity return values * fix incorrect delta accounting when modifying liquidity * fix todo, use CurrencySettleTake * remove deadcode * update core; use StateLibrary; update sqrtRatio to sqrtPrice * fix beforeSwap return signatures * forge fmt; remove commented out code * update core (wow gas savings) * update core * update core * update core; hook flags LSB * update core * update core * chore: update v4 core (#115) * Update v4-core * CurrencySettleTake -> CurrencySettler * Snapshots * compiling but very broken * replace PoolStateLibrary * update currency settle take * compiling * wip * use v4-core's forge-std * test liquidity increase * additional fixes for collection and liquidity decrease * test migration * replace old implementation with new --------- Signed-off-by: saucepoint <[email protected]> Co-authored-by: 0x57 <[email protected]> * cleanup: TODOs and imports * Position manager Consolidate (#3) * wip: consolidation * further consolidation * consolidate to single file * yay no more stack too deep * some code comments * use currency settler syntax * use v4-core's gas snapshot * use snapLastCall and isolate for posm benchmarks * Update contracts/libraries/CurrencySettleTake.sol Co-authored-by: 0x57 <[email protected]> * use v4-core's solmate its more recent * use v4-core's openzeppelin-contracts * add ERC721Permit * feedback: memory hookData * initial refactor. stack too deep * passing tests * gutted LockAndBatchCall * cleanup diff * renaming vanilla functions * sanitize * change add liq accounting (#126) * change add liq accounting * remove rand comments * fix exact fees * use closeAllDeltas * comments cleanup * additional liquidity tests (#129) * additional increase liquidity tests * edge case of using cached fees for autocompound * wip * fix autocompound bug, use custodied and unclaimed fees in the autocompound * fix tests and use BalanceDeltas (#130) * fix some assertions * use BalanceDeltas for arithmetic * cleanest code in the game??? * additional cleaning * typo lol * autocompound gas benchmarks * autocompound excess credit gas benchmark * save 600 gas, cleaner code when moving caller delta to tokensOwed --------- Co-authored-by: saucepoint <[email protected]> * create compatibility with arbitrary execute handler * being supporting batched ops on vanilla functions * some initial tests to drive TDD * gas with isolate * mint to recipient * refactor for external call and code reuse (#134) * updated interface with unlockAndExecute * update decrease (#133) * update decrease * update collect * update decrease/collect * remove delta function * update burn * fix bubbling different return types because of recursive calls * all operations only return a BalanceDelta type (#136) * temp-dev-update (#135) * checkpointing * move decrease and collect to transient storage * remove returns since they are now saved to transient storage * draft: delta closing * wip * Sra/edits (#137) * consolidate using owner, update burn * fix: accrue deltas to caller in increase * Rip Out Vanilla (#138) * rip out vanilla and benchmark * fix gas benchmark * check posm is the locker before allowing access to external functions * restore execute tests * posm takes as 6909; remove legacy deadcode * restore tests * move helpers to the same file * fix: cleanup --------- Co-authored-by: Sara Reynolds <[email protected]> Co-authored-by: Sara Reynolds <[email protected]> * using internal calls, first pass (#143) * using internal calls, first pass * fix gas tests * fix execute test * fix tests * edit mint gas test * fix mint test * fix warnings * dumb fix to test ci (#146) * dumb fix to test ci * memory limit * update gas limit to pass tests --------- Co-authored-by: gretzke <[email protected]> * some more gas snapshots (#150) * feat: posm, use salts for all positions & update permit (#148) * use position salts * use fees owed in some tests * remove claims from increase,decrease * increment token id before reading it * Revert "increment token id before reading it" This reverts commit d366d75. * owner to alice * add more mint gas tests * update comment * Owner-level ERC721 Permit (#153) * checkpointing * move decrease and collect to transient storage * remove returns since they are now saved to transient storage * draft: delta closing * wip * Sra/edits (#137) * consolidate using owner, update burn * fix: accrue deltas to caller in increase * Rip Out Vanilla (#138) * rip out vanilla and benchmark * fix gas benchmark * check posm is the locker before allowing access to external functions * restore execute tests * posm takes as 6909; remove legacy deadcode * restore tests * move helpers to the same file * move operator to NFTposm; move nonce to ERC721Permit; owner-level nonce * tests for operator/permit * snapshots * gas benchmarks for permit * test fixes * unordered nonces * fix tests / cheatcode usage * fix tests --------- Co-authored-by: Sara Reynolds <[email protected]> --------- Co-authored-by: gretzke <[email protected]> Co-authored-by: saucepoint <[email protected]> * Multicall & initialize (#154) * add multicall and an external function for initialization, with tests * test multicall contract * gas snapshot multicall * fix ci test * fix tests * forge fmt * change how msg.value is used in multicall mock --------- Co-authored-by: Sara Reynolds <[email protected]> * prep shared actions (#158) * update main (#162) * ERC721Permit cleanup (#161) * wip Solmate ERC721 * the queens dead, you may put down your arms. with this commit, i hereby lay BaseLiquidityManagement and the ideals of fee accounting finally to rest * refactor tokenId => LiquidityRange; begin separating PoolKey * move comment --------- Co-authored-by: Sara Reynolds <[email protected]> * remove old files, imports * Update src/NonfungiblePositionManager.sol Co-authored-by: saucepoint <[email protected]> * pr comments * pr comments * refactor test helpers per feedback * fix gas * remove permit * fix compiler warnings * rename to PositionManager * cache length * skip take for 0 * fix tests * Update src/interfaces/IPositionManager.sol Co-authored-by: Alice <[email protected]> * update multicall tests per feedback * remove unused imports * more unused imports * improve assertion * assert mint recipient is the payer and not the recipient * pr feedback * assert pool creation * use poolManager * remove liquidityrange imports * remove version string * pr comments, use base test setup * fuzz sqrtPrice * use fuzz, assert eq * other final rand pr comment fixes * fix off by 1 * use bound * use nextTokenId --------- Signed-off-by: saucepoint <[email protected]> Co-authored-by: saucepoint <[email protected]> Co-authored-by: saucepoint <[email protected]> Co-authored-by: 0x57 <[email protected]> Co-authored-by: 0x57 <[email protected]> Co-authored-by: gretzke <[email protected]> Co-authored-by: Alice <[email protected]>
- Loading branch information
There are no files selected for viewing
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
162386 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
162386 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
127764 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
140645 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
157182 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
148692 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
184956 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
418192 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
360874 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
361516 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
287098 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
366892 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
462111 |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,5 +21,5 @@ jobs: | |
with: | ||
version: nightly | ||
|
||
- name: Run tests | ||
- name: Check format | ||
run: forge fmt --check |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,3 @@ | ||
[submodule "lib/forge-std"] | ||
path = lib/forge-std | ||
url = https://github.com/foundry-rs/forge-std | ||
[submodule "lib/openzeppelin-contracts"] | ||
path = lib/openzeppelin-contracts | ||
url = https://github.com/OpenZeppelin/openzeppelin-contracts | ||
[submodule "lib/forge-gas-snapshot"] | ||
path = lib/forge-gas-snapshot | ||
url = https://github.com/marktoda/forge-gas-snapshot | ||
[submodule "lib/v4-core"] | ||
path = lib/v4-core | ||
url = https://github.com/Uniswap/v4-core | ||
[submodule "lib/solmate"] | ||
path = lib/solmate | ||
url = https://github.com/transmissions11/solmate |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,2 @@ | ||
@uniswap/v4-core/=lib/v4-core/ | ||
solmate/=lib/solmate/src/ | ||
forge-std/=lib/forge-std/src/ | ||
@openzeppelin/=lib/openzeppelin-contracts/ | ||
@openzeppelin/=lib/v4-core/lib/openzeppelin-contracts/ |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,221 @@ | ||
// SPDX-License-Identifier: UNLICENSED | ||
pragma solidity ^0.8.24; | ||
|
||
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; | ||
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; | ||
import {PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; | ||
import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; | ||
import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; | ||
import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; | ||
import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; | ||
import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol"; | ||
|
||
import {ERC721Permit} from "./base/ERC721Permit.sol"; | ||
import {IPositionManager, Actions} from "./interfaces/IPositionManager.sol"; | ||
import {SafeCallback} from "./base/SafeCallback.sol"; | ||
import {Multicall} from "./base/Multicall.sol"; | ||
import {PoolInitializer} from "./base/PoolInitializer.sol"; | ||
import {CurrencySettleTake} from "./libraries/CurrencySettleTake.sol"; | ||
import {LiquidityRange} from "./types/LiquidityRange.sol"; | ||
|
||
contract PositionManager is IPositionManager, ERC721Permit, PoolInitializer, Multicall, SafeCallback { | ||
using CurrencyLibrary for Currency; | ||
using CurrencySettleTake for Currency; | ||
using PoolIdLibrary for PoolKey; | ||
using StateLibrary for IPoolManager; | ||
using TransientStateLibrary for IPoolManager; | ||
using SafeCast for uint256; | ||
|
||
/// @dev The ID of the next token that will be minted. Skips 0 | ||
uint256 public nextTokenId = 1; | ||
|
||
// maps the ERC721 tokenId to its Range (poolKey, tick range) | ||
mapping(uint256 tokenId => LiquidityRange range) public tokenRange; | ||
|
||
constructor(IPoolManager _poolManager) | ||
SafeCallback(_poolManager) | ||
ERC721Permit("Uniswap V4 Positions NFT", "UNI-V4-POSM", "1") | ||
{} | ||
|
||
modifier checkDeadline(uint256 deadline) { | ||
if (block.timestamp > deadline) revert DeadlinePassed(); | ||
_; | ||
} | ||
|
||
/// @param unlockData is an encoding of actions, params, and currencies | ||
/// @return returnData is the endocing of each actions return information | ||
function modifyLiquidities(bytes calldata unlockData, uint256 deadline) | ||
external | ||
checkDeadline(deadline) | ||
returns (bytes[] memory) | ||
{ | ||
// TODO: Edit the encoding/decoding. | ||
return abi.decode(poolManager.unlock(abi.encode(unlockData, msg.sender)), (bytes[])); | ||
} | ||
|
||
function _unlockCallback(bytes calldata payload) internal override returns (bytes memory) { | ||
// TODO: Fix double encode/decode | ||
(bytes memory unlockData, address sender) = abi.decode(payload, (bytes, address)); | ||
|
||
(Actions[] memory actions, bytes[] memory params) = abi.decode(unlockData, (Actions[], bytes[])); | ||
|
||
bytes[] memory returnData = _dispatch(actions, params, sender); | ||
|
||
return abi.encode(returnData); | ||
} | ||
|
||
function _dispatch(Actions[] memory actions, bytes[] memory params, address sender) | ||
internal | ||
returns (bytes[] memory returnData) | ||
{ | ||
uint256 length = actions.length; | ||
if (length != params.length) revert MismatchedLengths(); | ||
returnData = new bytes[](length); | ||
for (uint256 i; i < length; i++) { | ||
if (actions[i] == Actions.INCREASE) { | ||
returnData[i] = _increase(params[i], sender); | ||
} else if (actions[i] == Actions.DECREASE) { | ||
returnData[i] = _decrease(params[i], sender); | ||
} else if (actions[i] == Actions.MINT) { | ||
// TODO: Mint will be coupled with increase. | ||
returnData[i] = _mint(params[i]); | ||
} else if (actions[i] == Actions.CLOSE_CURRENCY) { | ||
returnData[i] = _close(params[i], sender); | ||
} else if (actions[i] == Actions.BURN) { | ||
// TODO: Burn will be coupled with decrease. | ||
(uint256 tokenId) = abi.decode(params[i], (uint256)); | ||
burn(tokenId, sender); | ||
} else { | ||
revert UnsupportedAction(); | ||
} | ||
} | ||
} | ||
|
||
/// @param param is an encoding of uint256 tokenId, uint256 liquidity, bytes hookData | ||
/// @param sender the msg.sender, set by the `modifyLiquidities` function before the `unlockCallback`. Using msg.sender directly inside | ||
/// the _unlockCallback will be the pool manager. | ||
/// @return returns an encoding of the BalanceDelta applied by this increase call, including credited fees. | ||
/// @dev Calling increase with 0 liquidity will credit the caller with any underlying fees of the position | ||
function _increase(bytes memory param, address sender) internal returns (bytes memory) { | ||
(uint256 tokenId, uint256 liquidity, bytes memory hookData) = abi.decode(param, (uint256, uint256, bytes)); | ||
|
||
_requireApprovedOrOwner(tokenId, sender); | ||
|
||
// Note: The tokenId is used as the salt for this position, so every minted liquidity has unique storage in the pool manager. | ||
(BalanceDelta delta,) = _modifyLiquidity(tokenRange[tokenId], liquidity.toInt256(), bytes32(tokenId), hookData); | ||
return abi.encode(delta); | ||
} | ||
|
||
/// @param params is an encoding of uint256 tokenId, uint256 liquidity, bytes hookData | ||
/// @param sender the msg.sender, set by the `modifyLiquidities` function before the `unlockCallback`. Using msg.sender directly inside | ||
/// the _unlockCallback will be the pool manager. | ||
/// @return returns an encoding of the BalanceDelta applied by this increase call, including credited fees. | ||
/// @dev Calling decrease with 0 liquidity will credit the caller with any underlying fees of the position | ||
function _decrease(bytes memory params, address sender) internal returns (bytes memory) { | ||
(uint256 tokenId, uint256 liquidity, bytes memory hookData) = abi.decode(params, (uint256, uint256, bytes)); | ||
|
||
_requireApprovedOrOwner(tokenId, sender); | ||
|
||
// Note: the tokenId is used as the salt. | ||
(BalanceDelta delta,) = | ||
_modifyLiquidity(tokenRange[tokenId], -(liquidity.toInt256()), bytes32(tokenId), hookData); | ||
return abi.encode(delta); | ||
} | ||
|
||
/// @param param is an encoding of LiquidityRange memory range, uint256 liquidity, address recipient, bytes hookData where recipient is the receiver / owner of the ERC721 | ||
/// @return returns an encoding of the BalanceDelta from the initial increase | ||
function _mint(bytes memory param) internal returns (bytes memory) { | ||
(LiquidityRange memory range, uint256 liquidity, address owner, bytes memory hookData) = | ||
abi.decode(param, (LiquidityRange, uint256, address, bytes)); | ||
|
||
// mint receipt token | ||
uint256 tokenId; | ||
// tokenId is assigned to current nextTokenId before incrementing it | ||
unchecked { | ||
tokenId = nextTokenId++; | ||
} | ||
_mint(owner, tokenId); | ||
|
||
(BalanceDelta delta,) = _modifyLiquidity(range, liquidity.toInt256(), bytes32(tokenId), hookData); | ||
|
||
tokenRange[tokenId] = range; | ||
|
||
return abi.encode(delta); | ||
} | ||
|
||
/// @param params is an encoding of the Currency to close | ||
/// @param sender is the msg.sender encoded by the `modifyLiquidities` function before the `unlockCallback`. | ||
/// @return an encoding of int256 the balance of the currency being settled by this call | ||
function _close(bytes memory params, address sender) internal returns (bytes memory) { | ||
(Currency currency) = abi.decode(params, (Currency)); | ||
// this address has applied all deltas on behalf of the user/owner | ||
// it is safe to close this entire delta because of slippage checks throughout the batched calls. | ||
int256 currencyDelta = poolManager.currencyDelta(address(this), currency); | ||
|
||
// the sender is the payer or receiver | ||
if (currencyDelta < 0) { | ||
currency.settle(poolManager, sender, uint256(-int256(currencyDelta)), false); | ||
} else if (currencyDelta > 0) { | ||
currency.take(poolManager, sender, uint256(int256(currencyDelta)), false); | ||
} | ||
|
||
return abi.encode(currencyDelta); | ||
} | ||
|
||
function burn(uint256 tokenId, address sender) internal { | ||
_requireApprovedOrOwner(tokenId, sender); | ||
|
||
// Checks that the full position's liquidity has been removed and all tokens have been collected from tokensOwed. | ||
_validateBurn(tokenId); | ||
|
||
delete tokenRange[tokenId]; | ||
// Burn the token. | ||
_burn(tokenId); | ||
} | ||
|
||
function _modifyLiquidity(LiquidityRange memory range, int256 liquidityChange, bytes32 salt, bytes memory hookData) | ||
internal | ||
returns (BalanceDelta liquidityDelta, BalanceDelta totalFeesAccrued) | ||
{ | ||
(liquidityDelta, totalFeesAccrued) = poolManager.modifyLiquidity( | ||
range.poolKey, | ||
IPoolManager.ModifyLiquidityParams({ | ||
tickLower: range.tickLower, | ||
tickUpper: range.tickUpper, | ||
liquidityDelta: liquidityChange, | ||
salt: salt | ||
}), | ||
hookData | ||
); | ||
} | ||
|
||
// ensures liquidity of the position is empty before burning the token. | ||
function _validateBurn(uint256 tokenId) internal view { | ||
bytes32 positionId = getPositionIdFromTokenId(tokenId); | ||
uint128 liquidity = poolManager.getPositionLiquidity(tokenRange[tokenId].poolKey.toId(), positionId); | ||
if (liquidity > 0) revert PositionMustBeEmpty(); | ||
} | ||
|
||
// TODO: Move this to a posm state-view library. | ||
function getPositionIdFromTokenId(uint256 tokenId) public view returns (bytes32 positionId) { | ||
LiquidityRange memory range = tokenRange[tokenId]; | ||
bytes32 salt = bytes32(tokenId); | ||
int24 tickLower = range.tickLower; | ||
int24 tickUpper = range.tickUpper; | ||
address owner = address(this); | ||
|
||
// positionId = keccak256(abi.encodePacked(owner, tickLower, tickUpper, salt)) | ||
assembly { | ||
mstore(0x26, salt) // [0x26, 0x46) | ||
mstore(0x06, tickUpper) // [0x23, 0x26) | ||
mstore(0x03, tickLower) // [0x20, 0x23) | ||
mstore(0, owner) // [0x0c, 0x20) | ||
positionId := keccak256(0x0c, 0x3a) // len is 58 bytes | ||
mstore(0x26, 0) // rewrite 0x26 to 0 | ||
} | ||
} | ||
|
||
function _requireApprovedOrOwner(uint256 tokenId, address sender) internal view { | ||
if (!_isApprovedOrOwner(sender, tokenId)) revert NotApproved(sender); | ||
} | ||
} |