Compare commits
25 Commits
43fb62c47c
...
923d9b93e7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
923d9b93e7 | ||
|
|
12957aaa51 | ||
|
|
457385e692 | ||
|
|
2d45296406 | ||
|
|
104c279123 | ||
|
|
98e80ab6ca | ||
|
|
ef039aa57e | ||
|
|
677ce4886c | ||
|
|
20af14c872 | ||
|
|
63f6e66d08 | ||
|
|
0049d27c90 | ||
|
|
b126c52c7c | ||
|
|
2e675bceb9 | ||
|
|
269236cfba | ||
|
|
c002d26daf | ||
|
|
b7e1b1cac2 | ||
|
|
5a2e7039d1 | ||
|
|
a6f6fd034c | ||
|
|
d46e60f83c | ||
|
|
3a5f0842b3 | ||
|
|
40e1d25e72 | ||
|
|
5ce14ab2e1 | ||
|
|
91e6a916ac | ||
|
|
28b9474363 | ||
|
|
9cac58013b |
@@ -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
551
doc/whitepaper.md
Normal 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 LMSR’s 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 AMM’s 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 5–6 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 pool’s 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 LMSR’s 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 user’s 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.2–A.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 AMM’s 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.2–A.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
100
doc/whitepaper2.md
Normal 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 market’s 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
73
src/Deploy.sol
Normal 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
123
src/ERC20External.sol
Normal 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
175
src/ERC20Internal.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
30
src/PartyPoolBalancedPair.sol
Normal file
30
src/PartyPoolBalancedPair.sol
Normal 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
88
src/PartyPoolBase.sol
Normal 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
87
src/PartyPoolDeployer.sol
Normal 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
50
src/PartyPoolHelpers.sol
Normal 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
593
src/PartyPoolMintImpl.sol
Normal 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
142
src/PartyPoolSwapImpl.sol
Normal 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
181
src/PartyPoolViewer.sol
Normal 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());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
170
test/GasTest.sol
170
test/GasTest.sol
@@ -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, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);}
|
||||
}
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user