Skip to content

Conversation

@fabianschu
Copy link
Contributor

@fabianschu fabianschu commented May 26, 2025

DiscreteCurveMathLib_v1

Purpose of Contract

The DiscreteCurveMathLib_v1 is a Solidity library designed to provide mathematical operations for discrete bonding curves. Its primary purpose is to perform calculations related to price, collateral reserve, purchase returns (issuance out for collateral in), and sale returns (collateral out for issuance in) in a gas-efficient manner. This efficiency is achieved mainly through the use of packed storage for curve segment data. It is intended to be used by other smart contracts, such as Funding Managers, that implement bonding curve logic.

Glossary

To understand the functionalities of this library and its context, it is important to be familiar with the following definitions.

Definition Explanation
PP Payment Processor module, typically handles queued payment operations.
FM Funding Manager type module, which would utilize this library for its bonding curve calculations.
Issuance Token Tokens that are distributed (minted/burned) by a Funding Manager contract, often based on the calculations provided by this library.
Discrete Bonding Curve A bonding curve where the price of the issuance token changes at discrete intervals (steps) rather than continuously.
Segment A distinct portion of the discrete bonding curve, defined by its own set of parameters: an initial price, a price increase per step, a supply amount per step, and a total number of steps.
Step The smallest unit within a segment where a specific quantity of issuance tokens (supplyPerStep) can be bought or sold at a fixed price.
PackedSegment A custom Solidity type (type PackedSegment is bytes32;) used by this library to store all four parameters of a curve segment into a single bytes32 value. This optimizes storage gas costs.
PackedSegmentLib A helper library (located in ../libraries/PackedSegmentLib.sol), imported by DiscreteCurveMathLib_v1, responsible for the creation, validation, packing, and unpacking of PackedSegment data.
Scaling Factor (1e18) A constant (10^18) used for fixed-point arithmetic to handle decimal precision for prices and token amounts, assuming standard 18-decimal tokens.
MAX_SEGMENTS A constant (10) defining the maximum number of segments a curve configuration can have, enforced by functions like _validateSegmentArray and _calculateReserveForSupply.

Implementation Design Decision

The purpose of this section is to inform the user about important design decisions made during the development process. The focus should be on why a certain decision was made and how it has been implemented.

Type-Safe Packed Storage for Segments

The core design decision for DiscreteCurveMathLib_v1 is the use of type-safe packed storage for bonding curve segment data. Each segment's configuration (initial price, price increase per step, supply per step, and number of steps) is packed into a single bytes32 slot using the custom type PackedSegment and the internal helper library PackedSegmentLib.

  • Why: Storing an array of segments for a bonding curve can be gas-intensive if each segment's parameters occupy separate storage slots. By packing all parameters into one bytes32 value, each segment effectively consumes only one storage slot when stored by a calling contract (e.g., in an array PackedSegment[] storage segments;). This significantly reduces gas costs for deployment and state modification of contracts that manage multiple curve segments.
  • How: PackedSegmentLib defines the bit allocation for each parameter within the bytes32 value, along with masks and offsets. It provides:
    • A create function that validates input parameters against their bit limits and packs them.
    • Accessor functions (initialPrice, priceIncrease, etc.) to retrieve individual parameters.
    • An unpack function to retrieve all parameters at once.
      The PackedSegment type itself (being bytes32) ensures type safety, preventing accidental mixing with other bytes32 values that do not represent curve segments.

Segment Validation Rules

To ensure economic sensibility and robustness, DiscreteCurveMathLib_v1 and its helper PackedSegmentLib enforce specific validation rules for segment configurations:

  1. No Free Segments (PackedSegmentLib._create):
    Segments that are entirely "free" – meaning their initialPrice is 0 AND their priceIncreasePerStep is also 0 – are disallowed. Attempting to create such a segment will cause PackedSegmentLib._create() to revert with the error IDiscreteCurveMathLib_v1.DiscreteCurveMathLib__SegmentIsFree(). This prevents scenarios where tokens could be minted indefinitely at no cost from a segment that never increases in price.

  2. Non-Decreasing Price Progression (DiscreteCurveMathLib_v1._validateSegmentArray):
    When an array of segments is validated using DiscreteCurveMathLib_v1._validateSegmentArray(), the library checks for logical price progression between consecutive segments. Specifically, the initialPrice of any segment N+1 must be greater than or equal to the calculated final price of the preceding segment N. The final price of segment N is determined as segments[N]._initialPrice() + (segments[N]._numberOfSteps() - 1) * segments[N]._priceIncrease().
    If this condition is violated (i.e., if a subsequent segment starts at a lower price than where the previous one ended), _validateSegmentArray() will revert with the error IDiscreteCurveMathLib_v1.DiscreteCurveMathLib__InvalidPriceProgression(uint256 segmentIndex, uint256 previousSegmentFinalPrice, uint256 nextSegmentInitialPrice).
    This rule ensures a generally non-decreasing (or strictly increasing, if price increases are positive) price curve across the entire set of segments.

  3. Specific Segment Structure ("True Flat" / "True Sloped") (PackedSegmentLib._create):
    PackedSegmentLib._create() enforces specific structural rules for segments:

    • A "True Flat" segment must have numberOfSteps == 1 and priceIncreasePerStep == 0. Attempting to create a multi-step flat segment (e.g., numberOfSteps > 1 and priceIncreasePerStep == 0) will revert with IDiscreteCurveMathLib_v1.DiscreteCurveMathLib__InvalidFlatSegment().
    • A "True Sloped" segment must have numberOfSteps > 1 and priceIncreasePerStep > 0. Attempting to create a single-step sloped segment (e.g., numberOfSteps == 1 and priceIncreasePerStep > 0) will revert with IDiscreteCurveMathLib_v1.DiscreteCurveMathLib__InvalidPointSegment().
      These rules ensure that segments are clearly defined as either single-step fixed-price points or multi-step incrementally priced slopes. The priceIncreasePerStep being uint256 inherently prevents price decreases within a single segment.

