removed tax coin support

This commit is contained in:
tim
2025-10-07 17:09:24 -04:00
parent 12957aaa51
commit 923d9b93e7
5 changed files with 135 additions and 41 deletions

View File

@@ -24,11 +24,14 @@ Naturally multi-asset, Liquidity Party altcoin pools provide direct, one-hop swa
| Assets | Pairs | Swap Gas | Mint Gas | | Assets | Pairs | Swap Gas | Mint Gas |
|-------:|------:|---------:|----------:| |-------:|------:|---------:|----------:|
| 2 | 1 | 146,000 | 149,000 | | 2 | 1 | 132,000 | 143,000 |
| 10 | 45 | 157,000 | 426,000 | | 2* | 1 | 119,000 | 143,000 |
| 20 | 190 | 171,000 | 772,000 | | 10 | 45 | 142,000 | 412,000 |
| 50 | 1225 | 213,000 | 1,810,000 | | 20 | 190 | 157,000 | 749,000 |
| 100 | 4950 | 283,000 | 3,542,000 | | 50 | 1225 | 199,000 | 1,760,000 |
| 100 | 4950 | 269,000 | 2,684,000 |
\* Stablecoin pair pool optimization
Liquidity Party aggregates scarce, low market cap assets into a single pool, providing one-hop liquidity for exotic pairs without fragmenting LP assets. CP pools would need 190x the LP assets to provide the same pairwise liquidity as a single 20-asset Liquidity Party pool, due to asset fragmentation. Liquidity Party aggregates scarce, low market cap assets into a single pool, providing one-hop liquidity for exotic pairs without fragmenting LP assets. CP pools would need 190x the LP assets to provide the same pairwise liquidity as a single 20-asset Liquidity Party pool, due to asset fragmentation.

100
doc/whitepaper2.md Normal file
View File

