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

Feat: Reintroduce segmented fees #455

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
40 changes: 29 additions & 11 deletions contracts/Portfolio.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -298,6 +302,7 @@ contract Portfolio is ERC1155, IPortfolio {
_postLock();
}


/// @inheritdoc IPortfolioActions
function allocate(
bool useMax,
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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.
Expand All @@ -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();
Expand All @@ -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;
(
Expand All @@ -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
poolId, inter.prevInvariant, adjustedVirtualX, adjustedVirtualY
);

if (!validInvariant) {
Expand All @@ -628,6 +637,17 @@ contract Portfolio is ERC1155, IPortfolio {
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,
0,
inter.feeAmountUnit
);
inter.feeGrowthGlobal += inter.feeAmountUnit;
}

// Increasing reserves requires Portfolio's balance of tokens to also increases by the end of `settlement`.
_increaseReserves(inter.tokenInput, inter.amountInputUnit);
Expand Down Expand Up @@ -737,8 +757,6 @@ contract Portfolio is ERC1155, IPortfolio {
reserveX: reserveXPerWad,
reserveY: reserveYPerWad,
feeBasisPoints: feeBasisPoints,
priorityFeeBasisPoints: priorityFeeBasisPoints,
controller: controller,
strategy: strategy
});

Expand Down
2 changes: 1 addition & 1 deletion contracts/PositionRenderer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
5 changes: 4 additions & 1 deletion contracts/interfaces/IPortfolio.sol
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,10 @@ interface IPortfolioState {
uint16 feeBasisPoints,
uint16 priorityFeeBasisPoints,
address controller,
address strategy
address strategy,
uint256 invariantGrowthGlobal,
uint256 feeGrowthGlobalAsset,
uint256 feeGrowthGlobalQuote
);
}

Expand Down
20 changes: 20 additions & 0 deletions contracts/interfaces/IStrategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
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
Expand Down Expand Up @@ -85,4 +87,22 @@ interface IStrategy is IPortfolioStrategy {
uint256 reserveX,
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);
}
19 changes: 19 additions & 0 deletions contracts/libraries/AssemblyLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
16 changes: 3 additions & 13 deletions contracts/libraries/PoolLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
}

// ----------------- //
Expand All @@ -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.
Expand All @@ -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;
}

Expand Down
83 changes: 83 additions & 0 deletions contracts/libraries/PositionLib.sol
Original file line number Diff line number Diff line change
@@ -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;
}
4 changes: 4 additions & 0 deletions contracts/libraries/SwapLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
20 changes: 15 additions & 5 deletions contracts/strategies/NormalStrategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -46,7 +47,7 @@ contract NormalStrategy is INormalStrategy {
_;
}

// ====== Required ====== //
// ====== Required ====== /

/// @inheritdoc IStrategy
function afterCreate(
Expand Down Expand Up @@ -178,8 +179,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;
Expand Down Expand Up @@ -292,13 +292,23 @@ 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);
}

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
function getInvariant(uint64 poolId)
public
Expand Down
Loading
Loading