Other Important Validation Rules & Errors:

  • No Segments Configured (_validateSegmentArray, _calculateReserveForSupply, _calculatePurchaseReturn via internal checks): If an operation requiring segments is attempted but no segments are defined (e.g., segments_ array is empty), the library may revert with IDiscreteCurveMathLib_v1.DiscreteCurveMathLib__NoSegmentsConfigured().
  • Too Many Segments (_validateSegmentArray, _calculateReserveForSupply): If the provided segments_ array exceeds MAX_SEGMENTS (currently 10), relevant functions will revert with IDiscreteCurveMathLib_v1.DiscreteCurveMathLib__TooManySegments().
  • Supply Exceeds Curve Capacity (_validateSupplyAgainstSegments, _findPositionForSupply): If a target supply or current supply exceeds the total possible supply defined by all segments, functions will revert with IDiscreteCurveMathLib_v1.DiscreteCurveMathLib__SupplyExceedsCurveCapacity(uint256 currentSupplyOrTarget, uint256 totalCapacity).
  • Zero Collateral Input (_calculatePurchaseReturn): Attempting a purchase with zero collateral reverts with IDiscreteCurveMathLib_v1.DiscreteCurveMathLib__ZeroCollateralInput().
  • Zero Issuance Input (_calculateSaleReturn): Attempting a sale with zero issuance tokens reverts with IDiscreteCurveMathLib_v1.DiscreteCurveMathLib__ZeroIssuanceInput().
  • Insufficient Issuance to Sell (_calculateSaleReturn): Attempting to sell more tokens than the currentTotalIssuanceSupply reverts with IDiscreteCurveMathLib_v1.DiscreteCurveMathLib__InsufficientIssuanceToSell(uint256 tokensToSell, uint256 currentSupply).

Note: All custom errors mentioned (e.g., DiscreteCurveMathLib__SegmentIsFree, DiscreteCurveMathLib__InvalidPriceProgression, DiscreteCurveMathLib__InvalidFlatSegment, DiscreteCurveMathLib__InvalidPointSegment, DiscreteCurveMathLib__NoSegmentsConfigured, DiscreteCurveMathLib__TooManySegments, DiscreteCurveMathLib__SupplyExceedsCurveCapacity, DiscreteCurveMathLib__ZeroCollateralInput, DiscreteCurveMathLib__ZeroIssuanceInput, DiscreteCurveMathLib__InsufficientIssuanceToSell) must be defined in the IDiscreteCurveMathLib_v1.sol interface file for the contracts to compile and function correctly.

Efficient Calculation Methods

To further optimize gas for on-chain computations:

  • Arithmetic Series for Reserve/Cost Calculation: For sloped segments (where priceIncreasePerStep > 0), functions like _calculateReserveForSupply (and its internal helper _calculateSegmentReserve) use the mathematical formula for the sum of an arithmetic series. This allows calculating the total collateral for multiple steps without iterating through each step individually, saving gas.
  • Direct Iteration for Purchase Calculation: The _calculatePurchaseReturn function uses a direct iterative approach to determine the number of tokens to be minted for a given collateral input. It iterates through the curve segments and steps, calculating the cost for each, until the provided collateral is exhausted or the curve capacity is reached.
  • Optimized Sale Calculation: The _calculateSaleReturn function determines the collateral out by calculating the total reserve locked in the curve before and after the sale, then taking the difference. This approach (R(S_current) - R(S_final)) is generally more efficient than iterating backward through curve steps. It leverages the internal helper _calculateReservesForTwoSupplies to efficiently get both reserve values in a single pass.

Internal Helper Functions for Calculation:
The library utilizes several internal helper functions to achieve its calculations efficiently and maintain modularity:

  • _validateSupplyAgainstSegments: Ensures a given supply is consistent with the curve's capacity.
  • _calculateSegmentReserve: Calculates the reserve for a portion of a single segment, handling flat and sloped logic.
  • _calculateReservesForTwoSupplies: An optimized helper for _calculateSaleReturn that calculates reserves for two different supply points in one pass.
    While these are internal, understanding their role can be helpful for a deeper analysis of the library's mechanics.

Limitations of Packed Storage and Low-Priced Collateral

While PackedSegment offers significant gas savings, its fixed bit allocation for price and supply parameters introduces limitations, particularly when dealing with collateral tokens that have a very low price per unit but maintain a high decimal precision (e.g., 18 decimals).

The Core Issue:
The initialPrice and priceIncrease fields currently use 72 bits each.

  • Maximum value: 2^72 - 1 (approx. 4.722 x 10^21) wei.
  • For a standard 18-decimal token, this translates to a maximum representable price of approx. 4,722,366.48 tokens.

Impact of Low-Priced Collateral:
If a collateral token is worth, for example, $0.000001 (one micro-dollar) and has 18 decimals:

  • The maximum dollar value that can be represented for initialPrice or priceIncrease is 4,722,366.48 tokens * $0.000001/token = ~$4.72.
    This means a bonding curve segment could not have an initial step price or a price increment (if denominated in such a collateral token) that represents more than ~$4.72 worth of that token.

Example: Extremely Low-Priced Token
Consider a hypothetical 18-decimal token worth $0.0000000001 (one-tenth of a nano-dollar).

  • To represent $1.00 worth of this token, one would need 1 / $0.0000000001 = 10,000,000,000 tokens.
  • In wei (18 decimals): 10,000,000,000 * 1e18 = 1e28 wei.
    This value (1e28 wei) significantly exceeds the 2^72 - 1 (approx. 4.7e21) wei capacity of the 72-bit price fields, leading to an overflow if one tried to set a price step equivalent to $1.00 of this token.

