Skip to content

Commit

Permalink
feat: exercise cost checks
Browse files Browse the repository at this point in the history
  • Loading branch information
PeakyCryptos committed Nov 30, 2023
1 parent be62ced commit dbe4d36
Show file tree
Hide file tree
Showing 5 changed files with 283 additions and 24 deletions.
77 changes: 54 additions & 23 deletions contracts/CollateralTracker.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity =0.8.18;

// Foundry
import "forge-std/Test.sol";
// Interfaces
import {PanopticFactory} from "./PanopticFactory.sol";
import {PanopticPool} from "./PanopticPool.sol";
Expand Down Expand Up @@ -842,16 +844,18 @@ contract CollateralTracker is ERC20Minimal, Multicall {
// we don't need the leg information itself, really just "the number of half ranges" from the strike price:
uint256 maxNumRangesFromStrike; // technically "maxNum(Half)RangesFromStrike" but the name is long

// stack rolling
int24 _currentTick = currentTick;
int24 _medianTick = medianTick;
uint256 _positionId = positionId;
uint128 _positionBalance = positionBalance;

unchecked {
for (uint256 leg = 0; leg < TokenId.countLegs(positionId); ++leg) {
console2.log("leg index", leg);
// short legs are not counted - exercise is intended to be based on long legs
if (positionId.isLong(leg) == 0) continue;

int24 strike = positionId.strike(leg);

uint256 currNumRangesFromStrike;

int24 rangeDown;
Expand All @@ -865,9 +869,11 @@ contract CollateralTracker is ERC20Minimal, Multicall {
/// otherwise rangeUp and rangeDown will be the same
int24 width = positionId.width(leg);
(rangeDown, rangeUp) = PanopticMath.mulDivAsTicks(width, s_tickSpacing);
console2.log("r width", width);
console2.log(" r s_tickSpacing", s_tickSpacing);
}

if (currentTick < (strike - rangeDown)) {
if (_currentTick < (_positionId.strike(leg) - rangeDown)) {
/**
current strike
tick │
Expand All @@ -877,9 +883,24 @@ contract CollateralTracker is ERC20Minimal, Multicall {
range=width/2
*/
currNumRangesFromStrike = uint256(
(2 * int256(strike - rangeUp - currentTick)) / rangeUp
(2 * int256(_positionId.strike(leg) - rangeUp - _currentTick)) / rangeUp
); // = (strike - range - _currentTick) / (range / 2); the "range/2" are the "half ranges"
} else if (currentTick > (strike + rangeUp)) {

console2.log("below");
console2.log("r currNumRangesFromStrike", currNumRangesFromStrike);
console2.log("r _positionId.strike(leg)", _positionId.strike(leg));
console2.log("r rangeUp", rangeUp);
console2.log("r _currentTick", _currentTick);

console2.log(
"r int256(_positionId.strike(leg) - rangeUp - _currentTick))",
int256(_positionId.strike(leg) - rangeUp - _currentTick)
);
console2.log(
"r int256(_positionId.strike(leg) - rangeUp - _currentTick))",
_positionId.strike(leg) - rangeUp - _currentTick
);
} else if (_currentTick > (_positionId.strike(leg) + rangeUp)) {
/**
strike current
│ tick
Expand All @@ -889,37 +910,45 @@ contract CollateralTracker is ERC20Minimal, Multicall {
range
*/
currNumRangesFromStrike = uint256(
(2 * int256(currentTick - strike - rangeUp)) / rangeUp
(2 * int256(_currentTick - _positionId.strike(leg) - rangeUp)) / rangeUp
);

console2.log("above");
}
maxNumRangesFromStrike = currNumRangesFromStrike > maxNumRangesFromStrike
? currNumRangesFromStrike
: maxNumRangesFromStrike;

uint256 tokenType = positionId.tokenType(leg);
//uint256 tokenType = positionId.tokenType(leg);

uint256 liquidityChunk = PanopticMath.getLiquidityChunk(
positionId,
leg,
positionBalance,
s_tickSpacing
);
uint256 currentValue0;
uint256 currentValue1;
uint256 medianValue0;
uint256 medianValue1;
{
uint256 liquidityChunk = PanopticMath.getLiquidityChunk(
_positionId,
leg,
_positionBalance,
s_tickSpacing
);

(uint256 currentValue0, uint256 currentValue1) = Math.getAmountsForLiquidity(
_currentTick,
liquidityChunk
);
(currentValue0, currentValue1) = Math.getAmountsForLiquidity(
_currentTick,
liquidityChunk
);

(uint256 medianValue0, uint256 medianValue1) = Math.getAmountsForLiquidity(
_medianTick,
liquidityChunk
);
(medianValue0, medianValue1) = Math.getAmountsForLiquidity(
_medianTick,
liquidityChunk
);
}

// compensate user for loss in value if chunk has lost money between current and median tick
// note: the delta for one token will be positive and the other will be negative. This cancels out any moves in their positions
if (
(tokenType == 0 && currentValue1 < medianValue1) ||
(tokenType == 1 && currentValue0 < medianValue0)
(_positionId.tokenType(leg) == 0 && currentValue1 < medianValue1) ||
(_positionId.tokenType(leg) == 1 && currentValue0 < medianValue0)
)
exerciseFees = exerciseFees.sub(
int256(0)
Expand All @@ -932,6 +961,8 @@ contract CollateralTracker is ERC20Minimal, Multicall {
);
}

console2.log(" r maxNumRangesFromStrike", maxNumRangesFromStrike);

// note: we HAVE to start with a negative number as the base exercise cost because when shifting a negative number right by n bits,
// the result is rounded DOWN and NOT toward zero
// this divergence is observed when n (the number of half ranges) is > 10 (ensuring the floor is not zero, but -1 = 1bps at that point)
Expand Down
4 changes: 4 additions & 0 deletions contracts/libraries/PanopticMath.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

// Foundry
import "forge-std/Test.sol";
// Interfaces
import {IUniswapV3Pool} from "univ3-core/interfaces/IUniswapV3Pool.sol";
// Libraries
Expand Down Expand Up @@ -466,6 +468,8 @@ library PanopticMath {
int24 width,
int24 tickSpacing
) external pure returns (int24 rangeDown, int24 rangeUp) {
console2.log("width in func", width);
console2.log("tickSpacing in func", tickSpacing);
/// @solidity memory-safe-assembly
//cache product and denominator
assembly {
Expand Down
155 changes: 154 additions & 1 deletion test/foundry/core/CollateralTracker.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,9 @@ contract CollateralTrackerTest is Test, PositionUtils {

IUniswapV3Pool constant DAI_USDC_1 = IUniswapV3Pool(0x5777d92f208679DB4b9778590Fa3CAB3aC9e2168);

IUniswapV3Pool[3] public pools = [USDC_WETH_5, WBTC_ETH_30, MATIC_ETH_30, DAI_USDC_1];
// IUniswapV3Pool[4] public pools = [USDC_WETH_5, WBTC_ETH_30, MATIC_ETH_30, DAI_USDC_1];

IUniswapV3Pool[1] public pools = [DAI_USDC_1];

// Mainnet factory address
IUniswapV3Factory V3FACTORY = IUniswapV3Factory(0x1F98431c8aD98523631AE4a59f267346ea31F984);
Expand Down Expand Up @@ -5785,6 +5787,157 @@ contract CollateralTrackerTest is Test, PositionUtils {
}
}

// check force exercise range changes
// check ranges are indeed evaluated at correct lower and upper tick bounds

// try to force exercise an OTM option
// call -> _currentTick < (strike - rangeDown)

// put -> _currentTick > (strike + rangeUp)

function test_Success_exerciseCostRanges_OTMCall(
uint256 x,
uint128 positionSizeSeed,
uint256 widthSeed,
int256 strikeSeed,
int24 atTick
) public {
uint128 required;

{
_initWorld(x);

// initalize a custom Panoptic pool
_deployCustomPanopticPool(token0, token1, pool);

// Invoke all interactions with the Collateral Tracker from user Bob
vm.startPrank(Bob);

// give Bob the max amount of tokens
_grantTokens(Bob);

// approve collateral tracker to move tokens on Bob's behalf
IERC20Partial(token0).approve(address(collateralToken0), type(uint128).max);
IERC20Partial(token1).approve(address(collateralToken1), type(uint128).max);

// equal deposits for both collateral token pairs for testing purposes
_mockMaxDeposit(Bob);

// have Bob sell
(width, strike) = PositionUtils.getOTMSW(
widthSeed,
strikeSeed,
uint24(tickSpacing),
currentTick,
0
);

tokenId = uint256(0).addUniv3pool(poolId).addLeg(0, 1, 1, 0, 0, 0, strike, width);
positionIdList.push(tokenId);

// must be minimum at least 2 so there is enough liquidity to buy
positionSize0 = uint128(bound(positionSizeSeed, 2, 2 ** 120));

_assumePositionValidity(Bob, tokenId, positionSize0);

panopticPool.mintOptions(
positionIdList,
positionSize0,
type(uint64).max,
TickMath.MIN_TICK,
TickMath.MAX_TICK
);
}

{
// Alice buys
changePrank(Alice);

// give Bob the max amount of tokens
_grantTokens(Alice);

// approve collateral tracker to move tokens on Bob's behalf
IERC20Partial(token0).approve(address(collateralToken0), type(uint128).max);
IERC20Partial(token1).approve(address(collateralToken1), type(uint128).max);

// equal deposits for both collateral token pairs for testing purposes
_mockMaxDeposit(Alice);

tokenId1 = uint256(0).addUniv3pool(poolId).addLeg(0, 1, 1, 1, 0, 0, strike, width);
positionIdList1.push(tokenId1);

_assumePositionValidity(Alice, tokenId1, positionSize0);

panopticPool.mintOptions(
positionIdList1,
positionSize0 / 2,
type(uint64).max,
TickMath.MIN_TICK,
TickMath.MAX_TICK
);
}

// check requirement at fuzzed tick
{
atTick = int24(bound(atTick, TickMath.MIN_TICK, TickMath.MAX_TICK));
atTick = (atTick / tickSpacing) * tickSpacing;

(legLowerTick, legUpperTick) = tokenId1.asTicks(0, tickSpacing);
(int24 rangeUp, int24 rangeDown) = PanopticMath.mulDivAsTicks(width, tickSpacing);

// strike - rangeDown
vm.assume(atTick < legLowerTick);

(int256 longAmounts, ) = PanopticMath.computeExercisedAmounts(
tokenId1,
0,
positionSize0 / 2,
tickSpacing
);

uint256 currNumRangesFromStrike = uint256(
(2 * int256(strike - rangeUp - currentTick)) / rangeUp
);

int256 fee = (-1_024 >> currNumRangesFromStrike);

int256 exerciseFee0 = (longAmounts.rightSlot() * fee) / 10_000;
int256 exerciseFee1 = (longAmounts.leftSlot() * fee) / 10_000;

int256 exerciseFees = collateralToken0.exerciseCost(
atTick,
atTick, // use the fuzzed tick as the median tick for testing purposes
tokenId1,
positionSize0 / 2,
longAmounts
);

console2.log(" r exerciseFees - right", exerciseFees.rightSlot());
console2.log(" r exerciseFees - left", exerciseFees.leftSlot());

console2.log(" t exerciseFees - right", exerciseFee0);
console2.log(" t exerciseFees - left", exerciseFee1);
console2.log(" t currNumRangesFromStrike", currNumRangesFromStrike);
console2.log(
"t int256(strike - rangeUp - currentTick)",
int256(strike - rangeUp - atTick)
);
console2.log("t int256(strike - rangeUp - currentTick)", strike - rangeUp - atTick);

console2.log("t strike", strike);
console2.log("t rangeUp", rangeUp);
console2.log("t _currentTick", atTick);

console2.log("t width", width);
console2.log("t s_tickSpacing", tickSpacing);
}
}

// try to force exercise an ITM option

// close a position that is ATM
// check additional requirement when ATM

/* Utilization setter */
function setUtilization(
CollateralTrackerHarness collateralToken,
Expand Down
63 changes: 63 additions & 0 deletions test/foundry/libraries/PanopticMath.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -1548,4 +1548,67 @@ contract PanopticMathTest is Test, PositionUtils {
assertEq(0, returnedShorts);
assertEq(expectedLongs, returnedLongs);
}

// mul div as ticks
function test_Success_mulDivAsTicks_1bps_1TickWide() public {
int24 width = 1;
int24 tickSpacing = 1;

(int24 rangeDown, int24 rangeUp) = harness.mulDivAsTicks(width, tickSpacing);

assertEq(rangeDown, 0, "rangeDown");
assertEq(rangeUp, 1, "rangeUp");
}

function test_Success_mulDivAsTicks_allCombos(
uint16 widthSeed,
uint16 tickSpacing,
int24 strike
) public {
// bound the width (1 -> 4094)
uint24 widthBounded = uint24(bound(widthSeed, 1, 4094));

// bound the tickSpacing
uint24 tickSpacingBounded = uint24(bound(tickSpacing, 1, 1000));

// get a valid strike
strike = int24((strike / int24(tickSpacingBounded)) * int24(tickSpacingBounded));

// validate bounds
vm.assume(strike > TickMath.MIN_TICK && strike < TickMath.MAX_TICK);

// invoke
(int24 rangeDown, int24 rangeUp) = harness.mulDivAsTicks(
int24(widthBounded),
int24(tickSpacingBounded)
);

// if width is odd and tickSpacing is odd
// then actual range will not be a whole number
if (widthBounded % 2 == 1 && tickSpacingBounded % 2 == 1) {
uint256 mulDivRangeDown = Math.mulDivDown(widthBounded, tickSpacingBounded, 2);

uint256 mulDivRangeUp = Math.mulDivUp(widthBounded, tickSpacingBounded, 2);

// check against mulDivRoundDown
assertEq(uint24(rangeDown), mulDivRangeDown);

// check against mulDivRoundUp
assertEq(uint24(rangeUp), mulDivRangeUp);
} else {
int24 range = int24((widthBounded * tickSpacingBounded) / 2);

int24 lowerTick = strike - range;
int24 upperTick = strike + range;

assertEq(strike - rangeDown, strike - range);
assertEq(strike + rangeUp, strike + range);
}

// ensure range is rounded down if width * tickSpacing is odd

// ensure range is rounded up if width * tickSpacing is odd

// else even -> rangeDown and rangeUp are both just (width * ts) / 2
}
}
Loading

0 comments on commit dbe4d36

Please sign in to comment.