Compare commits

2 Commits

Author SHA1 Message Date
tim
86410c9a91 burnSwap outputIndex/token naming fix 2025-10-28 12:17:01 -04:00
tim
5c5a000961 whitepaper rendering fix 2025-10-27 17:54:44 -04:00
6 changed files with 71 additions and 29 deletions

View File

@@ -8,78 +8,106 @@ Multi-asset liquidity typically trades off simplicity and expressivity. Classica
## System Model and Pricing Kernel
We consider $n\ge 2$ normalized assets with state vector $\mathbf{q}=(q_0,\dots,q_{n-1})\in\mathbb{R}_{\ge 0}^{\,n}$ and size metric $S(\mathbf{q})=\sum_i q_i$. The kernel is the LMSR cost function
$$
C(\mathbf{q}) = b(\mathbf{q}) \log\!\left(\sum_{i=0}^{n-1} e^{q_i / b(\mathbf{q})}\right), \qquad b(\mathbf{q})=\kappa\,S(\mathbf{q}),\quad \kappa>0.
$$
For numerical stability we evaluate $C$ with a log-sum-exp recentering. Let $y_i := q_i/b(\mathbf{q})$ and $M:=\max_i y_i$. Then
$$
C(\mathbf{q}) \;=\; b(\mathbf{q}) \left( M + \log \sum_{i=0}^{n-1} e^{\,y_i - M} \right),
$$
which prevents overflow/underflow when the $y_i$ are dispersed. Quantities are represented in fixed-point with explicit range and domain guards; equations are presented over the reals for clarity.
## Gradient, Price Shares, and Pairwise Prices
With $b$ treated as a constant parameter, the LMSR gradient recovers softmax shares
$$
\frac{\partial C}{\partial q_i} \;=\; \frac{e^{q_i/b}}{\sum_k e^{q_k/b}} \;=:\; \pi_i(\mathbf{q}),
$$
so that the ratio of marginal prices is $\pi_j/\pi_i = \exp\!\big((q_j-q_i)/b\big)$. When $b(\mathbf{q})=\kappa S(\mathbf{q})$ depends on state, $\frac{\partial C}{\partial q_i}$ acquires a common additive term across $i$ from $\partial b/\partial q_i$, but pairwise ratios remain governed by softmax differences. We therefore use a quasi-static-$b$ view for pricing steps, holding $b$ fixed at the pre-trade state for the infinitesimal move, and define the instantaneous pairwise marginal price ratio for exchanging $i$ into $j$ as
$$
P(i\to j \mid \mathbf{q}) \;=\; \exp\!\left(\frac{q_j - q_i}{b(\mathbf{q})}\right).
$$
This ratio drives swap computations and is invariant to proportional rescaling $\mathbf{q}\mapsto \lambda\mathbf{q}$ because $b$ scales by the same factor.
## Two-Asset Reduction and Exact Swap Mappings
Swaps are computed in the two-asset subspace spanned by the in-asset $i$ and out-asset $j$, with all other coordinates held fixed under a quasi-static-$b$ step. Let
$$
r_0 \;:=\; \exp\!\left(\frac{q_i - q_j}{b}\right), \qquad b \equiv b(\mathbf{q})\;\text{ held quasi-static}.
$$
Along the $i\!\to\! j$ path, the instantaneous ratio evolves multiplicatively as $r(t)=r_0\,e^{t/b}$ where $t$ denotes cumulative input of asset $i$. In the two-asset reduction the infinitesimal output satisfies
$$
\mathrm{d}y \;=\; \frac{r(t)}{1+r(t)}\,\mathrm{d}t.
$$
Integrating from $t=0$ to $t=a$ yields the exact-in closed form
$$
y(a) \;=\; b \,\ln\!\Big( 1 + r_0 \,\big(1 - e^{-a/b}\big) \Big).
$$
This mapping has $y(0)=0$, is strictly increasing and concave in $a$, and satisfies $y'(0)=\frac{r_0}{1+r_0}$ with asymptote $\lim_{a\to\infty} y = b\,\ln(1+r_0)$. The inverse exact-out mapping follows by solving for $a$ in terms of target $y$. Writing $E:=e^{y/b}$, we obtain
$$
a(y) \;=\; b \,\ln\!\left(\frac{r_0}{\,r_0 + 1 - E\,}\right),
$$
which is strictly increasing and convex for $y\in\big[0,\, b\ln(1+r_0)\big]$. These two expressions are the workhorses for exact-in and exact-out swaps in our kernel.
## Price Limits, Swap-to-Limit, and Capacity Caps
Users may provide a maximum acceptable marginal price ratio $\Lambda>0$ for $p_i/p_j$. The marginal ratio trajectory $r(t)=r_0 e^{t/b}$ first reaches the limit at the unique
$$
a_{\text{lim}} \;=\; b \,\ln\!\left(\frac{\Lambda}{r_0}\right),
$$
and the output realized at that truncation is
$$
y_{\text{lim}} \;=\; b \,\ln\!\Big( 1 + r_0 \,\big(1 - r_0/\Lambda\big) \Big).
$$
Outputs are further bounded by available inventory; if a computed $y$ would exceed $q_j$, we cap at $y=q_j$ and compute the implied input by inverting the exact-out formula,
$$
a_{\text{cap}} \;=\; b \,\ln\!\left(\frac{r_0}{\,r_0 + 1 - e^{\,q_j/b}\,}\right).
$$
These limit and capacity branches ensure monotone, conservative behavior near domain edges.
## Liquidity Operations from the Same Potential
Liquidity is accounted via pool shares $L$ taken proportional to the size metric, and we set $L=S(\mathbf{q})$ without loss of generality. At initialization with seed balances $\mathbf{q}^{(0)}$ the pool sets $L^{(0)}=S^{(0)}$ and $b^{(0)}=\kappa S^{(0)}$. A proportional deposit that scales balances to $\mathbf{q}'=(1+\alpha)\mathbf{q}$ mints $\Delta L = \alpha S(\mathbf{q})$ shares and scales liquidity to $b'=(1+\alpha)b$. Single-asset deposits target a proportional growth while rebalancing through kernel swaps: providing amount $a$ of asset $i$ induces a growth factor $\alpha\ge 0$ satisfying the monotone equation
$$
a \;=\; a_{\text{req}}(\alpha) \;=\; \alpha q_i \;+\; \sum_{j\ne i} b \,\ln\!\left(\frac{r_{0,j}}{\,r_{0,j} + 1 - e^{\,\alpha q_j/b}\,}\right), \quad r_{0,j}:=\exp\!\left(\frac{q_i-q_j}{b}\right),
$$
and mints $\Delta L=\alpha S(\mathbf{q})$ upon the unique solution. Proportional withdrawals burn $\Delta L$ and return $\alpha=\Delta L/S(\mathbf{q})$ of each asset, updating $b$ to $(1-\alpha)b$. Single-asset withdrawals redeem $\alpha q_i$ directly and swap each redeemed $\alpha q_j$ for $j\ne i$ into $i$ using the exact-in mapping evaluated on the local post-burn state; any capacity overrun is handled by a cap-and-invert branch as above. Because all operations reduce to the same two-asset closed forms, they inherit monotonicity and uniqueness.
### Single-Asset Mint and Redeem
#### Single-Asset Mint
Given a deposit of amount $a>0$ of asset $i$, the pool targets a proportional growth factor $\alpha \ge 0$ so that the post-mint state can be rebalanced to $(1+\alpha)\,\mathbf{q}$ using fee-free kernel swaps from $i$ into each $j\ne i$. For each $j\ne i$, let $y_j := \alpha\,q_j$ and define $r_{0,j} := \exp\!\big((q_i - q_j)/b\big)$. The input required to realize $y_j$ via the exact-out inverse is
$$
x_j(\alpha) \;=\; b \,\ln\!\left(\frac{r_{0,j}}{\,r_{0,j} + 1 - e^{\,y_j/b}\,}\right),
$$
so the total required input for growth $\alpha$ is
$$
a_{\text{req}}(\alpha) \;=\; \alpha\,q_i \;+\; \sum_{j\ne i} x_j(\alpha).
$$
Properties and solver:
- Monotonicity: $a_{\text{req}}(\alpha)$ is strictly increasing on its feasible domain, guaranteeing a unique solution.
- Solver: bracket $\alpha$ (e.g., start from $\alpha\sim a/S$ and double until $a_{\text{req}}(\alpha)\ge a$ or a safety cap), then bisection to a small tolerance $\varepsilon$ (e.g., $\sim10^{-6}$ in fixed-point units).
@@ -91,28 +119,36 @@ Burning a proportional share $\alpha \in (0,1]$ returns a single asset $i$ by re
1) Form the local state after burn, $\mathbf{q}_{\text{local}}=(1-\alpha)\,\mathbf{q}$.
2) Start with the direct redemption $\alpha\,q_i$ in asset $i$.
3) For each $j\ne i$, withdraw $a_j := \alpha\,q_j$ and swap $j\to i$ using the exact-in form evaluated at $\mathbf{q}_{\text{local}}$:
$$
r_{0,j} \;=\; \exp\!\left(\frac{q^{\text{local}}_j - q^{\text{local}}_i}{b}\right),\qquad
y_{j\to i} \;=\; b \,\ln\!\Big(1 + r_{0,j}\,\big(1 - e^{-a_j/b}\big)\Big).
$$
4) Capacity cap and inverse: if $y_{j\to i} > q^{\text{local}}_i$, cap to $y=q^{\text{local}}_i$ and solve the implied input via
$$
a_{j,\text{used}} \;=\; b \,\ln\!\left(\frac{r_{0,j}}{\,r_{0,j} + 1 - e^{\,q^{\text{local}}_i/b}\,}\right),
$$
then update $\mathbf{q}_{\text{local}}$ accordingly.
5) The single-asset payout is
$$
Y_i \;=\; \alpha\,q_i \;+\; \sum_{j\ne i} y_{j\to i}, \qquad \text{with LP burned } L_{\text{in}} = \alpha \, S(\mathbf{q}).
$$
Guards and behavior:
- Enforce $b>0$, positivity of inner terms (e.g., $r_{0,j} + 1 - e^{y/b} > 0$), and safe exponent ranges; treat any per-asset numerical failure as zero contribution rather than aborting the whole redeem.
- The mapping is monotone in $\alpha$; the cap-and-invert branch preserves safety near capacity.
### LP Pricing vs. an Asset Token
With LP supply set to $L=S(\mathbf{q})$, the instantaneous price of one LP share in units of asset $k$ aggregates marginal exchange rates from each asset into $k$:
$$
P_L^{(k)}(\mathbf{q}) \;=\; \frac{1}{S(\mathbf{q})}\,\sum_{j=0}^{n-1} q_j \,\exp\!\left(\frac{q_j - q_k}{b(\mathbf{q})}\right).
$$
Interpretation: proportional deposits leave $P_L^{(k)}$ unchanged; swap fees retained in the pool increase $S$ relative to outstanding $L$, raising $P_L^{(k)}$ (implicit fee accrual). This expression helps LPs and integrators reason about share valuation and dilution across assets.
## Numerical Methods and Safety Guarantees
@@ -120,13 +156,17 @@ We evaluate log-sum-exp with recentring, compute ratios like $r_0=\exp((q_i-q_j)
## Balanced Regime Optimization: Approximations, Dispatcher, and Stability
Since transcendental operations are gas-expensive on EVM chains, we use polynomial approximations in near-balanced regimes (e.g., stable-asset pairs) while preserving monotonicity and domain safety. Parameterize $\delta := (q_i - q_j)/b$ and $\tau := a/b$ for an $i\!\to\! j$ exact-in step. The exact mapping
$$
y(a) \;=\; b \,\ln\!\Big(1 + e^{\delta}\,\big(1 - e^{-\tau}\big)\Big)
$$
admits small-argument expansions for $|\delta|\ll 1$ and $|\tau|\ll 1$. Using $e^{\pm x}\approx 1\pm x+\tfrac{x^2}{2}$ and $\ln(1+u)\approx u - \tfrac{u^2}{2}$, we obtain
$$
y(a) \;\approx\; b \left[ r_0 \tau - \tfrac{1}{2} r_0 \tau^2 \right] + \mathcal{O}\!\left(\tau^3,\, |\delta|\,\tau^2\right), \qquad r_0=e^{\delta}\approx 1+\delta+\tfrac{\delta^2}{2},
$$
and at $\delta=0$ the symmetry reduces to $y(a)\approx \tfrac{a}{2} - \tfrac{a^2}{4b} + \cdots$.
### Dispatcher preconditions and thresholds (approx path):
@@ -158,9 +198,11 @@ Under constant $b$, classical LMSR admits a worst-case loss bound of $b \ln n$ i
## Deployment and Parameter Fixity
The parameter tuple $(\kappa, f_{\text{swap}}, \phi)$ is set at deployment and remains immutable, with $\kappa>0$ defining $b(\mathbf{q})=\kappa S(\mathbf{q})$, $f_{\text{swap}}$ the swap fee rate, and $\phi$ the protocol share of fees. Given the initial state $\mathbf{q}^{(0)}$ with $S^{(0)}>0$, the induced pricing map is fully determined by
$$
C(\mathbf{q}) = b(\mathbf{q}) \log\!\left(\sum_i e^{q_i / b(\mathbf{q})}\right), \qquad b(\mathbf{q})=\kappa S(\mathbf{q}),
$$
and the two-asset closed forms above. Fixity eliminates governance risk, makes depth calibration transparent, and simplifies integration for external routers and valuation tools.
## Conclusion

View File

@@ -230,19 +230,19 @@ interface IPartyPool is IERC20Metadata, IOwnable {
uint256 deadline
) external payable returns (uint256 lpMinted);
/// @notice Burn LP tokens then swap the redeemed proportional basket into a single asset `inputTokenIndex` and send to receiver.
/// @notice Burn LP tokens then swap the redeemed proportional basket into a single asset `outputTokenIndex` and send to receiver.
/// @dev The function burns LP tokens (authorization via allowance if needed), sends the single-asset payout and updates LMSR state.
/// @param payer who burns LP tokens
/// @param receiver who receives the single asset
/// @param lpAmount amount of LP tokens to burn
/// @param inputTokenIndex index of target asset to receive
/// @param outputTokenIndex index of target asset to receive
/// @param deadline optional deadline
/// @return amountOutUint uint amount of asset inputTokenIndex sent to receiver
/// @return amountOutUint uint amount of asset outputTokenIndex sent to receiver
function burnSwap(
address payer,
address receiver,
uint256 lpAmount,
uint256 inputTokenIndex,
uint256 outputTokenIndex,
uint256 deadline,
bool unwrap
) external returns (uint256 amountOutUint);

View File

@@ -52,8 +52,8 @@ interface IPartyPoolViewer {
/// @notice Calculate the amounts for a burn swap operation
/// @dev This is a pure view function that computes burn swap amounts from provided state
/// @param lpAmount amount of LP _tokens to burn
/// @param inputTokenIndex index of target asset to receive
function burnSwapAmounts(IPartyPool pool, uint256 lpAmount, uint256 inputTokenIndex) external view
/// @param outputTokenIndex index of target asset to receive
function burnSwapAmounts(IPartyPool pool, uint256 lpAmount, uint256 outputTokenIndex) external view
returns (uint256 amountOut);
/// @notice Compute repayment amounts (principal + flash fee) for a proposed flash loan.

View File

@@ -407,14 +407,14 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
/// @param payer who burns LP _tokens
/// @param receiver who receives the single asset
/// @param lpAmount amount of LP _tokens to burn
/// @param inputTokenIndex index of target asset to receive
/// @param outputTokenIndex index of target asset to receive
/// @param deadline optional deadline
/// @return amountOutUint uint amount of asset i sent to receiver
function burnSwap(
address payer,
address receiver,
uint256 lpAmount,
uint256 inputTokenIndex,
uint256 outputTokenIndex,
uint256 deadline,
bool unwrap
) external returns (uint256 amountOutUint) {
@@ -423,7 +423,7 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
payer,
receiver,
lpAmount,
inputTokenIndex,
outputTokenIndex,
deadline,
unwrap,
SWAP_FEE_PPM,

View File

@@ -441,7 +441,7 @@ contract PartyPoolMintImpl is PartyPoolBase {
/// @notice Calculate the amounts for a burn swap operation
/// @dev This is a pure view function that computes burn swap amounts from provided state
/// @param lpAmount amount of LP _tokens to burn
/// @param inputTokenIndex index of target asset to receive
/// @param outputTokenIndex index of target asset to receive
/// @param swapFeePpm fee in parts-per-million
/// @param lmsrState current LMSR state
/// @param bases_ scaling _bases for each token
@@ -449,13 +449,13 @@ contract PartyPoolMintImpl is PartyPoolBase {
/// @return amountOut amount of target asset that would be received
function burnSwapAmounts(
uint256 lpAmount,
uint256 inputTokenIndex,
uint256 outputTokenIndex,
uint256 swapFeePpm,
LMSRStabilized.State memory lmsrState,
uint256[] memory bases_,
uint256 totalSupply_
) public pure returns (uint256 amountOut) {
require(inputTokenIndex < bases_.length, "burnSwapAmounts: idx");
require(outputTokenIndex < bases_.length, "burnSwapAmounts: idx");
require(lpAmount > 0, "burnSwapAmounts: zero lp");
require(totalSupply_ > 0, "burnSwapAmounts: empty supply");
@@ -465,21 +465,21 @@ contract PartyPoolMintImpl is PartyPoolBase {
// Use LMSR view to compute single-asset payout and burned size-metric
(int128 payoutInternal, ) = LMSRStabilized.swapAmountsForBurn(lmsrState.nAssets, lmsrState.kappa, lmsrState.qInternal,
inputTokenIndex, alpha);
outputTokenIndex, alpha);
// Convert payoutInternal -> uint (floor) to favor pool
amountOut = _internalToUintFloorPure(payoutInternal, bases_[inputTokenIndex]);
amountOut = _internalToUintFloorPure(payoutInternal, bases_[outputTokenIndex]);
require(amountOut > 0, "burnSwapAmounts: output zero");
}
/// @notice Burn LP _tokens then swap the redeemed proportional basket into a single asset `inputTokenIndex` and send to receiver.
/// @notice Burn LP _tokens then swap the redeemed proportional basket into a single asset `outputTokenIndex` and send to receiver.
/// This version of burn does not work if the vault has been killed, because it involves a swap. Use regular burn()
/// to recover funds if the pool has been killed.
/// @dev The function burns LP _tokens (authorization via allowance if needed), sends the single-asset payout and updates LMSR state.
/// @param payer who burns LP _tokens
/// @param receiver who receives the single asset
/// @param lpAmount amount of LP _tokens to burn
/// @param inputTokenIndex index of target asset to receive
/// @param outputTokenIndex index of target asset to receive
/// @param deadline optional deadline
/// @param swapFeePpm fee in parts-per-million for this pool (may be used for future fee logic)
/// @return amountOutUint uint amount of asset i sent to receiver
@@ -487,14 +487,14 @@ contract PartyPoolMintImpl is PartyPoolBase {
address payer,
address receiver,
uint256 lpAmount,
uint256 inputTokenIndex,
uint256 outputTokenIndex,
uint256 deadline,
bool unwrap,
uint256 swapFeePpm,
uint256 protocolFeePpm
) external nonReentrant killable returns (uint256 amountOutUint) {
uint256 n = _tokens.length;
require(inputTokenIndex < n, "burnSwap: idx");
require(outputTokenIndex < n, "burnSwap: idx");
require(lpAmount > 0, "burnSwap: zero lp");
require(deadline == 0 || block.timestamp <= deadline, "burnSwap: deadline");
@@ -506,16 +506,16 @@ contract PartyPoolMintImpl is PartyPoolBase {
.mul(ABDKMath64x64.divu(1000000-swapFeePpm, 1000000)); // adjusted for fee
// Use LMSR view to compute single-asset payout and burned size-metric
(int128 payoutInternal, ) = _lmsr.swapAmountsForBurn(inputTokenIndex, alpha);
(int128 payoutInternal, ) = _lmsr.swapAmountsForBurn(outputTokenIndex, alpha);
// Convert payoutInternal -> uint (floor) to favor pool
amountOutUint = _internalToUintFloor(payoutInternal, _bases[inputTokenIndex]);
amountOutUint = _internalToUintFloor(payoutInternal, _bases[outputTokenIndex]);
require(amountOutUint > 0, "burnSwap: output zero");
// Compute gross payout (no swap fee) so we can determine token-side fee = gross - net
int128 alphaGross = ABDKMath64x64.divu(lpAmount, supply); // gross fraction (no swap fee)
(int128 payoutGrossInternal, ) = _lmsr.swapAmountsForBurn(inputTokenIndex, alphaGross);
uint256 payoutGrossUint = _internalToUintFloor(payoutGrossInternal, _bases[inputTokenIndex]);
(int128 payoutGrossInternal, ) = _lmsr.swapAmountsForBurn(outputTokenIndex, alphaGross);
uint256 payoutGrossUint = _internalToUintFloor(payoutGrossInternal, _bases[outputTokenIndex]);
uint256 feeTokenUint = (payoutGrossUint > amountOutUint) ? (payoutGrossUint - amountOutUint) : 0;
// Accrue protocol share (floor) from the token-side fee
@@ -523,13 +523,13 @@ contract PartyPoolMintImpl is PartyPoolBase {
if (protocolFeePpm > 0 && feeTokenUint > 0) {
protoShare = (feeTokenUint * protocolFeePpm) / 1_000_000;
if (protoShare > 0) {
_protocolFeesOwed[inputTokenIndex] += protoShare;
_protocolFeesOwed[outputTokenIndex] += protoShare;
}
}
// Transfer the payout to receiver via centralized helper
IERC20 inputToken = _tokens[inputTokenIndex];
_sendTokenTo(inputToken, receiver, amountOutUint, unwrap);
IERC20 outputToken = _tokens[outputTokenIndex];
_sendTokenTo(outputToken, receiver, amountOutUint, unwrap);
// Burn LP _tokens from payer (authorization via allowance)
if (msg.sender != payer) {
@@ -542,7 +542,7 @@ contract PartyPoolMintImpl is PartyPoolBase {
int128[] memory newQInternal = new int128[](n);
for (uint256 idx = 0; idx < n; idx++) {
uint256 newBal = _cachedUintBalances[idx];
if (idx == inputTokenIndex) {
if (idx == outputTokenIndex) {
// Effective LP balance decreases by net payout and increased protocol owed
newBal = newBal - amountOutUint - protoShare;
}
@@ -561,7 +561,7 @@ contract PartyPoolMintImpl is PartyPoolBase {
_lmsr.updateForProportionalChange(newQInternal);
}
emit IPartyPool.BurnSwap(payer, receiver, inputToken, lpAmount, amountOutUint,
emit IPartyPool.BurnSwap(payer, receiver, outputToken, lpAmount, amountOutUint,
feeTokenUint-protoShare, protoShare);
return amountOutUint;

View File

@@ -123,12 +123,12 @@ contract PartyPoolViewer is PartyPoolHelpers, IPartyPoolViewer {
}
function burnSwapAmounts(IPartyPool pool, uint256 lpAmount, uint256 inputTokenIndex) external view
function burnSwapAmounts(IPartyPool pool, uint256 lpAmount, uint256 outputTokenIndex) external view
returns (uint256 amountOut) {
LMSRStabilized.State memory lmsr = pool.LMSR();
return MINT_IMPL.burnSwapAmounts(
lpAmount,
inputTokenIndex,
outputTokenIndex,
pool.swapFeePpm(),
lmsr,
pool.denominators(),