Potential Solutions and Workarounds:

  1. Collateral Token Choice & Decimal Precision:

    • Using collateral tokens with fewer decimals (e.g., 6 or 8, like many stablecoins) significantly increases the nominal range.
    • Protocols can restrict collateral to tokens that fit reasonably within the existing bit allocation.
  2. Price Scaling Factor:

    • The bonding curve logic (in the consuming contract) could implement an additional scaling factor for prices. For example, a PRICE_SCALING_FACTOR of 1e12 could be used. A packed price of 1 would then represent an actual price of 1 * 1e12. This allows storing scaled-down values in PackedSegment while representing larger actual prices.
  3. Alternative Bit Allocation in PackedSegment:

    • A future version of the library or a different packing scheme could allocate more bits to price fields (e.g., 96 bits) at the expense of bits for supply or by using more than one bytes32 slot per segment if necessary. For instance, allocating 96 bits for price would allow values up to 2^96 - 1 (approx. 7.9e28 wei), accommodating even extremely low-priced 18-decimal tokens.
  4. Protocol-Level Policies:

    • Collateral Whitelisting: Enforce requirements on collateral tokens, such as minimum price or maximum effective decimals, to ensure compatibility.
    • Dynamic Configuration: Allow curve deployers to specify bit allocations or scaling factors per curve instance, though this adds complexity.

Assessment for Current DiscreteCurveMathLib_v1:
The current 72-bit allocation for prices is a deliberate trade-off favoring gas efficiency and is generally sufficient for many common use cases, especially with typical collateral like ETH, wBTC, or stablecoins (USDC, USDT, DAI) which have prices or decimal counts that fit well. For protocols like House Protocol, which are likely to use established stablecoins, the existing 72-bit precision for prices should provide ample headroom.

The library is well-suited for its primary intended applications. If support for extremely micro-cap tokens with high decimal precision becomes a strict requirement, deploying a new version of the library with adjusted bit allocations or incorporating an explicit price scaling mechanism in the consuming contract would be the recommended approaches.

Internal Functions and Composability

Most functions in the library are internal pure, designed to be called by other smart contracts (typically Funding Managers). This makes the library a set of reusable mathematical tools rather than a standalone stateful contract. The using PackedSegmentLib for PackedSegment; directive enables convenient syntax for accessing segment data (e.g., mySegment._initialPrice()).

Inheritance

UML Class Diagram

This diagram illustrates the relationships between the library, its internal helper library, and associated types/interfaces.

classDiagram
    direction LR
    note "DiscreteCurveMathLib_v1 is a Solidity library providing pure functions for bonding curve calculations."

    class DiscreteCurveMathLib_v1 {
        <<library>>
        +SCALING_FACTOR : uint256
        +MAX_SEGMENTS : uint256
        ---
        #_findPositionForSupply(PackedSegment[] memory, uint256) internal pure returns (uint segmentIndex, uint stepIndexWithinSegment, uint priceAtCurrentStep)
        #_calculateReserveForSupply(PackedSegment[] memory, uint256) internal pure returns (uint256 totalReserve_)
        #_calculatePurchaseReturn(PackedSegment[] memory, uint256, uint256) internal pure returns (uint256 tokensToMint_, uint256 collateralSpentByPurchaser_)
        #_calculateSaleReturn(PackedSegment[] memory, uint256, uint256) internal pure returns (uint256 collateralToReturn_, uint256 tokensToBurn_)
        #_createSegment(uint256, uint256, uint256, uint256) internal pure returns (PackedSegment)
        #_validateSegmentArray(PackedSegment[] memory) internal pure
        #_validateSupplyAgainstSegments(PackedSegment[] memory, uint256) internal pure returns (uint totalCurveCapacity_)
        #_calculateReservesForTwoSupplies(PackedSegment[] memory, uint256, uint256) internal pure returns (uint lowerReserve_, uint higherReserve_)
        #_calculateSegmentReserve(uint256, uint256, uint256, uint256) internal pure returns (uint collateral_)
    }

    class PackedSegmentLib {
        <<library>>
        -INITIAL_PRICE_BITS : uint256
        -PRICE_INCREASE_BITS : uint256
        -SUPPLY_BITS : uint256
        -STEPS_BITS : uint256
        ---
        #_create(uint256, uint256, uint256, uint256) internal pure returns (PackedSegment)
        #_initialPrice(PackedSegment) internal pure returns (uint256)
        #_priceIncrease(PackedSegment) internal pure returns (uint256)
        #_supplyPerStep(PackedSegment) internal pure returns (uint256)
        #_numberOfSteps(PackedSegment) internal pure returns (uint256)
        #_unpack(PackedSegment) internal pure returns (uint256, uint256, uint256, uint256)
    }

    class PackedSegment {
        <<type is bytes32>>
        note "User-defined value type wrapping bytes32"
    }

    class IDiscreteCurveMathLib_v1 {
        <<interface>>
        +Errors...
        +Events...
        // Note: CurvePosition struct might be defined here if used by _findPositionForSupply's NatSpec,
        // but the function itself returns a tuple.
        // SegmentConfig struct is not directly used by _createSegment's signature.
    }

    note for DiscreteCurveMathLib_v1 "Uses PackedSegmentLib for segment data manipulation"
    DiscreteCurveMathLib_v1 ..> PackedSegmentLib : uses
    note for DiscreteCurveMathLib_v1 "Operates on PackedSegment data"
    DiscreteCurveMathLib_v1 ..> PackedSegment : uses
    note for DiscreteCurveMathLib_v1 "References error definitions from the interface"
    DiscreteCurveMathLib_v1 ..> IDiscreteCurveMathLib_v1 : uses (errors)

    note for PackedSegmentLib "Creates and unpacks PackedSegment types"
    PackedSegmentLib ..> PackedSegment : manipulates
    note for PackedSegmentLib "References error definitions from the interface for validation"
    PackedSegmentLib ..> IDiscreteCurveMathLib_v1 : uses (errors)

Loading

Base Contracts

DiscreteCurveMathLib_v1 is a Solidity library and does not inherit from any base contracts. It is a standalone collection of functions.

Key Changes to Base Contract

Not applicable, as this is a library, not an upgrade or modification of a base contract.

User Interactions

This library itself does not have direct user interactions with state changes. It provides pure functions to be used by other contracts (e.g., a Funding Manager). Below are examples of how a calling contract might use this library.

Example: Calculating Purchase Return

A Funding Manager (FM) contract would use _calculatePurchaseReturn to determine how many issuance tokens a user receives for a given amount of collateral.

