Skip to content

Pricing Functions (Internal)

All pricing logic is internal to LiquidityVault. These functions are not directly callable externally but underpin all share pricing and redemption calculations.


Per-Position Pricing

_modeledPrice

function _modeledPrice(Position storage p) internal view returns (uint256)

Computes the current modeled price of NO shares for a single position:

if p.status == SETTLING:
    return adapter.currentPrice()    // gap = 0 for this slot; accrual frozen

if p.entryPrice == 0:
    return 0                         // written off

// status == ACTIVE: linear accrual
elapsed      = block.timestamp - p.startTime
duration     = p.maturity  - p.startTime
accrualRate  = min(elapsed / duration, 1.0)
modeledPrice = p.entryPrice + (1e18 - p.entryPrice) × accrualRate

For SETTLING slots, the modeled price is the live market price — no accrual, no gap. For ACTIVE slots, the modeled price accrues linearly from entryPrice toward 1e18 ($1.00).

positionModeledValue

function positionModeledValue(uint256 slotIndex) internal view returns (uint256)
Status Return value
ACTIVE _modeledPrice(position) × adapter.positionSize() / 1e18 (linear accrual)
SETTLING adapter.positionValue() — market value; gap contribution collapses to zero
WRITTEN_OFF 0
EMPTY 0

positionMarketValue

function positionMarketValue(uint256 slotIndex) internal view returns (uint256)
Status Return value
ACTIVE adapter.positionValue() (live market price × position size)
SETTLING adapter.positionValue() (same as ACTIVE)
WRITTEN_OFF 0
EMPTY 0

Aggregate NAV Functions

aggregateModeledNav

function aggregateModeledNav() internal view returns (uint256)
aggModeledNAV = Σ positionModeledValue(i) + idleReserve    // i in [0..3]
// ACTIVE:    linear accrual value
// SETTLING:  adapter.positionValue() — gap = 0 for this slot
// WRITTEN_OFF, EMPTY: 0

This is the investor-facing share price basis — what the vault is modeled to be worth. SETTLING slots contribute their market value, not their linear accrual, so they do not inflate the modeled NAV beyond what the market price supports.

aggregateMarketNav

function aggregateMarketNav() internal view returns (uint256)
aggMarketNAV = Σ positionMarketValue(i) + idleReserve     // i in [0..3]
// ACTIVE or SETTLING: adapter.positionValue()
// WRITTEN_OFF, EMPTY: 0

This is the true mark-to-market value — what the vault is worth based on live prediction market prices.

aggregateGapBps

function aggregateGapBps() internal view returns (uint256)
gap    = max(0, aggModeledNAV - aggMarketNAV)
gapBps = gap × 10000 / aggModeledNAV

Returns the spread between modeled and market NAV as basis points. SETTLING slots contribute zero to the gap — their modeled and market values are identical by definition.


Curve Integration

_avgCurvePrice

function _avgCurvePrice(
    uint256 fillBefore,   // [0, 1e18] — pre-scaled by caller: redeemedToday*1e18/dailyCap
    uint256 fillAfter,    // [0, 1e18] — pre-scaled by caller: (redeemedToday+reqVal)*1e18/dailyCap
    uint256 mdl,          // aggModeledNAV (USDC, 6 dec)
    uint256 mkt           // aggMarketNAV  (USDC, 6 dec)
) internal pure returns (uint256 curveNAV)

Return value is a total vault NAV, not a per-share price

curveNAV is the curve-weighted aggregate vault NAV (USDC, 6 dec) — a total vault valuation between aggMarketNAV and aggModeledNAV. It is not a per-share price. The only correct conversion: exitValue = shares * curveNAV / totalShares. Writing shares * curveNAV / 1e18 is wrong.

Computes the exact closed-form average curve-weighted vault NAV over the fill range [fillBefore, fillAfter]. A loop approximation introduces precision error that varies with step size and creates consensus risk if the keeper is replaced. The closed form must be used.

Closed-form derivation:

exitNAV(x) = mkt + (mdl - mkt) × (1 - x)²

avgNAV(a, b) = (1 / (b-a)) × ∫[a..b] exitNAV(x) dx
             = mkt + (mdl - mkt) × [(1-a)³ - (1-b)³] / [3 × (b-a)]

Returns: curveNAV (USDC, 6 dec) — total vault valuation.

Integer arithmetic implementation (1e18 fixed-point):

function _avgCurvePrice(
    uint256 fillBefore,
    uint256 fillAfter,
    uint256 mdl,
    uint256 mkt
) internal pure returns (uint256 curveNAV) {
    uint256 oneMinusA  = 1e18 - fillBefore;
    uint256 oneMinusB  = 1e18 - fillAfter;
    uint256 fillRange  = fillAfter - fillBefore;    // non-zero: enforced by caller

    // Compute (1-a)³ and (1-b)³, dividing after each multiply
    // to keep intermediates within uint256.
    uint256 cubeA = oneMinusA * oneMinusA / 1e18 * oneMinusA / 1e18;
    uint256 cubeB = oneMinusB * oneMinusB / 1e18 * oneMinusB / 1e18;
    uint256 cubeDiff = cubeA - cubeB;  // cubeA >= cubeB always

    if (mdl <= mkt) return mkt;        // no gap — return market NAV
    uint256 gap = mdl - mkt;

    // curveNAV = mkt + gap × cubeDiff / (3 × fillRange)
    // Units: gap (USDC 6dec) * cubeDiff (1e18) / fillRange (1e18) = USDC 6dec.
    curveNAV = mkt + gap * cubeDiff / (3 * fillRange);
}

// CALLER USAGE:
// exitValue = request.shares * curveNAV / totalShares;   ✓ correct
// exitValue = request.shares * curveNAV / 1e18;          ✗ wrong divisor

fillBefore/fillAfter must be pre-scaled to 1e18 by the caller

The function does not perform scaling internally. fillRange must be non-zero — the caller must ensure fillAfter > fillBefore. Division by zero is not handled inside the function.