Contract Name:
LPManagerHelper
Contract Source Code:
// SPDX-License-Identifier: BUSL-1.1
// (c) Long Gamma Labs, 2024.
pragma solidity ^0.8.28;
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { ErrorReporter } from "./ErrorReporter.sol";
import { ILPManager } from "./interfaces/ILPManager.sol";
import { IProxyLOB } from "./interfaces/IProxyLOB.sol";
import { TokenValueCalculator } from "./utils/TokenValueCalculator.sol";
contract LPManagerHelper {
/// @notice Calculates the total USD value of all tokens in the liquidity pool.
/// @return totalUSDValue The total USD value of the liquidity pool, normalized to 18 decimal places.
function getTotalValue(address lpManagerAddress) external view returns (uint256 totalUSDValue) {
ILPManager lpManager = ILPManager(lpManagerAddress);
IProxyLOB proxyLOB = IProxyLOB(lpManagerAddress);
uint256 tokensCount = lpManager.getTokensCount();
for (uint8 i = 0; i < tokensCount; ++i) {
(address tokenAddress,,,,, uint8 decimals,,) = lpManager.tokens(i);
IERC20 token = IERC20(tokenAddress);
uint256 currentTokenShares = token.balanceOf(lpManagerAddress) + proxyLOB.lobReservesByTokenId(i);
(uint256 currentTokenPrice, int32 currentTokenPriceExpo) = proxyLOB.getPriceOf(i);
uint256 currentTokenValue = TokenValueCalculator.calcNormalizedValue(
currentTokenShares,
decimals,
currentTokenPrice,
currentTokenPriceExpo
);
totalUSDValue += currentTokenValue;
}
}
/// @notice Returns two arrays, one with the shares of each token, and the other with their USD value.
/// @dev This function is primarily intended for off-chain analysis.
/// @return shares An array with the shares of each token.
/// @return usdValues An array with the USD values of each token.
function getTokenSharesAndValues(address lpManagerAddress) external view returns (uint256[] memory shares, uint256[] memory usdValues) {
ILPManager lpManager = ILPManager(lpManagerAddress);
IProxyLOB proxyLOB = IProxyLOB(lpManagerAddress);
uint256 tokenCount = lpManager.getTokensCount();
shares = new uint256[](tokenCount);
usdValues = new uint256[](tokenCount);
for (uint8 i = 0; i < tokenCount; ++i) {
(address tokenAddress,,,,, uint8 decimals,,) = lpManager.tokens(i);
IERC20 token = IERC20(tokenAddress);
uint256 currentTokenShares = token.balanceOf(lpManagerAddress) + proxyLOB.lobReservesByTokenId(i);
shares[i] = currentTokenShares;
(uint256 currentTokenPrice, int32 currentTokenPriceExpo) = proxyLOB.getPriceOf(i);
usdValues[i] = TokenValueCalculator.calcNormalizedValue(
currentTokenShares,
decimals,
currentTokenPrice,
currentTokenPriceExpo
);
}
}
/// @notice Retrieves the total number of token shares, considering the balance on the contract and orders.
/// @param tokenId The identifier of the token in the tokens array.
/// @return The total number of token shares.
function getTotalSharesOf(address lpManagerAddress, uint8 tokenId) external view returns (uint256) {
ILPManager lpManager = ILPManager(lpManagerAddress);
IProxyLOB proxyLOB = IProxyLOB(lpManagerAddress);
require(tokenId < lpManager.getTokensCount(), ErrorReporter.WrongTokenId());
(address tokenAddress,,,,,,,) = lpManager.tokens(tokenId);
IERC20 token = IERC20(tokenAddress);
uint256 tokensInContract = token.balanceOf(lpManagerAddress);
uint256 tokensInOrders = proxyLOB.lobReservesByTokenId(tokenId);
return (tokensInContract + tokensInOrders);
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/IERC20.sol)
pragma solidity ^0.8.20;
/**
* @dev Interface of the ERC-20 standard as defined in the ERC.
*/
interface IERC20 {
/**
* @dev Emitted when `value` tokens are moved from one account (`from`) to
* another (`to`).
*
* Note that `value` may be zero.
*/
event Transfer(address indexed from, address indexed to, uint256 value);
/**
* @dev Emitted when the allowance of a `spender` for an `owner` is set by
* a call to {approve}. `value` is the new allowance.
*/
event Approval(address indexed owner, address indexed spender, uint256 value);
/**
* @dev Returns the value of tokens in existence.
*/
function totalSupply() external view returns (uint256);
/**
* @dev Returns the value of tokens owned by `account`.
*/
function balanceOf(address account) external view returns (uint256);
/**
* @dev Moves a `value` amount of tokens from the caller's account to `to`.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transfer(address to, uint256 value) external returns (bool);
/**
* @dev Returns the remaining number of tokens that `spender` will be
* allowed to spend on behalf of `owner` through {transferFrom}. This is
* zero by default.
*
* This value changes when {approve} or {transferFrom} are called.
*/
function allowance(address owner, address spender) external view returns (uint256);
/**
* @dev Sets a `value` amount of tokens as the allowance of `spender` over the
* caller's tokens.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* IMPORTANT: Beware that changing an allowance with this method brings the risk
* that someone may use both the old and the new allowance by unfortunate
* transaction ordering. One possible solution to mitigate this race
* condition is to first reduce the spender's allowance to 0 and set the
* desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*
* Emits an {Approval} event.
*/
function approve(address spender, uint256 value) external returns (bool);
/**
* @dev Moves a `value` amount of tokens from `from` to `to` using the
* allowance mechanism. `value` is then deducted from the caller's
* allowance.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transferFrom(address from, address to, uint256 value) external returns (bool);
}
// SPDX-License-Identifier: BUSL-1.1
// (c) Long Gamma Labs, 2024.
pragma solidity ^0.8.28;
contract ErrorReporter {
error CooldownDurationNotYetPassed();
// 0x5fba365d
error EmptyDomainName();
// 0x2f601761
error EmptyTokenName();
// 0xe2592aed
error EmptyTokenSymbol();
// 0x19c7070a
error Expired();
// 0x203d82d8
error FeeBpsExceedsMaximum();
// 0x132df9c5
error Forbidden();
// 0xee90c468
error InsufficientBalance();
// 0xf4d678b8
error InsufficientFeeForPythUpdate();
// 0xe4764c6f
error InsufficientLiquidityValue();
// 0x5b635d0b
error InsufficientMarketMakerLPShare();
// 0x28f53493
error InsufficientMintedLP();
// 0x212c18d0
error InsufficientTokenAmount();
// 0x2ec48042
error InsufficientUSDValue();
// 0xd6f69157
error InvalidFloatingPointRepresentation();
// 0xa25f85b7
error InvalidLob();
// 0xb9c44f1a
error InvalidLobAddress();
// 0xec09da35
error InvalidOracleConfidenceLevel();
// 0xe6b9bbad
error InvalidSignature();
// 0x8baa579f
error InvalidTokenWeights();
// 0x4b8072c3
error InvalidTrader();
// 0xfb7595a2
error InvalidTransfer();
// 0x2f352531
error LobDisabled();
// 0xa6876da4
error MarketMakerLPShareExceedsMaximum();
// 0x84d3d6cb
error MaxLiquidityValueExceeded();
// 0x9009a2d8
error MaxOracleAgeExceedsMaximum();
// 0x8b7df994
error NativeGasTokenDisabled();
// 0x60787531
error NonPositivePrice();
// 0x13caeeae
error NotImplementedYet();
// 0xf88c75b4
error OracleConfTooHigh();
// 0x004f2349
error OrderAlreadyUsed();
// 0x88b39043
error PartiallyPaused();
// 0x1fa6172a
error PriceTooBig();
// 0x9bec8e38
error PriceTooSmall();
// 0x8460540d
error SlashingUnAvailable();
// 0xd7b45887
error TokenDisabled();
// 0x1931ea85
error TokenWeightExceeded();
// 0x725ad4f5
error TransferFailed();
// 0x90b8ec18
error UnknownLob();
// 0x0b1066eb
error WrongNumber();
// 0x3546a07e
error WrongTokenId();
// 0x749aeece
error ZeroAddress();
// 0xd92e233d
error ZeroAmount();
// 0x1f2a2005
}
// SPDX-License-Identifier: BUSL-1.1
// (c) Long Gamma Labs, 2024.
pragma solidity ^0.8.28;
struct FeeConfig {
/// @notice Enable or disable dynamic fees adjustment based on token weight imbalances.
bool dynamicFeesEnabled;
/// @notice Admin fee for minting LP tokens (100 = 1%, maximum 1000 = 10%).
uint16 adminMintLPFeeBps;
/// @notice Admin fee for burning LP tokens (100 = 1%, maximum 1000 = 10%).
uint16 adminBurnLPFeeBps;
/// @notice Base protocol fee (100 = 1%, maximum 1000 = 10%).
uint16 feeBasisBps;
/// @notice Additional tax fee for imbalanced operations (100 = 1%, maximum 1000 = 10%).
uint16 taxBasisBps;
/// @notice Performance fee in basis points charged when LP token price reaches new high-water mark (100 = 1%, maximum 3000 = 30%).
uint16 perfFeeBps;
/// @notice Admin's share of the performance fee in basis points (100 = 1%, maximum 10000 = 100%).
uint16 adminPerfFeeBps;
/// @notice The address that will receive the admin fees.
address adminFeeRecipient;
}
struct LiquidityConfig {
/// @notice Lockup period after adding liquidity in seconds
/// during which removeLiquidity and LP token transfers are blocked
/// (maximum 16 777 215 seconds ≈ 194 days)
uint24 cooldownDuration;
/// @notice Minimum liquidity value in USD (scaled by 1e18).
uint128 minLiquidityValueUsd;
/// @notice Maximum liquidity value in USD (scaled by 1e18).
uint128 maxLiquidityValueUsd;
}
struct MarketMakerConfig {
/// @notice Enable or disable minimum market maker share in LP tokens.
bool marketMakerLPShareEnabled;
/// @notice Minimum market maker share in LP tokens (100 = 1%, maximum 10000 = 100%).
uint16 marketMakerLPShareBps;
}
struct NativeTokenConfig {
/// @notice Flag to enable or disable the use of the native GAS token for transactions.
bool enabled;
/// @notice The index of the token in the tokens array that is used as the native GAS token, if enabled.
uint8 tokenId;
}
struct PriceConfig {
/// @notice Maximum allowable age of oracle price data in seconds.
uint16 maxOracleAge;
/// @notice The period in seconds for which the LP token price is considered valid.
uint24 priceValidityPeriod;
/// @notice Base multiplier for calculating the allowed LP token price deviation (scaled by 1e18).
uint64 baseMultiplier;
/// @notice The maximum allowed deviation of the LP token price from the previous valid price.
uint64 maxAllowedPriceDeviation;
}
interface ILPManager {
// governance
function setConfig(
FeeConfig calldata feeConfig,
LiquidityConfig calldata liquidityConfig,
MarketMakerConfig calldata mmConfig,
NativeTokenConfig calldata nativeConfig,
PriceConfig calldata priceConfig
) external;
function getConfig() external view returns (
FeeConfig memory feeConfig,
LiquidityConfig memory liquidityConfig,
MarketMakerConfig memory mmConfig,
NativeTokenConfig memory nativeConfig,
PriceConfig memory priceConfig,
bool slashingStatus
);
function changeToken(
address tokenAddress,
bool isActive,
uint16 targetWeight,
uint16 lowerBoundWeight,
uint16 upperBoundWeight,
uint8 decimals,
uint24 oracleConfRel,
bytes32 oraclePriceId
) external;
function changeLob(
address lobAddress,
bool isActive,
uint8 tokenIdX,
uint8 tokenIdY,
uint16 maxOrderDistanceBps
) external;
function slashMakersShares(uint256 amount) external;
function disableSlashingStatus() external;
function pause() external;
function validateLPPriceAndDistributeFees(bytes[] calldata priceUpdateData) payable external;
// client entries
function addLiquidity(
uint8 tokenID,
uint256 amount,
uint256 minUsdValue,
uint256 minLPMinted,
uint256 expires,
bytes[] calldata priceUpdateData
) external payable returns (uint256);
function removeLiquidity(
uint8 tokenID,
uint256 burnLP,
uint256 minUsdValue,
uint256 minTokenGet,
uint256 expires,
bytes[] calldata priceUpdateData
) external payable returns (uint256);
function collectFees() external;
// views
function getFeeBasisPoints(
uint256 totalValue,
uint256 initialTokenValue,
uint256 nextTokenValue,
uint16 targetTokenWeight
) external view returns (uint256);
function tokens(uint256 index) external view returns (
address tokenAddress,
bool isActive,
uint16 targetWeight,
uint16 lowerBoundWeight,
uint16 upperBoundWeight,
uint8 decimals,
uint24 oracleConfRel,
bytes32 oraclePriceId
);
function lastAddedAt(address account) external view returns (uint256);
function totalWeight() external view returns (uint24);
function checkCooldown(address account) external view;
function getTokensCount() external view returns (uint256);
function marketMakers(address account) external view returns (bool);
function primaryMarketMaker() external view returns (address);
function validateMarketMakerLPShare() external view;
function ensureNotPartiallyPaused() external view;
function lobs(uint256 index) external view returns (
address lobAddress,
uint8 tokenIdX,
uint8 tokenIdY,
bool isActive,
uint16 maxOrderDistanceBps
);
}
// SPDX-License-Identifier: BUSL-1.1
// (c) Long Gamma Labs, 2024.
pragma solidity ^0.8.28;
interface IProxyLOB {
function lobReservesByTokenId(uint8 tokenId) external view returns (uint256);
function getPriceOf(uint8 tokenId) external view returns (uint256, int32);
function placeOrder(
uint8 lobId,
bool isAsk,
uint128 quantity,
uint72 price,
uint128 maxCommission,
bool marketOnly,
bool postOnly,
uint256 expires,
bytes[] calldata priceUpdateData
) external payable returns (uint64 orderId);
function claimOrder(uint8 lobId, uint64 orderId, bool onlyClaim, uint256 expires) external;
}
// SPDX-License-Identifier: BUSL-1.1
// (c) Long Gamma Labs, 2024.
pragma solidity ^0.8.28;
library TokenValueCalculator {
/// @notice Calculates normalized USD value of tokens with 18 decimals precision.
/// @param tokenShares The amount of token shares.
/// @param tokenDecimals The number of decimals the token uses.
/// @param price The price of one token in USD, scaled by 10^priceExpo.
/// @param priceExpo The exponent that the price is scaled by.
/// @return The normalized USD value of the token shares, normalized to 18 decimal places.
function calcNormalizedValue(
uint256 tokenShares,
uint8 tokenDecimals,
uint256 price,
int32 priceExpo
) internal pure returns (uint256) {
int256 decimalAdjustment;
unchecked {
decimalAdjustment = int256(uint256(tokenDecimals)) - priceExpo - 18;
}
uint256 normalizedValue = tokenShares * price;
if (decimalAdjustment > 0) {
normalizedValue /= 10 ** uint256(decimalAdjustment);
} else {
normalizedValue *= 10 ** uint256(-decimalAdjustment);
}
return normalizedValue;
}
/// @notice Calculates the amount of token shares corresponding to a given USD value, taking into account
/// token decimals and price normalization. This is the inverse operation of calcNormalizedValue.
/// @param usdValue The value in USD for which to calculate the corresponding token shares.
/// @param tokenDecimals The number of decimals of the token for which shares are being calculated.
/// @param price The price of the token in USD, adjusted by the price exponent.
/// @param priceExpo The exponent to which the price is normalized (e.g., -18 for wei normalization).
/// @return tokenAmount The calculated amount of token shares.
function calcTokenShares(
uint256 usdValue,
uint8 tokenDecimals,
uint256 price,
int32 priceExpo
) internal pure returns (uint256) {
int256 decimalAdjustment;
unchecked {
decimalAdjustment = int256(uint256(tokenDecimals)) - priceExpo - 18;
}
uint256 tokenAmount;
if (decimalAdjustment > 0) {
// There is no practical benefit to using mulDiv with real data here.
tokenAmount = usdValue * 10 ** uint256(decimalAdjustment) / price;
} else {
tokenAmount = usdValue / (price * 10 ** uint256(-decimalAdjustment));
}
return tokenAmount;
}
}