Preconditions (for the FM, not the library call itself):

  • The FM has access to the array of PackedSegment data defining the curve.
  • The FM knows the currentTotalIssuanceSupply of its token.
  • The user (caller of the FM) has sufficient collateral and has approved it to the FM.
  1. FM calls _calculatePurchaseReturn from the library:
    The FM passes its segment data, the user's collateralAmountIn, and the currentTotalIssuanceSupply to the library function.

    // In a Funding Manager contract
    // import {DiscreteCurveMathLib_v1, PackedSegment} from ".../DiscreteCurveMathLib_v1.sol";
    //
    // PackedSegment[] internal _segments;
    // IERC20 public _issuanceToken; // Assume it has a totalSupply()
    // IERC20 public _collateralToken;
    
    function getPurchaseReturn(uint256 collateralAmountIn)
        public
        view
        returns (uint256 issuanceAmountOut, uint256 collateralAmountSpent)
    {
        // This is a simplified example. A real FM would get _segments from storage.
        // PackedSegment[] memory currentSegments = _segments; // If _segments is storage array
        // For this example, assume segments are passed or constructed.
        PackedSegment[] memory segments = new PackedSegment[](1); // Example segments
        segments[0] = DiscreteCurveMathLib_v1._createSegment(1e18, 0.1e18, 10e18, 100);
    
    
        // uint256 currentTotalIssuanceSupply = _issuanceToken.totalSupply(); // Get current supply
    
        // For example purposes, let's assume a current supply
        uint256 currentTotalIssuanceSupply = 50e18;
    
    
        (issuanceAmountOut, collateralAmountSpent) =
            DiscreteCurveMathLib_v1._calculatePurchaseReturn(
                segments,
                collateralAmountIn,
                currentTotalIssuanceSupply
            );
    }
  2. FM uses the result:
    The FM would then use issuanceAmountOut and collateralAmountSpent to handle the token transfers (take collateral, mint issuance tokens).

Sequence Diagram (Conceptual for FM using the Library)

sequenceDiagram
    participant User
    participant FM as Funding Manager
    participant Lib as DiscreteCurveMathLib_v1
    participant CT as Collateral Token
    participant IT as Issuance Token

    User->>FM: buyTokens(collateralAmountIn, minIssuanceOut)
    FM->>Lib: _calculatePurchaseReturn(segments, collateralAmountIn, currentSupply)
    Lib-->>FM: issuanceAmountOut, collateralSpent
    FM->>FM: Check issuanceAmountOut >= minIssuanceOut
    FM->>CT: transferFrom(User, FM, collateralSpent)
    FM->>IT: mint(User, issuanceAmountOut)
    FM-->>User: Success/Tokens
Loading

Deployment

Preconditions

DiscreteCurveMathLib_v1 is a library. It is not deployed as a standalone contract instance that holds state or requires ownership. Libraries are typically linked by other contracts during their compilation/deployment.

A contract intending to use this library would need:

  • An array of PackedSegment data, correctly configured and validated, representing its desired bonding curve. This data would typically be initialized in the constructor or a setup function of the consuming contract.

Deployment Parameters

Not applicable. Libraries do not have constructors or init() functions in the same way contracts do.

Deployment

The library's code is included in contracts that use it. When a contract using DiscreteCurveMathLib_v1 is compiled and deployed:

  • If all library functions are internal, the library code is embedded directly into the consuming contract's bytecode.
  • If the library had public or external functions (which DiscreteCurveMathLib_v1 does not, for its core logic), it might need to be deployed separately and linked, but this is not the case here for its primary usage pattern.

Deployment of contracts using this library would follow standard Inverter Network procedures:

Setup Steps

Not applicable for the library itself. A contract using this library (e.g., a Funding Manager) would require setup steps to define its curve segments. This typically involves:

  1. Preparing Segment Data: For each segment, the individual parameters (initialPrice_, priceIncrease_, supplyPerStep_, numberOfSteps_) need to be determined. While an off-chain script or helper might use a struct similar to SegmentConfig for convenience, the library's _createSegment function takes these as individual arguments.
  2. Creating PackedSegments: Iterating through the prepared segment data and calling DiscreteCurveMathLib_v1._createSegment() for each set of parameters to get the PackedSegment bytes32 value. PackedSegmentLib (used by _createSegment) will validate individual parameters.
  3. Storing PackedSegments: Storing the resulting PackedSegment[] array in the consuming contract's state.
  4. Validating Segment Array: Validating the entire PackedSegment[] array using DiscreteCurveMathLib_v1._validateSegmentArray() to check for array-level properties like MAX_SEGMENTS and correct inter-segment price progression.

The NatSpec comments within DiscreteCurveMathLib_v1.sol and IDiscreteCurveMathLib_v1.sol provide details on function parameters and errors, which would be relevant for developers integrating this library.

FM_BC_Discrete_Redeeming_VirtualSupply_v1

Purpose of Contract

This contract serves as a Funding Manager (FM) module for the Inverter Protocol. It implements a discrete bonding curve with redeeming capabilities, allowing users to buy (mint) and sell (redeem) an issuance token against a collateral token. The curve's price points are defined by an array of packed segments. A key feature is its management of a virtual collateral supply, which is used in conjunction with the actual issuance token supply for price calculations and curve operations. It also incorporates a fee mechanism, including fixed project fees and cached protocol fees.

Glossary

To understand the functionalities of the following contract, it is important to be familiar with the following definitions.

Definition Explanation
FM Funding Manager type module, responsible for managing the issuance and redemption of tokens, and holding collateral.
Issuance Token The ERC20 token minted by this FM when users deposit collateral (e.g., $HOUSE token).
Collateral Token The ERC20 token users deposit to buy issuance tokens, or receive when selling issuance tokens (e.g., a stablecoin).
PackedSegment A struct that efficiently stores the parameters of a single segment of the discrete bonding curve (initial price, price increase per step, supply per step, number of steps).
Discrete Bonding Curve (DBC) A bonding curve where the price changes in discrete steps rather than continuously.
Virtual Collateral Supply A state variable representing the total collateral that should be backing the issuance tokens according to the curve's state, used for calculations. It's updated during buy/sell operations.
Protocol Fee Fees defined by the broader protocol/orchestrator, managed by a FeeManager contract. These are cached by this FM during initialization.
Project Fee Fees specific to this FM instance, currently hardcoded as constants for buy and sell operations.
Orchestrator The central contract that manages and coordinates different modules within a workflow.

