Compare commits

5 Commits

Author SHA1 Message Date
tim
43fb62c47c Deploy; PartyPoolBalancedPair as separate contract 2025-09-29 18:05:04 -04:00
tim
a43c893609 PoolBase; warnings cleanup 2025-09-29 17:32:09 -04:00
tim
e5b2577ba9 swapMintAmounts 2025-09-29 17:17:23 -04:00
tim
8e69bfac5c PoolLib 2025-09-29 17:07:55 -04:00
tim
77683555e8 removed console logs 2025-09-29 16:40:39 -04:00
29 changed files with 1957 additions and 3077 deletions

View File

@@ -24,14 +24,11 @@ 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 | 132,000 | 143,000 | | 2 | 1 | 146,000 | 149,000 |
| 2* | 1 | 119,000 | 143,000 | | 10 | 45 | 157,000 | 426,000 |
| 10 | 45 | 142,000 | 412,000 | | 20 | 190 | 171,000 | 772,000 |
| 20 | 190 | 157,000 | 749,000 | | 50 | 1225 | 213,000 | 1,810,000 |
| 50 | 1225 | 199,000 | 1,760,000 | | 100 | 4950 | 283,000 | 3,542,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.

View File

@@ -1,551 +0,0 @@
# LMSR-based Multi-Asset AMM
Abstract
We propose a multi-asset automated market maker (AMM) whose pricing kernel is the Logarithmic Market Scoring Rule (LMSR) (Hanson, 2002.) The AMM maintains a 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, $b(\mathbf{q}) = \kappa \, S(\mathbf{q})$ with $S(\mathbf{q}) = \sum_i q_i$ and fixed $\kappa>0$. This choice preserves scale-invariant responsiveness while retaining LMSRs softmax pricing structure under a quasi-static-$b$ view. We derive closed-form expressions for asset-to-asset swaps in the induced two-asset subspace, including exact-in, exact-out, limit-hitting (swap-to-limit), and capped-output variants. We discuss numerical stability techniques (e.g., log-sum-exp reformulations, guarded domains) and a balanced two-asset specialization that enables polynomial approximations with provable error bounds. The protocol is parameter-fixed at deployment (no governance over $\kappa$ or fees), yielding reproducible behavior and transparent depth calibration. Analytical and numerical evidence suggest that this LMSR AMM combines desirable theoretical properties (convexity, path-independent cost differences at constant $b$, multi-asset support) with practical robustness (monotonicity preservation and conservative fallbacks). We outline liquidity operations (proportional and single-asset joins/exits), fee accrual to LPs via state appreciation, and risk considerations.
Keywords
AMM; LMSR; cost-function market maker; multi-asset liquidity; bounded loss; convex optimization; fixed-point arithmetic; numerical stability.
2) Executive Summary
Motivation and problem
- Multi-asset liquidity often faces a trade-off between simplicity (CFMM invariants) and expressivity (risk sharing across many assets). We pursue an AMM that natively supports many assets while delivering predictable depth and strong theoretical guarantees.
Mechanism overview
- The AMMs kernel is the LMSR cost function
$$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}),\;\; S(\mathbf{q})=\sum_i q_i.$$
- Instantaneous marginal price ratios follow the softmax structure under quasi-static $b$:
$$P(\text{base}\to\text{quote}\mid \mathbf{q})=\exp\!\left(\frac{q_{\text{quote}}-q_{\text{base}}}{b(\mathbf{q})}\right).$$
Key properties
- Convex potential and softmax gradient (at constant $b$), multi-asset support via a single potential.
- Path independence of cost differences under constant $b$; pairwise price ratios preserved under $b=\kappa S$ via a common additive gradient term.
- Bounded-loss intuition from LMSR extends proportionally with $b$; scale invariance via $b=\kappa S$.
- Closed forms for two-asset reductions enable exact-in/exact-out and limit-hitting swaps with monotonicity.
Main contributions
- Fixed-parameter LMSR AMM: $b=\kappa S$ with immutable $\kappa$ and fees post-deployment.
- Stability techniques: log-sum-exp evaluation, domain guards, ratio shortcuts, and conservative fallbacks.
- Balanced two-asset optimization: polynomial approximations with error bounds and dispatcher preconditions.
- Evaluation plan: analytical depth comparisons, numerical accuracy, monotonicity/no-negative-arbitrage checks, and gas microbenchmarks.
3) Background and Related Work
AMM landscape
- CFMMs define implicit invariants over reserves. Examples include:
- Constant product (e.g., Uniswap): $x y = k$, offering simple two-asset liquidity and predictable slippage profiles.
- Constant mean (e.g., Balancer): $\prod_i q_i^{w_i} = \text{const}$, generalizing to many assets with weights.
- Stableswap (e.g., Curve): combines constant sum and constant product to target low-slippage near parity.
- LMSR differs by specifying a convex cost function $C(\mathbf{q})$; prices are given by its gradient (or ratios thereof), rather than from a multiplicative invariant.
LMSR primer and adaptation to AMMs
- The classical LMSR potential with constant $b$ is
$$C(\mathbf{q}) = b \log\!\left(\sum_i e^{q_i/b}\right),\qquad \frac{\partial C}{\partial q_i} = \frac{e^{q_i/b}}{\sum_k e^{q_k/b}}.$$
- In our AMM setting, we parameterize $b(\mathbf{q})=\kappa S(\mathbf{q})$ for scale-invariant responsiveness, and use a quasi-static-$b$ view to compute instantaneous price ratios:
$$P(\text{base}\to\text{quote})=\frac{e^{q_{\text{quote}}/b}}{e^{q_{\text{base}}/b}}=\exp\!\left(\frac{q_{\text{quote}}-q_{\text{base}}}{b}\right).$$
- Two-asset reduction yields closed forms for exact-in and exact-out trades (see Sections 56 and Appendix A).
Related LMSR applications and prior DeFi adaptations
- LMSR originates from scoring-rule-based market making in information markets. Subsequent adaptations explored cost-function AMMs and variants that tailor $b$ to achieve targeted responsiveness. We adopt a proportional $b$ tied to total pool size for transparency and predictable scaling across liquidity regimes.
When LMSR is preferable and limitations
- Preferable when:
- Multi-asset exposure and cross-asset risk sharing are primary goals.
- A convex potential with softmax-driven pricing is desired for analytical tractability and monotonic behavior.
- Limitations and design choices:
- With $b=\kappa S$, gradient components include a common additive term; we rely on pairwise ratios for pricing.
- Extreme imbalances can induce steep price moves; capacity caps and domain guards mitigate numerical and economic edge cases.
4) System Model and Notation
4.1 State, assets, and units
- Assets and indices: We consider $n \ge 2$ assets indexed by $i \in \{0,\dots,n-1\}$.
- State vector: $\mathbf{q} = (q_0,\dots,q_{n-1}) \in \mathbb{R}_{\ge 0}^{\,n}$ denotes normalized internal quantities held by the pool. Each $q_i$ is in common “internal units” so cross-asset operations are comparable. In practice, token amounts are scaled to a common unit (e.g., $10^{-\text{decimals}}$ normalization) and represented with fixed-point arithmetic; equations here are stated over reals for clarity.
- Size metric: $S(\mathbf{q}) := \sum_i q_i$. This is the aggregate pool size used both to summarize liquidity and to set the effective liquidity parameter.
- Liquidity parameterization: The effective LMSR liquidity parameter is $b(\mathbf{q}) := \kappa \cdot S(\mathbf{q})$, where $\kappa > 0$ is a fixed, deployment-time constant. Intuitively, $b$ scales linearly with the pools total size, preserving responsiveness under proportional rescaling of $\mathbf{q}$.
- Fees: Let $f_{\text{swap}} \in [0,1)$ denote the swap fee applied at the token layer (separate from the fee-free pricing kernel). Optionally a protocol fee $f_{\text{proto}}$ can be taken as a fraction of the swap fee. All derivations of kernel prices below are fee-free; fees are applied as multiplicative factors to input/output amounts outside the kernel.
4.2 Cost function and prices
- Cost function: We use the LMSR cost function
$$C(\mathbf{q}) \;=\; b(\mathbf{q}) \,\log\!\left(\sum_{i} e^{\,q_i / b(\mathbf{q})}\right).$$
For numerical stability we compute it via a log-sum-exp formulation. Define $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} e^{\,y_i - M} \right).$$
- Price shares (softmax): Define the unnormalized weights $w_i := e^{q_i / b(\mathbf{q})}$ and $W := \sum_i w_i$. The price share of asset $i$ is
$$\pi_i(\mathbf{q}) := \frac{w_i}{W} \;=\; \frac{e^{q_i / b(\mathbf{q})}}{\sum_j e^{q_j / b(\mathbf{q})}}.$$
Note: With $b$ constant, the gradient satisfies $\partial C/\partial q_i = \pi_i$. With $b = \kappa \, S(\mathbf{q})$, $\partial C/\partial q_i$ includes an additive term common across $i$; differences in marginal prices still track $\pi_i$ up to an additive constant, and the pairwise ratios below remain unchanged.
- Pairwise marginal price ratio: The instantaneous marginal price of “base” in units of “quote” is
$$P(\text{base}\to\text{quote}\mid \mathbf{q}) \;=\; \exp\!\left(\frac{q_{\text{quote}} - q_{\text{base}}}{b(\mathbf{q})}\right).$$
This equals $w_{\text{quote}}/w_{\text{base}}$ and is invariant to the common softmax denominator, and remains valid under the quasi-static-$b$ swap model (holding $b$ constant over an infinitesimal trade or a single pricing step; see Appendix A).
4.3 Swap quantities and conventions
- Exact-in: Given an input amount $a \ge 0$ of asset $i$, the fee-free kernel determines the output amount $y \ge 0$ of asset $j$ by integrating the marginal price along the $i\to j$ path with $b$ held quasi-static at its pre-trade value (see Appendix A).
- Exact-out: Given a desired output $y \ge 0$ of asset $j$, the fee-free kernel solves for the required input $a \ge 0$ of asset $i$ (inverse of exact-in).
- Price limits (swap-to-limit): A user can provide a maximum acceptable marginal price ratio $\Lambda > 0$ for $p_i/p_j$. If the marginal price trajectory would exceed $\Lambda$ before consuming the full $a$, the swap truncates at the unique $a_{\text{lim}}$ that reaches $\Lambda$ (see Appendix A).
- Capacity caps: Outputs cannot exceed the available balance of the out-asset; if a formula would produce $y > q_j$, we cap to $q_j$ and solve inversely for the corresponding input.
4.4 Units, scaling, and normalization
- Token decimals: For each token $i$ with decimals $d_i$, on-chain amounts are normalized to a common internal unit so that arithmetic over $\mathbf{q}$ is coherent. Let $s_i$ be the scale factor implied by $d_i$; normalized internal balances are proportional to on-chain token balances via $s_i$.
- Scale invariance: If $\mathbf{q}$ is scaled by $\lambda > 0$ (all assets multiplied by the same $\lambda$), then $S$ scales by $\lambda$ and $b = \kappa S$ scales by $\lambda$; prices as pairwise ratios, $P(\text{base}\to\text{quote})$, are invariant to this rescaling.
4.5 Assumptions
- Tokens are standard fungible assets with deterministic, non-rebasing balances; no transfer fees are embedded at the kernel level.
- Swaps and liquidity operations are atomic; the mechanism is permissionless (subject to access rules at the wrapper layer).
- No external price oracles are required by the kernel; price discovery is endogenous via the cost function.
- Numerical computations are performed in fixed-point with explicit domain guards (see Appendix B).
5) AMM Design and LMSR Formulation
5.1 Convex potential and invariant view
- We model the pool by the LMSR potential
$$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}),\;\; S(\mathbf{q})=\sum_i q_i,$$
which induces a softmax over coordinates of $\mathbf{q}$. For pricing, we operate under a quasi-static-$b$ view for an infinitesimal step, recovering the usual LMSR gradient structure (see Section 6 and Appendix A).
- Intuition: $C$ is a convex potential in $\mathbf{q}$ when $b$ is treated as a constant parameter; gradient components are softmax probabilities. This convexity underpins path independence of cost differences and no-arbitrage properties in the constant-$b$ setting. With $b=\kappa S(\mathbf{q})$, the gradient picks up a common additive term across coordinates; pairwise price ratios, which drive swaps, remain governed by softmax differences (Appendix A).
5.2 Kappa parameterization: $b=\kappa \cdot S$
- Rationale: Choosing $b$ proportional to pool size preserves responsiveness under proportional rescalings of inventory. If all $q_i$ are multiplied by $\lambda>0$, then $S$ and $b$ scale by $\lambda$, while price ratios
$$P(\text{base}\to\text{quote}\mid \mathbf{q}) \;=\; \exp\!\left(\frac{q_{\text{quote}}-q_{\text{base}}}{b(\mathbf{q})}\right)$$
remain invariant.
- Responsiveness: Smaller $\kappa$ yields smaller $b$ for a given $S$, producing steeper price impact (more responsive market); larger $\kappa$ produces deeper liquidity and gentler impact.
- Conformance: The induced prices coincide with the classic LMSR softmax ratios under quasi-static-$b$, ensuring consistency with LMSRs scoring-rule interpretation for marginal moves.
5.3 Choice of $S$ and alternatives
- Default choice: $S(\mathbf{q})=\sum_i q_i$ (the $\ell_1$ size metric) is transparent, easy to compute, and scale-consistent across assets in normalized units.
- Alternatives:
- Weighted sum: $S_w(\mathbf{q})=\sum_i w_i q_i$ for exogenous weights $w_i>0$; may bias depth across assets.
- Quadratic norm: $S_2(\mathbf{q})=(\sum_i q_i^2)^{1/2}$; increases $b$ more when inventory is concentrated, potentially dampening extreme price moves.
- Trade-offs: Simplicity, composability, and transparency argue for $S=\sum_i q_i$. Alternatives can tailor depth but complicate interpretation and comparability.
5.4 Fixed-parameter policy
- The proportionality constant $\kappa$ and fee parameters are set at deployment and remain immutable. This yields reproducible behavior, eliminates governance risk, and allows users to evaluate depth and price impact ex-ante for a given $S$.
5.5 Bounded-loss and capital efficiency implications
- Classical LMSR with constant $b$ admits a bounded-loss guarantee of $b\ln n$ in appropriate units. With $b=\kappa S(\mathbf{q})$, scale invariance implies an instantaneous bound per unit of $S$ that is proportional to $\kappa \ln n$; as liquidity scales, so does absolute depth and the notional bound.
- Capital efficiency: For fixed $\kappa$, larger $S$ linearly increases price depth while preserving price ratios; for fixed $S$, tuning $\kappa$ linearly trades off depth versus responsiveness. The two-asset closed forms in Section 6 quantify this via $y(a)$ and its slope at the origin $y'(0)=\frac{r_0}{1+r_0}$.
6) Pricing and Swap Mechanics
6.1 Instantaneous pricing from the gradient
- Define unnormalized weights $w_i = e^{q_i/b}$ and $W=\sum_k w_k$. Under quasi-static $b$,
$$\pi_i(\mathbf{q})=\frac{\partial C}{\partial q_i}=\frac{w_i}{W},\qquad
\frac{\pi_{\text{quote}}}{\pi_{\text{base}}}=\frac{w_{\text{quote}}}{w_{\text{base}}}=\exp\!\left(\frac{q_{\text{quote}}-q_{\text{base}}}{b}\right).$$
We interpret $P(\text{base}\to\text{quote}) := \pi_{\text{quote}}/\pi_{\text{base}}$ as the instantaneous marginal price ratio.
6.2 Cost differences and asset-to-asset swaps
- Conceptually, an asset-to-asset trade from $i$ to $j$ of sizes $(+a,-y)$ satisfies
$$\Delta C \;=\; C(\mathbf{q} + a\,\mathbf{e}_i - y\,\mathbf{e}_j) - C(\mathbf{q}),$$
and the two-asset reduction with quasi-static $b$ yields a closed-form relation between $a$ and $y$ (Appendix A):
$$y(a) \;=\; b \,\ln\!\Big( 1 + r_0 \,\big(1 - e^{-a/b}\big) \Big),\qquad r_0 := \exp\!\left(\frac{q_i - q_j}{b}\right).$$
- The inverse exact-out mapping for a target $y$ is
$$a(y) \;=\; b \,\ln\!\left(\frac{r_0}{\,r_0 + 1 - e^{\,y/b}\,}\right).$$
6.3 Limit-hitting swaps and swap-to-limit
- For a user-specified price limit $\Lambda > 0$ on $p_i/p_j$, the marginal price trajectory $r(t)=r_0 e^{t/b}$ hits the limit at
$$a_{\text{lim}} \;=\; b \,\ln\!\left(\frac{\Lambda}{r_0}\right),\qquad
y_{\text{lim}} \;=\; b \,\ln\!\Big( 1 + r_0 \,\big(1 - r_0/\Lambda\big) \Big).$$
- Capacity cap: If $y_{\text{lim}} > q_j$, cap to $y=q_j$ and use the inverse mapping to compute the implied input
$$a_{\text{cap}} \;=\; b \,\ln\!\left(\frac{r_0}{\,r_0 + 1 - e^{\,q_j/b}\,}\right).$$
6.4 Properties: monotonicity, uniqueness, and stability
- Monotonicity and uniqueness: For feasible states, $y(a)$ is strictly increasing and concave in $a$; the inverse $a(y)$ is strictly increasing and convex in $y$. The limit-hitting $a_{\text{lim}}$ is unique when $\Lambda>r_0$.
- Numerical stability: Evaluations use log-sum-exp style reformulations, direct ratio formation for $r_0$, and argument guards for $\exp$ and $\ln$. Domain checks ensure denominators (e.g., $r_0 + 1 - e^{y/b}$) remain positive. See Appendix B for details.
7) Liquidity Operations
7.1 Pool initialization and bootstrap
- Let the seed inventory be $\mathbf{q}^{(0)} \in \mathbb{R}_{\ge 0}^{\,n}$ with $S^{(0)} := \sum_i q_i^{(0)} > 0$. The effective liquidity is $b^{(0)} = \kappa\,S^{(0)}$.
- LP supply: We take LP supply proportional to the size metric, $L := \eta\,S(\mathbf{q})$, with a fixed conversion $\eta>0$. Without loss of generality we set $\eta=1$ so that
$$L \;=\; S(\mathbf{q}) \;=\; \sum_i q_i.$$
At bootstrap, the seeder mints $L^{(0)} = S^{(0)}$ LP shares against $\mathbf{q}^{(0)}$.
- LP price in units of asset $k$: Define the marginal value of one unit of $S$ in asset $k$ as
$$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),$$
which aggregates the marginal exchange rates from each asset into $k$ (cf. Section 6).
7.2 Proportional deposit (mint)
- A proportional deposit scales all coordinates by $(1+\alpha)$ for some $\alpha \ge 0$:
$$\mathbf{q}' \;=\; (1+\alpha)\,\mathbf{q},\qquad \Delta q_i \;=\; \alpha\,q_i.$$
- Minted LP shares are linear in the size-metric increase:
$$\Delta L \;=\; L' - L \;=\; S(\mathbf{q}') - S(\mathbf{q}) \;=\; \alpha\,S(\mathbf{q}).$$
- The post-deposit liquidity is $b'=\kappa\,S(\mathbf{q}')=(1+\alpha)\,b$.
7.3 Single-asset deposit (exact-in)
- A contributor provides amount $a$ of asset $i$ and receives a proportional growth $\alpha \ge 0$ such that the system state can be rebalanced to $(1+\alpha)\,\mathbf{q}$ by swapping from $i$ into $j\ne i$ along the fee-free kernel. For each $j \ne i$, target out-amount $y_j := \alpha\,q_j$ requires input
$$x_j(\alpha) \;=\; b \,\ln\!\left(\frac{r_{0,j}}{\,r_{0,j} + 1 - e^{\,y_j/b}\,}\right),\qquad
r_{0,j} \;:=\; \exp\!\left(\frac{q_i - q_j}{b}\right).$$
- The total input required to realize proportional growth $\alpha$ is
$$a_{\text{req}}(\alpha) \;=\; \alpha\,q_i \;+\; \sum_{j\ne i} x_j(\alpha).$$
- The minted shares are
$$\Delta L \;=\; \alpha\,S(\mathbf{q}),$$
where $\alpha$ is the unique solution to $a_{\text{req}}(\alpha)=a$ on its feasible domain (see Appendix A.5).
7.4 Multi-asset deposit (arbitrary vector)
- Given a deposit vector $\mathbf{a} \in \mathbb{R}_{\ge 0}^{\,n}$, decompose it into:
- a proportional component $\bar{\alpha} := \min_i \{ a_i / q_i \}$ (with convention $a_i/q_i=+\infty$ if $q_i=0$ and $a_i>0$), which mints $\bar{\alpha}\,S(\mathbf{q})$ shares and updates $\mathbf{q}$ proportionally, and
- residuals $\tilde{\mathbf{a}} := \mathbf{a} - \bar{\alpha}\,\mathbf{q}$ that can be contributed via single-asset deposit(s) using 7.3 (sequence or batching).
- The total minted shares are additive in the realized proportional growths:
$$\Delta L \;=\; \left(\bar{\alpha} + \sum_{m} \alpha_m\right)\,S(\mathbf{q})$$
where each $\alpha_m$ solves $a_{\text{req}}(\alpha_m)$ for a residual leg.
7.5 Proportional withdrawal (burn)
- Burning $\Delta L$ LP shares effects a proportional redemption with factor
$$\alpha \;=\; \frac{\Delta L}{S(\mathbf{q})} \in (0,1],\qquad \mathbf{q}' \;=\; (1-\alpha)\,\mathbf{q}.$$
- The holder receives $\alpha\,q_i$ units of each asset $i$; equivalently, $L' = L - \Delta L = S(\mathbf{q}')$ and $b'=(1-\alpha)\,b$.
7.6 Single-asset withdrawal (exact-out)
- A holder burns $\Delta L$ shares (i.e., $\alpha=\Delta L/S(\mathbf{q})$) and requests payout exclusively in asset $i$. Starting from $\mathbf{q}_\text{local}=(1-\alpha)\,\mathbf{q}$, for each $j\ne i$:
- withdraw $\alpha\,q_j$ units of $j$, and
- swap $j \to i$ along the fee-free kernel using the two-asset closed form. The candidate out-amount is
$$y_{j\to i} \;=\; b \,\ln\!\Big( 1 + r_{0,j}\,\big(1 - e^{-a_j/b}\big) \Big),\quad
a_j := \alpha\,q_j,\quad r_{0,j} := \exp\!\left(\frac{q^{\text{local}}_j - q^{\text{local}}_i}{b}\right),$$
with $b=\kappa\,S(\mathbf{q})$ evaluated at pre-burn or quasi-static local state.
- If the computed $y_{j\to i}$ would exceed $q^{\text{local}}_i$, cap to capacity and invert to solve the implied input (Appendix A.6). The total single-asset payout is
$$Y_i \;=\; \alpha\,q_i \;+\; \sum_{j\ne i} y_{j\to i}.$$
7.7 Share issuance, pricing, and dilution
- With $L=S$, share issuance is linear in the size-metric; proportional joins/exits preserve relative ownership. The instantaneous LP price in units of asset $k$ is $P_L^{(k)}(\mathbf{q})$ from 7.1. Under joins, $P_L^{(k)}$ remains unchanged for proportional deposits; under single-asset joins, it adjusts according to the realized rebalancing path.
7.8 Fee accrual to LPs and value capture
- Swap fees are taken at the token layer and retained in the pool balances, increasing $S(\mathbf{q})$ relative to $L$ and thereby raising $P_L^{(k)}$ for all $k$. This constitutes implicit fee accrual to LPs via state appreciation rather than explicit distributions.
7.9 Edge cases and operational notes
- Tiny liquidity: When $S$ is small, $b=\kappa S$ is small and price impact is steep; deployments SHOULD enforce a minimum bootstrap $S^{(0)}$ and/or minimum minted $L^{(0)}$.
- Extreme imbalances: As some $q_j \to 0$, price ratios $\exp((q_{\text{quote}}-q_{\text{base}})/b)$ can become large; capacity caps ($y \le q_j$) and positivity checks on logarithm arguments ensure safe evaluation.
- Asset additions/removals: Changing the asset set alters $n$ and the pricing manifold. Deployments typically fix the asset universe; adding/removing assets is best handled via new pool instances with fresh initialization.
8) Fees and Incentives (Static Parameters)
8.1 Fee model and placement
- Let the swap fee rate be $f_{\text{swap}} \in [0,1)$, and let the protocol capture a fraction $\phi \in [0,1]$ of that fee (so LPs receive the remaining $1-\phi$ share via state appreciation).
- We apply fees outside the fee-free pricing kernel. For an exact-in trade with submitted input $a$ on asset $i$, the effective kernel input is
$$a_{\text{eff}} \;=\; (1 - f_{\text{swap}})\,a.$$
The fee amount is $a - a_{\text{eff}}$, of which $\phi\,(a - a_{\text{eff}})$ accrues to the protocol and $(1-\phi)\,(a - a_{\text{eff}})$ to LPs (retained in the pool state).
- The fee-free kernel computes the out-amount $y_{\text{ker}}$ using $a_{\text{eff}}$; the user receives $y_{\text{user}} = y_{\text{ker}}$ (or a fee-adjusted variant if fees are taken from output instead). The invariant and closed forms remain unaffected because pricing is computed on $a_{\text{eff}}$.
8.2 Economic impact
- Effective price and slippage: With input-side fees, the users effective marginal price scales by $(1 - f_{\text{swap}})^{-1}$ for small trades; total slippage decomposes into kernel slippage (from the LMSR curve) plus a constant offset due to fees.
- LP returns: Fees retained in the pool increase $S(\mathbf{q})$ relative to outstanding $L$ and thus raise LP share value. Protocol revenue scales with $\phi$ and trade flow; LP revenue scales with $(1-\phi)$.
8.3 Static-parameter policy (immutability)
- Parameters $\kappa$, $f_{\text{swap}}$, and $\phi$ are set at deployment and are immutable thereafter. Benefits include:
- Predictability: depth and fee impact are ex-ante auditable for a given $S$.
- Governance minimization: no discretionary levers to be toggled post-deployment.
- Composability: integrators can rely on stable behavior across time.
9) Risk Analysis and Theoretical Properties
9.1 Convexity and path independence
- With constant $b$, $C(\mathbf{q}) = b\log\!\big(\sum_i e^{q_i/b}\big)$ is convex and cost differences are path independent:
$$\Delta C \;=\; C(\mathbf{q}+\Delta\mathbf{q}) - C(\mathbf{q}) \quad \text{depends only on the endpoints}.$$
- With $b=\kappa S(\mathbf{q})$, $\partial C/\partial q_i$ includes a common additive term (Section 4); pairwise ratios
$$P(\text{base}\to\text{quote}\mid \mathbf{q}) \;=\; \exp\!\left(\frac{q_{\text{quote}}-q_{\text{base}}}{b(\mathbf{q})}\right)$$
remain valid under a quasi-static-$b$ view, which is the pricing lens used for infinitesimal (or discretized) steps.
9.2 Bounded loss (intuition) and capital efficiency
- For constant $b$, the classic LMSR worst-case loss is $b\ln n$ in the payout numéraire. Under $b=\kappa S$, this scales proportionally with $S$, giving an instantaneous per-unit-$S$ bound proportional to $\kappa \ln n$.
- Capital efficiency follows from linear scaling: increasing $S$ (or $\kappa$) linearly deepens liquidity. The two-asset exact-in form
$$y(a) \;=\; b\,\ln\!\Big(1 + r_0(1 - e^{-a/b})\Big)$$
exhibits $y'(0)=\frac{r_0}{1+r_0}$ and curvature $\frac{\mathrm{d}^2 y}{\mathrm{d}a^2}<0$, quantifying the marginal depth and diminishing returns for larger $a$.
9.3 Sensitivity to $b$ and reserve scales
- Scale invariance: If $\mathbf{q}\mapsto \lambda \mathbf{q}$, then $S\mapsto \lambda S$, $b\mapsto \lambda b$, and $P(\text{base}\to\text{quote})$ is unchanged. Thus depth in notional terms scales linearly with $\lambda$.
- As $b$ increases (via larger $S$ or $\kappa$), the function $y(a)$ becomes less curved (greater depth), reducing slippage for a given input size $a$.
9.4 Failure modes and mitigations
- Thin liquidity: Small $S$ implies small $b$, steep impact, and sensitivity to large orders. Mitigations: enforce minimum bootstrap $S^{(0)}$, external routing safeguards, and user-specified price limits $\Lambda$.
- Extreme concentration: As some $q_j \to 0$, prices can become very large; capacity caps ($y\le q_j$) and limit-hitting logic prevent pathological outputs.
- Numerical edge cases: Guard $\exp$/$\ln$ domains, ensure denominators like $r_0 + 1 - e^{y/b} > 0$, and prefer ratio-based computations (Appendix B).
- No-arbitrage hygiene: Maintain monotonicity of $y(a)$ and its inverse $a(y)$; avoid rounding that could create free lunches (see Section 12).
10) Numerical Methods and Implementation Considerations
10.1 Fixed-point arithmetic and precision policy
- Representation: Quantities are computed in fixed-point; equations are presented over reals for clarity. Let $F$ denote the fractional precision (bits or decimal places).
- Range limits: For stability of exponentials and logarithms, enforce
$$|x| \le X_{\max} \quad \text{when evaluating } \exp(x),\qquad u > 0 \quad \text{when evaluating } \ln(u).$$
Practical choices take $X_{\max}$ large enough to cover the operating envelope while preventing overflow.
- Rounding: Use round-toward-zero or round-to-nearest consistently, prioritizing order-preservation (see 10.6). Avoid mixed rounding modes within a single expression.
10.2 Stable exp/log evaluation (log-sum-exp)
- Cost and shares are evaluated via a log-sum-exp recentering. Define $y_i := q_i/b$, $M := \max_i y_i$, then
$$C(\mathbf{q}) \;=\; b\left(M + \log \sum_i e^{\,y_i - M}\right),\qquad
\pi_i \;=\; \frac{e^{\,y_i - M}}{\sum_k e^{\,y_k - M}}.$$
Centering at $M$ prevents overflow/underflow when $y_i$ are far apart.
- Ratio formation: Compute ratios directly to avoid extra $\exp$/$\ln$ where possible, e.g.
$$r_0 \;=\; \exp\!\left(\frac{q_i - q_j}{b}\right)$$
rather than $e^{q_i/b}/e^{q_j/b}$.
10.3 Reformulations for numerical stability
- Use $\ln(1+u)$ and $e^x-1$ style identities for small arguments:
$$\ln(1+u) \approx u - \frac{u^2}{2} \quad (|u|\ll 1),\qquad e^{x}-1 \approx x + \frac{x^2}{2} \quad (|x|\ll 1),$$
switching to series forms when $|u|$ or $|x|$ are below thresholds to reduce cancellation.
- Inverse mapping stability: For exact-out inversion
$$a(y) \;=\; b \,\ln\!\left(\frac{r_0}{\,r_0 + 1 - e^{\,y/b}\,}\right),$$
compute $E:=e^{y/b}$ once; if $E\approx 1$, use series for $E-1$ to avoid subtractive cancellation.
- Precompute reciprocals: Cache $b^{-1}$ to replace divisions by multiplications and reduce dispersion.
10.4 Algorithm selection, termination, and convergence
- Closed forms: Prefer the exact two-asset formulas for exact-in and exact-out when applicable (Sections 6 and A.2A.3).
- Root-finding for proportional joins: Solve $a_{\text{req}}(\alpha)=a$ via bracketing and bisection on a monotone map:
$$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).$$
Terminate when the interval width is below $\varepsilon$ or the function gap is within tolerance. Monotonicity guarantees uniqueness.
- Limit-hitting: Compute $a_{\text{lim}}=b\ln(\Lambda/r_0)$ directly; validate $\Lambda>r_0$ and that intermediate expressions stay in-range.
10.5 Performance vs precision trade-offs
- Caching: Reuse common subexpressions (e.g., $b$, $b^{-1}$, $r_{0,j}$) across loops and iterative steps.
- Operation count: Prefer fused operations and single-pass accumulations (e.g., recentered $\sum e^{\cdot}$ with on-the-fly rescaling).
- Approximation regions: In designated near-balanced regimes (Section 11), switch to polynomial approximations to avoid transcendental calls, respecting global error budgets.
10.6 Error analysis, monotonicity, and arbitrage-safety
- Error budgeting: Allocate a maximum relative error $\epsilon_{\text{rel}}$ to each primitive ($\exp$, $\ln$, polynomials), ensuring the composed map (e.g., $y(a)$) meets end-to-end bounds.
- Monotonicity preservation: Ensure numerical implementations of $y(a)$ are strictly increasing and of $a(y)$ are strictly increasing by:
- enforcing positive denominators (e.g., $r_0 + 1 - e^{y/b} > 0$),
- clamping intermediate “inner” terms to $(0,\infty)$,
- preferring formulations without subtractive cancellation near boundaries.
- Arbitrage hygiene: Use conservative branches (cap-and-invert) when near capacity or domain boundaries to avoid nonphysical outputs (negative or exceeding balances).
11) BalancedPair Optimization (Dedicated Section)
11.1 Applicability conditions
- Define $\delta := (q_i - q_j)/b$ and $\tau := a/b$. The balanced regime is characterized by
$$|\delta| \le \delta_\star,\qquad |\tau| \le \tau_\star,$$
with design thresholds $(\delta_\star,\tau_\star)$ chosen so that polynomial approximations meet the global error budget while preserving monotonicity.
11.2 Balanced 2-asset closed form and small-argument expansions
- The exact two-asset mapping is
$$y(a) \;=\; b \,\ln\!\Big(1 + r_0 (1 - e^{-a/b})\Big),\qquad r_0=e^{\delta}.$$
- For $|\delta|\ll 1$ and $|\tau|\ll 1$, use expansions
$$e^{\pm x} \approx 1 \pm x + \frac{x^2}{2},\qquad \ln(1+u) \approx u - \frac{u^2}{2},$$
yielding
$$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),\quad r_0 \approx 1 + \delta + \frac{\delta^2}{2}.$$
- Symmetry: At $\delta=0$, the mapping satisfies $y(a)\approx \tfrac{a}{2} - \tfrac{a^2}{4b} + \cdots$, reflecting equal liquidity on both sides.
11.3 Polynomial approximations without $\exp/\ln$
- Construct minimax polynomials $P_d(x)\approx e^{x}$ on $[-\tau_\star,0]$ and $Q_d(u)\approx \ln(1+u)$ on $[0,u_\star]$, where $u_\star$ is induced by the range of $r_0(1-e^{-\tau})$ in the regime.
- Compose
$$\tilde{y}(a) \;=\; b \, Q_d\!\Big( r_0 \,\big(1 - P_d(-\tau)\big) \Big),\qquad \tau=\frac{a}{b},$$
with $r_0$ optionally approximated by a low-degree polynomial in $\delta$ when $|\delta|\le \delta_\star$.
- Error bounds:
$$\big|e^{x}-P_d(x)\big| \le e^{\tau_\star}\,\frac{\tau_\star^{\,d+1}}{(d+1)!},\qquad
\big|\ln(1+u)-Q_d(u)\big| \le \frac{u^{\,d+1}}{(d+1)\,(1-u)^{\,d+1}},$$
ensuring $\big|y(a)-\tilde{y}(a)\big| \le \epsilon$ for a target $\epsilon$ via appropriate $d$, $\delta_\star$, $\tau_\star$, $u_\star$.
11.4 Dispatcher logic and safe fallback
- Preconditions:
- check $|\delta|\le \delta_\star$, $|a|/b \le \tau_\star$, and positivity of intermediate “inner” terms,
- ensure capacity is respected ($\tilde{y}(a)\le q_j$) or switch to cap-and-invert branch.
- If any precondition fails, fall back to the general closed-form path with full transcendental evaluations.
- Price-limit compatibility: When a price limit $\Lambda$ is set, verify that the approximated trajectory respects $r(t)\le \Lambda$; otherwise, revert to exact limit-hitting computation $a_{\text{lim}}=b\ln(\Lambda/r_0)$.
11.5 Invariant preservation and monotonicity guarantees
- Enforce $\tilde{y}'(a) > 0$ on the approximation domain by design (choose $P_d$, $Q_d$ that are monotonically increasing on their intervals and validate numerically).
- Guard inner arguments to keep them in $(0,\infty)$, preventing nonphysical outputs or $\ln$ domain violations.
- Capacity and nonnegativity: Clamp to $[0, q_j]$ and use inverse mapping to reconcile inputs in cap branches.
11.6 Gas and performance analysis
- Eliminating transcendental calls in the balanced regime reduces cost to a small fixed number of polynomial evaluations and multiplications.
- Dispatcher overhead is minimal (few comparisons and a couple of scaled differences). Overall, the optimization provides substantial speedups in near-parity trades while maintaining accuracy guarantees.
- The fallback ensures worst-case performance is bounded by the general path.
12) Protocol Safety: Numerical and Invariant Guarantees
12.1 Invariant checks and fail-fast conditions
- Domain guards:
- Size metric: $S(\mathbf{q}) = \sum_i q_i > 0$, hence $b=\kappa S > 0$.
- Valid indices and nonnegative inputs/outputs for user-facing operations.
- Exponential and logarithm arguments within bounded, valid domains; prefer log-sum-exp recentering.
- Capacity and limit checks:
- Out-amounts are capped by available balances ($y \le q_j$).
- Price-limit trades enforce $\Lambda > r_0$ and truncate at $a_{\text{lim}}=b\ln(\Lambda/r_0)$.
- Consistency guards:
- Denominators (e.g., $r_0 + 1 - e^{y/b}$) must be positive.
- Reciprocal quantities (e.g., $1/b$) are computed once and reused to avoid drift.
12.2 Precision-induced error handling and rounding
- Monotonicity-first policy: Prefer formulations that preserve order (e.g., log-sum-exp, ratio formation for $r_0$).
- Conservative rounding: When a decision boundary is approached (e.g., inner argument of $\ln$ near zero), choose the conservative branch (cap-and-invert) rather than extrapolating.
- Bounded evaluations: Enforce $|x| \le X_{\max}$ for $\exp(x)$ to prevent overflow; clamp inputs that would violate this bound and surface clear errors to callers.
12.3 Verification targets
- Structural properties:
- Convexity under constant $b$; gradient softmax identities.
- Pairwise price ratios consistent with $P(\text{base}\to\text{quote})$ under quasi-static $b$.
- Numerical properties:
- Monotonicity of $y(a)$ and $a(y)$, uniqueness of $a_{\text{lim}}$ when $\Lambda>r_0$.
- Path-independence of $\Delta C$ in constant-$b$ tests; bounded relative error within predefined budgets.
12.4 Testing approach
- Property-based tests across randomized states $\mathbf{q}$, input sizes, and asset pairs, including adversarial edge cases (tiny $S$, extreme $r_0$, near-capacity).
- Boundary tests for domain guards (e.g., $S\downarrow 0$, $\Lambda \downarrow r_0$, inner argument of $\ln$ near zero).
- Differential tests against high-precision reference implementations for $y(a)$, $a(y)$, and price ratios.
- No-negative-arbitrage checks under rounding: ensure discrete effects cannot be exploited for profit with zero risk.
13) Deployment Model and Parameter Fixity
13.1 Immutable parameters and non-upgradability
- The pool is deployed with a fixed asset set and immutable parameters $(\kappa, f_{\text{swap}}, \phi)$, where $\kappa>0$ determines $b(\mathbf{q})=\kappa S(\mathbf{q})$, $f_{\text{swap}}$ is the swap fee rate, and $\phi$ is the protocol share of fees.
- Contracts are deployed in a non-upgradable, ownerless configuration. No governance can modify $\kappa$, fees, or the asset universe after deployment.
13.2 Deployment inputs and initialization
- Deployment specifies: the asset list, normalization conventions, and parameter tuple $(\kappa, f_{\text{swap}}, \phi)$. Initialization requires a seed inventory $\mathbf{q}^{(0)}$ with $S^{(0)}=\sum_i q_i^{(0)}>0$, yielding initial liquidity
$$b^{(0)} \;=\; \kappa \, S^{(0)}.$$
- A minimum bootstrap size $S^{(0)}$ SHOULD be enforced to avoid thin-liquidity regimes at genesis.
13.3 Reproducibility and transparency
- Given $(\kappa, f_{\text{swap}}, \phi)$ and the initial state $\mathbf{q}^{(0)}$, the AMMs pricing map is fully determined for all subsequent states via
$$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}).$$
- Observability: Public views expose instantaneous price ratios $P(\text{base}\to\text{quote})$, price shares $\pi_i$, LP price $P_L^{(k)}$, and size metric $S(\mathbf{q})$.
13.4 Operational implications
- Asset changes: To add or remove assets, deploy a new pool instance and migrate liquidity; existing pools remain immutable.
- Ecosystem integration: The fixed-parameter model simplifies routing and valuation, enabling integrators to precompute depth profiles for anticipated $S$ regimes.
- Risk discipline: The absence of admin levers eliminates governance risk but requires careful parameter selection at deployment.
15) Limitations and Future Work
15.1 Limitations
- Quasi-static $b$ assumption: Pricing steps treat $b$ as locally constant. While pairwise ratios are exact for infinitesimal moves, finite trades are evaluated with closed forms derived from the two-asset reduction; model fidelity remains high but is not a full global-gradient integration under state-dependent $b$.
- Thin-liquidity vulnerability: For small $S$, $b=\kappa S$ is small and price impact is steep; users should rely on price limits $\Lambda$ and integrators should enforce minimum bootstrap sizes.
- Extreme concentration and domain edges: As some $q_j \to 0$, ratios $\exp((q_{\text{quote}}-q_{\text{base}})/b)$ become large; capacity caps and domain guards prevent nonphysical outcomes but can truncate trades.
- Expressivity: The mechanism is not tuned for pegged pairs like stableswap, nor does it encode cross-asset correlations by default.
- Numerical constraints: Fixed-point range/precision, exponential/log bounds, and approximation regimes impose domain restrictions for safe operation.
- Static parameters: Immutability removes governance agility; mis-specified $\kappa$ or fees require new deployments.
15.2 Future work
- Adaptive proportionality: Explore principled adaptive $\kappa$ while preserving LMSR properties (e.g., bounded-loss analogues) and avoiding governance risk (e.g., rule-based or oracle-free triggers).
- Correlated or basket-targeted variants: Incorporate weighted size metrics $S_w(\mathbf{q})$ or correlation-aware formulations, with rigorous analysis of price and risk implications.
- Enhanced approximations: Extend polynomial or rational approximations with verified remainder bounds, larger safe domains, and automatic dispatchers with certified monotonicity.
- Off-chain assists and proofs: Use off-chain computation for heavy numerical routines with on-chain verification (e.g., succinct proofs) while maintaining transparency.
- MEV-aware design: Integrate user-settled price limits, batch auctions, or commit-reveal to mitigate adverse selection and sandwich risk.
- Layer-2 deployment: Leverage lower fees and faster settlement to widen the safe domain for numerical precision and to support more assets.
16) Conclusion
- We presented a multi-asset AMM whose pricing kernel is the LMSR cost function with an effective liquidity parameter proportional to pool size, $b(\mathbf{q})=\kappa S(\mathbf{q})$. This delivers scale-invariant responsiveness, preserves softmax-derived pairwise price ratios, and supports any-to-any swaps via a single convex potential.
- Closed-form two-asset reductions provide exact-in, exact-out, and limit-hitting formulas with strong monotonicity and uniqueness properties, while capacity caps and conservative inverses ensure safety at domain boundaries. Liquidity operations (proportional and single-asset joins/exits) follow directly from the same potential framework.
- A fixed-parameter policy eliminates governance risk and makes depth calibration transparent. Numerical stability is achieved through log-sum-exp reformulations, guarded transcendental domains, and optional balanced-regime polynomial approximations with error bounds.
- Outlook: This LMSR AMM complements CFMMs by offering multi-asset price discovery under a convex potential with predictable scaling. Future work includes adaptive yet governance-minimized responsiveness, correlation-aware variants, and verifiable off-chain assists—aimed at retaining theoretical guarantees while broadening applicability.
17) Appendices
A. Full derivations and proofs
A.1 Gradient, price shares, and pairwise prices
- With $b$ treated as a constant parameter, the LMSR cost $C(\mathbf{q}) = b \log\!\big(\sum_k e^{q_k/b}\big)$ yields
$$\frac{\partial C}{\partial q_i} \;=\; \frac{e^{q_i/b}}{\sum_k e^{q_k/b}} \;=\; \pi_i(\mathbf{q}).$$
- With $b = \kappa \, S(\mathbf{q})$, the total derivative becomes
$$\frac{\partial C}{\partial q_i} \;=\; A(\mathbf{q}) \;+\; \frac{e^{q_i/b}}{\sum_k e^{q_k/b}},$$
where $A(\mathbf{q})$ is an additive term common to all $i$ that arises from $\partial b/\partial q_i$. Consequently, differences between marginal prices are preserved, and the pairwise marginal price ratio reduces to
$$P(\text{base}\to\text{quote}\mid \mathbf{q}) \;=\; \exp\!\left(\frac{q_{\text{quote}} - q_{\text{base}}}{b(\mathbf{q})}\right).$$
This is the exchange rate used by the kernel, under the quasi-static-$b$ assumption.
A.2 Two-asset closed form: exact-in
- Consider a swap from $i$ (in) to $j$ (out) with quasi-static $b$. Let $r_0 := \exp\!\big((q_i - q_j)/b\big)$. Along the trade path, the instantaneous marginal price ratio evolves as $r(t) = r_0 \, e^{t/b}$, where $t$ is the cumulative input of asset $i$.
- The infinitesimal output satisfies
$$\mathrm{d}y \;=\; \frac{r(t)}{1 + r(t)} \,\mathrm{d}t,$$
in the two-asset reduction induced by the LMSR gradient. Integrating from $0$ to $a$ yields the closed form
$$y(a) \;=\; b \,\ln\!\Big( 1 + r_0 \,\big(1 - e^{-a/b}\big) \Big).$$
- Properties: $y(0) = 0$, $y'(0) = \frac{r_0}{1 + r_0}$, $y$ is increasing and concave in $a$, and $\lim_{a\to\infty} y = b \,\ln(1 + r_0)$.
A.3 Two-asset closed form: exact-out (inverse)
- Given $y \ge 0$, invert the relation in A.2. Let $E := e^{y/b}$. Then
$$1 + r_0 \,\big(1 - e^{-a/b}\big) \;=\; E
\;\Rightarrow\; e^{-a/b} \;=\; 1 - \frac{E - 1}{r_0} \;=\; \frac{r_0 + 1 - E}{r_0}$$
$$\Rightarrow\quad a(y) \;=\; -\,b \,\ln\!\left(\frac{r_0 + 1 - E}{r_0}\right) \;=\; b \,\ln\!\left(\frac{r_0}{\,r_0 + 1 - E\,}\right).$$
- This inverse exists and is unique for $y \in \big[0,\, b \ln(1 + r_0)\big]$.
A.4 Limit-hitting and swap-to-limit
- Let $\Lambda > 0$ be a maximum acceptable marginal price ratio for $p_i/p_j$. With $r(t) = r_0 \, e^{t/b}$, the limit is reached when $r(t) = \Lambda$, giving the unique truncated input
$$a_{\text{lim}} \;=\; b \,\ln\!\left(\frac{\Lambda}{r_0}\right).$$
- The corresponding output at the limit is
$$y_{\text{lim}} \;=\; b \,\ln\!\Big( 1 + r_0 \,\big(1 - e^{-a_{\text{lim}}/b}\big) \Big)
\;=\; b \,\ln\!\Big( 1 + r_0 \,\big(1 - r_0/\Lambda\big) \Big).$$
- If $y_{\text{lim}}$ exceeds the available out-asset balance $q_j$, cap output to $q_j$ and solve for the input required to realize $y = q_j$ using the inverse formula of A.3:
$$a_{\text{cap}} \;=\; b \,\ln\!\left(\frac{r_0}{\,r_0 + 1 - e^{\,q_j / b}\,}\right).$$
A.5 Single-asset mint via proportional growth (exact-in to many)
- Suppose a user contributes amount $a$ of asset $i$ and wishes to increase the pool proportionally by factor $1 + \alpha$ (with $\alpha \ge 0$). For each $j \ne i$, let $y_j := \alpha \, q_j$ be the target out-amount in $j$ when swapping from $i$. Define $r_{0,j} := \exp\!\big((q_i - q_j)/b\big)$. From A.3, the input required to realize $y_j$ is
$$x_j(\alpha) \;=\; b \,\ln\!\left(\frac{r_{0,j}}{\,r_{0,j} + 1 - e^{\,y_j / b}\,}\right).$$
- The self-asset contribution is $\alpha \, q_i$. The total input required is
$$a_{\text{req}}(\alpha) \;=\; \alpha \, q_i \;+\; \sum_{j \ne i} x_j(\alpha).$$
- Solve for $\alpha$ via a monotone root-find on $a_{\text{req}}(\alpha) = a$. The function is increasing in $\alpha$ on its domain, with unique solution when feasible.
A.6 Single-asset burn via proportional redemption
- Burning a proportional share $\alpha \in (0, 1]$ reduces the pool balances to $(1 - \alpha)\,\mathbf{q}$. A single-asset payout in asset $i$ aggregates (i) the direct $\alpha \, q_i$ redemption and (ii) the swaps from each asset $j \ne i$ of their redeemed $\alpha \, q_j$ portions into $i$ using A.2 with the local (post-burn) state. If a computed out-amount would exceed the local $q_i$, cap to capacity and solve the inverse for the input used.
A.7 Balanced 2-asset special case and polynomial approximations
- Near balance, define $\delta := (q_i - q_j)/b$ with $|\delta| \ll 1$ and let $a/b$ be small. Using second-order expansions:
$$e^{\pm x} \approx 1 \pm x + \frac{x^2}{2},\qquad \ln(1 + u) \approx u - \frac{u^2}{2},$$
we obtain for small $a/b$ and $\delta$:
$$r_0 = e^{\delta} \approx 1 + \delta + \frac{\delta^2}{2},$$
$$y(a) = b \,\ln\!\big(1 + r_0(1 - e^{-a/b})\big)
\approx b \left[ r_0 \left(\frac{a}{b}\right) - \frac{1}{2} r_0 \left(\frac{a}{b}\right)^2 \right]
+ \mathcal{O}\!\left(\left(\frac{a}{b}\right)^3,\, \delta \left(\frac{a}{b}\right)^2\right).$$
- In particular, when $\delta \approx 0$,
$$y(a) \approx \frac{a}{2} - \frac{a^2}{4b} + \cdots,$$
which admits efficient evaluation via fixed low-degree polynomials. This motivates a “balanced pair” dispatcher that, under explicit near-balance preconditions ($|\delta| \le \delta_\star$ and $a/b \le \tau_\star$), uses minimax Chebyshev polynomials for $\exp$ and $\ln$ on compact intervals to meet a specified error budget, and otherwise falls back to the general path.
B. Error bounds and approximation details
B.1 Fixed-point arithmetic and stability policy
- Representation: All quantities are computed in fixed-point with a wide fractional field; equations are written over reals for exposition. Overflow/underflow and domain errors are prevented with explicit guards.
- Log-sum-exp: The cost is evaluated as $C = b \left(M + \log \sum_i e^{\,y_i - M}\right)$ with $y_i := q_i/b$ and $M := \max_i y_i$. This ensures stable accumulation even when some $y_i$ are far apart.
- Exponential guard: The arguments to $\exp(\cdot)$ are restricted to a bounded interval to ensure finite, monotone outputs. A practical bound is $|x| \le 32$ (in internal fixed-point units), which comfortably covers operational regimes while preventing overflow.
- Ratio shortcuts: Where possible, we form ratios such as $r_0 = \exp\!\big((q_i - q_j)/b\big)$ directly, avoiding separate exponentials and a division, improving both precision and cost.
B.2 Monotonicity and error budgets
- Kernel monotonicity: The closed forms in A.2A.4 are strictly increasing in input and satisfy $y'(a) \in (0,1)$ for feasible states. Numerical implementations preserve monotonicity by:
- using log-sum-exp,
- guarding denominators (e.g., $r_0 + 1 - e^{\,y/b} > 0$),
- clamping to capacity when necessary and solving inverses in the capped branch.
- Error targets: Prices, shares, and swap amounts are computed to within small relative error (e.g., $\le 10^{-9}$ for typical ranges). Guards reject or cap inputs that would violate error or domain constraints.
B.3 Polynomial approximations in balanced 2-asset mode
- Domains: For $|\delta| \le \delta_\star$ and $\big|a/b\big| \le \tau_\star$, $\exp$ and $\ln$ are approximated by minimax polynomials on compact intervals $[-\tau_\star, \tau_\star]$ and $[\,1 - u_\star,\, 1 + u_\star\,]$, respectively, with $u_\star$ induced by the $\exp$ range.
- Construction: Coefficients are obtained offline (e.g., via Remez) to minimize the maximum relative error over the domain.
- Remainder bounds: Standard analytic bounds apply:
$$\big|e^{x} - P_d(x)\big| \;\le\; e^{\tau_\star}\,\frac{\tau_\star^{\,d+1}}{(d+1)!},$$
$$\big|\ln(1 + u) - Q_d(u)\big| \;\le\; \frac{|u|^{\,d+1}}{(d+1)\,(1 - |u|)^{\,d+1}},\qquad |u| < 1.$$
Degree $d$ and domain parameters $(\delta_\star, \tau_\star, u_\star)$ are chosen to meet the global error budget while maintaining monotonicity of the composed swap mapping.
B.4 Implementation notes for stability
- Prefer single-pass accumulations with on-the-fly recentering for $\sum \exp(\cdot)$.
- Maintain consistent reciprocals (e.g., precompute $1/b$) to reduce rounding dispersion.
- Use explicit positivity checks (e.g., size metric $S > 0$, $\exp$ arguments within bounds, denominators $> 0$).
- When a computed “inner” argument to $\ln(\cdot)$ is $\le 0$ due to rounding, switch to a conservative branch (cap-and-invert) rather than continuing.
C. Additional figures and tables (to be included)
- Price impact curves vs. constant-product and constant-mean baselines across $\kappa$.
- Parameter sweeps for $\kappa$ and $S$ showing depth and slippage profiles.
- Numerical accuracy: worst-case relative error heatmaps for prices and swap amounts; monotonicity checks.
- Gas microbenchmarks: general path vs. balanced 2-asset approximations; cache effects.
D. Glossary and notation
- $n$: number of assets.
- $i, j$: asset indices in $\{0,\dots,n-1\}$.
- $\mathbf{q}\in \mathbb{R}_{\ge 0}^{\,n}$: vector of normalized internal quantities; $q_i$ is the $i$-th component.
- $S(\mathbf{q}) = \sum_i q_i$: size metric (aggregate pool size).
- $\kappa > 0$: fixed liquidity proportionality constant.
- $b(\mathbf{q}) = \kappa \cdot S(\mathbf{q})$: effective LMSR liquidity parameter.
- $w_i = e^{q_i / b}$; $W = \sum_i w_i$.
- $\pi_i = w_i / W$: price share (softmax probability).
- $P(\text{base}\to\text{quote}) = \exp\!\big((q_{\text{quote}} - q_{\text{base}})/b\big)$: pairwise marginal price ratio.
- $a$: exact-in input amount for asset $i$ (fee-free kernel).
- $y$: exact-out output amount for asset $j$ (fee-free kernel).
- $r_0 = \exp\!\big((q_i - q_j)/b\big)$: initial ratio for an $i\to j$ swap.
- $\Lambda$: user-specified price limit (maximum acceptable $p_i/p_j$).
- $\alpha$: proportional growth/redeem factor for liquidity operations.
E. References
- Hanson, R. (2002). [Logarithmic Market Scoring Rules for Modular Combinatorial Information Aggregation.](https://mason.gmu.edu/~rhanson/mktscore.pdf)
- Abernethy, J., Chen, Y., & Waggoner, B. (2013). Low-Regret Learning in Prediction Markets.
- Uniswap (Hayden Adams et al.). Constant product market maker design docs and whitepapers.
- Balancer. Constant mean market makers and multi-asset pool design notes.
- Curve Finance. StableSwap invariant design notes.
- Fixed-point arithmetic references and standard libraries for 64.64 computations.

View File

@@ -1,100 +0,0 @@
# 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,10 +9,10 @@ remappings = [
optimizer=true optimizer=true
optimizer_runs=999999999 optimizer_runs=999999999
viaIR=true viaIR=true
gas_reports = ['PartyPool', 'PartyPoolBalancedPair', 'PartyPlanner', 'PartyPoolSwapImpl', 'PartyPoolMintImpl',] gas_reports = ['PartyPlanner', 'PartyPoolBalancedPair', 'PartyPool', ]
fs_permissions = [{ access = "write", path = "chain.json"}] fs_permissions = [{ access = "write", path = "chain.json"}]
[lint] [lint]
lint_on_build=false # more annoying than helpful exclude_lints=['mixed-case-variable', 'unaliased-plain-import', ]
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options

View File

@@ -1,20 +1,19 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30; pragma solidity ^0.8.30;
import "../src/Deploy.sol";
import "../src/IPartyPool.sol";
import "../src/PartyPlanner.sol";
import "../src/PartyPool.sol";
import "../test/MockERC20.sol";
import "@abdk/ABDKMath64x64.sol";
import "forge-std/Script.sol"; import "forge-std/Script.sol";
import "forge-std/console2.sol"; import "forge-std/console2.sol";
import "@abdk/ABDKMath64x64.sol";
import "../test/MockERC20.sol";
import "../src/IPartyPool.sol";
import "../src/PartyPool.sol";
import "../src/PartyPlanner.sol";
contract DeployMock is Script { contract DeployMock is Script {
address constant public DEV_ACCOUNT_0 = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266; address constant devAccount0 = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266;
// private key 0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356 // private key 0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356
address constant public DEV_ACCOUNT_7 = 0x14dC79964da2C08b23698B3D3cc7Ca32193d9955; address constant devAccount7 = 0x14dC79964da2C08b23698B3D3cc7Ca32193d9955;
function run() public { function run() public {
vm.startBroadcast(); vm.startBroadcast();
@@ -39,7 +38,7 @@ contract DeployMock is Script {
uint256 _feePpm = 100; uint256 _feePpm = 100;
// deploy a PartyPlanner factory and create the pool via factory // deploy a PartyPlanner factory and create the pool via factory
PartyPlanner planner = Deploy.newPartyPlanner(); PartyPlanner planner = new PartyPlanner();
// prepare initial deposits (10_000 units of each token, scaled by bases) // prepare initial deposits (10_000 units of each token, scaled by bases)
uint256[] memory initialDeposits = new uint256[](3); uint256[] memory initialDeposits = new uint256[](3);
@@ -57,8 +56,8 @@ contract DeployMock is Script {
IERC20(tokens[i]).approve(address(planner), initialDeposits[i]); IERC20(tokens[i]).approve(address(planner), initialDeposits[i]);
} }
// call full newPool signature on factory which will take the deposits and mint initial LP // call full createPool signature on factory which will take the deposits and mint initial LP
(IPartyPool pool, ) = planner.newPool( (PartyPool pool, uint256 lpAmount) = planner.createPool(
name, name,
symbol, symbol,
tokens, tokens,
@@ -69,14 +68,14 @@ contract DeployMock is Script {
_feePpm, _feePpm,
false, false,
msg.sender, // payer: this script msg.sender, // payer: this script
DEV_ACCOUNT_7, // receiver of initial LP devAccount7, // receiver of initial LP
initialDeposits, initialDeposits,
initialLpAmount, initialLpAmount,
deadline deadline
); );
// give tokens to dev7 for later use // give tokens to dev7 for later use
mintAll(DEV_ACCOUNT_7, 1_000_000); mintAll(devAccount7, 1_000_000);
vm.stopBroadcast(); vm.stopBroadcast();
@@ -95,15 +94,11 @@ contract DeployMock is Script {
string memory configStr = vm.serializeString(config, vm.toString(block.chainid), chainConfigStr); string memory configStr = vm.serializeString(config, vm.toString(block.chainid), chainConfigStr);
vm.writeJson(configStr, 'chain.json'); vm.writeJson(configStr, 'chain.json');
PartyPoolViewer viewer = Deploy.newViewer(); console2.log('\nPartyPlanner', address(planner));
console2.log(' PartyPool', address(pool));
console2.log(); console2.log(' USXD', address(usxd));
console2.log(' PartyPlanner', address(planner)); console2.log(' FUSD', address(fusd));
console2.log(' PartyPool', address(pool)); console2.log(' DIVE', address(dive));
console2.log('PartyPoolViewer', address(viewer));
console2.log(' USXD', address(usxd));
console2.log(' FUSD', address(fusd));
console2.log(' DIVE', address(dive));
} }
MockERC20 private usxd; MockERC20 private usxd;

View File

@@ -1,26 +1,15 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30; pragma solidity ^0.8.30;
import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import "./PartyPoolBalancedPair.sol";
import {PartyPlanner} from "./PartyPlanner.sol"; import {PartyPlanner} from "./PartyPlanner.sol";
import {PartyPool} from "./PartyPool.sol"; import {PartyPool} from "./PartyPool.sol";
import {PartyPoolBalancedPair} from "./PartyPoolBalancedPair.sol";
import {PartyPoolDeployer, PartyPoolBalancedPairDeployer} from "./PartyPoolDeployer.sol";
import {PartyPoolMintImpl} from "./PartyPoolMintImpl.sol";
import {PartyPoolSwapImpl} from "./PartyPoolSwapImpl.sol";
import {PartyPoolViewer} from "./PartyPoolViewer.sol";
library Deploy { library Deploy {
function newPartyPlanner() internal returns (PartyPlanner) { function newPartyPlanner() internal returns (PartyPlanner) {
return new PartyPlanner( return new PartyPlanner();
new PartyPoolSwapImpl(),
new PartyPoolMintImpl(),
new PartyPoolDeployer(),
new PartyPoolBalancedPairDeployer(),
0, // protocolFeePpm = 0 for deploy helper
address(0) // protocolFeeAddress = address(0) for deploy helper
);
} }
function newPartyPool( function newPartyPool(
@@ -28,46 +17,14 @@ library Deploy {
string memory symbol_, string memory symbol_,
IERC20[] memory tokens_, IERC20[] memory tokens_,
uint256[] memory bases_, uint256[] memory bases_,
int128 _kappa, int128 kappa_,
uint256 _swapFeePpm, uint256 swapFeePpm_,
uint256 _flashFeePpm, uint256 flashFeePpm_,
bool _stable bool stablePair_
) internal returns (PartyPool) { ) internal returns (PartyPool) {
// default protocol fee/off parameters (per your instruction) - set to 0 / address(0) return stablePair_ ?
uint256 protocolFeePpm = 0; new PartyPoolBalancedPair(name_, symbol_, tokens_, bases_, kappa_, swapFeePpm_, flashFeePpm_) :
address protocolAddr = address(0); new PartyPool(name_, symbol_, tokens_, bases_, kappa_, swapFeePpm_, flashFeePpm_);
return _stable && tokens_.length == 2 ?
new PartyPoolBalancedPair(
name_,
symbol_,
tokens_,
bases_,
_kappa,
_swapFeePpm,
_flashFeePpm,
protocolFeePpm,
protocolAddr,
new PartyPoolSwapImpl(),
new PartyPoolMintImpl()
) :
new PartyPool(
name_,
symbol_,
tokens_,
bases_,
_kappa,
_swapFeePpm,
_flashFeePpm,
protocolFeePpm,
protocolAddr,
new PartyPoolSwapImpl(),
new PartyPoolMintImpl()
);
} }
function newViewer() internal returns (PartyPoolViewer) {
return new PartyPoolViewer(new PartyPoolSwapImpl(), new PartyPoolMintImpl());
}
} }

View File

@@ -1,123 +0,0 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30;
import {IERC20Errors} from "../lib/openzeppelin-contracts/contracts/interfaces/draft-IERC6093.sol";
import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {Context} from "../lib/openzeppelin-contracts/contracts/utils/Context.sol";
import {IERC20Metadata} from "../lib/openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import {ERC20Internal} from "./ERC20Internal.sol";
// Copied from OpenZeppelin's ERC20 implementation, but split into internal and external parts
contract ERC20External is ERC20Internal, IERC20Metadata {
/**
* @dev Sets the values for {name} and {symbol}.
*
* Both values are immutable: they can only be set once during construction.
*/
constructor(string memory name_, string memory symbol_) {
_name = name_;
_symbol = symbol_;
}
/**
* @dev Returns the name of the token.
*/
function name() public view virtual returns (string memory) {
return _name;
}
/**
* @dev Returns the symbol of the token, usually a shorter version of the
* name.
*/
function symbol() public view virtual returns (string memory) {
return _symbol;
}
/**
* @dev Returns the number of decimals used to get its user representation.
* For example, if `decimals` equals `2`, a balance of `505` tokens should
* be displayed to a user as `5.05` (`505 / 10 ** 2`).
*
* Tokens usually opt for a value of 18, imitating the relationship between
* Ether and Wei. This is the default value returned by this function, unless
* it's overridden.
*
* NOTE: This information is only used for _display_ purposes: it in
* no way affects any of the arithmetic of the contract, including
* {IERC20-balanceOf} and {IERC20-transfer}.
*/
function decimals() public view virtual returns (uint8) {
return 18;
}
/// @inheritdoc IERC20
function totalSupply() public view virtual returns (uint256) {
return _totalSupply;
}
/// @inheritdoc IERC20
function balanceOf(address account) public view virtual returns (uint256) {
return _balances[account];
}
/**
* @dev See {IERC20-transfer}.
*
* Requirements:
*
* - `to` cannot be the zero address.
* - the caller must have a balance of at least `value`.
*/
function transfer(address to, uint256 value) public virtual returns (bool) {
address owner = _msgSender();
_transfer(owner, to, value);
return true;
}
/// @inheritdoc IERC20
function allowance(address owner, address spender) public view virtual returns (uint256) {
return _allowances[owner][spender];
}
/**
* @dev See {IERC20-approve}.
*
* NOTE: If `value` is the maximum `uint256`, the allowance is not updated on
* `transferFrom`. This is semantically equivalent to an infinite approval.
*
* Requirements:
*
* - `spender` cannot be the zero address.
*/
function approve(address spender, uint256 value) public virtual returns (bool) {
address owner = _msgSender();
_approve(owner, spender, value);
return true;
}
/**
* @dev See {IERC20-transferFrom}.
*
* Skips emitting an {Approval} event indicating an allowance update. This is not
* required by the ERC. See {xref-ERC20-_approve-address-address-uint256-bool-}[_approve].
*
* NOTE: Does not update the allowance if the current allowance
* is the maximum `uint256`.
*
* Requirements:
*
* - `from` and `to` cannot be the zero address.
* - `from` must have a balance of at least `value`.
* - the caller must have allowance for ``from``'s tokens of at least
* `value`.
*/
function transferFrom(address from, address to, uint256 value) public virtual returns (bool) {
address spender = _msgSender();
_spendAllowance(from, spender, value);
_transfer(from, to, value);
return true;
}
}

View File

@@ -1,175 +0,0 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30;
import {IERC20Errors} from "../lib/openzeppelin-contracts/contracts/interfaces/draft-IERC6093.sol";
import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {Context} from "../lib/openzeppelin-contracts/contracts/utils/Context.sol";
// Copied from OpenZeppelin's ERC20 implementation, but split into internal and external parts
abstract contract ERC20Internal is Context, IERC20Errors {
mapping(address account => uint256) internal _balances;
mapping(address account => mapping(address spender => uint256)) internal _allowances;
uint256 internal _totalSupply;
string internal _name;
string internal _symbol;
/**
* @dev Moves a `value` amount of tokens from `from` to `to`.
*
* This internal function is equivalent to {transfer}, and can be used to
* e.g. implement automatic token fees, slashing mechanisms, etc.
*
* Emits a {Transfer} event.
*
* NOTE: This function is not virtual, {_update} should be overridden instead.
*/
function _transfer(address from, address to, uint256 value) internal {
if (from == address(0)) {
revert ERC20InvalidSender(address(0));
}
if (to == address(0)) {
revert ERC20InvalidReceiver(address(0));
}
_update(from, to, value);
}
/**
* @dev Transfers a `value` amount of tokens from `from` to `to`, or alternatively mints (or burns) if `from`
* (or `to`) is the zero address. All customizations to transfers, mints, and burns should be done by overriding
* this function.
*
* Emits a {Transfer} event.
*/
function _update(address from, address to, uint256 value) internal virtual {
if (from == address(0)) {
// Overflow check required: The rest of the code assumes that totalSupply never overflows
_totalSupply += value;
} else {
uint256 fromBalance = _balances[from];
if (fromBalance < value) {
revert ERC20InsufficientBalance(from, fromBalance, value);
}
unchecked {
// Overflow not possible: value <= fromBalance <= totalSupply.
_balances[from] = fromBalance - value;
}
}
if (to == address(0)) {
unchecked {
// Overflow not possible: value <= totalSupply or value <= fromBalance <= totalSupply.
_totalSupply -= value;
}
} else {
unchecked {
// Overflow not possible: balance + value is at most totalSupply, which we know fits into a uint256.
_balances[to] += value;
}
}
emit IERC20.Transfer(from, to, value);
}
/**
* @dev Creates a `value` amount of tokens and assigns them to `account`, by transferring it from address(0).
* Relies on the `_update` mechanism
*
* Emits a {Transfer} event with `from` set to the zero address.
*
* NOTE: This function is not virtual, {_update} should be overridden instead.
*/
function _mint(address account, uint256 value) internal {
if (account == address(0)) {
revert ERC20InvalidReceiver(address(0));
}
_update(address(0), account, value);
}
/**
* @dev Destroys a `value` amount of tokens from `account`, lowering the total supply.
* Relies on the `_update` mechanism.
*
* Emits a {Transfer} event with `to` set to the zero address.
*
* NOTE: This function is not virtual, {_update} should be overridden instead
*/
function _burn(address account, uint256 value) internal {
if (account == address(0)) {
revert ERC20InvalidSender(address(0));
}
_update(account, address(0), value);
}
/**
* @dev Sets `value` as the allowance of `spender` over the `owner`'s tokens.
*
* This internal function is equivalent to `approve`, and can be used to
* e.g. set automatic allowances for certain subsystems, etc.
*
* Emits an {Approval} event.
*
* Requirements:
*
* - `owner` cannot be the zero address.
* - `spender` cannot be the zero address.
*
* Overrides to this logic should be done to the variant with an additional `bool emitEvent` argument.
*/
function _approve(address owner, address spender, uint256 value) internal {
_approve(owner, spender, value, true);
}
/**
* @dev Variant of {_approve} with an optional flag to enable or disable the {Approval} event.
*
* By default (when calling {_approve}) the flag is set to true. On the other hand, approval changes made by
* `_spendAllowance` during the `transferFrom` operation set the flag to false. This saves gas by not emitting any
* `Approval` event during `transferFrom` operations.
*
* Anyone who wishes to continue emitting `Approval` events on the`transferFrom` operation can force the flag to
* true using the following override:
*
* ```solidity
* function _approve(address owner, address spender, uint256 value, bool) internal virtual override {
* super._approve(owner, spender, value, true);
* }
* ```
*
* Requirements are the same as {_approve}.
*/
function _approve(address owner, address spender, uint256 value, bool emitEvent) internal virtual {
if (owner == address(0)) {
revert ERC20InvalidApprover(address(0));
}
if (spender == address(0)) {
revert ERC20InvalidSpender(address(0));
}
_allowances[owner][spender] = value;
if (emitEvent) {
emit IERC20.Approval(owner, spender, value);
}
}
/**
* @dev Updates `owner`'s allowance for `spender` based on spent `value`.
*
* Does not update the allowance value in case of infinite allowance.
* Revert if not enough allowance is available.
*
* Does not emit an {Approval} event.
*/
function _spendAllowance(address owner, address spender, uint256 value) internal virtual {
uint256 currentAllowance = _allowances[owner][spender];
if (currentAllowance < type(uint256).max) {
if (currentAllowance < value) {
revert ERC20InsufficientAllowance(spender, currentAllowance, value);
}
unchecked {
_approve(owner, spender, currentAllowance - value, false);
}
}
}
}

View File

@@ -1,16 +1,14 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30; pragma solidity ^0.8.30;
import "./IPartyPool.sol"; import "./PartyPool.sol";
import "./PartyPoolMintImpl.sol";
import "./PartyPoolSwapImpl.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
/// @title IPartyPlanner /// @title IPartyPlanner
/// @notice Interface for factory contract for creating and tracking PartyPool instances /// @notice Interface for factory contract for creating and tracking PartyPool instances
interface IPartyPlanner { interface IPartyPlanner {
// Event emitted when a new pool is created // Event emitted when a new pool is created
event PartyStarted(IPartyPool indexed pool, string name, string symbol, IERC20[] tokens); event PartyStarted(PartyPool indexed pool, string name, string symbol, IERC20[] tokens);
/// @notice Creates a new PartyPool instance and initializes it with initial deposits (legacy signature). /// @notice Creates a new PartyPool instance and initializes it with initial deposits (legacy signature).
/// @dev Deprecated in favour of the kappa-based overload below; kept for backwards compatibility. /// @dev Deprecated in favour of the kappa-based overload below; kept for backwards compatibility.
@@ -29,7 +27,7 @@ interface IPartyPlanner {
/// @param deadline Reverts if nonzero and the current blocktime is later than the deadline /// @param deadline Reverts if nonzero and the current blocktime is later than the deadline
/// @return pool Address of the newly created and initialized PartyPool /// @return pool Address of the newly created and initialized PartyPool
/// @return lpAmount Amount of LP tokens minted to the receiver /// @return lpAmount Amount of LP tokens minted to the receiver
function newPool( function createPool(
// Pool constructor args (legacy) // Pool constructor args (legacy)
string memory name_, string memory name_,
string memory symbol_, string memory symbol_,
@@ -46,7 +44,7 @@ interface IPartyPlanner {
uint256[] memory initialDeposits, uint256[] memory initialDeposits,
uint256 initialLpAmount, uint256 initialLpAmount,
uint256 deadline uint256 deadline
) external returns (IPartyPool pool, uint256 lpAmount); ) external returns (PartyPool pool, uint256 lpAmount);
/// @notice Creates a new PartyPool instance and initializes it with initial deposits (kappa-based). /// @notice Creates a new PartyPool instance and initializes it with initial deposits (kappa-based).
/// @param name_ LP token name /// @param name_ LP token name
@@ -63,7 +61,7 @@ interface IPartyPlanner {
/// @param deadline Reverts if nonzero and the current blocktime is later than the deadline /// @param deadline Reverts if nonzero and the current blocktime is later than the deadline
/// @return pool Address of the newly created and initialized PartyPool /// @return pool Address of the newly created and initialized PartyPool
/// @return lpAmount Amount of LP tokens minted to the receiver /// @return lpAmount Amount of LP tokens minted to the receiver
function newPool( function createPool(
// Pool constructor args (kappa-based) // Pool constructor args (kappa-based)
string memory name_, string memory name_,
string memory symbol_, string memory symbol_,
@@ -79,7 +77,7 @@ interface IPartyPlanner {
uint256[] memory initialDeposits, uint256[] memory initialDeposits,
uint256 initialLpAmount, uint256 initialLpAmount,
uint256 deadline uint256 deadline
) external returns (IPartyPool pool, uint256 lpAmount); ) external returns (PartyPool pool, uint256 lpAmount);
/// @notice Checks if a pool is supported /// @notice Checks if a pool is supported
/// @param pool The pool address to check /// @param pool The pool address to check
@@ -94,7 +92,7 @@ interface IPartyPlanner {
/// @param offset Starting index for pagination /// @param offset Starting index for pagination
/// @param limit Maximum number of items to return /// @param limit Maximum number of items to return
/// @return pools Array of pool addresses for the requested page /// @return pools Array of pool addresses for the requested page
function getAllPools(uint256 offset, uint256 limit) external view returns (IPartyPool[] memory pools); function getAllPools(uint256 offset, uint256 limit) external view returns (PartyPool[] memory pools);
/// @notice Returns the total number of unique tokens /// @notice Returns the total number of unique tokens
/// @return The total count of unique tokens /// @return The total count of unique tokens
@@ -116,12 +114,5 @@ interface IPartyPlanner {
/// @param offset Starting index for pagination /// @param offset Starting index for pagination
/// @param limit Maximum number of items to return /// @param limit Maximum number of items to return
/// @return pools Array of pool addresses containing the specified token /// @return pools Array of pool addresses containing the specified token
function getPoolsByToken(IERC20 token, uint256 offset, uint256 limit) external view returns (IPartyPool[] memory pools); function getPoolsByToken(IERC20 token, uint256 offset, uint256 limit) external view returns (PartyPool[] memory pools);
/// @notice Address of the mint implementation contract used by all pools created by this factory
function mintImpl() external view returns (PartyPoolMintImpl);
/// @notice Address of the swap implementation contract used by all pools created by this factory
function swapMintImpl() external view returns (PartyPoolSwapImpl);
} }

View File

@@ -1,10 +1,7 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30; pragma solidity ^0.8.30;
import "../lib/openzeppelin-contracts/contracts/interfaces/IERC3156FlashBorrower.sol"; import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import "./LMSRStabilized.sol";
import {IERC20Metadata} from "../lib/openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
/// @title PartyPool - LMSR-backed multi-asset pool with LP ERC20 token /// @title PartyPool - LMSR-backed multi-asset pool with LP ERC20 token
/// @notice A multi-asset liquidity pool backed by the LMSRStabilized pricing model. /// @notice A multi-asset liquidity pool backed by the LMSRStabilized pricing model.
@@ -58,11 +55,10 @@ interface IPartyPool is IERC20Metadata {
); );
function LMSR() external view returns (LMSRStabilized.State memory); // Immutable pool configuration (public getters)
/// @notice Token addresses comprising the pool. Effectively immutable after construction. /// @notice Token addresses comprising the pool. Effectively immutable after construction.
/// @dev tokens[i] corresponds to the i-th asset and maps to index i in the internal LMSR arrays. /// @dev tokens[i] corresponds to the i-th asset and maps to index i in the internal LMSR arrays.
function getToken(uint256) external view returns (IERC20); // get single token function tokens(uint256) external view returns (IERC20); // get single token
/// @notice Returns the number of tokens (n) in the pool. /// @notice Returns the number of tokens (n) in the pool.
function numTokens() external view returns (uint256); function numTokens() external view returns (uint256);
@@ -80,32 +76,23 @@ interface IPartyPool is IERC20Metadata {
/// @notice Flash-loan fee in parts-per-million (ppm) applied to flash borrow amounts. /// @notice Flash-loan fee in parts-per-million (ppm) applied to flash borrow amounts.
function flashFeePpm() external view returns (uint256); function flashFeePpm() external view returns (uint256);
/// @notice Protocol fee share (ppm) applied to fees collected by the pool (floored when accrued)
/// @dev This is the fraction (in ppm) of the pool-collected fees that are owed to the protocol.
function protocolFeePpm() external view returns (uint256);
/// @notice Address that will receive collected protocol tokens when collectProtocolFees() is called.
function protocolFeeAddress() external view returns (address);
/// @notice Protocol fee ledger accessor. Returns tokens owed (raw uint token units) from this pool as protocol fees
/// that have not yet been transferred out.
function allProtocolFeesOwed() external view returns (uint256[] memory);
/// @notice Callable by anyone, sends any owed protocol fees to the protocol fee address.
function collectProtocolFees() external;
/// @notice Liquidity parameter κ (Q64.64) used by the LMSR kernel: b = κ * S(q) /// @notice Liquidity parameter κ (Q64.64) used by the LMSR kernel: b = κ * S(q)
/// @dev Pools are constructed with a κ value; this getter exposes the κ used by the pool. /// @dev Pools are constructed with a κ value; this getter exposes the κ used by the pool.
function kappa() external view returns (int128); function kappa() external view returns (int128);
/// @notice Mapping from token address => (index+1). A zero value indicates the token is not in the pool.
/// @dev Use index = tokenAddressToIndexPlusOne[token] - 1 when non-zero.
function tokenAddressToIndexPlusOne(IERC20) external view returns (uint);
// Initialization / Mint / Burn (LP token managed) // Initialization / Mint / Burn (LP token managed)
/// @notice Initial mint to set up pool for the first time. /// @notice Calculate the proportional deposit amounts required for a given LP token amount
/// @dev Assumes tokens have already been transferred to the pool prior to calling. /// @dev Returns the minimum token amounts (rounded up) that must be supplied to receive lpTokenAmount
/// Can only be called when the pool is uninitialized (totalSupply() == 0 or lmsr.nAssets == 0). /// LP tokens at current pool proportions. If the pool is empty (initial deposit) returns zeros
/// @param receiver address that receives the LP tokens /// because the initial deposit is handled by transferring tokens then calling mint().
/// @param lpTokens The number of LP tokens to issue for this mint. If 0, then the number of tokens returned will equal the LMSR internal q total /// @param lpTokenAmount The amount of LP tokens desired
function initialMint(address receiver, uint256 lpTokens) external returns (uint256 lpMinted); /// @return depositAmounts Array of token amounts to deposit (rounded up)
function mintDepositAmounts(uint256 lpTokenAmount) external view returns (uint256[] memory depositAmounts);
/// @notice Proportional mint (or initial supply if first call). /// @notice Proportional mint (or initial supply if first call).
/// @dev - For initial supply: assumes tokens have already been transferred to the pool prior to calling. /// @dev - For initial supply: assumes tokens have already been transferred to the pool prior to calling.
@@ -118,6 +105,13 @@ interface IPartyPool is IERC20Metadata {
/// @return lpMinted the actual amount of lpToken minted /// @return lpMinted the actual amount of lpToken minted
function mint(address payer, address receiver, uint256 lpTokenAmount, uint256 deadline) external returns (uint256 lpMinted); function mint(address payer, address receiver, uint256 lpTokenAmount, uint256 deadline) external returns (uint256 lpMinted);
/// @notice Calculate the proportional withdrawal amounts for a given LP token amount
/// @dev Returns the maximum token amounts (rounded down) that will be withdrawn when burning lpTokenAmount.
/// If the pool is uninitialized or supply is zero, returns zeros.
/// @param lpTokenAmount The amount of LP tokens to burn
/// @return withdrawAmounts Array of token amounts to withdraw (rounded down)
function burnReceiveAmounts(uint256 lpTokenAmount) external view returns (uint256[] memory withdrawAmounts);
/// @notice Burn LP tokens and withdraw the proportional basket to receiver. /// @notice Burn LP tokens and withdraw the proportional basket to receiver.
/// @dev Payer must own or approve the LP tokens being burned. The function updates LMSR state /// @dev Payer must own or approve the LP tokens being burned. The function updates LMSR state
/// proportionally to reflect the reduced pool size after the withdrawal. /// proportionally to reflect the reduced pool size after the withdrawal.
@@ -125,7 +119,7 @@ interface IPartyPool is IERC20Metadata {
/// @param receiver address that receives the withdrawn tokens /// @param receiver address that receives the withdrawn tokens
/// @param lpAmount amount of LP tokens to burn (proportional withdrawal) /// @param lpAmount amount of LP tokens to burn (proportional withdrawal)
/// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore. /// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore.
function burn(address payer, address receiver, uint256 lpAmount, uint256 deadline) external returns (uint256[] memory withdrawAmounts); function burn(address payer, address receiver, uint256 lpAmount, uint256 deadline) external;
// Swaps // Swaps
@@ -136,14 +130,12 @@ interface IPartyPool is IERC20Metadata {
/// @param maxAmountIn maximum gross input allowed (inclusive of fee) /// @param maxAmountIn maximum gross input allowed (inclusive of fee)
/// @param limitPrice maximum acceptable marginal price (pass 0 to ignore) /// @param limitPrice maximum acceptable marginal price (pass 0 to ignore)
/// @return amountIn gross input amount to transfer (includes fee), amountOut output amount user would receive, fee fee amount taken /// @return amountIn gross input amount to transfer (includes fee), amountOut output amount user would receive, fee fee amount taken
/*
function swapAmounts( function swapAmounts(
uint256 inputTokenIndex, uint256 inputTokenIndex,
uint256 outputTokenIndex, uint256 outputTokenIndex,
uint256 maxAmountIn, uint256 maxAmountIn,
int128 limitPrice int128 limitPrice
) external view returns (uint256 amountIn, uint256 amountOut, uint256 fee); ) external view returns (uint256 amountIn, uint256 amountOut, uint256 fee);
*/
/// @notice Swap input token inputTokenIndex -> token outputTokenIndex. Payer must approve token inputTokenIndex. /// @notice Swap input token inputTokenIndex -> token outputTokenIndex. Payer must approve token inputTokenIndex.
/// @dev This function transfers the exact gross input (including fee) from payer and sends the computed output to receiver. /// @dev This function transfers the exact gross input (including fee) from payer and sends the computed output to receiver.
@@ -166,6 +158,17 @@ interface IPartyPool is IERC20Metadata {
uint256 deadline uint256 deadline
) external returns (uint256 amountIn, uint256 amountOut, uint256 fee); ) external returns (uint256 amountIn, uint256 amountOut, uint256 fee);
/// @notice External view to quote swap-to-limit amounts (gross input incl. fee and output), matching swapToLimit() computations
/// @param inputTokenIndex index of input token
/// @param outputTokenIndex index of output token
/// @param limitPrice target marginal price to reach (must be > 0)
/// @return amountIn gross input amount to transfer (includes fee), amountOut output amount user would receive, fee fee amount taken
function swapToLimitAmounts(
uint256 inputTokenIndex,
uint256 outputTokenIndex,
int128 limitPrice
) external view returns (uint256 amountIn, uint256 amountOut, uint256 fee);
/// @notice Swap up to the price limit; computes max input to reach limit then performs swap. /// @notice Swap up to the price limit; computes max input to reach limit then performs swap.
/// @dev If balances prevent fully reaching the limit, the function caps and returns actuals. /// @dev If balances prevent fully reaching the limit, the function caps and returns actuals.
/// The payer must transfer the exact gross input computed by the view. /// The payer must transfer the exact gross input computed by the view.
@@ -185,6 +188,15 @@ interface IPartyPool is IERC20Metadata {
uint256 deadline uint256 deadline
) external returns (uint256 amountInUsed, uint256 amountOut, uint256 fee); ) external returns (uint256 amountInUsed, uint256 amountOut, uint256 fee);
/// @notice External view to quote swapMint amounts, matching swapMint() computations
/// @param inputTokenIndex index of input token to deposit
/// @param maxAmountIn maximum gross input allowed (inclusive of fee)
/// @return totalTransfer gross input amount to transfer (includes fee), amountIn net input amount used for minting, fee fee amount taken, lpMinted LP tokens that would be minted
function swapMintAmounts(
uint256 inputTokenIndex,
uint256 maxAmountIn
) external view returns (uint256 totalTransfer, uint256 amountIn, uint256 fee, uint256 lpMinted);
/// @notice Single-token mint: deposit a single token, charge swap-LMSR cost, and mint LP. /// @notice Single-token mint: deposit a single token, charge swap-LMSR cost, and mint LP.
/// @dev swapMint executes as an exact-in planned swap followed by proportional scaling of qInternal. /// @dev swapMint executes as an exact-in planned swap followed by proportional scaling of qInternal.
/// The function emits SwapMint (gross, net, fee) and also emits Mint for LP issuance. /// The function emits SwapMint (gross, net, fee) and also emits Mint for LP issuance.
@@ -202,6 +214,15 @@ interface IPartyPool is IERC20Metadata {
uint256 deadline uint256 deadline
) external returns (uint256 lpMinted); ) external returns (uint256 lpMinted);
/// @notice External view to quote burnSwap amounts, matching burnSwap() computations
/// @param lpAmount amount of LP tokens to burn
/// @param inputTokenIndex index of target asset to receive
/// @return amountOut output amount user would receive after fees
function burnSwapAmounts(
uint256 lpAmount,
uint256 inputTokenIndex
) external view returns (uint256 amountOut);
/// @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 `inputTokenIndex` and send to receiver.
/// @dev The function burns LP tokens (authorization via allowance if needed), sends the single-asset payout and updates LMSR state. /// @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 payer who burns LP tokens
@@ -218,17 +239,39 @@ interface IPartyPool is IERC20Metadata {
uint256 deadline uint256 deadline
) external returns (uint256 amountOutUint); ) external returns (uint256 amountOutUint);
/** /// @notice Marginal price of `base` denominated in `quote` as Q64.64.
* @dev Initiate a flash loan. /// @dev Returns the LMSR marginal price p_quote / p_base in ABDK 64.64 fixed-point format.
* @param receiver The receiver of the tokens in the loan, and the receiver of the callback. /// Useful for off-chain quoting; raw 64.64 value is returned (no scaling to token units).
* @param token The loan currency. /// @param baseTokenIndex index of the base asset (e.g., ETH)
* @param amount The amount of tokens lent. /// @param quoteTokenIndex index of the quote asset (e.g., USD)
* @param data Arbitrary data structure, intended to contain user-defined parameters. /// @return price Q64.64 value equal to quote per base (p_quote / p_base)
*/ function price(uint256 baseTokenIndex, uint256 quoteTokenIndex) external view returns (int128);
function flashLoan(
IERC3156FlashBorrower receiver, /// @notice Price of one LP token denominated in `quote` as Q64.64.
address token, /// @dev Computes LMSR poolPrice (quote per unit internal qTotal) and scales it to LP units:
uint256 amount, /// returns price_per_LP = poolPrice_quote * (totalSupply() / qTotal) in ABDK 64.64 format.
/// The returned value is raw Q64.64 and represents quote units per one LP token unit.
/// @param quoteTokenIndex index of the quote asset in which to denominate the LP price
/// @return price Q64.64 value equal to quote per LP token unit
function poolPrice(uint256 quoteTokenIndex) external view returns (int128);
/// @notice Compute repayment amounts (principal + flash fee) for a proposed flash loan.
/// @param loanAmounts array of per-token loan amounts; must match the pool's token ordering.
/// @return repaymentAmounts array where repaymentAmounts[i] = loanAmounts[i] + ceil(loanAmounts[i] * flashFeePpm)
function flashRepaymentAmounts(uint256[] memory loanAmounts) external view
returns (uint256[] memory repaymentAmounts);
/// @notice Receive token amounts and require them to be repaid plus a fee inside a callback.
/// @dev The caller must implement IPartyFlashCallback#partyFlashCallback which receives (amounts, repaymentAmounts, data).
/// This function verifies that, after the callback returns, the pool's balances have increased by at least the fees
/// for each borrowed token. Reverts if repayment (including fee) did not occur.
/// @param recipient The address which will receive the token amounts
/// @param amounts The amount of each token to send (array length must equal pool size)
/// @param data Any data to be passed through to the callback
function flash(
address recipient,
uint256[] memory amounts,
bytes calldata data bytes calldata data
) external returns (bool); ) external;
} }

View File

@@ -1,7 +1,7 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30; pragma solidity ^0.8.30;
import {ABDKMath64x64} from "../lib/abdk-libraries-solidity/ABDKMath64x64.sol"; import "@abdk/ABDKMath64x64.sol";
/// @notice Stabilized LMSR library with incremental exp(z) caching for gas efficiency. /// @notice Stabilized LMSR library with incremental exp(z) caching for gas efficiency.
/// - Stores b (64.64), M (shift), Z = sum exp(z_i), z[i] = (q_i / b) - M /// - Stores b (64.64), M (shift), Z = sum exp(z_i), z[i] = (q_i / b) - M
@@ -53,15 +53,8 @@ library LMSRStabilized {
/// @notice Cost C(q) = b * (M + ln(Z)) /// @notice Cost C(q) = b * (M + ln(Z))
function cost(State storage s) internal view returns (int128) { function cost(State storage s) internal view returns (int128) {
return cost(s.kappa, s.qInternal); int128 b = _computeB(s);
} (int128 M, int128 Z) = _computeMAndZ(b, s.qInternal);
/// @notice Pure version: Cost C(q) = b * (M + ln(Z))
function cost(int128 kappa, int128[] memory qInternal) internal pure returns (int128) {
int128 sizeMetric = _computeSizeMetric(qInternal);
require(sizeMetric > int128(0), "LMSR: size metric zero");
int128 b = kappa.mul(sizeMetric);
(int128 M, int128 Z) = _computeMAndZ(b, qInternal);
int128 lnZ = _ln(Z); int128 lnZ = _ln(Z);
int128 inner = M.add(lnZ); int128 inner = M.add(lnZ);
int128 c = b.mul(inner); int128 c = b.mul(inner);
@@ -98,58 +91,23 @@ library LMSRStabilized {
int128 a, int128 a,
int128 limitPrice int128 limitPrice
) internal view returns (int128 amountIn, int128 amountOut) { ) internal view returns (int128 amountIn, int128 amountOut) {
return swapAmountsForExactInput(s.nAssets, s.kappa, s.qInternal, i, j, a, limitPrice); require(i < s.nAssets && j < s.nAssets, "LMSR: idx");
}
/// @notice Pure version: Closed-form asset-i -> asset-j amountOut in 64.64 fixed-point format (fee-free kernel)
/// Uses the closed-form two-asset LMSR formula (no fees in kernel):
/// y = b * ln(1 + r0 * (1 - exp(-a / b)))
/// where r0 = e_i / e_j.
///
/// This variant accepts an additional `limitPrice` (64.64) which represents the
/// maximum acceptable marginal price (p_i / p_j). If the marginal price would
/// exceed `limitPrice` before the requested `a` is fully consumed, the input
/// `a` is truncated to the value that makes the marginal price equal `limitPrice`.
///
/// NOTE: Kernel is fee-free; fees should be handled by the wrapper/token layer.
///
/// @param nAssets Number of assets in the pool
/// @param kappa Liquidity parameter κ (64.64 fixed point)
/// @param qInternal Cached internal balances in 64.64 fixed-point format
/// @param i Index of input asset
/// @param j Index of output asset
/// @param a Amount of input asset (in int128 format, 64.64 fixed-point)
/// @param limitPrice Maximum acceptable price ratio (64.64). If <= current price, this call reverts.
/// @return amountIn Actual amount of input asset used (may be less than `a` if limited by price)
/// @return amountOut Amount of output asset j in 64.64 fixed-point format
function swapAmountsForExactInput(
uint256 nAssets,
int128 kappa,
int128[] memory qInternal,
uint256 i,
uint256 j,
int128 a,
int128 limitPrice
) internal pure returns (int128 amountIn, int128 amountOut) {
require(i < nAssets && j < nAssets, "LMSR: idx");
// Initialize amountIn to full amount (will be adjusted if limit price is hit) // Initialize amountIn to full amount (will be adjusted if limit price is hit)
amountIn = a; amountIn = a;
// Compute b and ensure positivity before deriving invB // Compute b and ensure positivity before deriving invB
int128 sizeMetric = _computeSizeMetric(qInternal); int128 b = _computeB(s);
require(sizeMetric > int128(0), "LMSR: size metric zero");
int128 b = kappa.mul(sizeMetric);
require(b > int128(0), "LMSR: b<=0"); require(b > int128(0), "LMSR: b<=0");
// Precompute reciprocal of b to avoid repeated divisions // Precompute reciprocal of b to avoid repeated divisions
int128 invB = ABDKMath64x64.div(ONE, b); int128 invB = ABDKMath64x64.div(ONE, b);
// Guard: output asset must have non-zero effective weight to avoid degenerate/div-by-zero-like conditions // Guard: output asset must have non-zero effective weight to avoid degenerate/div-by-zero-like conditions
require(qInternal[j] > int128(0), "LMSR: e_j==0"); require(s.qInternal[j] > int128(0), "LMSR: e_j==0");
// Compute r0 = exp((q_i - q_j) / b) directly using invB // Compute r0 = exp((q_i - q_j) / b) directly using invB
int128 r0 = _exp(qInternal[i].sub(qInternal[j]).mul(invB)); int128 r0 = _exp(s.qInternal[i].sub(s.qInternal[j]).mul(invB));
require(r0 > int128(0), "LMSR: r0<=0"); // equivalent to e_j > 0 check require(r0 > int128(0), "LMSR: r0<=0"); // equivalent to e_j > 0 check
// If a positive limitPrice is given, determine whether the full `a` would // If a positive limitPrice is given, determine whether the full `a` would
@@ -159,12 +117,10 @@ library LMSRStabilized {
// r0 must be positive; if r0 == 0 then no risk of exceeding limit by increasing r. // r0 must be positive; if r0 == 0 then no risk of exceeding limit by increasing r.
require(r0 >= int128(0), "LMSR: r0<0"); require(r0 >= int128(0), "LMSR: r0<0");
if (r0 == int128(0)) { if (r0 == int128(0)) {
// console2.log("r0 == 0 (input asset has zero weight), no limit truncation needed"); // console2.log("r0 == 0 (input asset has zero weight), no limit truncation needed");
} else { } else {
// If limitPrice <= current price, we revert (caller must choose a limit > current price to allow any fill) // If limitPrice <= current price, we revert (caller must choose a limit > current price to allow any fill)
if (limitPrice <= r0) { require(limitPrice > r0, "LMSR: limitPrice <= current price");
revert("LMSR: limitPrice <= current price");
}
// Compute a_limit directly from ln(limit / r0): a_limit = b * ln(limit / r0) // Compute a_limit directly from ln(limit / r0): a_limit = b * ln(limit / r0)
int128 ratioLimitOverR0 = limitPrice.div(r0); int128 ratioLimitOverR0 = limitPrice.div(r0);
@@ -180,7 +136,7 @@ library LMSRStabilized {
amountIn = aLimit64; // Store the truncated input amount amountIn = aLimit64; // Store the truncated input amount
a = aLimit64; // Use truncated amount for calculations a = aLimit64; // Use truncated amount for calculations
} else { } else {
// console2.log("Not truncating: aLimit64 >= a"); // console2.log("Not truncating: aLimit64 >= a");
} }
} }
} }
@@ -198,12 +154,12 @@ library LMSRStabilized {
// If inner <= 0 then cap output to the current balance q_j (cannot withdraw more than q_j) // If inner <= 0 then cap output to the current balance q_j (cannot withdraw more than q_j)
if (inner <= int128(0)) { if (inner <= int128(0)) {
int128 qj64 = qInternal[j]; int128 qj64 = s.qInternal[j];
return (amountIn, qj64); return (amountIn, qj64);
} }
int128 lnInner = _ln(inner); int128 lnInner = _ln(inner);
int128 b_lnInner = b.mul(lnInner); int128 b_lnInner = b.mul(lnInner);
amountOut = b_lnInner; amountOut = b_lnInner;
// Safety check // Safety check
@@ -230,48 +186,21 @@ library LMSRStabilized {
uint256 j, uint256 j,
int128 limitPrice int128 limitPrice
) internal view returns (int128 amountIn, int128 amountOut) { ) internal view returns (int128 amountIn, int128 amountOut) {
return swapAmountsForPriceLimit(s.nAssets, s.kappa, s.qInternal, i, j, limitPrice); require(i < s.nAssets && j < s.nAssets, "LMSR: idx");
}
/// @notice Pure version: Maximum input/output pair possible when swapping from asset i to asset j
/// given a maximum acceptable price ratio (p_i/p_j).
/// Returns the input amount that would drive the marginal price to the limit (amountIn)
/// and the corresponding output amount (amountOut). If the output would exceed the
/// j-balance, amountOut is capped and amountIn is solved for the capped output.
///
/// @param nAssets Number of assets in the pool
/// @param kappa Liquidity parameter κ (64.64 fixed point)
/// @param qInternal Cached internal balances in 64.64 fixed-point format
/// @param i Index of input asset
/// @param j Index of output asset
/// @param limitPrice Maximum acceptable price ratio (64.64)
/// @return amountIn Maximum input amount in 64.64 fixed-point format that reaches the price limit
/// @return amountOut Corresponding maximum output amount in 64.64 fixed-point format
function swapAmountsForPriceLimit(
uint256 nAssets,
int128 kappa,
int128[] memory qInternal,
uint256 i,
uint256 j,
int128 limitPrice
) internal pure returns (int128 amountIn, int128 amountOut) {
require(i < nAssets && j < nAssets, "LMSR: idx");
require(limitPrice > int128(0), "LMSR: limitPrice <= 0"); require(limitPrice > int128(0), "LMSR: limitPrice <= 0");
// Compute b and ensure positivity before deriving invB // Compute b and ensure positivity before deriving invB
int128 sizeMetric = _computeSizeMetric(qInternal); int128 b = _computeB(s);
require(sizeMetric > int128(0), "LMSR: size metric zero");
int128 b = kappa.mul(sizeMetric);
require(b > int128(0), "LMSR: b<=0"); require(b > int128(0), "LMSR: b<=0");
// Precompute reciprocal of b to avoid repeated divisions // Precompute reciprocal of b to avoid repeated divisions
int128 invB = ABDKMath64x64.div(ONE, b); int128 invB = ABDKMath64x64.div(ONE, b);
// Guard: output asset must have non-zero effective weight to avoid degenerate/div-by-zero-like conditions // Guard: output asset must have non-zero effective weight to avoid degenerate/div-by-zero-like conditions
require(qInternal[j] > int128(0), "LMSR: e_j==0"); require(s.qInternal[j] > int128(0), "LMSR: e_j==0");
// Compute r0 = exp((q_i - q_j) / b) directly using invB // Compute r0 = exp((q_i - q_j) / b) directly using invB
int128 r0 = _exp(qInternal[i].sub(qInternal[j]).mul(invB)); int128 r0 = _exp(s.qInternal[i].sub(s.qInternal[j]).mul(invB));
// Mirror swapAmountsForExactInput behavior: treat invalid r0 as an error condition. // Mirror swapAmountsForExactInput behavior: treat invalid r0 as an error condition.
// Revert if r0 is non-positive (no finite trade under a price limit). // Revert if r0 is non-positive (no finite trade under a price limit).
@@ -295,12 +224,15 @@ library LMSRStabilized {
// The maximum output y corresponding to that input: // The maximum output y corresponding to that input:
// y = b * ln(1 + (e_i/e_j) * (1 - exp(-x_max/b))) // y = b * ln(1 + (e_i/e_j) * (1 - exp(-x_max/b)))
int128 expTerm = ONE.sub(_exp(maxDeltaExponent.neg())); int128 expTerm = ONE.sub(_exp(maxDeltaExponent.neg()));
int128 innerTerm = r0.mul(expTerm); int128 innerTerm = r0.mul(expTerm);
int128 lnTerm = _ln(ONE.add(innerTerm)); int128 lnTerm = _ln(ONE.add(innerTerm));
int128 maxOutput = b.mul(lnTerm); int128 maxOutput = b.mul(lnTerm);
// Current balance of asset j (in 64.64) // Current balance of asset j (in 64.64)
int128 qj64 = qInternal[j]; int128 qj64 = s.qInternal[j];
// Initialize outputs to the computed maxima // Initialize outputs to the computed maxima
amountIn = amountInMax; amountIn = amountInMax;
@@ -340,45 +272,20 @@ library LMSRStabilized {
uint256 i, uint256 i,
int128 a int128 a
) internal view returns (int128 amountIn, int128 amountOut) { ) internal view returns (int128 amountIn, int128 amountOut) {
return swapAmountsForMint(s.nAssets, s.kappa, s.qInternal, i, a); require(i < s.nAssets, "LMSR: idx");
}
/// @notice Pure version: Compute LP-size increase when minting from a single-token input using bisection only.
/// @dev Solve for α >= 0 such that:
/// a = α*q_i + sum_{j != i} x_j(α)
/// where x_j(α) is the input to swap i->j that yields y_j = α*q_j and
/// x_j = b * ln( r0_j / (r0_j + 1 - exp(y_j / b)) ), r0_j = exp((q_i - q_j)/b).
/// Bisection is used (no Newton) to keep implementation compact and gas-friendly.
/// @param nAssets Number of assets in the pool
/// @param kappa Liquidity parameter κ (64.64 fixed point)
/// @param qInternal Cached internal balances in 64.64 fixed-point format
/// @param i Index of input asset
/// @param a Amount of input asset (in int128 format, 64.64 fixed-point)
/// @return amountIn Actual amount of input consumed
/// @return amountOut LP size-metric increase (alpha * S)
function swapAmountsForMint(
uint256 nAssets,
int128 kappa,
int128[] memory qInternal,
uint256 i,
int128 a
) internal pure returns (int128 amountIn, int128 amountOut) {
require(i < nAssets, "LMSR: idx");
require(a > int128(0), "LMSR: amount <= 0"); require(a > int128(0), "LMSR: amount <= 0");
int128 sizeMetric = _computeSizeMetric(qInternal); int128 b = _computeB(s);
require(sizeMetric > int128(0), "LMSR: size metric zero");
int128 b = kappa.mul(sizeMetric);
require(b > int128(0), "LMSR: b<=0"); require(b > int128(0), "LMSR: b<=0");
int128 invB = ABDKMath64x64.div(ONE, b); int128 invB = ABDKMath64x64.div(ONE, b);
int128 S = sizeMetric; int128 S = _computeSizeMetric(s.qInternal);
uint256 n = nAssets; uint256 n = s.nAssets;
// Precompute r0_j = exp((q_i - q_j) / b) for all j to avoid recomputing during search. // Precompute r0_j = exp((q_i - q_j) / b) for all j to avoid recomputing during search.
int128[] memory r0 = new int128[](n); int128[] memory r0 = new int128[](n);
for (uint256 j = 0; j < n; ) { for (uint256 j = 0; j < n; ) {
r0[j] = _exp(qInternal[i].sub(qInternal[j]).mul(invB)); r0[j] = _exp(s.qInternal[i].sub(s.qInternal[j]).mul(invB));
unchecked { j++; } unchecked { j++; }
} }
@@ -414,7 +321,7 @@ library LMSRStabilized {
// loop j != i // loop j != i
for (uint256 j = 0; j < n; ) { for (uint256 j = 0; j < n; ) {
if (j != i) { if (j != i) {
int128 yj = alpha.mul(qInternal[j]); // target output y_j = alpha * q_j int128 yj = alpha.mul(s.qInternal[j]); // target output y_j = alpha * q_j
if (yj > int128(0)) { if (yj > int128(0)) {
int128 expArg = yj.mul(invB); int128 expArg = yj.mul(invB);
// Guard exp arg // Guard exp arg
@@ -432,7 +339,7 @@ library LMSRStabilized {
unchecked { j++; } unchecked { j++; }
} }
int128 aReq = fail ? int128(type(int128).max) : alpha.mul(qInternal[i]).add(sumX); int128 aReq = fail ? int128(type(int128).max) : alpha.mul(s.qInternal[i]).add(sumX);
if (aReq >= a || high >= alphaCap) { if (aReq >= a || high >= alphaCap) {
break; break;
@@ -454,7 +361,7 @@ library LMSRStabilized {
for (uint256 j = 0; j < n; ) { for (uint256 j = 0; j < n; ) {
if (j != i) { if (j != i) {
int128 yj = alpha.mul(qInternal[j]); int128 yj = alpha.mul(s.qInternal[j]);
if (yj > int128(0)) { if (yj > int128(0)) {
int128 expArg = yj.mul(invB); int128 expArg = yj.mul(invB);
if (expArg > EXP_LIMIT) { fail = true; break; } if (expArg > EXP_LIMIT) { fail = true; break; }
@@ -471,7 +378,7 @@ library LMSRStabilized {
unchecked { j++; } unchecked { j++; }
} }
int128 aReq = fail ? int128(type(int128).max) : alpha.mul(qInternal[i]).add(sumX); int128 aReq = fail ? int128(type(int128).max) : alpha.mul(s.qInternal[i]).add(sumX);
if (aReq > a) { if (aReq > a) {
// mid requires more input than provided -> decrease alpha // mid requires more input than provided -> decrease alpha
@@ -501,7 +408,7 @@ library LMSRStabilized {
bool failFinal = false; bool failFinal = false;
for (uint256 j = 0; j < n; ) { for (uint256 j = 0; j < n; ) {
if (j != i) { if (j != i) {
int128 yj = alphaFinal.mul(qInternal[j]); int128 yj = alphaFinal.mul(s.qInternal[j]);
if (yj > int128(0)) { if (yj > int128(0)) {
int128 expArg = yj.mul(invB); int128 expArg = yj.mul(invB);
if (expArg > EXP_LIMIT) { failFinal = true; break; } if (expArg > EXP_LIMIT) { failFinal = true; break; }
@@ -523,7 +430,7 @@ library LMSRStabilized {
return (int128(0), int128(0)); return (int128(0), int128(0));
} }
int128 aRequired = alphaFinal.mul(qInternal[i]).add(sumXFinal); int128 aRequired = alphaFinal.mul(s.qInternal[i]).add(sumXFinal);
// amountIn is actual consumed input (may be <= provided a) // amountIn is actual consumed input (may be <= provided a)
amountIn = aRequired; amountIn = aRequired;
@@ -552,56 +459,28 @@ library LMSRStabilized {
uint256 i, uint256 i,
int128 alpha int128 alpha
) internal view returns (int128 amountOut, int128 amountIn) { ) internal view returns (int128 amountOut, int128 amountIn) {
return swapAmountsForBurn(s.nAssets, s.kappa, s.qInternal, i, alpha); require(i < s.nAssets, "LMSR: idx");
}
/// @notice Pure version: Compute single-asset payout when burning a proportional share alpha of the pool.
/// @dev Simulate q_after = (1 - alpha) * q, return the amount of asset `i` the burner
/// would receive after swapping each other asset's withdrawn portion into `i`.
/// For each j != i:
/// - wrapper holds a_j = alpha * q_j
/// - swap j->i with closed-form exact-input formula using the current q_local
/// - cap output to q_local[i] when necessary (solve inverse for input used)
/// Treat any per-asset rhs<=0 as "this asset contributes zero" (do not revert).
/// Revert only if the final single-asset payout is zero.
/// @param nAssets Number of assets in the pool
/// @param kappa Liquidity parameter κ (64.64 fixed point)
/// @param qInternal Cached internal balances in 64.64 fixed-point format
/// @param i Index of output asset
/// @param alpha Proportional share to burn (0 < alpha <= 1)
/// @return amountOut Amount of asset i received (in 64.64 fixed-point)
/// @return amountIn LP size-metric redeemed (alpha * S)
function swapAmountsForBurn(
uint256 nAssets,
int128 kappa,
int128[] memory qInternal,
uint256 i,
int128 alpha
) internal pure returns (int128 amountOut, int128 amountIn) {
require(i < nAssets, "LMSR: idx");
require(alpha > int128(0) && alpha <= ONE, "LMSR: alpha"); require(alpha > int128(0) && alpha <= ONE, "LMSR: alpha");
int128 sizeMetric = _computeSizeMetric(qInternal); int128 b = _computeB(s);
require(sizeMetric > int128(0), "LMSR: size metric zero");
int128 b = kappa.mul(sizeMetric);
require(b > int128(0), "LMSR: b<=0"); require(b > int128(0), "LMSR: b<=0");
int128 invB = ABDKMath64x64.div(ONE, b); int128 invB = ABDKMath64x64.div(ONE, b);
uint256 n = nAssets; uint256 n = s.nAssets;
// Size metric and burned size (amountIn returned) // Size metric and burned size (amountIn returned)
int128 S = sizeMetric; int128 S = _computeSizeMetric(s.qInternal);
amountIn = alpha.mul(S); // total size-metric redeemed amountIn = alpha.mul(S); // total size-metric redeemed
// Build q_local := q_after_burn = (1 - alpha) * q // Build q_local := q_after_burn = (1 - alpha) * q
int128[] memory qLocal = new int128[](n); int128[] memory qLocal = new int128[](n);
for (uint256 j = 0; j < n; ) { for (uint256 j = 0; j < n; ) {
qLocal[j] = qInternal[j].mul(ONE.sub(alpha)); qLocal[j] = s.qInternal[j].mul(ONE.sub(alpha));
unchecked { j++; } unchecked { j++; }
} }
// Start totalOut with direct portion of asset i redeemed // Start totalOut with direct portion of asset i redeemed
int128 totalOut = alpha.mul(qInternal[i]); int128 totalOut = alpha.mul(s.qInternal[i]);
// Track whether any non-zero contribution was produced // Track whether any non-zero contribution was produced
bool anyNonZero = (totalOut > int128(0)); bool anyNonZero = (totalOut > int128(0));
@@ -609,7 +488,7 @@ library LMSRStabilized {
// For each asset j != i, swap the withdrawn a_j := alpha * q_j into i // For each asset j != i, swap the withdrawn a_j := alpha * q_j into i
for (uint256 j = 0; j < n; ) { for (uint256 j = 0; j < n; ) {
if (j != i) { if (j != i) {
int128 aj = alpha.mul(qInternal[j]); // wrapper-held withdrawn amount of j int128 aj = alpha.mul(s.qInternal[j]); // wrapper-held withdrawn amount of j
if (aj > int128(0)) { if (aj > int128(0)) {
// expArg = aj / b // expArg = aj / b
int128 expArg = aj.mul(invB); int128 expArg = aj.mul(invB);
@@ -716,7 +595,6 @@ library LMSRStabilized {
// Compute new total for validation // Compute new total for validation
int128 newTotal = _computeSizeMetric(newQInternal); int128 newTotal = _computeSizeMetric(newQInternal);
require(newTotal > int128(0), "LMSR: new total zero"); require(newTotal > int128(0), "LMSR: new total zero");
// Update the cached qInternal with new values // Update the cached qInternal with new values
@@ -724,30 +602,20 @@ library LMSRStabilized {
s.qInternal[i] = newQInternal[i]; s.qInternal[i] = newQInternal[i];
unchecked { i++; } unchecked { i++; }
} }
} }
/// @notice Price-share of asset i: exp(z_i) / Z (64.64) /// @notice Price-share of asset i: exp(z_i) / Z (64.64)
function priceShare(State storage s, uint256 i) internal view returns (int128) { function priceShare(State storage s, uint256 i) internal view returns (int128) {
return priceShare(s.kappa, s.qInternal, i); int128 b = _computeB(s);
} uint len = s.qInternal.length;
/// @notice Pure version: Price-share of asset i: exp(z_i) / Z (64.64)
/// @param kappa Liquidity parameter κ (64.64 fixed point)
/// @param qInternal Cached internal balances in 64.64 fixed-point format
/// @param i Index of asset
/// @return Price share in 64.64 fixed-point format
function priceShare(int128 kappa, int128[] memory qInternal, uint256 i) internal pure returns (int128) {
int128 sizeMetric = _computeSizeMetric(qInternal);
require(sizeMetric > int128(0), "LMSR: size metric zero");
int128 b = kappa.mul(sizeMetric);
uint len = qInternal.length;
require(len > 0, "LMSR: no assets"); require(len > 0, "LMSR: no assets");
// Precompute reciprocal of b and perform a single pass that tracks M, Z, and e_i // Precompute reciprocal of b and perform a single pass that tracks M, Z, and e_i
int128 invB = ABDKMath64x64.div(ONE, b); int128 invB = ABDKMath64x64.div(ONE, b);
// Initialize from the first element // Initialize from the first element
int128 M = qInternal[0].mul(invB); int128 M = s.qInternal[0].mul(invB);
int128 Z = ONE; // exp(0) int128 Z = ONE; // exp(0)
int128 e_i_acc; int128 e_i_acc;
bool setEi; bool setEi;
@@ -758,7 +626,7 @@ library LMSRStabilized {
} }
for (uint idx = 1; idx < len; ) { for (uint idx = 1; idx < len; ) {
int128 yi = qInternal[idx].mul(invB); int128 yi = s.qInternal[idx].mul(invB);
if (yi <= M) { if (yi <= M) {
// Add contribution under current center // Add contribution under current center
int128 term = _exp(yi.sub(M)); int128 term = _exp(yi.sub(M));
@@ -787,7 +655,7 @@ library LMSRStabilized {
if (!setEi) { if (!setEi) {
// Only possible when len == 1 and i != 0, guarded by caller invariants typically // Only possible when len == 1 and i != 0, guarded by caller invariants typically
// Fallback: compute directly (kept for completeness) // Fallback: compute directly (kept for completeness)
int128 yi = qInternal[i].mul(invB); int128 yi = s.qInternal[i].mul(invB);
e_i_acc = _exp(yi.sub(M)); e_i_acc = _exp(yi.sub(M));
} }
@@ -797,54 +665,27 @@ library LMSRStabilized {
/// @notice Marginal price of `base` in terms of `quote` (p_quote / p_base) as Q64.64 /// @notice Marginal price of `base` in terms of `quote` (p_quote / p_base) as Q64.64
/// @dev Returns exp((q_quote - q_base) / b). Indices must be valid and b > 0. /// @dev Returns exp((q_quote - q_base) / b). Indices must be valid and b > 0.
function price(State storage s, uint256 baseTokenIndex, uint256 quoteTokenIndex) internal view returns (int128) { function price(State storage s, uint256 baseTokenIndex, uint256 quoteTokenIndex) internal view returns (int128) {
return price(s.nAssets, s.kappa, s.qInternal, baseTokenIndex, quoteTokenIndex); require(baseTokenIndex < s.nAssets && quoteTokenIndex < s.nAssets, "LMSR: idx");
} int128 b = _computeB(s);
/// @notice Pure version: Marginal price of `base` in terms of `quote` (p_quote / p_base) as Q64.64
/// @dev Returns exp((q_quote - q_base) / b). Indices must be valid and b > 0.
/// @param nAssets Number of assets in the pool
/// @param kappa Liquidity parameter κ (64.64 fixed point)
/// @param qInternal Cached internal balances in 64.64 fixed-point format
/// @param baseTokenIndex Index of base token
/// @param quoteTokenIndex Index of quote token
/// @return Price in 64.64 fixed-point format
function price(uint256 nAssets, int128 kappa, int128[] memory qInternal, uint256 baseTokenIndex, uint256 quoteTokenIndex) internal pure returns (int128) {
require(baseTokenIndex < nAssets && quoteTokenIndex < nAssets, "LMSR: idx");
int128 sizeMetric = _computeSizeMetric(qInternal);
require(sizeMetric > int128(0), "LMSR: size metric zero");
int128 b = kappa.mul(sizeMetric);
require(b > int128(0), "LMSR: b<=0"); require(b > int128(0), "LMSR: b<=0");
// Use reciprocal of b to avoid repeated divisions // Use reciprocal of b to avoid repeated divisions
int128 invB = ABDKMath64x64.div(ONE, b); int128 invB = ABDKMath64x64.div(ONE, b);
// Marginal price p_quote / p_base = exp((q_quote - q_base) / b) // Marginal price p_quote / p_base = exp((q_quote - q_base) / b)
return _exp(qInternal[quoteTokenIndex].sub(qInternal[baseTokenIndex]).mul(invB)); return _exp(s.qInternal[quoteTokenIndex].sub(s.qInternal[baseTokenIndex]).mul(invB));
} }
/// @notice Price of one unit of the LP size-metric (S = sum q_i) denominated in `quote` asset (Q64.64) /// @notice Price of one unit of the LP size-metric (S = sum q_i) denominated in `quote` asset (Q64.64)
/// @dev Computes: poolPrice_quote = (1 / S) * sum_j q_j * exp((q_j - q_quote) / b) /// @dev Computes: poolPrice_quote = (1 / S) * sum_j q_j * exp((q_j - q_quote) / b)
function poolPrice(State storage s, uint256 quoteTokenIndex) internal view returns (int128) { function poolPrice(State storage s, uint256 quoteTokenIndex) internal view returns (int128) {
return poolPrice(s.nAssets, s.kappa, s.qInternal, quoteTokenIndex); require(quoteTokenIndex < s.nAssets, "LMSR: idx");
}
/// @notice Pure version: Price of one unit of the LP size-metric (S = sum q_i) denominated in `quote` asset (Q64.64)
/// @dev Computes: poolPrice_quote = (1 / S) * sum_j q_j * exp((q_j - q_quote) / b)
/// @param nAssets Number of assets in the pool
/// @param kappa Liquidity parameter κ (64.64 fixed point)
/// @param qInternal Cached internal balances in 64.64 fixed-point format
/// @param quoteTokenIndex Index of quote token
/// @return Pool price in 64.64 fixed-point format
function poolPrice(uint256 nAssets, int128 kappa, int128[] memory qInternal, uint256 quoteTokenIndex) internal pure returns (int128) {
require(quoteTokenIndex < nAssets, "LMSR: idx");
// Compute b and ensure positivity // Compute b and ensure positivity
int128 sizeMetric = _computeSizeMetric(qInternal); int128 b = _computeB(s);
require(sizeMetric > int128(0), "LMSR: size metric zero");
int128 b = kappa.mul(sizeMetric);
require(b > int128(0), "LMSR: b<=0"); require(b > int128(0), "LMSR: b<=0");
// Compute total size metric S = sum q_i // Compute total size metric S = sum q_i
int128 S = sizeMetric; int128 S = _computeSizeMetric(s.qInternal);
require(S > int128(0), "LMSR: size zero"); require(S > int128(0), "LMSR: size zero");
// Precompute reciprocal of b // Precompute reciprocal of b
@@ -852,12 +693,12 @@ library LMSRStabilized {
// Accumulate weighted exponentials: sum_j q_j * exp((q_j - q_quote) / b) // Accumulate weighted exponentials: sum_j q_j * exp((q_j - q_quote) / b)
int128 acc = int128(0); int128 acc = int128(0);
uint256 n = nAssets; uint256 n = s.nAssets;
for (uint256 j = 0; j < n; ) { for (uint256 j = 0; j < n; ) {
// factor = exp((q_j - q_quote) / b) // factor = exp((q_j - q_quote) / b)
int128 factor = _exp(qInternal[j].sub(qInternal[quoteTokenIndex]).mul(invB)); int128 factor = _exp(s.qInternal[j].sub(s.qInternal[quoteTokenIndex]).mul(invB));
// term = q_j * factor // term = q_j * factor
int128 term = qInternal[j].mul(factor); int128 term = s.qInternal[j].mul(factor);
acc = acc.add(term); acc = acc.add(term);
unchecked { j++; } unchecked { j++; }
} }

View File

@@ -1,8 +1,8 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30; pragma solidity ^0.8.30;
import {ABDKMath64x64} from "../lib/abdk-libraries-solidity/ABDKMath64x64.sol"; import "@abdk/ABDKMath64x64.sol";
import {LMSRStabilized} from "./LMSRStabilized.sol"; import "./LMSRStabilized.sol";
/// @notice Specialized functions for the 2-asset stablecoin case /// @notice Specialized functions for the 2-asset stablecoin case
library LMSRStabilizedBalancedPair { library LMSRStabilizedBalancedPair {
@@ -104,7 +104,7 @@ library LMSRStabilizedBalancedPair {
if (aLimit64 < a) { if (aLimit64 < a) {
a = aLimit64; a = aLimit64;
} else { } else {
// console2.log("balanced2: limitPrice does not truncate input"); // console2.log("balanced2: limitPrice does not truncate input");
} }
// Note: after potential truncation we continue with the polynomial approximation below // Note: after potential truncation we continue with the polynomial approximation below

View File

@@ -1,75 +1,28 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30; pragma solidity ^0.8.30;
import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import "./IPartyPlanner.sol";
import {SafeERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; import "./LMSRStabilized.sol";
import {IPartyPlanner} from "./IPartyPlanner.sol"; import "./PartyPool.sol";
import {LMSRStabilized} from "./LMSRStabilized.sol"; import "./PartyPoolBalancedPair.sol";
import {IPartyPool} from "./IPartyPool.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {PartyPoolMintImpl} from "./PartyPoolMintImpl.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {PartyPoolSwapImpl} from "./PartyPoolSwapImpl.sol";
import {IPartyPoolDeployer} from "./PartyPoolDeployer.sol";
/// @title PartyPlanner /// @title PartyPlanner
/// @notice Factory contract for creating and tracking PartyPool instances /// @notice Factory contract for creating and tracking PartyPool instances
contract PartyPlanner is IPartyPlanner { contract PartyPlanner is IPartyPlanner {
using SafeERC20 for IERC20; using SafeERC20 for IERC20;
int128 private constant ONE = int128(1) << 64; int128 private constant FIXED_ONE_64x64 = int128(1) << 64;
/// @notice Address of the Mint implementation contract used by all pools created by this factory
PartyPoolMintImpl private immutable MINT_IMPL;
function mintImpl() external view returns (PartyPoolMintImpl) { return MINT_IMPL; }
/// @notice Address of the SwapMint implementation contract used by all pools created by this factory
PartyPoolSwapImpl private immutable SWAP_MINT_IMPL;
function swapMintImpl() external view returns (PartyPoolSwapImpl) { return SWAP_MINT_IMPL; }
/// @notice Protocol fee share (ppm) applied to fees collected by pools created by this planner
uint256 private immutable PROTOCOL_FEE_PPM;
function protocolFeePpm() external view returns (uint256) { return PROTOCOL_FEE_PPM; }
/// @notice Address to receive protocol fees for pools created by this planner (may be address(0))
address private immutable PROTOCOL_FEE_ADDRESS;
function protocolFeeAddress() external view returns (address) { return PROTOCOL_FEE_ADDRESS; }
IPartyPoolDeployer private immutable NORMAL_POOL_DEPLOYER;
IPartyPoolDeployer private immutable BALANCED_PAIR_DEPLOYER;
// On-chain pool indexing // On-chain pool indexing
IPartyPool[] private _allPools; PartyPool[] private _allPools;
IERC20[] private _allTokens; IERC20[] private _allTokens;
mapping(IPartyPool => bool) private _poolSupported; mapping(PartyPool => bool) private _poolSupported;
mapping(IERC20 => bool) private _tokenSupported; mapping(IERC20 => bool) private _tokenSupported;
mapping(IERC20 => IPartyPool[]) private _poolsByToken; mapping(IERC20 => PartyPool[]) private _poolsByToken;
/// @param _swapMintImpl address of the SwapMint implementation contract to be used by all pools /// Main createPool variant: accepts kappa directly (preferred).
/// @param _mintImpl address of the Mint implementation contract to be used by all pools function createPool(
/// @param _protocolFeePpm protocol fee share (ppm) to be used for pools created by this planner
/// @param _protocolFeeAddress recipient address for protocol fees for pools created by this planner (may be address(0))
constructor(
PartyPoolSwapImpl _swapMintImpl,
PartyPoolMintImpl _mintImpl,
IPartyPoolDeployer _deployer,
IPartyPoolDeployer _balancedPairDeployer,
uint256 _protocolFeePpm,
address _protocolFeeAddress
) {
require(address(_swapMintImpl) != address(0), "Planner: swapMintImpl address cannot be zero");
SWAP_MINT_IMPL = _swapMintImpl;
require(address(_mintImpl) != address(0), "Planner: mintImpl address cannot be zero");
MINT_IMPL = _mintImpl;
require(address(_deployer) != address(0), "Planner: deployer address cannot be zero");
NORMAL_POOL_DEPLOYER = _deployer;
require(address(_balancedPairDeployer) != address(0), "Planner: balanced pair deployer address cannot be zero");
BALANCED_PAIR_DEPLOYER = _balancedPairDeployer;
require(_protocolFeePpm < 1_000_000, "Planner: protocol fee >= ppm");
PROTOCOL_FEE_PPM = _protocolFeePpm;
PROTOCOL_FEE_ADDRESS = _protocolFeeAddress;
}
/// Main newPool variant: accepts kappa directly (preferred).
function newPool(
// Pool constructor args // Pool constructor args
string memory name_, string memory name_,
string memory symbol_, string memory symbol_,
@@ -85,7 +38,7 @@ contract PartyPlanner is IPartyPlanner {
uint256[] memory initialDeposits, uint256[] memory initialDeposits,
uint256 initialLpAmount, uint256 initialLpAmount,
uint256 deadline uint256 deadline
) public returns (IPartyPool pool, uint256 lpAmount) { ) public returns (PartyPool pool, uint256 lpAmount) {
// Validate inputs // Validate inputs
require(deadline == 0 || block.timestamp <= deadline, "Planner: deadline exceeded"); require(deadline == 0 || block.timestamp <= deadline, "Planner: deadline exceeded");
require(_tokens.length == initialDeposits.length, "Planner: tokens and deposits length mismatch"); require(_tokens.length == initialDeposits.length, "Planner: tokens and deposits length mismatch");
@@ -96,20 +49,9 @@ contract PartyPlanner is IPartyPlanner {
require(_kappa > int128(0), "Planner: kappa must be > 0"); require(_kappa > int128(0), "Planner: kappa must be > 0");
// Create a new PartyPool instance (kappa-based constructor) // Create a new PartyPool instance (kappa-based constructor)
IPartyPoolDeployer deployer = _stable && _tokens.length == 2 ? BALANCED_PAIR_DEPLOYER : NORMAL_POOL_DEPLOYER; pool = _stable && _tokens.length == 2 ?
pool = deployer.deploy( new PartyPoolBalancedPair(name_, symbol_, _tokens, _bases, _kappa, _swapFeePpm, _flashFeePpm) :
name_, new PartyPool(name_, symbol_, _tokens, _bases, _kappa, _swapFeePpm, _flashFeePpm);
symbol_,
_tokens,
_bases,
_kappa,
_swapFeePpm,
_flashFeePpm,
PROTOCOL_FEE_PPM,
PROTOCOL_FEE_ADDRESS,
PartyPoolSwapImpl(SWAP_MINT_IMPL),
MINT_IMPL
);
_allPools.push(pool); _allPools.push(pool);
_poolSupported[pool] = true; _poolSupported[pool] = true;
@@ -134,7 +76,6 @@ 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');
} }
} }
@@ -142,10 +83,8 @@ contract PartyPlanner is IPartyPlanner {
lpAmount = pool.initialMint(receiver, initialLpAmount); lpAmount = pool.initialMint(receiver, initialLpAmount);
} }
// NOTE that the slippage target is only exactly achieved in completely balanced pools where all assets are /// Backwards-compatible convenience overload: compute kappa from (tradeFrac, targetSlippage) then call kappa-based createPool.
// priced the same. This target is actually a minimum slippage that the pool imposes on traders, and the actual function createPool(
// slippage cost can be multiples bigger in practice due to pool inventory imbalances.
function newPool(
// Pool constructor args (old signature) // Pool constructor args (old signature)
string memory name_, string memory name_,
string memory symbol_, string memory symbol_,
@@ -162,16 +101,16 @@ contract PartyPlanner is IPartyPlanner {
uint256[] memory initialDeposits, uint256[] memory initialDeposits,
uint256 initialLpAmount, uint256 initialLpAmount,
uint256 deadline uint256 deadline
) external returns (IPartyPool pool, uint256 lpAmount) { ) external returns (PartyPool pool, uint256 lpAmount) {
// Validate fixed-point fractions: must be less than 1.0 in 64.64 fixed-point // Validate fixed-point fractions: must be less than 1.0 in 64.64 fixed-point
require(_tradeFrac < ONE, "Planner: tradeFrac must be < 1 (64.64)"); require(_tradeFrac < FIXED_ONE_64x64, "Planner: tradeFrac must be < 1 (64.64)");
require(_targetSlippage < ONE, "Planner: targetSlippage must be < 1 (64.64)"); require(_targetSlippage < FIXED_ONE_64x64, "Planner: targetSlippage must be < 1 (64.64)");
// Compute kappa from slippage params using LMSR helper (kappa depends only on n, f and s) // Compute kappa from slippage params using LMSR helper (kappa depends only on n, f and s)
int128 computedKappa = LMSRStabilized.computeKappaFromSlippage(_tokens.length, _tradeFrac, _targetSlippage); int128 computedKappa = LMSRStabilized.computeKappaFromSlippage(_tokens.length, _tradeFrac, _targetSlippage);
// Delegate to the kappa-based newPool variant // Delegate to the kappa-based createPool variant
return newPool( return createPool(
name_, name_,
symbol_, symbol_,
_tokens, _tokens,
@@ -190,7 +129,7 @@ contract PartyPlanner is IPartyPlanner {
/// @inheritdoc IPartyPlanner /// @inheritdoc IPartyPlanner
function getPoolSupported(address pool) external view returns (bool) { function getPoolSupported(address pool) external view returns (bool) {
return _poolSupported[IPartyPool(pool)]; return _poolSupported[PartyPool(pool)];
} }
/// @inheritdoc IPartyPlanner /// @inheritdoc IPartyPlanner
@@ -199,19 +138,19 @@ contract PartyPlanner is IPartyPlanner {
} }
/// @inheritdoc IPartyPlanner /// @inheritdoc IPartyPlanner
function getAllPools(uint256 offset, uint256 limit) external view returns (IPartyPool[] memory pools) { function getAllPools(uint256 offset, uint256 limit) external view returns (PartyPool[] memory pools) {
uint256 totalPools = _allPools.length; uint256 totalPools = _allPools.length;
// If offset is beyond array bounds, return empty array // If offset is beyond array bounds, return empty array
if (offset >= totalPools) { if (offset >= totalPools) {
return new IPartyPool[](0); return new PartyPool[](0);
} }
// Calculate actual number of pools to return (respecting bounds) // Calculate actual number of pools to return (respecting bounds)
uint256 itemsToReturn = (offset + limit > totalPools) ? (totalPools - offset) : limit; uint256 itemsToReturn = (offset + limit > totalPools) ? (totalPools - offset) : limit;
// Create result array of appropriate size // Create result array of appropriate size
pools = new IPartyPool[](itemsToReturn); pools = new PartyPool[](itemsToReturn);
// Fill the result array // Fill the result array
for (uint256 i = 0; i < itemsToReturn; i++) { for (uint256 i = 0; i < itemsToReturn; i++) {
@@ -255,20 +194,20 @@ contract PartyPlanner is IPartyPlanner {
} }
/// @inheritdoc IPartyPlanner /// @inheritdoc IPartyPlanner
function getPoolsByToken(IERC20 token, uint256 offset, uint256 limit) external view returns (IPartyPool[] memory pools) { function getPoolsByToken(IERC20 token, uint256 offset, uint256 limit) external view returns (PartyPool[] memory pools) {
IPartyPool[] storage tokenPools = _poolsByToken[token]; PartyPool[] storage tokenPools = _poolsByToken[token];
uint256 totalPools = tokenPools.length; uint256 totalPools = tokenPools.length;
// If offset is beyond array bounds, return empty array // If offset is beyond array bounds, return empty array
if (offset >= totalPools) { if (offset >= totalPools) {
return new IPartyPool[](0); return new PartyPool[](0);
} }
// Calculate actual number of pools to return (respecting bounds) // Calculate actual number of pools to return (respecting bounds)
uint256 itemsToReturn = (offset + limit > totalPools) ? (totalPools - offset) : limit; uint256 itemsToReturn = (offset + limit > totalPools) ? (totalPools - offset) : limit;
// Create result array of appropriate size // Create result array of appropriate size
pools = new IPartyPool[](itemsToReturn); pools = new PartyPool[](itemsToReturn);
// Fill the result array // Fill the result array
for (uint256 i = 0; i < itemsToReturn; i++) { for (uint256 i = 0; i < itemsToReturn; i++) {

View File

@@ -1,22 +1,12 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30; pragma solidity ^0.8.30;
import "../lib/openzeppelin-contracts/contracts/interfaces/IERC3156FlashBorrower.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ABDKMath64x64} from "../lib/abdk-libraries-solidity/ABDKMath64x64.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {Address} from "../lib/openzeppelin-contracts/contracts/utils/Address.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {ERC20External} from "./ERC20External.sol"; import "./PoolLib.sol";
import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import "./IPartyPool.sol";
import {IPartyFlashCallback} from "./IPartyFlashCallback.sol"; import {PoolBase} from "./PoolBase.sol";
import {IPartyPool} from "./IPartyPool.sol";
import {LMSRStabilizedBalancedPair} from "./LMSRStabilizedBalancedPair.sol";
import {LMSRStabilized} from "./LMSRStabilized.sol";
import {PartyPoolBase} from "./PartyPoolBase.sol";
import {PartyPoolMintImpl} from "./PartyPoolMintImpl.sol";
import {PartyPoolSwapImpl} from "./PartyPoolSwapImpl.sol";
import {Proxy} from "../lib/openzeppelin-contracts/contracts/proxy/Proxy.sol";
import {ReentrancyGuard} from "../lib/openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol";
import {SafeERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
import {IERC3156FlashLender} from "../lib/openzeppelin-contracts/contracts/interfaces/IERC3156FlashLender.sol";
/// @title PartyPool - LMSR-backed multi-asset pool with LP ERC20 token /// @title PartyPool - LMSR-backed multi-asset pool with LP ERC20 token
/// @notice A multi-asset liquidity pool backed by the LMSRStabilized pricing model. /// @notice A multi-asset liquidity pool backed by the LMSRStabilized pricing model.
@@ -27,63 +17,43 @@ import {IERC3156FlashLender} from "../lib/openzeppelin-contracts/contracts/inter
/// - Exact-input swaps and swaps-to-price-limits, /// - Exact-input swaps and swaps-to-price-limits,
/// - Flash loans via a callback interface. /// - Flash loans via a callback interface.
/// ///
/// @dev The contract stores per-token uint "bases" used to scale token units into the internal Q64.64 /// @dev The contract uses PoolLib for all implementation logic and maintains state in a PoolLib.State struct
/// representation used by the LMSR library. Cached on-chain uint balances are kept to reduce balanceOf calls. contract PartyPool is PoolBase, IPartyPool {
/// The contract uses ceiling/floor rules described in function comments to bias rounding in favor of the pool using PoolLib for PoolLib.State;
/// (i.e., floor outputs to users, ceil inputs/fees where appropriate).
contract PartyPool is PartyPoolBase, ERC20External, IPartyPool {
using ABDKMath64x64 for int128;
using LMSRStabilized for LMSRStabilized.State;
using SafeERC20 for IERC20;
/// @notice Liquidity parameter κ (Q64.64) used by the LMSR kernel: b = κ * S(q) /// @notice Liquidity parameter κ (Q64.64) used by the LMSR kernel: b = κ * S(q)
/// @dev Pool is constructed with a fixed κ. Clients that previously passed tradeFrac/targetSlippage /// @dev Pool is constructed with a fixed κ. Clients may use LMSRStabilized.computeKappaFromSlippage(...) to
/// should use LMSRStabilized.computeKappaFromSlippage(...) to derive κ and pass it here. /// derive κ and pass it here.
int128 private immutable KAPPA; // kappa in Q64.64 int128 public immutable kappa; // kappa in Q64.64
function kappa() external view returns (int128) { return KAPPA; }
/// @notice Per-swap fee in parts-per-million (ppm). Fee is taken from input amounts before LMSR computations. /// @notice Per-swap fee in parts-per-million (ppm). Fee is taken from input amounts before LMSR computations.
uint256 private immutable SWAP_FEE_PPM; uint256 public immutable swapFeePpm;
function swapFeePpm() external view returns (uint256) { return SWAP_FEE_PPM; }
/// @notice Flash-loan fee in parts-per-million (ppm) applied to flash borrow amounts. /// @notice Flash-loan fee in parts-per-million (ppm) applied to flash borrow amounts.
uint256 private immutable FLASH_FEE_PPM; uint256 public immutable flashFeePpm;
function flashFeePpm() external view returns (uint256) { return FLASH_FEE_PPM; }
/// @notice Protocol fee share (ppm) applied to fees collected by the pool (floored when accrued)
uint256 private immutable PROTOCOL_FEE_PPM;
function protocolFeePpm() external view returns (uint256) { return PROTOCOL_FEE_PPM; }
/// @notice Address to which collected protocol tokens will be sent on collectProtocolFees()
address private immutable PROTOCOL_FEE_ADDRESS;
function protocolFeeAddress() external view returns (address) { return PROTOCOL_FEE_ADDRESS; }
// @inheritdoc IPartyPool
function allProtocolFeesOwed() external view returns (uint256[] memory) { return protocolFeesOwed; }
/// @notice Address of the Mint implementation contract for delegatecall
PartyPoolMintImpl private immutable MINT_IMPL;
function mintImpl() external view returns (PartyPoolMintImpl) { return MINT_IMPL; }
/// @notice Address of the SwapMint implementation contract for delegatecall
PartyPoolSwapImpl private immutable SWAP_IMPL;
function swapMintImpl() external view returns (PartyPoolSwapImpl) { return SWAP_IMPL; }
/// @inheritdoc IPartyPool /// @inheritdoc IPartyPool
function getToken(uint256 i) external view returns (IERC20) { return tokens[i]; } function tokens(uint256 i) external view returns (IERC20) { return s.tokens[i]; }
/// @inheritdoc IPartyPool /// @inheritdoc IPartyPool
function numTokens() external view returns (uint256) { return tokens.length; } function numTokens() external view returns (uint256) { return s.tokens.length; }
/// @inheritdoc IPartyPool /// @inheritdoc IPartyPool
function allTokens() external view returns (IERC20[] memory) { return tokens; } function allTokens() external view returns (IERC20[] memory) { return s.tokens; }
/// @inheritdoc IPartyPool /// @inheritdoc IPartyPool
function denominators() external view returns (uint256[] memory) { return bases; } function denominators() external view returns (uint256[] memory) { return s.bases; }
function LMSR() external view returns (LMSRStabilized.State memory) { return lmsr; } /// @notice Mapping from token address => (index+1). A zero value indicates the token is not in the pool.
/// @dev Use index = tokenAddressToIndexPlusOne[token] - 1 when non-zero.
function tokenAddressToIndexPlusOne(IERC20 token) external view returns (uint256) {
return s.tokenAddressToIndexPlusOne[token];
}
/// @notice Scale factor used when converting LMSR Q64.64 totals to LP token units (uint).
/// @dev LP tokens are minted in units equal to ABDK.mulu(lastTotalQ64x64, LP_SCALE).
uint256 public constant LP_SCALE = 1e18; // Scale used to convert LMSR lastTotal (Q64.64) into LP token units (uint)
/// @param name_ LP token name /// @param name_ LP token name
/// @param symbol_ LP token symbol /// @param symbol_ LP token symbol
@@ -92,8 +62,6 @@ contract PartyPool is PartyPoolBase, ERC20External, IPartyPool {
/// @param kappa_ liquidity parameter κ (Q64.64) used to derive b = κ * S(q) /// @param kappa_ liquidity parameter κ (Q64.64) used to derive b = κ * S(q)
/// @param swapFeePpm_ fee in parts-per-million, taken from swap input amounts before LMSR calculations /// @param swapFeePpm_ fee in parts-per-million, taken from swap input amounts before LMSR calculations
/// @param flashFeePpm_ fee in parts-per-million, taken for flash loans /// @param flashFeePpm_ fee in parts-per-million, taken for flash loans
/// @param swapImpl_ address of the SwapMint implementation contract
/// @param mintImpl_ address of the Mint implementation contract
constructor( constructor(
string memory name_, string memory name_,
string memory symbol_, string memory symbol_,
@@ -101,114 +69,106 @@ contract PartyPool is PartyPoolBase, ERC20External, IPartyPool {
uint256[] memory bases_, uint256[] memory bases_,
int128 kappa_, int128 kappa_,
uint256 swapFeePpm_, uint256 swapFeePpm_,
uint256 flashFeePpm_, uint256 flashFeePpm_
uint256 protocolFeePpm_, // NEW: protocol share of fees (ppm) ) ERC20(name_, symbol_) {
address protocolFeeAddress_, // NEW: recipient for collected protocol tokens kappa = kappa_;
PartyPoolSwapImpl swapImpl_,
PartyPoolMintImpl mintImpl_
) ERC20External(name_, symbol_) {
require(tokens_.length > 1, "Pool: need >1 asset");
require(tokens_.length == bases_.length, "Pool: lengths mismatch");
tokens = tokens_;
bases = bases_;
KAPPA = kappa_;
require(swapFeePpm_ < 1_000_000, "Pool: fee >= ppm"); require(swapFeePpm_ < 1_000_000, "Pool: fee >= ppm");
SWAP_FEE_PPM = swapFeePpm_; swapFeePpm = swapFeePpm_;
require(flashFeePpm_ < 1_000_000, "Pool: flash fee >= ppm"); require(flashFeePpm_ < 1_000_000, "Pool: flash fee >= ppm");
FLASH_FEE_PPM = flashFeePpm_; flashFeePpm = flashFeePpm_;
require(protocolFeePpm_ < 1_000_000, "Pool: protocol fee >= ppm");
PROTOCOL_FEE_PPM = protocolFeePpm_;
PROTOCOL_FEE_ADDRESS = protocolFeeAddress_;
SWAP_IMPL = swapImpl_;
MINT_IMPL = mintImpl_;
uint256 n = tokens_.length; // Initialize state using library
s.initialize(tokens_, bases_);
// Initialize LMSR state nAssets; full init occurs on first mint when quantities are known.
lmsr.nAssets = n;
// Initialize token address to index mapping
for (uint i = 0; i < n;) {
tokenAddressToIndexPlusOne[tokens_[i]] = i + 1;
unchecked {i++;}
}
// Initialize caches to zero and protocol ledger
cachedUintBalances = new uint256[](n);
protocolFeesOwed = new uint256[](n);
} }
/* ---------------------- /* ----------------------
Initialization / Mint / Burn (LP token managed) Initialization / Mint / Burn (LP token managed)
---------------------- */ ---------------------- */
/// @inheritdoc IPartyPool /// @inheritdoc IPartyPool
function initialMint(address receiver, uint256 lpTokens) external function mintDepositAmounts(uint256 lpTokenAmount) public view returns (uint256[] memory depositAmounts) {
return s.mintDepositAmounts(lpTokenAmount, totalSupply());
}
/// @notice Initial mint to set up pool for the first time.
/// @dev Assumes tokens have already been transferred to the pool prior to calling.
/// Can only be called when the pool is uninitialized (totalSupply() == 0 or lmsr.nAssets == 0).
/// @param receiver address that receives the LP tokens
/// @param lpTokens The number of LP tokens to issue for this mint. If 0, then the number of tokens returned will equal the LMSR internal q total
function initialMint(address receiver, uint256 lpTokens) external nonReentrant
returns (uint256 lpMinted) { returns (uint256 lpMinted) {
bytes memory data = abi.encodeWithSignature( lpMinted = s.initialMint(receiver, lpTokens, kappa, totalSupply());
"initialMint(address,uint256,int128)", _mint(receiver, lpMinted);
receiver,
lpTokens,
KAPPA
);
bytes memory result = Address.functionDelegateCall(address(MINT_IMPL), data);
return abi.decode(result, (uint256));
} }
/// @notice Proportional mint for existing pool. /// @notice Proportional mint for existing pool.
/// @dev This function forwards the call to the mint implementation via delegatecall /// @dev Payer must approve the required token amounts before calling.
/// Can only be called when pool is already initialized (totalSupply() > 0 and lmsr.nAssets > 0).
/// Rounds follow the pool-favorable conventions documented in helpers (ceil inputs, floor outputs).
/// @param payer address that provides the input tokens /// @param payer address that provides the input tokens
/// @param receiver address that receives the LP tokens /// @param receiver address that receives the LP tokens
/// @param lpTokenAmount desired amount of LP tokens to mint /// @param lpTokenAmount desired amount of LP tokens to mint
/// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore. /// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore.
function mint(address payer, address receiver, uint256 lpTokenAmount, uint256 deadline) external function mint(address payer, address receiver, uint256 lpTokenAmount, uint256 deadline) external nonReentrant
returns (uint256 lpMinted) { returns (uint256 lpMinted) {
bytes memory data = abi.encodeWithSignature( lpMinted = s.mint(payer, receiver, lpTokenAmount, deadline, totalSupply());
"mint(address,address,uint256,uint256)", _mint(receiver, lpMinted);
payer, }
receiver,
lpTokenAmount, /// @inheritdoc IPartyPool
deadline function burnReceiveAmounts(uint256 lpTokenAmount) external view returns (uint256[] memory withdrawAmounts) {
); return s.burnReceiveAmounts(lpTokenAmount, totalSupply());
bytes memory result = Address.functionDelegateCall(address(MINT_IMPL), data);
return abi.decode(result, (uint256));
} }
/// @notice Burn LP tokens and withdraw the proportional basket to receiver. /// @notice Burn LP tokens and withdraw the proportional basket to receiver.
/// @dev This function forwards the call to the burn implementation via delegatecall /// @dev Payer must own or approve the LP tokens being burned. The function updates LMSR state
/// proportionally to reflect the reduced pool size after the withdrawal.
/// @param payer address that provides the LP tokens to burn /// @param payer address that provides the LP tokens to burn
/// @param receiver address that receives the withdrawn tokens /// @param receiver address that receives the withdrawn tokens
/// @param lpAmount amount of LP tokens to burn (proportional withdrawal) /// @param lpAmount amount of LP tokens to burn (proportional withdrawal)
/// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore. /// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore.
function burn(address payer, address receiver, uint256 lpAmount, uint256 deadline) external function burn(address payer, address receiver, uint256 lpAmount, uint256 deadline) external nonReentrant {
returns (uint256[] memory withdrawAmounts) { /* uint256[] memory withdrawAmounts = */ s.burn(payer, receiver, lpAmount, deadline, totalSupply(), balanceOf(payer));
bytes memory data = abi.encodeWithSignature(
"burn(address,address,uint256,uint256)", // Handle LP token burning with allowance
payer, if (msg.sender != payer) {
receiver, uint256 allowed = allowance(payer, msg.sender);
lpAmount, require(allowed >= lpAmount, "burn: allowance insufficient");
deadline _approve(payer, msg.sender, allowed - lpAmount);
); }
bytes memory result = Address.functionDelegateCall(address(MINT_IMPL), data); _burn(payer, lpAmount);
return abi.decode(result, (uint256[]));
} }
/* ---------------------- /* ----------------------
Swaps Swaps
---------------------- */ ---------------------- */
/* /// @inheritdoc IPartyPool
function swapAmounts( function swapAmounts(
uint256 inputTokenIndex, uint256 inputTokenIndex,
uint256 outputTokenIndex, uint256 outputTokenIndex,
uint256 maxAmountIn, uint256 maxAmountIn,
int128 limitPrice int128 limitPrice
) external view returns (uint256 amountIn, uint256 amountOut, uint256 fee) { ) virtual external view returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
(uint256 grossIn, uint256 outUint,,,, uint256 feeUint) = _quoteSwapExactIn(inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice); return s.swapAmounts(inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice, swapFeePpm);
return (grossIn, outUint, feeUint); }
/// @inheritdoc IPartyPool
function swapToLimitAmounts(
uint256 inputTokenIndex,
uint256 outputTokenIndex,
int128 limitPrice
) external view returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
return s.swapToLimitAmounts(inputTokenIndex, outputTokenIndex, limitPrice, swapFeePpm);
}
/// @inheritdoc IPartyPool
function swapMintAmounts(
uint256 inputTokenIndex,
uint256 maxAmountIn
) external view returns (uint256 totalTransfer, uint256 amountIn, uint256 fee, uint256 lpMinted) {
return s.swapMintAmounts(inputTokenIndex, maxAmountIn, swapFeePpm, totalSupply());
} }
*/
/// @inheritdoc IPartyPool /// @inheritdoc IPartyPool
function swap( function swap(
@@ -219,95 +179,14 @@ contract PartyPool is PartyPoolBase, ERC20External, IPartyPool {
uint256 maxAmountIn, uint256 maxAmountIn,
int128 limitPrice, int128 limitPrice,
uint256 deadline uint256 deadline
) external nonReentrant returns (uint256 amountIn, uint256 amountOut, uint256 fee) { ) virtual external nonReentrant returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
require(deadline == 0 || block.timestamp <= deadline, "swap: deadline exceeded"); return s.swap(payer, receiver, inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice, deadline, swapFeePpm);
// Compute amounts using the same path as views
(uint256 totalTransferAmount, uint256 amountOutUint, int128 amountInInternalUsed, int128 amountOutInternal, , uint256 feeUint) =
_quoteSwapExactIn(inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice);
// Transfer tokens
tokens[inputTokenIndex].safeTransferFrom(payer, address(this), totalTransferAmount);
uint256 balIAfter = IERC20(tokens[inputTokenIndex]).balanceOf(address(this));
tokens[outputTokenIndex].safeTransfer(receiver, amountOutUint);
uint256 balJAfter = IERC20(tokens[outputTokenIndex]).balanceOf(address(this));
// Accrue protocol share (floor) from the fee on input token
if (PROTOCOL_FEE_PPM > 0 && feeUint > 0) {
uint256 protoShare = (feeUint * PROTOCOL_FEE_PPM) / 1_000_000; // floor
if (protoShare > 0) {
protocolFeesOwed[inputTokenIndex] += protoShare;
}
}
// Update cached uint balances for i and j using effective balances (onchain - owed)
_recordCachedBalance(inputTokenIndex, balIAfter);
_recordCachedBalance(outputTokenIndex, balJAfter);
// Apply swap to LMSR state with the internal amounts actually used
lmsr.applySwap(inputTokenIndex, outputTokenIndex, amountInInternalUsed, amountOutInternal);
emit Swap(payer, receiver, tokens[inputTokenIndex], tokens[outputTokenIndex], totalTransferAmount, amountOutUint);
return (totalTransferAmount, amountOutUint, feeUint);
} }
/// @notice Internal quote for exact-input swap that mirrors swap() rounding and fee application /// @notice Swap up to the price limit; computes max input to reach limit then performs swap.
/// @dev Returns amounts consistent with swap() semantics: grossIn includes fees (ceil), amountOut is floored. /// @dev If balances prevent fully reaching the limit, the function caps and returns actuals.
/// @return grossIn amount to transfer in (inclusive of fee), amountOutUint output amount (uint), /// The payer must transfer the exact gross input computed by the view.
/// amountInInternalUsed and amountOutInternal (64.64), amountInUintNoFee input amount excluding fee (uint), /// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore.
/// feeUint fee taken from the gross input (uint)
function _quoteSwapExactIn(
uint256 inputTokenIndex,
uint256 outputTokenIndex,
uint256 maxAmountIn,
int128 limitPrice
)
internal
view
returns (
uint256 grossIn,
uint256 amountOutUint,
int128 amountInInternalUsed,
int128 amountOutInternal,
uint256 amountInUintNoFee,
uint256 feeUint
)
{
uint256 n = tokens.length;
require(inputTokenIndex < n && outputTokenIndex < n, "swap: idx");
require(maxAmountIn > 0, "swap: input zero");
// Estimate max net input (fee on gross rounded up, then subtract)
(, uint256 netUintForSwap) = _computeFee(maxAmountIn, SWAP_FEE_PPM);
// Convert to internal (floor)
int128 deltaInternalI = _uintToInternalFloor(netUintForSwap, bases[inputTokenIndex]);
require(deltaInternalI > int128(0), "swap: input too small after fee");
// Compute internal amounts using LMSR (exact-input with price limit)
(amountInInternalUsed, amountOutInternal) = _swapAmountsForExactInput(inputTokenIndex, outputTokenIndex, deltaInternalI, limitPrice);
// Convert actual used input internal -> uint (ceil)
amountInUintNoFee = _internalToUintCeil(amountInInternalUsed, bases[inputTokenIndex]);
// Compute gross transfer including fee on the used input (ceil)
feeUint = 0;
grossIn = amountInUintNoFee;
if (SWAP_FEE_PPM > 0) {
feeUint = _ceilFee(amountInUintNoFee, SWAP_FEE_PPM);
grossIn += feeUint;
}
// Ensure within user max
require(grossIn <= maxAmountIn, "swap: transfer exceeds max");
// Compute output (floor)
amountOutUint = _internalToUintFloor(amountOutInternal, bases[outputTokenIndex]);
}
/// @inheritdoc IPartyPool
function swapToLimit( function swapToLimit(
address payer, address payer,
address receiver, address receiver,
@@ -316,24 +195,12 @@ contract PartyPool is PartyPoolBase, ERC20External, IPartyPool {
int128 limitPrice, int128 limitPrice,
uint256 deadline uint256 deadline
) external returns (uint256 amountInUsed, uint256 amountOut, uint256 fee) { ) external returns (uint256 amountInUsed, uint256 amountOut, uint256 fee) {
bytes memory data = abi.encodeWithSignature( return s.swapToLimit(payer, receiver, inputTokenIndex, outputTokenIndex, limitPrice, deadline, swapFeePpm);
'swapToLimit(address,address,uint256,uint256,int128,uint256,uint256,uint256)',
payer,
receiver,
inputTokenIndex,
outputTokenIndex,
limitPrice,
deadline,
SWAP_FEE_PPM,
PROTOCOL_FEE_PPM
);
bytes memory result = Address.functionDelegateCall(address(SWAP_IMPL), data);
return abi.decode(result, (uint256,uint256,uint256));
} }
/// @notice Single-token mint: deposit a single token, charge swap-LMSR cost, and mint LP. /// @notice Single-token mint: deposit a single token, charge swap-LMSR cost, and mint LP.
/// @dev This function forwards the call to the swapMint implementation via delegatecall /// @dev swapMint executes as an exact-in planned swap followed by proportional scaling of qInternal.
/// The function emits SwapMint (gross, net, fee) and also emits Mint for LP issuance.
/// @param payer who transfers the input token /// @param payer who transfers the input token
/// @param receiver who receives the minted LP tokens /// @param receiver who receives the minted LP tokens
/// @param inputTokenIndex index of the input token /// @param inputTokenIndex index of the input token
@@ -346,24 +213,21 @@ contract PartyPool is PartyPoolBase, ERC20External, IPartyPool {
uint256 inputTokenIndex, uint256 inputTokenIndex,
uint256 maxAmountIn, uint256 maxAmountIn,
uint256 deadline uint256 deadline
) external returns (uint256 lpMinted) { ) external nonReentrant returns (uint256 lpMinted) {
bytes memory data = abi.encodeWithSignature( lpMinted = s.swapMint(payer, receiver, inputTokenIndex, maxAmountIn, deadline, swapFeePpm, totalSupply());
"swapMint(address,address,uint256,uint256,uint256,uint256,uint256)", _mint(receiver, lpMinted);
payer, }
receiver,
inputTokenIndex,
maxAmountIn,
deadline,
SWAP_FEE_PPM,
PROTOCOL_FEE_PPM
);
bytes memory result = Address.functionDelegateCall(address(MINT_IMPL), data); /// @inheritdoc IPartyPool
return abi.decode(result, (uint256)); function burnSwapAmounts(
uint256 lpAmount,
uint256 inputTokenIndex
) external view returns (uint256 amountOut) {
return s.burnSwapAmounts(lpAmount, inputTokenIndex, swapFeePpm, totalSupply());
} }
/// @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 `inputTokenIndex` and send to receiver.
/// @dev This function forwards the call to the burnSwap implementation via delegatecall /// @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 payer who burns LP tokens
/// @param receiver who receives the single asset /// @param receiver who receives the single asset
/// @param lpAmount amount of LP tokens to burn /// @param lpAmount amount of LP tokens to burn
@@ -376,87 +240,49 @@ contract PartyPool is PartyPoolBase, ERC20External, IPartyPool {
uint256 lpAmount, uint256 lpAmount,
uint256 inputTokenIndex, uint256 inputTokenIndex,
uint256 deadline uint256 deadline
) external returns (uint256 amountOutUint) { ) external nonReentrant returns (uint256 amountOutUint) {
bytes memory data = abi.encodeWithSignature( amountOutUint = s.burnSwap(payer, receiver, lpAmount, inputTokenIndex, deadline, swapFeePpm, totalSupply(), balanceOf(payer));
"burnSwap(address,address,uint256,uint256,uint256,uint256,uint256)",
payer,
receiver,
lpAmount,
inputTokenIndex,
deadline,
SWAP_FEE_PPM,
PROTOCOL_FEE_PPM
);
bytes memory result = Address.functionDelegateCall(address(MINT_IMPL), data); // Handle LP token burning with allowance
return abi.decode(result, (uint256)); if (msg.sender != payer) {
uint256 allowed = allowance(payer, msg.sender);
require(allowed >= lpAmount, "burnSwap: allowance insufficient");
_approve(payer, msg.sender, allowed - lpAmount);
}
_burn(payer, lpAmount);
} }
/// @inheritdoc IPartyPool
function flashRepaymentAmounts(uint256[] memory loanAmounts) external view
returns (uint256[] memory repaymentAmounts) {
return s.flashRepaymentAmounts(loanAmounts, flashFeePpm);
}
bytes32 internal constant FLASH_CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan"); /// @notice Receive token amounts and require them to be repaid plus a fee inside a callback.
/// @dev The caller must implement IPartyFlashCallback#partyFlashCallback which receives (amounts, repaymentAmounts, data).
/** /// This function verifies that, after the callback returns, the pool's balances have increased by at least the fees
* @dev Loan `amount` tokens to `receiver`, and takes it back plus a `flashFee` after the callback. /// for each borrowed token. Reverts if repayment (including fee) did not occur.
* @param receiver The contract receiving the tokens, needs to implement the `onFlashLoan(address user, uint256 amount, uint256 fee, bytes calldata)` interface. /// @param recipient The address which will receive the token amounts
* @param tokenAddr The loan currency. /// @param amounts The amount of each token to send (array length must equal pool size)
* @param amount The amount of tokens lent. /// @param data Any data to be passed through to the callback
* @param data A data parameter to be passed on to the `receiver` for any custom use. function flash(
*/ address recipient,
function flashLoan( uint256[] memory amounts,
IERC3156FlashBorrower receiver,
address tokenAddr,
uint256 amount,
bytes calldata data bytes calldata data
) external nonReentrant returns (bool) ) external nonReentrant {
{ s.flash(recipient, amounts, data, flashFeePpm);
IERC20 token = IERC20(tokenAddr);
require(amount <= token.balanceOf(address(this)));
uint256 tokenIndex = tokenAddressToIndexPlusOne[token] - 1;
(uint256 fee, ) = _computeFee(amount, FLASH_FEE_PPM);
// Compute protocol share of flash fee
if (PROTOCOL_FEE_PPM > 0 && fee > 0) {
uint256 protoShare = (fee * PROTOCOL_FEE_PPM) / 1_000_000; // floor
if (protoShare > 0) {
protocolFeesOwed[tokenIndex] += protoShare;
}
}
require(token.transfer(address(receiver), amount));
require(receiver.onFlashLoan(msg.sender, address(token), amount, fee, data) == FLASH_CALLBACK_SUCCESS);
require(token.transferFrom(address(receiver), address(this), amount + fee));
// Update cached balance for the borrowed token
uint256 balAfter = token.balanceOf(address(this));
_recordCachedBalance(tokenIndex, balAfter);
return true;
} }
/// @notice Marginal price of `base` in terms of `quote` (p_quote / p_base) as Q64.64
/// @notice Transfer all protocol fees to the configured protocolFeeAddress and zero the ledger. /// @dev Returns the LMSR marginal price directly (raw 64.64) for use by off-chain quoting logic.
/// @dev Anyone can call; must have protocolFeeAddress != address(0) to be operational. function price(uint256 baseTokenIndex, uint256 quoteTokenIndex) external view returns (int128) {
function collectProtocolFees() external nonReentrant { return s.price(baseTokenIndex, quoteTokenIndex);
address dest = PROTOCOL_FEE_ADDRESS;
require(dest != address(0), "collect: zero addr");
uint256 n = tokens.length;
for (uint256 i = 0; i < n; i++) {
uint256 owed = protocolFeesOwed[i];
if (owed == 0) continue;
uint256 bal = IERC20(tokens[i]).balanceOf(address(this));
require(bal >= owed, "collect: fee > bal");
protocolFeesOwed[i] = 0;
// transfer owed tokens to protocol destination
tokens[i].safeTransfer(dest, owed);
// update cached to effective onchain minus owed
cachedUintBalances[i] = bal - owed;
}
} }
function _swapAmountsForExactInput(uint256 i, uint256 j, int128 a, int128 limitPrice) internal virtual view /// @notice Price of one LP token denominated in `quote` asset as Q64.64
returns (int128 amountIn, int128 amountOut) { /// @dev Computes LMSR poolPrice (quote per unit qTotal) and scales it by totalSupply() / qTotal
return lmsr.swapAmountsForExactInput(i, j, a, limitPrice); /// to return price per LP token unit in quote asset (raw 64.64).
function poolPrice(uint256 quoteTokenIndex) external view returns (int128) {
return s.poolPrice(quoteTokenIndex, totalSupply());
} }
} }

View File

@@ -1,30 +1,42 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30; pragma solidity ^0.8.30;
import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import "./PartyPool.sol";
import {LMSRStabilizedBalancedPair} from "./LMSRStabilizedBalancedPair.sol"; import {PoolLibBalancedPair} from "./PoolLibBalancedPair.sol";
import {PartyPool} from "./PartyPool.sol";
import {PartyPoolMintImpl} from "./PartyPoolMintImpl.sol";
import {PartyPoolSwapImpl} from "./PartyPoolSwapImpl.sol";
contract PartyPoolBalancedPair is PartyPool { contract PartyPoolBalancedPair is PartyPool {
constructor( constructor(
string memory name_, string memory name_,
string memory symbol_, string memory symbol_,
IERC20[] memory tokens_, IERC20[] memory tokens_,
uint256[] memory bases_, uint256[] memory bases_,
int128 kappa_, int128 kappa_,
uint256 swapFeePpm_, uint256 swapFeePpm_,
uint256 flashFeePpm_, uint256 flashFeePpm_
uint256 protocolFeePpm_, // NEW: protocol share of fees (ppm) ) PartyPool(name_, symbol_, tokens_, bases_, kappa_, swapFeePpm_, flashFeePpm_) {
address protocolFeeAddress_, // NEW: recipient for collected protocol tokens
PartyPoolSwapImpl swapMintImpl_,
PartyPoolMintImpl mintImpl_
) PartyPool(name_, symbol_, tokens_, bases_, kappa_, swapFeePpm_, flashFeePpm_, protocolFeePpm_, protocolFeeAddress_, swapMintImpl_, mintImpl_)
{}
function _swapAmountsForExactInput(uint256 i, uint256 j, int128 a, int128 limitPrice) internal virtual override view
returns (int128 amountIn, int128 amountOut) {
return LMSRStabilizedBalancedPair.swapAmountsForExactInput(lmsr, i, j, a, limitPrice);
} }
function swapAmounts(
uint256 inputTokenIndex,
uint256 outputTokenIndex,
uint256 maxAmountIn,
int128 limitPrice
) virtual override external view returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
return PoolLibBalancedPair.swapAmounts(
s, inputTokenIndex, outputTokenIndex,
maxAmountIn, limitPrice, swapFeePpm);
}
function swap(
address payer,
address receiver,
uint256 inputTokenIndex,
uint256 outputTokenIndex,
uint256 maxAmountIn,
int128 limitPrice,
uint256 deadline
) virtual override external nonReentrant returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
return PoolLibBalancedPair.swap(s, payer, receiver, inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice, deadline, swapFeePpm);
}
} }

View File

@@ -1,88 +0,0 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30;
import {ABDKMath64x64} from "../lib/abdk-libraries-solidity/ABDKMath64x64.sol";
import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {ReentrancyGuard} from "../lib/openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol";
import {ERC20Internal} from "./ERC20Internal.sol";
import {LMSRStabilized} from "./LMSRStabilized.sol";
import {PartyPoolHelpers} from "./PartyPoolHelpers.sol";
/// @notice Abstract base contract that contains storage and internal helpers only.
/// No external/public functions or constructor here — derived implementations own immutables and constructors.
abstract contract PartyPoolBase is ERC20Internal, ReentrancyGuard, PartyPoolHelpers {
using ABDKMath64x64 for int128;
using LMSRStabilized for LMSRStabilized.State;
//
// Internal state (no immutables here; immutables belong to derived contracts)
//
// LMSR internal state
LMSRStabilized.State internal lmsr;
/// @notice Scale factor used when converting LMSR Q64.64 totals to LP token units (uint).
/// @dev LP tokens are minted in units equal to ABDK.mulu(lastTotalQ64x64, LP_SCALE).
uint256 internal constant LP_SCALE = 1e18; // Scale used to convert LMSR lastTotal (Q64.64) into LP token units (uint)
/// @notice Token addresses comprising the pool. Effectively immutable after construction.
/// @dev tokens[i] corresponds to the i-th asset and maps to index i in the internal LMSR arrays.
IERC20[] internal tokens; // effectively immutable since there is no interface to change the tokens
/// @notice Amounts of token owed as protocol fees but not yet collected. Subtract this amount from the pool's token
/// balances to compute the tokens owned by LP's.
uint256[] internal protocolFeesOwed;
/// @notice Per-token uint base denominators used to convert uint token amounts <-> internal Q64.64 representation.
/// @dev denominators()[i] is the base for tokens[i]. These bases are chosen by deployer and must match token decimals.
uint256[] internal bases; // per-token uint base used to scale token amounts <-> internal
/// @notice Mapping from token address => (index+1). A zero value indicates the token is not in the pool.
/// @dev Use index = tokenAddressToIndexPlusOne[token] - 1 when non-zero.
mapping(IERC20=>uint) internal tokenAddressToIndexPlusOne; // Uses index+1 so a result of 0 indicates a failed lookup
// Cached on-chain balances (uint) and internal 64.64 representation
// balance / base = internal
uint256[] internal cachedUintBalances;
/* ----------------------
Conversion & fee helpers (internal)
---------------------- */
// Convert uint token amount -> internal 64.64 (floor). Uses ABDKMath64x64.divu which truncates.
function _uintToInternalFloor(uint256 amount, uint256 base) internal pure returns (int128) {
// internal = amount / base (as Q64.64)
return ABDKMath64x64.divu(amount, base);
}
// Convert internal 64.64 -> uint token amount (floor). Uses ABDKMath64x64.mulu which floors the product.
function _internalToUintFloor(int128 internalAmount, uint256 base) internal pure returns (uint256) {
// uint = internal * base (floored)
return ABDKMath64x64.mulu(internalAmount, base);
}
// Convert internal 64.64 -> uint token amount (ceiling). Rounds up to protect the pool.
function _internalToUintCeil(int128 internalAmount, uint256 base) internal pure returns (uint256) {
// Get the floor value first
uint256 floorValue = ABDKMath64x64.mulu(internalAmount, base);
// Check if there was any fractional part by comparing to a reconstruction of the original
int128 reconstructed = ABDKMath64x64.divu(floorValue, base);
// If reconstructed is less than original, there was a fractional part that was truncated
if (reconstructed < internalAmount) {
return floorValue + 1;
}
return floorValue;
}
/// @dev Helper to record cached balances as effectiveBalance = onchain - owed. Reverts if owed > onchain.
function _recordCachedBalance(uint256 idx, uint256 onchainBal) internal {
uint256 owed = protocolFeesOwed[idx];
require(onchainBal >= owed, "balance < protocol owed");
cachedUintBalances[idx] = onchainBal - owed;
}
}

View File

@@ -1,87 +0,0 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30;
import "./PartyPoolMintImpl.sol";
import "./PartyPoolSwapImpl.sol";
import {PartyPool} from "./PartyPool.sol";
import {PartyPoolBalancedPair} from "./PartyPoolBalancedPair.sol";
// This pattern is needed because the PartyPlanner constructs two different types of pools (regular and balanced-pair)
// but doesn't have room to store the initialization code of both contracts. Therefore, we delegate pool construction.
interface IPartyPoolDeployer {
function deploy(
string memory name_,
string memory symbol_,
IERC20[] memory tokens_,
uint256[] memory bases_,
int128 kappa_,
uint256 swapFeePpm_,
uint256 flashFeePpm_,
uint256 protocolFeePpm_,
address protocolFeeAddress_,
PartyPoolSwapImpl swapImpl_,
PartyPoolMintImpl mintImpl_
) external returns (IPartyPool pool);
}
contract PartyPoolDeployer is IPartyPoolDeployer {
function deploy(
string memory name_,
string memory symbol_,
IERC20[] memory tokens_,
uint256[] memory bases_,
int128 kappa_,
uint256 swapFeePpm_,
uint256 flashFeePpm_,
uint256 protocolFeePpm_,
address protocolFeeAddress_,
PartyPoolSwapImpl swapImpl_,
PartyPoolMintImpl mintImpl_
) external returns (IPartyPool) {
return new PartyPool(
name_,
symbol_,
tokens_,
bases_,
kappa_,
swapFeePpm_,
flashFeePpm_,
protocolFeePpm_,
protocolFeeAddress_,
swapImpl_,
mintImpl_
);
}
}
contract PartyPoolBalancedPairDeployer is IPartyPoolDeployer {
function deploy(
string memory name_,
string memory symbol_,
IERC20[] memory tokens_,
uint256[] memory bases_,
int128 kappa_,
uint256 swapFeePpm_,
uint256 flashFeePpm_,
uint256 protocolFeePpm_,
address protocolFeeAddress_,
PartyPoolSwapImpl swapImpl_,
PartyPoolMintImpl mintImpl_
) external returns (IPartyPool) {
return new PartyPoolBalancedPair(
name_,
symbol_,
tokens_,
bases_,
kappa_,
swapFeePpm_,
flashFeePpm_,
protocolFeePpm_,
protocolFeeAddress_,
swapImpl_,
mintImpl_
);
}
}

View File

@@ -1,50 +0,0 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30;
import {ABDKMath64x64} from "../lib/abdk-libraries-solidity/ABDKMath64x64.sol";
abstract contract PartyPoolHelpers {
using ABDKMath64x64 for int128;
/// @notice Ceiling fee helper: computes ceil(x * feePpm / 1_000_000)
/// @dev Internal helper; public-facing functions use this to ensure fees round up in favor of pool.
function _ceilFee(uint256 x, uint256 feePpm) internal pure returns (uint256) {
if (feePpm == 0) return 0;
// ceil division: (num + denom - 1) / denom
return (x * feePpm + 1_000_000 - 1) / 1_000_000;
}
/// @notice Compute fee and net amounts for a gross input (fee rounded up to favor the pool).
/// @param gross total gross input
/// @param feePpm fee in ppm to apply
/// @return feeUint fee taken (uint) and netUint remaining for protocol use (uint)
function _computeFee(uint256 gross, uint256 feePpm) internal pure returns (uint256 feeUint, uint256 netUint) {
if (feePpm == 0) {
return (0, gross);
}
feeUint = _ceilFee(gross, feePpm);
netUint = gross - feeUint;
}
/// @notice Convenience: return gross = net + fee(net) using ceiling for fee.
/// @param netUint net amount
/// @param feePpm fee in ppm to apply
function _addFee(uint256 netUint, uint256 feePpm) internal pure returns (uint256 gross) {
if (feePpm == 0) return netUint;
uint256 fee = _ceilFee(netUint, feePpm);
return netUint + fee;
}
/// @notice Helper to compute size metric (sum of all asset quantities) from internal balances
/// @dev Returns the sum of all provided qInternal_ entries as a Q64.64 value.
function _computeSizeMetric(int128[] memory qInternal_) internal pure returns (int128) {
int128 total = int128(0);
for (uint i = 0; i < qInternal_.length; ) {
total = total.add(qInternal_[i]);
unchecked { i++; }
}
return total;
}
}

View File

@@ -1,593 +0,0 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30;
import {ABDKMath64x64} from "../lib/abdk-libraries-solidity/ABDKMath64x64.sol";
import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
import {ERC20Internal} from "./ERC20Internal.sol";
import {IPartyPool} from "./IPartyPool.sol";
import {LMSRStabilized} from "./LMSRStabilized.sol";
import {PartyPoolBase} from "./PartyPoolBase.sol";
/// @title PartyPoolMintImpl - Implementation contract for mint and burn functions
/// @notice This contract contains the mint and burn implementation that will be called via delegatecall
/// @dev This contract inherits from PartyPoolBase to access storage and internal functions
contract PartyPoolMintImpl is PartyPoolBase {
using ABDKMath64x64 for int128;
using LMSRStabilized for LMSRStabilized.State;
using SafeERC20 for IERC20;
//
// Initialization Mint
//
function initialMint(address receiver, uint256 lpTokens, int128 KAPPA) external nonReentrant
returns (uint256 lpMinted) {
uint256 n = tokens.length;
// Check if this is initial deposit - revert if not
bool isInitialDeposit = _totalSupply == 0 || lmsr.nAssets == 0;
require(isInitialDeposit, "initialMint: pool already initialized");
// Update cached balances for all assets
int128[] memory newQInternal = new int128[](n);
uint256[] memory depositAmounts = new uint256[](n);
for (uint i = 0; i < n; ) {
uint256 bal = IERC20(tokens[i]).balanceOf(address(this));
cachedUintBalances[i] = bal;
newQInternal[i] = _uintToInternalFloor(bal, bases[i]);
depositAmounts[i] = bal;
unchecked { i++; }
}
// Initialize the stabilized LMSR state with provided kappa
lmsr.init(newQInternal, KAPPA);
// Compute actual LP tokens to mint based on size metric (scaled)
if( lpTokens != 0 )
lpMinted = lpTokens;
else {
int128 newTotal = _computeSizeMetric(newQInternal);
lpMinted = ABDKMath64x64.mulu(newTotal, LP_SCALE);
}
require(lpMinted > 0, "initialMint: zero LP amount");
_mint(receiver, lpMinted);
emit IPartyPool.Mint(address(0), receiver, depositAmounts, lpMinted);
}
//
// Regular Mint and Burn
//
function mint(address payer, address receiver, uint256 lpTokenAmount, uint256 deadline) external nonReentrant
returns (uint256 lpMinted) {
require(deadline == 0 || block.timestamp <= deadline, "mint: deadline exceeded");
uint256 n = tokens.length;
// Check if this is NOT initial deposit - revert if it is
bool isInitialDeposit = _totalSupply == 0 || lmsr.nAssets == 0;
require(!isInitialDeposit, "mint: use initialMint for pool initialization");
require(lpTokenAmount > 0, "mint: zero LP amount");
// Capture old pool size metric (scaled) by computing from current balances
int128 oldTotal = _computeSizeMetric(lmsr.qInternal);
uint256 oldScaled = ABDKMath64x64.mulu(oldTotal, LP_SCALE);
// Calculate required deposit amounts for the desired LP tokens
uint256[] memory depositAmounts = mintAmounts(lpTokenAmount, lmsr.nAssets, _totalSupply, cachedUintBalances);
// Transfer in all token amounts
for (uint i = 0; i < n; ) {
if (depositAmounts[i] > 0) {
tokens[i].safeTransferFrom(payer, address(this), depositAmounts[i]);
}
unchecked { i++; }
}
// Update cached balances and internal q for all assets using depositAmounts
int128[] memory newQInternal = new int128[](n);
for (uint i = 0; i < n; ) {
uint256 newBal = cachedUintBalances[i] + depositAmounts[i];
cachedUintBalances[i] = newBal;
newQInternal[i] = _uintToInternalFloor(newBal, bases[i]);
unchecked { i++; }
}
// Update for proportional change
lmsr.updateForProportionalChange(newQInternal);
// Compute actual LP tokens to mint based on change in size metric (scaled)
// floor truncation rounds in favor of the pool
int128 newTotal = _computeSizeMetric(newQInternal);
uint256 newScaled = ABDKMath64x64.mulu(newTotal, LP_SCALE);
uint256 actualLpToMint;
uint256 delta = (newScaled > oldScaled) ? (newScaled - oldScaled) : 0;
// Proportional issuance: totalSupply * delta / oldScaled
if (delta > 0) {
// floor truncation rounds in favor of the pool
actualLpToMint = (_totalSupply * delta) / oldScaled;
} else {
actualLpToMint = 0;
}
// Ensure the calculated LP amount is not too different from requested
require(actualLpToMint > 0, "mint: zero LP minted");
// Allow actual amount to be at most 0.00001% less than requested
// This accounts for rounding in deposit calculations
uint256 minAcceptable = lpTokenAmount * 99_999 / 100_000;
require(actualLpToMint >= minAcceptable, "mint: insufficient LP minted");
_mint(receiver, actualLpToMint);
emit IPartyPool.Mint(payer, receiver, depositAmounts, actualLpToMint);
return actualLpToMint;
}
/// @notice Burn LP tokens and withdraw the proportional basket to receiver.
/// @dev Payer must own or approve the LP tokens being burned. The function updates LMSR state
/// proportionally to reflect the reduced pool size after the withdrawal.
/// @param payer address that provides the LP tokens to burn
/// @param receiver address that receives the withdrawn tokens
/// @param lpAmount amount of LP tokens to burn (proportional withdrawal)
/// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore.
function burn(address payer, address receiver, uint256 lpAmount, uint256 deadline) external nonReentrant
returns (uint256[] memory withdrawAmounts) {
require(deadline == 0 || block.timestamp <= deadline, "burn: deadline exceeded");
uint256 n = tokens.length;
require(lpAmount > 0, "burn: zero lp");
uint256 supply = _totalSupply;
require(supply > 0, "burn: empty supply");
// Use cached balances; assume standard ERC20 transfers without external interference
// Compute proportional withdrawal amounts for the requested LP amount (rounded down)
withdrawAmounts = burnAmounts(lpAmount, lmsr.nAssets, _totalSupply, cachedUintBalances);
// Transfer underlying tokens out to receiver according to computed proportions
for (uint i = 0; i < n; ) {
if (withdrawAmounts[i] > 0) {
tokens[i].safeTransfer(receiver, withdrawAmounts[i]);
}
unchecked { i++; }
}
// Update cached balances and internal q for all assets using computed withdrawals
int128[] memory newQInternal = new int128[](n);
for (uint i = 0; i < n; ) {
uint256 newBal = cachedUintBalances[i] - withdrawAmounts[i];
cachedUintBalances[i] = newBal;
newQInternal[i] = _uintToInternalFloor(newBal, bases[i]);
unchecked { i++; }
}
// Apply proportional update or deinitialize if drained
bool allZero = true;
for (uint i = 0; i < n; ) {
if (newQInternal[i] != int128(0)) {
allZero = false;
break;
}
unchecked { i++; }
}
if (allZero) {
lmsr.deinit();
} else {
lmsr.updateForProportionalChange(newQInternal);
}
// Burn exactly the requested LP amount from payer (authorization via allowance)
if (msg.sender != payer) {
uint256 allowed = _allowances[payer][msg.sender];
// Rely on Solidity's checked arithmetic to revert on underflow if allowance is insufficient
_approve(payer, msg.sender, allowed - lpAmount);
}
_burn(payer, lpAmount);
emit IPartyPool.Burn(payer, receiver, withdrawAmounts, lpAmount);
}
/// @notice Calculate the proportional deposit amounts required for a given LP token amount
/// @dev Returns the minimum token amounts (rounded up) that must be supplied to receive lpTokenAmount
/// LP tokens at current pool proportions. If the pool is empty (initial deposit) returns zeros
/// because the initial deposit is handled by transferring tokens then calling mint().
/// @param lpTokenAmount The amount of LP tokens desired
/// @return depositAmounts Array of token amounts to deposit (rounded up)
function mintAmounts(uint256 lpTokenAmount,
uint256 numAssets, uint256 totalSupply, uint256[] memory cachedUintBalances) public pure
returns (uint256[] memory depositAmounts) {
depositAmounts = new uint256[](numAssets);
// If this is the first mint or pool is empty, return zeros
// For first mint, tokens should already be transferred to the pool
if (totalSupply == 0 || numAssets == 0) {
return depositAmounts; // Return zeros, initial deposit handled differently
}
// lpTokenAmount / totalLpSupply = depositAmount / currentBalance
// Therefore: depositAmount = (lpTokenAmount * currentBalance) / totalLpSupply
// We round up to protect the pool
for (uint i = 0; i < numAssets; i++) {
uint256 currentBalance = cachedUintBalances[i];
// Calculate with rounding up: (a * b + c - 1) / c
depositAmounts[i] = (lpTokenAmount * currentBalance + totalSupply - 1) / totalSupply;
}
return depositAmounts;
}
function burnAmounts(uint256 lpTokenAmount,
uint256 numAssets, uint256 totalSupply, uint256[] memory cachedUintBalances) public pure
returns (uint256[] memory withdrawAmounts) {
withdrawAmounts = new uint256[](numAssets);
// If supply is zero or pool uninitialized, return zeros
if (totalSupply == 0 || numAssets == 0) {
return withdrawAmounts; // Return zeros, nothing to withdraw
}
// withdrawAmount = floor(lpTokenAmount * currentBalance / totalLpSupply)
for (uint i = 0; i < numAssets; i++) {
uint256 currentBalance = cachedUintBalances[i];
withdrawAmounts[i] = (lpTokenAmount * currentBalance) / totalSupply;
}
return withdrawAmounts;
}
//
// Swap-Mint and Burn-Swap
//
/// @notice Calculate the amounts for a swap mint operation
/// @dev This is a pure view function that computes swap mint amounts from provided state
/// @param inputTokenIndex index of the input token
/// @param maxAmountIn maximum amount of token to deposit (inclusive of fee)
/// @param swapFeePpm fee in parts-per-million
/// @param lmsrState current LMSR state
/// @param bases_ scaling bases for each token
/// @param totalSupply_ current total LP token supply
/// @return amountInUsed actual input amount used (excluding fee)
/// @return fee fee amount charged
/// @return lpMinted LP tokens that would be minted
function swapMintAmounts(
uint256 inputTokenIndex,
uint256 maxAmountIn,
uint256 swapFeePpm,
LMSRStabilized.State memory lmsrState,
uint256[] memory bases_,
uint256 totalSupply_
) public pure returns (uint256 amountInUsed, uint256 fee, uint256 lpMinted) {
require(inputTokenIndex < bases_.length, "swapMintAmounts: idx");
require(maxAmountIn > 0, "swapMintAmounts: input zero");
require(lmsrState.nAssets > 0, "swapMintAmounts: uninit pool");
// Compute fee on gross maxAmountIn to get an initial net estimate
uint256 feeGuess = 0;
uint256 netUintGuess = maxAmountIn;
if (swapFeePpm > 0) {
feeGuess = (maxAmountIn * swapFeePpm + 999999) / 1000000; // ceil fee
netUintGuess = maxAmountIn - feeGuess;
}
// Convert the net guess to internal (floor)
int128 netInternalGuess = _uintToInternalFloorPure(netUintGuess, bases_[inputTokenIndex]);
require(netInternalGuess > int128(0), "swapMintAmounts: input too small after fee");
// Use LMSR view to determine actual internal consumed and size-increase (ΔS) for mint
(int128 amountInInternalUsed, int128 sizeIncreaseInternal) =
LMSRStabilized.swapAmountsForMint(lmsrState.nAssets, lmsrState.kappa, lmsrState.qInternal,
inputTokenIndex, netInternalGuess);
// amountInInternalUsed may be <= netInternalGuess. Convert to uint (ceil) to determine actual transfer
amountInUsed = _internalToUintCeilPure(amountInInternalUsed, bases_[inputTokenIndex]);
require(amountInUsed > 0, "swapMintAmounts: input zero after internal conversion");
// Compute fee on the actual used input (ceiling)
fee = 0;
if (swapFeePpm > 0) {
fee = (amountInUsed * swapFeePpm + 999999) / 1000000; // ceil fee
}
uint256 totalTransfer = amountInUsed + fee;
require(totalTransfer > 0 && totalTransfer <= maxAmountIn, "swapMintAmounts: transfer exceeds max");
// Compute old and new scaled size metrics to determine LP minted
int128 oldTotal = _computeSizeMetricPure(lmsrState.qInternal);
require(oldTotal > int128(0), "swapMintAmounts: zero total");
uint256 oldScaled = ABDKMath64x64.mulu(oldTotal, LP_SCALE);
int128 newTotal = oldTotal.add(sizeIncreaseInternal);
uint256 newScaled = ABDKMath64x64.mulu(newTotal, LP_SCALE);
if (totalSupply_ == 0) {
// If somehow supply zero (shouldn't happen as lmsr.nAssets>0), mint newScaled
lpMinted = newScaled;
} else {
require(oldScaled > 0, "swapMintAmounts: oldScaled zero");
uint256 delta = (newScaled > oldScaled) ? (newScaled - oldScaled) : 0;
if (delta > 0) {
// floor truncation rounds in favor of pool
lpMinted = (totalSupply_ * delta) / oldScaled;
} else {
lpMinted = 0;
}
}
require(lpMinted > 0, "swapMintAmounts: zero LP minted");
}
/// @notice Single-token mint: deposit a single token, charge swap-LMSR cost, and mint LP.
/// @dev swapMint executes as an exact-in planned swap followed by proportional scaling of qInternal.
/// The function emits SwapMint (gross, net, fee) and also emits Mint for LP issuance.
/// @param payer who transfers the input token
/// @param receiver who receives the minted LP tokens
/// @param inputTokenIndex index of the input token
/// @param maxAmountIn maximum uint token input (inclusive of fee)
/// @param deadline optional deadline
/// @param swapFeePpm fee in parts-per-million for this pool
/// @return lpMinted actual LP minted (uint)
function swapMint(
address payer,
address receiver,
uint256 inputTokenIndex,
uint256 maxAmountIn,
uint256 deadline,
uint256 swapFeePpm,
uint256 protocolFeePpm
) external nonReentrant returns (uint256 lpMinted) {
uint256 n = tokens.length;
require(inputTokenIndex < n, "swapMint: idx");
require(maxAmountIn > 0, "swapMint: input zero");
require(deadline == 0 || block.timestamp <= deadline, "swapMint: deadline");
require(lmsr.nAssets > 0, "swapMint: uninit pool");
// compute fee on gross maxAmountIn to get an initial net estimate (we'll recompute based on actual used)
(, uint256 netUintGuess) = _computeFee(maxAmountIn, swapFeePpm);
// Convert the net guess to internal (floor)
int128 netInternalGuess = _uintToInternalFloor(netUintGuess, bases[inputTokenIndex]);
require(netInternalGuess > int128(0), "swapMint: input too small after fee");
// Use LMSR view to determine actual internal consumed and size-increase (ΔS) for mint
(int128 amountInInternalUsed, int128 sizeIncreaseInternal) = lmsr.swapAmountsForMint(inputTokenIndex, netInternalGuess);
// amountInInternalUsed may be <= netInternalGuess. Convert to uint (ceil) to determine actual transfer
uint256 amountInUint = _internalToUintCeil(amountInInternalUsed, bases[inputTokenIndex]);
require(amountInUint > 0, "swapMint: input zero after internal conversion");
// Compute fee on the actual used input and total transfer amount (ceiling)
uint256 feeUintActual = _ceilFee(amountInUint, swapFeePpm);
uint256 totalTransfer = amountInUint + feeUintActual;
require(totalTransfer > 0 && totalTransfer <= maxAmountIn, "swapMint: transfer exceeds max");
// Transfer tokens from payer (assume standard ERC20 without transfer fees)
tokens[inputTokenIndex].safeTransferFrom(payer, address(this), totalTransfer);
// Accrue protocol share (floor) from the fee on the input token
uint256 protoShare = 0;
if (protocolFeePpm > 0 && feeUintActual > 0) {
protoShare = (feeUintActual * protocolFeePpm) / 1_000_000;
if (protoShare > 0) {
protocolFeesOwed[inputTokenIndex] += protoShare;
}
}
// Update cached effective balance directly: add totalTransfer minus protocol share
cachedUintBalances[inputTokenIndex] += (totalTransfer - protoShare);
// Compute old and new scaled size metrics to determine LP minted
int128 oldTotal = _computeSizeMetric(lmsr.qInternal);
uint256 oldScaled = ABDKMath64x64.mulu(oldTotal, LP_SCALE);
int128 newTotal = oldTotal.add(sizeIncreaseInternal);
uint256 newScaled = ABDKMath64x64.mulu(newTotal, LP_SCALE);
uint256 actualLpToMint;
// Use natural ERC20 function since base contract inherits from ERC20
uint256 currentSupply = _totalSupply;
if (currentSupply == 0) {
// If somehow supply zero (shouldn't happen as lmsr.nAssets>0), mint newScaled
actualLpToMint = newScaled;
} else {
uint256 delta = (newScaled > oldScaled) ? (newScaled - oldScaled) : 0;
if (delta > 0) {
// floor truncation rounds in favor of pool
actualLpToMint = (currentSupply * delta) / oldScaled;
} else {
actualLpToMint = 0;
}
}
require(actualLpToMint > 0, "swapMint: zero LP minted");
// Update LMSR internal state: scale qInternal proportionally by newTotal/oldTotal
int128[] memory newQInternal = new int128[](n);
for (uint256 idx = 0; idx < n; idx++) {
// newQInternal[idx] = qInternal[idx] * (newTotal / oldTotal)
newQInternal[idx] = lmsr.qInternal[idx].mul(newTotal).div(oldTotal);
}
// Update cached internal and kappa via updateForProportionalChange
lmsr.updateForProportionalChange(newQInternal);
// Use natural ERC20 function since base contract inherits from ERC20
_mint(receiver, actualLpToMint);
// Emit SwapMint event with gross transfer, net input and fee (planned exact-in)
emit IPartyPool.SwapMint(payer, receiver, inputTokenIndex, totalTransfer, amountInUint, feeUintActual);
// Emit standard Mint event which records deposit amounts and LP minted
emit IPartyPool.Mint(payer, receiver, new uint256[](n), actualLpToMint);
// Note: depositAmounts array omitted (empty) since swapMint uses single-token input
return actualLpToMint;
}
/// @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 swapFeePpm fee in parts-per-million
/// @param lmsrState current LMSR state
/// @param bases_ scaling bases for each token
/// @param totalSupply_ current total LP token supply
/// @return amountOut amount of target asset that would be received
function burnSwapAmounts(
uint256 lpAmount,
uint256 inputTokenIndex,
uint256 swapFeePpm,
LMSRStabilized.State memory lmsrState,
uint256[] memory bases_,
uint256 totalSupply_
) public pure returns (uint256 amountOut) {
require(inputTokenIndex < bases_.length, "burnSwapAmounts: idx");
require(lpAmount > 0, "burnSwapAmounts: zero lp");
require(totalSupply_ > 0, "burnSwapAmounts: empty supply");
// alpha = lpAmount / supply as Q64.64
int128 alpha = ABDKMath64x64.divu(lpAmount, totalSupply_) // fraction of total supply to burn
.mul(ABDKMath64x64.divu(1000000-swapFeePpm, 1000000)); // adjusted for fee
// Use LMSR view to compute single-asset payout and burned size-metric
(int128 payoutInternal, ) = LMSRStabilized.swapAmountsForBurn(lmsrState.nAssets, lmsrState.kappa, lmsrState.qInternal,
inputTokenIndex, alpha);
// Convert payoutInternal -> uint (floor) to favor pool
amountOut = _internalToUintFloorPure(payoutInternal, bases_[inputTokenIndex]);
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.
/// @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 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
function burnSwap(
address payer,
address receiver,
uint256 lpAmount,
uint256 inputTokenIndex,
uint256 deadline,
uint256 swapFeePpm,
uint256 protocolFeePpm
) external nonReentrant returns (uint256 amountOutUint) {
uint256 n = tokens.length;
require(inputTokenIndex < n, "burnSwap: idx");
require(lpAmount > 0, "burnSwap: zero lp");
require(deadline == 0 || block.timestamp <= deadline, "burnSwap: deadline");
uint256 supply = _totalSupply;
require(supply > 0, "burnSwap: empty supply");
// alpha = lpAmount / supply as Q64.64 (adjusted for fee)
int128 alpha = ABDKMath64x64.divu(lpAmount, supply) // fraction of total supply to burn
.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);
// Convert payoutInternal -> uint (floor) to favor pool
amountOutUint = _internalToUintFloor(payoutInternal, bases[inputTokenIndex]);
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]);
uint256 feeTokenUint = (payoutGrossUint > amountOutUint) ? (payoutGrossUint - amountOutUint) : 0;
// Accrue protocol share (floor) from the token-side fee
uint256 protoShare = 0;
if (protocolFeePpm > 0 && feeTokenUint > 0) {
protoShare = (feeTokenUint * protocolFeePpm) / 1_000_000;
if (protoShare > 0) {
protocolFeesOwed[inputTokenIndex] += protoShare;
}
}
// Transfer the payout to receiver
tokens[inputTokenIndex].safeTransfer(receiver, amountOutUint);
// Burn LP tokens from payer (authorization via allowance)
if (msg.sender != payer) {
uint256 allowed = _allowances[payer][msg.sender];
_approve(payer, msg.sender, allowed - lpAmount);
}
_burn(payer, lpAmount);
// Update cached balances using computed payout and protocol fee; no on-chain reads
int128[] memory newQInternal = new int128[](n);
for (uint256 idx = 0; idx < n; idx++) {
uint256 newBal = cachedUintBalances[idx];
if (idx == inputTokenIndex) {
// Effective LP balance decreases by net payout and increased protocol owed
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 IPartyPool.BurnSwap(payer, receiver, inputTokenIndex, amountOutUint);
// If entire pool drained, deinit; else update proportionally
bool allZero = true;
for (uint256 idx = 0; idx < n; idx++) {
if (newQInternal[idx] != int128(0)) { allZero = false; break; }
}
if (allZero) {
lmsr.deinit();
} else {
lmsr.updateForProportionalChange(newQInternal);
}
emit IPartyPool.Burn(payer, receiver, new uint256[](n), lpAmount);
return amountOutUint;
}
/// @notice Pure version of _uintToInternalFloor for use in view functions
function _uintToInternalFloorPure(uint256 amount, uint256 base) internal pure returns (int128) {
// amount / base as Q64.64, floored
return ABDKMath64x64.divu(amount, base);
}
/// @notice Pure version of _internalToUintCeil for use in view functions
function _internalToUintCeilPure(int128 amount, uint256 base) internal pure returns (uint256) {
// Convert Q64.64 to uint with ceiling: ceil(amount * base)
// Use mulu which floors, then add remainder check for ceiling
uint256 floored = ABDKMath64x64.mulu(amount, base);
// Check if there's a fractional part by computing amount * base - floored
int128 baseQ64 = ABDKMath64x64.fromUInt(base);
int128 flooredQ64 = ABDKMath64x64.fromUInt(floored);
int128 product = amount.mul(baseQ64);
if (product > flooredQ64) {
return floored + 1; // Ceiling
}
return floored;
}
/// @notice Pure version of _internalToUintFloor for use in view functions
function _internalToUintFloorPure(int128 amount, uint256 base) internal pure returns (uint256) {
// Convert Q64.64 to uint with floor: floor(amount * base)
return ABDKMath64x64.mulu(amount, base);
}
/// @notice Pure version of _computeSizeMetric for use in view functions
function _computeSizeMetricPure(int128[] memory qInternal) internal pure returns (int128) {
int128 sum = int128(0);
for (uint256 i = 0; i < qInternal.length; i++) {
sum = sum.add(qInternal[i]);
}
return sum;
}
}

View File

@@ -1,142 +0,0 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30;
import {ABDKMath64x64} from "../lib/abdk-libraries-solidity/ABDKMath64x64.sol";
import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
import {LMSRStabilized} from "./LMSRStabilized.sol";
import {PartyPoolBase} from "./PartyPoolBase.sol";
import {IPartyPool} from "./IPartyPool.sol";
/// @title PartyPoolSwapMintImpl - Implementation contract for swapMint and burnSwap functions
/// @notice This contract contains the swapMint and burnSwap implementation that will be called via delegatecall
/// @dev This contract inherits from PartyPoolBase to access storage and internal functions
contract PartyPoolSwapImpl is PartyPoolBase {
using ABDKMath64x64 for int128;
using LMSRStabilized for LMSRStabilized.State;
using SafeERC20 for IERC20;
function swapToLimitAmounts(
uint256 inputTokenIndex,
uint256 outputTokenIndex,
int128 limitPrice,
uint256[] memory bases,
int128 kappa,
int128[] memory qInternal,
uint256 swapFeePpm
) external pure returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
// Compute internal maxima at the price limit
(int128 amountInInternal, int128 amountOutInternal) = LMSRStabilized.swapAmountsForPriceLimit(
bases.length, kappa, qInternal,
inputTokenIndex, outputTokenIndex, limitPrice);
// Convert input to uint (ceil) and output to uint (floor)
uint256 amountInUintNoFee = _internalToUintCeil(amountInInternal, bases[inputTokenIndex]);
require(amountInUintNoFee > 0, "swapToLimit: input zero");
fee = 0;
amountIn = amountInUintNoFee;
if (swapFeePpm > 0) {
fee = _ceilFee(amountInUintNoFee, swapFeePpm);
amountIn += fee;
}
amountOut = _internalToUintFloor(amountOutInternal, bases[outputTokenIndex]);
require(amountOut > 0, "swapToLimit: output zero");
}
function swapToLimit(
address payer,
address receiver,
uint256 inputTokenIndex,
uint256 outputTokenIndex,
int128 limitPrice,
uint256 deadline,
uint256 swapFeePpm,
uint256 protocolFeePpm
) external returns (uint256 amountInUsed, uint256 amountOut, uint256 fee) {
uint256 n = tokens.length;
require(inputTokenIndex < n && outputTokenIndex < n, "swapToLimit: idx");
require(limitPrice > int128(0), "swapToLimit: limit <= 0");
require(deadline == 0 || block.timestamp <= deadline, "swapToLimit: deadline exceeded");
// Read previous balances for affected assets
uint256 prevBalI = IERC20(tokens[inputTokenIndex]).balanceOf(address(this));
uint256 prevBalJ = IERC20(tokens[outputTokenIndex]).balanceOf(address(this));
// Compute amounts using the same path as views
(uint256 totalTransferAmount, uint256 amountOutUint, int128 amountInInternalMax, int128 amountOutInternal, uint256 amountInUsedUint, uint256 feeUint) =
_quoteSwapToLimit(inputTokenIndex, outputTokenIndex, limitPrice, swapFeePpm);
// Transfer the exact amount needed from payer and require exact receipt (revert on fee-on-transfer)
tokens[inputTokenIndex].safeTransferFrom(payer, address(this), totalTransferAmount);
uint256 balIAfter = IERC20(tokens[inputTokenIndex]).balanceOf(address(this));
require(balIAfter == prevBalI + totalTransferAmount, "swapToLimit: non-standard tokenIn");
// Transfer output to receiver and verify exact decrease
tokens[outputTokenIndex].safeTransfer(receiver, amountOutUint);
uint256 balJAfter = IERC20(tokens[outputTokenIndex]).balanceOf(address(this));
require(balJAfter == prevBalJ - amountOutUint, "swapToLimit: non-standard tokenOut");
// Accrue protocol share (floor) from the fee on input token
if (protocolFeePpm > 0 && feeUint > 0 ) {
uint256 protoShare = (feeUint * protocolFeePpm) / 1_000_000; // floor
if (protoShare > 0) {
protocolFeesOwed[inputTokenIndex] += protoShare;
}
}
// Update caches to effective balances
_recordCachedBalance(inputTokenIndex, balIAfter);
_recordCachedBalance(outputTokenIndex, balJAfter);
// Apply swap to LMSR state with the internal amounts
lmsr.applySwap(inputTokenIndex, outputTokenIndex, amountInInternalMax, amountOutInternal);
// Maintain original event semantics (logs input without fee)
emit IPartyPool.Swap(payer, receiver, tokens[inputTokenIndex], tokens[outputTokenIndex], amountInUsedUint, amountOutUint);
return (amountInUsedUint, amountOutUint, feeUint);
}
/// @notice Internal quote for swap-to-limit that mirrors swapToLimit() rounding and fee application
/// @dev Computes the input required to reach limitPrice and the resulting output; all rounding matches swapToLimit.
/// @return grossIn amount to transfer in (inclusive of fee), amountOutUint output amount (uint),
/// amountInInternal and amountOutInternal (64.64), amountInUintNoFee input amount excluding fee (uint),
/// feeUint fee taken from the gross input (uint)
function _quoteSwapToLimit(
uint256 inputTokenIndex,
uint256 outputTokenIndex,
int128 limitPrice,
uint256 swapFeePpm
) internal view
returns (
uint256 grossIn,
uint256 amountOutUint,
int128 amountInInternal,
int128 amountOutInternal,
uint256 amountInUintNoFee,
uint256 feeUint
)
{
// Compute internal maxima at the price limit
(amountInInternal, amountOutInternal) = lmsr.swapAmountsForPriceLimit(inputTokenIndex, outputTokenIndex, limitPrice);
// Convert input to uint (ceil) and output to uint (floor)
amountInUintNoFee = _internalToUintCeil(amountInInternal, bases[inputTokenIndex]);
require(amountInUintNoFee > 0, "swapToLimit: input zero");
feeUint = 0;
grossIn = amountInUintNoFee;
if (swapFeePpm > 0) {
feeUint = _ceilFee(amountInUintNoFee, swapFeePpm);
grossIn += feeUint;
}
amountOutUint = _internalToUintFloor(amountOutInternal, bases[outputTokenIndex]);
require(amountOutUint > 0, "swapToLimit: output zero");
}
}

View File

@@ -1,181 +0,0 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30;
import {ABDKMath64x64} from "../lib/abdk-libraries-solidity/ABDKMath64x64.sol";
import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {IPartyPool} from "./IPartyPool.sol";
import {LMSRStabilized} from "./LMSRStabilized.sol";
import {PartyPoolHelpers} from "./PartyPoolHelpers.sol";
import {PartyPoolMintImpl} from "./PartyPoolMintImpl.sol";
import {PartyPoolSwapImpl} from "./PartyPoolSwapImpl.sol";
contract PartyPoolViewer is PartyPoolHelpers {
using ABDKMath64x64 for int128;
PartyPoolSwapImpl immutable internal SWAP_IMPL;
PartyPoolMintImpl immutable internal MINT_IMPL;
constructor(PartyPoolSwapImpl swapImpl_, PartyPoolMintImpl mintImpl) {
SWAP_IMPL = swapImpl_;
MINT_IMPL = mintImpl;
}
//
// Current marginal prices
//
/// @notice Marginal price of `base` denominated in `quote` as Q64.64.
/// @dev Returns the LMSR marginal price p_quote / p_base in ABDK 64.64 fixed-point format.
/// Useful for off-chain quoting; raw 64.64 value is returned (no scaling to token units).
/// @param baseTokenIndex index of the base asset (e.g., ETH)
/// @param quoteTokenIndex index of the quote asset (e.g., USD)
/// @return price Q64.64 value equal to quote per base (p_quote / p_base)
function price(IPartyPool pool, uint256 baseTokenIndex, uint256 quoteTokenIndex) external view returns (int128) {
LMSRStabilized.State memory lmsr = pool.LMSR();
require(baseTokenIndex < lmsr.nAssets && quoteTokenIndex < lmsr.nAssets, "price: idx");
require(lmsr.nAssets > 0, "price: uninit");
return LMSRStabilized.price(lmsr.nAssets, pool.kappa(), lmsr.qInternal, baseTokenIndex, quoteTokenIndex);
}
/// @notice Price of one LP token denominated in `quote` as Q64.64.
/// @dev Computes LMSR poolPrice (quote per unit internal qTotal) and scales it to LP units:
/// returns price_per_LP = poolPrice_quote * (totalSupply() / qTotal) in ABDK 64.64 format.
/// The returned value is raw Q64.64 and represents quote units per one LP token unit.
/// @param quoteTokenIndex index of the quote asset in which to denominate the LP price
/// @return price Q64.64 value equal to quote per LP token unit
function poolPrice(IPartyPool pool, uint256 quoteTokenIndex) external view returns (int128) {
LMSRStabilized.State memory lmsr = pool.LMSR();
require(lmsr.nAssets > 0, "poolPrice: uninit");
require(quoteTokenIndex < lmsr.nAssets, "poolPrice: idx");
// price per unit of qTotal (Q64.64) from LMSR
int128 pricePerQ = LMSRStabilized.poolPrice(lmsr.nAssets, pool.kappa(), lmsr.qInternal, quoteTokenIndex);
// total internal q (qTotal) as Q64.64
int128 qTotal = LMSRStabilized._computeSizeMetric(lmsr.qInternal);
require(qTotal > int128(0), "poolPrice: qTotal zero");
// totalSupply as Q64.64
uint256 supply = pool.totalSupply();
require(supply > 0, "poolPrice: zero supply");
int128 supplyQ64 = ABDKMath64x64.fromUInt(supply);
// factor = totalSupply / qTotal (Q64.64)
int128 factor = supplyQ64.div(qTotal);
// price per LP token = pricePerQ * factor (Q64.64)
return pricePerQ.mul(factor);
}
function mintAmounts(IPartyPool pool, uint256 lpTokenAmount) public view returns (uint256[] memory depositAmounts) {
LMSRStabilized.State memory lmsr = pool.LMSR();
uint256[] memory cachedUintBalances = new uint256[](lmsr.nAssets);
for( uint256 i=0; i<lmsr.nAssets; i++ )
cachedUintBalances[i] = pool.getToken(i).balanceOf(address(pool));
return MINT_IMPL.mintAmounts(lpTokenAmount, lmsr.nAssets, pool.totalSupply(), cachedUintBalances);
}
function burnAmounts(IPartyPool pool, uint256 lpTokenAmount) external view returns (uint256[] memory withdrawAmounts) {
LMSRStabilized.State memory lmsr = pool.LMSR();
uint256[] memory cachedUintBalances = new uint256[](lmsr.nAssets);
for( uint256 i=0; i<lmsr.nAssets; i++ )
cachedUintBalances[i] = pool.getToken(i).balanceOf(address(pool));
return MINT_IMPL.burnAmounts(lpTokenAmount, lmsr.nAssets, pool.totalSupply(), cachedUintBalances);
}
/// @notice External view to quote swap-to-limit amounts (gross input incl. fee and output), matching swapToLimit() computations
/// @param inputTokenIndex index of input token
/// @param outputTokenIndex index of output token
/// @param limitPrice target marginal price to reach (must be > 0)
/// @return amountIn gross input amount to transfer (includes fee), amountOut output amount user would receive, fee fee amount taken
function swapToLimitAmounts(
IPartyPool pool,
uint256 inputTokenIndex,
uint256 outputTokenIndex,
int128 limitPrice
) external view returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
LMSRStabilized.State memory lmsr = pool.LMSR();
require(inputTokenIndex < lmsr.nAssets && outputTokenIndex < lmsr.nAssets, "swapToLimit: idx");
require(limitPrice > int128(0), "swapToLimit: limit <= 0");
require(lmsr.nAssets > 0, "swapToLimit: pool uninitialized");
return SWAP_IMPL.swapToLimitAmounts(
inputTokenIndex, outputTokenIndex, limitPrice,
pool.denominators(), pool.kappa(), lmsr.qInternal, pool.swapFeePpm());
}
function swapMintAmounts(IPartyPool pool, uint256 inputTokenIndex, uint256 maxAmountIn) external view
returns (uint256 amountInUsed, uint256 fee, uint256 lpMinted) {
LMSRStabilized.State memory lmsr = pool.LMSR();
return MINT_IMPL.swapMintAmounts(
inputTokenIndex,
maxAmountIn,
pool.swapFeePpm(),
lmsr,
pool.denominators(),
pool.totalSupply()
);
}
function burnSwapAmounts(IPartyPool pool, uint256 lpAmount, uint256 inputTokenIndex) external view
returns (uint256 amountOut) {
LMSRStabilized.State memory lmsr = pool.LMSR();
return MINT_IMPL.burnSwapAmounts(
lpAmount,
inputTokenIndex,
pool.swapFeePpm(),
lmsr,
pool.denominators(),
pool.totalSupply()
);
}
/// @notice Compute repayment amounts (principal + flash fee) for a proposed flash loan.
/// @param loanAmounts array of per-token loan amounts; must match the pool's token ordering.
/// @return repaymentAmounts array where repaymentAmounts[i] = loanAmounts[i] + ceil(loanAmounts[i] * flashFeePpm)
function flashRepaymentAmounts(IPartyPool pool, uint256[] memory loanAmounts) external view
returns (uint256[] memory repaymentAmounts) {
LMSRStabilized.State memory lmsr = pool.LMSR();
repaymentAmounts = new uint256[](lmsr.nAssets);
for (uint256 i = 0; i < lmsr.nAssets; i++) {
uint256 amount = loanAmounts[i];
if (amount > 0) {
repaymentAmounts[i] = amount + _ceilFee(amount, pool.flashFeePpm());
}
}
}
/**
* @dev The amount of currency available to be lent.
* @param token The loan currency.
* @return The amount of `token` that can be borrowed.
*/
function maxFlashLoan(
IPartyPool pool,
address token
) external view returns (uint256) {
return IERC20(token).balanceOf(address(pool));
}
/**
* @dev The fee to be charged for a given loan.
* @param amount The amount of tokens lent.
* @return fee The amount of `token` to be charged for the loan, on top of the returned principal.
*/
function flashFee(
IPartyPool pool,
address /*token*/,
uint256 amount
) external view returns (uint256 fee) {
(fee,) = _computeFee(amount, pool.flashFeePpm());
}
}

14
src/PoolBase.sol Normal file
View File

@@ -0,0 +1,14 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "./PoolLib.sol";
/// @notice This contract has all the storage for PartyPool and can be inherited by implementation contracts that
/// are delegate-called
abstract contract PoolBase is ERC20, ReentrancyGuard {
/// @notice Pool state containing all storage variables
PoolLib.State internal s;
}

935
src/PoolLib.sol Normal file
View File

@@ -0,0 +1,935 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30;
import "@abdk/ABDKMath64x64.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "./LMSRStabilized.sol";
import "./LMSRStabilizedBalancedPair.sol";
import "./IPartyFlashCallback.sol";
/// @title PoolLib - Library containing all PartyPool implementation logic
/// @notice This library contains the core implementation for LMSR-backed multi-asset pools
/// @dev All functions are internal and accept State as the first parameter
library PoolLib {
using ABDKMath64x64 for int128;
using LMSRStabilized for LMSRStabilized.State;
using SafeERC20 for IERC20;
/// @notice State struct containing all storage variables from PartyPool
/// @dev This struct is passed to all library functions as the first parameter
struct State {
/// @notice Token addresses comprising the pool
IERC20[] tokens;
/// @notice LMSR state for pricing computations
LMSRStabilized.State lmsr;
/// @notice Cached on-chain balances (uint) for each token
uint256[] cachedUintBalances;
/// @notice Per-token uint base denominators used to convert uint <-> internal Q64.64
uint256[] bases;
/// @notice Mapping from token address => (index+1). Zero means token not in pool.
mapping(IERC20=>uint) tokenAddressToIndexPlusOne;
}
/// @notice Scale factor used when converting LMSR Q64.64 totals to LP token units (uint)
uint256 internal constant LP_SCALE = 1e18;
// Events that mirror the main contract events
event Mint(address indexed payer, address indexed receiver, uint256[] depositAmounts, uint256 lpMinted);
event Burn(address indexed payer, address indexed receiver, uint256[] withdrawAmounts, uint256 lpBurned);
event Swap(address indexed payer, address indexed receiver, IERC20 indexed tokenIn, IERC20 tokenOut, uint256 amountIn, uint256 amountOut);
event SwapMint(address indexed payer, address indexed receiver, uint256 indexed inputTokenIndex, uint256 totalTransfer, uint256 amountInUint, uint256 feeUintActual);
event BurnSwap(address indexed payer, address indexed receiver, uint256 indexed inputTokenIndex, uint256 amountOutUint);
/// @notice Initialize the pool state with tokens and bases
/// @param state The pool state
/// @param tokens_ Array of token addresses
/// @param bases_ Array of base denominators for each token
function initialize(
PoolLib.State storage state,
IERC20[] memory tokens_,
uint256[] memory bases_
) internal {
require(tokens_.length > 1, "Pool: need >1 asset");
require(tokens_.length == bases_.length, "Pool: lengths mismatch");
state.tokens = tokens_;
state.bases = bases_;
uint256 n = tokens_.length;
// Initialize LMSR state nAssets; full init occurs on first mint
state.lmsr.nAssets = n;
// Initialize token address to index mapping
for (uint i = 0; i < n;) {
state.tokenAddressToIndexPlusOne[tokens_[i]] = i + 1;
unchecked {i++;}
}
// Initialize caches to zero
state.cachedUintBalances = new uint256[](n);
}
/// @notice Get deposit amounts needed for minting LP tokens
function mintDepositAmounts(
PoolLib.State storage state,
uint256 lpTokenAmount,
uint256 totalSupply
) internal view returns (uint256[] memory depositAmounts) {
uint256 n = state.tokens.length;
depositAmounts = new uint256[](n);
// If this is the first mint or pool is empty, return zeros
if (totalSupply == 0 || state.lmsr.nAssets == 0) {
return depositAmounts;
}
// Calculate deposit based on current proportions
for (uint i = 0; i < n; i++) {
uint256 currentBalance = state.cachedUintBalances[i];
// Calculate with rounding up: (a * b + c - 1) / c
depositAmounts[i] = (lpTokenAmount * currentBalance + totalSupply - 1) / totalSupply;
}
return depositAmounts;
}
/// @notice Initial mint to set up pool for the first time
function initialMint(
PoolLib.State storage state,
address receiver,
uint256 lpTokens,
int128 kappa,
uint256 totalSupply
) internal returns (uint256 lpMinted) {
uint256 n = state.tokens.length;
// Check if this is initial deposit
bool isInitialDeposit = totalSupply == 0 || state.lmsr.nAssets == 0;
require(isInitialDeposit, "initialMint: pool already initialized");
// Update cached balances for all assets
int128[] memory newQInternal = new int128[](n);
uint256[] memory depositAmounts = new uint256[](n);
for (uint i = 0; i < n; ) {
uint256 bal = IERC20(state.tokens[i]).balanceOf(address(this));
state.cachedUintBalances[i] = bal;
newQInternal[i] = _uintToInternalFloor(bal, state.bases[i]);
depositAmounts[i] = bal;
unchecked { i++; }
}
// Initialize the stabilized LMSR state with provided kappa
state.lmsr.init(newQInternal, kappa);
// Compute actual LP tokens to mint based on size metric (scaled)
if( lpTokens != 0 )
lpMinted = lpTokens;
else {
int128 newTotal = _computeSizeMetric(newQInternal);
lpMinted = ABDKMath64x64.mulu(newTotal, LP_SCALE);
}
require(lpMinted > 0, "initialMint: zero LP amount");
emit Mint(address(0), receiver, depositAmounts, lpMinted);
}
/// @notice Proportional mint for existing pool
function mint(
PoolLib.State storage state,
address payer,
address receiver,
uint256 lpTokenAmount,
uint256 deadline,
uint256 totalSupply
) internal returns (uint256 lpMinted) {
require(deadline == 0 || block.timestamp <= deadline, "mint: deadline exceeded");
uint256 n = state.tokens.length;
// Check if this is NOT initial deposit
bool isInitialDeposit = totalSupply == 0 || state.lmsr.nAssets == 0;
require(!isInitialDeposit, "mint: use initialMint for pool initialization");
require(lpTokenAmount > 0, "mint: zero LP amount");
// Capture old pool size metric (scaled)
int128 oldTotal = _computeSizeMetric(state.lmsr.qInternal);
uint256 oldScaled = ABDKMath64x64.mulu(oldTotal, LP_SCALE);
// Calculate required deposit amounts
uint256[] memory depositAmounts = mintDepositAmounts(state, lpTokenAmount, totalSupply);
// Transfer in all token amounts
for (uint i = 0; i < n; ) {
if (depositAmounts[i] > 0) {
state.tokens[i].safeTransferFrom(payer, address(this), depositAmounts[i]);
}
unchecked { i++; }
}
// Update cached balances for all assets
int128[] memory newQInternal = new int128[](n);
for (uint i = 0; i < n; ) {
uint256 bal = IERC20(state.tokens[i]).balanceOf(address(this));
state.cachedUintBalances[i] = bal;
newQInternal[i] = _uintToInternalFloor(bal, state.bases[i]);
unchecked { i++; }
}
// Update for proportional change
state.lmsr.updateForProportionalChange(newQInternal);
// Compute actual LP tokens to mint
int128 newTotal = _computeSizeMetric(newQInternal);
uint256 newScaled = ABDKMath64x64.mulu(newTotal, LP_SCALE);
uint256 actualLpToMint;
require(oldScaled > 0, "mint: oldScaled zero");
uint256 delta = (newScaled > oldScaled) ? (newScaled - oldScaled) : 0;
if (delta > 0) {
actualLpToMint = (totalSupply * delta) / oldScaled;
} else {
actualLpToMint = 0;
}
require(actualLpToMint > 0, "mint: zero LP minted");
// Allow actual amount to be at most 0.00001% less than requested
uint256 minAcceptable = lpTokenAmount * 99_999 / 100_000;
require(actualLpToMint >= minAcceptable, "mint: insufficient LP minted");
emit Mint(payer, receiver, depositAmounts, actualLpToMint);
return actualLpToMint;
}
/// @notice Get withdrawal amounts for burning LP tokens
function burnReceiveAmounts(
PoolLib.State storage state,
uint256 lpTokenAmount,
uint256 totalSupply
) internal view returns (uint256[] memory withdrawAmounts) {
uint256 n = state.tokens.length;
withdrawAmounts = new uint256[](n);
if (totalSupply == 0 || state.lmsr.nAssets == 0) {
return withdrawAmounts;
}
for (uint i = 0; i < n; i++) {
uint256 currentBalance = state.cachedUintBalances[i];
withdrawAmounts[i] = (lpTokenAmount * currentBalance) / totalSupply;
}
return withdrawAmounts;
}
/// @notice Burn LP tokens and withdraw proportional basket
function burn(
PoolLib.State storage state,
address payer,
address receiver,
uint256 lpAmount,
uint256 deadline,
uint256 totalSupply,
uint256 payerBalance
) internal returns (uint256[] memory withdrawAmounts) {
require(deadline == 0 || block.timestamp <= deadline, "burn: deadline exceeded");
uint256 n = state.tokens.length;
require(lpAmount > 0, "burn: zero lp");
require(totalSupply > 0, "burn: empty supply");
require(state.lmsr.nAssets > 0, "burn: uninit pool");
require(payerBalance >= lpAmount, "burn: insufficient LP");
// Refresh cached balances
for (uint i = 0; i < n; ) {
uint256 bal = IERC20(state.tokens[i]).balanceOf(address(this));
state.cachedUintBalances[i] = bal;
unchecked { i++; }
}
// Compute proportional withdrawal amounts
withdrawAmounts = burnReceiveAmounts(state, lpAmount, totalSupply);
// Transfer underlying tokens out
for (uint i = 0; i < n; ) {
if (withdrawAmounts[i] > 0) {
state.tokens[i].safeTransfer(receiver, withdrawAmounts[i]);
}
unchecked { i++; }
}
// Update cached balances and internal q
int128[] memory newQInternal = new int128[](n);
for (uint i = 0; i < n; ) {
uint256 bal = IERC20(state.tokens[i]).balanceOf(address(this));
state.cachedUintBalances[i] = bal;
newQInternal[i] = _uintToInternalFloor(bal, state.bases[i]);
unchecked { i++; }
}
// Apply proportional update or deinitialize if drained
bool allZero = true;
for (uint i = 0; i < n; ) {
if (newQInternal[i] != int128(0)) {
allZero = false;
break;
}
unchecked { i++; }
}
if (allZero) {
state.lmsr.deinit();
} else {
state.lmsr.updateForProportionalChange(newQInternal);
}
emit Burn(payer, receiver, withdrawAmounts, lpAmount);
}
/// @notice Get swap amounts for exact input swap
function swapAmounts(
PoolLib.State storage state,
uint256 inputTokenIndex,
uint256 outputTokenIndex,
uint256 maxAmountIn,
int128 limitPrice,
uint256 swapFeePpm
) internal view returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
(uint256 grossIn, uint256 outUint,,,, uint256 feeUint) = _quoteSwapExactIn(
state, inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice, swapFeePpm
);
return (grossIn, outUint, feeUint);
}
/// @notice Get swap amounts for swap to price limit
function swapToLimitAmounts(
PoolLib.State storage state,
uint256 inputTokenIndex,
uint256 outputTokenIndex,
int128 limitPrice,
uint256 swapFeePpm
) internal view returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
(uint256 grossIn, uint256 outUint,,,, uint256 feeUint) = _quoteSwapToLimit(
state, inputTokenIndex, outputTokenIndex, limitPrice, swapFeePpm
);
return (grossIn, outUint, feeUint);
}
/// @notice Get amounts for swapMint operation
function swapMintAmounts(
PoolLib.State storage state,
uint256 inputTokenIndex,
uint256 maxAmountIn,
uint256 swapFeePpm,
uint256 totalSupply
) internal view returns (uint256 totalTransfer, uint256 amountIn, uint256 fee, uint256 lpMinted) {
uint256 n = state.tokens.length;
require(inputTokenIndex < n, "swapMintAmounts: idx");
require(maxAmountIn > 0, "swapMintAmounts: input zero");
require(state.lmsr.nAssets > 0, "swapMintAmounts: uninit pool");
// Compute fee on gross maxAmountIn to get initial net estimate
(, uint256 netUintGuess) = _computeFee(maxAmountIn, swapFeePpm);
// Convert the net guess to internal (floor)
int128 netInternalGuess = _uintToInternalFloor(netUintGuess, state.bases[inputTokenIndex]);
require(netInternalGuess > int128(0), "swapMintAmounts: input too small after fee");
// Use LMSR view to determine actual internal consumed and size-increase
(int128 amountInInternalUsed, int128 sizeIncreaseInternal) = state.lmsr.swapAmountsForMint(inputTokenIndex, netInternalGuess);
// Convert to uint (ceil) to determine actual transfer
uint256 amountInUint = _internalToUintCeil(amountInInternalUsed, state.bases[inputTokenIndex]);
require(amountInUint > 0, "swapMintAmounts: input zero after internal conversion");
// Compute fee on actual used input and total transfer amount
uint256 feeUintActual = _ceilFee(amountInUint, swapFeePpm);
uint256 totalTransferAmount = amountInUint + feeUintActual;
require(totalTransferAmount > 0 && totalTransferAmount <= maxAmountIn, "swapMintAmounts: transfer exceeds max");
// Compute old and new scaled size metrics
int128 oldTotal = _computeSizeMetric(state.lmsr.qInternal);
require(oldTotal > int128(0), "swapMintAmounts: zero total");
uint256 oldScaled = ABDKMath64x64.mulu(oldTotal, LP_SCALE);
int128 newTotal = oldTotal.add(sizeIncreaseInternal);
uint256 newScaled = ABDKMath64x64.mulu(newTotal, LP_SCALE);
uint256 actualLpToMint;
if (totalSupply == 0) {
actualLpToMint = newScaled;
} else {
require(oldScaled > 0, "swapMintAmounts: oldScaled zero");
uint256 delta = (newScaled > oldScaled) ? (newScaled - oldScaled) : 0;
if (delta > 0) {
actualLpToMint = (totalSupply * delta) / oldScaled;
} else {
actualLpToMint = 0;
}
}
require(actualLpToMint > 0, "swapMintAmounts: zero LP minted");
return (totalTransferAmount, amountInUint, feeUintActual, actualLpToMint);
}
/// @notice Execute exact input swap
function swap(
PoolLib.State storage state,
address payer,
address receiver,
uint256 inputTokenIndex,
uint256 outputTokenIndex,
uint256 maxAmountIn,
int128 limitPrice,
uint256 deadline,
uint256 swapFeePpm
) internal returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
uint256 n = state.tokens.length;
require(inputTokenIndex < n && outputTokenIndex < n, "swap: idx");
require(maxAmountIn > 0, "swap: input zero");
require(deadline == 0 || block.timestamp <= deadline, "swap: deadline exceeded");
// Read previous balances
uint256 prevBalI = IERC20(state.tokens[inputTokenIndex]).balanceOf(address(this));
uint256 prevBalJ = IERC20(state.tokens[outputTokenIndex]).balanceOf(address(this));
// Compute amounts
(uint256 totalTransferAmount, uint256 amountOutUint, int128 amountInInternalUsed, int128 amountOutInternal, , uint256 feeUint) =
_quoteSwapExactIn(state, inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice, swapFeePpm);
// Transfer exact amount from payer
state.tokens[inputTokenIndex].safeTransferFrom(payer, address(this), totalTransferAmount);
uint256 balIAfter = IERC20(state.tokens[inputTokenIndex]).balanceOf(address(this));
require(balIAfter == prevBalI + totalTransferAmount, "swap: non-standard tokenIn");
// Transfer output to receiver
state.tokens[outputTokenIndex].safeTransfer(receiver, amountOutUint);
uint256 balJAfter = IERC20(state.tokens[outputTokenIndex]).balanceOf(address(this));
require(balJAfter == prevBalJ - amountOutUint, "swap: non-standard tokenOut");
// Update cached balances
state.cachedUintBalances[inputTokenIndex] = balIAfter;
state.cachedUintBalances[outputTokenIndex] = balJAfter;
// Apply swap to LMSR state
state.lmsr.applySwap(inputTokenIndex, outputTokenIndex, amountInInternalUsed, amountOutInternal);
emit Swap(payer, receiver, state.tokens[inputTokenIndex], state.tokens[outputTokenIndex], totalTransferAmount, amountOutUint);
return (totalTransferAmount, amountOutUint, feeUint);
}
/// @notice Execute swap to price limit
function swapToLimit(
PoolLib.State storage state,
address payer,
address receiver,
uint256 inputTokenIndex,
uint256 outputTokenIndex,
int128 limitPrice,
uint256 deadline,
uint256 swapFeePpm
) internal returns (uint256 amountInUsed, uint256 amountOut, uint256 fee) {
uint256 n = state.tokens.length;
require(inputTokenIndex < n && outputTokenIndex < n, "swapToLimit: idx");
require(limitPrice > int128(0), "swapToLimit: limit <= 0");
require(deadline == 0 || block.timestamp <= deadline, "swapToLimit: deadline exceeded");
// Read previous balances
uint256 prevBalI = IERC20(state.tokens[inputTokenIndex]).balanceOf(address(this));
uint256 prevBalJ = IERC20(state.tokens[outputTokenIndex]).balanceOf(address(this));
// Compute amounts
(uint256 totalTransferAmount, uint256 amountOutUint, int128 amountInInternalMax, int128 amountOutInternal, uint256 amountInUsedUint, uint256 feeUint) =
_quoteSwapToLimit(state, inputTokenIndex, outputTokenIndex, limitPrice, swapFeePpm);
// Transfer exact amount from payer
state.tokens[inputTokenIndex].safeTransferFrom(payer, address(this), totalTransferAmount);
uint256 balIAfter = IERC20(state.tokens[inputTokenIndex]).balanceOf(address(this));
require(balIAfter == prevBalI + totalTransferAmount, "swapToLimit: non-standard tokenIn");
// Transfer output to receiver
state.tokens[outputTokenIndex].safeTransfer(receiver, amountOutUint);
uint256 balJAfter = IERC20(state.tokens[outputTokenIndex]).balanceOf(address(this));
require(balJAfter == prevBalJ - amountOutUint, "swapToLimit: non-standard tokenOut");
// Update caches
state.cachedUintBalances[inputTokenIndex] = balIAfter;
state.cachedUintBalances[outputTokenIndex] = balJAfter;
// Apply swap to LMSR state
state.lmsr.applySwap(inputTokenIndex, outputTokenIndex, amountInInternalMax, amountOutInternal);
emit Swap(payer, receiver, state.tokens[inputTokenIndex], state.tokens[outputTokenIndex], amountInUsedUint, amountOutUint);
return (amountInUsedUint, amountOutUint, feeUint);
}
/// @notice Get amounts for burnSwap operation
function burnSwapAmounts(
PoolLib.State storage state,
uint256 lpAmount,
uint256 inputTokenIndex,
uint256 swapFeePpm,
uint256 totalSupply
) internal view returns (uint256 amountOut) {
uint256 n = state.tokens.length;
require(inputTokenIndex < n, "burnSwapAmounts: idx");
require(lpAmount > 0, "burnSwapAmounts: zero lp");
require(totalSupply > 0, "burnSwapAmounts: empty supply");
// alpha = lpAmount / supply as Q64.64
int128 alpha = ABDKMath64x64.divu(lpAmount, totalSupply);
// Use LMSR view to compute single-asset payout
(int128 payoutInternal, ) = state.lmsr.swapAmountsForBurn(inputTokenIndex, alpha);
// Convert payoutInternal -> uint (floor) to favor pool
uint256 amountOutUint = _internalToUintFloor(payoutInternal, state.bases[inputTokenIndex]);
require(amountOutUint > 0, "burnSwapAmounts: output zero");
// Apply swap fee to the output
if (swapFeePpm > 0) {
uint256 feeUint = _ceilFee(amountOutUint, swapFeePpm);
require(amountOutUint > feeUint, "burnSwapAmounts: fee exceeds output");
amountOutUint -= feeUint;
}
return amountOutUint;
}
/// @notice Single-token mint (swapMint)
function swapMint(
PoolLib.State storage state,
address payer,
address receiver,
uint256 inputTokenIndex,
uint256 maxAmountIn,
uint256 deadline,
uint256 swapFeePpm,
uint256 totalSupply
) internal returns (uint256 lpMinted) {
uint256 n = state.tokens.length;
require(inputTokenIndex < n, "swapMint: idx");
require(maxAmountIn > 0, "swapMint: input zero");
require(deadline == 0 || block.timestamp <= deadline, "swapMint: deadline");
require(state.lmsr.nAssets > 0, "swapMint: uninit pool");
// Calculate amounts using view function
(uint256 totalTransfer, uint256 amountInUint, uint256 feeUintActual, uint256 actualLpToMint) = swapMintAmounts(
state, inputTokenIndex, maxAmountIn, swapFeePpm, totalSupply
);
// Record pre-balance and transfer tokens
uint256 prevBalI = IERC20(state.tokens[inputTokenIndex]).balanceOf(address(this));
state.tokens[inputTokenIndex].safeTransferFrom(payer, address(this), totalTransfer);
uint256 balIAfter = IERC20(state.tokens[inputTokenIndex]).balanceOf(address(this));
require(balIAfter == prevBalI + totalTransfer, "swapMint: non-standard tokenIn");
// Update cached uint balances
state.cachedUintBalances[inputTokenIndex] = balIAfter;
// Update LMSR internal state
int128 oldTotal = _computeSizeMetric(state.lmsr.qInternal);
(, int128 sizeIncreaseInternal) = state.lmsr.swapAmountsForMint(inputTokenIndex, _uintToInternalFloor(amountInUint, state.bases[inputTokenIndex]));
int128 newTotal = oldTotal.add(sizeIncreaseInternal);
int128[] memory newQInternal = new int128[](n);
for (uint256 idx = 0; idx < n; idx++) {
newQInternal[idx] = state.lmsr.qInternal[idx].mul(newTotal).div(oldTotal);
}
state.lmsr.updateForProportionalChange(newQInternal);
emit SwapMint(payer, receiver, inputTokenIndex, totalTransfer, amountInUint, feeUintActual);
emit Mint(payer, receiver, new uint256[](n), actualLpToMint);
return actualLpToMint;
}
/// @notice Burn LP tokens and swap to single asset (burnSwap)
function burnSwap(
PoolLib.State storage state,
address payer,
address receiver,
uint256 lpAmount,
uint256 inputTokenIndex,
uint256 deadline,
uint256 swapFeePpm,
uint256 totalSupply,
uint256 payerBalance
) internal returns (uint256 amountOutUint) {
uint256 n = state.tokens.length;
require(inputTokenIndex < n, "burnSwap: idx");
require(lpAmount > 0, "burnSwap: zero lp");
require(deadline == 0 || block.timestamp <= deadline, "burnSwap: deadline");
require(totalSupply > 0, "burnSwap: empty supply");
require(payerBalance >= lpAmount, "burnSwap: insufficient LP");
// Calculate amounts using view function
amountOutUint = burnSwapAmounts(state, lpAmount, inputTokenIndex, swapFeePpm, totalSupply);
// Transfer the payout to receiver
state.tokens[inputTokenIndex].safeTransfer(receiver, amountOutUint);
// Update cached balances by reading on-chain balances for all tokens
int128[] memory newQInternal = new int128[](n);
for (uint256 idx = 0; idx < n; idx++) {
uint256 bal = IERC20(state.tokens[idx]).balanceOf(address(this));
state.cachedUintBalances[idx] = bal;
newQInternal[idx] = _uintToInternalFloor(bal, state.bases[idx]);
}
emit BurnSwap(payer, receiver, inputTokenIndex, amountOutUint);
// If entire pool drained, deinit; else update proportionally
bool allZero = true;
for (uint256 idx = 0; idx < n; idx++) {
if (newQInternal[idx] != int128(0)) { allZero = false; break; }
}
if (allZero) {
state.lmsr.deinit();
} else {
state.lmsr.updateForProportionalChange(newQInternal);
}
emit Burn(payer, receiver, new uint256[](n), lpAmount);
return amountOutUint;
}
/// @notice Calculate flash loan repayment amounts
function flashRepaymentAmounts(
PoolLib.State storage state,
uint256[] memory loanAmounts,
uint256 flashFeePpm
) internal view returns (uint256[] memory repaymentAmounts) {
repaymentAmounts = new uint256[](state.tokens.length);
for (uint256 i = 0; i < state.tokens.length; i++) {
uint256 amount = loanAmounts[i];
if (amount > 0) {
repaymentAmounts[i] = amount + _ceilFee(amount, flashFeePpm);
}
}
}
/// @notice Execute flash loan
function flash(
PoolLib.State storage state,
address recipient,
uint256[] memory amounts,
bytes calldata data,
uint256 flashFeePpm
) internal {
require(recipient != address(0), "flash: zero recipient");
require(amounts.length == state.tokens.length, "flash: amounts length mismatch");
// Calculate repayment amounts for each token including fee
uint256[] memory repaymentAmounts = new uint256[](state.tokens.length);
// Store initial balances to verify repayment later
uint256[] memory initialBalances = new uint256[](state.tokens.length);
// Track if any token amount is non-zero
bool hasNonZeroAmount = false;
// Process each token, skipping those with zero amounts
for (uint256 i = 0; i < state.tokens.length; i++) {
uint256 amount = amounts[i];
if (amount > 0) {
hasNonZeroAmount = true;
// Calculate repayment amount with fee (ceiling)
repaymentAmounts[i] = amount + _ceilFee(amount, flashFeePpm);
// Record initial balance
initialBalances[i] = IERC20(state.tokens[i]).balanceOf(address(this));
// Transfer token to recipient
state.tokens[i].safeTransfer(recipient, amount);
}
}
// Ensure at least one token is being borrowed
require(hasNonZeroAmount, "flash: no tokens requested");
// Call flash callback with expected repayment amounts
IPartyFlashCallback(msg.sender).partyFlashCallback(amounts, repaymentAmounts, data);
// Verify repayment amounts for tokens that were borrowed
for (uint256 i = 0; i < state.tokens.length; i++) {
if (amounts[i] > 0) {
uint256 currentBalance = IERC20(state.tokens[i]).balanceOf(address(this));
// Verify repayment: current balance must be at least (initial balance + fee)
require(
currentBalance >= initialBalances[i] + _ceilFee(amounts[i], flashFeePpm),
"flash: repayment failed"
);
// Update cached balance
state.cachedUintBalances[i] = currentBalance;
}
}
}
/// @notice Get marginal price between two tokens
function price(
PoolLib.State storage state,
uint256 baseTokenIndex,
uint256 quoteTokenIndex
) internal view returns (int128) {
uint256 n = state.tokens.length;
require(baseTokenIndex < n && quoteTokenIndex < n, "price: idx");
require(state.lmsr.nAssets > 0, "price: uninit");
return state.lmsr.price(baseTokenIndex, quoteTokenIndex);
}
/// @notice Get price of one LP token in quote asset
function poolPrice(
PoolLib.State storage state,
uint256 quoteTokenIndex,
uint256 totalSupply
) internal view returns (int128) {
uint256 n = state.tokens.length;
require(quoteTokenIndex < n, "poolPrice: idx");
require(state.lmsr.nAssets > 0, "poolPrice: uninit");
// price per unit of qTotal (Q64.64) from LMSR
int128 pricePerQ = state.lmsr.poolPrice(quoteTokenIndex);
// total internal q (qTotal) as Q64.64
int128 qTotal = _computeSizeMetric(state.lmsr.qInternal);
require(qTotal > int128(0), "poolPrice: qTotal zero");
// totalSupply as Q64.64
require(totalSupply > 0, "poolPrice: zero supply");
int128 supplyQ64 = ABDKMath64x64.fromUInt(totalSupply);
// factor = totalSupply / qTotal (Q64.64)
int128 factor = supplyQ64.div(qTotal);
// price per LP token = pricePerQ * factor (Q64.64)
return pricePerQ.mul(factor);
}
// Internal helper functions
/// @notice Ceiling fee helper
function _ceilFee(uint256 x, uint256 feePpm) internal pure returns (uint256) {
if (feePpm == 0) return 0;
return (x * feePpm + 1_000_000 - 1) / 1_000_000;
}
/// @notice Compute fee and net amounts for a gross input
function _computeFee(uint256 gross, uint256 swapFeePpm) internal pure returns (uint256 feeUint, uint256 netUint) {
if (swapFeePpm == 0) {
return (0, gross);
}
feeUint = _ceilFee(gross, swapFeePpm);
netUint = gross - feeUint;
}
/// @notice Add fee to net amount
function _addFee(uint256 netUint, uint256 swapFeePpm) internal pure returns (uint256 gross) {
if (swapFeePpm == 0) return netUint;
uint256 fee = _ceilFee(netUint, swapFeePpm);
return netUint + fee;
}
/// @notice Internal quote for exact-input swap
function _quoteSwapExactIn(
PoolLib.State storage state,
uint256 inputTokenIndex,
uint256 outputTokenIndex,
uint256 maxAmountIn,
int128 limitPrice,
uint256 swapFeePpm
)
internal
view
returns (
uint256 grossIn,
uint256 amountOutUint,
int128 amountInInternalUsed,
int128 amountOutInternal,
uint256 amountInUintNoFee,
uint256 feeUint
)
{
uint256 n = state.tokens.length;
require(inputTokenIndex < n && outputTokenIndex < n, "swap: idx");
require(maxAmountIn > 0, "swap: input zero");
require(state.lmsr.nAssets > 0, "swap: empty pool");
// Estimate max net input
(, uint256 netUintForSwap) = _computeFee(maxAmountIn, swapFeePpm);
// Convert to internal (floor)
int128 deltaInternalI = _uintToInternalFloor(netUintForSwap, state.bases[inputTokenIndex]);
require(deltaInternalI > int128(0), "swap: input too small after fee");
// Compute internal amounts using LMSR
(amountInInternalUsed, amountOutInternal) = state.lmsr.swapAmountsForExactInput(
inputTokenIndex, outputTokenIndex, deltaInternalI, limitPrice);
// Convert actual used input internal -> uint (ceil)
amountInUintNoFee = _internalToUintCeil(amountInInternalUsed, state.bases[inputTokenIndex]);
require(amountInUintNoFee > 0, "swap: input zero");
// Compute gross transfer including fee
feeUint = 0;
grossIn = amountInUintNoFee;
if (swapFeePpm > 0) {
feeUint = _ceilFee(amountInUintNoFee, swapFeePpm);
grossIn += feeUint;
}
// Ensure within user max
require(grossIn <= maxAmountIn, "swap: transfer exceeds max");
// Compute output (floor)
amountOutUint = _internalToUintFloor(amountOutInternal, state.bases[outputTokenIndex]);
require(amountOutUint > 0, "swap: output zero");
}
/// @notice Internal quote for exact-input swap
function _quoteSwapExactIn_BalancedPair(
PoolLib.State storage state,
uint256 inputTokenIndex,
uint256 outputTokenIndex,
uint256 maxAmountIn,
int128 limitPrice,
uint256 swapFeePpm
)
internal
view
returns (
uint256 grossIn,
uint256 amountOutUint,
int128 amountInInternalUsed,
int128 amountOutInternal,
uint256 amountInUintNoFee,
uint256 feeUint
)
{
uint256 n = state.tokens.length;
require(inputTokenIndex < n && outputTokenIndex < n, "swap: idx");
require(maxAmountIn > 0, "swap: input zero");
require(state.lmsr.nAssets > 0, "swap: empty pool");
// Estimate max net input
(, uint256 netUintForSwap) = _computeFee(maxAmountIn, swapFeePpm);
// Convert to internal (floor)
int128 deltaInternalI = _uintToInternalFloor(netUintForSwap, state.bases[inputTokenIndex]);
require(deltaInternalI > int128(0), "swap: input too small after fee");
// Compute internal amounts using LMSR
(amountInInternalUsed, amountOutInternal) = LMSRStabilizedBalancedPair.swapAmountsForExactInput(
state.lmsr, inputTokenIndex, outputTokenIndex, deltaInternalI, limitPrice);
// Convert actual used input internal -> uint (ceil)
amountInUintNoFee = _internalToUintCeil(amountInInternalUsed, state.bases[inputTokenIndex]);
require(amountInUintNoFee > 0, "swap: input zero");
// Compute gross transfer including fee
feeUint = 0;
grossIn = amountInUintNoFee;
if (swapFeePpm > 0) {
feeUint = _ceilFee(amountInUintNoFee, swapFeePpm);
grossIn += feeUint;
}
// Ensure within user max
require(grossIn <= maxAmountIn, "swap: transfer exceeds max");
// Compute output (floor)
amountOutUint = _internalToUintFloor(amountOutInternal, state.bases[outputTokenIndex]);
require(amountOutUint > 0, "swap: output zero");
}
/// @notice Internal quote for swap-to-limit
function _quoteSwapToLimit(
PoolLib.State storage state,
uint256 inputTokenIndex,
uint256 outputTokenIndex,
int128 limitPrice,
uint256 swapFeePpm
)
internal
view
returns (
uint256 grossIn,
uint256 amountOutUint,
int128 amountInInternal,
int128 amountOutInternal,
uint256 amountInUintNoFee,
uint256 feeUint
)
{
uint256 n = state.tokens.length;
require(inputTokenIndex < n && outputTokenIndex < n, "swapToLimit: idx");
require(limitPrice > int128(0), "swapToLimit: limit <= 0");
require(state.lmsr.nAssets > 0, "swapToLimit: pool uninitialized");
// Compute internal maxima at the price limit
(amountInInternal, amountOutInternal) = state.lmsr.swapAmountsForPriceLimit(inputTokenIndex, outputTokenIndex, limitPrice);
// Convert input to uint (ceil) and output to uint (floor)
amountInUintNoFee = _internalToUintCeil(amountInInternal, state.bases[inputTokenIndex]);
require(amountInUintNoFee > 0, "swapToLimit: input zero");
feeUint = 0;
grossIn = amountInUintNoFee;
if (swapFeePpm > 0) {
feeUint = _ceilFee(amountInUintNoFee, swapFeePpm);
grossIn += feeUint;
}
amountOutUint = _internalToUintFloor(amountOutInternal, state.bases[outputTokenIndex]);
require(amountOutUint > 0, "swapToLimit: output zero");
}
// Convert uint token amount -> internal 64.64 (floor)
function _uintToInternalFloor(uint256 amount, uint256 base) internal pure returns (int128) {
return ABDKMath64x64.divu(amount, base);
}
// Convert internal 64.64 -> uint token amount (floor)
function _internalToUintFloor(int128 internalAmount, uint256 base) internal pure returns (uint256) {
return ABDKMath64x64.mulu(internalAmount, base);
}
// Convert internal 64.64 -> uint token amount (ceiling)
function _internalToUintCeil(int128 internalAmount, uint256 base) internal pure returns (uint256) {
// Get the floor value first
uint256 floorValue = ABDKMath64x64.mulu(internalAmount, base);
// Check if there was any fractional part by comparing to a reconstruction
int128 reconstructed = ABDKMath64x64.divu(floorValue, base);
// If reconstructed is less than original, there was a fractional part that was truncated
if (reconstructed < internalAmount) {
return floorValue + 1;
}
return floorValue;
}
/// @notice Helper to compute size metric (sum of all asset quantities) from internal balances
function _computeSizeMetric(int128[] memory qInternal_) internal pure returns (int128) {
int128 total = int128(0);
for (uint i = 0; i < qInternal_.length; ) {
total = total.add(qInternal_[i]);
unchecked { i++; }
}
return total;
}
}

139
src/PoolLibBalancedPair.sol Normal file
View File

@@ -0,0 +1,139 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30;
import "@abdk/ABDKMath64x64.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "./LMSRStabilized.sol";
import "./LMSRStabilizedBalancedPair.sol";
import "./IPartyFlashCallback.sol";
import {PoolLib} from "./PoolLib.sol";
/// @title PoolLibBalancedPair - Library with optimized functions for stablecoin pair pools
/// @dev All functions are internal and accept State as the first parameter
library PoolLibBalancedPair {
using ABDKMath64x64 for int128;
using LMSRStabilized for LMSRStabilized.State;
using PoolLib for PoolLib.State;
using SafeERC20 for IERC20;
/// @notice Get swap amounts for exact input swap
function swapAmounts(
PoolLib.State storage state,
uint256 inputTokenIndex,
uint256 outputTokenIndex,
uint256 maxAmountIn,
int128 limitPrice,
uint256 swapFeePpm
) internal view returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
(uint256 grossIn, uint256 outUint,,,, uint256 feeUint) = _quoteSwapExactIn(
state, inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice, swapFeePpm
);
return (grossIn, outUint, feeUint);
}
/// @notice Internal quote for exact-input swap
function _quoteSwapExactIn(
PoolLib.State storage state,
uint256 inputTokenIndex,
uint256 outputTokenIndex,
uint256 maxAmountIn,
int128 limitPrice,
uint256 swapFeePpm
)
internal
view
returns (
uint256 grossIn,
uint256 amountOutUint,
int128 amountInInternalUsed,
int128 amountOutInternal,
uint256 amountInUintNoFee,
uint256 feeUint
)
{
uint256 n = state.tokens.length;
require(inputTokenIndex < n && outputTokenIndex < n, "swap: idx");
require(maxAmountIn > 0, "swap: input zero");
require(state.lmsr.nAssets > 0, "swap: empty pool");
// Estimate max net input
(, uint256 netUintForSwap) = PoolLib._computeFee(maxAmountIn, swapFeePpm);
// Convert to internal (floor)
int128 deltaInternalI = PoolLib._uintToInternalFloor(netUintForSwap, state.bases[inputTokenIndex]);
require(deltaInternalI > int128(0), "swap: input too small after fee");
// Compute internal amounts using LMSR
(amountInInternalUsed, amountOutInternal) = LMSRStabilizedBalancedPair.swapAmountsForExactInput(state.lmsr,
inputTokenIndex, outputTokenIndex, deltaInternalI, limitPrice);
// Convert actual used input internal -> uint (ceil)
amountInUintNoFee = PoolLib._internalToUintCeil(amountInInternalUsed, state.bases[inputTokenIndex]);
require(amountInUintNoFee > 0, "swap: input zero");
// Compute gross transfer including fee
feeUint = 0;
grossIn = amountInUintNoFee;
if (swapFeePpm > 0) {
feeUint = PoolLib._ceilFee(amountInUintNoFee, swapFeePpm);
grossIn += feeUint;
}
// Ensure within user max
require(grossIn <= maxAmountIn, "swap: transfer exceeds max");
// Compute output (floor)
amountOutUint = PoolLib._internalToUintFloor(amountOutInternal, state.bases[outputTokenIndex]);
require(amountOutUint > 0, "swap: output zero");
}
/// @notice Execute exact input swap
function swap(
PoolLib.State storage state,
address payer,
address receiver,
uint256 inputTokenIndex,
uint256 outputTokenIndex,
uint256 maxAmountIn,
int128 limitPrice,
uint256 deadline,
uint256 swapFeePpm
) internal returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
uint256 n = state.tokens.length;
require(inputTokenIndex < n && outputTokenIndex < n, "swap: idx");
require(maxAmountIn > 0, "swap: input zero");
require(deadline == 0 || block.timestamp <= deadline, "swap: deadline exceeded");
// Read previous balances
uint256 prevBalI = IERC20(state.tokens[inputTokenIndex]).balanceOf(address(this));
uint256 prevBalJ = IERC20(state.tokens[outputTokenIndex]).balanceOf(address(this));
// Compute amounts
(uint256 totalTransferAmount, uint256 amountOutUint, int128 amountInInternalUsed, int128 amountOutInternal, , uint256 feeUint) =
_quoteSwapExactIn(state, inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice, swapFeePpm);
// Transfer exact amount from payer
state.tokens[inputTokenIndex].safeTransferFrom(payer, address(this), totalTransferAmount);
uint256 balIAfter = IERC20(state.tokens[inputTokenIndex]).balanceOf(address(this));
require(balIAfter == prevBalI + totalTransferAmount, "swap: non-standard tokenIn");
// Transfer output to receiver
state.tokens[outputTokenIndex].safeTransfer(receiver, amountOutUint);
uint256 balJAfter = IERC20(state.tokens[outputTokenIndex]).balanceOf(address(this));
require(balJAfter == prevBalJ - amountOutUint, "swap: non-standard tokenOut");
// Update cached balances
state.cachedUintBalances[inputTokenIndex] = balIAfter;
state.cachedUintBalances[outputTokenIndex] = balJAfter;
// Apply swap to LMSR state
state.lmsr.applySwap(inputTokenIndex, outputTokenIndex, amountInInternalUsed, amountOutInternal);
emit PoolLib.Swap(payer, receiver, state.tokens[inputTokenIndex], state.tokens[outputTokenIndex], totalTransferAmount, amountOutUint);
return (totalTransferAmount, amountOutUint, feeUint);
}
}

View File

@@ -1,77 +1,111 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30; pragma solidity ^0.8.30;
/* solhint-disable erc20-unchecked-transfer */
import "forge-std/Test.sol"; import "../src/Deploy.sol";
import "@abdk/ABDKMath64x64.sol"; import "../src/IPartyFlashCallback.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "../src/LMSRStabilized.sol"; import "../src/LMSRStabilized.sol";
import "../src/PartyPool.sol"; import "../src/PartyPool.sol";
import "../src/PartyPlanner.sol"; import "@abdk/ABDKMath64x64.sol";
import "../lib/openzeppelin-contracts/contracts/interfaces/IERC3156FlashBorrower.sol";
import {Deploy} from "../src/Deploy.sol"; // Import the flash callback interface
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "forge-std/Test.sol";
/// @notice Test contract that implements the flash callback for testing flash loans /// @notice Test contract that implements the flash callback for testing flash loans
contract FlashBorrower is IERC3156FlashBorrower { contract FlashBorrower is IPartyFlashCallback {
enum Action { enum Action {
NORMAL, // Normal repayment NORMAL, // Normal repayment
REPAY_NONE, // Don't repay anything REPAY_NONE, // Don't repay anything
REPAY_PARTIAL, // Repay less than required REPAY_PARTIAL, // Repay less than required
REPAY_NO_FEE, // Repay only the principal without fee REPAY_NO_FEE, // Repay only the principal without fee
REPAY_EXACT // Repay exactly the required amount REPAY_EXACT, // Repay exactly the required amount
REPAY_EXTRA // Repay more than required (donation)
} }
Action public action; Action public action;
address public pool; address public pool;
address public payer; address public recipient;
address[] public tokens;
constructor(address _pool) { constructor(address _pool, IERC20[] memory _tokens) {
pool = _pool; pool = _pool;
tokens = new address[](_tokens.length);
for (uint i = 0; i < _tokens.length; i++) {
tokens[i] = address(_tokens[i]);
}
} }
function setAction(Action _action, address _payer) external { function setAction(Action _action, address _recipient) external {
action = _action; action = _action;
payer = _payer; recipient = _recipient;
} }
function onFlashLoan( function flash(uint256[] memory amounts) external {
address /*initiator*/, PartyPool(pool).flash(recipient, amounts, "");
address token, }
uint256 amount,
uint256 fee, function partyFlashCallback(
uint256[] memory loanAmounts,
uint256[] memory repaymentAmounts,
bytes calldata /* data */ bytes calldata /* data */
) external override returns (bytes32) { ) external override {
require(msg.sender == pool, "Callback not called by pool"); require(msg.sender == pool, "Callback not called by pool");
if (action == Action.NORMAL) { if (action == Action.NORMAL || action == Action.REPAY_EXTRA) {
// Normal repayment // Normal or extra repayment - transfer required amounts back to pool
// We received 'amount' from the pool, need to pay back amount + fee for (uint256 i = 0; i < loanAmounts.length; i++) {
uint256 repaymentAmount = amount + fee; if (loanAmounts[i] > 0) {
uint256 repaymentAmount = repaymentAmounts[i];
// Transfer the fee from payer to this contract // For REPAY_EXTRA, add 1 to each repayment
// (we already have the principal 'amount' from the flash loan) if (action == Action.REPAY_EXTRA) {
TestERC20(token).transferFrom(payer, address(this), fee); repaymentAmount += 1;
}
// Approve pool to pull back the full repayment // Transfer from recipient back to pool
TestERC20(token).approve(pool, repaymentAmount); TestERC20(tokens[i]).transferFrom(
recipient,
pool,
repaymentAmount
);
}
}
} else if (action == Action.REPAY_PARTIAL) { } else if (action == Action.REPAY_PARTIAL) {
// Repay half of the required amount // Repay half of the required amounts
uint256 partialRepayment = (amount + fee) / 2; for (uint256 i = 0; i < loanAmounts.length; i++) {
TestERC20(token).approve(pool, partialRepayment); if (loanAmounts[i] > 0) {
uint256 partialRepayment = repaymentAmounts[i] / 2;
TestERC20(tokens[i]).transferFrom(
recipient,
pool,
partialRepayment
);
}
}
} else if (action == Action.REPAY_NO_FEE) { } else if (action == Action.REPAY_NO_FEE) {
// Repay only the principal without fee (we already have it from the loan) // Repay only the principal without fee
TestERC20(token).approve(pool, amount); for (uint256 i = 0; i < loanAmounts.length; i++) {
if (loanAmounts[i] > 0) {
TestERC20(tokens[i]).transferFrom(
recipient,
pool,
loanAmounts[i]
);
}
}
} else if (action == Action.REPAY_EXACT) { } else if (action == Action.REPAY_EXACT) {
// Repay exactly what was required // Repay exactly what was required
uint256 repaymentAmount = amount + fee; for (uint256 i = 0; i < loanAmounts.length; i++) {
// Transfer the fee from payer (we have the principal from the loan) if (loanAmounts[i] > 0) {
TestERC20(token).transferFrom(payer, address(this), fee); TestERC20(tokens[i]).transferFrom(
// Approve pool to pull back the full repayment recipient,
TestERC20(token).approve(pool, repaymentAmount); pool,
repaymentAmounts[i]
);
}
}
} }
// For REPAY_NONE, do nothing (don't approve repayment) // For REPAY_NONE, do nothing (don't repay)
return keccak256("ERC3156FlashBorrower.onFlashLoan");
} }
} }
@@ -96,23 +130,21 @@ contract TestERC20 is ERC20 {
/// @notice Gas testing contract for PartyPool - contains all gas measurement tests /// @notice Gas testing contract for PartyPool - contains all gas measurement tests
contract GasTest is Test { contract GasTest is Test {
using ABDKMath64x64 for int128; using ABDKMath64x64 for int128;
using SafeERC20 for TestERC20;
PartyPlanner internal planner; PartyPool pool2;
PartyPool internal pool2; PartyPool pool10;
PartyPool internal pool10; PartyPool pool20;
PartyPool internal pool20; PartyPool pool50;
PartyPool internal pool50;
address internal alice; address alice;
address internal bob; address bob;
// Common parameters // Common parameters
int128 internal tradeFrac; int128 tradeFrac;
int128 internal targetSlippage; int128 targetSlippage;
uint256 constant internal INIT_BAL = 1_000_000; // initial token units for each token (internal==amount when base==1) uint256 constant INIT_BAL = 1_000_000; // initial token units for each token (internal==amount when base==1)
uint256 constant internal BASE = 1; // use base=1 so internal amounts correspond to raw integers (Q64.64 units) uint256 constant BASE = 1; // use base=1 so internal amounts correspond to raw integers (Q64.64 units)
/// @notice Helper function to create a pool with the specified number of tokens /// @notice Helper function to create a pool with the specified number of tokens
function createPool(uint256 numTokens) internal returns (PartyPool) { function createPool(uint256 numTokens) internal returns (PartyPool) {
@@ -198,8 +230,6 @@ contract GasTest is Test {
alice = address(0xA11ce); alice = address(0xA11ce);
bob = address(0xB0b); bob = address(0xB0b);
planner = Deploy.newPartyPlanner();
// Configure LMSR parameters similar to other tests: trade size 1% of asset -> 0.01, slippage 0.001 // Configure LMSR parameters similar to other tests: trade size 1% of asset -> 0.01, slippage 0.001
tradeFrac = ABDKMath64x64.divu(100, 10_000); // 0.01 tradeFrac = ABDKMath64x64.divu(100, 10_000); // 0.01
targetSlippage = ABDKMath64x64.divu(10, 10_000); // 0.001 targetSlippage = ABDKMath64x64.divu(10, 10_000); // 0.001
@@ -213,11 +243,13 @@ contract GasTest is Test {
/// @notice Setup a flash borrower for testing /// @notice Setup a flash borrower for testing
function setupFlashBorrower() internal returns (FlashBorrower borrower) { function setupFlashBorrower() internal returns (FlashBorrower borrower) {
// Get token addresses from the 2-token pool
IERC20[] memory tokenAddresses = pool2.allTokens();
// Deploy the borrower contract // Deploy the borrower contract
borrower = new FlashBorrower(address(pool2)); borrower = new FlashBorrower(address(pool2), tokenAddresses);
// Mint tokens to alice to be used for repayments and approve borrower // Mint tokens to alice to be used for repayments and approve borrower
IERC20[] memory tokenAddresses = pool2.allTokens();
vm.startPrank(alice); vm.startPrank(alice);
for (uint256 i = 0; i < tokenAddresses.length; i++) { for (uint256 i = 0; i < tokenAddresses.length; i++) {
TestERC20(address(tokenAddresses[i])).mint(alice, INIT_BAL * 2); TestERC20(address(tokenAddresses[i])).mint(alice, INIT_BAL * 2);
@@ -405,14 +437,33 @@ contract GasTest is Test {
// Configure borrower // Configure borrower
borrower.setAction(FlashBorrower.Action.NORMAL, alice); borrower.setAction(FlashBorrower.Action.NORMAL, alice);
// Get first token from pool // Create loan request for single token (get array size from pool)
IERC20[] memory poolTokens = pool2.allTokens(); IERC20[] memory poolTokens = pool2.allTokens();
address token = address(poolTokens[0]); uint256[] memory amounts = new uint256[](poolTokens.length);
uint256 amount = 1000; amounts[0] = 1000;
// Execute flash loan 10 times to measure gas // Execute flash loan 10 times to measure gas
for (uint256 i = 0; i < 10; i++) { for (uint256 i = 0; i < 10; i++) {
pool2.flashLoan(borrower, token, amount, ""); borrower.flash(amounts);
}
}
/// @notice Gas measurement: flash with multiple tokens
function testFlashGasMultipleTokens() public {
FlashBorrower borrower = setupFlashBorrower();
// Configure borrower
borrower.setAction(FlashBorrower.Action.NORMAL, alice);
// Create loan request for multiple tokens (get array size from pool)
IERC20[] memory poolTokens = pool2.allTokens();
uint256[] memory amounts = new uint256[](poolTokens.length);
amounts[0] = 1000;
amounts[1] = 2000;
// Execute flash loan 10 times to measure gas
for (uint256 i = 0; i < 10; i++) {
borrower.flash(amounts);
} }
} }
} }

View File

@@ -2,7 +2,7 @@
pragma solidity ^0.8.20; pragma solidity ^0.8.20;
import "forge-std/Test.sol"; import "forge-std/Test.sol";
import "forge-std/console2.sol"; import "forge-std/console.sol";
import "@openzeppelin/contracts/interfaces/IERC20Metadata.sol"; import "@openzeppelin/contracts/interfaces/IERC20Metadata.sol";
import "../src/LMSRStabilized.sol"; import "../src/LMSRStabilized.sol";
import "../src/LMSRStabilizedBalancedPair.sol"; import "../src/LMSRStabilizedBalancedPair.sol";
@@ -711,7 +711,7 @@ contract LMSRStabilizedTest is Test {
} }
// Path 1: Direct swap from asset 0 to asset 2 // Path 1: Direct swap from asset 0 to asset 2
(/*int128 directAmountIn*/, int128 directAmountOut) = s.swapAmountsForExactInput(0, 2, directSwapAmount, 0); (/* int128 directAmountIn */, int128 directAmountOut) = s.swapAmountsForExactInput(0, 2, directSwapAmount, 0);
// Restore original state for second path // Restore original state for second path
_updateCachedQInternal(backupQ); _updateCachedQInternal(backupQ);
@@ -724,7 +724,7 @@ contract LMSRStabilizedTest is Test {
s.qInternal[1] = s.qInternal[1].add(indirectAmountOut1); s.qInternal[1] = s.qInternal[1].add(indirectAmountOut1);
// Second swap: asset 1 -> asset 2 // Second swap: asset 1 -> asset 2
(/*int128 indirectAmountIn2*/, int128 indirectAmountOut2) = s.swapAmountsForExactInput(1, 2, indirectAmountOut1, 0); (/* int128 indirectAmountIn2 */, int128 indirectAmountOut2) = s.swapAmountsForExactInput(1, 2, indirectAmountOut1, 0);
// The path independence property isn't perfect due to discrete swap mechanics, // The path independence property isn't perfect due to discrete swap mechanics,
// but the difference should be within reasonable bounds // but the difference should be within reasonable bounds
@@ -765,7 +765,7 @@ contract LMSRStabilizedTest is Test {
s.qInternal[1] = s.qInternal[1].add(amountOut1); s.qInternal[1] = s.qInternal[1].add(amountOut1);
// Step 2: Swap back asset 1 -> asset 0 // Step 2: Swap back asset 1 -> asset 0
(/*int128 amountIn2*/, int128 amountOut2) = s.swapAmountsForExactInput(1, 0, amountOut1, 0); (/* int128 amountIn2 */, int128 amountOut2) = s.swapAmountsForExactInput(1, 0, amountOut1, 0);
// Calculate round-trip slippage: (initial amount - final amount) / initial amount // Calculate round-trip slippage: (initial amount - final amount) / initial amount
int128 roundTripSlippage = (amountIn1.sub(amountOut2)).div(amountIn1); int128 roundTripSlippage = (amountIn1.sub(amountOut2)).div(amountIn1);

View File

@@ -4,11 +4,11 @@ pragma solidity ^0.8.30;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MockERC20 is ERC20 { contract MockERC20 is ERC20 {
uint8 private immutable DECIMALS; uint8 private immutable _decimals;
constructor(string memory name, string memory symbol, uint8 decimals_) ERC20(name, symbol) {DECIMALS = decimals_;} constructor(string memory name, string memory symbol, uint8 decimals_) ERC20(name, symbol) {_decimals = decimals_;}
function decimals() public view virtual override returns (uint8) {return DECIMALS;} function decimals() public view virtual override returns (uint8) {return _decimals;}
function mint(address account, uint256 amount) external {_mint(account, amount);} function mint(address account, uint256 amount) external {_mint(account, amount);}
function burn(address account, uint256 amount) external {_burn(account, amount);} function burn(address account, uint256 amount) external {_burn(account, amount);}
} }

View File

@@ -1,20 +1,11 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30; pragma solidity ^0.8.30;
import {CommonBase} from "../lib/forge-std/src/Base.sol"; import "forge-std/Test.sol";
import {StdAssertions} from "../lib/forge-std/src/StdAssertions.sol"; import "../src/LMSRStabilized.sol";
import {StdChains} from "../lib/forge-std/src/StdChains.sol"; import "../src/PartyPlanner.sol";
import {StdCheats, StdCheatsSafe} from "../lib/forge-std/src/StdCheats.sol"; import "../src/PartyPool.sol";
import {StdUtils} from "../lib/forge-std/src/StdUtils.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {Test} from "../lib/forge-std/src/Test.sol";
import {ERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {Deploy} from "../src/Deploy.sol";
import {IPartyPool} from "../src/IPartyPool.sol";
import {LMSRStabilized} from "../src/LMSRStabilized.sol";
import {PartyPlanner} from "../src/PartyPlanner.sol";
import {PartyPool} from "../src/PartyPool.sol";
import {MockERC20} from "./PartyPlanner.t.sol";
// Mock ERC20 token for testing // Mock ERC20 token for testing
contract MockERC20 is ERC20 { contract MockERC20 is ERC20 {
@@ -47,7 +38,7 @@ contract PartyPlannerTest is Test {
function setUp() public { function setUp() public {
// Deploy PartyPlanner // Deploy PartyPlanner
planner = Deploy.newPartyPlanner(); planner = new PartyPlanner();
// Deploy mock tokens // Deploy mock tokens
tokenA = new MockERC20("Token A", "TKNA", 18); tokenA = new MockERC20("Token A", "TKNA", 18);
@@ -96,7 +87,7 @@ contract PartyPlannerTest is Test {
// Compute kappa then create pool via kappa overload // Compute kappa then create pool via kappa overload
int128 computedKappa = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage); int128 computedKappa = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage);
(IPartyPool pool, uint256 lpAmount) = planner.newPool( (PartyPool pool, uint256 lpAmount) = planner.createPool(
name, name,
symbol, symbol,
tokens, tokens,
@@ -125,7 +116,7 @@ contract PartyPlannerTest is Test {
assertEq(planner.poolsByTokenCount(IERC20(address(tokenB))), initialTokenBCount + 1, "TokenB pool count should increase"); assertEq(planner.poolsByTokenCount(IERC20(address(tokenB))), initialTokenBCount + 1, "TokenB pool count should increase");
// Verify pools can be retrieved // Verify pools can be retrieved
IPartyPool[] memory allPools = planner.getAllPools(0, 10); PartyPool[] memory allPools = planner.getAllPools(0, 10);
bool poolFound = false; bool poolFound = false;
for (uint256 i = 0; i < allPools.length; i++) { for (uint256 i = 0; i < allPools.length; i++) {
if (allPools[i] == pool) { if (allPools[i] == pool) {
@@ -136,7 +127,7 @@ contract PartyPlannerTest is Test {
assertTrue(poolFound, "Created pool should be in getAllPools result"); assertTrue(poolFound, "Created pool should be in getAllPools result");
// Verify pool appears in token-specific queries // Verify pool appears in token-specific queries
IPartyPool[] memory tokenAPools = planner.getPoolsByToken(IERC20(address(tokenA)), 0, 10); PartyPool[] memory tokenAPools = planner.getPoolsByToken(IERC20(address(tokenA)), 0, 10);
bool poolInTokenA = false; bool poolInTokenA = false;
for (uint256 i = 0; i < tokenAPools.length; i++) { for (uint256 i = 0; i < tokenAPools.length; i++) {
if (tokenAPools[i] == pool) { if (tokenAPools[i] == pool) {
@@ -146,7 +137,7 @@ contract PartyPlannerTest is Test {
} }
assertTrue(poolInTokenA, "Pool should be indexed under tokenA"); assertTrue(poolInTokenA, "Pool should be indexed under tokenA");
IPartyPool[] memory tokenBPools = planner.getPoolsByToken(IERC20(address(tokenB)), 0, 10); PartyPool[] memory tokenBPools = planner.getPoolsByToken(IERC20(address(tokenB)), 0, 10);
bool poolInTokenB = false; bool poolInTokenB = false;
for (uint256 i = 0; i < tokenBPools.length; i++) { for (uint256 i = 0; i < tokenBPools.length; i++) {
if (tokenBPools[i] == pool) { if (tokenBPools[i] == pool) {
@@ -175,7 +166,7 @@ contract PartyPlannerTest is Test {
deposits1[1] = INITIAL_DEPOSIT_AMOUNT; deposits1[1] = INITIAL_DEPOSIT_AMOUNT;
int128 kappa1 = LMSRStabilized.computeKappaFromSlippage(tokens1.length, int128((1 << 64) - 1), int128(1 << 62)); int128 kappa1 = LMSRStabilized.computeKappaFromSlippage(tokens1.length, int128((1 << 64) - 1), int128(1 << 62));
(IPartyPool pool1,) = planner.newPool( (PartyPool pool1,) = planner.createPool(
"Pool 1", "LP1", tokens1, bases1, "Pool 1", "LP1", tokens1, bases1,
kappa1, 3000, 5000, false, kappa1, 3000, 5000, false,
payer, receiver, deposits1, 1000e18, 0 payer, receiver, deposits1, 1000e18, 0
@@ -195,7 +186,7 @@ contract PartyPlannerTest is Test {
deposits2[1] = INITIAL_DEPOSIT_AMOUNT / 1e12; // Adjust for 6 decimals deposits2[1] = INITIAL_DEPOSIT_AMOUNT / 1e12; // Adjust for 6 decimals
int128 kappa2 = LMSRStabilized.computeKappaFromSlippage(tokens2.length, int128((1 << 64) - 1), int128(1 << 62)); int128 kappa2 = LMSRStabilized.computeKappaFromSlippage(tokens2.length, int128((1 << 64) - 1), int128(1 << 62));
(IPartyPool pool2,) = planner.newPool( (PartyPool pool2,) = planner.createPool(
"Pool 2", "LP2", tokens2, bases2, "Pool 2", "LP2", tokens2, bases2,
kappa2, 3000, 5000, false, kappa2, 3000, 5000, false,
payer, receiver, deposits2, 1000e18, 0 payer, receiver, deposits2, 1000e18, 0
@@ -211,7 +202,7 @@ contract PartyPlannerTest is Test {
assertEq(planner.poolsByTokenCount(IERC20(address(tokenC))), 1, "TokenC should be in 1 pool"); assertEq(planner.poolsByTokenCount(IERC20(address(tokenC))), 1, "TokenC should be in 1 pool");
// Verify tokenB appears in both pools // Verify tokenB appears in both pools
IPartyPool[] memory tokenBPools = planner.getPoolsByToken(IERC20(address(tokenB)), 0, 10); PartyPool[] memory tokenBPools = planner.getPoolsByToken(IERC20(address(tokenB)), 0, 10);
assertEq(tokenBPools.length, 2, "TokenB should have 2 pools"); assertEq(tokenBPools.length, 2, "TokenB should have 2 pools");
bool pool1Found = false; bool pool1Found = false;
@@ -239,7 +230,7 @@ contract PartyPlannerTest is Test {
// Test token/deposit length mismatch // Test token/deposit length mismatch
vm.expectRevert("Planner: tokens and deposits length mismatch"); vm.expectRevert("Planner: tokens and deposits length mismatch");
// call old-signature convenience (it will still exist) for the mismatched-length revert check // call old-signature convenience (it will still exist) for the mismatched-length revert check
planner.newPool( planner.createPool(
"Test Pool", "TESTLP", tokens, bases, "Test Pool", "TESTLP", tokens, bases,
int128((1 << 64) - 1), int128(1 << 62), 3000, 5000, false, int128((1 << 64) - 1), int128(1 << 62), 3000, 5000, false,
payer, receiver, deposits, 1000e18, 0 payer, receiver, deposits, 1000e18, 0
@@ -253,7 +244,7 @@ contract PartyPlannerTest is Test {
int128 kappaErr = LMSRStabilized.computeKappaFromSlippage(tokens.length, int128((1 << 64) - 1), int128(1 << 62)); int128 kappaErr = LMSRStabilized.computeKappaFromSlippage(tokens.length, int128((1 << 64) - 1), int128(1 << 62));
vm.expectRevert("Planner: payer cannot be zero address"); vm.expectRevert("Planner: payer cannot be zero address");
planner.newPool( planner.createPool(
"Test Pool", "TESTLP", tokens, bases, "Test Pool", "TESTLP", tokens, bases,
kappaErr, 3000, 5000, false, kappaErr, 3000, 5000, false,
address(0), receiver, validDeposits, 1000e18, 0 address(0), receiver, validDeposits, 1000e18, 0
@@ -261,7 +252,7 @@ contract PartyPlannerTest is Test {
// Test zero receiver address // Test zero receiver address
vm.expectRevert("Planner: receiver cannot be zero address"); vm.expectRevert("Planner: receiver cannot be zero address");
planner.newPool( planner.createPool(
"Test Pool", "TESTLP", tokens, bases, "Test Pool", "TESTLP", tokens, bases,
kappaErr, 3000, 5000, false, kappaErr, 3000, 5000, false,
payer, address(0), validDeposits, 1000e18, 0 payer, address(0), validDeposits, 1000e18, 0
@@ -272,7 +263,7 @@ contract PartyPlannerTest is Test {
int128 kappaDeadline = LMSRStabilized.computeKappaFromSlippage(tokens.length, int128((1 << 64) - 1), int128(1 << 62)); int128 kappaDeadline = LMSRStabilized.computeKappaFromSlippage(tokens.length, int128((1 << 64) - 1), int128(1 << 62));
vm.warp(1000); vm.warp(1000);
vm.expectRevert("Planner: deadline exceeded"); vm.expectRevert("Planner: deadline exceeded");
planner.newPool( planner.createPool(
"Test Pool", "TESTLP", tokens, bases, "Test Pool", "TESTLP", tokens, bases,
kappaDeadline, 3000, 5000, false, kappaDeadline, 3000, 5000, false,
payer, receiver, validDeposits, 1000e18, block.timestamp - 1 payer, receiver, validDeposits, 1000e18, block.timestamp - 1
@@ -282,7 +273,7 @@ contract PartyPlannerTest is Test {
function test_poolIndexing_Pagination() public { function test_poolIndexing_Pagination() public {
// Create multiple pools for pagination testing // Create multiple pools for pagination testing
uint256 numPools = 5; uint256 numPools = 5;
IPartyPool[] memory createdPools = new IPartyPool[](numPools); PartyPool[] memory createdPools = new PartyPool[](numPools);
for (uint256 i = 0; i < numPools; i++) { for (uint256 i = 0; i < numPools; i++) {
IERC20[] memory tokens = new IERC20[](2); IERC20[] memory tokens = new IERC20[](2);
@@ -298,7 +289,7 @@ contract PartyPlannerTest is Test {
deposits[1] = INITIAL_DEPOSIT_AMOUNT; deposits[1] = INITIAL_DEPOSIT_AMOUNT;
int128 kappaLoop = LMSRStabilized.computeKappaFromSlippage(tokens.length, int128((1 << 64) - 1), int128(1 << 62)); int128 kappaLoop = LMSRStabilized.computeKappaFromSlippage(tokens.length, int128((1 << 64) - 1), int128(1 << 62));
(IPartyPool pool,) = planner.newPool( (PartyPool pool,) = planner.createPool(
string(abi.encodePacked("Pool ", vm.toString(i))), string(abi.encodePacked("Pool ", vm.toString(i))),
string(abi.encodePacked("LP", vm.toString(i))), string(abi.encodePacked("LP", vm.toString(i))),
tokens, bases, tokens, bases,
@@ -312,19 +303,19 @@ contract PartyPlannerTest is Test {
assertEq(planner.poolCount(), numPools, "Should have created all pools"); assertEq(planner.poolCount(), numPools, "Should have created all pools");
// Test pagination - get first 3 pools // Test pagination - get first 3 pools
IPartyPool[] memory page1 = planner.getAllPools(0, 3); PartyPool[] memory page1 = planner.getAllPools(0, 3);
assertEq(page1.length, 3, "First page should have 3 pools"); assertEq(page1.length, 3, "First page should have 3 pools");
// Test pagination - get next 2 pools // Test pagination - get next 2 pools
IPartyPool[] memory page2 = planner.getAllPools(3, 3); PartyPool[] memory page2 = planner.getAllPools(3, 3);
assertEq(page2.length, 2, "Second page should have 2 pools"); assertEq(page2.length, 2, "Second page should have 2 pools");
// Test pagination - offset beyond bounds // Test pagination - offset beyond bounds
IPartyPool[] memory emptyPage = planner.getAllPools(10, 3); PartyPool[] memory emptyPage = planner.getAllPools(10, 3);
assertEq(emptyPage.length, 0, "Should return empty array for out of bounds offset"); assertEq(emptyPage.length, 0, "Should return empty array for out of bounds offset");
// Verify all pools are accessible through pagination // Verify all pools are accessible through pagination
IPartyPool[] memory allPools = planner.getAllPools(0, 10); PartyPool[] memory allPools = planner.getAllPools(0, 10);
assertEq(allPools.length, numPools, "Should return all pools"); assertEq(allPools.length, numPools, "Should return all pools");
for (uint256 i = 0; i < numPools; i++) { for (uint256 i = 0; i < numPools; i++) {

View File

@@ -1,80 +1,108 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
/* solhint-disable */
pragma solidity ^0.8.30; pragma solidity ^0.8.30;
import "forge-std/Test.sol"; import "../src/Deploy.sol";
import "@abdk/ABDKMath64x64.sol"; import "../src/IPartyFlashCallback.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "../src/LMSRStabilized.sol"; import "../src/LMSRStabilized.sol";
import "../src/PartyPool.sol"; import "../src/PartyPool.sol";
import "@abdk/ABDKMath64x64.sol";
// Import the flash callback interface // Import the flash callback interface
import "../lib/openzeppelin-contracts/contracts/interfaces/IERC3156FlashBorrower.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {PartyPlanner} from "../src/PartyPlanner.sol"; import "forge-std/Test.sol";
import {Deploy} from "../src/Deploy.sol";
import {PartyPoolViewer} from "../src/PartyPoolViewer.sol";
/// @notice Test contract that implements the flash callback for testing flash loans /// @notice Test contract that implements the flash callback for testing flash loans
contract FlashBorrower is IERC3156FlashBorrower { contract FlashBorrower is IPartyFlashCallback {
enum Action { enum Action {
NORMAL, // Normal repayment NORMAL, // Normal repayment
REPAY_NONE, // Don't repay anything REPAY_NONE, // Don't repay anything
REPAY_PARTIAL, // Repay less than required REPAY_PARTIAL, // Repay less than required
REPAY_NO_FEE, // Repay only the principal without fee REPAY_NO_FEE, // Repay only the principal without fee
REPAY_EXACT // Repay exactly the required amount REPAY_EXACT, // Repay exactly the required amount
REPAY_EXTRA // Repay more than required (donation)
} }
Action public action; Action public action;
address public pool; address public pool;
address public payer; address public recipient;
address[] public tokens;
constructor(address _pool) { constructor(address _pool, address[] memory _tokens) {
pool = _pool; pool = _pool;
tokens = _tokens;
} }
function setAction(Action _action, address _payer) external { function setAction(Action _action, address _recipient) external {
action = _action; action = _action;
payer = _payer; recipient = _recipient;
} }
function onFlashLoan( function flash(uint256[] memory amounts) external {
address /*initiator*/, PartyPool(pool).flash(recipient, amounts, "");
address token, }
uint256 amount,
uint256 fee, function partyFlashCallback(
uint256[] memory loanAmounts,
uint256[] memory repaymentAmounts,
bytes calldata /* data */ bytes calldata /* data */
) external override returns (bytes32) { ) external override {
require(msg.sender == pool, "Callback not called by pool"); require(msg.sender == pool, "Callback not called by pool");
if (action == Action.NORMAL) { if (action == Action.NORMAL || action == Action.REPAY_EXTRA) {
// Normal repayment // Normal or extra repayment - transfer required amounts back to pool
// We received 'amount' from the pool, need to pay back amount + fee for (uint256 i = 0; i < loanAmounts.length; i++) {
uint256 repaymentAmount = amount + fee; if (loanAmounts[i] > 0) {
uint256 repaymentAmount = repaymentAmounts[i];
// Transfer the fee from payer to this contract // For REPAY_EXTRA, add 1 to each repayment
// (we already have the principal 'amount' from the flash loan) if (action == Action.REPAY_EXTRA) {
TestERC20(token).transferFrom(payer, address(this), fee); repaymentAmount += 1;
}
// Approve pool to pull back the full repayment // Transfer from recipient back to pool
TestERC20(token).approve(pool, repaymentAmount); TestERC20(tokens[i]).transferFrom(
recipient,
pool,
repaymentAmount
);
}
}
} else if (action == Action.REPAY_PARTIAL) { } else if (action == Action.REPAY_PARTIAL) {
// Repay half of the required amount // Repay half of the required amounts
uint256 partialRepayment = (amount + fee) / 2; for (uint256 i = 0; i < loanAmounts.length; i++) {
TestERC20(token).approve(pool, partialRepayment); if (loanAmounts[i] > 0) {
uint256 partialRepayment = repaymentAmounts[i] / 2;
TestERC20(tokens[i]).transferFrom(
recipient,
pool,
partialRepayment
);
}
}
} else if (action == Action.REPAY_NO_FEE) { } else if (action == Action.REPAY_NO_FEE) {
// Repay only the principal without fee (we already have it from the loan) // Repay only the principal without fee
TestERC20(token).approve(pool, amount); for (uint256 i = 0; i < loanAmounts.length; i++) {
if (loanAmounts[i] > 0) {
TestERC20(tokens[i]).transferFrom(
recipient,
pool,
loanAmounts[i]
);
}
}
} else if (action == Action.REPAY_EXACT) { } else if (action == Action.REPAY_EXACT) {
// Repay exactly what was required // Repay exactly what was required
uint256 repaymentAmount = amount + fee; for (uint256 i = 0; i < loanAmounts.length; i++) {
// Transfer the fee from payer (we have the principal from the loan) if (loanAmounts[i] > 0) {
TestERC20(token).transferFrom(payer, address(this), fee); TestERC20(tokens[i]).transferFrom(
// Approve pool to pull back the full repayment recipient,
TestERC20(token).approve(pool, repaymentAmount); pool,
repaymentAmounts[i]
);
}
}
} }
// For REPAY_NONE, do nothing (don't approve repayment) // For REPAY_NONE, do nothing (don't repay)
return keccak256("ERC3156FlashBorrower.onFlashLoan");
} }
} }
@@ -110,10 +138,8 @@ contract PartyPoolTest is Test {
TestERC20 token7; TestERC20 token7;
TestERC20 token8; TestERC20 token8;
TestERC20 token9; TestERC20 token9;
PartyPlanner planner;
PartyPool pool; PartyPool pool;
PartyPool pool10; PartyPool pool10;
PartyPoolViewer viewer;
address alice; address alice;
address bob; address bob;
@@ -126,7 +152,6 @@ contract PartyPoolTest is Test {
uint256 constant BASE = 1; // use base=1 so internal amounts correspond to raw integers (Q64.64 units) uint256 constant BASE = 1; // use base=1 so internal amounts correspond to raw integers (Q64.64 units)
function setUp() public { function setUp() public {
planner = Deploy.newPartyPlanner();
alice = address(0xA11ce); alice = address(0xA11ce);
bob = address(0xB0b); bob = address(0xB0b);
@@ -254,8 +279,6 @@ contract PartyPoolTest is Test {
token7.mint(bob, INIT_BAL); token7.mint(bob, INIT_BAL);
token8.mint(bob, INIT_BAL); token8.mint(bob, INIT_BAL);
token9.mint(bob, INIT_BAL); token9.mint(bob, INIT_BAL);
viewer = Deploy.newViewer();
} }
/// @notice Basic sanity: initial mint should have produced LP tokens for this contract and the pool holds tokens. /// @notice Basic sanity: initial mint should have produced LP tokens for this contract and the pool holds tokens.
@@ -297,7 +320,7 @@ contract PartyPoolTest is Test {
token2.approve(address(pool), type(uint256).max); token2.approve(address(pool), type(uint256).max);
// Inspect the deposit amounts that the pool will require (these are rounded up) // Inspect the deposit amounts that the pool will require (these are rounded up)
uint256[] memory deposits = viewer.mintAmounts(pool, 1); uint256[] memory deposits = pool.mintDepositAmounts(1);
// Basic sanity: deposits array length must match token count and not all zero necessarily // Basic sanity: deposits array length must match token count and not all zero necessarily
assertEq(deposits.length, 3); assertEq(deposits.length, 3);
@@ -339,7 +362,7 @@ contract PartyPoolTest is Test {
uint256 totalLpBefore = pool.totalSupply(); uint256 totalLpBefore = pool.totalSupply();
// Compute required deposits and perform mint for 1 wei // Compute required deposits and perform mint for 1 wei
uint256[] memory deposits = viewer.mintAmounts(pool, 1); uint256[] memory deposits = pool.mintDepositAmounts(1);
// Sum deposits as deposited_value // Sum deposits as deposited_value
uint256 depositedValue = 0; uint256 depositedValue = 0;
@@ -373,14 +396,14 @@ contract PartyPoolTest is Test {
vm.stopPrank(); vm.stopPrank();
} }
/// @notice mintAmounts should round up deposit amounts to protect the pool. /// @notice mintDepositAmounts should round up deposit amounts to protect the pool.
function testMintDepositAmountsRoundingUp() public view { function testMintDepositAmountsRoundingUp() public view {
uint256 totalLp = pool.totalSupply(); uint256 totalLp = pool.totalSupply();
assertTrue(totalLp > 0, "precondition: total supply > 0"); assertTrue(totalLp > 0, "precondition: total supply > 0");
// Request half of LP supply // Request half of LP supply
uint256 want = totalLp / 2; uint256 want = totalLp / 2;
uint256[] memory deposits = viewer.mintAmounts(pool, want); uint256[] memory deposits = pool.mintDepositAmounts(want);
// We expect each deposit to be roughly half the pool balance, but due to rounding up it should satisfy: // We expect each deposit to be roughly half the pool balance, but due to rounding up it should satisfy:
// deposits[i] * 2 >= cached balance (i.e., rounding up) // deposits[i] * 2 >= cached balance (i.e., rounding up)
@@ -397,7 +420,7 @@ contract PartyPoolTest is Test {
assertTrue(totalLp > 0, "precondition: LP > 0"); assertTrue(totalLp > 0, "precondition: LP > 0");
// Compute amounts required to redeem entire supply (should be current balances) // Compute amounts required to redeem entire supply (should be current balances)
uint256[] memory withdrawAmounts = viewer.burnAmounts(pool, totalLp); uint256[] memory withdrawAmounts = pool.burnReceiveAmounts(totalLp);
// Sanity: withdrawAmounts should equal pool balances (or very close due to rounding) // Sanity: withdrawAmounts should equal pool balances (or very close due to rounding)
for (uint i = 0; i < withdrawAmounts.length; i++) { for (uint i = 0; i < withdrawAmounts.length; i++) {
@@ -487,7 +510,7 @@ contract PartyPoolTest is Test {
} }
/// @notice Verify mintAmounts matches the actual token transfers performed by mint() /// @notice Verify mintDepositAmounts matches the actual token transfers performed by mint()
function testMintDepositAmountsMatchesMint_3TokenPool() public { function testMintDepositAmountsMatchesMint_3TokenPool() public {
// Use a range of LP requests (tiny to large fraction) // Use a range of LP requests (tiny to large fraction)
uint256 totalLp = pool.totalSupply(); uint256 totalLp = pool.totalSupply();
@@ -501,7 +524,7 @@ contract PartyPoolTest is Test {
if (req == 0) req = 1; if (req == 0) req = 1;
// Compute expected deposit amounts via view // Compute expected deposit amounts via view
uint256[] memory expected = viewer.mintAmounts(pool, req); uint256[] memory expected = pool.mintDepositAmounts(req);
// Ensure alice has tokens and approve pool // Ensure alice has tokens and approve pool
vm.startPrank(alice); vm.startPrank(alice);
@@ -515,7 +538,7 @@ contract PartyPoolTest is Test {
uint256 a2Before = token2.balanceOf(alice); uint256 a2Before = token2.balanceOf(alice);
// Perform mint (may revert for zero-request; ensure req>0 above) // Perform mint (may revert for zero-request; ensure req>0 above)
// Guard: if mintAmounts returned all zeros, skip (nothing to transfer) // Guard: if mintDepositAmounts returned all zeros, skip (nothing to transfer)
bool allZero = (expected[0] == 0 && expected[1] == 0 && expected[2] == 0); bool allZero = (expected[0] == 0 && expected[1] == 0 && expected[2] == 0);
if (!allZero) { if (!allZero) {
uint256 lpBefore = pool.balanceOf(alice); uint256 lpBefore = pool.balanceOf(alice);
@@ -534,7 +557,7 @@ contract PartyPoolTest is Test {
} }
} }
/// @notice Verify mintAmounts matches the actual token transfers performed by mint() for 10-token pool /// @notice Verify mintDepositAmounts matches the actual token transfers performed by mint() for 10-token pool
function testMintDepositAmountsMatchesMint_10TokenPool() public { function testMintDepositAmountsMatchesMint_10TokenPool() public {
uint256 totalLp = pool10.totalSupply(); uint256 totalLp = pool10.totalSupply();
uint256[] memory requests = new uint256[](4); uint256[] memory requests = new uint256[](4);
@@ -546,7 +569,7 @@ contract PartyPoolTest is Test {
uint256 req = requests[k]; uint256 req = requests[k];
if (req == 0) req = 1; if (req == 0) req = 1;
uint256[] memory expected = viewer.mintAmounts(pool10, req); uint256[] memory expected = pool10.mintDepositAmounts(req);
// Approve all tokens from alice // Approve all tokens from alice
vm.startPrank(alice); vm.startPrank(alice);
@@ -597,7 +620,7 @@ contract PartyPoolTest is Test {
} }
} }
/// @notice Verify burnAmounts matches actual transfers performed by burn() for 3-token pool /// @notice Verify burnReceiveAmounts matches actual transfers performed by burn() for 3-token pool
function testBurnReceiveAmountsMatchesBurn_3TokenPool() public { function testBurnReceiveAmountsMatchesBurn_3TokenPool() public {
// Use address(this) as payer (holds initial LP from setUp) // Use address(this) as payer (holds initial LP from setUp)
uint256 totalLp = pool.totalSupply(); uint256 totalLp = pool.totalSupply();
@@ -624,7 +647,7 @@ contract PartyPoolTest is Test {
} }
// Recompute withdraw amounts via view after any top-up // Recompute withdraw amounts via view after any top-up
uint256[] memory expected = viewer.burnAmounts(pool, req); uint256[] memory expected = pool.burnReceiveAmounts(req);
// If expected withdraws are all zero (rounding edge), skip this iteration // If expected withdraws are all zero (rounding edge), skip this iteration
if (expected[0] == 0 && expected[1] == 0 && expected[2] == 0) { if (expected[0] == 0 && expected[1] == 0 && expected[2] == 0) {
@@ -650,7 +673,7 @@ contract PartyPoolTest is Test {
} }
} }
/// @notice Verify burnAmounts matches actual transfers performed by burn() for 10-token pool /// @notice Verify burnReceiveAmounts matches actual transfers performed by burn() for 10-token pool
function testBurnReceiveAmountsMatchesBurn_10TokenPool() public { function testBurnReceiveAmountsMatchesBurn_10TokenPool() public {
uint256 totalLp = pool10.totalSupply(); uint256 totalLp = pool10.totalSupply();
uint256[] memory burns = new uint256[](4); uint256[] memory burns = new uint256[](4);
@@ -681,7 +704,7 @@ contract PartyPoolTest is Test {
vm.stopPrank(); vm.stopPrank();
} }
uint256[] memory expected = viewer.burnAmounts(pool10, req); uint256[] memory expected = pool10.burnReceiveAmounts(req);
// If expected withdraws are all zero (rounding edge), skip this iteration // If expected withdraws are all zero (rounding edge), skip this iteration
bool allZero = true; bool allZero = true;
@@ -815,8 +838,14 @@ contract PartyPoolTest is Test {
/// @notice Setup a flash borrower for testing /// @notice Setup a flash borrower for testing
function setupFlashBorrower() internal returns (FlashBorrower borrower) { function setupFlashBorrower() internal returns (FlashBorrower borrower) {
// Create array of token addresses for borrower
address[] memory tokenAddresses = new address[](3);
tokenAddresses[0] = address(token0);
tokenAddresses[1] = address(token1);
tokenAddresses[2] = address(token2);
// Deploy the borrower contract // Deploy the borrower contract
borrower = new FlashBorrower(address(pool)); borrower = new FlashBorrower(address(pool), tokenAddresses);
// Mint tokens to alice to be used for repayments // Mint tokens to alice to be used for repayments
token0.mint(alice, INIT_BAL * 2); token0.mint(alice, INIT_BAL * 2);
@@ -839,17 +868,18 @@ contract PartyPoolTest is Test {
borrower.setAction(FlashBorrower.Action.NORMAL, alice); borrower.setAction(FlashBorrower.Action.NORMAL, alice);
// Create loan request for token0 only // Create loan request for token0 only
uint256 amount = 1000; uint256[] memory amounts = new uint256[](3);
amounts[0] = 1000; // Only borrow token0
// Record balances before flash // Record balances before flash
uint256 aliceToken0Before = token0.balanceOf(alice); uint256 aliceToken0Before = token0.balanceOf(alice);
uint256 poolToken0Before = token0.balanceOf(address(pool)); uint256 poolToken0Before = token0.balanceOf(address(pool));
// Execute flash loan // Execute flash loan
pool.flashLoan(borrower, address(token0), amount, ""); borrower.flash(amounts);
// Net change for alice should equal the flash fee (principal is returned during repayment) // Net change for alice should equal the flash fee (principal is returned during repayment)
uint256 fee = (amount * pool.flashFeePpm() + 1_000_000 - 1) / 1_000_000; // ceil fee calculation uint256 fee = (amounts[0] * pool.flashFeePpm() + 1_000_000 - 1) / 1_000_000; // ceil fee calculation
uint256 expectedAliceDecrease = fee; uint256 expectedAliceDecrease = fee;
assertEq( assertEq(
aliceToken0Before - token0.balanceOf(alice), aliceToken0Before - token0.balanceOf(alice),
@@ -865,6 +895,126 @@ contract PartyPoolTest is Test {
); );
} }
/// @notice Test flash loan with multiple tokens
function testFlashLoanMultipleTokens() public {
FlashBorrower borrower = setupFlashBorrower();
// Configure borrower to repay normally
borrower.setAction(FlashBorrower.Action.NORMAL, alice);
// Create loan request for all tokens
uint256[] memory amounts = new uint256[](3);
amounts[0] = 1000;
amounts[1] = 2000;
amounts[2] = 3000;
// Record balances before flash
uint256[] memory aliceBalancesBefore = new uint256[](3);
uint256[] memory poolBalancesBefore = new uint256[](3);
aliceBalancesBefore[0] = token0.balanceOf(alice);
aliceBalancesBefore[1] = token1.balanceOf(alice);
aliceBalancesBefore[2] = token2.balanceOf(alice);
poolBalancesBefore[0] = token0.balanceOf(address(pool));
poolBalancesBefore[1] = token1.balanceOf(address(pool));
poolBalancesBefore[2] = token2.balanceOf(address(pool));
// Execute flash loan
borrower.flash(amounts);
// Check balances for each token
for (uint256 i = 0; i < 3; i++) {
uint256 fee = (amounts[i] * pool.flashFeePpm() + 1_000_000 - 1) / 1_000_000; // ceil fee calculation
uint256 expectedAliceDecrease = fee;
IERC20 token;
if (i == 0) token = token0;
else if (i == 1) token = token1;
else token = token2;
// Net change for Alice should equal the flash fee for this token (principal was returned)
assertEq(
aliceBalancesBefore[i] - token.balanceOf(alice),
expectedAliceDecrease,
"Alice should pay flash fee for token"
);
// Pool's balance increased by fee
assertEq(
token.balanceOf(address(pool)),
poolBalancesBefore[i] + fee,
"Pool should receive fee for token"
);
}
}
/// @notice Test flash loan with some zero amounts (should be skipped)
function testFlashLoanWithZeroAmounts() public {
FlashBorrower borrower = setupFlashBorrower();
// Configure borrower to repay normally
borrower.setAction(FlashBorrower.Action.NORMAL, alice);
// Create loan request with mix of zero and non-zero amounts
uint256[] memory amounts = new uint256[](3);
amounts[0] = 0; // Zero - should be skipped
amounts[1] = 2000; // Non-zero
amounts[2] = 0; // Zero - should be skipped
// Record balances before flash
uint256 aliceToken1Before = token1.balanceOf(alice);
uint256 poolToken1Before = token1.balanceOf(address(pool));
// Tokens that should remain unchanged
uint256 aliceToken0Before = token0.balanceOf(alice);
uint256 aliceToken2Before = token2.balanceOf(alice);
uint256 poolToken0Before = token0.balanceOf(address(pool));
uint256 poolToken2Before = token2.balanceOf(address(pool));
// Execute flash loan
borrower.flash(amounts);
// Check token1 balances changed appropriately
uint256 fee = (amounts[1] * pool.flashFeePpm() + 1_000_000 - 1) / 1_000_000; // ceil fee calculation
uint256 expectedAliceDecrease = fee;
assertEq(
aliceToken1Before - token1.balanceOf(alice),
expectedAliceDecrease,
"Alice should pay flash fee for token1"
);
assertEq(
token1.balanceOf(address(pool)),
poolToken1Before + fee,
"Pool should receive fee for token1"
);
// Check token0 and token2 balances remained unchanged
assertEq(token0.balanceOf(alice), aliceToken0Before, "Alice token0 balance should be unchanged");
assertEq(token2.balanceOf(alice), aliceToken2Before, "Alice token2 balance should be unchanged");
assertEq(token0.balanceOf(address(pool)), poolToken0Before, "Pool token0 balance should be unchanged");
assertEq(token2.balanceOf(address(pool)), poolToken2Before, "Pool token2 balance should be unchanged");
}
/// @notice Test that flash reverts when all amounts are zero
function testFlashLoanAllZeroAmountsReverts() public {
FlashBorrower borrower = setupFlashBorrower();
// Configure borrower to repay normally
borrower.setAction(FlashBorrower.Action.NORMAL, alice);
// Create loan request with all zeros
uint256[] memory amounts = new uint256[](3);
amounts[0] = 0;
amounts[1] = 0;
amounts[2] = 0;
// Execute flash loan - should revert
vm.expectRevert(bytes("flash: no tokens requested"));
borrower.flash(amounts);
}
/// @notice Test flash loan with incorrect repayment (none) /// @notice Test flash loan with incorrect repayment (none)
function testFlashLoanNoRepaymentReverts() public { function testFlashLoanNoRepaymentReverts() public {
@@ -874,11 +1024,12 @@ contract PartyPoolTest is Test {
borrower.setAction(FlashBorrower.Action.REPAY_NONE, alice); borrower.setAction(FlashBorrower.Action.REPAY_NONE, alice);
// Create loan request // Create loan request
uint256 amount = 1000; uint256[] memory amounts = new uint256[](3);
amounts[0] = 1000;
// Execute flash loan - should revert due to insufficient allowance when pool tries to pull repayment // Execute flash loan - should revert on validation
vm.expectRevert(); vm.expectRevert(bytes("flash: repayment failed"));
pool.flashLoan(borrower, address(token0), amount, ""); borrower.flash(amounts);
} }
/// @notice Test flash loan with partial repayment (should revert) /// @notice Test flash loan with partial repayment (should revert)
@@ -889,11 +1040,12 @@ contract PartyPoolTest is Test {
borrower.setAction(FlashBorrower.Action.REPAY_PARTIAL, alice); borrower.setAction(FlashBorrower.Action.REPAY_PARTIAL, alice);
// Create loan request // Create loan request
uint256 amount = 1000; uint256[] memory amounts = new uint256[](3);
amounts[0] = 1000;
// Execute flash loan - should revert due to insufficient allowance when pool tries to pull full repayment // Execute flash loan - should revert on validation
vm.expectRevert(); vm.expectRevert(bytes("flash: repayment failed"));
pool.flashLoan(borrower, address(token0), amount, ""); borrower.flash(amounts);
} }
/// @notice Test flash loan with principal repayment but no fee (should revert) /// @notice Test flash loan with principal repayment but no fee (should revert)
@@ -904,15 +1056,16 @@ contract PartyPoolTest is Test {
borrower.setAction(FlashBorrower.Action.REPAY_NO_FEE, alice); borrower.setAction(FlashBorrower.Action.REPAY_NO_FEE, alice);
// Create loan request // Create loan request
uint256 amount = 1000; uint256[] memory amounts = new uint256[](3);
amounts[0] = 1000;
// Execute flash loan - should revert due to insufficient allowance if fee > 0 // Execute flash loan - should revert on validation if fee > 0
if (pool.flashFeePpm() > 0) { if (pool.flashFeePpm() > 0) {
vm.expectRevert(); vm.expectRevert(bytes("flash: repayment failed"));
pool.flashLoan(borrower, address(token0), amount, ""); borrower.flash(amounts);
} else { } else {
// If fee is zero, this should succeed // If fee is zero, this should succeed
pool.flashLoan(borrower, address(token0), amount, ""); borrower.flash(amounts);
} }
} }
@@ -924,17 +1077,18 @@ contract PartyPoolTest is Test {
borrower.setAction(FlashBorrower.Action.REPAY_EXACT, alice); borrower.setAction(FlashBorrower.Action.REPAY_EXACT, alice);
// Create loan request // Create loan request
uint256 amount = 1000; uint256[] memory amounts = new uint256[](3);
amounts[0] = 1000;
// Record balances before flash // Record balances before flash
uint256 aliceToken0Before = token0.balanceOf(alice); uint256 aliceToken0Before = token0.balanceOf(alice);
uint256 poolToken0Before = token0.balanceOf(address(pool)); uint256 poolToken0Before = token0.balanceOf(address(pool));
// Execute flash loan // Execute flash loan
pool.flashLoan(borrower, address(token0), amount, ""); borrower.flash(amounts);
// Check balances: net change for alice should equal the fee // Check balances: net change for alice should equal the fee
uint256 fee = (amount * pool.flashFeePpm() + 1_000_000 - 1) / 1_000_000; // ceil fee calculation uint256 fee = (amounts[0] * pool.flashFeePpm() + 1_000_000 - 1) / 1_000_000; // ceil fee calculation
uint256 expectedAliceDecrease = fee; uint256 expectedAliceDecrease = fee;
assertEq( assertEq(
@@ -950,29 +1104,115 @@ contract PartyPoolTest is Test {
); );
} }
/// @notice Test flashFee view function matches flash implementation /// @notice Test flash loan with extra repayment (donation, should succeed)
function testFlashFee() public view { function testFlashLoanExtraRepayment() public {
// Test different loan amounts FlashBorrower borrower = setupFlashBorrower();
uint256[] memory testAmounts = new uint256[](3);
testAmounts[0] = 1000;
testAmounts[1] = 2000;
testAmounts[2] = 3000;
for (uint256 i = 0; i < testAmounts.length; i++) { // Configure borrower to repay more than required
uint256 amount = testAmounts[i]; borrower.setAction(FlashBorrower.Action.REPAY_EXTRA, alice);
uint256 fee = viewer.flashFee(pool, address(token0), amount);
// Calculate expected fee // Create loan request
uint256 expectedFee = (amount * pool.flashFeePpm() + 1_000_000 - 1) / 1_000_000; // ceiling uint256[] memory amounts = new uint256[](3);
amounts[0] = 1000;
assertEq( // Record balances before flash
fee, uint256 aliceToken0Before = token0.balanceOf(alice);
expectedFee, uint256 poolToken0Before = token0.balanceOf(address(pool));
"Flash fee calculation mismatch"
); // Execute flash loan
borrower.flash(amounts);
// Check balances - net change for alice should equal fee + extra donation (principal returned)
uint256 fee = (amounts[0] * pool.flashFeePpm() + 1_000_000 - 1) / 1_000_000; // ceil fee calculation
uint256 extra = 1; // borrower donates +1 per token in REPAY_EXTRA
uint256 expectedAliceDecrease = fee + extra; // fee plus donation
assertEq(
aliceToken0Before - token0.balanceOf(alice),
expectedAliceDecrease,
"Alice should pay fee + extra"
);
assertEq(
token0.balanceOf(address(pool)),
poolToken0Before + fee + extra,
"Pool should receive fee + extra"
);
}
/// @notice Test flashRepaymentAmounts matches flash implementation
function testFlashRepaymentAmounts() public view {
// Create different loan amount scenarios
uint256[][] memory testCases = new uint256[][](3);
// Case 1: Single token
testCases[0] = new uint256[](3);
testCases[0][0] = 1000;
testCases[0][1] = 0;
testCases[0][2] = 0;
// Case 2: Multiple tokens
testCases[1] = new uint256[](3);
testCases[1][0] = 1000;
testCases[1][1] = 2000;
testCases[1][2] = 3000;
// Case 3: Mix of zero and non-zero
testCases[2] = new uint256[](3);
testCases[2][0] = 0;
testCases[2][1] = 2000;
testCases[2][2] = 0;
for (uint256 i = 0; i < testCases.length; i++) {
uint256[] memory loanAmounts = testCases[i];
uint256[] memory repaymentAmounts = pool.flashRepaymentAmounts(loanAmounts);
// Verify each repayment amount is correctly calculated
for (uint256 j = 0; j < loanAmounts.length; j++) {
if (loanAmounts[j] == 0) {
// Zero loans should have zero repayment
assertEq(repaymentAmounts[j], 0, "Zero loan should have zero repayment");
} else {
// Calculate expected repayment with fee
uint256 fee = (loanAmounts[j] * pool.flashFeePpm() + 1_000_000 - 1) / 1_000_000; // ceiling
uint256 expectedRepayment = loanAmounts[j] + fee;
assertEq(
repaymentAmounts[j],
expectedRepayment,
"Repayment calculation mismatch"
);
}
}
} }
} }
/// @notice Test flash with invalid recipient
function testFlashWithZeroRecipientReverts() public {
FlashBorrower borrower = setupFlashBorrower();
// Configure borrower with zero recipient
borrower.setAction(FlashBorrower.Action.NORMAL, address(0));
// Create loan request
uint256[] memory amounts = new uint256[](3);
amounts[0] = 1000;
// Execute flash loan - should revert due to zero recipient
vm.expectRevert(bytes("flash: zero recipient"));
borrower.flash(amounts);
}
/// @notice Test flash with incorrect amounts length
function testFlashWithIncorrectLengthReverts() public {
// Call flash directly with incorrect length
uint256[] memory wrongLengthAmounts = new uint256[](2); // Pool has 3 tokens
wrongLengthAmounts[0] = 1000;
wrongLengthAmounts[1] = 2000;
vm.expectRevert(bytes("flash: amounts length mismatch"));
pool.flash(alice, wrongLengthAmounts, "");
}
/// @notice Test that passing nonzero lpTokens to initialMint doesn't affect swap results /// @notice Test that passing nonzero lpTokens to initialMint doesn't affect swap results
/// compared to pools initialized with default lpTokens (0) /// compared to pools initialized with default lpTokens (0)
@@ -1090,7 +1330,7 @@ contract PartyPoolTest is Test {
uint256 lpDefault = poolDefault.initialMint(address(this), 0); uint256 lpDefault = poolDefault.initialMint(address(this), 0);
uint256 scaleFactor = 3; uint256 scaleFactor = 3;
uint256 customLpAmount = lpDefault * scaleFactor; uint256 customLpAmount = lpDefault * scaleFactor;
poolCustom.initialMint(address(this), customLpAmount); uint256 lpCustom = poolCustom.initialMint(address(this), customLpAmount);
// Verify initial LP supplies // Verify initial LP supplies
assertEq(poolDefault.totalSupply(), lpDefault, "Default pool should have default LP supply"); assertEq(poolDefault.totalSupply(), lpDefault, "Default pool should have default LP supply");
@@ -1117,8 +1357,8 @@ contract PartyPoolTest is Test {
token2.approve(address(poolCustom), type(uint256).max); token2.approve(address(poolCustom), type(uint256).max);
// Get required deposit amounts for both pools // Get required deposit amounts for both pools
uint256[] memory depositsDefault = viewer.mintAmounts(poolDefault, lpRequestDefault); uint256[] memory depositsDefault = poolDefault.mintDepositAmounts(lpRequestDefault);
uint256[] memory depositsCustom = viewer.mintAmounts(poolCustom, lpRequestCustom); uint256[] memory depositsCustom = poolCustom.mintDepositAmounts(lpRequestCustom);
// Deposits should be identical (same proportion of identical balances) // Deposits should be identical (same proportion of identical balances)
assertEq(depositsDefault[0], depositsCustom[0], "Token0 deposits should be identical"); assertEq(depositsDefault[0], depositsCustom[0], "Token0 deposits should be identical");
@@ -1146,4 +1386,3 @@ contract PartyPoolTest is Test {
} }
} }
/* solhint-enable */