Compare commits

25 Commits

Author SHA1 Message Date
tim
923d9b93e7 removed tax coin support 2025-10-07 17:09:24 -04:00
tim
12957aaa51 flashLoan gas test 2025-10-07 15:57:04 -04:00
tim
457385e692 flashLoan protocol fee 2025-10-07 15:46:51 -04:00
tim
2d45296406 DeployMock creates viewer 2025-10-07 15:02:43 -04:00
tim
104c279123 deployable sizes 2025-10-07 14:50:36 -04:00
tim
98e80ab6ca delegated reentrancy lock 2025-10-07 14:37:44 -04:00
tim
ef039aa57e flashLoan rewritten as ERC-3156 2025-10-07 14:12:27 -04:00
tim
677ce4886c PartyPoolView 2025-10-07 12:36:24 -04:00
tim
20af14c872 proxied swapToLimit 2025-10-06 17:38:47 -04:00
tim
63f6e66d08 PartyPoolBalancedPair as subclass 2025-10-06 16:09:34 -04:00
tim
0049d27c90 removed console logs 2025-10-03 13:50:41 -04:00
tim
b126c52c7c swapMintImpl moved into mintImpl 2025-10-03 13:42:28 -04:00
tim
2e675bceb9 ERC functionality split into internal/external 2025-10-03 12:48:45 -04:00
tim
269236cfba whitepaper rough draft 2025-10-03 12:14:13 -04:00
tim
c002d26daf protocol fees 2025-10-02 16:43:02 -04:00
tim
b7e1b1cac2 LMSRStabilized pure refactor; swapMintAmounts 2025-10-01 17:09:42 -04:00
tim
5a2e7039d1 LMSRStabilized pure refactor; swapMintAmounts 2025-10-01 17:08:02 -04:00
tim
a6f6fd034c bugfix 2025-10-01 15:22:04 -04:00
tim
d46e60f83c bugfix 2025-10-01 15:20:02 -04:00
tim
3a5f0842b3 burnAmounts 2025-10-01 15:17:48 -04:00
tim
40e1d25e72 mintAmounts 2025-10-01 15:02:11 -04:00
tim
5ce14ab2e1 linted 2025-09-30 15:42:52 -04:00
tim
91e6a916ac burnSwap fees 2025-09-29 16:10:30 -04:00
tim
28b9474363 PartyPoolMintImpl 2025-09-26 11:48:01 -04:00
tim
9cac58013b refactor PartyPoolSwapMintImpl 2025-09-25 21:46:59 -04:00
26 changed files with 3018 additions and 1591 deletions

View File

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

551
doc/whitepaper.md Normal file
View File

@@ -0,0 +1,551 @@
# 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.

100
doc/whitepaper2.md Normal file
View File

@@ -0,0 +1,100 @@
# LMSR-based Multi-Asset AMM
Abstract
We present a multi-asset automated market maker whose pricing kernel is the Logarithmic Market Scoring Rule (LMSR). The pool maintains the convex potential $C(\mathbf{q}) = b(\mathbf{q}) \log\!\Big(\sum_i e^{q_i / b(\mathbf{q})}\Big)$ over normalized inventories $\mathbf{q}$, and sets the effective liquidity parameter proportional to pool size as $b(\mathbf{q}) = \kappa \, S(\mathbf{q})$ with $S(\mathbf{q}) = \sum_i q_i$ and fixed $\kappa>0$. This proportional parameterization preserves scale-invariant responsiveness while retaining softmax-derived pairwise price ratios under a quasi-static-$b$ view, enabling any-to-any swaps within a single potential. We derive and use closed-form expressions for two-asset reductions to compute exact-in, exact-out, limit-hitting (swap-to-limit), and capped-output trades. We discuss stability techniques such as log-sum-exp, ratio-once shortcuts, and domain guards for fixed-point arithmetic. Liquidity operations (proportional and single-asset joins/exits) follow directly from the same potential and admit monotone, invertible mappings. Parameters are immutable post-deployment for transparency and predictable depth calibration.
Introduction and Motivation
Multi-asset liquidity typically trades off simplicity and expressivity. Classical CFMMs define multiplicative invariants over reserves, while LMSR specifies a convex cost function whose gradient yields prices. Our goal is a multi-asset AMM that uses LMSR to support any-to-any swaps, shares risk across many assets, and scales depth predictably with pool size. By setting $b(\mathbf{q})=\kappa S(\mathbf{q})$, we achieve scale invariance: proportional rescaling of all balances scales $b$ proportionally and preserves pairwise price ratios, so the markets responsiveness is consistent across liquidity regimes. The derivations below formulate instantaneous prices, closed-form swap mappings, limit logic, and liquidity operations tailored to this parameterization.
System Model and Pricing Kernel
We consider $n\ge 2$ normalized assets with state vector $\mathbf{q}=(q_0,\dots,q_{n-1})\in\mathbb{R}_{\ge 0}^{\,n}$ and size metric $S(\mathbf{q})=\sum_i q_i$. The kernel is the LMSR cost function
$$
C(\mathbf{q}) = b(\mathbf{q}) \log\!\left(\sum_{i=0}^{n-1} e^{q_i / b(\mathbf{q})}\right), \qquad b(\mathbf{q})=\kappa\,S(\mathbf{q}),\quad \kappa>0.
$$
For numerical stability we evaluate $C$ with a log-sum-exp recentering. Let $y_i := q_i/b(\mathbf{q})$ and $M:=\max_i y_i$. Then
$$
C(\mathbf{q}) \;=\; b(\mathbf{q}) \left( M + \log \sum_{i=0}^{n-1} e^{\,y_i - M} \right),
$$
which prevents overflow/underflow when the $y_i$ are dispersed. Quantities are represented in fixed-point with explicit range and domain guards; equations are presented over the reals for clarity.
Gradient, Price Shares, and Pairwise Prices
With $b$ treated as a constant parameter, the LMSR gradient recovers softmax shares
$$
\frac{\partial C}{\partial q_i} \;=\; \frac{e^{q_i/b}}{\sum_k e^{q_k/b}} \;=:\; \pi_i(\mathbf{q}),
$$
so that the ratio of marginal prices is $\pi_j/\pi_i = \exp\!\big((q_j-q_i)/b\big)$. When $b(\mathbf{q})=\kappa S(\mathbf{q})$ depends on state, $\frac{\partial C}{\partial q_i}$ acquires a common additive term across $i$ from $\partial b/\partial q_i$, but pairwise ratios remain governed by softmax differences. We therefore use a quasi-static-$b$ view for pricing steps, holding $b$ fixed at the pre-trade state for the infinitesimal move, and define the instantaneous pairwise marginal price ratio for exchanging $i$ into $j$ as
$$
P(i\to j \mid \mathbf{q}) \;=\; \exp\!\left(\frac{q_j - q_i}{b(\mathbf{q})}\right).
$$
This ratio drives swap computations and is invariant to proportional rescaling $\mathbf{q}\mapsto \lambda\mathbf{q}$ because $b$ scales by the same factor.
Two-Asset Reduction and Exact Swap Mappings
Swaps are computed in the two-asset subspace spanned by the in-asset $i$ and out-asset $j$, with all other coordinates held fixed under a quasi-static-$b$ step. Let
$$
r_0 \;:=\; \exp\!\left(\frac{q_i - q_j}{b}\right), \qquad b \equiv b(\mathbf{q})\;\text{ held quasi-static}.
$$
Along the $i\!\to\! j$ path, the instantaneous ratio evolves multiplicatively as $r(t)=r_0\,e^{t/b}$ where $t$ denotes cumulative input of asset $i$. In the two-asset reduction the infinitesimal output satisfies
$$
\mathrm{d}y \;=\; \frac{r(t)}{1+r(t)}\,\mathrm{d}t.
$$
Integrating from $t=0$ to $t=a$ yields the exact-in closed form
$$
y(a) \;=\; b \,\ln\!\Big( 1 + r_0 \,\big(1 - e^{-a/b}\big) \Big).
$$
This mapping has $y(0)=0$, is strictly increasing and concave in $a$, and satisfies $y'(0)=\frac{r_0}{1+r_0}$ with asymptote $\lim_{a\to\infty} y = b\,\ln(1+r_0)$. The inverse exact-out mapping follows by solving for $a$ in terms of target $y$. Writing $E:=e^{y/b}$, we obtain
$$
a(y) \;=\; b \,\ln\!\left(\frac{r_0}{\,r_0 + 1 - E\,}\right),
$$
which is strictly increasing and convex for $y\in\big[0,\, b\ln(1+r_0)\big]$. These two expressions are the workhorses for exact-in and exact-out swaps in our kernel.
Price Limits, Swap-to-Limit, and Capacity Caps
Users may provide a maximum acceptable marginal price ratio $\Lambda>0$ for $p_i/p_j$. The marginal ratio trajectory $r(t)=r_0 e^{t/b}$ first reaches the limit at the unique
$$
a_{\text{lim}} \;=\; b \,\ln\!\left(\frac{\Lambda}{r_0}\right),
$$
and the output realized at that truncation is
$$
y_{\text{lim}} \;=\; b \,\ln\!\Big( 1 + r_0 \,\big(1 - r_0/\Lambda\big) \Big).
$$
Outputs are further bounded by available inventory; if a computed $y$ would exceed $q_j$, we cap at $y=q_j$ and compute the implied input by inverting the exact-out formula,
$$
a_{\text{cap}} \;=\; b \,\ln\!\left(\frac{r_0}{\,r_0 + 1 - e^{\,q_j/b}\,}\right).
$$
These limit and capacity branches ensure monotone, conservative behavior near domain edges.
Liquidity Operations from the Same Potential
Liquidity is accounted via pool shares $L$ taken proportional to the size metric, and we set $L=S(\mathbf{q})$ without loss of generality. At initialization with seed balances $\mathbf{q}^{(0)}$ the pool sets $L^{(0)}=S^{(0)}$ and $b^{(0)}=\kappa S^{(0)}$. A proportional deposit that scales balances to $\mathbf{q}'=(1+\alpha)\mathbf{q}$ mints $\Delta L = \alpha S(\mathbf{q})$ shares and scales liquidity to $b'=(1+\alpha)b$. Single-asset deposits target a proportional growth while rebalancing through kernel swaps: providing amount $a$ of asset $i$ induces a growth factor $\alpha\ge 0$ satisfying the monotone equation
$$
a \;=\; a_{\text{req}}(\alpha) \;=\; \alpha q_i \;+\; \sum_{j\ne i} b \,\ln\!\left(\frac{r_{0,j}}{\,r_{0,j} + 1 - e^{\,\alpha q_j/b}\,}\right), \quad r_{0,j}:=\exp\!\left(\frac{q_i-q_j}{b}\right),
$$
and mints $\Delta L=\alpha S(\mathbf{q})$ upon the unique solution. Proportional withdrawals burn $\Delta L$ and return $\alpha=\Delta L/S(\mathbf{q})$ of each asset, updating $b$ to $(1-\alpha)b$. Single-asset withdrawals redeem $\alpha q_i$ directly and swap each redeemed $\alpha q_j$ for $j\ne i$ into $i$ using the exact-in mapping evaluated on the local post-burn state; any capacity overrun is handled by a cap-and-invert branch as above. Because all operations reduce to the same two-asset closed forms, they inherit monotonicity and uniqueness.
Fees and Economic Considerations
Swap fees are applied outside the fee-free kernel. For an exact-in submission $a$ on asset $i$, the effective kernel input is $a_{\text{eff}}=(1-f_{\text{swap}})a$. The kernel computes output using $a_{\text{eff}}$, while the retained fee remains in the pool, increasing $S(\mathbf{q})$ relative to outstanding $L$ and thereby accruing value to LPs implicitly. Scale invariance under $b=\kappa S$ implies that proportional growth of inventories preserves price ratios while deepening notional liquidity linearly. The classical LMSR bounded-loss intuition for constant $b$ gives $b\ln n$ in appropriate units; under our proportional $b$, this scales with $S$, so the instantaneous bound per unit of $S$ is proportional to $\kappa\ln n$.
Balanced Regime, Approximations, and Stability
Near balance it is useful to parameterize $\delta := (q_i - q_j)/b$ and $\tau := a/b$. The exact mapping
$$
y(a) \;=\; b \,\ln\!\Big(1 + e^{\delta}\,\big(1 - e^{-\tau}\big)\Big)
$$
admits small-argument expansions when $|\delta|\ll 1$ and $|\tau|\ll 1$. Using $e^{\pm x}\approx 1\pm x+\tfrac{x^2}{2}$ and $\ln(1+u)\approx u - \tfrac{u^2}{2}$, we obtain
$$
y(a) \;\approx\; b \left[ r_0 \tau - \frac{1}{2} r_0 \tau^2 \right] + \mathcal{O}\!\left(\tau^3,\, |\delta|\,\tau^2\right), \qquad r_0=e^{\delta}\approx 1+\delta+\tfrac{\delta^2}{2},
$$
and at $\delta=0$ the symmetry reduces to $y(a)\approx \tfrac{a}{2} - \tfrac{a^2}{4b} + \cdots$. In designated near-balance domains one may replace $\exp$ and $\ln$ with verified polynomial approximations to reduce computational cost while maintaining monotonicity and bounded error; a dispatcher enforces preconditions on $|\delta|$, $|a|/b$, and positivity of inner arguments, otherwise falling back to the exact forms. Regardless of path, our numerical policy prioritizes monotonicity, domain safety, and conservative branches at decision boundaries.
Numerical Methods and Safety Guarantees
We evaluate log-sum-exp with recentring, compute ratios like $r_0=\exp((q_i-q_j)/b)$ directly rather than dividing exponentials, and guard all $\exp$ and $\ln$ calls to bounded domains with explicit checks on positivity of inner terms such as $r_0+1-e^{y/b}$. Fixed-point implementations precompute reciprocals like $1/b$ to reduce dispersion, clamp to capacity before inversion, and select cap-and-invert rather than extrapolating when inner terms approach zero. These measures ensure the swap maps remain strictly order-preserving and free of nonphysical outputs. Property-based and differential testing can confirm monotonicity of $y(a)$ and $a(y)$, uniqueness of limit hits when $\Lambda>r_0$, and adherence to predefined error budgets.
Deployment and Parameter Fixity
The parameter tuple $(\kappa, f_{\text{swap}}, \phi)$ is set at deployment and remains immutable, with $\kappa>0$ defining $b(\mathbf{q})=\kappa S(\mathbf{q})$, $f_{\text{swap}}$ the swap fee rate, and $\phi$ the protocol share of fees. Given the initial state $\mathbf{q}^{(0)}$ with $S^{(0)}>0$, the induced pricing map is fully determined by
$$
C(\mathbf{q}) = b(\mathbf{q}) \log\!\left(\sum_i e^{q_i / b(\mathbf{q})}\right), \qquad b(\mathbf{q})=\kappa S(\mathbf{q}),
$$
and the two-asset closed forms above. Fixity eliminates governance risk, makes depth calibration transparent, and simplifies integration for external routers and valuation tools.
Conclusion
By coupling LMSR with the proportional parameterization $b(\mathbf{q})=\kappa S(\mathbf{q})$, we obtain a multi-asset AMM that preserves softmax-driven price ratios under a quasi-static-$b$ view and supports any-to-any swaps via a single convex potential. Exact two-asset reductions yield closed-form mappings for exact-in, exact-out, limit-hitting, and capped-output trades, and the same formulas underpin liquidity operations with monotonicity and uniqueness. Numerical stability follows from log-sum-exp evaluation, ratio-first derivations, guarded transcendental domains, and optional near-balance approximations, while fixed parameters provide predictable scaling and transparent economics.
References
Hanson, R. (2002). Logarithmic Market Scoring Rules for Modular Combinatorial Information Aggregation. https://mason.gmu.edu/~rhanson/mktscore.pdf