Implementation Design Decision

The purpose of this section is to inform the user about important design decisions made during the development process. The focus should be on why a certain decision was made and how it has been implemented.

  • Discrete Bonding Curve Model: The contract employs a discrete bonding curve, where prices are defined by a series of segments, and within each segment, by discrete steps. This provides predictable price points and allows for flexible curve shapes.
  • PackedSegment for Efficiency: Curve segments are defined using PackedSegment structs, which utilize bit-packing (via DiscreteCurveMathLib_v1 which internally uses PackedSegmentLib.sol principles) to store segment parameters gas-efficiently on-chain.
  • DiscreteCurveMathLib_v1 for Calculations: All core mathematical operations for the bonding curve (calculating purchase/sale returns, finding positions on the curve, validating segments) are delegated to the DiscreteCurveMathLib_v1 library. This separation enhances modularity, testability, and auditability.
  • Virtual Collateral Supply: The contract maintains a virtualCollateralSupply. This value is updated with the net collateral added or removed during buy/sell operations. It, along with the issuanceToken.totalSupply(), serves as a crucial input for the DiscreteCurveMathLib_v1 to determine current price points and calculate transaction outcomes. This decouples the mathematical model slightly from the instantaneous physical balance for certain calculations, especially relevant for getStaticPriceForBuying.
  • Protocol Fee Caching: To optimize gas usage and reduce external calls, protocol fees (both collateral and issuance side, for buy and sell operations) and their respective treasury addresses are fetched from the FeeManager contract once during initialization (__FM_BC_Discrete_Redeeming_VirtualSupply_v1_Init) and stored in the _protocolFeeCache struct. The overridden _getFunctionFeesAndTreasuryAddresses function then serves these cached values for internal calls during calculatePurchaseReturn, calculateSaleReturn, _buyOrder, and _sellOrder.
  • Fixed Project Fees: Project-specific fees for buy and sell operations are currently implemented as hardcoded constants (PROJECT_BUY_FEE_BPS, PROJECT_SELL_FEE_BPS) and are set during initialization. These are retrieved via the overridden _getBuyFee() and _getSellFee() internal functions.
  • Modular Inheritance: The contract inherits from several base contracts (VirtualCollateralSupplyBase_v1, RedeemingBondingCurveBase_v1, Module_v1) to reuse common functionalities and adhere to the Inverter module framework.

Inheritance

UML Class Diagramm

classDiagram
    direction RL
    class ERC165Upgradeable {
        <<abstract>>
    }
    class Module_v1 {
        <<abstract>>
        +init(IOrchestrator_v1, Metadata, bytes)
        #_getFunctionFeesAndTreasuryAddresses()
    }
    class BondingCurveBase_v1 {
        <<abstract>>
        +buyFee
        +issuanceToken
        +buyFor(address,uint,uint)
        +calculatePurchaseReturn(uint)
        +getStaticPriceForBuying() uint
        #_issueTokensFormulaWrapper(uint) uint
        #_handleCollateralTokensBeforeBuy(address,uint)
        #_handleIssuanceTokensAfterBuy(address,uint)
        #_getBuyFee() uint
    }
    class RedeemingBondingCurveBase_v1 {
        <<abstract>>
        +sellFee
        +sellTo(address,uint,uint)
        +calculateSaleReturn(uint)
        +getStaticPriceForSelling() uint
        #_redeemTokensFormulaWrapper(uint) uint
        #_handleCollateralTokensAfterSell(address,uint)
        #_getSellFee() uint
    }
    class VirtualCollateralSupplyBase_v1 {
        <<abstract>>
        +virtualCollateralSupply
        +getVirtualCollateralSupply() uint
        +setVirtualCollateralSupply(uint)
        #_setVirtualCollateralSupply(uint)
        #_addVirtualCollateralAmount(uint)
        #_subVirtualCollateralAmount(uint)
    }
    class IFundingManager_v1 {
        <<interface>>
        +token() IERC20
        +transferOrchestratorToken(address,uint)
    }
    class IFM_BC_Discrete_Redeeming_VirtualSupply_v1 {
        <<interface>>
        +getSegments() PackedSegment[]
        +reconfigureSegments(PackedSegment[])
        +ProtocolFeeCache
    }
    class FM_BC_Discrete_Redeeming_VirtualSupply_v1 {
        -_token: IERC20
        -_segments: PackedSegment[]
        -_protocolFeeCache: ProtocolFeeCache
        +PROJECT_BUY_FEE_BPS
        +PROJECT_SELL_FEE_BPS
        +init(IOrchestrator_v1, Metadata, bytes)
        +supportsInterface(bytes4) bool
        +token() IERC20
        +getIssuanceToken() address
        +getSegments() PackedSegment[]
        +getStaticPriceForBuying() uint
        +getStaticPriceForSelling() uint
        +buyFor(address,uint,uint)
        +sellTo(address,uint,uint)
        +transferOrchestratorToken(address,uint)
        +setVirtualCollateralSupply(uint)
        +reconfigureSegments(PackedSegment[])
        +calculatePurchaseReturn(uint) uint
        +calculateSaleReturn(uint) uint
        #_getFunctionFeesAndTreasuryAddresses()
        #_getBuyFee() uint
        #_getSellFee() uint
        #_setIssuanceToken(ERC20Issuance_v1)
        #_setSegments(PackedSegment[])
        #_setVirtualCollateralSupply(uint)
        #_redeemTokensFormulaWrapper(uint) uint
        #_handleCollateralTokensAfterSell(address,uint)
        #_handleCollateralTokensBeforeBuy(address,uint)
        #_handleIssuanceTokensAfterBuy(address,uint)
        #_issueTokensFormulaWrapper(uint) uint
    }

    Module_v1 <|-- BondingCurveBase_v1
    BondingCurveBase_v1 <|-- RedeemingBondingCurveBase_v1
    ERC165Upgradeable <|-- VirtualCollateralSupplyBase_v1
    Module_v1 <|-- VirtualCollateralSupplyBase_v1

    IFM_BC_Discrete_Redeeming_VirtualSupply_v1 <|.. FM_BC_Discrete_Redeeming_VirtualSupply_v1
    IFundingManager_v1 <|.. FM_BC_Discrete_Redeeming_VirtualSupply_v1
    VirtualCollateralSupplyBase_v1 <|-- FM_BC_Discrete_Redeeming_VirtualSupply_v1
    RedeemingBondingCurveBase_v1 <|-- FM_BC_Discrete_Redeeming_VirtualSupply_v1

    note for FM_BC_Discrete_Redeeming_VirtualSupply_v1 "Manages a discrete bonding curve with redeeming and virtual collateral supply."
