From a1ad41399afc37db6de6a073f0037ec0b4c03213 Mon Sep 17 00:00:00 2001 From: Kinrezc Date: Thu, 25 Apr 2024 18:53:44 -0400 Subject: [PATCH 1/2] feat: conditionally segment fees if nextInvariant > 0 --- contracts/Portfolio.sol | 50 ++++++++----- contracts/PositionRenderer.sol | 2 +- contracts/interfaces/IPortfolio.sol | 5 +- contracts/interfaces/IStrategy.sol | 7 +- contracts/libraries/AssemblyLib.sol | 19 +++++ contracts/libraries/PoolLib.sol | 16 +---- contracts/libraries/PositionLib.sol | 83 ++++++++++++++++++++++ contracts/libraries/SwapLib.sol | 4 ++ contracts/strategies/NormalStrategy.sol | 35 +++++---- contracts/strategies/NormalStrategyLib.sol | 12 ++-- test/Configuration.sol | 2 +- test/TestPortfolioAllocate.t.sol | 2 +- test/TestPortfolioSwap.t.sol | 50 +++++++++++++ test/strategies/NormalConfiguration.sol | 4 +- 14 files changed, 233 insertions(+), 58 deletions(-) create mode 100644 contracts/libraries/PositionLib.sol diff --git a/contracts/Portfolio.sol b/contracts/Portfolio.sol index 850bf0a7..3b87d9d8 100644 --- a/contracts/Portfolio.sol +++ b/contracts/Portfolio.sol @@ -3,6 +3,8 @@ pragma solidity 0.8.19; import "solmate/tokens/ERC1155.sol"; import "./libraries/PortfolioLib.sol"; +import "./libraries/PositionLib.sol"; +import "./libraries/PoolLib.sol"; import "./interfaces/IERC20.sol"; import "./interfaces/IPortfolio.sol"; import "./interfaces/IPortfolioRegistry.sol"; @@ -73,6 +75,8 @@ contract Portfolio is ERC1155, IPortfolio { /// @inheritdoc IPortfolioState mapping(uint64 => PortfolioPool) public pools; + mapping(address => mapping(uint64 => PortfolioPosition)) public positions; + /// @inheritdoc IPortfolioState mapping(address => mapping(address => uint24)) public getPairId; @@ -298,6 +302,7 @@ contract Portfolio is ERC1155, IPortfolio { _postLock(); } + /// @inheritdoc IPortfolioActions function allocate( bool useMax, @@ -461,8 +466,12 @@ contract Portfolio is ERC1155, IPortfolio { * If allocating to an instantiated pool, a minimum amount of liquidity is permanently * burned to prevent the pool from reaching 0 liquidity. */ - function _changeLiquidity(ChangeLiquidityParams memory args) internal { - PortfolioPool storage pool = pools[args.poolId]; + function _changeLiquidity(ChangeLiquidityParams memory args) internal returns (uint256 feeAsset, uint256 feeQuote, uint256 invariantGrowth) { + (PortfolioPool storage pool, PortfolioPosition storage position) = (pools[args.poolId], positions[args.owner][args.poolId]); + + (feeAsset, feeQuote, invariantGrowth) = position.syncPositionFees( + pool.feeGrowthGlobalAsset, pool.feeGrowthGlobalQuote, pool.invariantGrowthGlobal + ); (uint128 deltaAssetWad, uint128 deltaQuoteWad) = (args.deltaAsset.safeCastTo128(), args.deltaQuote.safeCastTo128()); @@ -496,6 +505,7 @@ contract Portfolio is ERC1155, IPortfolio { _burn(args.owner, args.poolId, uint256(-int256(positionLiquidity))); } + position.changePositionLiquidity(args.timestamp, args.deltaLiquidity); pools[args.poolId].changePoolLiquidity(args.deltaLiquidity); (address asset, address quote) = (args.tokenAsset, args.tokenQuote); @@ -549,11 +559,13 @@ contract Portfolio is ERC1155, IPortfolio { inter.decimalsOutput = pair.decimalsQuote; inter.tokenInput = pair.tokenAsset; inter.tokenOutput = pair.tokenQuote; + inter.feeGrowthGlobal = pool.feeGrowthGlobalAsset; } else { inter.decimalsInput = pair.decimalsQuote; inter.decimalsOutput = pair.decimalsAsset; inter.tokenInput = pair.tokenQuote; inter.tokenOutput = pair.tokenAsset; + inter.feeGrowthGlobal = pool.feeGrowthGlobalQuote; } // Overwrites the input argument with the token surplus, if available. @@ -568,6 +580,7 @@ contract Portfolio is ERC1155, IPortfolio { inter.prevInvariant = invariant; inter.amountInputUnit = input; inter.amountOutputUnit = output; + inter.liquidity = pool.liquidity; // Converts input and output amounts to WAD units for the swap math. inter = inter.toWad(); @@ -586,13 +599,9 @@ contract Portfolio is ERC1155, IPortfolio { ); { - // Use the priority fee if the pool controller is the caller. - uint256 feeBps = pool.controller == msg.sender - ? pool.priorityFeeBasisPoints - : pool.feeBasisPoints; - // Compute the respective fee and protocol fee amounts. // Compute the reserves after applying the desired swap amounts and fees. + bool sellAsset = args.sellAsset; uint256 adjustedVirtualX; uint256 adjustedVirtualY; ( @@ -601,14 +610,14 @@ contract Portfolio is ERC1155, IPortfolio { adjustedVirtualX, adjustedVirtualY ) = orderInWad.computeSwapResult( - pool.virtualX, pool.virtualY, feeBps, protocolFee + pool.virtualX, pool.virtualY, pool.feeBasisPoints, protocolFee ); // ====== Invariant Check ====== // bool validInvariant; - (validInvariant, inter.nextInvariant) = strategy.validateSwap( - poolId, inter.prevInvariant, adjustedVirtualX, adjustedVirtualY + (validInvariant, inter.segmentFees, inter.nextInvariant) = strategy.validateSwap( + poolId, inter.prevInvariant, adjustedVirtualX, adjustedVirtualY, sellAsset, inter.feeAmountUnit ); if (!validInvariant) { @@ -623,11 +632,20 @@ contract Portfolio is ERC1155, IPortfolio { // Take the protocol fee from the input amount, so that protocol fee is not re-invested into pool. // Increases the independent pool reserve by the input amount, including fee and excluding protocol fee. // Decrease the dependent pool reserve by the output amount. - pool.adjustReserves( - args.sellAsset, - inter.amountInputUnit - inter.protocolFeeAmountUnit, - inter.amountOutputUnit - ); + if (inter.segmentFees) { + pool.adjustReserves( + args.sellAsset, + inter.amountInputUnit - inter.protocolFeeAmountUnit - inter.feeAmountUnit, + inter.amountOutputUnit + ); + inter.feeGrowthGlobal += inter.feeAmountUnit; + } else { + pool.adjustReserves( + args.sellAsset, + inter.amountInputUnit - inter.protocolFeeAmountUnit, + inter.amountOutputUnit + ); + } // Increasing reserves requires Portfolio's balance of tokens to also increases by the end of `settlement`. _increaseReserves(inter.tokenInput, inter.amountInputUnit); @@ -737,8 +755,6 @@ contract Portfolio is ERC1155, IPortfolio { reserveX: reserveXPerWad, reserveY: reserveYPerWad, feeBasisPoints: feeBasisPoints, - priorityFeeBasisPoints: priorityFeeBasisPoints, - controller: controller, strategy: strategy }); diff --git a/contracts/PositionRenderer.sol b/contracts/PositionRenderer.sol index 6cf66048..673b3d94 100644 --- a/contracts/PositionRenderer.sol +++ b/contracts/PositionRenderer.sol @@ -143,7 +143,7 @@ contract PositionRenderer { uint16 feeBasisPoints, uint16 priorityFeeBasisPoints, address controller, - address strategy + address strategy,,, ) = IPortfolio(msg.sender).pools(uint64(id)); uint256 spotPriceWad = IPortfolio(msg.sender).getSpotPrice(uint64(id)); diff --git a/contracts/interfaces/IPortfolio.sol b/contracts/interfaces/IPortfolio.sol index a776e19d..75817f64 100644 --- a/contracts/interfaces/IPortfolio.sol +++ b/contracts/interfaces/IPortfolio.sol @@ -266,7 +266,10 @@ interface IPortfolioState { uint16 feeBasisPoints, uint16 priorityFeeBasisPoints, address controller, - address strategy + address strategy, + uint256 invariantGrowthGlobal, + uint256 feeGrowthGlobalAsset, + uint256 feeGrowthGlobalQuote ); } diff --git a/contracts/interfaces/IStrategy.sol b/contracts/interfaces/IStrategy.sol index 88361904..ebefcaf5 100644 --- a/contracts/interfaces/IStrategy.sol +++ b/contracts/interfaces/IStrategy.sol @@ -2,6 +2,7 @@ pragma solidity >=0.8.0; import { Order } from "../libraries/SwapLib.sol"; +import { SwapState } from "../libraries/SwapLib.sol"; import { IPortfolioStrategy } from "./IPortfolio.sol"; /** @@ -83,6 +84,8 @@ interface IStrategy is IPortfolioStrategy { uint64 poolId, int256 invariant, uint256 reserveX, - uint256 reserveY - ) external view returns (bool, int256); + uint256 reserveY, + bool sellAsset, + uint256 feeAmountUnit + ) external view returns (bool, bool, int256); } diff --git a/contracts/libraries/AssemblyLib.sol b/contracts/libraries/AssemblyLib.sol index df226eed..7d2d3c36 100644 --- a/contracts/libraries/AssemblyLib.sol +++ b/contracts/libraries/AssemblyLib.sol @@ -173,4 +173,23 @@ library AssemblyLib { upper = data >> 4; lower = data & 0x0f; } + + /** + * @notice Computes the difference between two checkpoints. + * @dev Underflows. + * @custom:example + * ``` + * uint256 distance = computeCheckpointDistance(50, 25); + * assertEq(distance, 25); + * ``` + */ + function computeCheckpointDistance( + uint256 present, + uint256 past + ) internal pure returns (uint256 distance) { + // Underflow by design, as these are checkpoints which can measure the distance even if underflowed. + assembly { + distance := sub(present, past) + } + } } diff --git a/contracts/libraries/PoolLib.sol b/contracts/libraries/PoolLib.sol index 526415a3..37de0b4c 100644 --- a/contracts/libraries/PoolLib.sol +++ b/contracts/libraries/PoolLib.sol @@ -206,6 +206,9 @@ struct PortfolioPool { uint16 priorityFeeBasisPoints; address controller; // Address that can call `changeParameters()`. address strategy; + uint256 invariantGrowthGlobal; // Cumulative sum of positive invariant growth. + uint256 feeGrowthGlobalAsset; // Cumulative sum of fee's denominated in the `asset` with positive invariant. + uint256 feeGrowthGlobalQuote; // Cumulative sum of fee's denominated in the `quote` with positive invariant. } // ----------------- // @@ -216,8 +219,6 @@ function createPool( uint256 reserveX, uint256 reserveY, uint256 feeBasisPoints, - uint256 priorityFeeBasisPoints, - address controller, address strategy ) { // Check if the pool has already been created. @@ -235,17 +236,6 @@ function createPool( } self.feeBasisPoints = feeBasisPoints.safeCastTo16(); - // Controller is not required, so it can remain uninitialized at the zero address. - bool controlled = controller != address(0); - if (controlled) { - if (!priorityFeeBasisPoints.isBetween(MIN_FEE, feeBasisPoints)) { - revert PoolLib_InvalidPriorityFee(priorityFeeBasisPoints); - } - - self.controller = controller; - self.priorityFeeBasisPoints = priorityFeeBasisPoints.safeCastTo16(); - } - self.strategy = strategy; } diff --git a/contracts/libraries/PositionLib.sol b/contracts/libraries/PositionLib.sol new file mode 100644 index 00000000..94379154 --- /dev/null +++ b/contracts/libraries/PositionLib.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.19; + +import "solmate/utils/SafeCastLib.sol"; +import "solmate/utils/FixedPointMathLib.sol"; +import "./AssemblyLib.sol"; + +using { + changePositionLiquidity, + syncPositionFees, + getTimeSinceChanged +} for PortfolioPosition global; + +struct PortfolioPosition { + uint128 freeLiquidity; + uint32 lastTimestamp; + uint256 invariantGrowthLast; // Increases when the invariant increases from a positive value. + uint256 feeGrowthAssetLast; + uint256 feeGrowthQuoteLast; + uint128 tokensOwedAsset; + uint128 tokensOwedQuote; + uint128 invariantOwed; // Not used by Portfolio, but can be used by a pool controller. +} + +/** + * @dev Liquidity must be altered after syncing positions and not before. + */ +function syncPositionFees( + PortfolioPosition storage self, + uint256 feeGrowthAsset, + uint256 feeGrowthQuote, + uint256 invariantGrowth +) + returns ( + uint256 feeAssetEarned, + uint256 feeQuoteEarned, + uint256 feeInvariantEarned + ) +{ + // fee growth current - position fee growth last + uint256 differenceAsset = AssemblyLib.computeCheckpointDistance( + feeGrowthAsset, self.feeGrowthAssetLast + ); + uint256 differenceQuote = AssemblyLib.computeCheckpointDistance( + feeGrowthQuote, self.feeGrowthQuoteLast + ); + uint256 differenceInvariant = AssemblyLib.computeCheckpointDistance( + invariantGrowth, self.invariantGrowthLast + ); + + // fee growth per liquidity * position liquidity + feeAssetEarned = + FixedPointMathLib.mulWadDown(differenceAsset, self.freeLiquidity); + feeQuoteEarned = + FixedPointMathLib.mulWadDown(differenceQuote, self.freeLiquidity); + feeInvariantEarned = + FixedPointMathLib.mulWadDown(differenceInvariant, self.freeLiquidity); + + self.feeGrowthAssetLast = feeGrowthAsset; + self.feeGrowthQuoteLast = feeGrowthQuote; + self.invariantGrowthLast = invariantGrowth; + + self.tokensOwedAsset += SafeCastLib.safeCastTo128(feeAssetEarned); + self.tokensOwedQuote += SafeCastLib.safeCastTo128(feeQuoteEarned); + self.invariantOwed += SafeCastLib.safeCastTo128(feeInvariantEarned); +} + +function changePositionLiquidity( + PortfolioPosition storage self, + uint256 timestamp, + int128 liquidityDelta +) { + self.lastTimestamp = uint32(timestamp); + self.freeLiquidity = + AssemblyLib.addSignedDelta(self.freeLiquidity, liquidityDelta); +} + +function getTimeSinceChanged( + PortfolioPosition memory self, + uint256 timestamp +) pure returns (uint256 distance) { + return timestamp - self.lastTimestamp; +} diff --git a/contracts/libraries/SwapLib.sol b/contracts/libraries/SwapLib.sol index 0cc10b1f..b5a50dc1 100644 --- a/contracts/libraries/SwapLib.sol +++ b/contracts/libraries/SwapLib.sol @@ -201,6 +201,10 @@ struct SwapState { address tokenOutput; uint8 decimalsInput; uint8 decimalsOutput; + uint256 feeGrowthGlobal; + int256 invariantGrowthGlobal; + uint256 liquidity; + bool segmentFees; } using { toWad, fromWad } for SwapState global; diff --git a/contracts/strategies/NormalStrategy.sol b/contracts/strategies/NormalStrategy.sol index 1ce03ef0..74b9496a 100644 --- a/contracts/strategies/NormalStrategy.sol +++ b/contracts/strategies/NormalStrategy.sol @@ -46,7 +46,7 @@ contract NormalStrategy is INormalStrategy { _; } - // ====== Required ====== // + // ====== Required ====== / /// @inheritdoc IStrategy function afterCreate( @@ -109,9 +109,12 @@ contract NormalStrategy is INormalStrategy { uint64 poolId, int256 invariant, uint256 reserveX, - uint256 reserveY - ) public view returns (bool, int256) { + uint256 reserveY, + bool sellAsset, + uint256 feeAmountUnit + ) public view returns (bool, bool, int256) { PortfolioPool memory pool = IPortfolioStruct(portfolio).pools(poolId); + bool segmentFees = false; // Update the reserves in memory. pool.virtualX = reserveX.safeCastTo128(); @@ -119,9 +122,14 @@ contract NormalStrategy is INormalStrategy { // Compute the new invariant. int256 invariantAfterSwap = pool.getInvariantDown(configs[poolId]); - bool valid = _validateSwap(invariant, invariantAfterSwap); + if (invariantAfterSwap > 0) { + segmentFees = true; + sellAsset ? pool.virtualX -= feeAmountUnit.safeCastTo128() : pool.virtualY -= feeAmountUnit.safeCastTo128(); + invariantAfterSwap = pool.getInvariantDown(configs[poolId]); + } + bool valid = _validateSwap(invariant, invariantAfterSwap, segmentFees); - return (valid, invariantAfterSwap); + return (valid, segmentFees, invariantAfterSwap); } /** @@ -144,10 +152,15 @@ contract NormalStrategy is INormalStrategy { */ function _validateSwap( int256 invariantBefore, - int256 invariantAfter + int256 invariantAfter, + bool segmentFees ) internal pure returns (bool) { int256 delta = invariantAfter - invariantBefore; - if (delta < MINIMUM_INVARIANT_DELTA) return false; + if (segmentFees) { + if (invariantBefore < invariantAfter) return false; + } else { + if (delta < MINIMUM_INVARIANT_DELTA) return false; + } return true; } @@ -178,8 +191,7 @@ contract NormalStrategy is INormalStrategy { sellAsset: sellAsset }), timestamp: block.timestamp, - protocolFee: IPortfolio(portfolio).protocolFee(), - swapper: swapper + protocolFee: IPortfolio(portfolio).protocolFee() }); uint256 outputDec = sellAsset ? pair.decimalsQuote : pair.decimalsAsset; @@ -292,11 +304,10 @@ contract NormalStrategy is INormalStrategy { config: configs[order.poolId], order: order, timestamp: timestamp, - protocolFee: IPortfolio(portfolio).protocolFee(), - swapper: swapper + protocolFee: IPortfolio(portfolio).protocolFee() }); - success = _validateSwap(prevInvariant, postInvariant); + success = _validateSwap(prevInvariant, postInvariant, false); } /// @inheritdoc IPortfolioStrategy diff --git a/contracts/strategies/NormalStrategyLib.sol b/contracts/strategies/NormalStrategyLib.sol index 3f5bb482..69be56cd 100644 --- a/contracts/strategies/NormalStrategyLib.sol +++ b/contracts/strategies/NormalStrategyLib.sol @@ -737,8 +737,7 @@ library NormalStrategyLib { PortfolioConfig memory config, Order memory order, uint256 timestamp, - uint256 protocolFee, - address swapper + uint256 protocolFee ) internal view @@ -754,9 +753,7 @@ library NormalStrategyLib { // Compute the next invariant if the swap amounts are non zero. (uint256 reserveX, uint256 reserveY) = (self.virtualX, self.virtualY); - uint256 feeBps = swapper == self.controller - ? self.priorityFeeBasisPoints - : self.feeBasisPoints; + uint256 feeBps = self.feeBasisPoints; // Compute the adjusted reserves. (,, reserveX, reserveY) = @@ -785,11 +782,10 @@ library NormalStrategyLib { PortfolioConfig memory config, Order memory order, uint256 timestamp, - uint256 protocolFee, - address swapper + uint256 protocolFee ) internal view returns (uint256 amountOutWad) { (uint256 independentReserve, int256 prevInv, int256 postInv) = - getSwapInvariants(self, config, order, timestamp, protocolFee, swapper); + getSwapInvariants(self, config, order, timestamp, protocolFee); NormalCurve memory curve = transform(config); curve.invariant = prevInv; diff --git a/test/Configuration.sol b/test/Configuration.sol index 0fe6d870..271ab5da 100644 --- a/test/Configuration.sol +++ b/test/Configuration.sol @@ -52,7 +52,7 @@ error Configuration_FuzzInvalidKey(bytes32 what); address constant Configuration_DEFAULT_CONTROLLER = address(0); address constant Configuration_DEFAULT_STRATEGY = address(0); -uint16 constant Configuration_DEFAULT_FEE = 30; +uint16 constant Configuration_DEFAULT_FEE = 1; uint16 constant Configuration_DEFAULT_PRIORITY_FEE = 0; /// @dev Instantiates a configuration with default values. diff --git a/test/TestPortfolioAllocate.t.sol b/test/TestPortfolioAllocate.t.sol index 5782d893..2571576f 100644 --- a/test/TestPortfolioAllocate.t.sol +++ b/test/TestPortfolioAllocate.t.sol @@ -59,7 +59,7 @@ contract TestPortfolioAllocate is Setup { subject().multicall(data); - (,, uint128 liquidity,,,,,) = subject().pools(poolId); + (,, uint128 liquidity,,,,,,,,) = subject().pools(poolId); assertEq(liquidity, 1 ether, "liquidity"); } diff --git a/test/TestPortfolioSwap.t.sol b/test/TestPortfolioSwap.t.sol index 41fd4e82..a3160424 100644 --- a/test/TestPortfolioSwap.t.sol +++ b/test/TestPortfolioSwap.t.sol @@ -112,6 +112,56 @@ contract TestPortfolioSwap is Setup { assertTrue(post > prev, "physical-balance-did-not-increase"); } + function test_swap_invariant_overtime() + public + defaultConfig + useActor + usePairTokens(10 ether) + allocateSome(1 ether) + { + bool sellAsset = true; + uint128 amtIn = 0.5 ether; + int256 initialInvariant = subject().getInvariant(ghost().poolId); + console.logInt(initialInvariant); + + uint256 i = 0; + while (i < 5) { + vm.warp(block.timestamp + 1 days); + (uint256 reserveAsset, uint256 reserveQuote) = + subject().getPoolReserves(ghost().poolId); + sellAsset = reserveQuote > 1 ether ? true : sellAsset; + sellAsset = reserveAsset > 1 ether ? false : sellAsset; + amtIn = uint128(sellAsset ? reserveQuote - 0.12 ether : reserveAsset - 0.12 ether); + // amtIn = 1000; + console.log("amtIn", amtIn); + uint128 amtOut = uint128( + subject().getAmountOut(ghost().poolId, sellAsset, amtIn, actor()) + ); + console.log("sellAsset", sellAsset); + console.log("reserveAsset", reserveAsset); + console.log("reserveQuote", reserveQuote); + console2.log("iter", i); + + + Order memory order = Order({ + useMax: false, + poolId: ghost().poolId, + input: amtIn, + output: amtOut, + sellAsset: sellAsset + }); + + subject().swap(order); + + int256 invariant = subject().getInvariant(ghost().poolId); + console.logInt(invariant); + + sellAsset = !sellAsset; + i++; + } + } + + function test_swap_protocol_fee() public defaultConfig diff --git a/test/strategies/NormalConfiguration.sol b/test/strategies/NormalConfiguration.sol index 5431e501..3d33dc2b 100644 --- a/test/strategies/NormalConfiguration.sol +++ b/test/strategies/NormalConfiguration.sol @@ -20,8 +20,8 @@ import "contracts/strategies/NormalStrategyLib.sol"; uint256 constant NormalConfiguration_DEFAULT_PRICE = 1 ether; uint256 constant NormalConfiguration_DEFAULT_STRIKE_WAD = 1 ether; -uint256 constant NormalConfiguration_DEFAULT_VOLATILITY_BPS = 1000 wei; // in bps -uint256 constant NormalConfiguration_DEFAULT_DURATION_SEC = 1 days; // in seconds +uint256 constant NormalConfiguration_DEFAULT_VOLATILITY_BPS = 100 wei; // in bps +uint256 constant NormalConfiguration_DEFAULT_DURATION_SEC = 365 days + 100; // in seconds uint256 constant NormalConfiguration_DEFAULT_CREATION_TIMESTAMP = 0; bool constant NormalConfiguration_DEFAULT_IS_PERPETUAL = false; From b130033587972843a731569a68d14ff362bff4c2 Mon Sep 17 00:00:00 2001 From: Kinrezc Date: Fri, 26 Apr 2024 16:52:19 -0400 Subject: [PATCH 2/2] fix: correctly segment fees on condition that invariant > 0 --- contracts/Portfolio.sol | 26 +++++++++-------- contracts/interfaces/IStrategy.sol | 25 +++++++++++++--- contracts/strategies/NormalStrategy.sol | 39 ++++++++++++------------- test/TestPortfolioSwap.t.sol | 12 ++++---- test/strategies/NormalConfiguration.sol | 2 +- 5 files changed, 61 insertions(+), 43 deletions(-) diff --git a/contracts/Portfolio.sol b/contracts/Portfolio.sol index 3b87d9d8..ed096361 100644 --- a/contracts/Portfolio.sol +++ b/contracts/Portfolio.sol @@ -616,8 +616,8 @@ contract Portfolio is ERC1155, IPortfolio { // ====== Invariant Check ====== // bool validInvariant; - (validInvariant, inter.segmentFees, inter.nextInvariant) = strategy.validateSwap( - poolId, inter.prevInvariant, adjustedVirtualX, adjustedVirtualY, sellAsset, inter.feeAmountUnit + (validInvariant, inter.nextInvariant) = strategy.validateSwap( + poolId, inter.prevInvariant, adjustedVirtualX, adjustedVirtualY ); if (!validInvariant) { @@ -632,19 +632,21 @@ contract Portfolio is ERC1155, IPortfolio { // Take the protocol fee from the input amount, so that protocol fee is not re-invested into pool. // Increases the independent pool reserve by the input amount, including fee and excluding protocol fee. // Decrease the dependent pool reserve by the output amount. - if (inter.segmentFees) { + pool.adjustReserves( + args.sellAsset, + inter.amountInputUnit - inter.protocolFeeAmountUnit, + inter.amountOutputUnit + ); + // returns false if the invariant is > 0 + bool valid = strategy.checkInvariant(poolId, pool); + if (!valid) { + // If the invariant is > 0, the pool is segmented and the fees are not reinvested. pool.adjustReserves( - args.sellAsset, - inter.amountInputUnit - inter.protocolFeeAmountUnit - inter.feeAmountUnit, - inter.amountOutputUnit + !args.sellAsset, + 0, + inter.feeAmountUnit ); inter.feeGrowthGlobal += inter.feeAmountUnit; - } else { - pool.adjustReserves( - args.sellAsset, - inter.amountInputUnit - inter.protocolFeeAmountUnit, - inter.amountOutputUnit - ); } // Increasing reserves requires Portfolio's balance of tokens to also increases by the end of `settlement`. diff --git a/contracts/interfaces/IStrategy.sol b/contracts/interfaces/IStrategy.sol index ebefcaf5..5e0ac9d7 100644 --- a/contracts/interfaces/IStrategy.sol +++ b/contracts/interfaces/IStrategy.sol @@ -4,6 +4,7 @@ pragma solidity >=0.8.0; import { Order } from "../libraries/SwapLib.sol"; import { SwapState } from "../libraries/SwapLib.sol"; import { IPortfolioStrategy } from "./IPortfolio.sol"; +import { PortfolioPool } from "../libraries/PoolLib.sol"; /** * @title @@ -84,8 +85,24 @@ interface IStrategy is IPortfolioStrategy { uint64 poolId, int256 invariant, uint256 reserveX, - uint256 reserveY, - bool sellAsset, - uint256 feeAmountUnit - ) external view returns (bool, bool, int256); + uint256 reserveY + ) external view returns (bool, int256); + + /** + * @notice + * Checks the validity of the invariant of the in-memory pool during swap execution. + * + * @dev + * Critical function that is responsible for the economic validity of the protocol. + * Swaps should not push the invariant over 0 in many pools + * This tells portfolio if it should segment fees or not + * + * @param poolId Id of the pool. + * @param pool intermediate pool struct in swap execution + * @return success Whether the invariant is positive. + */ + function checkInvariant( + uint64 poolId, + PortfolioPool memory pool + ) external view returns (bool); } diff --git a/contracts/strategies/NormalStrategy.sol b/contracts/strategies/NormalStrategy.sol index 74b9496a..adc15e6c 100644 --- a/contracts/strategies/NormalStrategy.sol +++ b/contracts/strategies/NormalStrategy.sol @@ -6,6 +6,7 @@ import "../libraries/BisectionLib.sol"; import "../libraries/PortfolioLib.sol"; import "./INormalStrategy.sol"; import "./NormalStrategyLib.sol"; +import "forge-std/console2.sol"; /// @dev Emitted when a hook is called by a non-portfolio address. error NormalStrategy_NotPortfolio(); @@ -109,12 +110,9 @@ contract NormalStrategy is INormalStrategy { uint64 poolId, int256 invariant, uint256 reserveX, - uint256 reserveY, - bool sellAsset, - uint256 feeAmountUnit - ) public view returns (bool, bool, int256) { + uint256 reserveY + ) public view returns (bool, int256) { PortfolioPool memory pool = IPortfolioStruct(portfolio).pools(poolId); - bool segmentFees = false; // Update the reserves in memory. pool.virtualX = reserveX.safeCastTo128(); @@ -122,14 +120,9 @@ contract NormalStrategy is INormalStrategy { // Compute the new invariant. int256 invariantAfterSwap = pool.getInvariantDown(configs[poolId]); - if (invariantAfterSwap > 0) { - segmentFees = true; - sellAsset ? pool.virtualX -= feeAmountUnit.safeCastTo128() : pool.virtualY -= feeAmountUnit.safeCastTo128(); - invariantAfterSwap = pool.getInvariantDown(configs[poolId]); - } - bool valid = _validateSwap(invariant, invariantAfterSwap, segmentFees); + bool valid = _validateSwap(invariant, invariantAfterSwap); - return (valid, segmentFees, invariantAfterSwap); + return (valid, invariantAfterSwap); } /** @@ -152,15 +145,10 @@ contract NormalStrategy is INormalStrategy { */ function _validateSwap( int256 invariantBefore, - int256 invariantAfter, - bool segmentFees + int256 invariantAfter ) internal pure returns (bool) { int256 delta = invariantAfter - invariantBefore; - if (segmentFees) { - if (invariantBefore < invariantAfter) return false; - } else { - if (delta < MINIMUM_INVARIANT_DELTA) return false; - } + if (delta < MINIMUM_INVARIANT_DELTA) return false; return true; } @@ -307,7 +295,18 @@ contract NormalStrategy is INormalStrategy { protocolFee: IPortfolio(portfolio).protocolFee() }); - success = _validateSwap(prevInvariant, postInvariant, false); + success = _validateSwap(prevInvariant, postInvariant); + } + + function checkInvariant(uint64 poolId, PortfolioPool memory pool) + public + view + returns (bool) + { + int256 invariant = pool.getInvariantDown(configs[poolId]); + console2.log("invariant in checkInvariant", invariant); + return invariant < 0; + } /// @inheritdoc IPortfolioStrategy diff --git a/test/TestPortfolioSwap.t.sol b/test/TestPortfolioSwap.t.sol index a3160424..d61c5735 100644 --- a/test/TestPortfolioSwap.t.sol +++ b/test/TestPortfolioSwap.t.sol @@ -121,17 +121,17 @@ contract TestPortfolioSwap is Setup { { bool sellAsset = true; uint128 amtIn = 0.5 ether; - int256 initialInvariant = subject().getInvariant(ghost().poolId); - console.logInt(initialInvariant); uint256 i = 0; - while (i < 5) { + while (i < 365) { vm.warp(block.timestamp + 1 days); (uint256 reserveAsset, uint256 reserveQuote) = subject().getPoolReserves(ghost().poolId); + int256 invariant = subject().getInvariant(ghost().poolId); + console2.log("invariant before swap execution", invariant); sellAsset = reserveQuote > 1 ether ? true : sellAsset; sellAsset = reserveAsset > 1 ether ? false : sellAsset; - amtIn = uint128(sellAsset ? reserveQuote - 0.12 ether : reserveAsset - 0.12 ether); + amtIn = uint128(sellAsset ? reserveQuote - 0.4 ether : reserveAsset - 0.4 ether); // amtIn = 1000; console.log("amtIn", amtIn); uint128 amtOut = uint128( @@ -153,8 +153,8 @@ contract TestPortfolioSwap is Setup { subject().swap(order); - int256 invariant = subject().getInvariant(ghost().poolId); - console.logInt(invariant); + invariant = subject().getInvariant(ghost().poolId); + console2.log("invariant after swap execution", invariant); sellAsset = !sellAsset; i++; diff --git a/test/strategies/NormalConfiguration.sol b/test/strategies/NormalConfiguration.sol index 3d33dc2b..67dd32fb 100644 --- a/test/strategies/NormalConfiguration.sol +++ b/test/strategies/NormalConfiguration.sol @@ -20,7 +20,7 @@ import "contracts/strategies/NormalStrategyLib.sol"; uint256 constant NormalConfiguration_DEFAULT_PRICE = 1 ether; uint256 constant NormalConfiguration_DEFAULT_STRIKE_WAD = 1 ether; -uint256 constant NormalConfiguration_DEFAULT_VOLATILITY_BPS = 100 wei; // in bps +uint256 constant NormalConfiguration_DEFAULT_VOLATILITY_BPS = 500 wei; // in bps uint256 constant NormalConfiguration_DEFAULT_DURATION_SEC = 365 days + 100; // in seconds uint256 constant NormalConfiguration_DEFAULT_CREATION_TIMESTAMP = 0; bool constant NormalConfiguration_DEFAULT_IS_PERPETUAL = false;