@@ -0,0 +1,100 @@
# LMSR-based Multi-Asset AMM
Abstract
We present a multi-asset automated market maker whose pricing kernel is the Logarithmic Market Scoring Rule (LMSR). The pool maintains the convex potential $C(\mathbf{q}) = b(\mathbf{q}) \log\!\Big(\sum_i e^{q_i / b(\mathbf{q})}\Big)$ over normalized inventories $\mathbf{q}$, and sets the effective liquidity parameter proportional to pool size as $b(\mathbf{q}) = \kappa \, S(\mathbf{q})$ with $S(\mathbf{q}) = \sum_i q_i$ and fixed $\kappa>0$. This proportional parameterization preserves scale-invariant responsiveness while retaining softmax-derived pairwise price ratios under a quasi-static-$b$ view, enabling any-to-any swaps within a single potential. We derive and use closed-form expressions for two-asset reductions to compute exact-in, exact-out, limit-hitting (swap-to-limit), and capped-output trades. We discuss stability techniques such as log-sum-exp, ratio-once shortcuts, and domain guards for fixed-point arithmetic. Liquidity operations (proportional and single-asset joins/exits) follow directly from the same potential and admit monotone, invertible mappings. Parameters are immutable post-deployment for transparency and predictable depth calibration.
Introduction and Motivation
Multi-asset liquidity typically trades off simplicity and expressivity. Classical CFMMs define multiplicative invariants over reserves, while LMSR specifies a convex cost function whose gradient yields prices. Our goal is a multi-asset AMM that uses LMSR to support any-to-any swaps, shares risk across many assets, and scales depth predictably with pool size. By setting $b(\mathbf{q})=\kappa S(\mathbf{q})$, we achieve scale invariance: proportional rescaling of all balances scales $b$ proportionally and preserves pairwise price ratios, so the markets responsiveness is consistent across liquidity regimes. The derivations below formulate instantaneous prices, closed-form swap mappings, limit logic, and liquidity operations tailored to this parameterization.
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.
Fees and Economic Considerations
Swap fees are applied outside the fee-free kernel. For an exact-in submission $a$ on asset $i$, the effective kernel input is $a_{\text{eff}}=(1-f_{\text{swap}})a$. The kernel computes output using $a_{\text{eff}}$, while the retained fee remains in the pool, increasing $S(\mathbf{q})$ relative to outstanding $L$ and thereby accruing value to LPs implicitly. Scale invariance under $b=\kappa S$ implies that proportional growth of inventories preserves price ratios while deepening notional liquidity linearly. The classical LMSR bounded-loss intuition for constant $b$ gives $b\ln n$ in appropriate units; under our proportional $b$, this scales with $S$, so the instantaneous bound per unit of $S$ is proportional to $\kappa\ln n$.
Balanced Regime, Approximations, and Stability
Near balance it is useful to parameterize $\delta := (q_i - q_j)/b$ and $\tau := a/b$. The exact mapping
$$
y(a) \;=\; b \,\ln\!\Big(1 + e^{\delta}\,\big(1 - e^{-\tau}\big)\Big)
$$
admits small-argument expansions when $|\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 - \frac{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$. In designated near-balance domains one may replace $\exp$ and $\ln$ with verified polynomial approximations to reduce computational cost while maintaining monotonicity and bounded error; a dispatcher enforces preconditions on $|\delta|$, $|a|/b$, and positivity of inner arguments, otherwise falling back to the exact forms. Regardless of path, our numerical policy prioritizes monotonicity, domain safety, and conservative branches at decision boundaries.
Numerical Methods and Safety Guarantees
We evaluate log-sum-exp with recentring, compute ratios like $r_0=\exp((q_i-q_j)/b)$ directly rather than dividing exponentials, and guard all $\exp$ and $\ln$ calls to bounded domains with explicit checks on positivity of inner terms such as $r_0+1-e^{y/b}$. Fixed-point implementations precompute reciprocals like $1/b$ to reduce dispersion, clamp to capacity before inversion, and select cap-and-invert rather than extrapolating when inner terms approach zero. These measures ensure the swap maps remain strictly order-preserving and free of nonphysical outputs. Property-based and differential testing can confirm monotonicity of $y(a)$ and $a(y)$, uniqueness of limit hits when $\Lambda>r_0$, and adherence to predefined error budgets.
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
By coupling LMSR with the proportional parameterization $b(\mathbf{q})=\kappa S(\mathbf{q})$, we obtain a multi-asset AMM that preserves softmax-driven price ratios under a quasi-static-$b$ view and supports any-to-any swaps via a single convex potential. Exact two-asset reductions yield closed-form mappings for exact-in, exact-out, limit-hitting, and capped-output trades, and the same formulas underpin liquidity operations with monotonicity and uniqueness. Numerical stability follows from log-sum-exp evaluation, ratio-first derivations, guarded transcendental domains, and optional near-balance approximations, while fixed parameters provide predictable scaling and transparent economics.
References
Hanson, R. (2002). Logarithmic Market Scoring Rules for Modular Combinatorial Information Aggregation. https://mason.gmu.edu/~rhanson/mktscore.pdf

View File

@@ -9,7 +9,7 @@ remappings = [
optimizer=true optimizer=true
optimizer_runs=999999999 optimizer_runs=999999999
viaIR=true viaIR=true
gas_reports = ['PartyPool', 'PartyPlanner', 'PartyPoolSwapImpl', 'PartyPoolMintImpl',] gas_reports = ['PartyPool', 'PartyPoolBalancedPair', 'PartyPlanner', 'PartyPoolSwapImpl', 'PartyPoolMintImpl',]
fs_permissions = [{ access = "write", path = "chain.json"}] fs_permissions = [{ access = "write", path = "chain.json"}]
[lint] [lint]

View File

@@ -134,6 +134,7 @@ contract PartyPlanner is IPartyPlanner {
for (uint256 i = 0; i < _tokens.length; i++) { for (uint256 i = 0; i < _tokens.length; i++) {
if (initialDeposits[i] > 0) { if (initialDeposits[i] > 0) {
IERC20(_tokens[i]).safeTransferFrom(payer, address(pool), initialDeposits[i]); IERC20(_tokens[i]).safeTransferFrom(payer, address(pool), initialDeposits[i]);
require(IERC20(_tokens[i]).balanceOf(address(pool)) == initialDeposits[i], 'fee-on-transfer tokens not supported');
} }
} }

View File

@@ -87,12 +87,12 @@ contract PartyPoolMintImpl is PartyPoolBase {
unchecked { i++; } unchecked { i++; }
} }
// Update cached balances for all assets // Update cached balances and internal q for all assets using depositAmounts
int128[] memory newQInternal = new int128[](n); int128[] memory newQInternal = new int128[](n);
for (uint i = 0; i < n; ) { for (uint i = 0; i < n; ) {
uint256 bal = IERC20(tokens[i]).balanceOf(address(this)); uint256 newBal = cachedUintBalances[i] + depositAmounts[i];
cachedUintBalances[i] = bal; cachedUintBalances[i] = newBal;
newQInternal[i] = _uintToInternalFloor(bal, bases[i]); newQInternal[i] = _uintToInternalFloor(newBal, bases[i]);
unchecked { i++; } unchecked { i++; }
} }
@@ -105,7 +105,6 @@ contract PartyPoolMintImpl is PartyPoolBase {
uint256 newScaled = ABDKMath64x64.mulu(newTotal, LP_SCALE); uint256 newScaled = ABDKMath64x64.mulu(newTotal, LP_SCALE);
uint256 actualLpToMint; uint256 actualLpToMint;
require(oldScaled > 0, "mint: oldScaled zero");
uint256 delta = (newScaled > oldScaled) ? (newScaled - oldScaled) : 0; uint256 delta = (newScaled > oldScaled) ? (newScaled - oldScaled) : 0;
// Proportional issuance: totalSupply * delta / oldScaled // Proportional issuance: totalSupply * delta / oldScaled
if (delta > 0) { if (delta > 0) {
@@ -143,15 +142,8 @@ contract PartyPoolMintImpl is PartyPoolBase {
uint256 supply = _totalSupply; uint256 supply = _totalSupply;
require(supply > 0, "burn: empty supply"); require(supply > 0, "burn: empty supply");
require(lmsr.nAssets > 0, "burn: uninit pool");
require(_balances[payer] >= lpAmount, "burn: insufficient LP");
// Refresh cached balances to reflect current on-chain balances before computing withdrawal amounts // Use cached balances; assume standard ERC20 transfers without external interference
for (uint i = 0; i < n; ) {
uint256 bal = IERC20(tokens[i]).balanceOf(address(this));
cachedUintBalances[i] = bal;
unchecked { i++; }
}
// Compute proportional withdrawal amounts for the requested LP amount (rounded down) // Compute proportional withdrawal amounts for the requested LP amount (rounded down)
withdrawAmounts = burnAmounts(lpAmount, lmsr.nAssets, _totalSupply, cachedUintBalances); withdrawAmounts = burnAmounts(lpAmount, lmsr.nAssets, _totalSupply, cachedUintBalances);
@@ -164,12 +156,12 @@ contract PartyPoolMintImpl is PartyPoolBase {
unchecked { i++; } unchecked { i++; }
} }
// Update cached balances and internal q for all assets // Update cached balances and internal q for all assets using computed withdrawals
int128[] memory newQInternal = new int128[](n); int128[] memory newQInternal = new int128[](n);
for (uint i = 0; i < n; ) { for (uint i = 0; i < n; ) {
uint256 bal = IERC20(tokens[i]).balanceOf(address(this)); uint256 newBal = cachedUintBalances[i] - withdrawAmounts[i];
cachedUintBalances[i] = bal; cachedUintBalances[i] = newBal;
newQInternal[i] = _uintToInternalFloor(bal, bases[i]); newQInternal[i] = _uintToInternalFloor(newBal, bases[i]);
unchecked { i++; } unchecked { i++; }
} }
@@ -192,7 +184,7 @@ contract PartyPoolMintImpl is PartyPoolBase {
// Burn exactly the requested LP amount from payer (authorization via allowance) // Burn exactly the requested LP amount from payer (authorization via allowance)
if (msg.sender != payer) { if (msg.sender != payer) {
uint256 allowed = _allowances[payer][msg.sender]; uint256 allowed = _allowances[payer][msg.sender];
require(allowed >= lpAmount, "burn: allowance insufficient"); // Rely on Solidity's checked arithmetic to revert on underflow if allowance is insufficient
_approve(payer, msg.sender, allowed - lpAmount); _approve(payer, msg.sender, allowed - lpAmount);
} }
_burn(payer, lpAmount); _burn(payer, lpAmount);
@@ -374,25 +366,22 @@ contract PartyPoolMintImpl is PartyPoolBase {
uint256 totalTransfer = amountInUint + feeUintActual; uint256 totalTransfer = amountInUint + feeUintActual;
require(totalTransfer > 0 && totalTransfer <= maxAmountIn, "swapMint: transfer exceeds max"); require(totalTransfer > 0 && totalTransfer <= maxAmountIn, "swapMint: transfer exceeds max");
// Record pre-balance and transfer tokens from payer, require exact receipt (revert on fee-on-transfer) // Transfer tokens from payer (assume standard ERC20 without transfer fees)
uint256 prevBalI = IERC20(tokens[inputTokenIndex]).balanceOf(address(this));
tokens[inputTokenIndex].safeTransferFrom(payer, address(this), totalTransfer); tokens[inputTokenIndex].safeTransferFrom(payer, address(this), totalTransfer);
uint256 balIAfter = IERC20(tokens[inputTokenIndex]).balanceOf(address(this));
require(balIAfter == prevBalI + totalTransfer, "swapMint: non-standard tokenIn");
// Accrue protocol share (floor) from the fee on the input token // Accrue protocol share (floor) from the fee on the input token
uint256 protoShare = 0;
if (protocolFeePpm > 0 && feeUintActual > 0) { if (protocolFeePpm > 0 && feeUintActual > 0) {
uint256 protoShare = (feeUintActual * protocolFeePpm) / 1_000_000; protoShare = (feeUintActual * protocolFeePpm) / 1_000_000;
if (protoShare > 0) { if (protoShare > 0) {
protocolFeesOwed[inputTokenIndex] += protoShare; protocolFeesOwed[inputTokenIndex] += protoShare;
} }
} }
// Update cached balance for the input token to effective onchain - owed // Update cached effective balance directly: add totalTransfer minus protocol share
_recordCachedBalance(inputTokenIndex, balIAfter); cachedUintBalances[inputTokenIndex] += (totalTransfer - protoShare);
// Compute old and new scaled size metrics to determine LP minted // Compute old and new scaled size metrics to determine LP minted
int128 oldTotal = _computeSizeMetric(lmsr.qInternal); int128 oldTotal = _computeSizeMetric(lmsr.qInternal);
require(oldTotal > int128(0), "swapMint: zero total");
uint256 oldScaled = ABDKMath64x64.mulu(oldTotal, LP_SCALE); uint256 oldScaled = ABDKMath64x64.mulu(oldTotal, LP_SCALE);
int128 newTotal = oldTotal.add(sizeIncreaseInternal); int128 newTotal = oldTotal.add(sizeIncreaseInternal);
@@ -405,7 +394,6 @@ contract PartyPoolMintImpl is PartyPoolBase {
// If somehow supply zero (shouldn't happen as lmsr.nAssets>0), mint newScaled // If somehow supply zero (shouldn't happen as lmsr.nAssets>0), mint newScaled
actualLpToMint = newScaled; actualLpToMint = newScaled;
} else { } else {
require(oldScaled > 0, "swapMint: oldScaled zero");
uint256 delta = (newScaled > oldScaled) ? (newScaled - oldScaled) : 0; uint256 delta = (newScaled > oldScaled) ? (newScaled - oldScaled) : 0;
if (delta > 0) { if (delta > 0) {
// floor truncation rounds in favor of pool // floor truncation rounds in favor of pool
@@ -499,7 +487,6 @@ contract PartyPoolMintImpl is PartyPoolBase {
uint256 supply = _totalSupply; uint256 supply = _totalSupply;
require(supply > 0, "burnSwap: empty supply"); require(supply > 0, "burnSwap: empty supply");
require(_balances[payer] >= lpAmount, "burnSwap: insufficient LP");
// alpha = lpAmount / supply as Q64.64 (adjusted for fee) // alpha = lpAmount / supply as Q64.64 (adjusted for fee)
int128 alpha = ABDKMath64x64.divu(lpAmount, supply) // fraction of total supply to burn int128 alpha = ABDKMath64x64.divu(lpAmount, supply) // fraction of total supply to burn
@@ -519,8 +506,9 @@ contract PartyPoolMintImpl is PartyPoolBase {
uint256 feeTokenUint = (payoutGrossUint > amountOutUint) ? (payoutGrossUint - amountOutUint) : 0; uint256 feeTokenUint = (payoutGrossUint > amountOutUint) ? (payoutGrossUint - amountOutUint) : 0;
// Accrue protocol share (floor) from the token-side fee // Accrue protocol share (floor) from the token-side fee
uint256 protoShare = 0;
if (protocolFeePpm > 0 && feeTokenUint > 0) { if (protocolFeePpm > 0 && feeTokenUint > 0) {
uint256 protoShare = (feeTokenUint * protocolFeePpm) / 1_000_000; protoShare = (feeTokenUint * protocolFeePpm) / 1_000_000;
if (protoShare > 0) { if (protoShare > 0) {
protocolFeesOwed[inputTokenIndex] += protoShare; protocolFeesOwed[inputTokenIndex] += protoShare;
} }
@@ -532,18 +520,20 @@ contract PartyPoolMintImpl is PartyPoolBase {
// Burn LP tokens from payer (authorization via allowance) // Burn LP tokens from payer (authorization via allowance)
if (msg.sender != payer) { if (msg.sender != payer) {
uint256 allowed = _allowances[payer][msg.sender]; uint256 allowed = _allowances[payer][msg.sender];
require(allowed >= lpAmount, "burnSwap: allowance insufficient");
_approve(payer, msg.sender, allowed - lpAmount); _approve(payer, msg.sender, allowed - lpAmount);
} }
_burn(payer, lpAmount); _burn(payer, lpAmount);
// Update cached balances by reading on-chain balances for all tokens // Update cached balances using computed payout and protocol fee; no on-chain reads
int128[] memory newQInternal = new int128[](n); int128[] memory newQInternal = new int128[](n);
for (uint256 idx = 0; idx < n; idx++) { for (uint256 idx = 0; idx < n; idx++) {
uint256 bal = IERC20(tokens[idx]).balanceOf(address(this)); uint256 newBal = cachedUintBalances[idx];
cachedUintBalances[idx] = bal; if (idx == inputTokenIndex) {
_recordCachedBalance(inputTokenIndex, bal); // Effective LP balance decreases by net payout and increased protocol owed
newQInternal[idx] = _uintToInternalFloor(bal, bases[idx]); newBal = newBal - amountOutUint - protoShare;
}
cachedUintBalances[idx] = newBal;
newQInternal[idx] = _uintToInternalFloor(newBal, bases[idx]);
} }
// Emit BurnSwap with public-facing info only (do not expose ΔS or LP burned) // Emit BurnSwap with public-facing info only (do not expose ΔS or LP burned)