Loading

Base Contracts

The contract FM_BC_Discrete_Redeeming_VirtualSupply_v1 inherits from:

  • IFM_BC_Discrete_Redeeming_VirtualSupply_v1: Interface specific to this contract.
  • IFundingManager_v1: Standard interface for Funding Manager modules. ([Link to IFundingManager_v1 docs - Placeholder])
  • VirtualCollateralSupplyBase_v1: Abstract contract providing logic for managing a virtual collateral supply. ([Link to VirtualCollateralSupplyBase_v1 docs - Placeholder])
  • RedeemingBondingCurveBase_v1: Abstract contract providing base functionalities for a bonding curve that supports redeeming. This itself inherits from BondingCurveBase_v1 and Module_v1. ([Link to RedeemingBondingCurveBase_v1 docs - Placeholder])

Functions that have been overridden to adapt functionalities are outlined below.

Key Changes to Base Contract

The purpose of this section is to highlight which functions of the base contract have been overridden and why.

  • supportsInterface(bytes4 interfaceId): Overridden from RedeemingBondingCurveBase_v1 and VirtualCollateralSupplyBase_v1 to include type(IFM_BC_Discrete_Redeeming_VirtualSupply_v1).interfaceId and type(IFundingManager_v1).interfaceId in the check, in addition to calling super.supportsInterface(interfaceId).
  • init(IOrchestrator_v1 orchestrator_, Metadata memory metadata_, bytes memory configData_): Overridden from Module_v1 to decode configData_ (issuance token address, collateral token address, initial segments) and call __FM_BC_Discrete_Redeeming_VirtualSupply_v1_Init for specific initialization.
  • __FM_BC_Discrete_Redeeming_VirtualSupply_v1_Init(...): Internal initializer that sets up issuance token, collateral token, initial segments, project fees, and caches protocol fees.
  • token(): Implements IFundingManager_v1 to return the collateral token (_token).
  • getIssuanceToken(): Overridden from BondingCurveBase_v1 and IBondingCurveBase_v1 to return the address of the issuanceToken.
  • getStaticPriceForBuying(): Overridden from BondingCurveBase_v1, IBondingCurveBase_v1, and IFM_BC_Discrete_Redeeming_VirtualSupply_v1. Implemented to find the price on the curve for virtualCollateralSupply + 1 using _segments._findPositionForSupply.
  • getStaticPriceForSelling(): Overridden from RedeemingBondingCurveBase_v1 and IFM_BC_Discrete_Redeeming_VirtualSupply_v1. Implemented to find the price on the curve for issuanceToken.totalSupply() using _segments._findPositionForSupply.
  • buyFor(address _receiver, uint _depositAmount, uint _minAmountOut): Overridden from BondingCurveBase_v1 and IBondingCurveBase_v1. Calls _buyOrder and then updates the virtualCollateralSupply by adding the net collateral received.
  • sellTo(address _receiver, uint _depositAmount, uint _minAmountOut): Overridden from RedeemingBondingCurveBase_v1. Calls _sellOrder and then updates the virtualCollateralSupply by subtracting the total collateral paid out.
  • setVirtualCollateralSupply(uint virtualSupply_): Overridden from VirtualCollateralSupplyBase_v1 and IFM_BC_Discrete_Redeeming_VirtualSupply_v1 to be onlyOrchestratorAdmin and calls _setVirtualCollateralSupply.
  • _setVirtualCollateralSupply(uint virtualSupply_): Overridden from VirtualCollateralSupplyBase_v1 to call super._setVirtualCollateralSupply.
  • calculatePurchaseReturn(uint _depositAmount): Overridden from BondingCurveBase_v1 and IBondingCurveBase_v1. Calculates the net mint amount after deducting cached protocol fees (collateral and issuance side) and project buy fees from collateral. Uses _issueTokensFormulaWrapper for the gross calculation.
  • calculateSaleReturn(uint _depositAmount): Overridden from RedeemingBondingCurveBase_v1. Calculates the net redeem amount after deducting cached protocol fees (issuance and collateral side) and project sell fees from collateral. Uses _redeemTokensFormulaWrapper for the gross calculation.
  • _getFunctionFeesAndTreasuryAddresses(bytes4 functionSelector_): Overridden from Module_v1. Returns cached protocol fees and treasury addresses from _protocolFeeCache for buy/sell related function selectors, otherwise defers to super.
  • _getBuyFee(): Overridden from BondingCurveBase_v1 to return the constant PROJECT_BUY_FEE_BPS.
  • _getSellFee(): Overridden from RedeemingBondingCurveBase_v1 to return the constant PROJECT_SELL_FEE_BPS.
  • _issueTokensFormulaWrapper(uint _depositAmount): Implements the abstract function from BondingCurveBase_v1. Uses _segments._calculatePurchaseReturn from DiscreteCurveMathLib_v1.
  • _redeemTokensFormulaWrapper(uint _depositAmount): Implements the abstract function from RedeemingBondingCurveBase_v1. Uses _segments._calculateSaleReturn from DiscreteCurveMathLib_v1.
  • _handleCollateralTokensBeforeBuy(address _provider, uint _amount): Implements the abstract function from BondingCurveBase_v1. Transfers collateral from _provider to the contract.
  • _handleIssuanceTokensAfterBuy(address _receiver, uint _amount): Implements the abstract function from BondingCurveBase_v1. Mints issuance tokens to _receiver.
  • _handleCollateralTokensAfterSell(address _receiver, uint _collateralTokenAmount): Implements the abstract function from RedeemingBondingCurveBase_v1. Transfers collateral to _receiver.