View File

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

View File

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

73
src/Deploy.sol Normal file
View File

@@ -0,0 +1,73 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30;
import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {PartyPlanner} from "./PartyPlanner.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 {
function newPartyPlanner() internal returns (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(
string memory name_,
string memory symbol_,
IERC20[] memory tokens_,
uint256[] memory bases_,
int128 _kappa,
uint256 _swapFeePpm,
uint256 _flashFeePpm,
bool _stable
) internal returns (PartyPool) {
// default protocol fee/off parameters (per your instruction) - set to 0 / address(0)
uint256 protocolFeePpm = 0;
address protocolAddr = address(0);
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());
}
}

123
src/ERC20External.sol Normal file
View File

@@ -0,0 +1,123 @@
// 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;
}
}

175
src/ERC20Internal.sol Normal file
View File

@@ -0,0 +1,175 @@
// 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,14 +1,16 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30;
import "./PartyPool.sol";
import "./IPartyPool.sol";
import "./PartyPoolMintImpl.sol";
import "./PartyPoolSwapImpl.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
/// @title IPartyPlanner
/// @notice Interface for factory contract for creating and tracking PartyPool instances
interface IPartyPlanner {
// Event emitted when a new pool is created
event PartyStarted(PartyPool indexed pool, string name, string symbol, IERC20[] tokens);
event PartyStarted(IPartyPool indexed pool, string name, string symbol, IERC20[] tokens);
/// @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.
@@ -27,7 +29,7 @@ interface IPartyPlanner {
/// @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 lpAmount Amount of LP tokens minted to the receiver
function createPool(
function newPool(
// Pool constructor args (legacy)
string memory name_,
string memory symbol_,
@@ -44,7 +46,7 @@ interface IPartyPlanner {
uint256[] memory initialDeposits,
uint256 initialLpAmount,
uint256 deadline
) external returns (PartyPool pool, uint256 lpAmount);
) external returns (IPartyPool pool, uint256 lpAmount);
/// @notice Creates a new PartyPool instance and initializes it with initial deposits (kappa-based).
/// @param name_ LP token name
@@ -61,7 +63,7 @@ interface IPartyPlanner {
/// @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 lpAmount Amount of LP tokens minted to the receiver
function createPool(
function newPool(
// Pool constructor args (kappa-based)
string memory name_,
string memory symbol_,
@@ -77,7 +79,7 @@ interface IPartyPlanner {
uint256[] memory initialDeposits,
uint256 initialLpAmount,
uint256 deadline
) external returns (PartyPool pool, uint256 lpAmount);
) external returns (IPartyPool pool, uint256 lpAmount);
/// @notice Checks if a pool is supported
/// @param pool The pool address to check
@@ -92,7 +94,7 @@ interface IPartyPlanner {
/// @param offset Starting index for pagination
/// @param limit Maximum number of items to return
/// @return pools Array of pool addresses for the requested page
function getAllPools(uint256 offset, uint256 limit) external view returns (PartyPool[] memory pools);
function getAllPools(uint256 offset, uint256 limit) external view returns (IPartyPool[] memory pools);
/// @notice Returns the total number of unique tokens
/// @return The total count of unique tokens
@@ -114,5 +116,12 @@ interface IPartyPlanner {
/// @param offset Starting index for pagination
/// @param limit Maximum number of items to return
/// @return pools Array of pool addresses containing the specified token
function getPoolsByToken(IERC20 token, uint256 offset, uint256 limit) external view returns (PartyPool[] memory pools);
function getPoolsByToken(IERC20 token, uint256 offset, uint256 limit) external view returns (IPartyPool[] 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,7 +1,10 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30;
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import "../lib/openzeppelin-contracts/contracts/interfaces/IERC3156FlashBorrower.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
/// @notice A multi-asset liquidity pool backed by the LMSRStabilized pricing model.
@@ -55,10 +58,11 @@ interface IPartyPool is IERC20Metadata {
);
// Immutable pool configuration (public getters)
function LMSR() external view returns (LMSRStabilized.State memory);
/// @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.
function tokens(uint256) external view returns (IERC20); // get single token
function getToken(uint256) external view returns (IERC20); // get single token
/// @notice Returns the number of tokens (n) in the pool.
function numTokens() external view returns (uint256);
@@ -76,23 +80,32 @@ interface IPartyPool is IERC20Metadata {
/// @notice Flash-loan fee in parts-per-million (ppm) applied to flash borrow amounts.
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)
/// @dev Pools are constructed with a κ value; this getter exposes the κ used by the pool.
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)
/// @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 mintDepositAmounts(uint256 lpTokenAmount) external view returns (uint256[] memory depositAmounts);
/// @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 returns (uint256 lpMinted);
/// @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.
@@ -105,13 +118,6 @@ interface IPartyPool is IERC20Metadata {
/// @return lpMinted the actual amount of lpToken minted
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.
/// @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.
@@ -119,7 +125,7 @@ interface IPartyPool is IERC20Metadata {
/// @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;
function burn(address payer, address receiver, uint256 lpAmount, uint256 deadline) external returns (uint256[] memory withdrawAmounts);
// Swaps
@@ -130,12 +136,14 @@ interface IPartyPool is IERC20Metadata {
/// @param maxAmountIn maximum gross input allowed (inclusive of fee)
/// @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
/*
function swapAmounts(
uint256 inputTokenIndex,
uint256 outputTokenIndex,
uint256 maxAmountIn,
int128 limitPrice
) external view returns (uint256 amountIn, uint256 amountOut, uint256 fee);
*/
/// @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.
@@ -158,17 +166,6 @@ interface IPartyPool is IERC20Metadata {
uint256 deadline
) 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.
/// @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.
@@ -221,39 +218,17 @@ interface IPartyPool is IERC20Metadata {
uint256 deadline
) external returns (uint256 amountOutUint);
/// @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(uint256 baseTokenIndex, uint256 quoteTokenIndex) external view returns (int128);
/// @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(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,
/**
* @dev Initiate a flash loan.
* @param receiver The receiver of the tokens in the loan, and the receiver of the callback.
* @param token The loan currency.
* @param amount The amount of tokens lent.
* @param data Arbitrary data structure, intended to contain user-defined parameters.
*/
function flashLoan(
IERC3156FlashBorrower receiver,
address token,
uint256 amount,
bytes calldata data
) external;
) external returns (bool);
}

View File

@@ -1,8 +1,7 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30;
import "forge-std/console2.sol";
import "@abdk/ABDKMath64x64.sol";
import {ABDKMath64x64} from "../lib/abdk-libraries-solidity/ABDKMath64x64.sol";
/// @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
@@ -41,21 +40,11 @@ library LMSRStabilized {
}
int128 total = _computeSizeMetric(s.qInternal);
console2.log("total (internal 64.64)");
console2.logInt(total);
require(total > int128(0), "LMSR: total zero");
console2.log("LMSR.init: start");
console2.log("nAssets", s.nAssets);
console2.log("qInternal.length", s.qInternal.length);
// Set kappa directly (caller provides kappa)
s.kappa = kappa;
console2.log("kappa (64x64)");
console2.logInt(s.kappa);
require(s.kappa > int128(0), "LMSR: kappa>0");
console2.log("LMSR.init: done");
}
/* --------------------
@@ -64,8 +53,15 @@ library LMSRStabilized {
/// @notice Cost C(q) = b * (M + ln(Z))
function cost(State storage s) internal view returns (int128) {
int128 b = _computeB(s);
(int128 M, int128 Z) = _computeMAndZ(b, s.qInternal);
return cost(s.kappa, 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 inner = M.add(lnZ);
int128 c = b.mul(inner);
@@ -102,72 +98,89 @@ library LMSRStabilized {
int128 a,
int128 limitPrice
) internal view returns (int128 amountIn, int128 amountOut) {
require(i < s.nAssets && j < s.nAssets, "LMSR: idx");
return swapAmountsForExactInput(s.nAssets, s.kappa, s.qInternal, i, j, a, limitPrice);
}
/// @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)
amountIn = a;
// Compute b and ensure positivity before deriving invB
int128 b = _computeB(s);
int128 sizeMetric = _computeSizeMetric(qInternal);
require(sizeMetric > int128(0), "LMSR: size metric zero");
int128 b = kappa.mul(sizeMetric);
require(b > int128(0), "LMSR: b<=0");
// Precompute reciprocal of b to avoid repeated divisions
int128 invB = ABDKMath64x64.div(ONE, b);
// Guard: output asset must have non-zero effective weight to avoid degenerate/div-by-zero-like conditions
require(s.qInternal[j] > int128(0), "LMSR: e_j==0");
require(qInternal[j] > int128(0), "LMSR: e_j==0");
// Compute r0 = exp((q_i - q_j) / b) directly using invB
int128 r0 = _exp(s.qInternal[i].sub(s.qInternal[j]).mul(invB));
int128 r0 = _exp(qInternal[i].sub(qInternal[j]).mul(invB));
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
// push the marginal price p_i/p_j beyond the limit; if so, truncate `a`.
// Marginal price ratio evolves as r(t) = r0 * exp(t/b) (since e_i multiplies by exp(t/b))
if (limitPrice > int128(0)) {
console2.log("\n=== LimitPrice Logic Debug ===");
console2.log("Received limitPrice (64x64):");
console2.logInt(limitPrice);
console2.log("Current price ratio r0 (e_i/e_j, 64x64):");
console2.logInt(r0);
// r0 must be positive; if r0 == 0 then no risk of exceeding limit by increasing r.
require(r0 >= int128(0), "LMSR: r0<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 {
// If limitPrice <= current price, we revert (caller must choose a limit > current price to allow any fill)
if (limitPrice <= r0) {
console2.log("Limit price is <= current price: reverting");
revert("LMSR: limitPrice <= current price");
}
// Compute a_limit directly from ln(limit / r0): a_limit = b * ln(limit / r0)
int128 ratioLimitOverR0 = limitPrice.div(r0);
console2.log("limitPrice/r0 (64x64):");
console2.logInt(ratioLimitOverR0);
require(ratioLimitOverR0 > int128(0), "LMSR: ratio<=0");
int128 aLimitOverB = _ln(ratioLimitOverR0); // > 0
console2.log("ln(limitPrice/r0) (64x64):");
console2.logInt(aLimitOverB);
// aLimit = b * aLimitOverB
int128 aLimit64 = b.mul(aLimitOverB);
console2.log("aLimit in 64x64 format:");
console2.logInt(aLimit64);
// If computed aLimit is less than the requested a, use the truncated value.
if (aLimit64 < a) {
console2.log("TRUNCATING: a reduced from 64.64 value");
console2.logInt(a);
console2.log("to 64.64 value");
console2.logInt(aLimit64);
amountIn = aLimit64; // Store the truncated input amount
a = aLimit64; // Use truncated amount for calculations
} else {
console2.log("Not truncating: aLimit64 >= a");
// console2.log("Not truncating: aLimit64 >= a");
}
}
}
@@ -177,56 +190,24 @@ library LMSRStabilized {
// Protect exp from enormous inputs (consistent with recenter thresholds)
require(aOverB <= EXP_LIMIT, "LMSR: a/b too large (would overflow exp)");
console2.log("\n=== AmountOut Calculation Debug ===");
console2.log("Input amount (64.64):");
console2.logInt(a);
console2.log("a/b (64x64):");
console2.logInt(aOverB);
// Use the closed-form fee-free formula:
// y = b * ln(1 + r0 * (1 - exp(-a/b)))
console2.log("r0_for_calc (e_i/e_j):");
console2.logInt(r0);
int128 expNeg = _exp(aOverB.neg()); // exp(-a/b)
console2.log("exp(-a/b):");
console2.logInt(expNeg);
int128 oneMinusExpNeg = ONE.sub(expNeg);
console2.log("1 - exp(-a/b):");
console2.logInt(oneMinusExpNeg);
int128 inner = ONE.add(r0.mul(oneMinusExpNeg));
console2.log("inner = 1 + r0 * (1 - exp(-a/b)):");
console2.logInt(inner);
// If inner <= 0 then cap output to the current balance q_j (cannot withdraw more than q_j)
if (inner <= int128(0)) {
console2.log("WARNING: inner <= 0, capping output to balance q_j");
int128 qj64 = s.qInternal[j];
console2.log("Capped output (64.64):");
console2.logInt(qj64);
int128 qj64 = qInternal[j];
return (amountIn, qj64);
}
int128 lnInner = _ln(inner);
console2.log("ln(inner):");
console2.logInt(lnInner);
int128 b_lnInner = b.mul(lnInner);
console2.log("b*ln(inner):");
console2.logInt(b_lnInner);
amountOut = b_lnInner;
console2.log("amountOut = b*ln(inner):");
console2.logInt(amountOut);
console2.log("amountOut (final 64.64 amount):");
console2.logInt(amountOut);
// Safety check
if (amountOut <= 0) {
console2.log("WARNING: x64 <= 0, returning 0");
return (0, 0);
}
}
@@ -249,27 +230,48 @@ library LMSRStabilized {
uint256 j,
int128 limitPrice
) internal view returns (int128 amountIn, int128 amountOut) {
require(i < s.nAssets && j < s.nAssets, "LMSR: idx");
return swapAmountsForPriceLimit(s.nAssets, s.kappa, s.qInternal, i, j, limitPrice);
}
/// @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");
// Compute b and ensure positivity before deriving invB
int128 b = _computeB(s);
int128 sizeMetric = _computeSizeMetric(qInternal);
require(sizeMetric > int128(0), "LMSR: size metric zero");
int128 b = kappa.mul(sizeMetric);
require(b > int128(0), "LMSR: b<=0");
// Precompute reciprocal of b to avoid repeated divisions
int128 invB = ABDKMath64x64.div(ONE, b);
// Guard: output asset must have non-zero effective weight to avoid degenerate/div-by-zero-like conditions
require(s.qInternal[j] > int128(0), "LMSR: e_j==0");
require(qInternal[j] > int128(0), "LMSR: e_j==0");
// Compute r0 = exp((q_i - q_j) / b) directly using invB
int128 r0 = _exp(s.qInternal[i].sub(s.qInternal[j]).mul(invB));
console2.log("\n=== Max Input/Output Calculation ===");
console2.log("Limit price (64x64):");
console2.logInt(limitPrice);
console2.log("Current price ratio r0 (e_i/e_j, 64x64):");
console2.logInt(r0);
int128 r0 = _exp(qInternal[i].sub(qInternal[j]).mul(invB));
// Mirror swapAmountsForExactInput behavior: treat invalid r0 as an error condition.
// Revert if r0 is non-positive (no finite trade under a price limit).
@@ -277,48 +279,28 @@ library LMSRStabilized {
// If current price already exceeds or equals limit, revert the same way swapAmountsForExactInput does.
if (r0 >= limitPrice) {
console2.log("Limit price is <= current price: reverting");
revert("LMSR: limitPrice <= current price");
}
// Calculate the price change factor: limitPrice/r0
int128 priceChangeFactor = limitPrice.div(r0);
console2.log("Price change factor (limitPrice/r0):");
console2.logInt(priceChangeFactor);
// ln(priceChangeFactor) gives us the maximum allowed delta in the exponent
int128 maxDeltaExponent = _ln(priceChangeFactor);
console2.log("Max delta exponent ln(priceChangeFactor):");
console2.logInt(maxDeltaExponent);
// Maximum input capable of reaching the price limit:
// x_max = b * ln(limitPrice / r0)
int128 amountInMax = b.mul(maxDeltaExponent);
console2.log("Max input to reach limit (64.64):");
console2.logInt(amountInMax);
// The maximum output y corresponding to that input:
// y = b * ln(1 + (e_i/e_j) * (1 - exp(-x_max/b)))
int128 expTerm = ONE.sub(_exp(maxDeltaExponent.neg()));
console2.log("1 - exp(-maxDeltaExponent):");
console2.logInt(expTerm);
int128 innerTerm = r0.mul(expTerm);
console2.log("e_i/e_j * expTerm:");
console2.logInt(innerTerm);
int128 lnTerm = _ln(ONE.add(innerTerm));
console2.log("ln(1 + innerTerm):");
console2.logInt(lnTerm);
int128 maxOutput = b.mul(lnTerm);
console2.log("Max output (b * lnTerm):");
console2.logInt(maxOutput);
// Current balance of asset j (in 64.64)
int128 qj64 = s.qInternal[j];
console2.log("Current j balance (64.64):");
console2.logInt(qj64);
int128 qj64 = qInternal[j];
// Initialize outputs to the computed maxima
amountIn = amountInMax;
@@ -326,7 +308,6 @@ library LMSRStabilized {
// If the calculated maximum output exceeds the balance, cap output and solve for input.
if (maxOutput > qj64) {
console2.log("Max output exceeds balance, capping to balance");
amountOut = qj64;
// Solve inverse relation for input given capped output:
@@ -336,19 +317,12 @@ library LMSRStabilized {
// a = -b * ln( (r0 + 1 - E) / r0 ) = b * ln( r0 / (r0 + 1 - E) )
int128 E = _exp(amountOut.mul(invB)); // exp(y/b)
int128 rhs = r0.add(ONE).sub(E); // r0 + 1 - E
console2.log("E = exp(y/b):");
console2.logInt(E);
console2.log("rhs = r0 + 1 - E:");
console2.logInt(rhs);
// If rhs <= 0 due to numerical issues, fall back to amountInMax
if (rhs <= int128(0)) {
console2.log("Numerical issue solving inverse; using amountInMax as fallback");
amountIn = amountInMax;
} else {
amountIn = b.mul(_ln(r0.div(rhs)));
console2.log("Computed input required for capped output (64.64):");
console2.logInt(amountIn);
}
}
@@ -366,20 +340,45 @@ library LMSRStabilized {
uint256 i,
int128 a
) internal view returns (int128 amountIn, int128 amountOut) {
require(i < s.nAssets, "LMSR: idx");
return swapAmountsForMint(s.nAssets, s.kappa, s.qInternal, i, a);
}
/// @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");
int128 b = _computeB(s);
int128 sizeMetric = _computeSizeMetric(qInternal);
require(sizeMetric > int128(0), "LMSR: size metric zero");
int128 b = kappa.mul(sizeMetric);
require(b > int128(0), "LMSR: b<=0");
int128 invB = ABDKMath64x64.div(ONE, b);
int128 S = _computeSizeMetric(s.qInternal);
int128 S = sizeMetric;
uint256 n = s.nAssets;
uint256 n = nAssets;
// Precompute r0_j = exp((q_i - q_j) / b) for all j to avoid recomputing during search.
int128[] memory r0 = new int128[](n);
for (uint256 j = 0; j < n; ) {
r0[j] = _exp(s.qInternal[i].sub(s.qInternal[j]).mul(invB));
r0[j] = _exp(qInternal[i].sub(qInternal[j]).mul(invB));
unchecked { j++; }
}
@@ -415,7 +414,7 @@ library LMSRStabilized {
// loop j != i
for (uint256 j = 0; j < n; ) {
if (j != i) {
int128 yj = alpha.mul(s.qInternal[j]); // target output y_j = alpha * q_j
int128 yj = alpha.mul(qInternal[j]); // target output y_j = alpha * q_j
if (yj > int128(0)) {
int128 expArg = yj.mul(invB);
// Guard exp arg
@@ -433,7 +432,7 @@ library LMSRStabilized {
unchecked { j++; }
}
int128 aReq = fail ? int128(type(int128).max) : alpha.mul(s.qInternal[i]).add(sumX);
int128 aReq = fail ? int128(type(int128).max) : alpha.mul(qInternal[i]).add(sumX);
if (aReq >= a || high >= alphaCap) {
break;
@@ -455,7 +454,7 @@ library LMSRStabilized {
for (uint256 j = 0; j < n; ) {
if (j != i) {
int128 yj = alpha.mul(s.qInternal[j]);
int128 yj = alpha.mul(qInternal[j]);
if (yj > int128(0)) {
int128 expArg = yj.mul(invB);
if (expArg > EXP_LIMIT) { fail = true; break; }
@@ -472,7 +471,7 @@ library LMSRStabilized {
unchecked { j++; }
}
int128 aReq = fail ? int128(type(int128).max) : alpha.mul(s.qInternal[i]).add(sumX);
int128 aReq = fail ? int128(type(int128).max) : alpha.mul(qInternal[i]).add(sumX);
if (aReq > a) {
// mid requires more input than provided -> decrease alpha
@@ -502,7 +501,7 @@ library LMSRStabilized {
bool failFinal = false;
for (uint256 j = 0; j < n; ) {
if (j != i) {
int128 yj = alphaFinal.mul(s.qInternal[j]);
int128 yj = alphaFinal.mul(qInternal[j]);
if (yj > int128(0)) {
int128 expArg = yj.mul(invB);
if (expArg > EXP_LIMIT) { failFinal = true; break; }
@@ -524,7 +523,7 @@ library LMSRStabilized {
return (int128(0), int128(0));
}
int128 aRequired = alphaFinal.mul(s.qInternal[i]).add(sumXFinal);
int128 aRequired = alphaFinal.mul(qInternal[i]).add(sumXFinal);
// amountIn is actual consumed input (may be <= provided a)
amountIn = aRequired;
@@ -553,28 +552,56 @@ library LMSRStabilized {
uint256 i,
int128 alpha
) internal view returns (int128 amountOut, int128 amountIn) {
require(i < s.nAssets, "LMSR: idx");
return swapAmountsForBurn(s.nAssets, s.kappa, s.qInternal, i, alpha);
}
/// @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");
int128 b = _computeB(s);
int128 sizeMetric = _computeSizeMetric(qInternal);
require(sizeMetric > int128(0), "LMSR: size metric zero");
int128 b = kappa.mul(sizeMetric);
require(b > int128(0), "LMSR: b<=0");
int128 invB = ABDKMath64x64.div(ONE, b);
uint256 n = s.nAssets;
uint256 n = nAssets;
// Size metric and burned size (amountIn returned)
int128 S = _computeSizeMetric(s.qInternal);
int128 S = sizeMetric;
amountIn = alpha.mul(S); // total size-metric redeemed
// Build q_local := q_after_burn = (1 - alpha) * q
int128[] memory qLocal = new int128[](n);
for (uint256 j = 0; j < n; ) {
qLocal[j] = s.qInternal[j].mul(ONE.sub(alpha));
qLocal[j] = qInternal[j].mul(ONE.sub(alpha));
unchecked { j++; }
}
// Start totalOut with direct portion of asset i redeemed
int128 totalOut = alpha.mul(s.qInternal[i]);
int128 totalOut = alpha.mul(qInternal[i]);
// Track whether any non-zero contribution was produced
bool anyNonZero = (totalOut > int128(0));
@@ -582,7 +609,7 @@ library LMSRStabilized {
// For each asset j != i, swap the withdrawn a_j := alpha * q_j into i
for (uint256 j = 0; j < n; ) {
if (j != i) {
int128 aj = alpha.mul(s.qInternal[j]); // wrapper-held withdrawn amount of j
int128 aj = alpha.mul(qInternal[j]); // wrapper-held withdrawn amount of j
if (aj > int128(0)) {
// expArg = aj / b
int128 expArg = aj.mul(invB);
@@ -674,19 +701,9 @@ library LMSRStabilized {
require(amountIn > int128(0), "LMSR: amountIn <= 0");
require(amountOut > int128(0), "LMSR: amountOut <= 0");
console2.log("\n=== Applying Swap ===");
console2.log("Input asset:", i);
console2.log("Output asset:", j);
console2.log("Amount in (64.64):");
console2.logInt(amountIn);
console2.log("Amount out (64.64):");
console2.logInt(amountOut);
// Update internal balances
s.qInternal[i] = s.qInternal[i].add(amountIn);
s.qInternal[j] = s.qInternal[j].sub(amountOut);
console2.log("=== Swap Applied (qInternal updated) ===\n");
}
@@ -697,40 +714,40 @@ library LMSRStabilized {
function updateForProportionalChange(State storage s, int128[] memory newQInternal) internal {
require(newQInternal.length == s.nAssets, "LMSR: length mismatch");
console2.log("LMSR.updateForProportionalChange: start");
// Compute new total for validation
int128 newTotal = _computeSizeMetric(newQInternal);
console2.log("new total");
console2.logInt(newTotal);
require(newTotal > int128(0), "LMSR: new total zero");
// With kappa formulation, b automatically scales with pool size
int128 newB = s.kappa.mul(newTotal);
console2.log("new effective b");
console2.logInt(newB);
// Update the cached qInternal with new values
for (uint i = 0; i < s.nAssets; ) {
s.qInternal[i] = newQInternal[i];
unchecked { i++; }
}
console2.log("LMSR.updateForProportionalChange: end");
}
/// @notice Price-share of asset i: exp(z_i) / Z (64.64)
function priceShare(State storage s, uint256 i) internal view returns (int128) {
int128 b = _computeB(s);
uint len = s.qInternal.length;
return priceShare(s.kappa, s.qInternal, i);
}
/// @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");
// Precompute reciprocal of b and perform a single pass that tracks M, Z, and e_i
int128 invB = ABDKMath64x64.div(ONE, b);
// Initialize from the first element
int128 M = s.qInternal[0].mul(invB);
int128 M = qInternal[0].mul(invB);
int128 Z = ONE; // exp(0)
int128 e_i_acc;
bool setEi;
@@ -741,7 +758,7 @@ library LMSRStabilized {
}
for (uint idx = 1; idx < len; ) {
int128 yi = s.qInternal[idx].mul(invB);
int128 yi = qInternal[idx].mul(invB);
if (yi <= M) {
// Add contribution under current center
int128 term = _exp(yi.sub(M));
@@ -770,7 +787,7 @@ library LMSRStabilized {
if (!setEi) {
// Only possible when len == 1 and i != 0, guarded by caller invariants typically
// Fallback: compute directly (kept for completeness)
int128 yi = s.qInternal[i].mul(invB);
int128 yi = qInternal[i].mul(invB);
e_i_acc = _exp(yi.sub(M));
}
@@ -780,27 +797,54 @@ library LMSRStabilized {
/// @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.
function price(State storage s, uint256 baseTokenIndex, uint256 quoteTokenIndex) internal view returns (int128) {
require(baseTokenIndex < s.nAssets && quoteTokenIndex < s.nAssets, "LMSR: idx");
int128 b = _computeB(s);
return price(s.nAssets, s.kappa, s.qInternal, baseTokenIndex, quoteTokenIndex);
}
/// @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");
// Use reciprocal of b to avoid repeated divisions
int128 invB = ABDKMath64x64.div(ONE, b);
// Marginal price p_quote / p_base = exp((q_quote - q_base) / b)
return _exp(s.qInternal[quoteTokenIndex].sub(s.qInternal[baseTokenIndex]).mul(invB));
return _exp(qInternal[quoteTokenIndex].sub(qInternal[baseTokenIndex]).mul(invB));
}
/// @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)
function poolPrice(State storage s, uint256 quoteTokenIndex) internal view returns (int128) {
require(quoteTokenIndex < s.nAssets, "LMSR: idx");
return poolPrice(s.nAssets, s.kappa, s.qInternal, quoteTokenIndex);
}
/// @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
int128 b = _computeB(s);
int128 sizeMetric = _computeSizeMetric(qInternal);
require(sizeMetric > int128(0), "LMSR: size metric zero");
int128 b = kappa.mul(sizeMetric);
require(b > int128(0), "LMSR: b<=0");
// Compute total size metric S = sum q_i
int128 S = _computeSizeMetric(s.qInternal);
int128 S = sizeMetric;
require(S > int128(0), "LMSR: size zero");
// Precompute reciprocal of b
@@ -808,12 +852,12 @@ library LMSRStabilized {
// Accumulate weighted exponentials: sum_j q_j * exp((q_j - q_quote) / b)
int128 acc = int128(0);
uint256 n = s.nAssets;
uint256 n = nAssets;
for (uint256 j = 0; j < n; ) {
// factor = exp((q_j - q_quote) / b)
int128 factor = _exp(s.qInternal[j].sub(s.qInternal[quoteTokenIndex]).mul(invB));
int128 factor = _exp(qInternal[j].sub(qInternal[quoteTokenIndex]).mul(invB));
// term = q_j * factor
int128 term = s.qInternal[j].mul(factor);
int128 term = qInternal[j].mul(factor);
acc = acc.add(term);
unchecked { j++; }
}
@@ -904,8 +948,6 @@ library LMSRStabilized {
/// @notice De-initialize the LMSR state when the entire pool is drained.
/// This resets the state so the pool can be re-initialized by init(...) on next mint.
function deinit(State storage s) internal {
console2.log("LMSR.deinit: resetting state");
// Reset core state
s.nAssets = 0;
s.kappa = int128(0);

View File

@@ -1,9 +1,8 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30;
import "forge-std/console2.sol";
import "@abdk/ABDKMath64x64.sol";
import "./LMSRStabilized.sol";
import {ABDKMath64x64} from "../lib/abdk-libraries-solidity/ABDKMath64x64.sol";
import {LMSRStabilized} from "./LMSRStabilized.sol";
/// @notice Specialized functions for the 2-asset stablecoin case
library LMSRStabilizedBalancedPair {
@@ -40,7 +39,6 @@ library LMSRStabilizedBalancedPair {
// If not exactly a two-asset pool, fall back to the general routine.
if (s.nAssets != 2) {
console2.log('balanced2: fallback nAssets!=n2');
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
}
@@ -48,7 +46,6 @@ library LMSRStabilizedBalancedPair {
int128 b = LMSRStabilized._computeB(s);
// Guard: if b not positive, fallback to exact implementation (will revert there if necessary)
if (!(b > int128(0))) {
console2.log("balanced2: fallback b<=0");
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
}
int128 invB = ABDKMath64x64.div(ONE, b);
@@ -58,8 +55,6 @@ library LMSRStabilizedBalancedPair {
// If a positive limitPrice is given, attempt a 2-asset near-parity polynomial solution
if (limitPrice > int128(0)) {
console2.log("balanced2: handling limitPrice via small-delta approx");
// Approximate r0 = exp(delta) using Taylor: 1 + δ + δ^2/2 + δ^3/6
int128 delta_sq = delta.mul(delta);
int128 delta_cu = delta_sq.mul(delta);
@@ -68,19 +63,13 @@ library LMSRStabilizedBalancedPair {
.add(delta_sq.div(ABDKMath64x64.fromUInt(2)))
.add(delta_cu.div(ABDKMath64x64.fromUInt(6)));
console2.log("r0_approx:");
console2.logInt(r0_approx);
// If limitPrice <= r0 (current price) we must revert (same semantic as original)
if (limitPrice <= r0_approx) {
console2.log("balanced2: limitPrice <= r0_approx -> revert");
revert("LMSR: limitPrice <= current price");
}
// Ratio = limitPrice / r0_approx
int128 ratio = limitPrice.div(r0_approx);
console2.log("limitPrice/r0_approx:");
console2.logInt(ratio);
// x = ratio - 1; use Taylor for ln(1+x) when |x| is small
int128 x = ratio.sub(ONE);
@@ -90,7 +79,6 @@ library LMSRStabilizedBalancedPair {
int128 X_MAX = ABDKMath64x64.divu(1, 10); // 0.1
if (absX > X_MAX) {
// Too large to safely approximate; fall back to exact computation
console2.log("balanced2: fallback limitPrice ratio too far from 1");
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
}
@@ -101,63 +89,34 @@ library LMSRStabilizedBalancedPair {
.sub(x_sq.div(ABDKMath64x64.fromUInt(2)))
.add(x_cu.div(ABDKMath64x64.fromUInt(3)));
console2.log("lnRatioApprox (64x64):");
console2.logInt(lnRatioApprox);
// aLimitOverB = ln(limitPrice / r0) approximated
int128 aLimitOverB = lnRatioApprox;
// Must be > 0; otherwise fall back
if (!(aLimitOverB > int128(0))) {
console2.log("balanced2: fallback non-positive aLimitOverB");
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
}
// aLimit = b * aLimitOverB (in Q64.64)
int128 aLimit64 = b.mul(aLimitOverB);
console2.log("aLimit64 (64x64):");
console2.logInt(aLimit64);
// If computed aLimit is less than requested a, use the truncated value.
if (aLimit64 < a) {
console2.log("balanced2: truncating input a to aLimit64 due to limitPrice");
console2.log("original a:");
console2.logInt(a);
console2.log("truncated aLimit64:");
console2.logInt(aLimit64);
a = aLimit64;
} 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
}
// Debug: entry trace
console2.log("balanced2: enter");
console2.log("i", i);
console2.log("j", j);
console2.log("nAssets", s.nAssets);
console2.log("a (64x64):");
console2.logInt(a);
console2.log("b (64x64):");
console2.logInt(b);
console2.log("invB (64x64):");
console2.logInt(invB);
// Small-signal delta already computed above; reuse it
int128 absDelta = delta >= int128(0) ? delta : delta.neg();
console2.log("delta (q_i - q_j)/b:");
console2.logInt(delta);
console2.log("absDelta:");
console2.logInt(absDelta);
// Allow balanced pools only: require |delta| <= 1% (approx ln(1.01) ~ 0.00995; we use conservative 0.01)
int128 DELTA_MAX = ABDKMath64x64.divu(1, 100); // 0.01
if (absDelta > DELTA_MAX) {
// Not balanced within 1% -> use exact routine
console2.log("balanced2: fallback delta too large");
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
}
@@ -165,18 +124,13 @@ library LMSRStabilizedBalancedPair {
int128 u = a.mul(invB);
if (u <= int128(0)) {
// Non-positive input -> behave like exact implementation (will revert if invalid)
console2.log("balanced2: fallback u<=0");
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
}
console2.log("u = a/b (64x64):");
console2.logInt(u);
// Restrict to a conservative polynomial radius for accuracy; fallback otherwise.
// We choose u <= 0.5 (0.5 in Q64.64) as safe for cubic approximation in typical parameters.
int128 U_MAX = ABDKMath64x64.divu(1, 2); // 0.5
if (u > U_MAX) {
console2.log("balanced2: fallback u too large");
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
}
@@ -200,39 +154,26 @@ library LMSRStabilizedBalancedPair {
if (u <= U_TIER1) {
// Cheap quadratic ln(1+X) ≈ X - X^2/2
lnApprox = X.sub(X2.div(ABDKMath64x64.fromUInt(2)));
console2.log("balanced2: using tier1 quadratic approx");
} else if (u <= U_MAX_LOCAL) {
// Secondary cubic correction: ln(1+X) ≈ X - X^2/2 + X^3/3
int128 X3 = X2.mul(X);
lnApprox = X.sub(X2.div(ABDKMath64x64.fromUInt(2))).add(X3.div(ABDKMath64x64.fromUInt(3)));
console2.log("balanced2: using tier2 cubic approx");
} else {
// u beyond allowed range - fallback
console2.log("balanced2: fallback u too large for approximation");
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
}
console2.log("lnApprox (64x64):");
console2.logInt(lnApprox);
int128 approxOut = b.mul(lnApprox);
console2.log("approxOut (64x64):");
console2.logInt(approxOut);
// Safety sanity: approximation must be > 0
if (approxOut <= int128(0)) {
console2.log("balanced2: fallback approxOut <= 0");
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
}
// Cap to available j balance: if approximated output exceeds q_j, it's likely approximation break;
// fall back to the exact solver to handle capping/edge cases.
int128 qj64 = s.qInternal[j];
console2.log("qj64 (64x64):");
console2.logInt(qj64);
if (approxOut >= qj64) {
console2.log("balanced2: fallback approxOut >= qj");
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
}
@@ -240,15 +181,8 @@ library LMSRStabilizedBalancedPair {
amountIn = a;
amountOut = approxOut;
console2.log("balanced2: returning approx results");
console2.log("amountIn (64x64):");
console2.logInt(amountIn);
console2.log("amountOut (64x64):");
console2.logInt(amountOut);
// Final guard: ensure output is sensible and not NaN-like (rely on positivity checks above)
if (amountOut < int128(0)) {
console2.log("balanced2: fallback final guard amountOut<0");
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,30 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30;
import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {LMSRStabilizedBalancedPair} from "./LMSRStabilizedBalancedPair.sol";
import {PartyPool} from "./PartyPool.sol";
import {PartyPoolMintImpl} from "./PartyPoolMintImpl.sol";
import {PartyPoolSwapImpl} from "./PartyPoolSwapImpl.sol";
contract PartyPoolBalancedPair is PartyPool {
constructor(
string memory name_,
string memory symbol_,
IERC20[] memory tokens_,
uint256[] memory bases_,
int128 kappa_,
uint256 swapFeePpm_,
uint256 flashFeePpm_,
uint256 protocolFeePpm_, // NEW: protocol share of fees (ppm)
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);
}
}

88
src/PartyPoolBase.sol Normal file
View File

@@ -0,0 +1,88 @@
// 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;
}
}

87
src/PartyPoolDeployer.sol Normal file
View File

@@ -0,0 +1,87 @@
// 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_
);
}
}

50
src/PartyPoolHelpers.sol Normal file
View File

@@ -0,0 +1,50 @@
// 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;
}
}

593
src/PartyPoolMintImpl.sol Normal file
View File

@@ -0,0 +1,593 @@
// 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;
}
}

142
src/PartyPoolSwapImpl.sol Normal file
View File

@@ -0,0 +1,142 @@
// 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");
}
}

181
src/PartyPoolViewer.sol Normal file
View File

@@ -0,0 +1,181 @@
// 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());
}
}

View File

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

View File

@@ -2,7 +2,7 @@
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "forge-std/console.sol";
import "forge-std/console2.sol";
import "@openzeppelin/contracts/interfaces/IERC20Metadata.sol";
import "../src/LMSRStabilized.sol";
import "../src/LMSRStabilizedBalancedPair.sol";
@@ -711,7 +711,7 @@ contract LMSRStabilizedTest is Test {
}
// 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
_updateCachedQInternal(backupQ);
@@ -724,7 +724,7 @@ contract LMSRStabilizedTest is Test {
s.qInternal[1] = s.qInternal[1].add(indirectAmountOut1);
// 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,
// but the difference should be within reasonable bounds
@@ -765,7 +765,7 @@ contract LMSRStabilizedTest is Test {
s.qInternal[1] = s.qInternal[1].add(amountOut1);
// 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
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";
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 burn(address account, uint256 amount) external {_burn(account, amount);}
}

View File

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

View File

@@ -1,4 +1,5 @@
// SPDX-License-Identifier: UNLICENSED
/* solhint-disable */
pragma solidity ^0.8.30;
import "forge-std/Test.sol";
@@ -8,100 +9,72 @@ import "../src/LMSRStabilized.sol";
import "../src/PartyPool.sol";
// Import the flash callback interface
import "../src/IPartyFlashCallback.sol";
import "../lib/openzeppelin-contracts/contracts/interfaces/IERC3156FlashBorrower.sol";
import {PartyPlanner} from "../src/PartyPlanner.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
contract FlashBorrower is IPartyFlashCallback {
contract FlashBorrower is IERC3156FlashBorrower {
enum Action {
NORMAL, // Normal repayment
REPAY_NONE, // Don't repay anything
REPAY_PARTIAL, // Repay less than required
REPAY_NO_FEE, // Repay only the principal without fee
REPAY_EXACT, // Repay exactly the required amount
REPAY_EXTRA // Repay more than required (donation)
REPAY_EXACT // Repay exactly the required amount
}
Action public action;
address public pool;
address public recipient;
address[] public tokens;
address public payer;
constructor(address _pool, address[] memory _tokens) {
constructor(address _pool) {
pool = _pool;
tokens = _tokens;
}
function setAction(Action _action, address _recipient) external {
function setAction(Action _action, address _payer) external {
action = _action;
recipient = _recipient;
payer = _payer;
}
function flash(uint256[] memory amounts) external {
PartyPool(pool).flash(recipient, amounts, "");
}
function partyFlashCallback(
uint256[] memory loanAmounts,
uint256[] memory repaymentAmounts,
function onFlashLoan(
address /*initiator*/,
address token,
uint256 amount,
uint256 fee,
bytes calldata /* data */
) external override {
) external override returns (bytes32) {
require(msg.sender == pool, "Callback not called by pool");
if (action == Action.NORMAL || action == Action.REPAY_EXTRA) {
// Normal or extra repayment - transfer required amounts back to pool
for (uint256 i = 0; i < loanAmounts.length; i++) {
if (loanAmounts[i] > 0) {
uint256 repaymentAmount = repaymentAmounts[i];
if (action == Action.NORMAL) {
// Normal repayment
// We received 'amount' from the pool, need to pay back amount + fee
uint256 repaymentAmount = amount + fee;
// For REPAY_EXTRA, add 1 to each repayment
if (action == Action.REPAY_EXTRA) {
repaymentAmount += 1;
}
// Transfer the fee from payer to this contract
// (we already have the principal 'amount' from the flash loan)
TestERC20(token).transferFrom(payer, address(this), fee);
// Transfer from recipient back to pool
TestERC20(tokens[i]).transferFrom(
recipient,
pool,
repaymentAmount
);
}
}
// Approve pool to pull back the full repayment
TestERC20(token).approve(pool, repaymentAmount);
} else if (action == Action.REPAY_PARTIAL) {
// Repay half of the required amounts
for (uint256 i = 0; i < loanAmounts.length; i++) {
if (loanAmounts[i] > 0) {
uint256 partialRepayment = repaymentAmounts[i] / 2;
TestERC20(tokens[i]).transferFrom(
recipient,
pool,
partialRepayment
);
}
}
// Repay half of the required amount
uint256 partialRepayment = (amount + fee) / 2;
TestERC20(token).approve(pool, partialRepayment);
} else if (action == Action.REPAY_NO_FEE) {
// Repay only the principal without fee
for (uint256 i = 0; i < loanAmounts.length; i++) {
if (loanAmounts[i] > 0) {
TestERC20(tokens[i]).transferFrom(
recipient,
pool,
loanAmounts[i]
);
}
}
// Repay only the principal without fee (we already have it from the loan)
TestERC20(token).approve(pool, amount);
} else if (action == Action.REPAY_EXACT) {
// Repay exactly what was required
for (uint256 i = 0; i < loanAmounts.length; i++) {
if (loanAmounts[i] > 0) {
TestERC20(tokens[i]).transferFrom(
recipient,
pool,
repaymentAmounts[i]
);
}
}
uint256 repaymentAmount = amount + fee;
// Transfer the fee from payer (we have the principal from the loan)
TestERC20(token).transferFrom(payer, address(this), fee);
// Approve pool to pull back the full repayment
TestERC20(token).approve(pool, repaymentAmount);
}
// For REPAY_NONE, do nothing (don't repay)
// For REPAY_NONE, do nothing (don't approve repayment)
return keccak256("ERC3156FlashBorrower.onFlashLoan");
}
}
@@ -137,8 +110,10 @@ contract PartyPoolTest is Test {
TestERC20 token7;
TestERC20 token8;
TestERC20 token9;
PartyPlanner planner;
PartyPool pool;
PartyPool pool10;
PartyPoolViewer viewer;
address alice;
address bob;
@@ -151,6 +126,7 @@ contract PartyPoolTest is Test {
uint256 constant BASE = 1; // use base=1 so internal amounts correspond to raw integers (Q64.64 units)
function setUp() public {
planner = Deploy.newPartyPlanner();
alice = address(0xA11ce);
bob = address(0xB0b);
@@ -197,7 +173,7 @@ contract PartyPoolTest is Test {
uint256 feePpm = 1000;
int128 kappa3 = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage);
pool = new PartyPool("LP", "LP", tokens, bases, kappa3, feePpm, feePpm, false);
pool = Deploy.newPartyPool("LP", "LP", tokens, bases, kappa3, feePpm, feePpm, false);
// Transfer initial deposit amounts into pool before initial mint (pool expects tokens already in contract)
// We deposit equal amounts INIT_BAL for each token
@@ -227,7 +203,7 @@ contract PartyPoolTest is Test {
}
int128 kappa10 = LMSRStabilized.computeKappaFromSlippage(tokens10.length, tradeFrac, targetSlippage);
pool10 = new PartyPool("LP10", "LP10", tokens10, bases10, kappa10, feePpm, feePpm, false);
pool10 = Deploy.newPartyPool("LP10", "LP10", tokens10, bases10, kappa10, feePpm, feePpm, false);
// Mint additional tokens for pool10 initial deposit
token0.mint(address(this), INIT_BAL);
@@ -278,6 +254,8 @@ contract PartyPoolTest is Test {
token7.mint(bob, INIT_BAL);
token8.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.
@@ -319,7 +297,7 @@ contract PartyPoolTest is Test {
token2.approve(address(pool), type(uint256).max);
// Inspect the deposit amounts that the pool will require (these are rounded up)
uint256[] memory deposits = pool.mintDepositAmounts(1);
uint256[] memory deposits = viewer.mintAmounts(pool, 1);
// Basic sanity: deposits array length must match token count and not all zero necessarily
assertEq(deposits.length, 3);
@@ -361,7 +339,7 @@ contract PartyPoolTest is Test {
uint256 totalLpBefore = pool.totalSupply();
// Compute required deposits and perform mint for 1 wei
uint256[] memory deposits = pool.mintDepositAmounts(1);
uint256[] memory deposits = viewer.mintAmounts(pool, 1);
// Sum deposits as deposited_value
uint256 depositedValue = 0;
@@ -395,14 +373,14 @@ contract PartyPoolTest is Test {
vm.stopPrank();
}
/// @notice mintDepositAmounts should round up deposit amounts to protect the pool.
/// @notice mintAmounts should round up deposit amounts to protect the pool.
function testMintDepositAmountsRoundingUp() public view {
uint256 totalLp = pool.totalSupply();
assertTrue(totalLp > 0, "precondition: total supply > 0");
// Request half of LP supply
uint256 want = totalLp / 2;
uint256[] memory deposits = pool.mintDepositAmounts(want);
uint256[] memory deposits = viewer.mintAmounts(pool, want);
// 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)
@@ -419,7 +397,7 @@ contract PartyPoolTest is Test {
assertTrue(totalLp > 0, "precondition: LP > 0");
// Compute amounts required to redeem entire supply (should be current balances)
uint256[] memory withdrawAmounts = pool.burnReceiveAmounts(totalLp);
uint256[] memory withdrawAmounts = viewer.burnAmounts(pool, totalLp);
// Sanity: withdrawAmounts should equal pool balances (or very close due to rounding)
for (uint i = 0; i < withdrawAmounts.length; i++) {
@@ -509,7 +487,7 @@ contract PartyPoolTest is Test {
}
/// @notice Verify mintDepositAmounts matches the actual token transfers performed by mint()
/// @notice Verify mintAmounts matches the actual token transfers performed by mint()
function testMintDepositAmountsMatchesMint_3TokenPool() public {
// Use a range of LP requests (tiny to large fraction)
uint256 totalLp = pool.totalSupply();
@@ -523,7 +501,7 @@ contract PartyPoolTest is Test {
if (req == 0) req = 1;
// Compute expected deposit amounts via view
uint256[] memory expected = pool.mintDepositAmounts(req);
uint256[] memory expected = viewer.mintAmounts(pool, req);
// Ensure alice has tokens and approve pool
vm.startPrank(alice);
@@ -537,7 +515,7 @@ contract PartyPoolTest is Test {
uint256 a2Before = token2.balanceOf(alice);
// Perform mint (may revert for zero-request; ensure req>0 above)
// Guard: if mintDepositAmounts returned all zeros, skip (nothing to transfer)
// Guard: if mintAmounts returned all zeros, skip (nothing to transfer)
bool allZero = (expected[0] == 0 && expected[1] == 0 && expected[2] == 0);
if (!allZero) {
uint256 lpBefore = pool.balanceOf(alice);
@@ -556,7 +534,7 @@ contract PartyPoolTest is Test {
}
}
/// @notice Verify mintDepositAmounts matches the actual token transfers performed by mint() for 10-token pool
/// @notice Verify mintAmounts matches the actual token transfers performed by mint() for 10-token pool
function testMintDepositAmountsMatchesMint_10TokenPool() public {
uint256 totalLp = pool10.totalSupply();
uint256[] memory requests = new uint256[](4);
@@ -568,7 +546,7 @@ contract PartyPoolTest is Test {
uint256 req = requests[k];
if (req == 0) req = 1;
uint256[] memory expected = pool10.mintDepositAmounts(req);
uint256[] memory expected = viewer.mintAmounts(pool10, req);
// Approve all tokens from alice
vm.startPrank(alice);
@@ -619,7 +597,7 @@ contract PartyPoolTest is Test {
}
}
/// @notice Verify burnReceiveAmounts matches actual transfers performed by burn() for 3-token pool
/// @notice Verify burnAmounts matches actual transfers performed by burn() for 3-token pool
function testBurnReceiveAmountsMatchesBurn_3TokenPool() public {
// Use address(this) as payer (holds initial LP from setUp)
uint256 totalLp = pool.totalSupply();
@@ -646,7 +624,7 @@ contract PartyPoolTest is Test {
}
// Recompute withdraw amounts via view after any top-up
uint256[] memory expected = pool.burnReceiveAmounts(req);
uint256[] memory expected = viewer.burnAmounts(pool, req);
// If expected withdraws are all zero (rounding edge), skip this iteration
if (expected[0] == 0 && expected[1] == 0 && expected[2] == 0) {
@@ -672,7 +650,7 @@ contract PartyPoolTest is Test {
}
}
/// @notice Verify burnReceiveAmounts matches actual transfers performed by burn() for 10-token pool
/// @notice Verify burnAmounts matches actual transfers performed by burn() for 10-token pool
function testBurnReceiveAmountsMatchesBurn_10TokenPool() public {
uint256 totalLp = pool10.totalSupply();
uint256[] memory burns = new uint256[](4);
@@ -703,7 +681,7 @@ contract PartyPoolTest is Test {
vm.stopPrank();
}
uint256[] memory expected = pool10.burnReceiveAmounts(req);
uint256[] memory expected = viewer.burnAmounts(pool10, req);
// If expected withdraws are all zero (rounding edge), skip this iteration
bool allZero = true;
@@ -837,14 +815,8 @@ contract PartyPoolTest is Test {
/// @notice Setup a flash borrower for testing
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
borrower = new FlashBorrower(address(pool), tokenAddresses);
borrower = new FlashBorrower(address(pool));
// Mint tokens to alice to be used for repayments
token0.mint(alice, INIT_BAL * 2);
@@ -867,18 +839,17 @@ contract PartyPoolTest is Test {
borrower.setAction(FlashBorrower.Action.NORMAL, alice);
// Create loan request for token0 only
uint256[] memory amounts = new uint256[](3);
amounts[0] = 1000; // Only borrow token0
uint256 amount = 1000;
// Record balances before flash
uint256 aliceToken0Before = token0.balanceOf(alice);
uint256 poolToken0Before = token0.balanceOf(address(pool));
// Execute flash loan
borrower.flash(amounts);
pool.flashLoan(borrower, address(token0), amount, "");
// Net change for alice should equal the flash fee (principal is returned during repayment)
uint256 fee = (amounts[0] * pool.flashFeePpm() + 1_000_000 - 1) / 1_000_000; // ceil fee calculation
uint256 fee = (amount * pool.flashFeePpm() + 1_000_000 - 1) / 1_000_000; // ceil fee calculation
uint256 expectedAliceDecrease = fee;
assertEq(
aliceToken0Before - token0.balanceOf(alice),
@@ -894,126 +865,6 @@ 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)
function testFlashLoanNoRepaymentReverts() public {
@@ -1023,12 +874,11 @@ contract PartyPoolTest is Test {
borrower.setAction(FlashBorrower.Action.REPAY_NONE, alice);
// Create loan request
uint256[] memory amounts = new uint256[](3);
amounts[0] = 1000;
uint256 amount = 1000;
// Execute flash loan - should revert on validation
vm.expectRevert(bytes("flash: repayment failed"));
borrower.flash(amounts);
// Execute flash loan - should revert due to insufficient allowance when pool tries to pull repayment
vm.expectRevert();
pool.flashLoan(borrower, address(token0), amount, "");
}
/// @notice Test flash loan with partial repayment (should revert)
@@ -1039,12 +889,11 @@ contract PartyPoolTest is Test {
borrower.setAction(FlashBorrower.Action.REPAY_PARTIAL, alice);
// Create loan request
uint256[] memory amounts = new uint256[](3);
amounts[0] = 1000;
uint256 amount = 1000;
// Execute flash loan - should revert on validation
vm.expectRevert(bytes("flash: repayment failed"));
borrower.flash(amounts);
// Execute flash loan - should revert due to insufficient allowance when pool tries to pull full repayment
vm.expectRevert();
pool.flashLoan(borrower, address(token0), amount, "");
}
/// @notice Test flash loan with principal repayment but no fee (should revert)
@@ -1055,16 +904,15 @@ contract PartyPoolTest is Test {
borrower.setAction(FlashBorrower.Action.REPAY_NO_FEE, alice);
// Create loan request
uint256[] memory amounts = new uint256[](3);
amounts[0] = 1000;
uint256 amount = 1000;
// Execute flash loan - should revert on validation if fee > 0
// Execute flash loan - should revert due to insufficient allowance if fee > 0
if (pool.flashFeePpm() > 0) {
vm.expectRevert(bytes("flash: repayment failed"));
borrower.flash(amounts);
vm.expectRevert();
pool.flashLoan(borrower, address(token0), amount, "");
} else {
// If fee is zero, this should succeed
borrower.flash(amounts);
pool.flashLoan(borrower, address(token0), amount, "");
}
}
@@ -1076,18 +924,17 @@ contract PartyPoolTest is Test {
borrower.setAction(FlashBorrower.Action.REPAY_EXACT, alice);
// Create loan request
uint256[] memory amounts = new uint256[](3);
amounts[0] = 1000;
uint256 amount = 1000;
// Record balances before flash
uint256 aliceToken0Before = token0.balanceOf(alice);
uint256 poolToken0Before = token0.balanceOf(address(pool));
// Execute flash loan
borrower.flash(amounts);
pool.flashLoan(borrower, address(token0), amount, "");
// Check balances: net change for alice should equal the fee
uint256 fee = (amounts[0] * pool.flashFeePpm() + 1_000_000 - 1) / 1_000_000; // ceil fee calculation
uint256 fee = (amount * pool.flashFeePpm() + 1_000_000 - 1) / 1_000_000; // ceil fee calculation
uint256 expectedAliceDecrease = fee;
assertEq(
@@ -1103,115 +950,29 @@ contract PartyPoolTest is Test {
);
}
/// @notice Test flash loan with extra repayment (donation, should succeed)
function testFlashLoanExtraRepayment() public {
FlashBorrower borrower = setupFlashBorrower();
/// @notice Test flashFee view function matches flash implementation
function testFlashFee() public view {
// Test different loan amounts
uint256[] memory testAmounts = new uint256[](3);
testAmounts[0] = 1000;
testAmounts[1] = 2000;
testAmounts[2] = 3000;
// Configure borrower to repay more than required
borrower.setAction(FlashBorrower.Action.REPAY_EXTRA, alice);
for (uint256 i = 0; i < testAmounts.length; i++) {
uint256 amount = testAmounts[i];
uint256 fee = viewer.flashFee(pool, address(token0), amount);
// Create loan request
uint256[] memory amounts = new uint256[](3);
amounts[0] = 1000;
// Calculate expected fee
uint256 expectedFee = (amount * pool.flashFeePpm() + 1_000_000 - 1) / 1_000_000; // ceiling
// Record balances before flash
uint256 aliceToken0Before = token0.balanceOf(alice);
uint256 poolToken0Before = token0.balanceOf(address(pool));
// 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"
);
}
}
assertEq(
fee,
expectedFee,
"Flash fee 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
/// compared to pools initialized with default lpTokens (0)
@@ -1231,11 +992,11 @@ contract PartyPoolTest is Test {
// Pool with default initialization (lpTokens = 0)
int128 kappaDefault = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage);
PartyPool poolDefault = new PartyPool("LP_DEFAULT", "LP_DEFAULT", tokens, bases, kappaDefault, feePpm, feePpm, false);
PartyPool poolDefault = Deploy.newPartyPool("LP_DEFAULT", "LP_DEFAULT", tokens, bases, kappaDefault, feePpm, feePpm, false);
// Pool with custom initialization (lpTokens = custom amount)
int128 kappaCustom = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage);
PartyPool poolCustom = new PartyPool("LP_CUSTOM", "LP_CUSTOM", tokens, bases, kappaCustom, feePpm, feePpm, false);
PartyPool poolCustom = Deploy.newPartyPool("LP_CUSTOM", "LP_CUSTOM", tokens, bases, kappaCustom, feePpm, feePpm, false);
// Mint additional tokens for both pools
token0.mint(address(this), INIT_BAL * 2);
@@ -1307,9 +1068,9 @@ contract PartyPoolTest is Test {
uint256 feePpm = 1000;
int128 kappaDefault2 = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage);
PartyPool poolDefault = new PartyPool("LP_DEFAULT", "LP_DEFAULT", tokens, bases, kappaDefault2, feePpm, feePpm, false);
PartyPool poolDefault = Deploy.newPartyPool("LP_DEFAULT", "LP_DEFAULT", tokens, bases, kappaDefault2, feePpm, feePpm, false);
int128 kappaCustom2 = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage);
PartyPool poolCustom = new PartyPool("LP_CUSTOM", "LP_CUSTOM", tokens, bases, kappaCustom2, feePpm, feePpm, false);
PartyPool poolCustom = Deploy.newPartyPool("LP_CUSTOM", "LP_CUSTOM", tokens, bases, kappaCustom2, feePpm, feePpm, false);
// Mint additional tokens
token0.mint(address(this), INIT_BAL * 4);
@@ -1329,7 +1090,7 @@ contract PartyPoolTest is Test {
uint256 lpDefault = poolDefault.initialMint(address(this), 0);
uint256 scaleFactor = 3;
uint256 customLpAmount = lpDefault * scaleFactor;
uint256 lpCustom = poolCustom.initialMint(address(this), customLpAmount);
poolCustom.initialMint(address(this), customLpAmount);
// Verify initial LP supplies
assertEq(poolDefault.totalSupply(), lpDefault, "Default pool should have default LP supply");
@@ -1356,8 +1117,8 @@ contract PartyPoolTest is Test {
token2.approve(address(poolCustom), type(uint256).max);
// Get required deposit amounts for both pools
uint256[] memory depositsDefault = poolDefault.mintDepositAmounts(lpRequestDefault);
uint256[] memory depositsCustom = poolCustom.mintDepositAmounts(lpRequestCustom);
uint256[] memory depositsDefault = viewer.mintAmounts(poolDefault, lpRequestDefault);
uint256[] memory depositsCustom = viewer.mintAmounts(poolCustom, lpRequestCustom);
// Deposits should be identical (same proportion of identical balances)
assertEq(depositsDefault[0], depositsCustom[0], "Token0 deposits should be identical");
@@ -1385,3 +1146,4 @@ contract PartyPoolTest is Test {
}
}
/* solhint-enable */