Skip to content
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

posm: CLEAR_OR_TAKE #252

Merged
merged 9 commits into from
Aug 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1 +1 @@
140844
140956
2 changes: 1 addition & 1 deletion .forge-snapshots/V4Router_Bytecode.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
8068
8116
24 changes: 15 additions & 9 deletions src/PositionManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ contract PositionManager is
using StateLibrary for IPoolManager;
using TransientStateLibrary for IPoolManager;
using SafeCast for uint256;
using SafeCast for int256;
using CalldataDecoder for bytes;
using SlippageCheckLibrary for BalanceDelta;

Expand Down Expand Up @@ -155,9 +156,9 @@ contract PositionManager is
} else if (action == Actions.CLOSE_CURRENCY) {
Currency currency = params.decodeCurrency();
_close(currency);
} else if (action == Actions.CLEAR) {
} else if (action == Actions.CLEAR_OR_TAKE) {
(Currency currency, uint256 amountMax) = params.decodeCurrencyAndUint256();
_clear(currency, amountMax);
_clearOrTake(currency, amountMax);
} else if (action == Actions.BURN_POSITION) {
// Will automatically decrease liquidity to 0 if the position is not already empty.
(
Expand Down Expand Up @@ -251,18 +252,23 @@ contract PositionManager is
}

/// @dev integrators may elect to forfeit positive deltas with clear
/// provides a safety check that amount-to-clear is less than a user-provided maximum
function _clear(Currency currency, uint256 amountMax) internal {
int256 currencyDelta = poolManager.currencyDelta(address(this), currency);
if (uint256(currencyDelta) > amountMax) revert ClearExceedsMaxAmount(currency, currencyDelta, amountMax);
poolManager.clear(currency, uint256(currencyDelta));
/// if the forfeit amount exceeds the user-specified max, the amount is taken instead
function _clearOrTake(Currency currency, uint256 amountMax) internal {
uint256 delta = _getFullCredit(currency);

// forfeit the delta if its less than or equal to the user-specified limit
if (delta <= amountMax) {
poolManager.clear(currency, delta);
} else {
_take(currency, msgSender(), delta);
}
}

function _settlePair(Currency currency0, Currency currency1) internal {
// the locker is the payer when settling
address caller = msgSender();
_settle(currency0, caller, _getFullSettleAmount(currency0));
_settle(currency1, caller, _getFullSettleAmount(currency1));
_settle(currency0, caller, _getFullDebt(currency0));
_settle(currency1, caller, _getFullDebt(currency1));
}

/// @dev this is overloaded with ERC721Permit._burn
Expand Down
6 changes: 3 additions & 3 deletions src/V4Router.sol
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,17 @@ abstract contract V4Router is IV4Router, BaseActionsRouter, DeltaResolver {
if (action == Actions.SETTLE_ALL) {
Currency currency = params.decodeCurrency();
// TODO should it have a maxAmountOut added slippage protection?
_settle(currency, msgSender(), _getFullSettleAmount(currency));
_settle(currency, msgSender(), _getFullDebt(currency));
} else if (action == Actions.SETTLE) {
(Currency currency, uint256 amount, bool payerIsUser) = params.decodeCurrencyUint256AndBool();
_settle(currency, _mapPayer(payerIsUser), _mapSettleAmount(amount, currency));
} else if (action == Actions.TAKE_ALL) {
(Currency currency, address recipient) = params.decodeCurrencyAndAddress();
uint256 amount = _getFullTakeAmount(currency);
uint256 amount = _getFullCredit(currency);
_take(currency, _mapRecipient(recipient), amount);
} else if (action == Actions.TAKE_PORTION) {
(Currency currency, address recipient, uint256 bips) = params.decodeCurrencyAddressAndUint256();
_take(currency, _mapRecipient(recipient), _getFullTakeAmount(currency).calculatePortion(bips));
_take(currency, _mapRecipient(recipient), _getFullCredit(currency).calculatePortion(bips));
} else {
revert UnsupportedAction(action);
}
Expand Down
26 changes: 16 additions & 10 deletions src/base/DeltaResolver.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ abstract contract DeltaResolver is ImmutableState {
using SafeCast for *;

/// @notice Emitted trying to settle a positive delta.
error IncorrectUseOfSettle();
error DeltaNotPositive(Currency currency);
/// @notice Emitted trying to take a negative delta.
error IncorrectUseOfTake();
error DeltaNotNegative(Currency currency);

/// @notice Take an amount of currency out of the PoolManager
/// @param currency Currency to take
Expand Down Expand Up @@ -49,17 +49,23 @@ abstract contract DeltaResolver is ImmutableState {
/// @param amount The number of tokens to send
function _pay(Currency token, address payer, uint256 amount) internal virtual;

function _getFullSettleAmount(Currency currency) internal view returns (uint256 amount) {
/// @notice Obtain the full amount owed by this contract (negative delta)
/// @param currency Currency to get the delta for
/// @return amount The amount owed by this contract as a uint256
function _getFullDebt(Currency currency) internal view returns (uint256 amount) {
int256 _amount = poolManager.currencyDelta(address(this), currency);
// If the amount is positive, it should be taken not settled for.
if (_amount > 0) revert IncorrectUseOfSettle();
// If the amount is negative, it should be settled not taken.
if (_amount > 0) revert DeltaNotNegative(currency);
amount = uint256(-_amount);
}

function _getFullTakeAmount(Currency currency) internal view returns (uint256 amount) {
/// @notice Obtain the full credit owed to this contract (positive delta)
/// @param currency Currency to get the delta for
/// @return amount The amount owed to this contract as a uint256
function _getFullCredit(Currency currency) internal view returns (uint256 amount) {
int256 _amount = poolManager.currencyDelta(address(this), currency);
// If the amount is negative, it should be settled not taken.
if (_amount < 0) revert IncorrectUseOfTake();
// If the amount is negative, it should be taken not settled for.
if (_amount < 0) revert DeltaNotPositive(currency);
amount = uint256(_amount);
}

Expand All @@ -68,7 +74,7 @@ abstract contract DeltaResolver is ImmutableState {
if (amount == Constants.CONTRACT_BALANCE) {
return currency.balanceOfSelf();
} else if (amount == Constants.OPEN_DELTA) {
return _getFullSettleAmount(currency);
return _getFullDebt(currency);
}
return amount;
}
Expand All @@ -78,7 +84,7 @@ abstract contract DeltaResolver is ImmutableState {
if (amount == Constants.CONTRACT_BALANCE) {
return currency.balanceOfSelf().toUint128();
} else if (amount == Constants.OPEN_DELTA) {
return _getFullTakeAmount(currency).toUint128();
return _getFullCredit(currency).toUint128();
}
return amount;
}
Expand Down
1 change: 0 additions & 1 deletion src/interfaces/IPositionManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ interface IPositionManager is INotifier {
error NotApproved(address caller);
error DeadlinePassed();
error IncorrectPositionConfigForTokenId(uint256 tokenId);
error ClearExceedsMaxAmount(Currency currency, int256 amount, uint256 maxAmount);

/// @notice Batches many liquidity modification calls to pool manager
/// @param payload is an encoding of actions, and parameters for those actions
Expand Down
2 changes: 1 addition & 1 deletion src/libraries/Actions.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ library Actions {
uint256 constant TAKE_PORTION = 0x14;

uint256 constant CLOSE_CURRENCY = 0x15;
uint256 constant CLEAR = 0x16;
uint256 constant CLEAR_OR_TAKE = 0x16;
uint256 constant SWEEP = 0x17;

// minting/burning 6909s to close deltas
Expand Down
2 changes: 1 addition & 1 deletion test/BaseActionsRouter.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ contract BaseActionsRouterTest is Test, Deployers, GasSnapshot {
function test_clear_suceeds() public {
Plan memory plan = Planner.init();
for (uint256 i = 0; i < 10; i++) {
plan.add(Actions.CLEAR, "");
plan.add(Actions.CLEAR_OR_TAKE, "");
}

assertEq(router.clearCount(), 0);
Expand Down
2 changes: 1 addition & 1 deletion test/mocks/MockBaseActionsRouter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ contract MockBaseActionsRouter is BaseActionsRouter, ReentrancyLock {
} else {
if (action == Actions.SETTLE) _settle(params);
else if (action == Actions.TAKE) _take(params);
else if (action == Actions.CLEAR) _clear(params);
else if (action == Actions.CLEAR_OR_TAKE) _clear(params);
else if (action == Actions.MINT_6909) _mint6909(params);
else if (action == Actions.BURN_6909) _burn6909(params);
else revert UnsupportedAction(action);
Expand Down
50 changes: 29 additions & 21 deletions test/position-managers/IncreaseLiquidity.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol";
import {IERC20} from "forge-std/interfaces/IERC20.sol";

import {PositionManager} from "../../src/PositionManager.sol";
import {DeltaResolver} from "../../src/base/DeltaResolver.sol";
import {PositionConfig} from "../../src/libraries/PositionConfig.sol";
import {SlippageCheckLibrary} from "../../src/libraries/SlippageCheck.sol";
import {IPositionManager} from "../../src/interfaces/IPositionManager.sol";
Expand Down Expand Up @@ -177,8 +178,8 @@ contract IncreaseLiquidityTest is Test, PosmTestSetup, Fuzzers {
Actions.INCREASE_LIQUIDITY,
abi.encode(tokenIdAlice, config, liquidityDelta, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, ZERO_BYTES)
);
planner.add(Actions.CLEAR, abi.encode(config.poolKey.currency0, 18 wei)); // alice is willing to forfeit 18 wei
planner.add(Actions.CLEAR, abi.encode(config.poolKey.currency1, 18 wei));
planner.add(Actions.CLEAR_OR_TAKE, abi.encode(config.poolKey.currency0, 18 wei)); // alice is willing to forfeit 18 wei
planner.add(Actions.CLEAR_OR_TAKE, abi.encode(config.poolKey.currency1, 18 wei));
bytes memory calls = planner.encode();

vm.prank(alice);
Expand Down Expand Up @@ -280,8 +281,8 @@ contract IncreaseLiquidityTest is Test, PosmTestSetup, Fuzzers {
Actions.INCREASE_LIQUIDITY,
abi.encode(tokenIdAlice, config, liquidityDelta, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, ZERO_BYTES)
);
planner.add(Actions.CLEAR, abi.encode(config.poolKey.currency0, 1 wei)); // alice is willing to forfeit 1 wei
planner.add(Actions.CLEAR, abi.encode(config.poolKey.currency1, 1 wei));
planner.add(Actions.CLEAR_OR_TAKE, abi.encode(config.poolKey.currency0, 1 wei)); // alice is willing to forfeit 1 wei
planner.add(Actions.CLEAR_OR_TAKE, abi.encode(config.poolKey.currency1, 1 wei));
bytes memory calls = planner.encode();

vm.prank(alice);
Expand Down Expand Up @@ -692,7 +693,8 @@ contract IncreaseLiquidityTest is Test, PosmTestSetup, Fuzzers {
assertEq(currency1.balanceOf(address(this)), balanceBefore1 - amount1);
}

function test_increaseLiquidity_clearExceeds_revert() public {
/// @dev if clearing exceeds the max amount, the amount is taken instead
function test_increaseLiquidity_clearExceedsThenTake() public {
uint256 tokenId = lpm.nextTokenId();
mint(config, 1000e18, address(this), ZERO_BYTES);

Expand All @@ -702,6 +704,7 @@ contract IncreaseLiquidityTest is Test, PosmTestSetup, Fuzzers {

// calculate the amount of liquidity to add, using half of the proceeds
uint256 amountToReinvest = amountToDonate / 2;
uint256 amountToReclaim = amountToDonate / 2; // expect to reclaim the other half of the fee revenue
uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts(
SQRT_PRICE_1_1,
TickMath.getSqrtPriceAtTick(config.tickLower),
Expand All @@ -710,28 +713,33 @@ contract IncreaseLiquidityTest is Test, PosmTestSetup, Fuzzers {
amountToReinvest
);

// set the max-forfeit to less than the amount we expect to claim
uint256 maxClear = amountToReclaim - 2 wei;

Plan memory planner = Planner.init();
planner.add(
Actions.INCREASE_LIQUIDITY,
abi.encode(tokenId, config, liquidityDelta, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, ZERO_BYTES)
);
planner.add(Actions.CLEAR, abi.encode(config.poolKey.currency0, amountToReinvest - 2 wei));
planner.add(Actions.CLEAR, abi.encode(config.poolKey.currency1, amountToReinvest - 2 wei));
planner.add(Actions.CLEAR_OR_TAKE, abi.encode(config.poolKey.currency0, maxClear));
planner.add(Actions.CLEAR_OR_TAKE, abi.encode(config.poolKey.currency1, maxClear));
bytes memory calls = planner.encode();

// revert since we're forfeiting beyond the max tolerance
vm.expectRevert(
abi.encodeWithSelector(
IPositionManager.ClearExceedsMaxAmount.selector,
config.poolKey.currency0,
int256(amountToReinvest - 1 wei), // imprecision, PM expects us to collect half of the fees (minus 1 wei)
uint256(amountToReinvest - 2 wei) // the maximum amount we were willing to forfeit
)
);
uint256 balance0Before = currency0.balanceOf(address(this));
uint256 balance1Before = currency1.balanceOf(address(this));

// expect to take the excess, as it exceeds the amount to clear
lpm.modifyLiquidities(calls, _deadline);
BalanceDelta delta = getLastDelta();

assertEq(uint128(delta.amount0()), amountToReclaim - 1 wei); // imprecision
assertEq(uint128(delta.amount1()), amountToReclaim - 1 wei);

assertEq(currency0.balanceOf(address(this)), balance0Before + amountToReclaim - 1 wei);
assertEq(currency1.balanceOf(address(this)), balance1Before + amountToReclaim - 1 wei);
}

/// @dev clearing a negative delta reverts in core with SafeCastOverflow
/// @dev clearing a negative delta reverts
function test_increaseLiquidity_clearNegative_revert() public {
uint256 tokenId = lpm.nextTokenId();
mint(config, 1000e18, address(this), ZERO_BYTES);
Expand All @@ -742,12 +750,12 @@ contract IncreaseLiquidityTest is Test, PosmTestSetup, Fuzzers {
Actions.INCREASE_LIQUIDITY,
abi.encode(tokenId, config, 100e18, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, ZERO_BYTES)
);
planner.add(Actions.CLEAR, abi.encode(config.poolKey.currency0, type(uint256).max));
planner.add(Actions.CLEAR, abi.encode(config.poolKey.currency1, type(uint256).max));
planner.add(Actions.CLEAR_OR_TAKE, abi.encode(config.poolKey.currency0, type(uint256).max));
planner.add(Actions.CLEAR_OR_TAKE, abi.encode(config.poolKey.currency1, type(uint256).max));
bytes memory calls = planner.encode();

// revert since we're forfeiting beyond the max tolerance
vm.expectRevert(SafeCast.SafeCastOverflow.selector);
// revert since we're trying to clear a negative delta
vm.expectRevert(abi.encodeWithSelector(DeltaResolver.DeltaNotPositive.selector, currency0));
lpm.modifyLiquidities(calls, _deadline);
}
}
4 changes: 2 additions & 2 deletions test/position-managers/PositionManager.gas.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -263,8 +263,8 @@ contract PosMGasTest is Test, PosmTestSetup, GasSnapshot {
Actions.INCREASE_LIQUIDITY,
abi.encode(tokenIdAlice, config, liquidityDelta, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, ZERO_BYTES)
);
planner.add(Actions.CLEAR, abi.encode(config.poolKey.currency0, halfTokensOwedAlice + 1 wei));
planner.add(Actions.CLEAR, abi.encode(config.poolKey.currency1, halfTokensOwedAlice + 1 wei));
planner.add(Actions.CLEAR_OR_TAKE, abi.encode(config.poolKey.currency0, halfTokensOwedAlice + 1 wei));
planner.add(Actions.CLEAR_OR_TAKE, abi.encode(config.poolKey.currency1, halfTokensOwedAlice + 1 wei));
bytes memory calls = planner.encode();

vm.prank(alice);
Expand Down
Loading
Loading