User Interactions

The purpose of this section is to highlight common user interaction flows, specifically those that involve a multi-step process. Please note that only the user interactions defined in this contract should be listed. Functionalities inherited from base contracts should be referenced accordingly.

Function: buyFor (Buy Issuance Tokens)

To execute a buy operation (mint issuance tokens by depositing collateral):
Precondition

  • Buying must be enabled (inherited from BondingCurveBase_v1).
  • The _receiver address must be valid (not address(0)).
  • The caller (or msg.sender if they are the provider) must have sufficient collateral tokens.
  • The caller must have approved the FM contract to spend their collateral tokens.
  1. Get minAmountOut (Recommended):
    To protect against slippage, the minimum amount of issuance tokens expected can be pre-computed.
    // Assuming 'fm' is an instance of FM_BC_Discrete_Redeeming_VirtualSupply_v1
    // and 'collateralTokenAmountToDeposit' is the amount of collateral tokens the user wants to spend.
    uint collateralTokenAmountToDeposit = 1000 * 10**18; // Example: 1000 collateral tokens
    uint minIssuanceTokensOut = fm.calculatePurchaseReturn(collateralTokenAmountToDeposit);
    // Apply a slippage tolerance if desired, e.g., minIssuanceTokensOut = minIssuanceTokensOut * 99 / 100; (1% slippage)
  2. Call buyFor Function:
    solidity // User wants to buy for themselves address receiver = msg.sender; fm.buyFor(receiver, collateralTokenAmountToDeposit, minIssuanceTokensOut);
    Sequence Diagram
sequenceDiagram
    participant User
    participant FM_BC_Discrete as FM_BC_Discrete_Redeeming_VirtualSupply_v1
    participant CollateralToken as Collateral Token (IERC20)
    participant IssuanceToken as Issuance Token (ERC20Issuance_v1)
    participant DiscreteMathLib as DiscreteCurveMathLib_v1
    participant FeeManager as Fee Manager (via super call in init)

    User->>FM_BC_Discrete: buyFor(receiver, depositAmount, minAmountOut)
    FM_BC_Discrete->>FM_BC_Discrete: _buyOrder(...)
    FM_BC_Discrete->>CollateralToken: safeTransferFrom(user, this, depositAmount)
    Note over FM_BC_Discrete: Calculate net deposit after project & protocol collateral fees (using cached fees)
    FM_BC_Discrete->>DiscreteMathLib: _calculatePurchaseReturn(netDeposit, currentSupply)
    DiscreteMathLib-->>FM_BC_Discrete: grossTokensToMint
    Note over FM_BC_Discrete: Calculate net tokensToMint after protocol issuance fees (using cached fees)
    FM_BC_Discrete->>IssuanceToken: mint(receiver, netTokensToMint)
    Note over FM_BC_Discrete: Update projectCollateralFeeCollected
    Note over FM_BC_Discrete: Transfer protocol collateral fees to treasury (cached address)
    Note over FM_BC_Discrete: Mint protocol issuance fees to treasury (cached address)
    FM_BC_Discrete->>FM_BC_Discrete: _addVirtualCollateralAmount(netCollateralAdded)
    FM_BC_Discrete-->>User: (implicit success or revert)
Loading

Function: sellTo (Sell Issuance Tokens)

To execute a sell operation (redeem issuance tokens for collateral):
Precondition

  • Selling must be enabled (inherited from RedeemingBondingCurveBase_v1).
  • The _receiver address must be valid.
  • The caller (or msg.sender if they are the provider) must have sufficient issuance tokens.
  • The caller must have approved the FM contract to spend their issuance tokens.
  1. Get minAmountOut (Recommended):
    To protect against slippage, the minimum amount of collateral tokens expected can be pre-computed.
    // Assuming 'fm' is an instance of FM_BC_Discrete_Redeeming_VirtualSupply_v1
    // and 'issuanceTokenAmountToDeposit' is the amount of issuance tokens the user wants to sell.
    uint issuanceTokenAmountToDeposit = 500 * 10**18; // Example: 500 issuance tokens
    uint minCollateralTokensOut = fm.calculateSaleReturn(issuanceTokenAmountToDeposit);
    // Apply a slippage tolerance if desired
  2. Call sellTo Function:
    solidity // User wants to sell and receive collateral themselves address receiver = msg.sender; fm.sellTo(receiver, issuanceTokenAmountToDeposit, minCollateralTokensOut);
    Sequence Diagram
sequenceDiagram
    participant User
    participant FM_BC_Discrete as FM_BC_Discrete_Redeeming_VirtualSupply_v1
    participant IssuanceToken as Issuance Token (ERC20Issuance_v1)
    participant CollateralToken as Collateral Token (IERC20)
    participant DiscreteMathLib as DiscreteCurveMathLib_v1
    participant FeeManager as Fee Manager (via super call in init)

    User->>IssuanceToken: approve(FM_BC_Discrete, issuanceTokenAmountToDeposit)
    User->>FM_BC_Discrete: sellTo(receiver, issuanceTokenAmountToDeposit, minAmountOut)
    FM_BC_Discrete->>FM_BC_Discrete: _sellOrder(...)
    FM_BC_Discrete->>IssuanceToken: burnFrom(user, netIssuanceDepositAfterProtocolFee)
    Note over FM_BC_Discrete: Calculate net issuance deposit after protocol issuance fees (using cached fees)
    FM_BC_Discrete->>DiscreteMathLib: _calculateSaleReturn(netIssuanceDeposit, currentSupply)
    DiscreteMathLib-->>FM_BC_Discrete: grossCollateralToReturn
    Note over FM_BC_Discrete: Calculate net collateralToReturn after project & protocol collateral fees (using cached fees)
    FM_BC_Discrete->>CollateralToken: safeTransfer(receiver, netCollateralToReturn)
    Note over FM_BC_Discrete: Update projectCollateralFeeCollected
    Note over FM_BC_Discrete: Transfer protocol collateral fees to treasury (cached address)
    Note over FM_BC_Discrete: Mint protocol issuance fees to treasury (cached address)
    FM_BC_Discrete->>FM_BC_Discrete: _subVirtualCollateralAmount(totalCollateralTokenMovedOut)
    FM_BC_Discrete-->>User: (implicit success or revert)
Loading

Function: reconfigureSegments (Admin Interaction)

Allows an orchestratorAdmin to change the bonding curve's segment configuration.
Precondition

  • Caller must have the orchestratorAdmin role (enforced by onlyOrchestratorAdmin modifier).

Steps

  1. The admin prepares a new array of PackedSegment[] memory newSegments_.
  2. The admin calls fm.reconfigureSegments(newSegments_).

Important Note: The function includes an invariance check: newSegments_._calculateReserveForSupply(issuanceToken.totalSupply()) must equal virtualCollateralSupply. If not, the transaction reverts. This ensures the new curve configuration is consistent with the current backing.

Sequence Diagram

sequenceDiagram
    participant Admin
    participant FM_BC_Discrete as FM_BC_Discrete_Redeeming_VirtualSupply_v1
    participant DiscreteMathLib as DiscreteCurveMathLib_v1
    participant IssuanceToken as Issuance Token (ERC20Issuance_v1)

    Admin->>FM_BC_Discrete: reconfigureSegments(newSegments)
    FM_BC_Discrete->>IssuanceToken: totalSupply()
    IssuanceToken-->>FM_BC_Discrete: currentIssuanceSupply
    FM_BC_Discrete->>DiscreteMathLib: _calculateReserveForSupply(newSegments, currentIssuanceSupply)
    DiscreteMathLib-->>FM_BC_Discrete: newCalculatedReserve
    alt Invariance Check Passes (newCalculatedReserve == virtualCollateralSupply)
        FM_BC_Discrete->>FM_BC_Discrete: _setSegments(newSegments)
        FM_BC_Discrete-->>Admin: Event: SegmentsSet
    else Invariance Check Fails
        FM_BC_Discrete-->>Admin: Revert: InvarianceCheckFailed
    end
Loading

Deployment

Preconditions

The following preconditions must be met before deployment:

  • A deployed Orchestrator contract (IOrchestrator_v1).
  • A deployed Issuance Token contract that implements ERC20Issuance_v1 (to allow the FM to mint tokens).
  • A deployed Collateral Token contract (IERC20Metadata).
  • A deployed Fee Manager contract, configured with appropriate fees and treasury for this FM's orchestrator and module address.

Deployment Parameters

The init function is called by the Orchestrator during module registration. The configData_ bytes argument must be ABI encoded with the following parameters:

(
    address issuanceTokenAddress, // Address of the ERC20Issuance_v1 token
    address collateralTokenAddress, // Address of the IERC20Metadata collateral token
    PackedSegment[] memory initialSegments // The initial array of packed segments for the curve
)

Example (conceptual encoding):
abi.encode(0xIssuanceToken, 0xCollateralToken, initialSegmentsArray)

The list of deployment parameters can also be found in the Technical Reference section of the documentation under the init() function ([link - Placeholder for NatSpec link]).

Deployment

Deployment should be done using one of the methods provided below:

Setup Steps

  • The primary setup occurs during the init call, which configures tokens, segments, and fees.
  • After deployment, the orchestratorAdmin might need to:
    • Call setVirtualCollateralSupply(initialSupply) if the curve needs an initial virtual collateral amount not established through initial buys.
    • Configure other related modules or permissions in the Orchestrator if necessary.

After deployment, the mandatory and optional setup steps can be found in the contract NatSpec ([link - Placeholder for NatSpec link]).

@fabianschu fabianschu changed the title Experimental/per Experimental: Per May 26, 2025
@fabianschu fabianschu changed the title Experimental: Per Experimental: Discrete Lib May 28, 2025
Copy link

@leeftk leeftk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's an initial review, I'll still be looking at this until Wednesday, so will add more but this is enough to get us started.

fullStepBacking += stepCollateralCapacity_;
} else {
// Partial step purchase and exit
uint partialIssuance_ =
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's redundant calculation happening here. Lines 427-432 are calculating the same thing twice:

  1. First we calculate partialIssuance_ from remainingBudget_ using Math.mulDiv
  2. Then we recalculate the collateral cost from partialIssuance_ using FixedPointMathLib._mulDivUp

This is redundant because we're essentially doing: remainingBudget_ * SCALING_FACTOR / stepPrice_ then result * stepPrice_ / SCALING_FACTOR, which should give us back remainingBudget_ (with potential rounding differences).

The issue is that Math.mulDiv rounds down while _mulDivUp rounds up, so these calculations might not match exactly. We should just track the actual budget consumption instead of recalculating it.

Something like:

uint partialIssuance_ = Math.mulDiv(remainingBudget_, SCALING_FACTOR, stepPrice_);
tokensToMint_ += partialIssuance_;
// Use actual budget consumed instead of recalculating
collateralSpentByPurchaser_ = collateralToSpendProvided_ - remainingBudget_;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are exactly right. Already refactored this to eliminate the redundant calculations. The new version calculates partialIssuance_ once using Math.mulDiv(remainingBudget_, SCALING_FACTOR, stepPrice_) and then tracks budget consumption with collateralSpentByPurchaser_ = collateralToSpendProvided_ - remainingBudget_ at the end. This should avoid the rounding inconsistency between Math.mulDiv (rounds down) and FixedPointMathLib._mulDivUp (rounds up) that was causing the A ÷ B × B ≠ A issue.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing we can set this as resolved then ser!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We fixed this the wrong way and caused more errors in the end, now I think we have found the actual solution to it after looking at it again. Could you take another look? We fixed it here now.
@FHieser @leeftk 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants