Compare commits
5 Commits
923d9b93e7
...
43fb62c47c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43fb62c47c | ||
|
|
a43c893609 | ||
|
|
e5b2577ba9 | ||
|
|
8e69bfac5c | ||
|
|
77683555e8 |
@@ -24,14 +24,11 @@ Naturally multi-asset, Liquidity Party altcoin pools provide direct, one-hop swa
|
|||||||
|
|
||||||
| Assets | Pairs | Swap Gas | Mint Gas |
|
| Assets | Pairs | Swap Gas | Mint Gas |
|
||||||
|-------:|------:|---------:|----------:|
|
|-------:|------:|---------:|----------:|
|
||||||
| 2 | 1 | 132,000 | 143,000 |
|
| 2 | 1 | 146,000 | 149,000 |
|
||||||
| 2* | 1 | 119,000 | 143,000 |
|
| 10 | 45 | 157,000 | 426,000 |
|
||||||
| 10 | 45 | 142,000 | 412,000 |
|
| 20 | 190 | 171,000 | 772,000 |
|
||||||
| 20 | 190 | 157,000 | 749,000 |
|
| 50 | 1225 | 213,000 | 1,810,000 |
|
||||||
| 50 | 1225 | 199,000 | 1,760,000 |
|
| 100 | 4950 | 283,000 | 3,542,000 |
|
||||||
| 100 | 4950 | 269,000 | 2,684,000 |
|
|
||||||
|
|
||||||
\* Stablecoin pair pool optimization
|
|
||||||
|
|
||||||
Liquidity Party aggregates scarce, low market cap assets into a single pool, providing one-hop liquidity for exotic pairs without fragmenting LP assets. CP pools would need 190x the LP assets to provide the same pairwise liquidity as a single 20-asset Liquidity Party pool, due to asset fragmentation.
|
Liquidity Party aggregates scarce, low market cap assets into a single pool, providing one-hop liquidity for exotic pairs without fragmenting LP assets. CP pools would need 190x the LP assets to provide the same pairwise liquidity as a single 20-asset Liquidity Party pool, due to asset fragmentation.
|
||||||
|
|
||||||
|
|||||||
@@ -1,551 +0,0 @@
|
|||||||
# LMSR-based Multi-Asset AMM
|
|
||||||
|
|
||||||
Abstract
|
|
||||||
We propose a multi-asset automated market maker (AMM) whose pricing kernel is the Logarithmic Market Scoring Rule (LMSR) (Hanson, 2002.) The AMM maintains a convex potential $C(\mathbf{q}) = b(\mathbf{q}) \log\!\big(\sum_i e^{q_i / b(\mathbf{q})}\big)$ over normalized inventories $\mathbf{q}$ and sets the effective liquidity parameter proportional to pool size, $b(\mathbf{q}) = \kappa \, S(\mathbf{q})$ with $S(\mathbf{q}) = \sum_i q_i$ and fixed $\kappa>0$. This choice preserves scale-invariant responsiveness while retaining 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.
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
# LMSR-based Multi-Asset AMM
|
|
||||||
|
|
||||||
Abstract
|
|
||||||
We present a multi-asset automated market maker whose pricing kernel is the Logarithmic Market Scoring Rule (LMSR). The pool maintains the convex potential $C(\mathbf{q}) = b(\mathbf{q}) \log\!\Big(\sum_i e^{q_i / b(\mathbf{q})}\Big)$ over normalized inventories $\mathbf{q}$, and sets the effective liquidity parameter proportional to pool size as $b(\mathbf{q}) = \kappa \, S(\mathbf{q})$ with $S(\mathbf{q}) = \sum_i q_i$ and fixed $\kappa>0$. This proportional parameterization preserves scale-invariant responsiveness while retaining softmax-derived pairwise price ratios under a quasi-static-$b$ view, enabling any-to-any swaps within a single potential. We derive and use closed-form expressions for two-asset reductions to compute exact-in, exact-out, limit-hitting (swap-to-limit), and capped-output trades. We discuss stability techniques such as log-sum-exp, ratio-once shortcuts, and domain guards for fixed-point arithmetic. Liquidity operations (proportional and single-asset joins/exits) follow directly from the same potential and admit monotone, invertible mappings. Parameters are immutable post-deployment for transparency and predictable depth calibration.
|
|
||||||
|
|
||||||
Introduction and Motivation
|
|
||||||
Multi-asset liquidity typically trades off simplicity and expressivity. Classical CFMMs define multiplicative invariants over reserves, while LMSR specifies a convex cost function whose gradient yields prices. Our goal is a multi-asset AMM that uses LMSR to support any-to-any swaps, shares risk across many assets, and scales depth predictably with pool size. By setting $b(\mathbf{q})=\kappa S(\mathbf{q})$, we achieve scale invariance: proportional rescaling of all balances scales $b$ proportionally and preserves pairwise price ratios, so the 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=true
|
||||||
optimizer_runs=999999999
|
optimizer_runs=999999999
|
||||||
viaIR=true
|
viaIR=true
|
||||||
gas_reports = ['PartyPool', 'PartyPoolBalancedPair', 'PartyPlanner', 'PartyPoolSwapImpl', 'PartyPoolMintImpl',]
|
gas_reports = ['PartyPlanner', 'PartyPoolBalancedPair', 'PartyPool', ]
|
||||||
fs_permissions = [{ access = "write", path = "chain.json"}]
|
fs_permissions = [{ access = "write", path = "chain.json"}]
|
||||||
|
|
||||||
[lint]
|
[lint]
|
||||||
lint_on_build=false # more annoying than helpful
|
exclude_lints=['mixed-case-variable', 'unaliased-plain-import', ]
|
||||||
|
|
||||||
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
|
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
// SPDX-License-Identifier: UNLICENSED
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
pragma solidity ^0.8.30;
|
pragma solidity ^0.8.30;
|
||||||
|
|
||||||
import "../src/Deploy.sol";
|
|
||||||
import "../src/IPartyPool.sol";
|
|
||||||
import "../src/PartyPlanner.sol";
|
|
||||||
import "../src/PartyPool.sol";
|
|
||||||
import "../test/MockERC20.sol";
|
|
||||||
import "@abdk/ABDKMath64x64.sol";
|
|
||||||
import "forge-std/Script.sol";
|
import "forge-std/Script.sol";
|
||||||
import "forge-std/console2.sol";
|
import "forge-std/console2.sol";
|
||||||
|
import "@abdk/ABDKMath64x64.sol";
|
||||||
|
import "../test/MockERC20.sol";
|
||||||
|
import "../src/IPartyPool.sol";
|
||||||
|
import "../src/PartyPool.sol";
|
||||||
|
import "../src/PartyPlanner.sol";
|
||||||
|
|
||||||
contract DeployMock is Script {
|
contract DeployMock is Script {
|
||||||
|
|
||||||
address constant public DEV_ACCOUNT_0 = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266;
|
address constant devAccount0 = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266;
|
||||||
// private key 0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356
|
// private key 0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356
|
||||||
address constant public DEV_ACCOUNT_7 = 0x14dC79964da2C08b23698B3D3cc7Ca32193d9955;
|
address constant devAccount7 = 0x14dC79964da2C08b23698B3D3cc7Ca32193d9955;
|
||||||
|
|
||||||
function run() public {
|
function run() public {
|
||||||
vm.startBroadcast();
|
vm.startBroadcast();
|
||||||
@@ -39,7 +38,7 @@ contract DeployMock is Script {
|
|||||||
uint256 _feePpm = 100;
|
uint256 _feePpm = 100;
|
||||||
|
|
||||||
// deploy a PartyPlanner factory and create the pool via factory
|
// deploy a PartyPlanner factory and create the pool via factory
|
||||||
PartyPlanner planner = Deploy.newPartyPlanner();
|
PartyPlanner planner = new PartyPlanner();
|
||||||
|
|
||||||
// prepare initial deposits (10_000 units of each token, scaled by bases)
|
// prepare initial deposits (10_000 units of each token, scaled by bases)
|
||||||
uint256[] memory initialDeposits = new uint256[](3);
|
uint256[] memory initialDeposits = new uint256[](3);
|
||||||
@@ -57,8 +56,8 @@ contract DeployMock is Script {
|
|||||||
IERC20(tokens[i]).approve(address(planner), initialDeposits[i]);
|
IERC20(tokens[i]).approve(address(planner), initialDeposits[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// call full newPool signature on factory which will take the deposits and mint initial LP
|
// call full createPool signature on factory which will take the deposits and mint initial LP
|
||||||
(IPartyPool pool, ) = planner.newPool(
|
(PartyPool pool, uint256 lpAmount) = planner.createPool(
|
||||||
name,
|
name,
|
||||||
symbol,
|
symbol,
|
||||||
tokens,
|
tokens,
|
||||||
@@ -69,14 +68,14 @@ contract DeployMock is Script {
|
|||||||
_feePpm,
|
_feePpm,
|
||||||
false,
|
false,
|
||||||
msg.sender, // payer: this script
|
msg.sender, // payer: this script
|
||||||
DEV_ACCOUNT_7, // receiver of initial LP
|
devAccount7, // receiver of initial LP
|
||||||
initialDeposits,
|
initialDeposits,
|
||||||
initialLpAmount,
|
initialLpAmount,
|
||||||
deadline
|
deadline
|
||||||
);
|
);
|
||||||
|
|
||||||
// give tokens to dev7 for later use
|
// give tokens to dev7 for later use
|
||||||
mintAll(DEV_ACCOUNT_7, 1_000_000);
|
mintAll(devAccount7, 1_000_000);
|
||||||
|
|
||||||
vm.stopBroadcast();
|
vm.stopBroadcast();
|
||||||
|
|
||||||
@@ -95,15 +94,11 @@ contract DeployMock is Script {
|
|||||||
string memory configStr = vm.serializeString(config, vm.toString(block.chainid), chainConfigStr);
|
string memory configStr = vm.serializeString(config, vm.toString(block.chainid), chainConfigStr);
|
||||||
vm.writeJson(configStr, 'chain.json');
|
vm.writeJson(configStr, 'chain.json');
|
||||||
|
|
||||||
PartyPoolViewer viewer = Deploy.newViewer();
|
console2.log('\nPartyPlanner', address(planner));
|
||||||
|
console2.log(' PartyPool', address(pool));
|
||||||
console2.log();
|
console2.log(' USXD', address(usxd));
|
||||||
console2.log(' PartyPlanner', address(planner));
|
console2.log(' FUSD', address(fusd));
|
||||||
console2.log(' PartyPool', address(pool));
|
console2.log(' DIVE', address(dive));
|
||||||
console2.log('PartyPoolViewer', address(viewer));
|
|
||||||
console2.log(' USXD', address(usxd));
|
|
||||||
console2.log(' FUSD', address(fusd));
|
|
||||||
console2.log(' DIVE', address(dive));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MockERC20 private usxd;
|
MockERC20 private usxd;
|
||||||
|
|||||||
@@ -1,26 +1,15 @@
|
|||||||
// SPDX-License-Identifier: UNLICENSED
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
pragma solidity ^0.8.30;
|
pragma solidity ^0.8.30;
|
||||||
|
|
||||||
import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
|
import "./PartyPoolBalancedPair.sol";
|
||||||
import {PartyPlanner} from "./PartyPlanner.sol";
|
import {PartyPlanner} from "./PartyPlanner.sol";
|
||||||
import {PartyPool} from "./PartyPool.sol";
|
import {PartyPool} from "./PartyPool.sol";
|
||||||
import {PartyPoolBalancedPair} from "./PartyPoolBalancedPair.sol";
|
|
||||||
import {PartyPoolDeployer, PartyPoolBalancedPairDeployer} from "./PartyPoolDeployer.sol";
|
|
||||||
import {PartyPoolMintImpl} from "./PartyPoolMintImpl.sol";
|
|
||||||
import {PartyPoolSwapImpl} from "./PartyPoolSwapImpl.sol";
|
|
||||||
import {PartyPoolViewer} from "./PartyPoolViewer.sol";
|
|
||||||
|
|
||||||
library Deploy {
|
library Deploy {
|
||||||
|
|
||||||
function newPartyPlanner() internal returns (PartyPlanner) {
|
function newPartyPlanner() internal returns (PartyPlanner) {
|
||||||
return new PartyPlanner(
|
return new PartyPlanner();
|
||||||
new PartyPoolSwapImpl(),
|
|
||||||
new PartyPoolMintImpl(),
|
|
||||||
new PartyPoolDeployer(),
|
|
||||||
new PartyPoolBalancedPairDeployer(),
|
|
||||||
0, // protocolFeePpm = 0 for deploy helper
|
|
||||||
address(0) // protocolFeeAddress = address(0) for deploy helper
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function newPartyPool(
|
function newPartyPool(
|
||||||
@@ -28,46 +17,14 @@ library Deploy {
|
|||||||
string memory symbol_,
|
string memory symbol_,
|
||||||
IERC20[] memory tokens_,
|
IERC20[] memory tokens_,
|
||||||
uint256[] memory bases_,
|
uint256[] memory bases_,
|
||||||
int128 _kappa,
|
int128 kappa_,
|
||||||
uint256 _swapFeePpm,
|
uint256 swapFeePpm_,
|
||||||
uint256 _flashFeePpm,
|
uint256 flashFeePpm_,
|
||||||
bool _stable
|
bool stablePair_
|
||||||
) internal returns (PartyPool) {
|
) internal returns (PartyPool) {
|
||||||
// default protocol fee/off parameters (per your instruction) - set to 0 / address(0)
|
return stablePair_ ?
|
||||||
uint256 protocolFeePpm = 0;
|
new PartyPoolBalancedPair(name_, symbol_, tokens_, bases_, kappa_, swapFeePpm_, flashFeePpm_) :
|
||||||
address protocolAddr = address(0);
|
new PartyPool(name_, symbol_, tokens_, bases_, kappa_, swapFeePpm_, flashFeePpm_);
|
||||||
|
|
||||||
return _stable && tokens_.length == 2 ?
|
|
||||||
new PartyPoolBalancedPair(
|
|
||||||
name_,
|
|
||||||
symbol_,
|
|
||||||
tokens_,
|
|
||||||
bases_,
|
|
||||||
_kappa,
|
|
||||||
_swapFeePpm,
|
|
||||||
_flashFeePpm,
|
|
||||||
protocolFeePpm,
|
|
||||||
protocolAddr,
|
|
||||||
new PartyPoolSwapImpl(),
|
|
||||||
new PartyPoolMintImpl()
|
|
||||||
) :
|
|
||||||
new PartyPool(
|
|
||||||
name_,
|
|
||||||
symbol_,
|
|
||||||
tokens_,
|
|
||||||
bases_,
|
|
||||||
_kappa,
|
|
||||||
_swapFeePpm,
|
|
||||||
_flashFeePpm,
|
|
||||||
protocolFeePpm,
|
|
||||||
protocolAddr,
|
|
||||||
new PartyPoolSwapImpl(),
|
|
||||||
new PartyPoolMintImpl()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function newViewer() internal returns (PartyPoolViewer) {
|
|
||||||
return new PartyPoolViewer(new PartyPoolSwapImpl(), new PartyPoolMintImpl());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,123 +0,0 @@
|
|||||||
// SPDX-License-Identifier: UNLICENSED
|
|
||||||
pragma solidity ^0.8.30;
|
|
||||||
|
|
||||||
import {IERC20Errors} from "../lib/openzeppelin-contracts/contracts/interfaces/draft-IERC6093.sol";
|
|
||||||
import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
|
|
||||||
import {Context} from "../lib/openzeppelin-contracts/contracts/utils/Context.sol";
|
|
||||||
import {IERC20Metadata} from "../lib/openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol";
|
|
||||||
import {ERC20Internal} from "./ERC20Internal.sol";
|
|
||||||
|
|
||||||
// Copied from OpenZeppelin's ERC20 implementation, but split into internal and external parts
|
|
||||||
|
|
||||||
contract ERC20External is ERC20Internal, IERC20Metadata {
|
|
||||||
/**
|
|
||||||
* @dev Sets the values for {name} and {symbol}.
|
|
||||||
*
|
|
||||||
* Both values are immutable: they can only be set once during construction.
|
|
||||||
*/
|
|
||||||
constructor(string memory name_, string memory symbol_) {
|
|
||||||
_name = name_;
|
|
||||||
_symbol = symbol_;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dev Returns the name of the token.
|
|
||||||
*/
|
|
||||||
function name() public view virtual returns (string memory) {
|
|
||||||
return _name;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dev Returns the symbol of the token, usually a shorter version of the
|
|
||||||
* name.
|
|
||||||
*/
|
|
||||||
function symbol() public view virtual returns (string memory) {
|
|
||||||
return _symbol;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dev Returns the number of decimals used to get its user representation.
|
|
||||||
* For example, if `decimals` equals `2`, a balance of `505` tokens should
|
|
||||||
* be displayed to a user as `5.05` (`505 / 10 ** 2`).
|
|
||||||
*
|
|
||||||
* Tokens usually opt for a value of 18, imitating the relationship between
|
|
||||||
* Ether and Wei. This is the default value returned by this function, unless
|
|
||||||
* it's overridden.
|
|
||||||
*
|
|
||||||
* NOTE: This information is only used for _display_ purposes: it in
|
|
||||||
* no way affects any of the arithmetic of the contract, including
|
|
||||||
* {IERC20-balanceOf} and {IERC20-transfer}.
|
|
||||||
*/
|
|
||||||
function decimals() public view virtual returns (uint8) {
|
|
||||||
return 18;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @inheritdoc IERC20
|
|
||||||
function totalSupply() public view virtual returns (uint256) {
|
|
||||||
return _totalSupply;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @inheritdoc IERC20
|
|
||||||
function balanceOf(address account) public view virtual returns (uint256) {
|
|
||||||
return _balances[account];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dev See {IERC20-transfer}.
|
|
||||||
*
|
|
||||||
* Requirements:
|
|
||||||
*
|
|
||||||
* - `to` cannot be the zero address.
|
|
||||||
* - the caller must have a balance of at least `value`.
|
|
||||||
*/
|
|
||||||
function transfer(address to, uint256 value) public virtual returns (bool) {
|
|
||||||
address owner = _msgSender();
|
|
||||||
_transfer(owner, to, value);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @inheritdoc IERC20
|
|
||||||
function allowance(address owner, address spender) public view virtual returns (uint256) {
|
|
||||||
return _allowances[owner][spender];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dev See {IERC20-approve}.
|
|
||||||
*
|
|
||||||
* NOTE: If `value` is the maximum `uint256`, the allowance is not updated on
|
|
||||||
* `transferFrom`. This is semantically equivalent to an infinite approval.
|
|
||||||
*
|
|
||||||
* Requirements:
|
|
||||||
*
|
|
||||||
* - `spender` cannot be the zero address.
|
|
||||||
*/
|
|
||||||
function approve(address spender, uint256 value) public virtual returns (bool) {
|
|
||||||
address owner = _msgSender();
|
|
||||||
_approve(owner, spender, value);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dev See {IERC20-transferFrom}.
|
|
||||||
*
|
|
||||||
* Skips emitting an {Approval} event indicating an allowance update. This is not
|
|
||||||
* required by the ERC. See {xref-ERC20-_approve-address-address-uint256-bool-}[_approve].
|
|
||||||
*
|
|
||||||
* NOTE: Does not update the allowance if the current allowance
|
|
||||||
* is the maximum `uint256`.
|
|
||||||
*
|
|
||||||
* Requirements:
|
|
||||||
*
|
|
||||||
* - `from` and `to` cannot be the zero address.
|
|
||||||
* - `from` must have a balance of at least `value`.
|
|
||||||
* - the caller must have allowance for ``from``'s tokens of at least
|
|
||||||
* `value`.
|
|
||||||
*/
|
|
||||||
function transferFrom(address from, address to, uint256 value) public virtual returns (bool) {
|
|
||||||
address spender = _msgSender();
|
|
||||||
_spendAllowance(from, spender, value);
|
|
||||||
_transfer(from, to, value);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
// SPDX-License-Identifier: UNLICENSED
|
|
||||||
pragma solidity ^0.8.30;
|
|
||||||
|
|
||||||
import {IERC20Errors} from "../lib/openzeppelin-contracts/contracts/interfaces/draft-IERC6093.sol";
|
|
||||||
import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
|
|
||||||
import {Context} from "../lib/openzeppelin-contracts/contracts/utils/Context.sol";
|
|
||||||
|
|
||||||
// Copied from OpenZeppelin's ERC20 implementation, but split into internal and external parts
|
|
||||||
|
|
||||||
abstract contract ERC20Internal is Context, IERC20Errors {
|
|
||||||
mapping(address account => uint256) internal _balances;
|
|
||||||
mapping(address account => mapping(address spender => uint256)) internal _allowances;
|
|
||||||
uint256 internal _totalSupply;
|
|
||||||
string internal _name;
|
|
||||||
string internal _symbol;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dev Moves a `value` amount of tokens from `from` to `to`.
|
|
||||||
*
|
|
||||||
* This internal function is equivalent to {transfer}, and can be used to
|
|
||||||
* e.g. implement automatic token fees, slashing mechanisms, etc.
|
|
||||||
*
|
|
||||||
* Emits a {Transfer} event.
|
|
||||||
*
|
|
||||||
* NOTE: This function is not virtual, {_update} should be overridden instead.
|
|
||||||
*/
|
|
||||||
function _transfer(address from, address to, uint256 value) internal {
|
|
||||||
if (from == address(0)) {
|
|
||||||
revert ERC20InvalidSender(address(0));
|
|
||||||
}
|
|
||||||
if (to == address(0)) {
|
|
||||||
revert ERC20InvalidReceiver(address(0));
|
|
||||||
}
|
|
||||||
_update(from, to, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dev Transfers a `value` amount of tokens from `from` to `to`, or alternatively mints (or burns) if `from`
|
|
||||||
* (or `to`) is the zero address. All customizations to transfers, mints, and burns should be done by overriding
|
|
||||||
* this function.
|
|
||||||
*
|
|
||||||
* Emits a {Transfer} event.
|
|
||||||
*/
|
|
||||||
function _update(address from, address to, uint256 value) internal virtual {
|
|
||||||
if (from == address(0)) {
|
|
||||||
// Overflow check required: The rest of the code assumes that totalSupply never overflows
|
|
||||||
_totalSupply += value;
|
|
||||||
} else {
|
|
||||||
uint256 fromBalance = _balances[from];
|
|
||||||
if (fromBalance < value) {
|
|
||||||
revert ERC20InsufficientBalance(from, fromBalance, value);
|
|
||||||
}
|
|
||||||
unchecked {
|
|
||||||
// Overflow not possible: value <= fromBalance <= totalSupply.
|
|
||||||
_balances[from] = fromBalance - value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (to == address(0)) {
|
|
||||||
unchecked {
|
|
||||||
// Overflow not possible: value <= totalSupply or value <= fromBalance <= totalSupply.
|
|
||||||
_totalSupply -= value;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
unchecked {
|
|
||||||
// Overflow not possible: balance + value is at most totalSupply, which we know fits into a uint256.
|
|
||||||
_balances[to] += value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
emit IERC20.Transfer(from, to, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dev Creates a `value` amount of tokens and assigns them to `account`, by transferring it from address(0).
|
|
||||||
* Relies on the `_update` mechanism
|
|
||||||
*
|
|
||||||
* Emits a {Transfer} event with `from` set to the zero address.
|
|
||||||
*
|
|
||||||
* NOTE: This function is not virtual, {_update} should be overridden instead.
|
|
||||||
*/
|
|
||||||
function _mint(address account, uint256 value) internal {
|
|
||||||
if (account == address(0)) {
|
|
||||||
revert ERC20InvalidReceiver(address(0));
|
|
||||||
}
|
|
||||||
_update(address(0), account, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dev Destroys a `value` amount of tokens from `account`, lowering the total supply.
|
|
||||||
* Relies on the `_update` mechanism.
|
|
||||||
*
|
|
||||||
* Emits a {Transfer} event with `to` set to the zero address.
|
|
||||||
*
|
|
||||||
* NOTE: This function is not virtual, {_update} should be overridden instead
|
|
||||||
*/
|
|
||||||
function _burn(address account, uint256 value) internal {
|
|
||||||
if (account == address(0)) {
|
|
||||||
revert ERC20InvalidSender(address(0));
|
|
||||||
}
|
|
||||||
_update(account, address(0), value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dev Sets `value` as the allowance of `spender` over the `owner`'s tokens.
|
|
||||||
*
|
|
||||||
* This internal function is equivalent to `approve`, and can be used to
|
|
||||||
* e.g. set automatic allowances for certain subsystems, etc.
|
|
||||||
*
|
|
||||||
* Emits an {Approval} event.
|
|
||||||
*
|
|
||||||
* Requirements:
|
|
||||||
*
|
|
||||||
* - `owner` cannot be the zero address.
|
|
||||||
* - `spender` cannot be the zero address.
|
|
||||||
*
|
|
||||||
* Overrides to this logic should be done to the variant with an additional `bool emitEvent` argument.
|
|
||||||
*/
|
|
||||||
function _approve(address owner, address spender, uint256 value) internal {
|
|
||||||
_approve(owner, spender, value, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dev Variant of {_approve} with an optional flag to enable or disable the {Approval} event.
|
|
||||||
*
|
|
||||||
* By default (when calling {_approve}) the flag is set to true. On the other hand, approval changes made by
|
|
||||||
* `_spendAllowance` during the `transferFrom` operation set the flag to false. This saves gas by not emitting any
|
|
||||||
* `Approval` event during `transferFrom` operations.
|
|
||||||
*
|
|
||||||
* Anyone who wishes to continue emitting `Approval` events on the`transferFrom` operation can force the flag to
|
|
||||||
* true using the following override:
|
|
||||||
*
|
|
||||||
* ```solidity
|
|
||||||
* function _approve(address owner, address spender, uint256 value, bool) internal virtual override {
|
|
||||||
* super._approve(owner, spender, value, true);
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* Requirements are the same as {_approve}.
|
|
||||||
*/
|
|
||||||
function _approve(address owner, address spender, uint256 value, bool emitEvent) internal virtual {
|
|
||||||
if (owner == address(0)) {
|
|
||||||
revert ERC20InvalidApprover(address(0));
|
|
||||||
}
|
|
||||||
if (spender == address(0)) {
|
|
||||||
revert ERC20InvalidSpender(address(0));
|
|
||||||
}
|
|
||||||
_allowances[owner][spender] = value;
|
|
||||||
if (emitEvent) {
|
|
||||||
emit IERC20.Approval(owner, spender, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dev Updates `owner`'s allowance for `spender` based on spent `value`.
|
|
||||||
*
|
|
||||||
* Does not update the allowance value in case of infinite allowance.
|
|
||||||
* Revert if not enough allowance is available.
|
|
||||||
*
|
|
||||||
* Does not emit an {Approval} event.
|
|
||||||
*/
|
|
||||||
function _spendAllowance(address owner, address spender, uint256 value) internal virtual {
|
|
||||||
uint256 currentAllowance = _allowances[owner][spender];
|
|
||||||
if (currentAllowance < type(uint256).max) {
|
|
||||||
if (currentAllowance < value) {
|
|
||||||
revert ERC20InsufficientAllowance(spender, currentAllowance, value);
|
|
||||||
}
|
|
||||||
unchecked {
|
|
||||||
_approve(owner, spender, currentAllowance - value, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,16 +1,14 @@
|
|||||||
// SPDX-License-Identifier: UNLICENSED
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
pragma solidity ^0.8.30;
|
pragma solidity ^0.8.30;
|
||||||
|
|
||||||
import "./IPartyPool.sol";
|
import "./PartyPool.sol";
|
||||||
import "./PartyPoolMintImpl.sol";
|
|
||||||
import "./PartyPoolSwapImpl.sol";
|
|
||||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||||
|
|
||||||
/// @title IPartyPlanner
|
/// @title IPartyPlanner
|
||||||
/// @notice Interface for factory contract for creating and tracking PartyPool instances
|
/// @notice Interface for factory contract for creating and tracking PartyPool instances
|
||||||
interface IPartyPlanner {
|
interface IPartyPlanner {
|
||||||
// Event emitted when a new pool is created
|
// Event emitted when a new pool is created
|
||||||
event PartyStarted(IPartyPool indexed pool, string name, string symbol, IERC20[] tokens);
|
event PartyStarted(PartyPool indexed pool, string name, string symbol, IERC20[] tokens);
|
||||||
|
|
||||||
/// @notice Creates a new PartyPool instance and initializes it with initial deposits (legacy signature).
|
/// @notice Creates a new PartyPool instance and initializes it with initial deposits (legacy signature).
|
||||||
/// @dev Deprecated in favour of the kappa-based overload below; kept for backwards compatibility.
|
/// @dev Deprecated in favour of the kappa-based overload below; kept for backwards compatibility.
|
||||||
@@ -29,7 +27,7 @@ interface IPartyPlanner {
|
|||||||
/// @param deadline Reverts if nonzero and the current blocktime is later than the deadline
|
/// @param deadline Reverts if nonzero and the current blocktime is later than the deadline
|
||||||
/// @return pool Address of the newly created and initialized PartyPool
|
/// @return pool Address of the newly created and initialized PartyPool
|
||||||
/// @return lpAmount Amount of LP tokens minted to the receiver
|
/// @return lpAmount Amount of LP tokens minted to the receiver
|
||||||
function newPool(
|
function createPool(
|
||||||
// Pool constructor args (legacy)
|
// Pool constructor args (legacy)
|
||||||
string memory name_,
|
string memory name_,
|
||||||
string memory symbol_,
|
string memory symbol_,
|
||||||
@@ -46,7 +44,7 @@ interface IPartyPlanner {
|
|||||||
uint256[] memory initialDeposits,
|
uint256[] memory initialDeposits,
|
||||||
uint256 initialLpAmount,
|
uint256 initialLpAmount,
|
||||||
uint256 deadline
|
uint256 deadline
|
||||||
) external returns (IPartyPool pool, uint256 lpAmount);
|
) external returns (PartyPool pool, uint256 lpAmount);
|
||||||
|
|
||||||
/// @notice Creates a new PartyPool instance and initializes it with initial deposits (kappa-based).
|
/// @notice Creates a new PartyPool instance and initializes it with initial deposits (kappa-based).
|
||||||
/// @param name_ LP token name
|
/// @param name_ LP token name
|
||||||
@@ -63,7 +61,7 @@ interface IPartyPlanner {
|
|||||||
/// @param deadline Reverts if nonzero and the current blocktime is later than the deadline
|
/// @param deadline Reverts if nonzero and the current blocktime is later than the deadline
|
||||||
/// @return pool Address of the newly created and initialized PartyPool
|
/// @return pool Address of the newly created and initialized PartyPool
|
||||||
/// @return lpAmount Amount of LP tokens minted to the receiver
|
/// @return lpAmount Amount of LP tokens minted to the receiver
|
||||||
function newPool(
|
function createPool(
|
||||||
// Pool constructor args (kappa-based)
|
// Pool constructor args (kappa-based)
|
||||||
string memory name_,
|
string memory name_,
|
||||||
string memory symbol_,
|
string memory symbol_,
|
||||||
@@ -79,7 +77,7 @@ interface IPartyPlanner {
|
|||||||
uint256[] memory initialDeposits,
|
uint256[] memory initialDeposits,
|
||||||
uint256 initialLpAmount,
|
uint256 initialLpAmount,
|
||||||
uint256 deadline
|
uint256 deadline
|
||||||
) external returns (IPartyPool pool, uint256 lpAmount);
|
) external returns (PartyPool pool, uint256 lpAmount);
|
||||||
|
|
||||||
/// @notice Checks if a pool is supported
|
/// @notice Checks if a pool is supported
|
||||||
/// @param pool The pool address to check
|
/// @param pool The pool address to check
|
||||||
@@ -94,7 +92,7 @@ interface IPartyPlanner {
|
|||||||
/// @param offset Starting index for pagination
|
/// @param offset Starting index for pagination
|
||||||
/// @param limit Maximum number of items to return
|
/// @param limit Maximum number of items to return
|
||||||
/// @return pools Array of pool addresses for the requested page
|
/// @return pools Array of pool addresses for the requested page
|
||||||
function getAllPools(uint256 offset, uint256 limit) external view returns (IPartyPool[] memory pools);
|
function getAllPools(uint256 offset, uint256 limit) external view returns (PartyPool[] memory pools);
|
||||||
|
|
||||||
/// @notice Returns the total number of unique tokens
|
/// @notice Returns the total number of unique tokens
|
||||||
/// @return The total count of unique tokens
|
/// @return The total count of unique tokens
|
||||||
@@ -116,12 +114,5 @@ interface IPartyPlanner {
|
|||||||
/// @param offset Starting index for pagination
|
/// @param offset Starting index for pagination
|
||||||
/// @param limit Maximum number of items to return
|
/// @param limit Maximum number of items to return
|
||||||
/// @return pools Array of pool addresses containing the specified token
|
/// @return pools Array of pool addresses containing the specified token
|
||||||
function getPoolsByToken(IERC20 token, uint256 offset, uint256 limit) external view returns (IPartyPool[] memory pools);
|
function getPoolsByToken(IERC20 token, uint256 offset, uint256 limit) external view returns (PartyPool[] memory pools);
|
||||||
|
|
||||||
/// @notice Address of the mint implementation contract used by all pools created by this factory
|
|
||||||
function mintImpl() external view returns (PartyPoolMintImpl);
|
|
||||||
|
|
||||||
/// @notice Address of the swap implementation contract used by all pools created by this factory
|
|
||||||
function swapMintImpl() external view returns (PartyPoolSwapImpl);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
// SPDX-License-Identifier: UNLICENSED
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
pragma solidity ^0.8.30;
|
pragma solidity ^0.8.30;
|
||||||
|
|
||||||
import "../lib/openzeppelin-contracts/contracts/interfaces/IERC3156FlashBorrower.sol";
|
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
|
||||||
import "./LMSRStabilized.sol";
|
|
||||||
import {IERC20Metadata} from "../lib/openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol";
|
|
||||||
import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
|
|
||||||
|
|
||||||
/// @title PartyPool - LMSR-backed multi-asset pool with LP ERC20 token
|
/// @title PartyPool - LMSR-backed multi-asset pool with LP ERC20 token
|
||||||
/// @notice A multi-asset liquidity pool backed by the LMSRStabilized pricing model.
|
/// @notice A multi-asset liquidity pool backed by the LMSRStabilized pricing model.
|
||||||
@@ -58,11 +55,10 @@ interface IPartyPool is IERC20Metadata {
|
|||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
function LMSR() external view returns (LMSRStabilized.State memory);
|
// Immutable pool configuration (public getters)
|
||||||
|
|
||||||
/// @notice Token addresses comprising the pool. Effectively immutable after construction.
|
/// @notice Token addresses comprising the pool. Effectively immutable after construction.
|
||||||
/// @dev tokens[i] corresponds to the i-th asset and maps to index i in the internal LMSR arrays.
|
/// @dev tokens[i] corresponds to the i-th asset and maps to index i in the internal LMSR arrays.
|
||||||
function getToken(uint256) external view returns (IERC20); // get single token
|
function tokens(uint256) external view returns (IERC20); // get single token
|
||||||
|
|
||||||
/// @notice Returns the number of tokens (n) in the pool.
|
/// @notice Returns the number of tokens (n) in the pool.
|
||||||
function numTokens() external view returns (uint256);
|
function numTokens() external view returns (uint256);
|
||||||
@@ -80,32 +76,23 @@ interface IPartyPool is IERC20Metadata {
|
|||||||
/// @notice Flash-loan fee in parts-per-million (ppm) applied to flash borrow amounts.
|
/// @notice Flash-loan fee in parts-per-million (ppm) applied to flash borrow amounts.
|
||||||
function flashFeePpm() external view returns (uint256);
|
function flashFeePpm() external view returns (uint256);
|
||||||
|
|
||||||
/// @notice Protocol fee share (ppm) applied to fees collected by the pool (floored when accrued)
|
|
||||||
/// @dev This is the fraction (in ppm) of the pool-collected fees that are owed to the protocol.
|
|
||||||
function protocolFeePpm() external view returns (uint256);
|
|
||||||
|
|
||||||
/// @notice Address that will receive collected protocol tokens when collectProtocolFees() is called.
|
|
||||||
function protocolFeeAddress() external view returns (address);
|
|
||||||
|
|
||||||
/// @notice Protocol fee ledger accessor. Returns tokens owed (raw uint token units) from this pool as protocol fees
|
|
||||||
/// that have not yet been transferred out.
|
|
||||||
function allProtocolFeesOwed() external view returns (uint256[] memory);
|
|
||||||
|
|
||||||
/// @notice Callable by anyone, sends any owed protocol fees to the protocol fee address.
|
|
||||||
function collectProtocolFees() external;
|
|
||||||
|
|
||||||
/// @notice Liquidity parameter κ (Q64.64) used by the LMSR kernel: b = κ * S(q)
|
/// @notice Liquidity parameter κ (Q64.64) used by the LMSR kernel: b = κ * S(q)
|
||||||
/// @dev Pools are constructed with a κ value; this getter exposes the κ used by the pool.
|
/// @dev Pools are constructed with a κ value; this getter exposes the κ used by the pool.
|
||||||
function kappa() external view returns (int128);
|
function kappa() external view returns (int128);
|
||||||
|
|
||||||
|
/// @notice Mapping from token address => (index+1). A zero value indicates the token is not in the pool.
|
||||||
|
/// @dev Use index = tokenAddressToIndexPlusOne[token] - 1 when non-zero.
|
||||||
|
function tokenAddressToIndexPlusOne(IERC20) external view returns (uint);
|
||||||
|
|
||||||
// Initialization / Mint / Burn (LP token managed)
|
// Initialization / Mint / Burn (LP token managed)
|
||||||
|
|
||||||
/// @notice Initial mint to set up pool for the first time.
|
/// @notice Calculate the proportional deposit amounts required for a given LP token amount
|
||||||
/// @dev Assumes tokens have already been transferred to the pool prior to calling.
|
/// @dev Returns the minimum token amounts (rounded up) that must be supplied to receive lpTokenAmount
|
||||||
/// Can only be called when the pool is uninitialized (totalSupply() == 0 or lmsr.nAssets == 0).
|
/// LP tokens at current pool proportions. If the pool is empty (initial deposit) returns zeros
|
||||||
/// @param receiver address that receives the LP tokens
|
/// because the initial deposit is handled by transferring tokens then calling mint().
|
||||||
/// @param lpTokens The number of LP tokens to issue for this mint. If 0, then the number of tokens returned will equal the LMSR internal q total
|
/// @param lpTokenAmount The amount of LP tokens desired
|
||||||
function initialMint(address receiver, uint256 lpTokens) external returns (uint256 lpMinted);
|
/// @return depositAmounts Array of token amounts to deposit (rounded up)
|
||||||
|
function mintDepositAmounts(uint256 lpTokenAmount) external view returns (uint256[] memory depositAmounts);
|
||||||
|
|
||||||
/// @notice Proportional mint (or initial supply if first call).
|
/// @notice Proportional mint (or initial supply if first call).
|
||||||
/// @dev - For initial supply: assumes tokens have already been transferred to the pool prior to calling.
|
/// @dev - For initial supply: assumes tokens have already been transferred to the pool prior to calling.
|
||||||
@@ -118,6 +105,13 @@ interface IPartyPool is IERC20Metadata {
|
|||||||
/// @return lpMinted the actual amount of lpToken minted
|
/// @return lpMinted the actual amount of lpToken minted
|
||||||
function mint(address payer, address receiver, uint256 lpTokenAmount, uint256 deadline) external returns (uint256 lpMinted);
|
function mint(address payer, address receiver, uint256 lpTokenAmount, uint256 deadline) external returns (uint256 lpMinted);
|
||||||
|
|
||||||
|
/// @notice Calculate the proportional withdrawal amounts for a given LP token amount
|
||||||
|
/// @dev Returns the maximum token amounts (rounded down) that will be withdrawn when burning lpTokenAmount.
|
||||||
|
/// If the pool is uninitialized or supply is zero, returns zeros.
|
||||||
|
/// @param lpTokenAmount The amount of LP tokens to burn
|
||||||
|
/// @return withdrawAmounts Array of token amounts to withdraw (rounded down)
|
||||||
|
function burnReceiveAmounts(uint256 lpTokenAmount) external view returns (uint256[] memory withdrawAmounts);
|
||||||
|
|
||||||
/// @notice Burn LP tokens and withdraw the proportional basket to receiver.
|
/// @notice Burn LP tokens and withdraw the proportional basket to receiver.
|
||||||
/// @dev Payer must own or approve the LP tokens being burned. The function updates LMSR state
|
/// @dev Payer must own or approve the LP tokens being burned. The function updates LMSR state
|
||||||
/// proportionally to reflect the reduced pool size after the withdrawal.
|
/// proportionally to reflect the reduced pool size after the withdrawal.
|
||||||
@@ -125,7 +119,7 @@ interface IPartyPool is IERC20Metadata {
|
|||||||
/// @param receiver address that receives the withdrawn tokens
|
/// @param receiver address that receives the withdrawn tokens
|
||||||
/// @param lpAmount amount of LP tokens to burn (proportional withdrawal)
|
/// @param lpAmount amount of LP tokens to burn (proportional withdrawal)
|
||||||
/// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore.
|
/// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore.
|
||||||
function burn(address payer, address receiver, uint256 lpAmount, uint256 deadline) external returns (uint256[] memory withdrawAmounts);
|
function burn(address payer, address receiver, uint256 lpAmount, uint256 deadline) external;
|
||||||
|
|
||||||
|
|
||||||
// Swaps
|
// Swaps
|
||||||
@@ -136,14 +130,12 @@ interface IPartyPool is IERC20Metadata {
|
|||||||
/// @param maxAmountIn maximum gross input allowed (inclusive of fee)
|
/// @param maxAmountIn maximum gross input allowed (inclusive of fee)
|
||||||
/// @param limitPrice maximum acceptable marginal price (pass 0 to ignore)
|
/// @param limitPrice maximum acceptable marginal price (pass 0 to ignore)
|
||||||
/// @return amountIn gross input amount to transfer (includes fee), amountOut output amount user would receive, fee fee amount taken
|
/// @return amountIn gross input amount to transfer (includes fee), amountOut output amount user would receive, fee fee amount taken
|
||||||
/*
|
|
||||||
function swapAmounts(
|
function swapAmounts(
|
||||||
uint256 inputTokenIndex,
|
uint256 inputTokenIndex,
|
||||||
uint256 outputTokenIndex,
|
uint256 outputTokenIndex,
|
||||||
uint256 maxAmountIn,
|
uint256 maxAmountIn,
|
||||||
int128 limitPrice
|
int128 limitPrice
|
||||||
) external view returns (uint256 amountIn, uint256 amountOut, uint256 fee);
|
) external view returns (uint256 amountIn, uint256 amountOut, uint256 fee);
|
||||||
*/
|
|
||||||
|
|
||||||
/// @notice Swap input token inputTokenIndex -> token outputTokenIndex. Payer must approve token inputTokenIndex.
|
/// @notice Swap input token inputTokenIndex -> token outputTokenIndex. Payer must approve token inputTokenIndex.
|
||||||
/// @dev This function transfers the exact gross input (including fee) from payer and sends the computed output to receiver.
|
/// @dev This function transfers the exact gross input (including fee) from payer and sends the computed output to receiver.
|
||||||
@@ -166,6 +158,17 @@ interface IPartyPool is IERC20Metadata {
|
|||||||
uint256 deadline
|
uint256 deadline
|
||||||
) external returns (uint256 amountIn, uint256 amountOut, uint256 fee);
|
) external returns (uint256 amountIn, uint256 amountOut, uint256 fee);
|
||||||
|
|
||||||
|
/// @notice External view to quote swap-to-limit amounts (gross input incl. fee and output), matching swapToLimit() computations
|
||||||
|
/// @param inputTokenIndex index of input token
|
||||||
|
/// @param outputTokenIndex index of output token
|
||||||
|
/// @param limitPrice target marginal price to reach (must be > 0)
|
||||||
|
/// @return amountIn gross input amount to transfer (includes fee), amountOut output amount user would receive, fee fee amount taken
|
||||||
|
function swapToLimitAmounts(
|
||||||
|
uint256 inputTokenIndex,
|
||||||
|
uint256 outputTokenIndex,
|
||||||
|
int128 limitPrice
|
||||||
|
) external view returns (uint256 amountIn, uint256 amountOut, uint256 fee);
|
||||||
|
|
||||||
/// @notice Swap up to the price limit; computes max input to reach limit then performs swap.
|
/// @notice Swap up to the price limit; computes max input to reach limit then performs swap.
|
||||||
/// @dev If balances prevent fully reaching the limit, the function caps and returns actuals.
|
/// @dev If balances prevent fully reaching the limit, the function caps and returns actuals.
|
||||||
/// The payer must transfer the exact gross input computed by the view.
|
/// The payer must transfer the exact gross input computed by the view.
|
||||||
@@ -185,6 +188,15 @@ interface IPartyPool is IERC20Metadata {
|
|||||||
uint256 deadline
|
uint256 deadline
|
||||||
) external returns (uint256 amountInUsed, uint256 amountOut, uint256 fee);
|
) external returns (uint256 amountInUsed, uint256 amountOut, uint256 fee);
|
||||||
|
|
||||||
|
/// @notice External view to quote swapMint amounts, matching swapMint() computations
|
||||||
|
/// @param inputTokenIndex index of input token to deposit
|
||||||
|
/// @param maxAmountIn maximum gross input allowed (inclusive of fee)
|
||||||
|
/// @return totalTransfer gross input amount to transfer (includes fee), amountIn net input amount used for minting, fee fee amount taken, lpMinted LP tokens that would be minted
|
||||||
|
function swapMintAmounts(
|
||||||
|
uint256 inputTokenIndex,
|
||||||
|
uint256 maxAmountIn
|
||||||
|
) external view returns (uint256 totalTransfer, uint256 amountIn, uint256 fee, uint256 lpMinted);
|
||||||
|
|
||||||
/// @notice Single-token mint: deposit a single token, charge swap-LMSR cost, and mint LP.
|
/// @notice Single-token mint: deposit a single token, charge swap-LMSR cost, and mint LP.
|
||||||
/// @dev swapMint executes as an exact-in planned swap followed by proportional scaling of qInternal.
|
/// @dev swapMint executes as an exact-in planned swap followed by proportional scaling of qInternal.
|
||||||
/// The function emits SwapMint (gross, net, fee) and also emits Mint for LP issuance.
|
/// The function emits SwapMint (gross, net, fee) and also emits Mint for LP issuance.
|
||||||
@@ -202,6 +214,15 @@ interface IPartyPool is IERC20Metadata {
|
|||||||
uint256 deadline
|
uint256 deadline
|
||||||
) external returns (uint256 lpMinted);
|
) external returns (uint256 lpMinted);
|
||||||
|
|
||||||
|
/// @notice External view to quote burnSwap amounts, matching burnSwap() computations
|
||||||
|
/// @param lpAmount amount of LP tokens to burn
|
||||||
|
/// @param inputTokenIndex index of target asset to receive
|
||||||
|
/// @return amountOut output amount user would receive after fees
|
||||||
|
function burnSwapAmounts(
|
||||||
|
uint256 lpAmount,
|
||||||
|
uint256 inputTokenIndex
|
||||||
|
) external view returns (uint256 amountOut);
|
||||||
|
|
||||||
/// @notice Burn LP tokens then swap the redeemed proportional basket into a single asset `inputTokenIndex` and send to receiver.
|
/// @notice Burn LP tokens then swap the redeemed proportional basket into a single asset `inputTokenIndex` and send to receiver.
|
||||||
/// @dev The function burns LP tokens (authorization via allowance if needed), sends the single-asset payout and updates LMSR state.
|
/// @dev The function burns LP tokens (authorization via allowance if needed), sends the single-asset payout and updates LMSR state.
|
||||||
/// @param payer who burns LP tokens
|
/// @param payer who burns LP tokens
|
||||||
@@ -218,17 +239,39 @@ interface IPartyPool is IERC20Metadata {
|
|||||||
uint256 deadline
|
uint256 deadline
|
||||||
) external returns (uint256 amountOutUint);
|
) external returns (uint256 amountOutUint);
|
||||||
|
|
||||||
/**
|
/// @notice Marginal price of `base` denominated in `quote` as Q64.64.
|
||||||
* @dev Initiate a flash loan.
|
/// @dev Returns the LMSR marginal price p_quote / p_base in ABDK 64.64 fixed-point format.
|
||||||
* @param receiver The receiver of the tokens in the loan, and the receiver of the callback.
|
/// Useful for off-chain quoting; raw 64.64 value is returned (no scaling to token units).
|
||||||
* @param token The loan currency.
|
/// @param baseTokenIndex index of the base asset (e.g., ETH)
|
||||||
* @param amount The amount of tokens lent.
|
/// @param quoteTokenIndex index of the quote asset (e.g., USD)
|
||||||
* @param data Arbitrary data structure, intended to contain user-defined parameters.
|
/// @return price Q64.64 value equal to quote per base (p_quote / p_base)
|
||||||
*/
|
function price(uint256 baseTokenIndex, uint256 quoteTokenIndex) external view returns (int128);
|
||||||
function flashLoan(
|
|
||||||
IERC3156FlashBorrower receiver,
|
/// @notice Price of one LP token denominated in `quote` as Q64.64.
|
||||||
address token,
|
/// @dev Computes LMSR poolPrice (quote per unit internal qTotal) and scales it to LP units:
|
||||||
uint256 amount,
|
/// returns price_per_LP = poolPrice_quote * (totalSupply() / qTotal) in ABDK 64.64 format.
|
||||||
|
/// The returned value is raw Q64.64 and represents quote units per one LP token unit.
|
||||||
|
/// @param quoteTokenIndex index of the quote asset in which to denominate the LP price
|
||||||
|
/// @return price Q64.64 value equal to quote per LP token unit
|
||||||
|
function poolPrice(uint256 quoteTokenIndex) external view returns (int128);
|
||||||
|
|
||||||
|
/// @notice Compute repayment amounts (principal + flash fee) for a proposed flash loan.
|
||||||
|
/// @param loanAmounts array of per-token loan amounts; must match the pool's token ordering.
|
||||||
|
/// @return repaymentAmounts array where repaymentAmounts[i] = loanAmounts[i] + ceil(loanAmounts[i] * flashFeePpm)
|
||||||
|
function flashRepaymentAmounts(uint256[] memory loanAmounts) external view
|
||||||
|
returns (uint256[] memory repaymentAmounts);
|
||||||
|
|
||||||
|
/// @notice Receive token amounts and require them to be repaid plus a fee inside a callback.
|
||||||
|
/// @dev The caller must implement IPartyFlashCallback#partyFlashCallback which receives (amounts, repaymentAmounts, data).
|
||||||
|
/// This function verifies that, after the callback returns, the pool's balances have increased by at least the fees
|
||||||
|
/// for each borrowed token. Reverts if repayment (including fee) did not occur.
|
||||||
|
/// @param recipient The address which will receive the token amounts
|
||||||
|
/// @param amounts The amount of each token to send (array length must equal pool size)
|
||||||
|
/// @param data Any data to be passed through to the callback
|
||||||
|
function flash(
|
||||||
|
address recipient,
|
||||||
|
uint256[] memory amounts,
|
||||||
bytes calldata data
|
bytes calldata data
|
||||||
) external returns (bool);
|
) external;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// SPDX-License-Identifier: UNLICENSED
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
pragma solidity ^0.8.30;
|
pragma solidity ^0.8.30;
|
||||||
|
|
||||||
import {ABDKMath64x64} from "../lib/abdk-libraries-solidity/ABDKMath64x64.sol";
|
import "@abdk/ABDKMath64x64.sol";
|
||||||
|
|
||||||
/// @notice Stabilized LMSR library with incremental exp(z) caching for gas efficiency.
|
/// @notice Stabilized LMSR library with incremental exp(z) caching for gas efficiency.
|
||||||
/// - Stores b (64.64), M (shift), Z = sum exp(z_i), z[i] = (q_i / b) - M
|
/// - Stores b (64.64), M (shift), Z = sum exp(z_i), z[i] = (q_i / b) - M
|
||||||
@@ -53,15 +53,8 @@ library LMSRStabilized {
|
|||||||
|
|
||||||
/// @notice Cost C(q) = b * (M + ln(Z))
|
/// @notice Cost C(q) = b * (M + ln(Z))
|
||||||
function cost(State storage s) internal view returns (int128) {
|
function cost(State storage s) internal view returns (int128) {
|
||||||
return cost(s.kappa, s.qInternal);
|
int128 b = _computeB(s);
|
||||||
}
|
(int128 M, int128 Z) = _computeMAndZ(b, s.qInternal);
|
||||||
|
|
||||||
/// @notice Pure version: Cost C(q) = b * (M + ln(Z))
|
|
||||||
function cost(int128 kappa, int128[] memory qInternal) internal pure returns (int128) {
|
|
||||||
int128 sizeMetric = _computeSizeMetric(qInternal);
|
|
||||||
require(sizeMetric > int128(0), "LMSR: size metric zero");
|
|
||||||
int128 b = kappa.mul(sizeMetric);
|
|
||||||
(int128 M, int128 Z) = _computeMAndZ(b, qInternal);
|
|
||||||
int128 lnZ = _ln(Z);
|
int128 lnZ = _ln(Z);
|
||||||
int128 inner = M.add(lnZ);
|
int128 inner = M.add(lnZ);
|
||||||
int128 c = b.mul(inner);
|
int128 c = b.mul(inner);
|
||||||
@@ -98,58 +91,23 @@ library LMSRStabilized {
|
|||||||
int128 a,
|
int128 a,
|
||||||
int128 limitPrice
|
int128 limitPrice
|
||||||
) internal view returns (int128 amountIn, int128 amountOut) {
|
) internal view returns (int128 amountIn, int128 amountOut) {
|
||||||
return swapAmountsForExactInput(s.nAssets, s.kappa, s.qInternal, i, j, a, limitPrice);
|
require(i < s.nAssets && j < s.nAssets, "LMSR: idx");
|
||||||
}
|
|
||||||
|
|
||||||
/// @notice Pure version: Closed-form asset-i -> asset-j amountOut in 64.64 fixed-point format (fee-free kernel)
|
|
||||||
/// Uses the closed-form two-asset LMSR formula (no fees in kernel):
|
|
||||||
/// y = b * ln(1 + r0 * (1 - exp(-a / b)))
|
|
||||||
/// where r0 = e_i / e_j.
|
|
||||||
///
|
|
||||||
/// This variant accepts an additional `limitPrice` (64.64) which represents the
|
|
||||||
/// maximum acceptable marginal price (p_i / p_j). If the marginal price would
|
|
||||||
/// exceed `limitPrice` before the requested `a` is fully consumed, the input
|
|
||||||
/// `a` is truncated to the value that makes the marginal price equal `limitPrice`.
|
|
||||||
///
|
|
||||||
/// NOTE: Kernel is fee-free; fees should be handled by the wrapper/token layer.
|
|
||||||
///
|
|
||||||
/// @param nAssets Number of assets in the pool
|
|
||||||
/// @param kappa Liquidity parameter κ (64.64 fixed point)
|
|
||||||
/// @param qInternal Cached internal balances in 64.64 fixed-point format
|
|
||||||
/// @param i Index of input asset
|
|
||||||
/// @param j Index of output asset
|
|
||||||
/// @param a Amount of input asset (in int128 format, 64.64 fixed-point)
|
|
||||||
/// @param limitPrice Maximum acceptable price ratio (64.64). If <= current price, this call reverts.
|
|
||||||
/// @return amountIn Actual amount of input asset used (may be less than `a` if limited by price)
|
|
||||||
/// @return amountOut Amount of output asset j in 64.64 fixed-point format
|
|
||||||
function swapAmountsForExactInput(
|
|
||||||
uint256 nAssets,
|
|
||||||
int128 kappa,
|
|
||||||
int128[] memory qInternal,
|
|
||||||
uint256 i,
|
|
||||||
uint256 j,
|
|
||||||
int128 a,
|
|
||||||
int128 limitPrice
|
|
||||||
) internal pure returns (int128 amountIn, int128 amountOut) {
|
|
||||||
require(i < nAssets && j < nAssets, "LMSR: idx");
|
|
||||||
|
|
||||||
// Initialize amountIn to full amount (will be adjusted if limit price is hit)
|
// Initialize amountIn to full amount (will be adjusted if limit price is hit)
|
||||||
amountIn = a;
|
amountIn = a;
|
||||||
|
|
||||||
// Compute b and ensure positivity before deriving invB
|
// Compute b and ensure positivity before deriving invB
|
||||||
int128 sizeMetric = _computeSizeMetric(qInternal);
|
int128 b = _computeB(s);
|
||||||
require(sizeMetric > int128(0), "LMSR: size metric zero");
|
|
||||||
int128 b = kappa.mul(sizeMetric);
|
|
||||||
require(b > int128(0), "LMSR: b<=0");
|
require(b > int128(0), "LMSR: b<=0");
|
||||||
|
|
||||||
// Precompute reciprocal of b to avoid repeated divisions
|
// Precompute reciprocal of b to avoid repeated divisions
|
||||||
int128 invB = ABDKMath64x64.div(ONE, b);
|
int128 invB = ABDKMath64x64.div(ONE, b);
|
||||||
|
|
||||||
// Guard: output asset must have non-zero effective weight to avoid degenerate/div-by-zero-like conditions
|
// Guard: output asset must have non-zero effective weight to avoid degenerate/div-by-zero-like conditions
|
||||||
require(qInternal[j] > int128(0), "LMSR: e_j==0");
|
require(s.qInternal[j] > int128(0), "LMSR: e_j==0");
|
||||||
|
|
||||||
// Compute r0 = exp((q_i - q_j) / b) directly using invB
|
// Compute r0 = exp((q_i - q_j) / b) directly using invB
|
||||||
int128 r0 = _exp(qInternal[i].sub(qInternal[j]).mul(invB));
|
int128 r0 = _exp(s.qInternal[i].sub(s.qInternal[j]).mul(invB));
|
||||||
require(r0 > int128(0), "LMSR: r0<=0"); // equivalent to e_j > 0 check
|
require(r0 > int128(0), "LMSR: r0<=0"); // equivalent to e_j > 0 check
|
||||||
|
|
||||||
// If a positive limitPrice is given, determine whether the full `a` would
|
// If a positive limitPrice is given, determine whether the full `a` would
|
||||||
@@ -159,12 +117,10 @@ library LMSRStabilized {
|
|||||||
// r0 must be positive; if r0 == 0 then no risk of exceeding limit by increasing r.
|
// r0 must be positive; if r0 == 0 then no risk of exceeding limit by increasing r.
|
||||||
require(r0 >= int128(0), "LMSR: r0<0");
|
require(r0 >= int128(0), "LMSR: r0<0");
|
||||||
if (r0 == int128(0)) {
|
if (r0 == int128(0)) {
|
||||||
// console2.log("r0 == 0 (input asset has zero weight), no limit truncation needed");
|
// console2.log("r0 == 0 (input asset has zero weight), no limit truncation needed");
|
||||||
} else {
|
} else {
|
||||||
// If limitPrice <= current price, we revert (caller must choose a limit > current price to allow any fill)
|
// If limitPrice <= current price, we revert (caller must choose a limit > current price to allow any fill)
|
||||||
if (limitPrice <= r0) {
|
require(limitPrice > r0, "LMSR: limitPrice <= current price");
|
||||||
revert("LMSR: limitPrice <= current price");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute a_limit directly from ln(limit / r0): a_limit = b * ln(limit / r0)
|
// Compute a_limit directly from ln(limit / r0): a_limit = b * ln(limit / r0)
|
||||||
int128 ratioLimitOverR0 = limitPrice.div(r0);
|
int128 ratioLimitOverR0 = limitPrice.div(r0);
|
||||||
@@ -180,7 +136,7 @@ library LMSRStabilized {
|
|||||||
amountIn = aLimit64; // Store the truncated input amount
|
amountIn = aLimit64; // Store the truncated input amount
|
||||||
a = aLimit64; // Use truncated amount for calculations
|
a = aLimit64; // Use truncated amount for calculations
|
||||||
} else {
|
} else {
|
||||||
// console2.log("Not truncating: aLimit64 >= a");
|
// console2.log("Not truncating: aLimit64 >= a");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -198,12 +154,12 @@ library LMSRStabilized {
|
|||||||
|
|
||||||
// If inner <= 0 then cap output to the current balance q_j (cannot withdraw more than q_j)
|
// If inner <= 0 then cap output to the current balance q_j (cannot withdraw more than q_j)
|
||||||
if (inner <= int128(0)) {
|
if (inner <= int128(0)) {
|
||||||
int128 qj64 = qInternal[j];
|
int128 qj64 = s.qInternal[j];
|
||||||
return (amountIn, qj64);
|
return (amountIn, qj64);
|
||||||
}
|
}
|
||||||
|
|
||||||
int128 lnInner = _ln(inner);
|
int128 lnInner = _ln(inner);
|
||||||
int128 b_lnInner = b.mul(lnInner);
|
int128 b_lnInner = b.mul(lnInner);
|
||||||
|
|
||||||
amountOut = b_lnInner;
|
amountOut = b_lnInner;
|
||||||
|
|
||||||
// Safety check
|
// Safety check
|
||||||
@@ -230,48 +186,21 @@ library LMSRStabilized {
|
|||||||
uint256 j,
|
uint256 j,
|
||||||
int128 limitPrice
|
int128 limitPrice
|
||||||
) internal view returns (int128 amountIn, int128 amountOut) {
|
) internal view returns (int128 amountIn, int128 amountOut) {
|
||||||
return swapAmountsForPriceLimit(s.nAssets, s.kappa, s.qInternal, i, j, limitPrice);
|
require(i < s.nAssets && j < s.nAssets, "LMSR: idx");
|
||||||
}
|
|
||||||
|
|
||||||
/// @notice Pure version: Maximum input/output pair possible when swapping from asset i to asset j
|
|
||||||
/// given a maximum acceptable price ratio (p_i/p_j).
|
|
||||||
/// Returns the input amount that would drive the marginal price to the limit (amountIn)
|
|
||||||
/// and the corresponding output amount (amountOut). If the output would exceed the
|
|
||||||
/// j-balance, amountOut is capped and amountIn is solved for the capped output.
|
|
||||||
///
|
|
||||||
/// @param nAssets Number of assets in the pool
|
|
||||||
/// @param kappa Liquidity parameter κ (64.64 fixed point)
|
|
||||||
/// @param qInternal Cached internal balances in 64.64 fixed-point format
|
|
||||||
/// @param i Index of input asset
|
|
||||||
/// @param j Index of output asset
|
|
||||||
/// @param limitPrice Maximum acceptable price ratio (64.64)
|
|
||||||
/// @return amountIn Maximum input amount in 64.64 fixed-point format that reaches the price limit
|
|
||||||
/// @return amountOut Corresponding maximum output amount in 64.64 fixed-point format
|
|
||||||
function swapAmountsForPriceLimit(
|
|
||||||
uint256 nAssets,
|
|
||||||
int128 kappa,
|
|
||||||
int128[] memory qInternal,
|
|
||||||
uint256 i,
|
|
||||||
uint256 j,
|
|
||||||
int128 limitPrice
|
|
||||||
) internal pure returns (int128 amountIn, int128 amountOut) {
|
|
||||||
require(i < nAssets && j < nAssets, "LMSR: idx");
|
|
||||||
require(limitPrice > int128(0), "LMSR: limitPrice <= 0");
|
require(limitPrice > int128(0), "LMSR: limitPrice <= 0");
|
||||||
|
|
||||||
// Compute b and ensure positivity before deriving invB
|
// Compute b and ensure positivity before deriving invB
|
||||||
int128 sizeMetric = _computeSizeMetric(qInternal);
|
int128 b = _computeB(s);
|
||||||
require(sizeMetric > int128(0), "LMSR: size metric zero");
|
|
||||||
int128 b = kappa.mul(sizeMetric);
|
|
||||||
require(b > int128(0), "LMSR: b<=0");
|
require(b > int128(0), "LMSR: b<=0");
|
||||||
|
|
||||||
// Precompute reciprocal of b to avoid repeated divisions
|
// Precompute reciprocal of b to avoid repeated divisions
|
||||||
int128 invB = ABDKMath64x64.div(ONE, b);
|
int128 invB = ABDKMath64x64.div(ONE, b);
|
||||||
|
|
||||||
// Guard: output asset must have non-zero effective weight to avoid degenerate/div-by-zero-like conditions
|
// Guard: output asset must have non-zero effective weight to avoid degenerate/div-by-zero-like conditions
|
||||||
require(qInternal[j] > int128(0), "LMSR: e_j==0");
|
require(s.qInternal[j] > int128(0), "LMSR: e_j==0");
|
||||||
|
|
||||||
// Compute r0 = exp((q_i - q_j) / b) directly using invB
|
// Compute r0 = exp((q_i - q_j) / b) directly using invB
|
||||||
int128 r0 = _exp(qInternal[i].sub(qInternal[j]).mul(invB));
|
int128 r0 = _exp(s.qInternal[i].sub(s.qInternal[j]).mul(invB));
|
||||||
|
|
||||||
// Mirror swapAmountsForExactInput behavior: treat invalid r0 as an error condition.
|
// Mirror swapAmountsForExactInput behavior: treat invalid r0 as an error condition.
|
||||||
// Revert if r0 is non-positive (no finite trade under a price limit).
|
// Revert if r0 is non-positive (no finite trade under a price limit).
|
||||||
@@ -295,12 +224,15 @@ library LMSRStabilized {
|
|||||||
// The maximum output y corresponding to that input:
|
// The maximum output y corresponding to that input:
|
||||||
// y = b * ln(1 + (e_i/e_j) * (1 - exp(-x_max/b)))
|
// y = b * ln(1 + (e_i/e_j) * (1 - exp(-x_max/b)))
|
||||||
int128 expTerm = ONE.sub(_exp(maxDeltaExponent.neg()));
|
int128 expTerm = ONE.sub(_exp(maxDeltaExponent.neg()));
|
||||||
|
|
||||||
int128 innerTerm = r0.mul(expTerm);
|
int128 innerTerm = r0.mul(expTerm);
|
||||||
|
|
||||||
int128 lnTerm = _ln(ONE.add(innerTerm));
|
int128 lnTerm = _ln(ONE.add(innerTerm));
|
||||||
|
|
||||||
int128 maxOutput = b.mul(lnTerm);
|
int128 maxOutput = b.mul(lnTerm);
|
||||||
|
|
||||||
// Current balance of asset j (in 64.64)
|
// Current balance of asset j (in 64.64)
|
||||||
int128 qj64 = qInternal[j];
|
int128 qj64 = s.qInternal[j];
|
||||||
|
|
||||||
// Initialize outputs to the computed maxima
|
// Initialize outputs to the computed maxima
|
||||||
amountIn = amountInMax;
|
amountIn = amountInMax;
|
||||||
@@ -340,45 +272,20 @@ library LMSRStabilized {
|
|||||||
uint256 i,
|
uint256 i,
|
||||||
int128 a
|
int128 a
|
||||||
) internal view returns (int128 amountIn, int128 amountOut) {
|
) internal view returns (int128 amountIn, int128 amountOut) {
|
||||||
return swapAmountsForMint(s.nAssets, s.kappa, s.qInternal, i, a);
|
require(i < s.nAssets, "LMSR: idx");
|
||||||
}
|
|
||||||
|
|
||||||
/// @notice Pure version: Compute LP-size increase when minting from a single-token input using bisection only.
|
|
||||||
/// @dev Solve for α >= 0 such that:
|
|
||||||
/// a = α*q_i + sum_{j != i} x_j(α)
|
|
||||||
/// where x_j(α) is the input to swap i->j that yields y_j = α*q_j and
|
|
||||||
/// x_j = b * ln( r0_j / (r0_j + 1 - exp(y_j / b)) ), r0_j = exp((q_i - q_j)/b).
|
|
||||||
/// Bisection is used (no Newton) to keep implementation compact and gas-friendly.
|
|
||||||
/// @param nAssets Number of assets in the pool
|
|
||||||
/// @param kappa Liquidity parameter κ (64.64 fixed point)
|
|
||||||
/// @param qInternal Cached internal balances in 64.64 fixed-point format
|
|
||||||
/// @param i Index of input asset
|
|
||||||
/// @param a Amount of input asset (in int128 format, 64.64 fixed-point)
|
|
||||||
/// @return amountIn Actual amount of input consumed
|
|
||||||
/// @return amountOut LP size-metric increase (alpha * S)
|
|
||||||
function swapAmountsForMint(
|
|
||||||
uint256 nAssets,
|
|
||||||
int128 kappa,
|
|
||||||
int128[] memory qInternal,
|
|
||||||
uint256 i,
|
|
||||||
int128 a
|
|
||||||
) internal pure returns (int128 amountIn, int128 amountOut) {
|
|
||||||
require(i < nAssets, "LMSR: idx");
|
|
||||||
require(a > int128(0), "LMSR: amount <= 0");
|
require(a > int128(0), "LMSR: amount <= 0");
|
||||||
|
|
||||||
int128 sizeMetric = _computeSizeMetric(qInternal);
|
int128 b = _computeB(s);
|
||||||
require(sizeMetric > int128(0), "LMSR: size metric zero");
|
|
||||||
int128 b = kappa.mul(sizeMetric);
|
|
||||||
require(b > int128(0), "LMSR: b<=0");
|
require(b > int128(0), "LMSR: b<=0");
|
||||||
int128 invB = ABDKMath64x64.div(ONE, b);
|
int128 invB = ABDKMath64x64.div(ONE, b);
|
||||||
int128 S = sizeMetric;
|
int128 S = _computeSizeMetric(s.qInternal);
|
||||||
|
|
||||||
uint256 n = nAssets;
|
uint256 n = s.nAssets;
|
||||||
|
|
||||||
// Precompute r0_j = exp((q_i - q_j) / b) for all j to avoid recomputing during search.
|
// Precompute r0_j = exp((q_i - q_j) / b) for all j to avoid recomputing during search.
|
||||||
int128[] memory r0 = new int128[](n);
|
int128[] memory r0 = new int128[](n);
|
||||||
for (uint256 j = 0; j < n; ) {
|
for (uint256 j = 0; j < n; ) {
|
||||||
r0[j] = _exp(qInternal[i].sub(qInternal[j]).mul(invB));
|
r0[j] = _exp(s.qInternal[i].sub(s.qInternal[j]).mul(invB));
|
||||||
unchecked { j++; }
|
unchecked { j++; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,7 +321,7 @@ library LMSRStabilized {
|
|||||||
// loop j != i
|
// loop j != i
|
||||||
for (uint256 j = 0; j < n; ) {
|
for (uint256 j = 0; j < n; ) {
|
||||||
if (j != i) {
|
if (j != i) {
|
||||||
int128 yj = alpha.mul(qInternal[j]); // target output y_j = alpha * q_j
|
int128 yj = alpha.mul(s.qInternal[j]); // target output y_j = alpha * q_j
|
||||||
if (yj > int128(0)) {
|
if (yj > int128(0)) {
|
||||||
int128 expArg = yj.mul(invB);
|
int128 expArg = yj.mul(invB);
|
||||||
// Guard exp arg
|
// Guard exp arg
|
||||||
@@ -432,7 +339,7 @@ library LMSRStabilized {
|
|||||||
unchecked { j++; }
|
unchecked { j++; }
|
||||||
}
|
}
|
||||||
|
|
||||||
int128 aReq = fail ? int128(type(int128).max) : alpha.mul(qInternal[i]).add(sumX);
|
int128 aReq = fail ? int128(type(int128).max) : alpha.mul(s.qInternal[i]).add(sumX);
|
||||||
|
|
||||||
if (aReq >= a || high >= alphaCap) {
|
if (aReq >= a || high >= alphaCap) {
|
||||||
break;
|
break;
|
||||||
@@ -454,7 +361,7 @@ library LMSRStabilized {
|
|||||||
|
|
||||||
for (uint256 j = 0; j < n; ) {
|
for (uint256 j = 0; j < n; ) {
|
||||||
if (j != i) {
|
if (j != i) {
|
||||||
int128 yj = alpha.mul(qInternal[j]);
|
int128 yj = alpha.mul(s.qInternal[j]);
|
||||||
if (yj > int128(0)) {
|
if (yj > int128(0)) {
|
||||||
int128 expArg = yj.mul(invB);
|
int128 expArg = yj.mul(invB);
|
||||||
if (expArg > EXP_LIMIT) { fail = true; break; }
|
if (expArg > EXP_LIMIT) { fail = true; break; }
|
||||||
@@ -471,7 +378,7 @@ library LMSRStabilized {
|
|||||||
unchecked { j++; }
|
unchecked { j++; }
|
||||||
}
|
}
|
||||||
|
|
||||||
int128 aReq = fail ? int128(type(int128).max) : alpha.mul(qInternal[i]).add(sumX);
|
int128 aReq = fail ? int128(type(int128).max) : alpha.mul(s.qInternal[i]).add(sumX);
|
||||||
|
|
||||||
if (aReq > a) {
|
if (aReq > a) {
|
||||||
// mid requires more input than provided -> decrease alpha
|
// mid requires more input than provided -> decrease alpha
|
||||||
@@ -501,7 +408,7 @@ library LMSRStabilized {
|
|||||||
bool failFinal = false;
|
bool failFinal = false;
|
||||||
for (uint256 j = 0; j < n; ) {
|
for (uint256 j = 0; j < n; ) {
|
||||||
if (j != i) {
|
if (j != i) {
|
||||||
int128 yj = alphaFinal.mul(qInternal[j]);
|
int128 yj = alphaFinal.mul(s.qInternal[j]);
|
||||||
if (yj > int128(0)) {
|
if (yj > int128(0)) {
|
||||||
int128 expArg = yj.mul(invB);
|
int128 expArg = yj.mul(invB);
|
||||||
if (expArg > EXP_LIMIT) { failFinal = true; break; }
|
if (expArg > EXP_LIMIT) { failFinal = true; break; }
|
||||||
@@ -523,7 +430,7 @@ library LMSRStabilized {
|
|||||||
return (int128(0), int128(0));
|
return (int128(0), int128(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
int128 aRequired = alphaFinal.mul(qInternal[i]).add(sumXFinal);
|
int128 aRequired = alphaFinal.mul(s.qInternal[i]).add(sumXFinal);
|
||||||
|
|
||||||
// amountIn is actual consumed input (may be <= provided a)
|
// amountIn is actual consumed input (may be <= provided a)
|
||||||
amountIn = aRequired;
|
amountIn = aRequired;
|
||||||
@@ -552,56 +459,28 @@ library LMSRStabilized {
|
|||||||
uint256 i,
|
uint256 i,
|
||||||
int128 alpha
|
int128 alpha
|
||||||
) internal view returns (int128 amountOut, int128 amountIn) {
|
) internal view returns (int128 amountOut, int128 amountIn) {
|
||||||
return swapAmountsForBurn(s.nAssets, s.kappa, s.qInternal, i, alpha);
|
require(i < s.nAssets, "LMSR: idx");
|
||||||
}
|
|
||||||
|
|
||||||
/// @notice Pure version: Compute single-asset payout when burning a proportional share alpha of the pool.
|
|
||||||
/// @dev Simulate q_after = (1 - alpha) * q, return the amount of asset `i` the burner
|
|
||||||
/// would receive after swapping each other asset's withdrawn portion into `i`.
|
|
||||||
/// For each j != i:
|
|
||||||
/// - wrapper holds a_j = alpha * q_j
|
|
||||||
/// - swap j->i with closed-form exact-input formula using the current q_local
|
|
||||||
/// - cap output to q_local[i] when necessary (solve inverse for input used)
|
|
||||||
/// Treat any per-asset rhs<=0 as "this asset contributes zero" (do not revert).
|
|
||||||
/// Revert only if the final single-asset payout is zero.
|
|
||||||
/// @param nAssets Number of assets in the pool
|
|
||||||
/// @param kappa Liquidity parameter κ (64.64 fixed point)
|
|
||||||
/// @param qInternal Cached internal balances in 64.64 fixed-point format
|
|
||||||
/// @param i Index of output asset
|
|
||||||
/// @param alpha Proportional share to burn (0 < alpha <= 1)
|
|
||||||
/// @return amountOut Amount of asset i received (in 64.64 fixed-point)
|
|
||||||
/// @return amountIn LP size-metric redeemed (alpha * S)
|
|
||||||
function swapAmountsForBurn(
|
|
||||||
uint256 nAssets,
|
|
||||||
int128 kappa,
|
|
||||||
int128[] memory qInternal,
|
|
||||||
uint256 i,
|
|
||||||
int128 alpha
|
|
||||||
) internal pure returns (int128 amountOut, int128 amountIn) {
|
|
||||||
require(i < nAssets, "LMSR: idx");
|
|
||||||
require(alpha > int128(0) && alpha <= ONE, "LMSR: alpha");
|
require(alpha > int128(0) && alpha <= ONE, "LMSR: alpha");
|
||||||
|
|
||||||
int128 sizeMetric = _computeSizeMetric(qInternal);
|
int128 b = _computeB(s);
|
||||||
require(sizeMetric > int128(0), "LMSR: size metric zero");
|
|
||||||
int128 b = kappa.mul(sizeMetric);
|
|
||||||
require(b > int128(0), "LMSR: b<=0");
|
require(b > int128(0), "LMSR: b<=0");
|
||||||
int128 invB = ABDKMath64x64.div(ONE, b);
|
int128 invB = ABDKMath64x64.div(ONE, b);
|
||||||
|
|
||||||
uint256 n = nAssets;
|
uint256 n = s.nAssets;
|
||||||
|
|
||||||
// Size metric and burned size (amountIn returned)
|
// Size metric and burned size (amountIn returned)
|
||||||
int128 S = sizeMetric;
|
int128 S = _computeSizeMetric(s.qInternal);
|
||||||
amountIn = alpha.mul(S); // total size-metric redeemed
|
amountIn = alpha.mul(S); // total size-metric redeemed
|
||||||
|
|
||||||
// Build q_local := q_after_burn = (1 - alpha) * q
|
// Build q_local := q_after_burn = (1 - alpha) * q
|
||||||
int128[] memory qLocal = new int128[](n);
|
int128[] memory qLocal = new int128[](n);
|
||||||
for (uint256 j = 0; j < n; ) {
|
for (uint256 j = 0; j < n; ) {
|
||||||
qLocal[j] = qInternal[j].mul(ONE.sub(alpha));
|
qLocal[j] = s.qInternal[j].mul(ONE.sub(alpha));
|
||||||
unchecked { j++; }
|
unchecked { j++; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start totalOut with direct portion of asset i redeemed
|
// Start totalOut with direct portion of asset i redeemed
|
||||||
int128 totalOut = alpha.mul(qInternal[i]);
|
int128 totalOut = alpha.mul(s.qInternal[i]);
|
||||||
|
|
||||||
// Track whether any non-zero contribution was produced
|
// Track whether any non-zero contribution was produced
|
||||||
bool anyNonZero = (totalOut > int128(0));
|
bool anyNonZero = (totalOut > int128(0));
|
||||||
@@ -609,7 +488,7 @@ library LMSRStabilized {
|
|||||||
// For each asset j != i, swap the withdrawn a_j := alpha * q_j into i
|
// For each asset j != i, swap the withdrawn a_j := alpha * q_j into i
|
||||||
for (uint256 j = 0; j < n; ) {
|
for (uint256 j = 0; j < n; ) {
|
||||||
if (j != i) {
|
if (j != i) {
|
||||||
int128 aj = alpha.mul(qInternal[j]); // wrapper-held withdrawn amount of j
|
int128 aj = alpha.mul(s.qInternal[j]); // wrapper-held withdrawn amount of j
|
||||||
if (aj > int128(0)) {
|
if (aj > int128(0)) {
|
||||||
// expArg = aj / b
|
// expArg = aj / b
|
||||||
int128 expArg = aj.mul(invB);
|
int128 expArg = aj.mul(invB);
|
||||||
@@ -716,7 +595,6 @@ library LMSRStabilized {
|
|||||||
|
|
||||||
// Compute new total for validation
|
// Compute new total for validation
|
||||||
int128 newTotal = _computeSizeMetric(newQInternal);
|
int128 newTotal = _computeSizeMetric(newQInternal);
|
||||||
|
|
||||||
require(newTotal > int128(0), "LMSR: new total zero");
|
require(newTotal > int128(0), "LMSR: new total zero");
|
||||||
|
|
||||||
// Update the cached qInternal with new values
|
// Update the cached qInternal with new values
|
||||||
@@ -724,30 +602,20 @@ library LMSRStabilized {
|
|||||||
s.qInternal[i] = newQInternal[i];
|
s.qInternal[i] = newQInternal[i];
|
||||||
unchecked { i++; }
|
unchecked { i++; }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Price-share of asset i: exp(z_i) / Z (64.64)
|
/// @notice Price-share of asset i: exp(z_i) / Z (64.64)
|
||||||
function priceShare(State storage s, uint256 i) internal view returns (int128) {
|
function priceShare(State storage s, uint256 i) internal view returns (int128) {
|
||||||
return priceShare(s.kappa, s.qInternal, i);
|
int128 b = _computeB(s);
|
||||||
}
|
uint len = s.qInternal.length;
|
||||||
|
|
||||||
/// @notice Pure version: Price-share of asset i: exp(z_i) / Z (64.64)
|
|
||||||
/// @param kappa Liquidity parameter κ (64.64 fixed point)
|
|
||||||
/// @param qInternal Cached internal balances in 64.64 fixed-point format
|
|
||||||
/// @param i Index of asset
|
|
||||||
/// @return Price share in 64.64 fixed-point format
|
|
||||||
function priceShare(int128 kappa, int128[] memory qInternal, uint256 i) internal pure returns (int128) {
|
|
||||||
int128 sizeMetric = _computeSizeMetric(qInternal);
|
|
||||||
require(sizeMetric > int128(0), "LMSR: size metric zero");
|
|
||||||
int128 b = kappa.mul(sizeMetric);
|
|
||||||
uint len = qInternal.length;
|
|
||||||
require(len > 0, "LMSR: no assets");
|
require(len > 0, "LMSR: no assets");
|
||||||
|
|
||||||
// Precompute reciprocal of b and perform a single pass that tracks M, Z, and e_i
|
// Precompute reciprocal of b and perform a single pass that tracks M, Z, and e_i
|
||||||
int128 invB = ABDKMath64x64.div(ONE, b);
|
int128 invB = ABDKMath64x64.div(ONE, b);
|
||||||
|
|
||||||
// Initialize from the first element
|
// Initialize from the first element
|
||||||
int128 M = qInternal[0].mul(invB);
|
int128 M = s.qInternal[0].mul(invB);
|
||||||
int128 Z = ONE; // exp(0)
|
int128 Z = ONE; // exp(0)
|
||||||
int128 e_i_acc;
|
int128 e_i_acc;
|
||||||
bool setEi;
|
bool setEi;
|
||||||
@@ -758,7 +626,7 @@ library LMSRStabilized {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (uint idx = 1; idx < len; ) {
|
for (uint idx = 1; idx < len; ) {
|
||||||
int128 yi = qInternal[idx].mul(invB);
|
int128 yi = s.qInternal[idx].mul(invB);
|
||||||
if (yi <= M) {
|
if (yi <= M) {
|
||||||
// Add contribution under current center
|
// Add contribution under current center
|
||||||
int128 term = _exp(yi.sub(M));
|
int128 term = _exp(yi.sub(M));
|
||||||
@@ -787,7 +655,7 @@ library LMSRStabilized {
|
|||||||
if (!setEi) {
|
if (!setEi) {
|
||||||
// Only possible when len == 1 and i != 0, guarded by caller invariants typically
|
// Only possible when len == 1 and i != 0, guarded by caller invariants typically
|
||||||
// Fallback: compute directly (kept for completeness)
|
// Fallback: compute directly (kept for completeness)
|
||||||
int128 yi = qInternal[i].mul(invB);
|
int128 yi = s.qInternal[i].mul(invB);
|
||||||
e_i_acc = _exp(yi.sub(M));
|
e_i_acc = _exp(yi.sub(M));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -797,54 +665,27 @@ library LMSRStabilized {
|
|||||||
/// @notice Marginal price of `base` in terms of `quote` (p_quote / p_base) as Q64.64
|
/// @notice Marginal price of `base` in terms of `quote` (p_quote / p_base) as Q64.64
|
||||||
/// @dev Returns exp((q_quote - q_base) / b). Indices must be valid and b > 0.
|
/// @dev Returns exp((q_quote - q_base) / b). Indices must be valid and b > 0.
|
||||||
function price(State storage s, uint256 baseTokenIndex, uint256 quoteTokenIndex) internal view returns (int128) {
|
function price(State storage s, uint256 baseTokenIndex, uint256 quoteTokenIndex) internal view returns (int128) {
|
||||||
return price(s.nAssets, s.kappa, s.qInternal, baseTokenIndex, quoteTokenIndex);
|
require(baseTokenIndex < s.nAssets && quoteTokenIndex < s.nAssets, "LMSR: idx");
|
||||||
}
|
int128 b = _computeB(s);
|
||||||
|
|
||||||
/// @notice Pure version: Marginal price of `base` in terms of `quote` (p_quote / p_base) as Q64.64
|
|
||||||
/// @dev Returns exp((q_quote - q_base) / b). Indices must be valid and b > 0.
|
|
||||||
/// @param nAssets Number of assets in the pool
|
|
||||||
/// @param kappa Liquidity parameter κ (64.64 fixed point)
|
|
||||||
/// @param qInternal Cached internal balances in 64.64 fixed-point format
|
|
||||||
/// @param baseTokenIndex Index of base token
|
|
||||||
/// @param quoteTokenIndex Index of quote token
|
|
||||||
/// @return Price in 64.64 fixed-point format
|
|
||||||
function price(uint256 nAssets, int128 kappa, int128[] memory qInternal, uint256 baseTokenIndex, uint256 quoteTokenIndex) internal pure returns (int128) {
|
|
||||||
require(baseTokenIndex < nAssets && quoteTokenIndex < nAssets, "LMSR: idx");
|
|
||||||
int128 sizeMetric = _computeSizeMetric(qInternal);
|
|
||||||
require(sizeMetric > int128(0), "LMSR: size metric zero");
|
|
||||||
int128 b = kappa.mul(sizeMetric);
|
|
||||||
require(b > int128(0), "LMSR: b<=0");
|
require(b > int128(0), "LMSR: b<=0");
|
||||||
|
|
||||||
// Use reciprocal of b to avoid repeated divisions
|
// Use reciprocal of b to avoid repeated divisions
|
||||||
int128 invB = ABDKMath64x64.div(ONE, b);
|
int128 invB = ABDKMath64x64.div(ONE, b);
|
||||||
|
|
||||||
// Marginal price p_quote / p_base = exp((q_quote - q_base) / b)
|
// Marginal price p_quote / p_base = exp((q_quote - q_base) / b)
|
||||||
return _exp(qInternal[quoteTokenIndex].sub(qInternal[baseTokenIndex]).mul(invB));
|
return _exp(s.qInternal[quoteTokenIndex].sub(s.qInternal[baseTokenIndex]).mul(invB));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Price of one unit of the LP size-metric (S = sum q_i) denominated in `quote` asset (Q64.64)
|
/// @notice Price of one unit of the LP size-metric (S = sum q_i) denominated in `quote` asset (Q64.64)
|
||||||
/// @dev Computes: poolPrice_quote = (1 / S) * sum_j q_j * exp((q_j - q_quote) / b)
|
/// @dev Computes: poolPrice_quote = (1 / S) * sum_j q_j * exp((q_j - q_quote) / b)
|
||||||
function poolPrice(State storage s, uint256 quoteTokenIndex) internal view returns (int128) {
|
function poolPrice(State storage s, uint256 quoteTokenIndex) internal view returns (int128) {
|
||||||
return poolPrice(s.nAssets, s.kappa, s.qInternal, quoteTokenIndex);
|
require(quoteTokenIndex < s.nAssets, "LMSR: idx");
|
||||||
}
|
|
||||||
|
|
||||||
/// @notice Pure version: Price of one unit of the LP size-metric (S = sum q_i) denominated in `quote` asset (Q64.64)
|
|
||||||
/// @dev Computes: poolPrice_quote = (1 / S) * sum_j q_j * exp((q_j - q_quote) / b)
|
|
||||||
/// @param nAssets Number of assets in the pool
|
|
||||||
/// @param kappa Liquidity parameter κ (64.64 fixed point)
|
|
||||||
/// @param qInternal Cached internal balances in 64.64 fixed-point format
|
|
||||||
/// @param quoteTokenIndex Index of quote token
|
|
||||||
/// @return Pool price in 64.64 fixed-point format
|
|
||||||
function poolPrice(uint256 nAssets, int128 kappa, int128[] memory qInternal, uint256 quoteTokenIndex) internal pure returns (int128) {
|
|
||||||
require(quoteTokenIndex < nAssets, "LMSR: idx");
|
|
||||||
// Compute b and ensure positivity
|
// Compute b and ensure positivity
|
||||||
int128 sizeMetric = _computeSizeMetric(qInternal);
|
int128 b = _computeB(s);
|
||||||
require(sizeMetric > int128(0), "LMSR: size metric zero");
|
|
||||||
int128 b = kappa.mul(sizeMetric);
|
|
||||||
require(b > int128(0), "LMSR: b<=0");
|
require(b > int128(0), "LMSR: b<=0");
|
||||||
|
|
||||||
// Compute total size metric S = sum q_i
|
// Compute total size metric S = sum q_i
|
||||||
int128 S = sizeMetric;
|
int128 S = _computeSizeMetric(s.qInternal);
|
||||||
require(S > int128(0), "LMSR: size zero");
|
require(S > int128(0), "LMSR: size zero");
|
||||||
|
|
||||||
// Precompute reciprocal of b
|
// Precompute reciprocal of b
|
||||||
@@ -852,12 +693,12 @@ library LMSRStabilized {
|
|||||||
|
|
||||||
// Accumulate weighted exponentials: sum_j q_j * exp((q_j - q_quote) / b)
|
// Accumulate weighted exponentials: sum_j q_j * exp((q_j - q_quote) / b)
|
||||||
int128 acc = int128(0);
|
int128 acc = int128(0);
|
||||||
uint256 n = nAssets;
|
uint256 n = s.nAssets;
|
||||||
for (uint256 j = 0; j < n; ) {
|
for (uint256 j = 0; j < n; ) {
|
||||||
// factor = exp((q_j - q_quote) / b)
|
// factor = exp((q_j - q_quote) / b)
|
||||||
int128 factor = _exp(qInternal[j].sub(qInternal[quoteTokenIndex]).mul(invB));
|
int128 factor = _exp(s.qInternal[j].sub(s.qInternal[quoteTokenIndex]).mul(invB));
|
||||||
// term = q_j * factor
|
// term = q_j * factor
|
||||||
int128 term = qInternal[j].mul(factor);
|
int128 term = s.qInternal[j].mul(factor);
|
||||||
acc = acc.add(term);
|
acc = acc.add(term);
|
||||||
unchecked { j++; }
|
unchecked { j++; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// SPDX-License-Identifier: UNLICENSED
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
pragma solidity ^0.8.30;
|
pragma solidity ^0.8.30;
|
||||||
|
|
||||||
import {ABDKMath64x64} from "../lib/abdk-libraries-solidity/ABDKMath64x64.sol";
|
import "@abdk/ABDKMath64x64.sol";
|
||||||
import {LMSRStabilized} from "./LMSRStabilized.sol";
|
import "./LMSRStabilized.sol";
|
||||||
|
|
||||||
/// @notice Specialized functions for the 2-asset stablecoin case
|
/// @notice Specialized functions for the 2-asset stablecoin case
|
||||||
library LMSRStabilizedBalancedPair {
|
library LMSRStabilizedBalancedPair {
|
||||||
@@ -104,7 +104,7 @@ library LMSRStabilizedBalancedPair {
|
|||||||
if (aLimit64 < a) {
|
if (aLimit64 < a) {
|
||||||
a = aLimit64;
|
a = aLimit64;
|
||||||
} else {
|
} else {
|
||||||
// console2.log("balanced2: limitPrice does not truncate input");
|
// console2.log("balanced2: limitPrice does not truncate input");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: after potential truncation we continue with the polynomial approximation below
|
// Note: after potential truncation we continue with the polynomial approximation below
|
||||||
|
|||||||
@@ -1,75 +1,28 @@
|
|||||||
// SPDX-License-Identifier: UNLICENSED
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
pragma solidity ^0.8.30;
|
pragma solidity ^0.8.30;
|
||||||
|
|
||||||
import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
|
import "./IPartyPlanner.sol";
|
||||||
import {SafeERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
|
import "./LMSRStabilized.sol";
|
||||||
import {IPartyPlanner} from "./IPartyPlanner.sol";
|
import "./PartyPool.sol";
|
||||||
import {LMSRStabilized} from "./LMSRStabilized.sol";
|
import "./PartyPoolBalancedPair.sol";
|
||||||
import {IPartyPool} from "./IPartyPool.sol";
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||||
import {PartyPoolMintImpl} from "./PartyPoolMintImpl.sol";
|
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||||
import {PartyPoolSwapImpl} from "./PartyPoolSwapImpl.sol";
|
|
||||||
import {IPartyPoolDeployer} from "./PartyPoolDeployer.sol";
|
|
||||||
|
|
||||||
/// @title PartyPlanner
|
/// @title PartyPlanner
|
||||||
/// @notice Factory contract for creating and tracking PartyPool instances
|
/// @notice Factory contract for creating and tracking PartyPool instances
|
||||||
contract PartyPlanner is IPartyPlanner {
|
contract PartyPlanner is IPartyPlanner {
|
||||||
using SafeERC20 for IERC20;
|
using SafeERC20 for IERC20;
|
||||||
int128 private constant ONE = int128(1) << 64;
|
int128 private constant FIXED_ONE_64x64 = int128(1) << 64;
|
||||||
|
|
||||||
/// @notice Address of the Mint implementation contract used by all pools created by this factory
|
|
||||||
PartyPoolMintImpl private immutable MINT_IMPL;
|
|
||||||
function mintImpl() external view returns (PartyPoolMintImpl) { return MINT_IMPL; }
|
|
||||||
|
|
||||||
/// @notice Address of the SwapMint implementation contract used by all pools created by this factory
|
|
||||||
PartyPoolSwapImpl private immutable SWAP_MINT_IMPL;
|
|
||||||
function swapMintImpl() external view returns (PartyPoolSwapImpl) { return SWAP_MINT_IMPL; }
|
|
||||||
|
|
||||||
/// @notice Protocol fee share (ppm) applied to fees collected by pools created by this planner
|
|
||||||
uint256 private immutable PROTOCOL_FEE_PPM;
|
|
||||||
function protocolFeePpm() external view returns (uint256) { return PROTOCOL_FEE_PPM; }
|
|
||||||
|
|
||||||
/// @notice Address to receive protocol fees for pools created by this planner (may be address(0))
|
|
||||||
address private immutable PROTOCOL_FEE_ADDRESS;
|
|
||||||
function protocolFeeAddress() external view returns (address) { return PROTOCOL_FEE_ADDRESS; }
|
|
||||||
|
|
||||||
IPartyPoolDeployer private immutable NORMAL_POOL_DEPLOYER;
|
|
||||||
IPartyPoolDeployer private immutable BALANCED_PAIR_DEPLOYER;
|
|
||||||
|
|
||||||
// On-chain pool indexing
|
// On-chain pool indexing
|
||||||
IPartyPool[] private _allPools;
|
PartyPool[] private _allPools;
|
||||||
IERC20[] private _allTokens;
|
IERC20[] private _allTokens;
|
||||||
mapping(IPartyPool => bool) private _poolSupported;
|
mapping(PartyPool => bool) private _poolSupported;
|
||||||
mapping(IERC20 => bool) private _tokenSupported;
|
mapping(IERC20 => bool) private _tokenSupported;
|
||||||
mapping(IERC20 => IPartyPool[]) private _poolsByToken;
|
mapping(IERC20 => PartyPool[]) private _poolsByToken;
|
||||||
|
|
||||||
/// @param _swapMintImpl address of the SwapMint implementation contract to be used by all pools
|
/// Main createPool variant: accepts kappa directly (preferred).
|
||||||
/// @param _mintImpl address of the Mint implementation contract to be used by all pools
|
function createPool(
|
||||||
/// @param _protocolFeePpm protocol fee share (ppm) to be used for pools created by this planner
|
|
||||||
/// @param _protocolFeeAddress recipient address for protocol fees for pools created by this planner (may be address(0))
|
|
||||||
constructor(
|
|
||||||
PartyPoolSwapImpl _swapMintImpl,
|
|
||||||
PartyPoolMintImpl _mintImpl,
|
|
||||||
IPartyPoolDeployer _deployer,
|
|
||||||
IPartyPoolDeployer _balancedPairDeployer,
|
|
||||||
uint256 _protocolFeePpm,
|
|
||||||
address _protocolFeeAddress
|
|
||||||
) {
|
|
||||||
require(address(_swapMintImpl) != address(0), "Planner: swapMintImpl address cannot be zero");
|
|
||||||
SWAP_MINT_IMPL = _swapMintImpl;
|
|
||||||
require(address(_mintImpl) != address(0), "Planner: mintImpl address cannot be zero");
|
|
||||||
MINT_IMPL = _mintImpl;
|
|
||||||
require(address(_deployer) != address(0), "Planner: deployer address cannot be zero");
|
|
||||||
NORMAL_POOL_DEPLOYER = _deployer;
|
|
||||||
require(address(_balancedPairDeployer) != address(0), "Planner: balanced pair deployer address cannot be zero");
|
|
||||||
BALANCED_PAIR_DEPLOYER = _balancedPairDeployer;
|
|
||||||
|
|
||||||
require(_protocolFeePpm < 1_000_000, "Planner: protocol fee >= ppm");
|
|
||||||
PROTOCOL_FEE_PPM = _protocolFeePpm;
|
|
||||||
PROTOCOL_FEE_ADDRESS = _protocolFeeAddress;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Main newPool variant: accepts kappa directly (preferred).
|
|
||||||
function newPool(
|
|
||||||
// Pool constructor args
|
// Pool constructor args
|
||||||
string memory name_,
|
string memory name_,
|
||||||
string memory symbol_,
|
string memory symbol_,
|
||||||
@@ -85,7 +38,7 @@ contract PartyPlanner is IPartyPlanner {
|
|||||||
uint256[] memory initialDeposits,
|
uint256[] memory initialDeposits,
|
||||||
uint256 initialLpAmount,
|
uint256 initialLpAmount,
|
||||||
uint256 deadline
|
uint256 deadline
|
||||||
) public returns (IPartyPool pool, uint256 lpAmount) {
|
) public returns (PartyPool pool, uint256 lpAmount) {
|
||||||
// Validate inputs
|
// Validate inputs
|
||||||
require(deadline == 0 || block.timestamp <= deadline, "Planner: deadline exceeded");
|
require(deadline == 0 || block.timestamp <= deadline, "Planner: deadline exceeded");
|
||||||
require(_tokens.length == initialDeposits.length, "Planner: tokens and deposits length mismatch");
|
require(_tokens.length == initialDeposits.length, "Planner: tokens and deposits length mismatch");
|
||||||
@@ -96,20 +49,9 @@ contract PartyPlanner is IPartyPlanner {
|
|||||||
require(_kappa > int128(0), "Planner: kappa must be > 0");
|
require(_kappa > int128(0), "Planner: kappa must be > 0");
|
||||||
|
|
||||||
// Create a new PartyPool instance (kappa-based constructor)
|
// Create a new PartyPool instance (kappa-based constructor)
|
||||||
IPartyPoolDeployer deployer = _stable && _tokens.length == 2 ? BALANCED_PAIR_DEPLOYER : NORMAL_POOL_DEPLOYER;
|
pool = _stable && _tokens.length == 2 ?
|
||||||
pool = deployer.deploy(
|
new PartyPoolBalancedPair(name_, symbol_, _tokens, _bases, _kappa, _swapFeePpm, _flashFeePpm) :
|
||||||
name_,
|
new PartyPool(name_, symbol_, _tokens, _bases, _kappa, _swapFeePpm, _flashFeePpm);
|
||||||
symbol_,
|
|
||||||
_tokens,
|
|
||||||
_bases,
|
|
||||||
_kappa,
|
|
||||||
_swapFeePpm,
|
|
||||||
_flashFeePpm,
|
|
||||||
PROTOCOL_FEE_PPM,
|
|
||||||
PROTOCOL_FEE_ADDRESS,
|
|
||||||
PartyPoolSwapImpl(SWAP_MINT_IMPL),
|
|
||||||
MINT_IMPL
|
|
||||||
);
|
|
||||||
|
|
||||||
_allPools.push(pool);
|
_allPools.push(pool);
|
||||||
_poolSupported[pool] = true;
|
_poolSupported[pool] = true;
|
||||||
@@ -134,7 +76,6 @@ contract PartyPlanner is IPartyPlanner {
|
|||||||
for (uint256 i = 0; i < _tokens.length; i++) {
|
for (uint256 i = 0; i < _tokens.length; i++) {
|
||||||
if (initialDeposits[i] > 0) {
|
if (initialDeposits[i] > 0) {
|
||||||
IERC20(_tokens[i]).safeTransferFrom(payer, address(pool), initialDeposits[i]);
|
IERC20(_tokens[i]).safeTransferFrom(payer, address(pool), initialDeposits[i]);
|
||||||
require(IERC20(_tokens[i]).balanceOf(address(pool)) == initialDeposits[i], 'fee-on-transfer tokens not supported');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,10 +83,8 @@ contract PartyPlanner is IPartyPlanner {
|
|||||||
lpAmount = pool.initialMint(receiver, initialLpAmount);
|
lpAmount = pool.initialMint(receiver, initialLpAmount);
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE that the slippage target is only exactly achieved in completely balanced pools where all assets are
|
/// Backwards-compatible convenience overload: compute kappa from (tradeFrac, targetSlippage) then call kappa-based createPool.
|
||||||
// priced the same. This target is actually a minimum slippage that the pool imposes on traders, and the actual
|
function createPool(
|
||||||
// slippage cost can be multiples bigger in practice due to pool inventory imbalances.
|
|
||||||
function newPool(
|
|
||||||
// Pool constructor args (old signature)
|
// Pool constructor args (old signature)
|
||||||
string memory name_,
|
string memory name_,
|
||||||
string memory symbol_,
|
string memory symbol_,
|
||||||
@@ -162,16 +101,16 @@ contract PartyPlanner is IPartyPlanner {
|
|||||||
uint256[] memory initialDeposits,
|
uint256[] memory initialDeposits,
|
||||||
uint256 initialLpAmount,
|
uint256 initialLpAmount,
|
||||||
uint256 deadline
|
uint256 deadline
|
||||||
) external returns (IPartyPool pool, uint256 lpAmount) {
|
) external returns (PartyPool pool, uint256 lpAmount) {
|
||||||
// Validate fixed-point fractions: must be less than 1.0 in 64.64 fixed-point
|
// Validate fixed-point fractions: must be less than 1.0 in 64.64 fixed-point
|
||||||
require(_tradeFrac < ONE, "Planner: tradeFrac must be < 1 (64.64)");
|
require(_tradeFrac < FIXED_ONE_64x64, "Planner: tradeFrac must be < 1 (64.64)");
|
||||||
require(_targetSlippage < ONE, "Planner: targetSlippage must be < 1 (64.64)");
|
require(_targetSlippage < FIXED_ONE_64x64, "Planner: targetSlippage must be < 1 (64.64)");
|
||||||
|
|
||||||
// Compute kappa from slippage params using LMSR helper (kappa depends only on n, f and s)
|
// Compute kappa from slippage params using LMSR helper (kappa depends only on n, f and s)
|
||||||
int128 computedKappa = LMSRStabilized.computeKappaFromSlippage(_tokens.length, _tradeFrac, _targetSlippage);
|
int128 computedKappa = LMSRStabilized.computeKappaFromSlippage(_tokens.length, _tradeFrac, _targetSlippage);
|
||||||
|
|
||||||
// Delegate to the kappa-based newPool variant
|
// Delegate to the kappa-based createPool variant
|
||||||
return newPool(
|
return createPool(
|
||||||
name_,
|
name_,
|
||||||
symbol_,
|
symbol_,
|
||||||
_tokens,
|
_tokens,
|
||||||
@@ -190,7 +129,7 @@ contract PartyPlanner is IPartyPlanner {
|
|||||||
|
|
||||||
/// @inheritdoc IPartyPlanner
|
/// @inheritdoc IPartyPlanner
|
||||||
function getPoolSupported(address pool) external view returns (bool) {
|
function getPoolSupported(address pool) external view returns (bool) {
|
||||||
return _poolSupported[IPartyPool(pool)];
|
return _poolSupported[PartyPool(pool)];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @inheritdoc IPartyPlanner
|
/// @inheritdoc IPartyPlanner
|
||||||
@@ -199,19 +138,19 @@ contract PartyPlanner is IPartyPlanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// @inheritdoc IPartyPlanner
|
/// @inheritdoc IPartyPlanner
|
||||||
function getAllPools(uint256 offset, uint256 limit) external view returns (IPartyPool[] memory pools) {
|
function getAllPools(uint256 offset, uint256 limit) external view returns (PartyPool[] memory pools) {
|
||||||
uint256 totalPools = _allPools.length;
|
uint256 totalPools = _allPools.length;
|
||||||
|
|
||||||
// If offset is beyond array bounds, return empty array
|
// If offset is beyond array bounds, return empty array
|
||||||
if (offset >= totalPools) {
|
if (offset >= totalPools) {
|
||||||
return new IPartyPool[](0);
|
return new PartyPool[](0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate actual number of pools to return (respecting bounds)
|
// Calculate actual number of pools to return (respecting bounds)
|
||||||
uint256 itemsToReturn = (offset + limit > totalPools) ? (totalPools - offset) : limit;
|
uint256 itemsToReturn = (offset + limit > totalPools) ? (totalPools - offset) : limit;
|
||||||
|
|
||||||
// Create result array of appropriate size
|
// Create result array of appropriate size
|
||||||
pools = new IPartyPool[](itemsToReturn);
|
pools = new PartyPool[](itemsToReturn);
|
||||||
|
|
||||||
// Fill the result array
|
// Fill the result array
|
||||||
for (uint256 i = 0; i < itemsToReturn; i++) {
|
for (uint256 i = 0; i < itemsToReturn; i++) {
|
||||||
@@ -255,20 +194,20 @@ contract PartyPlanner is IPartyPlanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// @inheritdoc IPartyPlanner
|
/// @inheritdoc IPartyPlanner
|
||||||
function getPoolsByToken(IERC20 token, uint256 offset, uint256 limit) external view returns (IPartyPool[] memory pools) {
|
function getPoolsByToken(IERC20 token, uint256 offset, uint256 limit) external view returns (PartyPool[] memory pools) {
|
||||||
IPartyPool[] storage tokenPools = _poolsByToken[token];
|
PartyPool[] storage tokenPools = _poolsByToken[token];
|
||||||
uint256 totalPools = tokenPools.length;
|
uint256 totalPools = tokenPools.length;
|
||||||
|
|
||||||
// If offset is beyond array bounds, return empty array
|
// If offset is beyond array bounds, return empty array
|
||||||
if (offset >= totalPools) {
|
if (offset >= totalPools) {
|
||||||
return new IPartyPool[](0);
|
return new PartyPool[](0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate actual number of pools to return (respecting bounds)
|
// Calculate actual number of pools to return (respecting bounds)
|
||||||
uint256 itemsToReturn = (offset + limit > totalPools) ? (totalPools - offset) : limit;
|
uint256 itemsToReturn = (offset + limit > totalPools) ? (totalPools - offset) : limit;
|
||||||
|
|
||||||
// Create result array of appropriate size
|
// Create result array of appropriate size
|
||||||
pools = new IPartyPool[](itemsToReturn);
|
pools = new PartyPool[](itemsToReturn);
|
||||||
|
|
||||||
// Fill the result array
|
// Fill the result array
|
||||||
for (uint256 i = 0; i < itemsToReturn; i++) {
|
for (uint256 i = 0; i < itemsToReturn; i++) {
|
||||||
|
|||||||
@@ -1,22 +1,12 @@
|
|||||||
// SPDX-License-Identifier: UNLICENSED
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
pragma solidity ^0.8.30;
|
pragma solidity ^0.8.30;
|
||||||
|
|
||||||
import "../lib/openzeppelin-contracts/contracts/interfaces/IERC3156FlashBorrower.sol";
|
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||||
import {ABDKMath64x64} from "../lib/abdk-libraries-solidity/ABDKMath64x64.sol";
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||||
import {Address} from "../lib/openzeppelin-contracts/contracts/utils/Address.sol";
|
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
||||||
import {ERC20External} from "./ERC20External.sol";
|
import "./PoolLib.sol";
|
||||||
import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
|
import "./IPartyPool.sol";
|
||||||
import {IPartyFlashCallback} from "./IPartyFlashCallback.sol";
|
import {PoolBase} from "./PoolBase.sol";
|
||||||
import {IPartyPool} from "./IPartyPool.sol";
|
|
||||||
import {LMSRStabilizedBalancedPair} from "./LMSRStabilizedBalancedPair.sol";
|
|
||||||
import {LMSRStabilized} from "./LMSRStabilized.sol";
|
|
||||||
import {PartyPoolBase} from "./PartyPoolBase.sol";
|
|
||||||
import {PartyPoolMintImpl} from "./PartyPoolMintImpl.sol";
|
|
||||||
import {PartyPoolSwapImpl} from "./PartyPoolSwapImpl.sol";
|
|
||||||
import {Proxy} from "../lib/openzeppelin-contracts/contracts/proxy/Proxy.sol";
|
|
||||||
import {ReentrancyGuard} from "../lib/openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol";
|
|
||||||
import {SafeERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
||||||
import {IERC3156FlashLender} from "../lib/openzeppelin-contracts/contracts/interfaces/IERC3156FlashLender.sol";
|
|
||||||
|
|
||||||
/// @title PartyPool - LMSR-backed multi-asset pool with LP ERC20 token
|
/// @title PartyPool - LMSR-backed multi-asset pool with LP ERC20 token
|
||||||
/// @notice A multi-asset liquidity pool backed by the LMSRStabilized pricing model.
|
/// @notice A multi-asset liquidity pool backed by the LMSRStabilized pricing model.
|
||||||
@@ -27,63 +17,43 @@ import {IERC3156FlashLender} from "../lib/openzeppelin-contracts/contracts/inter
|
|||||||
/// - Exact-input swaps and swaps-to-price-limits,
|
/// - Exact-input swaps and swaps-to-price-limits,
|
||||||
/// - Flash loans via a callback interface.
|
/// - Flash loans via a callback interface.
|
||||||
///
|
///
|
||||||
/// @dev The contract stores per-token uint "bases" used to scale token units into the internal Q64.64
|
/// @dev The contract uses PoolLib for all implementation logic and maintains state in a PoolLib.State struct
|
||||||
/// representation used by the LMSR library. Cached on-chain uint balances are kept to reduce balanceOf calls.
|
contract PartyPool is PoolBase, IPartyPool {
|
||||||
/// The contract uses ceiling/floor rules described in function comments to bias rounding in favor of the pool
|
using PoolLib for PoolLib.State;
|
||||||
/// (i.e., floor outputs to users, ceil inputs/fees where appropriate).
|
|
||||||
contract PartyPool is PartyPoolBase, ERC20External, IPartyPool {
|
|
||||||
using ABDKMath64x64 for int128;
|
|
||||||
using LMSRStabilized for LMSRStabilized.State;
|
|
||||||
using SafeERC20 for IERC20;
|
|
||||||
|
|
||||||
/// @notice Liquidity parameter κ (Q64.64) used by the LMSR kernel: b = κ * S(q)
|
/// @notice Liquidity parameter κ (Q64.64) used by the LMSR kernel: b = κ * S(q)
|
||||||
/// @dev Pool is constructed with a fixed κ. Clients that previously passed tradeFrac/targetSlippage
|
/// @dev Pool is constructed with a fixed κ. Clients may use LMSRStabilized.computeKappaFromSlippage(...) to
|
||||||
/// should use LMSRStabilized.computeKappaFromSlippage(...) to derive κ and pass it here.
|
/// derive κ and pass it here.
|
||||||
int128 private immutable KAPPA; // kappa in Q64.64
|
int128 public immutable kappa; // kappa in Q64.64
|
||||||
function kappa() external view returns (int128) { return KAPPA; }
|
|
||||||
|
|
||||||
/// @notice Per-swap fee in parts-per-million (ppm). Fee is taken from input amounts before LMSR computations.
|
/// @notice Per-swap fee in parts-per-million (ppm). Fee is taken from input amounts before LMSR computations.
|
||||||
uint256 private immutable SWAP_FEE_PPM;
|
uint256 public immutable swapFeePpm;
|
||||||
function swapFeePpm() external view returns (uint256) { return SWAP_FEE_PPM; }
|
|
||||||
|
|
||||||
/// @notice Flash-loan fee in parts-per-million (ppm) applied to flash borrow amounts.
|
/// @notice Flash-loan fee in parts-per-million (ppm) applied to flash borrow amounts.
|
||||||
uint256 private immutable FLASH_FEE_PPM;
|
uint256 public immutable flashFeePpm;
|
||||||
function flashFeePpm() external view returns (uint256) { return FLASH_FEE_PPM; }
|
|
||||||
|
|
||||||
/// @notice Protocol fee share (ppm) applied to fees collected by the pool (floored when accrued)
|
|
||||||
uint256 private immutable PROTOCOL_FEE_PPM;
|
|
||||||
function protocolFeePpm() external view returns (uint256) { return PROTOCOL_FEE_PPM; }
|
|
||||||
|
|
||||||
/// @notice Address to which collected protocol tokens will be sent on collectProtocolFees()
|
|
||||||
address private immutable PROTOCOL_FEE_ADDRESS;
|
|
||||||
function protocolFeeAddress() external view returns (address) { return PROTOCOL_FEE_ADDRESS; }
|
|
||||||
|
|
||||||
// @inheritdoc IPartyPool
|
|
||||||
function allProtocolFeesOwed() external view returns (uint256[] memory) { return protocolFeesOwed; }
|
|
||||||
|
|
||||||
/// @notice Address of the Mint implementation contract for delegatecall
|
|
||||||
PartyPoolMintImpl private immutable MINT_IMPL;
|
|
||||||
function mintImpl() external view returns (PartyPoolMintImpl) { return MINT_IMPL; }
|
|
||||||
|
|
||||||
/// @notice Address of the SwapMint implementation contract for delegatecall
|
|
||||||
PartyPoolSwapImpl private immutable SWAP_IMPL;
|
|
||||||
function swapMintImpl() external view returns (PartyPoolSwapImpl) { return SWAP_IMPL; }
|
|
||||||
|
|
||||||
|
|
||||||
/// @inheritdoc IPartyPool
|
/// @inheritdoc IPartyPool
|
||||||
function getToken(uint256 i) external view returns (IERC20) { return tokens[i]; }
|
function tokens(uint256 i) external view returns (IERC20) { return s.tokens[i]; }
|
||||||
|
|
||||||
/// @inheritdoc IPartyPool
|
/// @inheritdoc IPartyPool
|
||||||
function numTokens() external view returns (uint256) { return tokens.length; }
|
function numTokens() external view returns (uint256) { return s.tokens.length; }
|
||||||
|
|
||||||
/// @inheritdoc IPartyPool
|
/// @inheritdoc IPartyPool
|
||||||
function allTokens() external view returns (IERC20[] memory) { return tokens; }
|
function allTokens() external view returns (IERC20[] memory) { return s.tokens; }
|
||||||
|
|
||||||
/// @inheritdoc IPartyPool
|
/// @inheritdoc IPartyPool
|
||||||
function denominators() external view returns (uint256[] memory) { return bases; }
|
function denominators() external view returns (uint256[] memory) { return s.bases; }
|
||||||
|
|
||||||
function LMSR() external view returns (LMSRStabilized.State memory) { return lmsr; }
|
/// @notice Mapping from token address => (index+1). A zero value indicates the token is not in the pool.
|
||||||
|
/// @dev Use index = tokenAddressToIndexPlusOne[token] - 1 when non-zero.
|
||||||
|
function tokenAddressToIndexPlusOne(IERC20 token) external view returns (uint256) {
|
||||||
|
return s.tokenAddressToIndexPlusOne[token];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Scale factor used when converting LMSR Q64.64 totals to LP token units (uint).
|
||||||
|
/// @dev LP tokens are minted in units equal to ABDK.mulu(lastTotalQ64x64, LP_SCALE).
|
||||||
|
uint256 public constant LP_SCALE = 1e18; // Scale used to convert LMSR lastTotal (Q64.64) into LP token units (uint)
|
||||||
|
|
||||||
/// @param name_ LP token name
|
/// @param name_ LP token name
|
||||||
/// @param symbol_ LP token symbol
|
/// @param symbol_ LP token symbol
|
||||||
@@ -92,8 +62,6 @@ contract PartyPool is PartyPoolBase, ERC20External, IPartyPool {
|
|||||||
/// @param kappa_ liquidity parameter κ (Q64.64) used to derive b = κ * S(q)
|
/// @param kappa_ liquidity parameter κ (Q64.64) used to derive b = κ * S(q)
|
||||||
/// @param swapFeePpm_ fee in parts-per-million, taken from swap input amounts before LMSR calculations
|
/// @param swapFeePpm_ fee in parts-per-million, taken from swap input amounts before LMSR calculations
|
||||||
/// @param flashFeePpm_ fee in parts-per-million, taken for flash loans
|
/// @param flashFeePpm_ fee in parts-per-million, taken for flash loans
|
||||||
/// @param swapImpl_ address of the SwapMint implementation contract
|
|
||||||
/// @param mintImpl_ address of the Mint implementation contract
|
|
||||||
constructor(
|
constructor(
|
||||||
string memory name_,
|
string memory name_,
|
||||||
string memory symbol_,
|
string memory symbol_,
|
||||||
@@ -101,114 +69,106 @@ contract PartyPool is PartyPoolBase, ERC20External, IPartyPool {
|
|||||||
uint256[] memory bases_,
|
uint256[] memory bases_,
|
||||||
int128 kappa_,
|
int128 kappa_,
|
||||||
uint256 swapFeePpm_,
|
uint256 swapFeePpm_,
|
||||||
uint256 flashFeePpm_,
|
uint256 flashFeePpm_
|
||||||
uint256 protocolFeePpm_, // NEW: protocol share of fees (ppm)
|
) ERC20(name_, symbol_) {
|
||||||
address protocolFeeAddress_, // NEW: recipient for collected protocol tokens
|
kappa = kappa_;
|
||||||
PartyPoolSwapImpl swapImpl_,
|
|
||||||
PartyPoolMintImpl mintImpl_
|
|
||||||
) ERC20External(name_, symbol_) {
|
|
||||||
require(tokens_.length > 1, "Pool: need >1 asset");
|
|
||||||
require(tokens_.length == bases_.length, "Pool: lengths mismatch");
|
|
||||||
tokens = tokens_;
|
|
||||||
bases = bases_;
|
|
||||||
KAPPA = kappa_;
|
|
||||||
require(swapFeePpm_ < 1_000_000, "Pool: fee >= ppm");
|
require(swapFeePpm_ < 1_000_000, "Pool: fee >= ppm");
|
||||||
SWAP_FEE_PPM = swapFeePpm_;
|
swapFeePpm = swapFeePpm_;
|
||||||
require(flashFeePpm_ < 1_000_000, "Pool: flash fee >= ppm");
|
require(flashFeePpm_ < 1_000_000, "Pool: flash fee >= ppm");
|
||||||
FLASH_FEE_PPM = flashFeePpm_;
|
flashFeePpm = flashFeePpm_;
|
||||||
require(protocolFeePpm_ < 1_000_000, "Pool: protocol fee >= ppm");
|
|
||||||
PROTOCOL_FEE_PPM = protocolFeePpm_;
|
|
||||||
PROTOCOL_FEE_ADDRESS = protocolFeeAddress_;
|
|
||||||
SWAP_IMPL = swapImpl_;
|
|
||||||
MINT_IMPL = mintImpl_;
|
|
||||||
|
|
||||||
uint256 n = tokens_.length;
|
// Initialize state using library
|
||||||
|
s.initialize(tokens_, bases_);
|
||||||
// Initialize LMSR state nAssets; full init occurs on first mint when quantities are known.
|
|
||||||
lmsr.nAssets = n;
|
|
||||||
|
|
||||||
// Initialize token address to index mapping
|
|
||||||
for (uint i = 0; i < n;) {
|
|
||||||
tokenAddressToIndexPlusOne[tokens_[i]] = i + 1;
|
|
||||||
unchecked {i++;}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize caches to zero and protocol ledger
|
|
||||||
cachedUintBalances = new uint256[](n);
|
|
||||||
protocolFeesOwed = new uint256[](n);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ----------------------
|
/* ----------------------
|
||||||
Initialization / Mint / Burn (LP token managed)
|
Initialization / Mint / Burn (LP token managed)
|
||||||
---------------------- */
|
---------------------- */
|
||||||
|
|
||||||
/// @inheritdoc IPartyPool
|
/// @inheritdoc IPartyPool
|
||||||
function initialMint(address receiver, uint256 lpTokens) external
|
function mintDepositAmounts(uint256 lpTokenAmount) public view returns (uint256[] memory depositAmounts) {
|
||||||
|
return s.mintDepositAmounts(lpTokenAmount, totalSupply());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Initial mint to set up pool for the first time.
|
||||||
|
/// @dev Assumes tokens have already been transferred to the pool prior to calling.
|
||||||
|
/// Can only be called when the pool is uninitialized (totalSupply() == 0 or lmsr.nAssets == 0).
|
||||||
|
/// @param receiver address that receives the LP tokens
|
||||||
|
/// @param lpTokens The number of LP tokens to issue for this mint. If 0, then the number of tokens returned will equal the LMSR internal q total
|
||||||
|
function initialMint(address receiver, uint256 lpTokens) external nonReentrant
|
||||||
returns (uint256 lpMinted) {
|
returns (uint256 lpMinted) {
|
||||||
bytes memory data = abi.encodeWithSignature(
|
lpMinted = s.initialMint(receiver, lpTokens, kappa, totalSupply());
|
||||||
"initialMint(address,uint256,int128)",
|
_mint(receiver, lpMinted);
|
||||||
receiver,
|
|
||||||
lpTokens,
|
|
||||||
KAPPA
|
|
||||||
);
|
|
||||||
bytes memory result = Address.functionDelegateCall(address(MINT_IMPL), data);
|
|
||||||
return abi.decode(result, (uint256));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Proportional mint for existing pool.
|
/// @notice Proportional mint for existing pool.
|
||||||
/// @dev This function forwards the call to the mint implementation via delegatecall
|
/// @dev Payer must approve the required token amounts before calling.
|
||||||
|
/// Can only be called when pool is already initialized (totalSupply() > 0 and lmsr.nAssets > 0).
|
||||||
|
/// Rounds follow the pool-favorable conventions documented in helpers (ceil inputs, floor outputs).
|
||||||
/// @param payer address that provides the input tokens
|
/// @param payer address that provides the input tokens
|
||||||
/// @param receiver address that receives the LP tokens
|
/// @param receiver address that receives the LP tokens
|
||||||
/// @param lpTokenAmount desired amount of LP tokens to mint
|
/// @param lpTokenAmount desired amount of LP tokens to mint
|
||||||
/// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore.
|
/// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore.
|
||||||
function mint(address payer, address receiver, uint256 lpTokenAmount, uint256 deadline) external
|
function mint(address payer, address receiver, uint256 lpTokenAmount, uint256 deadline) external nonReentrant
|
||||||
returns (uint256 lpMinted) {
|
returns (uint256 lpMinted) {
|
||||||
bytes memory data = abi.encodeWithSignature(
|
lpMinted = s.mint(payer, receiver, lpTokenAmount, deadline, totalSupply());
|
||||||
"mint(address,address,uint256,uint256)",
|
_mint(receiver, lpMinted);
|
||||||
payer,
|
}
|
||||||
receiver,
|
|
||||||
lpTokenAmount,
|
/// @inheritdoc IPartyPool
|
||||||
deadline
|
function burnReceiveAmounts(uint256 lpTokenAmount) external view returns (uint256[] memory withdrawAmounts) {
|
||||||
);
|
return s.burnReceiveAmounts(lpTokenAmount, totalSupply());
|
||||||
bytes memory result = Address.functionDelegateCall(address(MINT_IMPL), data);
|
|
||||||
return abi.decode(result, (uint256));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Burn LP tokens and withdraw the proportional basket to receiver.
|
/// @notice Burn LP tokens and withdraw the proportional basket to receiver.
|
||||||
/// @dev This function forwards the call to the burn implementation via delegatecall
|
/// @dev Payer must own or approve the LP tokens being burned. The function updates LMSR state
|
||||||
|
/// proportionally to reflect the reduced pool size after the withdrawal.
|
||||||
/// @param payer address that provides the LP tokens to burn
|
/// @param payer address that provides the LP tokens to burn
|
||||||
/// @param receiver address that receives the withdrawn tokens
|
/// @param receiver address that receives the withdrawn tokens
|
||||||
/// @param lpAmount amount of LP tokens to burn (proportional withdrawal)
|
/// @param lpAmount amount of LP tokens to burn (proportional withdrawal)
|
||||||
/// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore.
|
/// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore.
|
||||||
function burn(address payer, address receiver, uint256 lpAmount, uint256 deadline) external
|
function burn(address payer, address receiver, uint256 lpAmount, uint256 deadline) external nonReentrant {
|
||||||
returns (uint256[] memory withdrawAmounts) {
|
/* uint256[] memory withdrawAmounts = */ s.burn(payer, receiver, lpAmount, deadline, totalSupply(), balanceOf(payer));
|
||||||
bytes memory data = abi.encodeWithSignature(
|
|
||||||
"burn(address,address,uint256,uint256)",
|
// Handle LP token burning with allowance
|
||||||
payer,
|
if (msg.sender != payer) {
|
||||||
receiver,
|
uint256 allowed = allowance(payer, msg.sender);
|
||||||
lpAmount,
|
require(allowed >= lpAmount, "burn: allowance insufficient");
|
||||||
deadline
|
_approve(payer, msg.sender, allowed - lpAmount);
|
||||||
);
|
}
|
||||||
bytes memory result = Address.functionDelegateCall(address(MINT_IMPL), data);
|
_burn(payer, lpAmount);
|
||||||
return abi.decode(result, (uint256[]));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ----------------------
|
/* ----------------------
|
||||||
Swaps
|
Swaps
|
||||||
---------------------- */
|
---------------------- */
|
||||||
|
|
||||||
/*
|
/// @inheritdoc IPartyPool
|
||||||
function swapAmounts(
|
function swapAmounts(
|
||||||
uint256 inputTokenIndex,
|
uint256 inputTokenIndex,
|
||||||
uint256 outputTokenIndex,
|
uint256 outputTokenIndex,
|
||||||
uint256 maxAmountIn,
|
uint256 maxAmountIn,
|
||||||
int128 limitPrice
|
int128 limitPrice
|
||||||
) external view returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
|
) virtual external view returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
|
||||||
(uint256 grossIn, uint256 outUint,,,, uint256 feeUint) = _quoteSwapExactIn(inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice);
|
return s.swapAmounts(inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice, swapFeePpm);
|
||||||
return (grossIn, outUint, feeUint);
|
}
|
||||||
|
|
||||||
|
/// @inheritdoc IPartyPool
|
||||||
|
function swapToLimitAmounts(
|
||||||
|
uint256 inputTokenIndex,
|
||||||
|
uint256 outputTokenIndex,
|
||||||
|
int128 limitPrice
|
||||||
|
) external view returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
|
||||||
|
return s.swapToLimitAmounts(inputTokenIndex, outputTokenIndex, limitPrice, swapFeePpm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @inheritdoc IPartyPool
|
||||||
|
function swapMintAmounts(
|
||||||
|
uint256 inputTokenIndex,
|
||||||
|
uint256 maxAmountIn
|
||||||
|
) external view returns (uint256 totalTransfer, uint256 amountIn, uint256 fee, uint256 lpMinted) {
|
||||||
|
return s.swapMintAmounts(inputTokenIndex, maxAmountIn, swapFeePpm, totalSupply());
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
/// @inheritdoc IPartyPool
|
/// @inheritdoc IPartyPool
|
||||||
function swap(
|
function swap(
|
||||||
@@ -219,95 +179,14 @@ contract PartyPool is PartyPoolBase, ERC20External, IPartyPool {
|
|||||||
uint256 maxAmountIn,
|
uint256 maxAmountIn,
|
||||||
int128 limitPrice,
|
int128 limitPrice,
|
||||||
uint256 deadline
|
uint256 deadline
|
||||||
) external nonReentrant returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
|
) virtual external nonReentrant returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
|
||||||
require(deadline == 0 || block.timestamp <= deadline, "swap: deadline exceeded");
|
return s.swap(payer, receiver, inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice, deadline, swapFeePpm);
|
||||||
|
|
||||||
// Compute amounts using the same path as views
|
|
||||||
(uint256 totalTransferAmount, uint256 amountOutUint, int128 amountInInternalUsed, int128 amountOutInternal, , uint256 feeUint) =
|
|
||||||
_quoteSwapExactIn(inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice);
|
|
||||||
|
|
||||||
// Transfer tokens
|
|
||||||
tokens[inputTokenIndex].safeTransferFrom(payer, address(this), totalTransferAmount);
|
|
||||||
uint256 balIAfter = IERC20(tokens[inputTokenIndex]).balanceOf(address(this));
|
|
||||||
tokens[outputTokenIndex].safeTransfer(receiver, amountOutUint);
|
|
||||||
uint256 balJAfter = IERC20(tokens[outputTokenIndex]).balanceOf(address(this));
|
|
||||||
|
|
||||||
// Accrue protocol share (floor) from the fee on input token
|
|
||||||
if (PROTOCOL_FEE_PPM > 0 && feeUint > 0) {
|
|
||||||
uint256 protoShare = (feeUint * PROTOCOL_FEE_PPM) / 1_000_000; // floor
|
|
||||||
if (protoShare > 0) {
|
|
||||||
protocolFeesOwed[inputTokenIndex] += protoShare;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update cached uint balances for i and j using effective balances (onchain - owed)
|
|
||||||
_recordCachedBalance(inputTokenIndex, balIAfter);
|
|
||||||
_recordCachedBalance(outputTokenIndex, balJAfter);
|
|
||||||
|
|
||||||
// Apply swap to LMSR state with the internal amounts actually used
|
|
||||||
lmsr.applySwap(inputTokenIndex, outputTokenIndex, amountInInternalUsed, amountOutInternal);
|
|
||||||
|
|
||||||
emit Swap(payer, receiver, tokens[inputTokenIndex], tokens[outputTokenIndex], totalTransferAmount, amountOutUint);
|
|
||||||
|
|
||||||
return (totalTransferAmount, amountOutUint, feeUint);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Internal quote for exact-input swap that mirrors swap() rounding and fee application
|
/// @notice Swap up to the price limit; computes max input to reach limit then performs swap.
|
||||||
/// @dev Returns amounts consistent with swap() semantics: grossIn includes fees (ceil), amountOut is floored.
|
/// @dev If balances prevent fully reaching the limit, the function caps and returns actuals.
|
||||||
/// @return grossIn amount to transfer in (inclusive of fee), amountOutUint output amount (uint),
|
/// The payer must transfer the exact gross input computed by the view.
|
||||||
/// amountInInternalUsed and amountOutInternal (64.64), amountInUintNoFee input amount excluding fee (uint),
|
/// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore.
|
||||||
/// feeUint fee taken from the gross input (uint)
|
|
||||||
function _quoteSwapExactIn(
|
|
||||||
uint256 inputTokenIndex,
|
|
||||||
uint256 outputTokenIndex,
|
|
||||||
uint256 maxAmountIn,
|
|
||||||
int128 limitPrice
|
|
||||||
)
|
|
||||||
internal
|
|
||||||
view
|
|
||||||
returns (
|
|
||||||
uint256 grossIn,
|
|
||||||
uint256 amountOutUint,
|
|
||||||
int128 amountInInternalUsed,
|
|
||||||
int128 amountOutInternal,
|
|
||||||
uint256 amountInUintNoFee,
|
|
||||||
uint256 feeUint
|
|
||||||
)
|
|
||||||
{
|
|
||||||
uint256 n = tokens.length;
|
|
||||||
require(inputTokenIndex < n && outputTokenIndex < n, "swap: idx");
|
|
||||||
require(maxAmountIn > 0, "swap: input zero");
|
|
||||||
|
|
||||||
// Estimate max net input (fee on gross rounded up, then subtract)
|
|
||||||
(, uint256 netUintForSwap) = _computeFee(maxAmountIn, SWAP_FEE_PPM);
|
|
||||||
|
|
||||||
// Convert to internal (floor)
|
|
||||||
int128 deltaInternalI = _uintToInternalFloor(netUintForSwap, bases[inputTokenIndex]);
|
|
||||||
require(deltaInternalI > int128(0), "swap: input too small after fee");
|
|
||||||
|
|
||||||
// Compute internal amounts using LMSR (exact-input with price limit)
|
|
||||||
(amountInInternalUsed, amountOutInternal) = _swapAmountsForExactInput(inputTokenIndex, outputTokenIndex, deltaInternalI, limitPrice);
|
|
||||||
|
|
||||||
// Convert actual used input internal -> uint (ceil)
|
|
||||||
amountInUintNoFee = _internalToUintCeil(amountInInternalUsed, bases[inputTokenIndex]);
|
|
||||||
|
|
||||||
// Compute gross transfer including fee on the used input (ceil)
|
|
||||||
feeUint = 0;
|
|
||||||
grossIn = amountInUintNoFee;
|
|
||||||
if (SWAP_FEE_PPM > 0) {
|
|
||||||
feeUint = _ceilFee(amountInUintNoFee, SWAP_FEE_PPM);
|
|
||||||
grossIn += feeUint;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure within user max
|
|
||||||
require(grossIn <= maxAmountIn, "swap: transfer exceeds max");
|
|
||||||
|
|
||||||
// Compute output (floor)
|
|
||||||
amountOutUint = _internalToUintFloor(amountOutInternal, bases[outputTokenIndex]);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// @inheritdoc IPartyPool
|
|
||||||
function swapToLimit(
|
function swapToLimit(
|
||||||
address payer,
|
address payer,
|
||||||
address receiver,
|
address receiver,
|
||||||
@@ -316,24 +195,12 @@ contract PartyPool is PartyPoolBase, ERC20External, IPartyPool {
|
|||||||
int128 limitPrice,
|
int128 limitPrice,
|
||||||
uint256 deadline
|
uint256 deadline
|
||||||
) external returns (uint256 amountInUsed, uint256 amountOut, uint256 fee) {
|
) external returns (uint256 amountInUsed, uint256 amountOut, uint256 fee) {
|
||||||
bytes memory data = abi.encodeWithSignature(
|
return s.swapToLimit(payer, receiver, inputTokenIndex, outputTokenIndex, limitPrice, deadline, swapFeePpm);
|
||||||
'swapToLimit(address,address,uint256,uint256,int128,uint256,uint256,uint256)',
|
|
||||||
payer,
|
|
||||||
receiver,
|
|
||||||
inputTokenIndex,
|
|
||||||
outputTokenIndex,
|
|
||||||
limitPrice,
|
|
||||||
deadline,
|
|
||||||
SWAP_FEE_PPM,
|
|
||||||
PROTOCOL_FEE_PPM
|
|
||||||
);
|
|
||||||
bytes memory result = Address.functionDelegateCall(address(SWAP_IMPL), data);
|
|
||||||
return abi.decode(result, (uint256,uint256,uint256));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// @notice Single-token mint: deposit a single token, charge swap-LMSR cost, and mint LP.
|
/// @notice Single-token mint: deposit a single token, charge swap-LMSR cost, and mint LP.
|
||||||
/// @dev This function forwards the call to the swapMint implementation via delegatecall
|
/// @dev swapMint executes as an exact-in planned swap followed by proportional scaling of qInternal.
|
||||||
|
/// The function emits SwapMint (gross, net, fee) and also emits Mint for LP issuance.
|
||||||
/// @param payer who transfers the input token
|
/// @param payer who transfers the input token
|
||||||
/// @param receiver who receives the minted LP tokens
|
/// @param receiver who receives the minted LP tokens
|
||||||
/// @param inputTokenIndex index of the input token
|
/// @param inputTokenIndex index of the input token
|
||||||
@@ -346,24 +213,21 @@ contract PartyPool is PartyPoolBase, ERC20External, IPartyPool {
|
|||||||
uint256 inputTokenIndex,
|
uint256 inputTokenIndex,
|
||||||
uint256 maxAmountIn,
|
uint256 maxAmountIn,
|
||||||
uint256 deadline
|
uint256 deadline
|
||||||
) external returns (uint256 lpMinted) {
|
) external nonReentrant returns (uint256 lpMinted) {
|
||||||
bytes memory data = abi.encodeWithSignature(
|
lpMinted = s.swapMint(payer, receiver, inputTokenIndex, maxAmountIn, deadline, swapFeePpm, totalSupply());
|
||||||
"swapMint(address,address,uint256,uint256,uint256,uint256,uint256)",
|
_mint(receiver, lpMinted);
|
||||||
payer,
|
}
|
||||||
receiver,
|
|
||||||
inputTokenIndex,
|
|
||||||
maxAmountIn,
|
|
||||||
deadline,
|
|
||||||
SWAP_FEE_PPM,
|
|
||||||
PROTOCOL_FEE_PPM
|
|
||||||
);
|
|
||||||
|
|
||||||
bytes memory result = Address.functionDelegateCall(address(MINT_IMPL), data);
|
/// @inheritdoc IPartyPool
|
||||||
return abi.decode(result, (uint256));
|
function burnSwapAmounts(
|
||||||
|
uint256 lpAmount,
|
||||||
|
uint256 inputTokenIndex
|
||||||
|
) external view returns (uint256 amountOut) {
|
||||||
|
return s.burnSwapAmounts(lpAmount, inputTokenIndex, swapFeePpm, totalSupply());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Burn LP tokens then swap the redeemed proportional basket into a single asset `inputTokenIndex` and send to receiver.
|
/// @notice Burn LP tokens then swap the redeemed proportional basket into a single asset `inputTokenIndex` and send to receiver.
|
||||||
/// @dev This function forwards the call to the burnSwap implementation via delegatecall
|
/// @dev The function burns LP tokens (authorization via allowance if needed), sends the single-asset payout and updates LMSR state.
|
||||||
/// @param payer who burns LP tokens
|
/// @param payer who burns LP tokens
|
||||||
/// @param receiver who receives the single asset
|
/// @param receiver who receives the single asset
|
||||||
/// @param lpAmount amount of LP tokens to burn
|
/// @param lpAmount amount of LP tokens to burn
|
||||||
@@ -376,87 +240,49 @@ contract PartyPool is PartyPoolBase, ERC20External, IPartyPool {
|
|||||||
uint256 lpAmount,
|
uint256 lpAmount,
|
||||||
uint256 inputTokenIndex,
|
uint256 inputTokenIndex,
|
||||||
uint256 deadline
|
uint256 deadline
|
||||||
) external returns (uint256 amountOutUint) {
|
) external nonReentrant returns (uint256 amountOutUint) {
|
||||||
bytes memory data = abi.encodeWithSignature(
|
amountOutUint = s.burnSwap(payer, receiver, lpAmount, inputTokenIndex, deadline, swapFeePpm, totalSupply(), balanceOf(payer));
|
||||||
"burnSwap(address,address,uint256,uint256,uint256,uint256,uint256)",
|
|
||||||
payer,
|
|
||||||
receiver,
|
|
||||||
lpAmount,
|
|
||||||
inputTokenIndex,
|
|
||||||
deadline,
|
|
||||||
SWAP_FEE_PPM,
|
|
||||||
PROTOCOL_FEE_PPM
|
|
||||||
);
|
|
||||||
|
|
||||||
bytes memory result = Address.functionDelegateCall(address(MINT_IMPL), data);
|
// Handle LP token burning with allowance
|
||||||
return abi.decode(result, (uint256));
|
if (msg.sender != payer) {
|
||||||
|
uint256 allowed = allowance(payer, msg.sender);
|
||||||
|
require(allowed >= lpAmount, "burnSwap: allowance insufficient");
|
||||||
|
_approve(payer, msg.sender, allowed - lpAmount);
|
||||||
|
}
|
||||||
|
_burn(payer, lpAmount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// @inheritdoc IPartyPool
|
||||||
|
function flashRepaymentAmounts(uint256[] memory loanAmounts) external view
|
||||||
|
returns (uint256[] memory repaymentAmounts) {
|
||||||
|
return s.flashRepaymentAmounts(loanAmounts, flashFeePpm);
|
||||||
|
}
|
||||||
|
|
||||||
bytes32 internal constant FLASH_CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan");
|
/// @notice Receive token amounts and require them to be repaid plus a fee inside a callback.
|
||||||
|
/// @dev The caller must implement IPartyFlashCallback#partyFlashCallback which receives (amounts, repaymentAmounts, data).
|
||||||
/**
|
/// This function verifies that, after the callback returns, the pool's balances have increased by at least the fees
|
||||||
* @dev Loan `amount` tokens to `receiver`, and takes it back plus a `flashFee` after the callback.
|
/// for each borrowed token. Reverts if repayment (including fee) did not occur.
|
||||||
* @param receiver The contract receiving the tokens, needs to implement the `onFlashLoan(address user, uint256 amount, uint256 fee, bytes calldata)` interface.
|
/// @param recipient The address which will receive the token amounts
|
||||||
* @param tokenAddr The loan currency.
|
/// @param amounts The amount of each token to send (array length must equal pool size)
|
||||||
* @param amount The amount of tokens lent.
|
/// @param data Any data to be passed through to the callback
|
||||||
* @param data A data parameter to be passed on to the `receiver` for any custom use.
|
function flash(
|
||||||
*/
|
address recipient,
|
||||||
function flashLoan(
|
uint256[] memory amounts,
|
||||||
IERC3156FlashBorrower receiver,
|
|
||||||
address tokenAddr,
|
|
||||||
uint256 amount,
|
|
||||||
bytes calldata data
|
bytes calldata data
|
||||||
) external nonReentrant returns (bool)
|
) external nonReentrant {
|
||||||
{
|
s.flash(recipient, amounts, data, flashFeePpm);
|
||||||
IERC20 token = IERC20(tokenAddr);
|
|
||||||
require(amount <= token.balanceOf(address(this)));
|
|
||||||
uint256 tokenIndex = tokenAddressToIndexPlusOne[token] - 1;
|
|
||||||
(uint256 fee, ) = _computeFee(amount, FLASH_FEE_PPM);
|
|
||||||
|
|
||||||
// Compute protocol share of flash fee
|
|
||||||
if (PROTOCOL_FEE_PPM > 0 && fee > 0) {
|
|
||||||
uint256 protoShare = (fee * PROTOCOL_FEE_PPM) / 1_000_000; // floor
|
|
||||||
if (protoShare > 0) {
|
|
||||||
protocolFeesOwed[tokenIndex] += protoShare;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
require(token.transfer(address(receiver), amount));
|
|
||||||
require(receiver.onFlashLoan(msg.sender, address(token), amount, fee, data) == FLASH_CALLBACK_SUCCESS);
|
|
||||||
require(token.transferFrom(address(receiver), address(this), amount + fee));
|
|
||||||
|
|
||||||
// Update cached balance for the borrowed token
|
|
||||||
uint256 balAfter = token.balanceOf(address(this));
|
|
||||||
_recordCachedBalance(tokenIndex, balAfter);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// @notice Marginal price of `base` in terms of `quote` (p_quote / p_base) as Q64.64
|
||||||
/// @notice Transfer all protocol fees to the configured protocolFeeAddress and zero the ledger.
|
/// @dev Returns the LMSR marginal price directly (raw 64.64) for use by off-chain quoting logic.
|
||||||
/// @dev Anyone can call; must have protocolFeeAddress != address(0) to be operational.
|
function price(uint256 baseTokenIndex, uint256 quoteTokenIndex) external view returns (int128) {
|
||||||
function collectProtocolFees() external nonReentrant {
|
return s.price(baseTokenIndex, quoteTokenIndex);
|
||||||
address dest = PROTOCOL_FEE_ADDRESS;
|
|
||||||
require(dest != address(0), "collect: zero addr");
|
|
||||||
|
|
||||||
uint256 n = tokens.length;
|
|
||||||
for (uint256 i = 0; i < n; i++) {
|
|
||||||
uint256 owed = protocolFeesOwed[i];
|
|
||||||
if (owed == 0) continue;
|
|
||||||
uint256 bal = IERC20(tokens[i]).balanceOf(address(this));
|
|
||||||
require(bal >= owed, "collect: fee > bal");
|
|
||||||
protocolFeesOwed[i] = 0;
|
|
||||||
// transfer owed tokens to protocol destination
|
|
||||||
tokens[i].safeTransfer(dest, owed);
|
|
||||||
// update cached to effective onchain minus owed
|
|
||||||
cachedUintBalances[i] = bal - owed;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function _swapAmountsForExactInput(uint256 i, uint256 j, int128 a, int128 limitPrice) internal virtual view
|
/// @notice Price of one LP token denominated in `quote` asset as Q64.64
|
||||||
returns (int128 amountIn, int128 amountOut) {
|
/// @dev Computes LMSR poolPrice (quote per unit qTotal) and scales it by totalSupply() / qTotal
|
||||||
return lmsr.swapAmountsForExactInput(i, j, a, limitPrice);
|
/// to return price per LP token unit in quote asset (raw 64.64).
|
||||||
|
function poolPrice(uint256 quoteTokenIndex) external view returns (int128) {
|
||||||
|
return s.poolPrice(quoteTokenIndex, totalSupply());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,42 @@
|
|||||||
// SPDX-License-Identifier: UNLICENSED
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
pragma solidity ^0.8.30;
|
pragma solidity ^0.8.30;
|
||||||
|
|
||||||
import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
|
import "./PartyPool.sol";
|
||||||
import {LMSRStabilizedBalancedPair} from "./LMSRStabilizedBalancedPair.sol";
|
import {PoolLibBalancedPair} from "./PoolLibBalancedPair.sol";
|
||||||
import {PartyPool} from "./PartyPool.sol";
|
|
||||||
import {PartyPoolMintImpl} from "./PartyPoolMintImpl.sol";
|
|
||||||
import {PartyPoolSwapImpl} from "./PartyPoolSwapImpl.sol";
|
|
||||||
|
|
||||||
contract PartyPoolBalancedPair is PartyPool {
|
contract PartyPoolBalancedPair is PartyPool {
|
||||||
constructor(
|
constructor(
|
||||||
string memory name_,
|
string memory name_,
|
||||||
string memory symbol_,
|
string memory symbol_,
|
||||||
IERC20[] memory tokens_,
|
IERC20[] memory tokens_,
|
||||||
uint256[] memory bases_,
|
uint256[] memory bases_,
|
||||||
int128 kappa_,
|
int128 kappa_,
|
||||||
uint256 swapFeePpm_,
|
uint256 swapFeePpm_,
|
||||||
uint256 flashFeePpm_,
|
uint256 flashFeePpm_
|
||||||
uint256 protocolFeePpm_, // NEW: protocol share of fees (ppm)
|
) PartyPool(name_, symbol_, tokens_, bases_, kappa_, swapFeePpm_, flashFeePpm_) {
|
||||||
address protocolFeeAddress_, // NEW: recipient for collected protocol tokens
|
|
||||||
PartyPoolSwapImpl swapMintImpl_,
|
|
||||||
PartyPoolMintImpl mintImpl_
|
|
||||||
) PartyPool(name_, symbol_, tokens_, bases_, kappa_, swapFeePpm_, flashFeePpm_, protocolFeePpm_, protocolFeeAddress_, swapMintImpl_, mintImpl_)
|
|
||||||
{}
|
|
||||||
|
|
||||||
function _swapAmountsForExactInput(uint256 i, uint256 j, int128 a, int128 limitPrice) internal virtual override view
|
|
||||||
returns (int128 amountIn, int128 amountOut) {
|
|
||||||
return LMSRStabilizedBalancedPair.swapAmountsForExactInput(lmsr, i, j, a, limitPrice);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function swapAmounts(
|
||||||
|
uint256 inputTokenIndex,
|
||||||
|
uint256 outputTokenIndex,
|
||||||
|
uint256 maxAmountIn,
|
||||||
|
int128 limitPrice
|
||||||
|
) virtual override external view returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
|
||||||
|
return PoolLibBalancedPair.swapAmounts(
|
||||||
|
s, inputTokenIndex, outputTokenIndex,
|
||||||
|
maxAmountIn, limitPrice, swapFeePpm);
|
||||||
|
}
|
||||||
|
|
||||||
|
function swap(
|
||||||
|
address payer,
|
||||||
|
address receiver,
|
||||||
|
uint256 inputTokenIndex,
|
||||||
|
uint256 outputTokenIndex,
|
||||||
|
uint256 maxAmountIn,
|
||||||
|
int128 limitPrice,
|
||||||
|
uint256 deadline
|
||||||
|
) virtual override external nonReentrant returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
|
||||||
|
return PoolLibBalancedPair.swap(s, payer, receiver, inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice, deadline, swapFeePpm);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
// SPDX-License-Identifier: UNLICENSED
|
|
||||||
pragma solidity ^0.8.30;
|
|
||||||
|
|
||||||
import {ABDKMath64x64} from "../lib/abdk-libraries-solidity/ABDKMath64x64.sol";
|
|
||||||
import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
|
|
||||||
import {ReentrancyGuard} from "../lib/openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol";
|
|
||||||
import {ERC20Internal} from "./ERC20Internal.sol";
|
|
||||||
import {LMSRStabilized} from "./LMSRStabilized.sol";
|
|
||||||
import {PartyPoolHelpers} from "./PartyPoolHelpers.sol";
|
|
||||||
|
|
||||||
/// @notice Abstract base contract that contains storage and internal helpers only.
|
|
||||||
/// No external/public functions or constructor here — derived implementations own immutables and constructors.
|
|
||||||
abstract contract PartyPoolBase is ERC20Internal, ReentrancyGuard, PartyPoolHelpers {
|
|
||||||
using ABDKMath64x64 for int128;
|
|
||||||
using LMSRStabilized for LMSRStabilized.State;
|
|
||||||
|
|
||||||
//
|
|
||||||
// Internal state (no immutables here; immutables belong to derived contracts)
|
|
||||||
//
|
|
||||||
|
|
||||||
// LMSR internal state
|
|
||||||
LMSRStabilized.State internal lmsr;
|
|
||||||
|
|
||||||
/// @notice Scale factor used when converting LMSR Q64.64 totals to LP token units (uint).
|
|
||||||
/// @dev LP tokens are minted in units equal to ABDK.mulu(lastTotalQ64x64, LP_SCALE).
|
|
||||||
uint256 internal constant LP_SCALE = 1e18; // Scale used to convert LMSR lastTotal (Q64.64) into LP token units (uint)
|
|
||||||
|
|
||||||
/// @notice Token addresses comprising the pool. Effectively immutable after construction.
|
|
||||||
/// @dev tokens[i] corresponds to the i-th asset and maps to index i in the internal LMSR arrays.
|
|
||||||
IERC20[] internal tokens; // effectively immutable since there is no interface to change the tokens
|
|
||||||
|
|
||||||
/// @notice Amounts of token owed as protocol fees but not yet collected. Subtract this amount from the pool's token
|
|
||||||
/// balances to compute the tokens owned by LP's.
|
|
||||||
uint256[] internal protocolFeesOwed;
|
|
||||||
|
|
||||||
/// @notice Per-token uint base denominators used to convert uint token amounts <-> internal Q64.64 representation.
|
|
||||||
/// @dev denominators()[i] is the base for tokens[i]. These bases are chosen by deployer and must match token decimals.
|
|
||||||
uint256[] internal bases; // per-token uint base used to scale token amounts <-> internal
|
|
||||||
|
|
||||||
/// @notice Mapping from token address => (index+1). A zero value indicates the token is not in the pool.
|
|
||||||
/// @dev Use index = tokenAddressToIndexPlusOne[token] - 1 when non-zero.
|
|
||||||
mapping(IERC20=>uint) internal tokenAddressToIndexPlusOne; // Uses index+1 so a result of 0 indicates a failed lookup
|
|
||||||
|
|
||||||
// Cached on-chain balances (uint) and internal 64.64 representation
|
|
||||||
// balance / base = internal
|
|
||||||
uint256[] internal cachedUintBalances;
|
|
||||||
|
|
||||||
|
|
||||||
/* ----------------------
|
|
||||||
Conversion & fee helpers (internal)
|
|
||||||
---------------------- */
|
|
||||||
|
|
||||||
// Convert uint token amount -> internal 64.64 (floor). Uses ABDKMath64x64.divu which truncates.
|
|
||||||
function _uintToInternalFloor(uint256 amount, uint256 base) internal pure returns (int128) {
|
|
||||||
// internal = amount / base (as Q64.64)
|
|
||||||
return ABDKMath64x64.divu(amount, base);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert internal 64.64 -> uint token amount (floor). Uses ABDKMath64x64.mulu which floors the product.
|
|
||||||
function _internalToUintFloor(int128 internalAmount, uint256 base) internal pure returns (uint256) {
|
|
||||||
// uint = internal * base (floored)
|
|
||||||
return ABDKMath64x64.mulu(internalAmount, base);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert internal 64.64 -> uint token amount (ceiling). Rounds up to protect the pool.
|
|
||||||
function _internalToUintCeil(int128 internalAmount, uint256 base) internal pure returns (uint256) {
|
|
||||||
// Get the floor value first
|
|
||||||
uint256 floorValue = ABDKMath64x64.mulu(internalAmount, base);
|
|
||||||
|
|
||||||
// Check if there was any fractional part by comparing to a reconstruction of the original
|
|
||||||
int128 reconstructed = ABDKMath64x64.divu(floorValue, base);
|
|
||||||
|
|
||||||
// If reconstructed is less than original, there was a fractional part that was truncated
|
|
||||||
if (reconstructed < internalAmount) {
|
|
||||||
return floorValue + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return floorValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @dev Helper to record cached balances as effectiveBalance = onchain - owed. Reverts if owed > onchain.
|
|
||||||
function _recordCachedBalance(uint256 idx, uint256 onchainBal) internal {
|
|
||||||
uint256 owed = protocolFeesOwed[idx];
|
|
||||||
require(onchainBal >= owed, "balance < protocol owed");
|
|
||||||
cachedUintBalances[idx] = onchainBal - owed;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
// SPDX-License-Identifier: UNLICENSED
|
|
||||||
pragma solidity ^0.8.30;
|
|
||||||
|
|
||||||
import "./PartyPoolMintImpl.sol";
|
|
||||||
import "./PartyPoolSwapImpl.sol";
|
|
||||||
import {PartyPool} from "./PartyPool.sol";
|
|
||||||
import {PartyPoolBalancedPair} from "./PartyPoolBalancedPair.sol";
|
|
||||||
|
|
||||||
// This pattern is needed because the PartyPlanner constructs two different types of pools (regular and balanced-pair)
|
|
||||||
// but doesn't have room to store the initialization code of both contracts. Therefore, we delegate pool construction.
|
|
||||||
|
|
||||||
interface IPartyPoolDeployer {
|
|
||||||
function deploy(
|
|
||||||
string memory name_,
|
|
||||||
string memory symbol_,
|
|
||||||
IERC20[] memory tokens_,
|
|
||||||
uint256[] memory bases_,
|
|
||||||
int128 kappa_,
|
|
||||||
uint256 swapFeePpm_,
|
|
||||||
uint256 flashFeePpm_,
|
|
||||||
uint256 protocolFeePpm_,
|
|
||||||
address protocolFeeAddress_,
|
|
||||||
PartyPoolSwapImpl swapImpl_,
|
|
||||||
PartyPoolMintImpl mintImpl_
|
|
||||||
) external returns (IPartyPool pool);
|
|
||||||
}
|
|
||||||
|
|
||||||
contract PartyPoolDeployer is IPartyPoolDeployer {
|
|
||||||
function deploy(
|
|
||||||
string memory name_,
|
|
||||||
string memory symbol_,
|
|
||||||
IERC20[] memory tokens_,
|
|
||||||
uint256[] memory bases_,
|
|
||||||
int128 kappa_,
|
|
||||||
uint256 swapFeePpm_,
|
|
||||||
uint256 flashFeePpm_,
|
|
||||||
uint256 protocolFeePpm_,
|
|
||||||
address protocolFeeAddress_,
|
|
||||||
PartyPoolSwapImpl swapImpl_,
|
|
||||||
PartyPoolMintImpl mintImpl_
|
|
||||||
) external returns (IPartyPool) {
|
|
||||||
return new PartyPool(
|
|
||||||
name_,
|
|
||||||
symbol_,
|
|
||||||
tokens_,
|
|
||||||
bases_,
|
|
||||||
kappa_,
|
|
||||||
swapFeePpm_,
|
|
||||||
flashFeePpm_,
|
|
||||||
protocolFeePpm_,
|
|
||||||
protocolFeeAddress_,
|
|
||||||
swapImpl_,
|
|
||||||
mintImpl_
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
contract PartyPoolBalancedPairDeployer is IPartyPoolDeployer {
|
|
||||||
function deploy(
|
|
||||||
string memory name_,
|
|
||||||
string memory symbol_,
|
|
||||||
IERC20[] memory tokens_,
|
|
||||||
uint256[] memory bases_,
|
|
||||||
int128 kappa_,
|
|
||||||
uint256 swapFeePpm_,
|
|
||||||
uint256 flashFeePpm_,
|
|
||||||
uint256 protocolFeePpm_,
|
|
||||||
address protocolFeeAddress_,
|
|
||||||
PartyPoolSwapImpl swapImpl_,
|
|
||||||
PartyPoolMintImpl mintImpl_
|
|
||||||
) external returns (IPartyPool) {
|
|
||||||
return new PartyPoolBalancedPair(
|
|
||||||
name_,
|
|
||||||
symbol_,
|
|
||||||
tokens_,
|
|
||||||
bases_,
|
|
||||||
kappa_,
|
|
||||||
swapFeePpm_,
|
|
||||||
flashFeePpm_,
|
|
||||||
protocolFeePpm_,
|
|
||||||
protocolFeeAddress_,
|
|
||||||
swapImpl_,
|
|
||||||
mintImpl_
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
// SPDX-License-Identifier: UNLICENSED
|
|
||||||
pragma solidity ^0.8.30;
|
|
||||||
|
|
||||||
import {ABDKMath64x64} from "../lib/abdk-libraries-solidity/ABDKMath64x64.sol";
|
|
||||||
|
|
||||||
|
|
||||||
abstract contract PartyPoolHelpers {
|
|
||||||
using ABDKMath64x64 for int128;
|
|
||||||
|
|
||||||
/// @notice Ceiling fee helper: computes ceil(x * feePpm / 1_000_000)
|
|
||||||
/// @dev Internal helper; public-facing functions use this to ensure fees round up in favor of pool.
|
|
||||||
function _ceilFee(uint256 x, uint256 feePpm) internal pure returns (uint256) {
|
|
||||||
if (feePpm == 0) return 0;
|
|
||||||
// ceil division: (num + denom - 1) / denom
|
|
||||||
return (x * feePpm + 1_000_000 - 1) / 1_000_000;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @notice Compute fee and net amounts for a gross input (fee rounded up to favor the pool).
|
|
||||||
/// @param gross total gross input
|
|
||||||
/// @param feePpm fee in ppm to apply
|
|
||||||
/// @return feeUint fee taken (uint) and netUint remaining for protocol use (uint)
|
|
||||||
function _computeFee(uint256 gross, uint256 feePpm) internal pure returns (uint256 feeUint, uint256 netUint) {
|
|
||||||
if (feePpm == 0) {
|
|
||||||
return (0, gross);
|
|
||||||
}
|
|
||||||
feeUint = _ceilFee(gross, feePpm);
|
|
||||||
netUint = gross - feeUint;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @notice Convenience: return gross = net + fee(net) using ceiling for fee.
|
|
||||||
/// @param netUint net amount
|
|
||||||
/// @param feePpm fee in ppm to apply
|
|
||||||
function _addFee(uint256 netUint, uint256 feePpm) internal pure returns (uint256 gross) {
|
|
||||||
if (feePpm == 0) return netUint;
|
|
||||||
uint256 fee = _ceilFee(netUint, feePpm);
|
|
||||||
return netUint + fee;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @notice Helper to compute size metric (sum of all asset quantities) from internal balances
|
|
||||||
/// @dev Returns the sum of all provided qInternal_ entries as a Q64.64 value.
|
|
||||||
function _computeSizeMetric(int128[] memory qInternal_) internal pure returns (int128) {
|
|
||||||
int128 total = int128(0);
|
|
||||||
for (uint i = 0; i < qInternal_.length; ) {
|
|
||||||
total = total.add(qInternal_[i]);
|
|
||||||
unchecked { i++; }
|
|
||||||
}
|
|
||||||
return total;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,593 +0,0 @@
|
|||||||
// SPDX-License-Identifier: UNLICENSED
|
|
||||||
pragma solidity ^0.8.30;
|
|
||||||
|
|
||||||
import {ABDKMath64x64} from "../lib/abdk-libraries-solidity/ABDKMath64x64.sol";
|
|
||||||
import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
|
|
||||||
import {SafeERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
||||||
import {ERC20Internal} from "./ERC20Internal.sol";
|
|
||||||
import {IPartyPool} from "./IPartyPool.sol";
|
|
||||||
import {LMSRStabilized} from "./LMSRStabilized.sol";
|
|
||||||
import {PartyPoolBase} from "./PartyPoolBase.sol";
|
|
||||||
|
|
||||||
/// @title PartyPoolMintImpl - Implementation contract for mint and burn functions
|
|
||||||
/// @notice This contract contains the mint and burn implementation that will be called via delegatecall
|
|
||||||
/// @dev This contract inherits from PartyPoolBase to access storage and internal functions
|
|
||||||
contract PartyPoolMintImpl is PartyPoolBase {
|
|
||||||
using ABDKMath64x64 for int128;
|
|
||||||
using LMSRStabilized for LMSRStabilized.State;
|
|
||||||
using SafeERC20 for IERC20;
|
|
||||||
|
|
||||||
|
|
||||||
//
|
|
||||||
// Initialization Mint
|
|
||||||
//
|
|
||||||
|
|
||||||
function initialMint(address receiver, uint256 lpTokens, int128 KAPPA) external nonReentrant
|
|
||||||
returns (uint256 lpMinted) {
|
|
||||||
uint256 n = tokens.length;
|
|
||||||
|
|
||||||
// Check if this is initial deposit - revert if not
|
|
||||||
bool isInitialDeposit = _totalSupply == 0 || lmsr.nAssets == 0;
|
|
||||||
require(isInitialDeposit, "initialMint: pool already initialized");
|
|
||||||
|
|
||||||
// Update cached balances for all assets
|
|
||||||
int128[] memory newQInternal = new int128[](n);
|
|
||||||
uint256[] memory depositAmounts = new uint256[](n);
|
|
||||||
for (uint i = 0; i < n; ) {
|
|
||||||
uint256 bal = IERC20(tokens[i]).balanceOf(address(this));
|
|
||||||
cachedUintBalances[i] = bal;
|
|
||||||
newQInternal[i] = _uintToInternalFloor(bal, bases[i]);
|
|
||||||
depositAmounts[i] = bal;
|
|
||||||
unchecked { i++; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize the stabilized LMSR state with provided kappa
|
|
||||||
lmsr.init(newQInternal, KAPPA);
|
|
||||||
|
|
||||||
// Compute actual LP tokens to mint based on size metric (scaled)
|
|
||||||
if( lpTokens != 0 )
|
|
||||||
lpMinted = lpTokens;
|
|
||||||
else {
|
|
||||||
int128 newTotal = _computeSizeMetric(newQInternal);
|
|
||||||
lpMinted = ABDKMath64x64.mulu(newTotal, LP_SCALE);
|
|
||||||
}
|
|
||||||
|
|
||||||
require(lpMinted > 0, "initialMint: zero LP amount");
|
|
||||||
_mint(receiver, lpMinted);
|
|
||||||
emit IPartyPool.Mint(address(0), receiver, depositAmounts, lpMinted);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//
|
|
||||||
// Regular Mint and Burn
|
|
||||||
//
|
|
||||||
|
|
||||||
function mint(address payer, address receiver, uint256 lpTokenAmount, uint256 deadline) external nonReentrant
|
|
||||||
returns (uint256 lpMinted) {
|
|
||||||
require(deadline == 0 || block.timestamp <= deadline, "mint: deadline exceeded");
|
|
||||||
uint256 n = tokens.length;
|
|
||||||
|
|
||||||
// Check if this is NOT initial deposit - revert if it is
|
|
||||||
bool isInitialDeposit = _totalSupply == 0 || lmsr.nAssets == 0;
|
|
||||||
require(!isInitialDeposit, "mint: use initialMint for pool initialization");
|
|
||||||
require(lpTokenAmount > 0, "mint: zero LP amount");
|
|
||||||
|
|
||||||
// Capture old pool size metric (scaled) by computing from current balances
|
|
||||||
int128 oldTotal = _computeSizeMetric(lmsr.qInternal);
|
|
||||||
uint256 oldScaled = ABDKMath64x64.mulu(oldTotal, LP_SCALE);
|
|
||||||
|
|
||||||
// Calculate required deposit amounts for the desired LP tokens
|
|
||||||
uint256[] memory depositAmounts = mintAmounts(lpTokenAmount, lmsr.nAssets, _totalSupply, cachedUintBalances);
|
|
||||||
|
|
||||||
// Transfer in all token amounts
|
|
||||||
for (uint i = 0; i < n; ) {
|
|
||||||
if (depositAmounts[i] > 0) {
|
|
||||||
tokens[i].safeTransferFrom(payer, address(this), depositAmounts[i]);
|
|
||||||
}
|
|
||||||
unchecked { i++; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update cached balances and internal q for all assets using depositAmounts
|
|
||||||
int128[] memory newQInternal = new int128[](n);
|
|
||||||
for (uint i = 0; i < n; ) {
|
|
||||||
uint256 newBal = cachedUintBalances[i] + depositAmounts[i];
|
|
||||||
cachedUintBalances[i] = newBal;
|
|
||||||
newQInternal[i] = _uintToInternalFloor(newBal, bases[i]);
|
|
||||||
unchecked { i++; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update for proportional change
|
|
||||||
lmsr.updateForProportionalChange(newQInternal);
|
|
||||||
|
|
||||||
// Compute actual LP tokens to mint based on change in size metric (scaled)
|
|
||||||
// floor truncation rounds in favor of the pool
|
|
||||||
int128 newTotal = _computeSizeMetric(newQInternal);
|
|
||||||
uint256 newScaled = ABDKMath64x64.mulu(newTotal, LP_SCALE);
|
|
||||||
uint256 actualLpToMint;
|
|
||||||
|
|
||||||
uint256 delta = (newScaled > oldScaled) ? (newScaled - oldScaled) : 0;
|
|
||||||
// Proportional issuance: totalSupply * delta / oldScaled
|
|
||||||
if (delta > 0) {
|
|
||||||
// floor truncation rounds in favor of the pool
|
|
||||||
actualLpToMint = (_totalSupply * delta) / oldScaled;
|
|
||||||
} else {
|
|
||||||
actualLpToMint = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure the calculated LP amount is not too different from requested
|
|
||||||
require(actualLpToMint > 0, "mint: zero LP minted");
|
|
||||||
|
|
||||||
// Allow actual amount to be at most 0.00001% less than requested
|
|
||||||
// This accounts for rounding in deposit calculations
|
|
||||||
uint256 minAcceptable = lpTokenAmount * 99_999 / 100_000;
|
|
||||||
require(actualLpToMint >= minAcceptable, "mint: insufficient LP minted");
|
|
||||||
|
|
||||||
_mint(receiver, actualLpToMint);
|
|
||||||
emit IPartyPool.Mint(payer, receiver, depositAmounts, actualLpToMint);
|
|
||||||
return actualLpToMint;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @notice Burn LP tokens and withdraw the proportional basket to receiver.
|
|
||||||
/// @dev Payer must own or approve the LP tokens being burned. The function updates LMSR state
|
|
||||||
/// proportionally to reflect the reduced pool size after the withdrawal.
|
|
||||||
/// @param payer address that provides the LP tokens to burn
|
|
||||||
/// @param receiver address that receives the withdrawn tokens
|
|
||||||
/// @param lpAmount amount of LP tokens to burn (proportional withdrawal)
|
|
||||||
/// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore.
|
|
||||||
function burn(address payer, address receiver, uint256 lpAmount, uint256 deadline) external nonReentrant
|
|
||||||
returns (uint256[] memory withdrawAmounts) {
|
|
||||||
require(deadline == 0 || block.timestamp <= deadline, "burn: deadline exceeded");
|
|
||||||
uint256 n = tokens.length;
|
|
||||||
require(lpAmount > 0, "burn: zero lp");
|
|
||||||
|
|
||||||
uint256 supply = _totalSupply;
|
|
||||||
require(supply > 0, "burn: empty supply");
|
|
||||||
|
|
||||||
// Use cached balances; assume standard ERC20 transfers without external interference
|
|
||||||
|
|
||||||
// Compute proportional withdrawal amounts for the requested LP amount (rounded down)
|
|
||||||
withdrawAmounts = burnAmounts(lpAmount, lmsr.nAssets, _totalSupply, cachedUintBalances);
|
|
||||||
|
|
||||||
// Transfer underlying tokens out to receiver according to computed proportions
|
|
||||||
for (uint i = 0; i < n; ) {
|
|
||||||
if (withdrawAmounts[i] > 0) {
|
|
||||||
tokens[i].safeTransfer(receiver, withdrawAmounts[i]);
|
|
||||||
}
|
|
||||||
unchecked { i++; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update cached balances and internal q for all assets using computed withdrawals
|
|
||||||
int128[] memory newQInternal = new int128[](n);
|
|
||||||
for (uint i = 0; i < n; ) {
|
|
||||||
uint256 newBal = cachedUintBalances[i] - withdrawAmounts[i];
|
|
||||||
cachedUintBalances[i] = newBal;
|
|
||||||
newQInternal[i] = _uintToInternalFloor(newBal, bases[i]);
|
|
||||||
unchecked { i++; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply proportional update or deinitialize if drained
|
|
||||||
bool allZero = true;
|
|
||||||
for (uint i = 0; i < n; ) {
|
|
||||||
if (newQInternal[i] != int128(0)) {
|
|
||||||
allZero = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
unchecked { i++; }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allZero) {
|
|
||||||
lmsr.deinit();
|
|
||||||
} else {
|
|
||||||
lmsr.updateForProportionalChange(newQInternal);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Burn exactly the requested LP amount from payer (authorization via allowance)
|
|
||||||
if (msg.sender != payer) {
|
|
||||||
uint256 allowed = _allowances[payer][msg.sender];
|
|
||||||
// Rely on Solidity's checked arithmetic to revert on underflow if allowance is insufficient
|
|
||||||
_approve(payer, msg.sender, allowed - lpAmount);
|
|
||||||
}
|
|
||||||
_burn(payer, lpAmount);
|
|
||||||
|
|
||||||
emit IPartyPool.Burn(payer, receiver, withdrawAmounts, lpAmount);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @notice Calculate the proportional deposit amounts required for a given LP token amount
|
|
||||||
/// @dev Returns the minimum token amounts (rounded up) that must be supplied to receive lpTokenAmount
|
|
||||||
/// LP tokens at current pool proportions. If the pool is empty (initial deposit) returns zeros
|
|
||||||
/// because the initial deposit is handled by transferring tokens then calling mint().
|
|
||||||
/// @param lpTokenAmount The amount of LP tokens desired
|
|
||||||
/// @return depositAmounts Array of token amounts to deposit (rounded up)
|
|
||||||
function mintAmounts(uint256 lpTokenAmount,
|
|
||||||
uint256 numAssets, uint256 totalSupply, uint256[] memory cachedUintBalances) public pure
|
|
||||||
returns (uint256[] memory depositAmounts) {
|
|
||||||
depositAmounts = new uint256[](numAssets);
|
|
||||||
|
|
||||||
// If this is the first mint or pool is empty, return zeros
|
|
||||||
// For first mint, tokens should already be transferred to the pool
|
|
||||||
if (totalSupply == 0 || numAssets == 0) {
|
|
||||||
return depositAmounts; // Return zeros, initial deposit handled differently
|
|
||||||
}
|
|
||||||
|
|
||||||
// lpTokenAmount / totalLpSupply = depositAmount / currentBalance
|
|
||||||
// Therefore: depositAmount = (lpTokenAmount * currentBalance) / totalLpSupply
|
|
||||||
// We round up to protect the pool
|
|
||||||
for (uint i = 0; i < numAssets; i++) {
|
|
||||||
uint256 currentBalance = cachedUintBalances[i];
|
|
||||||
// Calculate with rounding up: (a * b + c - 1) / c
|
|
||||||
depositAmounts[i] = (lpTokenAmount * currentBalance + totalSupply - 1) / totalSupply;
|
|
||||||
}
|
|
||||||
|
|
||||||
return depositAmounts;
|
|
||||||
}
|
|
||||||
|
|
||||||
function burnAmounts(uint256 lpTokenAmount,
|
|
||||||
uint256 numAssets, uint256 totalSupply, uint256[] memory cachedUintBalances) public pure
|
|
||||||
returns (uint256[] memory withdrawAmounts) {
|
|
||||||
withdrawAmounts = new uint256[](numAssets);
|
|
||||||
|
|
||||||
// If supply is zero or pool uninitialized, return zeros
|
|
||||||
if (totalSupply == 0 || numAssets == 0) {
|
|
||||||
return withdrawAmounts; // Return zeros, nothing to withdraw
|
|
||||||
}
|
|
||||||
|
|
||||||
// withdrawAmount = floor(lpTokenAmount * currentBalance / totalLpSupply)
|
|
||||||
for (uint i = 0; i < numAssets; i++) {
|
|
||||||
uint256 currentBalance = cachedUintBalances[i];
|
|
||||||
withdrawAmounts[i] = (lpTokenAmount * currentBalance) / totalSupply;
|
|
||||||
}
|
|
||||||
|
|
||||||
return withdrawAmounts;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//
|
|
||||||
// Swap-Mint and Burn-Swap
|
|
||||||
//
|
|
||||||
|
|
||||||
/// @notice Calculate the amounts for a swap mint operation
|
|
||||||
/// @dev This is a pure view function that computes swap mint amounts from provided state
|
|
||||||
/// @param inputTokenIndex index of the input token
|
|
||||||
/// @param maxAmountIn maximum amount of token to deposit (inclusive of fee)
|
|
||||||
/// @param swapFeePpm fee in parts-per-million
|
|
||||||
/// @param lmsrState current LMSR state
|
|
||||||
/// @param bases_ scaling bases for each token
|
|
||||||
/// @param totalSupply_ current total LP token supply
|
|
||||||
/// @return amountInUsed actual input amount used (excluding fee)
|
|
||||||
/// @return fee fee amount charged
|
|
||||||
/// @return lpMinted LP tokens that would be minted
|
|
||||||
function swapMintAmounts(
|
|
||||||
uint256 inputTokenIndex,
|
|
||||||
uint256 maxAmountIn,
|
|
||||||
uint256 swapFeePpm,
|
|
||||||
LMSRStabilized.State memory lmsrState,
|
|
||||||
uint256[] memory bases_,
|
|
||||||
uint256 totalSupply_
|
|
||||||
) public pure returns (uint256 amountInUsed, uint256 fee, uint256 lpMinted) {
|
|
||||||
require(inputTokenIndex < bases_.length, "swapMintAmounts: idx");
|
|
||||||
require(maxAmountIn > 0, "swapMintAmounts: input zero");
|
|
||||||
require(lmsrState.nAssets > 0, "swapMintAmounts: uninit pool");
|
|
||||||
|
|
||||||
// Compute fee on gross maxAmountIn to get an initial net estimate
|
|
||||||
uint256 feeGuess = 0;
|
|
||||||
uint256 netUintGuess = maxAmountIn;
|
|
||||||
if (swapFeePpm > 0) {
|
|
||||||
feeGuess = (maxAmountIn * swapFeePpm + 999999) / 1000000; // ceil fee
|
|
||||||
netUintGuess = maxAmountIn - feeGuess;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert the net guess to internal (floor)
|
|
||||||
int128 netInternalGuess = _uintToInternalFloorPure(netUintGuess, bases_[inputTokenIndex]);
|
|
||||||
require(netInternalGuess > int128(0), "swapMintAmounts: input too small after fee");
|
|
||||||
|
|
||||||
// Use LMSR view to determine actual internal consumed and size-increase (ΔS) for mint
|
|
||||||
(int128 amountInInternalUsed, int128 sizeIncreaseInternal) =
|
|
||||||
LMSRStabilized.swapAmountsForMint(lmsrState.nAssets, lmsrState.kappa, lmsrState.qInternal,
|
|
||||||
inputTokenIndex, netInternalGuess);
|
|
||||||
|
|
||||||
// amountInInternalUsed may be <= netInternalGuess. Convert to uint (ceil) to determine actual transfer
|
|
||||||
amountInUsed = _internalToUintCeilPure(amountInInternalUsed, bases_[inputTokenIndex]);
|
|
||||||
require(amountInUsed > 0, "swapMintAmounts: input zero after internal conversion");
|
|
||||||
|
|
||||||
// Compute fee on the actual used input (ceiling)
|
|
||||||
fee = 0;
|
|
||||||
if (swapFeePpm > 0) {
|
|
||||||
fee = (amountInUsed * swapFeePpm + 999999) / 1000000; // ceil fee
|
|
||||||
}
|
|
||||||
uint256 totalTransfer = amountInUsed + fee;
|
|
||||||
require(totalTransfer > 0 && totalTransfer <= maxAmountIn, "swapMintAmounts: transfer exceeds max");
|
|
||||||
|
|
||||||
// Compute old and new scaled size metrics to determine LP minted
|
|
||||||
int128 oldTotal = _computeSizeMetricPure(lmsrState.qInternal);
|
|
||||||
require(oldTotal > int128(0), "swapMintAmounts: zero total");
|
|
||||||
uint256 oldScaled = ABDKMath64x64.mulu(oldTotal, LP_SCALE);
|
|
||||||
|
|
||||||
int128 newTotal = oldTotal.add(sizeIncreaseInternal);
|
|
||||||
uint256 newScaled = ABDKMath64x64.mulu(newTotal, LP_SCALE);
|
|
||||||
|
|
||||||
if (totalSupply_ == 0) {
|
|
||||||
// If somehow supply zero (shouldn't happen as lmsr.nAssets>0), mint newScaled
|
|
||||||
lpMinted = newScaled;
|
|
||||||
} else {
|
|
||||||
require(oldScaled > 0, "swapMintAmounts: oldScaled zero");
|
|
||||||
uint256 delta = (newScaled > oldScaled) ? (newScaled - oldScaled) : 0;
|
|
||||||
if (delta > 0) {
|
|
||||||
// floor truncation rounds in favor of pool
|
|
||||||
lpMinted = (totalSupply_ * delta) / oldScaled;
|
|
||||||
} else {
|
|
||||||
lpMinted = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
require(lpMinted > 0, "swapMintAmounts: zero LP minted");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @notice Single-token mint: deposit a single token, charge swap-LMSR cost, and mint LP.
|
|
||||||
/// @dev swapMint executes as an exact-in planned swap followed by proportional scaling of qInternal.
|
|
||||||
/// The function emits SwapMint (gross, net, fee) and also emits Mint for LP issuance.
|
|
||||||
/// @param payer who transfers the input token
|
|
||||||
/// @param receiver who receives the minted LP tokens
|
|
||||||
/// @param inputTokenIndex index of the input token
|
|
||||||
/// @param maxAmountIn maximum uint token input (inclusive of fee)
|
|
||||||
/// @param deadline optional deadline
|
|
||||||
/// @param swapFeePpm fee in parts-per-million for this pool
|
|
||||||
/// @return lpMinted actual LP minted (uint)
|
|
||||||
function swapMint(
|
|
||||||
address payer,
|
|
||||||
address receiver,
|
|
||||||
uint256 inputTokenIndex,
|
|
||||||
uint256 maxAmountIn,
|
|
||||||
uint256 deadline,
|
|
||||||
uint256 swapFeePpm,
|
|
||||||
uint256 protocolFeePpm
|
|
||||||
) external nonReentrant returns (uint256 lpMinted) {
|
|
||||||
uint256 n = tokens.length;
|
|
||||||
require(inputTokenIndex < n, "swapMint: idx");
|
|
||||||
require(maxAmountIn > 0, "swapMint: input zero");
|
|
||||||
require(deadline == 0 || block.timestamp <= deadline, "swapMint: deadline");
|
|
||||||
require(lmsr.nAssets > 0, "swapMint: uninit pool");
|
|
||||||
|
|
||||||
// compute fee on gross maxAmountIn to get an initial net estimate (we'll recompute based on actual used)
|
|
||||||
(, uint256 netUintGuess) = _computeFee(maxAmountIn, swapFeePpm);
|
|
||||||
|
|
||||||
// Convert the net guess to internal (floor)
|
|
||||||
int128 netInternalGuess = _uintToInternalFloor(netUintGuess, bases[inputTokenIndex]);
|
|
||||||
require(netInternalGuess > int128(0), "swapMint: input too small after fee");
|
|
||||||
|
|
||||||
// Use LMSR view to determine actual internal consumed and size-increase (ΔS) for mint
|
|
||||||
(int128 amountInInternalUsed, int128 sizeIncreaseInternal) = lmsr.swapAmountsForMint(inputTokenIndex, netInternalGuess);
|
|
||||||
|
|
||||||
// amountInInternalUsed may be <= netInternalGuess. Convert to uint (ceil) to determine actual transfer
|
|
||||||
uint256 amountInUint = _internalToUintCeil(amountInInternalUsed, bases[inputTokenIndex]);
|
|
||||||
require(amountInUint > 0, "swapMint: input zero after internal conversion");
|
|
||||||
|
|
||||||
// Compute fee on the actual used input and total transfer amount (ceiling)
|
|
||||||
uint256 feeUintActual = _ceilFee(amountInUint, swapFeePpm);
|
|
||||||
uint256 totalTransfer = amountInUint + feeUintActual;
|
|
||||||
require(totalTransfer > 0 && totalTransfer <= maxAmountIn, "swapMint: transfer exceeds max");
|
|
||||||
|
|
||||||
// Transfer tokens from payer (assume standard ERC20 without transfer fees)
|
|
||||||
tokens[inputTokenIndex].safeTransferFrom(payer, address(this), totalTransfer);
|
|
||||||
|
|
||||||
// Accrue protocol share (floor) from the fee on the input token
|
|
||||||
uint256 protoShare = 0;
|
|
||||||
if (protocolFeePpm > 0 && feeUintActual > 0) {
|
|
||||||
protoShare = (feeUintActual * protocolFeePpm) / 1_000_000;
|
|
||||||
if (protoShare > 0) {
|
|
||||||
protocolFeesOwed[inputTokenIndex] += protoShare;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Update cached effective balance directly: add totalTransfer minus protocol share
|
|
||||||
cachedUintBalances[inputTokenIndex] += (totalTransfer - protoShare);
|
|
||||||
|
|
||||||
// Compute old and new scaled size metrics to determine LP minted
|
|
||||||
int128 oldTotal = _computeSizeMetric(lmsr.qInternal);
|
|
||||||
uint256 oldScaled = ABDKMath64x64.mulu(oldTotal, LP_SCALE);
|
|
||||||
|
|
||||||
int128 newTotal = oldTotal.add(sizeIncreaseInternal);
|
|
||||||
uint256 newScaled = ABDKMath64x64.mulu(newTotal, LP_SCALE);
|
|
||||||
|
|
||||||
uint256 actualLpToMint;
|
|
||||||
// Use natural ERC20 function since base contract inherits from ERC20
|
|
||||||
uint256 currentSupply = _totalSupply;
|
|
||||||
if (currentSupply == 0) {
|
|
||||||
// If somehow supply zero (shouldn't happen as lmsr.nAssets>0), mint newScaled
|
|
||||||
actualLpToMint = newScaled;
|
|
||||||
} else {
|
|
||||||
uint256 delta = (newScaled > oldScaled) ? (newScaled - oldScaled) : 0;
|
|
||||||
if (delta > 0) {
|
|
||||||
// floor truncation rounds in favor of pool
|
|
||||||
actualLpToMint = (currentSupply * delta) / oldScaled;
|
|
||||||
} else {
|
|
||||||
actualLpToMint = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
require(actualLpToMint > 0, "swapMint: zero LP minted");
|
|
||||||
|
|
||||||
// Update LMSR internal state: scale qInternal proportionally by newTotal/oldTotal
|
|
||||||
int128[] memory newQInternal = new int128[](n);
|
|
||||||
for (uint256 idx = 0; idx < n; idx++) {
|
|
||||||
// newQInternal[idx] = qInternal[idx] * (newTotal / oldTotal)
|
|
||||||
newQInternal[idx] = lmsr.qInternal[idx].mul(newTotal).div(oldTotal);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update cached internal and kappa via updateForProportionalChange
|
|
||||||
lmsr.updateForProportionalChange(newQInternal);
|
|
||||||
|
|
||||||
// Use natural ERC20 function since base contract inherits from ERC20
|
|
||||||
_mint(receiver, actualLpToMint);
|
|
||||||
|
|
||||||
// Emit SwapMint event with gross transfer, net input and fee (planned exact-in)
|
|
||||||
emit IPartyPool.SwapMint(payer, receiver, inputTokenIndex, totalTransfer, amountInUint, feeUintActual);
|
|
||||||
|
|
||||||
// Emit standard Mint event which records deposit amounts and LP minted
|
|
||||||
emit IPartyPool.Mint(payer, receiver, new uint256[](n), actualLpToMint);
|
|
||||||
// Note: depositAmounts array omitted (empty) since swapMint uses single-token input
|
|
||||||
|
|
||||||
return actualLpToMint;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @notice Calculate the amounts for a burn swap operation
|
|
||||||
/// @dev This is a pure view function that computes burn swap amounts from provided state
|
|
||||||
/// @param lpAmount amount of LP tokens to burn
|
|
||||||
/// @param inputTokenIndex index of target asset to receive
|
|
||||||
/// @param swapFeePpm fee in parts-per-million
|
|
||||||
/// @param lmsrState current LMSR state
|
|
||||||
/// @param bases_ scaling bases for each token
|
|
||||||
/// @param totalSupply_ current total LP token supply
|
|
||||||
/// @return amountOut amount of target asset that would be received
|
|
||||||
function burnSwapAmounts(
|
|
||||||
uint256 lpAmount,
|
|
||||||
uint256 inputTokenIndex,
|
|
||||||
uint256 swapFeePpm,
|
|
||||||
LMSRStabilized.State memory lmsrState,
|
|
||||||
uint256[] memory bases_,
|
|
||||||
uint256 totalSupply_
|
|
||||||
) public pure returns (uint256 amountOut) {
|
|
||||||
require(inputTokenIndex < bases_.length, "burnSwapAmounts: idx");
|
|
||||||
require(lpAmount > 0, "burnSwapAmounts: zero lp");
|
|
||||||
require(totalSupply_ > 0, "burnSwapAmounts: empty supply");
|
|
||||||
|
|
||||||
// alpha = lpAmount / supply as Q64.64
|
|
||||||
int128 alpha = ABDKMath64x64.divu(lpAmount, totalSupply_) // fraction of total supply to burn
|
|
||||||
.mul(ABDKMath64x64.divu(1000000-swapFeePpm, 1000000)); // adjusted for fee
|
|
||||||
|
|
||||||
// Use LMSR view to compute single-asset payout and burned size-metric
|
|
||||||
(int128 payoutInternal, ) = LMSRStabilized.swapAmountsForBurn(lmsrState.nAssets, lmsrState.kappa, lmsrState.qInternal,
|
|
||||||
inputTokenIndex, alpha);
|
|
||||||
|
|
||||||
// Convert payoutInternal -> uint (floor) to favor pool
|
|
||||||
amountOut = _internalToUintFloorPure(payoutInternal, bases_[inputTokenIndex]);
|
|
||||||
require(amountOut > 0, "burnSwapAmounts: output zero");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @notice Burn LP tokens then swap the redeemed proportional basket into a single asset `inputTokenIndex` and send to receiver.
|
|
||||||
/// @dev The function burns LP tokens (authorization via allowance if needed), sends the single-asset payout and updates LMSR state.
|
|
||||||
/// @param payer who burns LP tokens
|
|
||||||
/// @param receiver who receives the single asset
|
|
||||||
/// @param lpAmount amount of LP tokens to burn
|
|
||||||
/// @param inputTokenIndex index of target asset to receive
|
|
||||||
/// @param deadline optional deadline
|
|
||||||
/// @param swapFeePpm fee in parts-per-million for this pool (may be used for future fee logic)
|
|
||||||
/// @return amountOutUint uint amount of asset i sent to receiver
|
|
||||||
function burnSwap(
|
|
||||||
address payer,
|
|
||||||
address receiver,
|
|
||||||
uint256 lpAmount,
|
|
||||||
uint256 inputTokenIndex,
|
|
||||||
uint256 deadline,
|
|
||||||
uint256 swapFeePpm,
|
|
||||||
uint256 protocolFeePpm
|
|
||||||
) external nonReentrant returns (uint256 amountOutUint) {
|
|
||||||
uint256 n = tokens.length;
|
|
||||||
require(inputTokenIndex < n, "burnSwap: idx");
|
|
||||||
require(lpAmount > 0, "burnSwap: zero lp");
|
|
||||||
require(deadline == 0 || block.timestamp <= deadline, "burnSwap: deadline");
|
|
||||||
|
|
||||||
uint256 supply = _totalSupply;
|
|
||||||
require(supply > 0, "burnSwap: empty supply");
|
|
||||||
|
|
||||||
// alpha = lpAmount / supply as Q64.64 (adjusted for fee)
|
|
||||||
int128 alpha = ABDKMath64x64.divu(lpAmount, supply) // fraction of total supply to burn
|
|
||||||
.mul(ABDKMath64x64.divu(1000000-swapFeePpm, 1000000)); // adjusted for fee
|
|
||||||
|
|
||||||
// Use LMSR view to compute single-asset payout and burned size-metric
|
|
||||||
(int128 payoutInternal, ) = lmsr.swapAmountsForBurn(inputTokenIndex, alpha);
|
|
||||||
|
|
||||||
// Convert payoutInternal -> uint (floor) to favor pool
|
|
||||||
amountOutUint = _internalToUintFloor(payoutInternal, bases[inputTokenIndex]);
|
|
||||||
require(amountOutUint > 0, "burnSwap: output zero");
|
|
||||||
|
|
||||||
// Compute gross payout (no swap fee) so we can determine token-side fee = gross - net
|
|
||||||
int128 alphaGross = ABDKMath64x64.divu(lpAmount, supply); // gross fraction (no swap fee)
|
|
||||||
(int128 payoutGrossInternal, ) = lmsr.swapAmountsForBurn(inputTokenIndex, alphaGross);
|
|
||||||
uint256 payoutGrossUint = _internalToUintFloor(payoutGrossInternal, bases[inputTokenIndex]);
|
|
||||||
uint256 feeTokenUint = (payoutGrossUint > amountOutUint) ? (payoutGrossUint - amountOutUint) : 0;
|
|
||||||
|
|
||||||
// Accrue protocol share (floor) from the token-side fee
|
|
||||||
uint256 protoShare = 0;
|
|
||||||
if (protocolFeePpm > 0 && feeTokenUint > 0) {
|
|
||||||
protoShare = (feeTokenUint * protocolFeePpm) / 1_000_000;
|
|
||||||
if (protoShare > 0) {
|
|
||||||
protocolFeesOwed[inputTokenIndex] += protoShare;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transfer the payout to receiver
|
|
||||||
tokens[inputTokenIndex].safeTransfer(receiver, amountOutUint);
|
|
||||||
|
|
||||||
// Burn LP tokens from payer (authorization via allowance)
|
|
||||||
if (msg.sender != payer) {
|
|
||||||
uint256 allowed = _allowances[payer][msg.sender];
|
|
||||||
_approve(payer, msg.sender, allowed - lpAmount);
|
|
||||||
}
|
|
||||||
_burn(payer, lpAmount);
|
|
||||||
|
|
||||||
// Update cached balances using computed payout and protocol fee; no on-chain reads
|
|
||||||
int128[] memory newQInternal = new int128[](n);
|
|
||||||
for (uint256 idx = 0; idx < n; idx++) {
|
|
||||||
uint256 newBal = cachedUintBalances[idx];
|
|
||||||
if (idx == inputTokenIndex) {
|
|
||||||
// Effective LP balance decreases by net payout and increased protocol owed
|
|
||||||
newBal = newBal - amountOutUint - protoShare;
|
|
||||||
}
|
|
||||||
cachedUintBalances[idx] = newBal;
|
|
||||||
newQInternal[idx] = _uintToInternalFloor(newBal, bases[idx]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Emit BurnSwap with public-facing info only (do not expose ΔS or LP burned)
|
|
||||||
emit IPartyPool.BurnSwap(payer, receiver, inputTokenIndex, amountOutUint);
|
|
||||||
|
|
||||||
// If entire pool drained, deinit; else update proportionally
|
|
||||||
bool allZero = true;
|
|
||||||
for (uint256 idx = 0; idx < n; idx++) {
|
|
||||||
if (newQInternal[idx] != int128(0)) { allZero = false; break; }
|
|
||||||
}
|
|
||||||
if (allZero) {
|
|
||||||
lmsr.deinit();
|
|
||||||
} else {
|
|
||||||
lmsr.updateForProportionalChange(newQInternal);
|
|
||||||
}
|
|
||||||
|
|
||||||
emit IPartyPool.Burn(payer, receiver, new uint256[](n), lpAmount);
|
|
||||||
return amountOutUint;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @notice Pure version of _uintToInternalFloor for use in view functions
|
|
||||||
function _uintToInternalFloorPure(uint256 amount, uint256 base) internal pure returns (int128) {
|
|
||||||
// amount / base as Q64.64, floored
|
|
||||||
return ABDKMath64x64.divu(amount, base);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @notice Pure version of _internalToUintCeil for use in view functions
|
|
||||||
function _internalToUintCeilPure(int128 amount, uint256 base) internal pure returns (uint256) {
|
|
||||||
// Convert Q64.64 to uint with ceiling: ceil(amount * base)
|
|
||||||
// Use mulu which floors, then add remainder check for ceiling
|
|
||||||
uint256 floored = ABDKMath64x64.mulu(amount, base);
|
|
||||||
// Check if there's a fractional part by computing amount * base - floored
|
|
||||||
int128 baseQ64 = ABDKMath64x64.fromUInt(base);
|
|
||||||
int128 flooredQ64 = ABDKMath64x64.fromUInt(floored);
|
|
||||||
int128 product = amount.mul(baseQ64);
|
|
||||||
if (product > flooredQ64) {
|
|
||||||
return floored + 1; // Ceiling
|
|
||||||
}
|
|
||||||
return floored;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @notice Pure version of _internalToUintFloor for use in view functions
|
|
||||||
function _internalToUintFloorPure(int128 amount, uint256 base) internal pure returns (uint256) {
|
|
||||||
// Convert Q64.64 to uint with floor: floor(amount * base)
|
|
||||||
return ABDKMath64x64.mulu(amount, base);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @notice Pure version of _computeSizeMetric for use in view functions
|
|
||||||
function _computeSizeMetricPure(int128[] memory qInternal) internal pure returns (int128) {
|
|
||||||
int128 sum = int128(0);
|
|
||||||
for (uint256 i = 0; i < qInternal.length; i++) {
|
|
||||||
sum = sum.add(qInternal[i]);
|
|
||||||
}
|
|
||||||
return sum;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
// SPDX-License-Identifier: UNLICENSED
|
|
||||||
pragma solidity ^0.8.30;
|
|
||||||
|
|
||||||
import {ABDKMath64x64} from "../lib/abdk-libraries-solidity/ABDKMath64x64.sol";
|
|
||||||
import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
|
|
||||||
import {SafeERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
||||||
import {LMSRStabilized} from "./LMSRStabilized.sol";
|
|
||||||
import {PartyPoolBase} from "./PartyPoolBase.sol";
|
|
||||||
import {IPartyPool} from "./IPartyPool.sol";
|
|
||||||
|
|
||||||
/// @title PartyPoolSwapMintImpl - Implementation contract for swapMint and burnSwap functions
|
|
||||||
/// @notice This contract contains the swapMint and burnSwap implementation that will be called via delegatecall
|
|
||||||
/// @dev This contract inherits from PartyPoolBase to access storage and internal functions
|
|
||||||
contract PartyPoolSwapImpl is PartyPoolBase {
|
|
||||||
using ABDKMath64x64 for int128;
|
|
||||||
using LMSRStabilized for LMSRStabilized.State;
|
|
||||||
using SafeERC20 for IERC20;
|
|
||||||
|
|
||||||
function swapToLimitAmounts(
|
|
||||||
uint256 inputTokenIndex,
|
|
||||||
uint256 outputTokenIndex,
|
|
||||||
int128 limitPrice,
|
|
||||||
uint256[] memory bases,
|
|
||||||
int128 kappa,
|
|
||||||
int128[] memory qInternal,
|
|
||||||
uint256 swapFeePpm
|
|
||||||
) external pure returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
|
|
||||||
// Compute internal maxima at the price limit
|
|
||||||
(int128 amountInInternal, int128 amountOutInternal) = LMSRStabilized.swapAmountsForPriceLimit(
|
|
||||||
bases.length, kappa, qInternal,
|
|
||||||
inputTokenIndex, outputTokenIndex, limitPrice);
|
|
||||||
|
|
||||||
// Convert input to uint (ceil) and output to uint (floor)
|
|
||||||
uint256 amountInUintNoFee = _internalToUintCeil(amountInInternal, bases[inputTokenIndex]);
|
|
||||||
require(amountInUintNoFee > 0, "swapToLimit: input zero");
|
|
||||||
|
|
||||||
fee = 0;
|
|
||||||
amountIn = amountInUintNoFee;
|
|
||||||
if (swapFeePpm > 0) {
|
|
||||||
fee = _ceilFee(amountInUintNoFee, swapFeePpm);
|
|
||||||
amountIn += fee;
|
|
||||||
}
|
|
||||||
|
|
||||||
amountOut = _internalToUintFloor(amountOutInternal, bases[outputTokenIndex]);
|
|
||||||
require(amountOut > 0, "swapToLimit: output zero");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function swapToLimit(
|
|
||||||
address payer,
|
|
||||||
address receiver,
|
|
||||||
uint256 inputTokenIndex,
|
|
||||||
uint256 outputTokenIndex,
|
|
||||||
int128 limitPrice,
|
|
||||||
uint256 deadline,
|
|
||||||
uint256 swapFeePpm,
|
|
||||||
uint256 protocolFeePpm
|
|
||||||
) external returns (uint256 amountInUsed, uint256 amountOut, uint256 fee) {
|
|
||||||
uint256 n = tokens.length;
|
|
||||||
require(inputTokenIndex < n && outputTokenIndex < n, "swapToLimit: idx");
|
|
||||||
require(limitPrice > int128(0), "swapToLimit: limit <= 0");
|
|
||||||
require(deadline == 0 || block.timestamp <= deadline, "swapToLimit: deadline exceeded");
|
|
||||||
|
|
||||||
// Read previous balances for affected assets
|
|
||||||
uint256 prevBalI = IERC20(tokens[inputTokenIndex]).balanceOf(address(this));
|
|
||||||
uint256 prevBalJ = IERC20(tokens[outputTokenIndex]).balanceOf(address(this));
|
|
||||||
|
|
||||||
// Compute amounts using the same path as views
|
|
||||||
(uint256 totalTransferAmount, uint256 amountOutUint, int128 amountInInternalMax, int128 amountOutInternal, uint256 amountInUsedUint, uint256 feeUint) =
|
|
||||||
_quoteSwapToLimit(inputTokenIndex, outputTokenIndex, limitPrice, swapFeePpm);
|
|
||||||
|
|
||||||
// Transfer the exact amount needed from payer and require exact receipt (revert on fee-on-transfer)
|
|
||||||
tokens[inputTokenIndex].safeTransferFrom(payer, address(this), totalTransferAmount);
|
|
||||||
uint256 balIAfter = IERC20(tokens[inputTokenIndex]).balanceOf(address(this));
|
|
||||||
require(balIAfter == prevBalI + totalTransferAmount, "swapToLimit: non-standard tokenIn");
|
|
||||||
|
|
||||||
// Transfer output to receiver and verify exact decrease
|
|
||||||
tokens[outputTokenIndex].safeTransfer(receiver, amountOutUint);
|
|
||||||
uint256 balJAfter = IERC20(tokens[outputTokenIndex]).balanceOf(address(this));
|
|
||||||
require(balJAfter == prevBalJ - amountOutUint, "swapToLimit: non-standard tokenOut");
|
|
||||||
|
|
||||||
// Accrue protocol share (floor) from the fee on input token
|
|
||||||
if (protocolFeePpm > 0 && feeUint > 0 ) {
|
|
||||||
uint256 protoShare = (feeUint * protocolFeePpm) / 1_000_000; // floor
|
|
||||||
if (protoShare > 0) {
|
|
||||||
protocolFeesOwed[inputTokenIndex] += protoShare;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update caches to effective balances
|
|
||||||
_recordCachedBalance(inputTokenIndex, balIAfter);
|
|
||||||
_recordCachedBalance(outputTokenIndex, balJAfter);
|
|
||||||
|
|
||||||
// Apply swap to LMSR state with the internal amounts
|
|
||||||
lmsr.applySwap(inputTokenIndex, outputTokenIndex, amountInInternalMax, amountOutInternal);
|
|
||||||
|
|
||||||
// Maintain original event semantics (logs input without fee)
|
|
||||||
emit IPartyPool.Swap(payer, receiver, tokens[inputTokenIndex], tokens[outputTokenIndex], amountInUsedUint, amountOutUint);
|
|
||||||
|
|
||||||
return (amountInUsedUint, amountOutUint, feeUint);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// @notice Internal quote for swap-to-limit that mirrors swapToLimit() rounding and fee application
|
|
||||||
/// @dev Computes the input required to reach limitPrice and the resulting output; all rounding matches swapToLimit.
|
|
||||||
/// @return grossIn amount to transfer in (inclusive of fee), amountOutUint output amount (uint),
|
|
||||||
/// amountInInternal and amountOutInternal (64.64), amountInUintNoFee input amount excluding fee (uint),
|
|
||||||
/// feeUint fee taken from the gross input (uint)
|
|
||||||
function _quoteSwapToLimit(
|
|
||||||
uint256 inputTokenIndex,
|
|
||||||
uint256 outputTokenIndex,
|
|
||||||
int128 limitPrice,
|
|
||||||
uint256 swapFeePpm
|
|
||||||
) internal view
|
|
||||||
returns (
|
|
||||||
uint256 grossIn,
|
|
||||||
uint256 amountOutUint,
|
|
||||||
int128 amountInInternal,
|
|
||||||
int128 amountOutInternal,
|
|
||||||
uint256 amountInUintNoFee,
|
|
||||||
uint256 feeUint
|
|
||||||
)
|
|
||||||
{
|
|
||||||
// Compute internal maxima at the price limit
|
|
||||||
(amountInInternal, amountOutInternal) = lmsr.swapAmountsForPriceLimit(inputTokenIndex, outputTokenIndex, limitPrice);
|
|
||||||
|
|
||||||
// Convert input to uint (ceil) and output to uint (floor)
|
|
||||||
amountInUintNoFee = _internalToUintCeil(amountInInternal, bases[inputTokenIndex]);
|
|
||||||
require(amountInUintNoFee > 0, "swapToLimit: input zero");
|
|
||||||
|
|
||||||
feeUint = 0;
|
|
||||||
grossIn = amountInUintNoFee;
|
|
||||||
if (swapFeePpm > 0) {
|
|
||||||
feeUint = _ceilFee(amountInUintNoFee, swapFeePpm);
|
|
||||||
grossIn += feeUint;
|
|
||||||
}
|
|
||||||
|
|
||||||
amountOutUint = _internalToUintFloor(amountOutInternal, bases[outputTokenIndex]);
|
|
||||||
require(amountOutUint > 0, "swapToLimit: output zero");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
// SPDX-License-Identifier: UNLICENSED
|
|
||||||
pragma solidity ^0.8.30;
|
|
||||||
|
|
||||||
import {ABDKMath64x64} from "../lib/abdk-libraries-solidity/ABDKMath64x64.sol";
|
|
||||||
import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
|
|
||||||
import {IPartyPool} from "./IPartyPool.sol";
|
|
||||||
import {LMSRStabilized} from "./LMSRStabilized.sol";
|
|
||||||
import {PartyPoolHelpers} from "./PartyPoolHelpers.sol";
|
|
||||||
import {PartyPoolMintImpl} from "./PartyPoolMintImpl.sol";
|
|
||||||
import {PartyPoolSwapImpl} from "./PartyPoolSwapImpl.sol";
|
|
||||||
|
|
||||||
contract PartyPoolViewer is PartyPoolHelpers {
|
|
||||||
using ABDKMath64x64 for int128;
|
|
||||||
|
|
||||||
PartyPoolSwapImpl immutable internal SWAP_IMPL;
|
|
||||||
PartyPoolMintImpl immutable internal MINT_IMPL;
|
|
||||||
|
|
||||||
constructor(PartyPoolSwapImpl swapImpl_, PartyPoolMintImpl mintImpl) {
|
|
||||||
SWAP_IMPL = swapImpl_;
|
|
||||||
MINT_IMPL = mintImpl;
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Current marginal prices
|
|
||||||
//
|
|
||||||
|
|
||||||
/// @notice Marginal price of `base` denominated in `quote` as Q64.64.
|
|
||||||
/// @dev Returns the LMSR marginal price p_quote / p_base in ABDK 64.64 fixed-point format.
|
|
||||||
/// Useful for off-chain quoting; raw 64.64 value is returned (no scaling to token units).
|
|
||||||
/// @param baseTokenIndex index of the base asset (e.g., ETH)
|
|
||||||
/// @param quoteTokenIndex index of the quote asset (e.g., USD)
|
|
||||||
/// @return price Q64.64 value equal to quote per base (p_quote / p_base)
|
|
||||||
function price(IPartyPool pool, uint256 baseTokenIndex, uint256 quoteTokenIndex) external view returns (int128) {
|
|
||||||
LMSRStabilized.State memory lmsr = pool.LMSR();
|
|
||||||
require(baseTokenIndex < lmsr.nAssets && quoteTokenIndex < lmsr.nAssets, "price: idx");
|
|
||||||
require(lmsr.nAssets > 0, "price: uninit");
|
|
||||||
return LMSRStabilized.price(lmsr.nAssets, pool.kappa(), lmsr.qInternal, baseTokenIndex, quoteTokenIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @notice Price of one LP token denominated in `quote` as Q64.64.
|
|
||||||
/// @dev Computes LMSR poolPrice (quote per unit internal qTotal) and scales it to LP units:
|
|
||||||
/// returns price_per_LP = poolPrice_quote * (totalSupply() / qTotal) in ABDK 64.64 format.
|
|
||||||
/// The returned value is raw Q64.64 and represents quote units per one LP token unit.
|
|
||||||
/// @param quoteTokenIndex index of the quote asset in which to denominate the LP price
|
|
||||||
/// @return price Q64.64 value equal to quote per LP token unit
|
|
||||||
function poolPrice(IPartyPool pool, uint256 quoteTokenIndex) external view returns (int128) {
|
|
||||||
LMSRStabilized.State memory lmsr = pool.LMSR();
|
|
||||||
require(lmsr.nAssets > 0, "poolPrice: uninit");
|
|
||||||
require(quoteTokenIndex < lmsr.nAssets, "poolPrice: idx");
|
|
||||||
|
|
||||||
// price per unit of qTotal (Q64.64) from LMSR
|
|
||||||
int128 pricePerQ = LMSRStabilized.poolPrice(lmsr.nAssets, pool.kappa(), lmsr.qInternal, quoteTokenIndex);
|
|
||||||
|
|
||||||
// total internal q (qTotal) as Q64.64
|
|
||||||
int128 qTotal = LMSRStabilized._computeSizeMetric(lmsr.qInternal);
|
|
||||||
require(qTotal > int128(0), "poolPrice: qTotal zero");
|
|
||||||
|
|
||||||
// totalSupply as Q64.64
|
|
||||||
uint256 supply = pool.totalSupply();
|
|
||||||
require(supply > 0, "poolPrice: zero supply");
|
|
||||||
int128 supplyQ64 = ABDKMath64x64.fromUInt(supply);
|
|
||||||
|
|
||||||
// factor = totalSupply / qTotal (Q64.64)
|
|
||||||
int128 factor = supplyQ64.div(qTotal);
|
|
||||||
|
|
||||||
// price per LP token = pricePerQ * factor (Q64.64)
|
|
||||||
return pricePerQ.mul(factor);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function mintAmounts(IPartyPool pool, uint256 lpTokenAmount) public view returns (uint256[] memory depositAmounts) {
|
|
||||||
LMSRStabilized.State memory lmsr = pool.LMSR();
|
|
||||||
uint256[] memory cachedUintBalances = new uint256[](lmsr.nAssets);
|
|
||||||
for( uint256 i=0; i<lmsr.nAssets; i++ )
|
|
||||||
cachedUintBalances[i] = pool.getToken(i).balanceOf(address(pool));
|
|
||||||
return MINT_IMPL.mintAmounts(lpTokenAmount, lmsr.nAssets, pool.totalSupply(), cachedUintBalances);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function burnAmounts(IPartyPool pool, uint256 lpTokenAmount) external view returns (uint256[] memory withdrawAmounts) {
|
|
||||||
LMSRStabilized.State memory lmsr = pool.LMSR();
|
|
||||||
uint256[] memory cachedUintBalances = new uint256[](lmsr.nAssets);
|
|
||||||
for( uint256 i=0; i<lmsr.nAssets; i++ )
|
|
||||||
cachedUintBalances[i] = pool.getToken(i).balanceOf(address(pool));
|
|
||||||
return MINT_IMPL.burnAmounts(lpTokenAmount, lmsr.nAssets, pool.totalSupply(), cachedUintBalances);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// @notice External view to quote swap-to-limit amounts (gross input incl. fee and output), matching swapToLimit() computations
|
|
||||||
/// @param inputTokenIndex index of input token
|
|
||||||
/// @param outputTokenIndex index of output token
|
|
||||||
/// @param limitPrice target marginal price to reach (must be > 0)
|
|
||||||
/// @return amountIn gross input amount to transfer (includes fee), amountOut output amount user would receive, fee fee amount taken
|
|
||||||
function swapToLimitAmounts(
|
|
||||||
IPartyPool pool,
|
|
||||||
uint256 inputTokenIndex,
|
|
||||||
uint256 outputTokenIndex,
|
|
||||||
int128 limitPrice
|
|
||||||
) external view returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
|
|
||||||
LMSRStabilized.State memory lmsr = pool.LMSR();
|
|
||||||
require(inputTokenIndex < lmsr.nAssets && outputTokenIndex < lmsr.nAssets, "swapToLimit: idx");
|
|
||||||
require(limitPrice > int128(0), "swapToLimit: limit <= 0");
|
|
||||||
require(lmsr.nAssets > 0, "swapToLimit: pool uninitialized");
|
|
||||||
|
|
||||||
return SWAP_IMPL.swapToLimitAmounts(
|
|
||||||
inputTokenIndex, outputTokenIndex, limitPrice,
|
|
||||||
pool.denominators(), pool.kappa(), lmsr.qInternal, pool.swapFeePpm());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function swapMintAmounts(IPartyPool pool, uint256 inputTokenIndex, uint256 maxAmountIn) external view
|
|
||||||
returns (uint256 amountInUsed, uint256 fee, uint256 lpMinted) {
|
|
||||||
LMSRStabilized.State memory lmsr = pool.LMSR();
|
|
||||||
return MINT_IMPL.swapMintAmounts(
|
|
||||||
inputTokenIndex,
|
|
||||||
maxAmountIn,
|
|
||||||
pool.swapFeePpm(),
|
|
||||||
lmsr,
|
|
||||||
pool.denominators(),
|
|
||||||
pool.totalSupply()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function burnSwapAmounts(IPartyPool pool, uint256 lpAmount, uint256 inputTokenIndex) external view
|
|
||||||
returns (uint256 amountOut) {
|
|
||||||
LMSRStabilized.State memory lmsr = pool.LMSR();
|
|
||||||
return MINT_IMPL.burnSwapAmounts(
|
|
||||||
lpAmount,
|
|
||||||
inputTokenIndex,
|
|
||||||
pool.swapFeePpm(),
|
|
||||||
lmsr,
|
|
||||||
pool.denominators(),
|
|
||||||
pool.totalSupply()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// @notice Compute repayment amounts (principal + flash fee) for a proposed flash loan.
|
|
||||||
/// @param loanAmounts array of per-token loan amounts; must match the pool's token ordering.
|
|
||||||
/// @return repaymentAmounts array where repaymentAmounts[i] = loanAmounts[i] + ceil(loanAmounts[i] * flashFeePpm)
|
|
||||||
function flashRepaymentAmounts(IPartyPool pool, uint256[] memory loanAmounts) external view
|
|
||||||
returns (uint256[] memory repaymentAmounts) {
|
|
||||||
LMSRStabilized.State memory lmsr = pool.LMSR();
|
|
||||||
repaymentAmounts = new uint256[](lmsr.nAssets);
|
|
||||||
for (uint256 i = 0; i < lmsr.nAssets; i++) {
|
|
||||||
uint256 amount = loanAmounts[i];
|
|
||||||
if (amount > 0) {
|
|
||||||
repaymentAmounts[i] = amount + _ceilFee(amount, pool.flashFeePpm());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dev The amount of currency available to be lent.
|
|
||||||
* @param token The loan currency.
|
|
||||||
* @return The amount of `token` that can be borrowed.
|
|
||||||
*/
|
|
||||||
function maxFlashLoan(
|
|
||||||
IPartyPool pool,
|
|
||||||
address token
|
|
||||||
) external view returns (uint256) {
|
|
||||||
return IERC20(token).balanceOf(address(pool));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dev The fee to be charged for a given loan.
|
|
||||||
* @param amount The amount of tokens lent.
|
|
||||||
* @return fee The amount of `token` to be charged for the loan, on top of the returned principal.
|
|
||||||
*/
|
|
||||||
function flashFee(
|
|
||||||
IPartyPool pool,
|
|
||||||
address /*token*/,
|
|
||||||
uint256 amount
|
|
||||||
) external view returns (uint256 fee) {
|
|
||||||
(fee,) = _computeFee(amount, pool.flashFeePpm());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
14
src/PoolBase.sol
Normal file
14
src/PoolBase.sol
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
|
pragma solidity ^0.8.30;
|
||||||
|
|
||||||
|
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||||
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||||
|
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
||||||
|
import "./PoolLib.sol";
|
||||||
|
|
||||||
|
/// @notice This contract has all the storage for PartyPool and can be inherited by implementation contracts that
|
||||||
|
/// are delegate-called
|
||||||
|
abstract contract PoolBase is ERC20, ReentrancyGuard {
|
||||||
|
/// @notice Pool state containing all storage variables
|
||||||
|
PoolLib.State internal s;
|
||||||
|
}
|
||||||
935
src/PoolLib.sol
Normal file
935
src/PoolLib.sol
Normal file
@@ -0,0 +1,935 @@
|
|||||||
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
|
pragma solidity ^0.8.30;
|
||||||
|
|
||||||
|
import "@abdk/ABDKMath64x64.sol";
|
||||||
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||||
|
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||||
|
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||||
|
import "./LMSRStabilized.sol";
|
||||||
|
import "./LMSRStabilizedBalancedPair.sol";
|
||||||
|
import "./IPartyFlashCallback.sol";
|
||||||
|
|
||||||
|
/// @title PoolLib - Library containing all PartyPool implementation logic
|
||||||
|
/// @notice This library contains the core implementation for LMSR-backed multi-asset pools
|
||||||
|
/// @dev All functions are internal and accept State as the first parameter
|
||||||
|
library PoolLib {
|
||||||
|
using ABDKMath64x64 for int128;
|
||||||
|
using LMSRStabilized for LMSRStabilized.State;
|
||||||
|
using SafeERC20 for IERC20;
|
||||||
|
|
||||||
|
/// @notice State struct containing all storage variables from PartyPool
|
||||||
|
/// @dev This struct is passed to all library functions as the first parameter
|
||||||
|
struct State {
|
||||||
|
/// @notice Token addresses comprising the pool
|
||||||
|
IERC20[] tokens;
|
||||||
|
|
||||||
|
/// @notice LMSR state for pricing computations
|
||||||
|
LMSRStabilized.State lmsr;
|
||||||
|
|
||||||
|
/// @notice Cached on-chain balances (uint) for each token
|
||||||
|
uint256[] cachedUintBalances;
|
||||||
|
|
||||||
|
/// @notice Per-token uint base denominators used to convert uint <-> internal Q64.64
|
||||||
|
uint256[] bases;
|
||||||
|
|
||||||
|
/// @notice Mapping from token address => (index+1). Zero means token not in pool.
|
||||||
|
mapping(IERC20=>uint) tokenAddressToIndexPlusOne;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Scale factor used when converting LMSR Q64.64 totals to LP token units (uint)
|
||||||
|
uint256 internal constant LP_SCALE = 1e18;
|
||||||
|
|
||||||
|
// Events that mirror the main contract events
|
||||||
|
event Mint(address indexed payer, address indexed receiver, uint256[] depositAmounts, uint256 lpMinted);
|
||||||
|
event Burn(address indexed payer, address indexed receiver, uint256[] withdrawAmounts, uint256 lpBurned);
|
||||||
|
event Swap(address indexed payer, address indexed receiver, IERC20 indexed tokenIn, IERC20 tokenOut, uint256 amountIn, uint256 amountOut);
|
||||||
|
event SwapMint(address indexed payer, address indexed receiver, uint256 indexed inputTokenIndex, uint256 totalTransfer, uint256 amountInUint, uint256 feeUintActual);
|
||||||
|
event BurnSwap(address indexed payer, address indexed receiver, uint256 indexed inputTokenIndex, uint256 amountOutUint);
|
||||||
|
|
||||||
|
/// @notice Initialize the pool state with tokens and bases
|
||||||
|
/// @param state The pool state
|
||||||
|
/// @param tokens_ Array of token addresses
|
||||||
|
/// @param bases_ Array of base denominators for each token
|
||||||
|
function initialize(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
IERC20[] memory tokens_,
|
||||||
|
uint256[] memory bases_
|
||||||
|
) internal {
|
||||||
|
require(tokens_.length > 1, "Pool: need >1 asset");
|
||||||
|
require(tokens_.length == bases_.length, "Pool: lengths mismatch");
|
||||||
|
|
||||||
|
state.tokens = tokens_;
|
||||||
|
state.bases = bases_;
|
||||||
|
|
||||||
|
uint256 n = tokens_.length;
|
||||||
|
|
||||||
|
// Initialize LMSR state nAssets; full init occurs on first mint
|
||||||
|
state.lmsr.nAssets = n;
|
||||||
|
|
||||||
|
// Initialize token address to index mapping
|
||||||
|
for (uint i = 0; i < n;) {
|
||||||
|
state.tokenAddressToIndexPlusOne[tokens_[i]] = i + 1;
|
||||||
|
unchecked {i++;}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize caches to zero
|
||||||
|
state.cachedUintBalances = new uint256[](n);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Get deposit amounts needed for minting LP tokens
|
||||||
|
function mintDepositAmounts(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
uint256 lpTokenAmount,
|
||||||
|
uint256 totalSupply
|
||||||
|
) internal view returns (uint256[] memory depositAmounts) {
|
||||||
|
uint256 n = state.tokens.length;
|
||||||
|
depositAmounts = new uint256[](n);
|
||||||
|
|
||||||
|
// If this is the first mint or pool is empty, return zeros
|
||||||
|
if (totalSupply == 0 || state.lmsr.nAssets == 0) {
|
||||||
|
return depositAmounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate deposit based on current proportions
|
||||||
|
for (uint i = 0; i < n; i++) {
|
||||||
|
uint256 currentBalance = state.cachedUintBalances[i];
|
||||||
|
// Calculate with rounding up: (a * b + c - 1) / c
|
||||||
|
depositAmounts[i] = (lpTokenAmount * currentBalance + totalSupply - 1) / totalSupply;
|
||||||
|
}
|
||||||
|
|
||||||
|
return depositAmounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Initial mint to set up pool for the first time
|
||||||
|
function initialMint(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
address receiver,
|
||||||
|
uint256 lpTokens,
|
||||||
|
int128 kappa,
|
||||||
|
uint256 totalSupply
|
||||||
|
) internal returns (uint256 lpMinted) {
|
||||||
|
uint256 n = state.tokens.length;
|
||||||
|
|
||||||
|
// Check if this is initial deposit
|
||||||
|
bool isInitialDeposit = totalSupply == 0 || state.lmsr.nAssets == 0;
|
||||||
|
require(isInitialDeposit, "initialMint: pool already initialized");
|
||||||
|
|
||||||
|
// Update cached balances for all assets
|
||||||
|
int128[] memory newQInternal = new int128[](n);
|
||||||
|
uint256[] memory depositAmounts = new uint256[](n);
|
||||||
|
for (uint i = 0; i < n; ) {
|
||||||
|
uint256 bal = IERC20(state.tokens[i]).balanceOf(address(this));
|
||||||
|
state.cachedUintBalances[i] = bal;
|
||||||
|
newQInternal[i] = _uintToInternalFloor(bal, state.bases[i]);
|
||||||
|
depositAmounts[i] = bal;
|
||||||
|
unchecked { i++; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the stabilized LMSR state with provided kappa
|
||||||
|
state.lmsr.init(newQInternal, kappa);
|
||||||
|
|
||||||
|
// Compute actual LP tokens to mint based on size metric (scaled)
|
||||||
|
if( lpTokens != 0 )
|
||||||
|
lpMinted = lpTokens;
|
||||||
|
else {
|
||||||
|
int128 newTotal = _computeSizeMetric(newQInternal);
|
||||||
|
lpMinted = ABDKMath64x64.mulu(newTotal, LP_SCALE);
|
||||||
|
}
|
||||||
|
|
||||||
|
require(lpMinted > 0, "initialMint: zero LP amount");
|
||||||
|
emit Mint(address(0), receiver, depositAmounts, lpMinted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Proportional mint for existing pool
|
||||||
|
function mint(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
address payer,
|
||||||
|
address receiver,
|
||||||
|
uint256 lpTokenAmount,
|
||||||
|
uint256 deadline,
|
||||||
|
uint256 totalSupply
|
||||||
|
) internal returns (uint256 lpMinted) {
|
||||||
|
require(deadline == 0 || block.timestamp <= deadline, "mint: deadline exceeded");
|
||||||
|
uint256 n = state.tokens.length;
|
||||||
|
|
||||||
|
// Check if this is NOT initial deposit
|
||||||
|
bool isInitialDeposit = totalSupply == 0 || state.lmsr.nAssets == 0;
|
||||||
|
require(!isInitialDeposit, "mint: use initialMint for pool initialization");
|
||||||
|
require(lpTokenAmount > 0, "mint: zero LP amount");
|
||||||
|
|
||||||
|
// Capture old pool size metric (scaled)
|
||||||
|
int128 oldTotal = _computeSizeMetric(state.lmsr.qInternal);
|
||||||
|
uint256 oldScaled = ABDKMath64x64.mulu(oldTotal, LP_SCALE);
|
||||||
|
|
||||||
|
// Calculate required deposit amounts
|
||||||
|
uint256[] memory depositAmounts = mintDepositAmounts(state, lpTokenAmount, totalSupply);
|
||||||
|
|
||||||
|
// Transfer in all token amounts
|
||||||
|
for (uint i = 0; i < n; ) {
|
||||||
|
if (depositAmounts[i] > 0) {
|
||||||
|
state.tokens[i].safeTransferFrom(payer, address(this), depositAmounts[i]);
|
||||||
|
}
|
||||||
|
unchecked { i++; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update cached balances for all assets
|
||||||
|
int128[] memory newQInternal = new int128[](n);
|
||||||
|
for (uint i = 0; i < n; ) {
|
||||||
|
uint256 bal = IERC20(state.tokens[i]).balanceOf(address(this));
|
||||||
|
state.cachedUintBalances[i] = bal;
|
||||||
|
newQInternal[i] = _uintToInternalFloor(bal, state.bases[i]);
|
||||||
|
unchecked { i++; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update for proportional change
|
||||||
|
state.lmsr.updateForProportionalChange(newQInternal);
|
||||||
|
|
||||||
|
// Compute actual LP tokens to mint
|
||||||
|
int128 newTotal = _computeSizeMetric(newQInternal);
|
||||||
|
uint256 newScaled = ABDKMath64x64.mulu(newTotal, LP_SCALE);
|
||||||
|
uint256 actualLpToMint;
|
||||||
|
|
||||||
|
require(oldScaled > 0, "mint: oldScaled zero");
|
||||||
|
uint256 delta = (newScaled > oldScaled) ? (newScaled - oldScaled) : 0;
|
||||||
|
if (delta > 0) {
|
||||||
|
actualLpToMint = (totalSupply * delta) / oldScaled;
|
||||||
|
} else {
|
||||||
|
actualLpToMint = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
require(actualLpToMint > 0, "mint: zero LP minted");
|
||||||
|
|
||||||
|
// Allow actual amount to be at most 0.00001% less than requested
|
||||||
|
uint256 minAcceptable = lpTokenAmount * 99_999 / 100_000;
|
||||||
|
require(actualLpToMint >= minAcceptable, "mint: insufficient LP minted");
|
||||||
|
|
||||||
|
emit Mint(payer, receiver, depositAmounts, actualLpToMint);
|
||||||
|
return actualLpToMint;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Get withdrawal amounts for burning LP tokens
|
||||||
|
function burnReceiveAmounts(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
uint256 lpTokenAmount,
|
||||||
|
uint256 totalSupply
|
||||||
|
) internal view returns (uint256[] memory withdrawAmounts) {
|
||||||
|
uint256 n = state.tokens.length;
|
||||||
|
withdrawAmounts = new uint256[](n);
|
||||||
|
|
||||||
|
if (totalSupply == 0 || state.lmsr.nAssets == 0) {
|
||||||
|
return withdrawAmounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint i = 0; i < n; i++) {
|
||||||
|
uint256 currentBalance = state.cachedUintBalances[i];
|
||||||
|
withdrawAmounts[i] = (lpTokenAmount * currentBalance) / totalSupply;
|
||||||
|
}
|
||||||
|
|
||||||
|
return withdrawAmounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Burn LP tokens and withdraw proportional basket
|
||||||
|
function burn(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
address payer,
|
||||||
|
address receiver,
|
||||||
|
uint256 lpAmount,
|
||||||
|
uint256 deadline,
|
||||||
|
uint256 totalSupply,
|
||||||
|
uint256 payerBalance
|
||||||
|
) internal returns (uint256[] memory withdrawAmounts) {
|
||||||
|
require(deadline == 0 || block.timestamp <= deadline, "burn: deadline exceeded");
|
||||||
|
uint256 n = state.tokens.length;
|
||||||
|
require(lpAmount > 0, "burn: zero lp");
|
||||||
|
require(totalSupply > 0, "burn: empty supply");
|
||||||
|
require(state.lmsr.nAssets > 0, "burn: uninit pool");
|
||||||
|
require(payerBalance >= lpAmount, "burn: insufficient LP");
|
||||||
|
|
||||||
|
// Refresh cached balances
|
||||||
|
for (uint i = 0; i < n; ) {
|
||||||
|
uint256 bal = IERC20(state.tokens[i]).balanceOf(address(this));
|
||||||
|
state.cachedUintBalances[i] = bal;
|
||||||
|
unchecked { i++; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute proportional withdrawal amounts
|
||||||
|
withdrawAmounts = burnReceiveAmounts(state, lpAmount, totalSupply);
|
||||||
|
|
||||||
|
// Transfer underlying tokens out
|
||||||
|
for (uint i = 0; i < n; ) {
|
||||||
|
if (withdrawAmounts[i] > 0) {
|
||||||
|
state.tokens[i].safeTransfer(receiver, withdrawAmounts[i]);
|
||||||
|
}
|
||||||
|
unchecked { i++; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update cached balances and internal q
|
||||||
|
int128[] memory newQInternal = new int128[](n);
|
||||||
|
for (uint i = 0; i < n; ) {
|
||||||
|
uint256 bal = IERC20(state.tokens[i]).balanceOf(address(this));
|
||||||
|
state.cachedUintBalances[i] = bal;
|
||||||
|
newQInternal[i] = _uintToInternalFloor(bal, state.bases[i]);
|
||||||
|
unchecked { i++; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply proportional update or deinitialize if drained
|
||||||
|
bool allZero = true;
|
||||||
|
for (uint i = 0; i < n; ) {
|
||||||
|
if (newQInternal[i] != int128(0)) {
|
||||||
|
allZero = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
unchecked { i++; }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allZero) {
|
||||||
|
state.lmsr.deinit();
|
||||||
|
} else {
|
||||||
|
state.lmsr.updateForProportionalChange(newQInternal);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit Burn(payer, receiver, withdrawAmounts, lpAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Get swap amounts for exact input swap
|
||||||
|
function swapAmounts(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
uint256 inputTokenIndex,
|
||||||
|
uint256 outputTokenIndex,
|
||||||
|
uint256 maxAmountIn,
|
||||||
|
int128 limitPrice,
|
||||||
|
uint256 swapFeePpm
|
||||||
|
) internal view returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
|
||||||
|
(uint256 grossIn, uint256 outUint,,,, uint256 feeUint) = _quoteSwapExactIn(
|
||||||
|
state, inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice, swapFeePpm
|
||||||
|
);
|
||||||
|
return (grossIn, outUint, feeUint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Get swap amounts for swap to price limit
|
||||||
|
function swapToLimitAmounts(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
uint256 inputTokenIndex,
|
||||||
|
uint256 outputTokenIndex,
|
||||||
|
int128 limitPrice,
|
||||||
|
uint256 swapFeePpm
|
||||||
|
) internal view returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
|
||||||
|
(uint256 grossIn, uint256 outUint,,,, uint256 feeUint) = _quoteSwapToLimit(
|
||||||
|
state, inputTokenIndex, outputTokenIndex, limitPrice, swapFeePpm
|
||||||
|
);
|
||||||
|
return (grossIn, outUint, feeUint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Get amounts for swapMint operation
|
||||||
|
function swapMintAmounts(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
uint256 inputTokenIndex,
|
||||||
|
uint256 maxAmountIn,
|
||||||
|
uint256 swapFeePpm,
|
||||||
|
uint256 totalSupply
|
||||||
|
) internal view returns (uint256 totalTransfer, uint256 amountIn, uint256 fee, uint256 lpMinted) {
|
||||||
|
uint256 n = state.tokens.length;
|
||||||
|
require(inputTokenIndex < n, "swapMintAmounts: idx");
|
||||||
|
require(maxAmountIn > 0, "swapMintAmounts: input zero");
|
||||||
|
require(state.lmsr.nAssets > 0, "swapMintAmounts: uninit pool");
|
||||||
|
|
||||||
|
// Compute fee on gross maxAmountIn to get initial net estimate
|
||||||
|
(, uint256 netUintGuess) = _computeFee(maxAmountIn, swapFeePpm);
|
||||||
|
|
||||||
|
// Convert the net guess to internal (floor)
|
||||||
|
int128 netInternalGuess = _uintToInternalFloor(netUintGuess, state.bases[inputTokenIndex]);
|
||||||
|
require(netInternalGuess > int128(0), "swapMintAmounts: input too small after fee");
|
||||||
|
|
||||||
|
// Use LMSR view to determine actual internal consumed and size-increase
|
||||||
|
(int128 amountInInternalUsed, int128 sizeIncreaseInternal) = state.lmsr.swapAmountsForMint(inputTokenIndex, netInternalGuess);
|
||||||
|
|
||||||
|
// Convert to uint (ceil) to determine actual transfer
|
||||||
|
uint256 amountInUint = _internalToUintCeil(amountInInternalUsed, state.bases[inputTokenIndex]);
|
||||||
|
require(amountInUint > 0, "swapMintAmounts: input zero after internal conversion");
|
||||||
|
|
||||||
|
// Compute fee on actual used input and total transfer amount
|
||||||
|
uint256 feeUintActual = _ceilFee(amountInUint, swapFeePpm);
|
||||||
|
uint256 totalTransferAmount = amountInUint + feeUintActual;
|
||||||
|
require(totalTransferAmount > 0 && totalTransferAmount <= maxAmountIn, "swapMintAmounts: transfer exceeds max");
|
||||||
|
|
||||||
|
// Compute old and new scaled size metrics
|
||||||
|
int128 oldTotal = _computeSizeMetric(state.lmsr.qInternal);
|
||||||
|
require(oldTotal > int128(0), "swapMintAmounts: zero total");
|
||||||
|
uint256 oldScaled = ABDKMath64x64.mulu(oldTotal, LP_SCALE);
|
||||||
|
|
||||||
|
int128 newTotal = oldTotal.add(sizeIncreaseInternal);
|
||||||
|
uint256 newScaled = ABDKMath64x64.mulu(newTotal, LP_SCALE);
|
||||||
|
|
||||||
|
uint256 actualLpToMint;
|
||||||
|
if (totalSupply == 0) {
|
||||||
|
actualLpToMint = newScaled;
|
||||||
|
} else {
|
||||||
|
require(oldScaled > 0, "swapMintAmounts: oldScaled zero");
|
||||||
|
uint256 delta = (newScaled > oldScaled) ? (newScaled - oldScaled) : 0;
|
||||||
|
if (delta > 0) {
|
||||||
|
actualLpToMint = (totalSupply * delta) / oldScaled;
|
||||||
|
} else {
|
||||||
|
actualLpToMint = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
require(actualLpToMint > 0, "swapMintAmounts: zero LP minted");
|
||||||
|
|
||||||
|
return (totalTransferAmount, amountInUint, feeUintActual, actualLpToMint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Execute exact input swap
|
||||||
|
function swap(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
address payer,
|
||||||
|
address receiver,
|
||||||
|
uint256 inputTokenIndex,
|
||||||
|
uint256 outputTokenIndex,
|
||||||
|
uint256 maxAmountIn,
|
||||||
|
int128 limitPrice,
|
||||||
|
uint256 deadline,
|
||||||
|
uint256 swapFeePpm
|
||||||
|
) internal returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
|
||||||
|
uint256 n = state.tokens.length;
|
||||||
|
require(inputTokenIndex < n && outputTokenIndex < n, "swap: idx");
|
||||||
|
require(maxAmountIn > 0, "swap: input zero");
|
||||||
|
require(deadline == 0 || block.timestamp <= deadline, "swap: deadline exceeded");
|
||||||
|
|
||||||
|
// Read previous balances
|
||||||
|
uint256 prevBalI = IERC20(state.tokens[inputTokenIndex]).balanceOf(address(this));
|
||||||
|
uint256 prevBalJ = IERC20(state.tokens[outputTokenIndex]).balanceOf(address(this));
|
||||||
|
|
||||||
|
// Compute amounts
|
||||||
|
(uint256 totalTransferAmount, uint256 amountOutUint, int128 amountInInternalUsed, int128 amountOutInternal, , uint256 feeUint) =
|
||||||
|
_quoteSwapExactIn(state, inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice, swapFeePpm);
|
||||||
|
|
||||||
|
// Transfer exact amount from payer
|
||||||
|
state.tokens[inputTokenIndex].safeTransferFrom(payer, address(this), totalTransferAmount);
|
||||||
|
uint256 balIAfter = IERC20(state.tokens[inputTokenIndex]).balanceOf(address(this));
|
||||||
|
require(balIAfter == prevBalI + totalTransferAmount, "swap: non-standard tokenIn");
|
||||||
|
|
||||||
|
// Transfer output to receiver
|
||||||
|
state.tokens[outputTokenIndex].safeTransfer(receiver, amountOutUint);
|
||||||
|
uint256 balJAfter = IERC20(state.tokens[outputTokenIndex]).balanceOf(address(this));
|
||||||
|
require(balJAfter == prevBalJ - amountOutUint, "swap: non-standard tokenOut");
|
||||||
|
|
||||||
|
// Update cached balances
|
||||||
|
state.cachedUintBalances[inputTokenIndex] = balIAfter;
|
||||||
|
state.cachedUintBalances[outputTokenIndex] = balJAfter;
|
||||||
|
|
||||||
|
// Apply swap to LMSR state
|
||||||
|
state.lmsr.applySwap(inputTokenIndex, outputTokenIndex, amountInInternalUsed, amountOutInternal);
|
||||||
|
|
||||||
|
emit Swap(payer, receiver, state.tokens[inputTokenIndex], state.tokens[outputTokenIndex], totalTransferAmount, amountOutUint);
|
||||||
|
|
||||||
|
return (totalTransferAmount, amountOutUint, feeUint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Execute swap to price limit
|
||||||
|
function swapToLimit(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
address payer,
|
||||||
|
address receiver,
|
||||||
|
uint256 inputTokenIndex,
|
||||||
|
uint256 outputTokenIndex,
|
||||||
|
int128 limitPrice,
|
||||||
|
uint256 deadline,
|
||||||
|
uint256 swapFeePpm
|
||||||
|
) internal returns (uint256 amountInUsed, uint256 amountOut, uint256 fee) {
|
||||||
|
uint256 n = state.tokens.length;
|
||||||
|
require(inputTokenIndex < n && outputTokenIndex < n, "swapToLimit: idx");
|
||||||
|
require(limitPrice > int128(0), "swapToLimit: limit <= 0");
|
||||||
|
require(deadline == 0 || block.timestamp <= deadline, "swapToLimit: deadline exceeded");
|
||||||
|
|
||||||
|
// Read previous balances
|
||||||
|
uint256 prevBalI = IERC20(state.tokens[inputTokenIndex]).balanceOf(address(this));
|
||||||
|
uint256 prevBalJ = IERC20(state.tokens[outputTokenIndex]).balanceOf(address(this));
|
||||||
|
|
||||||
|
// Compute amounts
|
||||||
|
(uint256 totalTransferAmount, uint256 amountOutUint, int128 amountInInternalMax, int128 amountOutInternal, uint256 amountInUsedUint, uint256 feeUint) =
|
||||||
|
_quoteSwapToLimit(state, inputTokenIndex, outputTokenIndex, limitPrice, swapFeePpm);
|
||||||
|
|
||||||
|
// Transfer exact amount from payer
|
||||||
|
state.tokens[inputTokenIndex].safeTransferFrom(payer, address(this), totalTransferAmount);
|
||||||
|
uint256 balIAfter = IERC20(state.tokens[inputTokenIndex]).balanceOf(address(this));
|
||||||
|
require(balIAfter == prevBalI + totalTransferAmount, "swapToLimit: non-standard tokenIn");
|
||||||
|
|
||||||
|
// Transfer output to receiver
|
||||||
|
state.tokens[outputTokenIndex].safeTransfer(receiver, amountOutUint);
|
||||||
|
uint256 balJAfter = IERC20(state.tokens[outputTokenIndex]).balanceOf(address(this));
|
||||||
|
require(balJAfter == prevBalJ - amountOutUint, "swapToLimit: non-standard tokenOut");
|
||||||
|
|
||||||
|
// Update caches
|
||||||
|
state.cachedUintBalances[inputTokenIndex] = balIAfter;
|
||||||
|
state.cachedUintBalances[outputTokenIndex] = balJAfter;
|
||||||
|
|
||||||
|
// Apply swap to LMSR state
|
||||||
|
state.lmsr.applySwap(inputTokenIndex, outputTokenIndex, amountInInternalMax, amountOutInternal);
|
||||||
|
|
||||||
|
emit Swap(payer, receiver, state.tokens[inputTokenIndex], state.tokens[outputTokenIndex], amountInUsedUint, amountOutUint);
|
||||||
|
|
||||||
|
return (amountInUsedUint, amountOutUint, feeUint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Get amounts for burnSwap operation
|
||||||
|
function burnSwapAmounts(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
uint256 lpAmount,
|
||||||
|
uint256 inputTokenIndex,
|
||||||
|
uint256 swapFeePpm,
|
||||||
|
uint256 totalSupply
|
||||||
|
) internal view returns (uint256 amountOut) {
|
||||||
|
uint256 n = state.tokens.length;
|
||||||
|
require(inputTokenIndex < n, "burnSwapAmounts: idx");
|
||||||
|
require(lpAmount > 0, "burnSwapAmounts: zero lp");
|
||||||
|
require(totalSupply > 0, "burnSwapAmounts: empty supply");
|
||||||
|
|
||||||
|
// alpha = lpAmount / supply as Q64.64
|
||||||
|
int128 alpha = ABDKMath64x64.divu(lpAmount, totalSupply);
|
||||||
|
|
||||||
|
// Use LMSR view to compute single-asset payout
|
||||||
|
(int128 payoutInternal, ) = state.lmsr.swapAmountsForBurn(inputTokenIndex, alpha);
|
||||||
|
|
||||||
|
// Convert payoutInternal -> uint (floor) to favor pool
|
||||||
|
uint256 amountOutUint = _internalToUintFloor(payoutInternal, state.bases[inputTokenIndex]);
|
||||||
|
require(amountOutUint > 0, "burnSwapAmounts: output zero");
|
||||||
|
|
||||||
|
// Apply swap fee to the output
|
||||||
|
if (swapFeePpm > 0) {
|
||||||
|
uint256 feeUint = _ceilFee(amountOutUint, swapFeePpm);
|
||||||
|
require(amountOutUint > feeUint, "burnSwapAmounts: fee exceeds output");
|
||||||
|
amountOutUint -= feeUint;
|
||||||
|
}
|
||||||
|
|
||||||
|
return amountOutUint;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Single-token mint (swapMint)
|
||||||
|
function swapMint(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
address payer,
|
||||||
|
address receiver,
|
||||||
|
uint256 inputTokenIndex,
|
||||||
|
uint256 maxAmountIn,
|
||||||
|
uint256 deadline,
|
||||||
|
uint256 swapFeePpm,
|
||||||
|
uint256 totalSupply
|
||||||
|
) internal returns (uint256 lpMinted) {
|
||||||
|
uint256 n = state.tokens.length;
|
||||||
|
require(inputTokenIndex < n, "swapMint: idx");
|
||||||
|
require(maxAmountIn > 0, "swapMint: input zero");
|
||||||
|
require(deadline == 0 || block.timestamp <= deadline, "swapMint: deadline");
|
||||||
|
require(state.lmsr.nAssets > 0, "swapMint: uninit pool");
|
||||||
|
|
||||||
|
// Calculate amounts using view function
|
||||||
|
(uint256 totalTransfer, uint256 amountInUint, uint256 feeUintActual, uint256 actualLpToMint) = swapMintAmounts(
|
||||||
|
state, inputTokenIndex, maxAmountIn, swapFeePpm, totalSupply
|
||||||
|
);
|
||||||
|
|
||||||
|
// Record pre-balance and transfer tokens
|
||||||
|
uint256 prevBalI = IERC20(state.tokens[inputTokenIndex]).balanceOf(address(this));
|
||||||
|
state.tokens[inputTokenIndex].safeTransferFrom(payer, address(this), totalTransfer);
|
||||||
|
uint256 balIAfter = IERC20(state.tokens[inputTokenIndex]).balanceOf(address(this));
|
||||||
|
require(balIAfter == prevBalI + totalTransfer, "swapMint: non-standard tokenIn");
|
||||||
|
|
||||||
|
// Update cached uint balances
|
||||||
|
state.cachedUintBalances[inputTokenIndex] = balIAfter;
|
||||||
|
|
||||||
|
// Update LMSR internal state
|
||||||
|
int128 oldTotal = _computeSizeMetric(state.lmsr.qInternal);
|
||||||
|
(, int128 sizeIncreaseInternal) = state.lmsr.swapAmountsForMint(inputTokenIndex, _uintToInternalFloor(amountInUint, state.bases[inputTokenIndex]));
|
||||||
|
int128 newTotal = oldTotal.add(sizeIncreaseInternal);
|
||||||
|
|
||||||
|
int128[] memory newQInternal = new int128[](n);
|
||||||
|
for (uint256 idx = 0; idx < n; idx++) {
|
||||||
|
newQInternal[idx] = state.lmsr.qInternal[idx].mul(newTotal).div(oldTotal);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.lmsr.updateForProportionalChange(newQInternal);
|
||||||
|
|
||||||
|
emit SwapMint(payer, receiver, inputTokenIndex, totalTransfer, amountInUint, feeUintActual);
|
||||||
|
emit Mint(payer, receiver, new uint256[](n), actualLpToMint);
|
||||||
|
|
||||||
|
return actualLpToMint;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Burn LP tokens and swap to single asset (burnSwap)
|
||||||
|
function burnSwap(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
address payer,
|
||||||
|
address receiver,
|
||||||
|
uint256 lpAmount,
|
||||||
|
uint256 inputTokenIndex,
|
||||||
|
uint256 deadline,
|
||||||
|
uint256 swapFeePpm,
|
||||||
|
uint256 totalSupply,
|
||||||
|
uint256 payerBalance
|
||||||
|
) internal returns (uint256 amountOutUint) {
|
||||||
|
uint256 n = state.tokens.length;
|
||||||
|
require(inputTokenIndex < n, "burnSwap: idx");
|
||||||
|
require(lpAmount > 0, "burnSwap: zero lp");
|
||||||
|
require(deadline == 0 || block.timestamp <= deadline, "burnSwap: deadline");
|
||||||
|
require(totalSupply > 0, "burnSwap: empty supply");
|
||||||
|
require(payerBalance >= lpAmount, "burnSwap: insufficient LP");
|
||||||
|
|
||||||
|
// Calculate amounts using view function
|
||||||
|
amountOutUint = burnSwapAmounts(state, lpAmount, inputTokenIndex, swapFeePpm, totalSupply);
|
||||||
|
|
||||||
|
// Transfer the payout to receiver
|
||||||
|
state.tokens[inputTokenIndex].safeTransfer(receiver, amountOutUint);
|
||||||
|
|
||||||
|
// Update cached balances by reading on-chain balances for all tokens
|
||||||
|
int128[] memory newQInternal = new int128[](n);
|
||||||
|
for (uint256 idx = 0; idx < n; idx++) {
|
||||||
|
uint256 bal = IERC20(state.tokens[idx]).balanceOf(address(this));
|
||||||
|
state.cachedUintBalances[idx] = bal;
|
||||||
|
newQInternal[idx] = _uintToInternalFloor(bal, state.bases[idx]);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit BurnSwap(payer, receiver, inputTokenIndex, amountOutUint);
|
||||||
|
|
||||||
|
// If entire pool drained, deinit; else update proportionally
|
||||||
|
bool allZero = true;
|
||||||
|
for (uint256 idx = 0; idx < n; idx++) {
|
||||||
|
if (newQInternal[idx] != int128(0)) { allZero = false; break; }
|
||||||
|
}
|
||||||
|
if (allZero) {
|
||||||
|
state.lmsr.deinit();
|
||||||
|
} else {
|
||||||
|
state.lmsr.updateForProportionalChange(newQInternal);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit Burn(payer, receiver, new uint256[](n), lpAmount);
|
||||||
|
return amountOutUint;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Calculate flash loan repayment amounts
|
||||||
|
function flashRepaymentAmounts(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
uint256[] memory loanAmounts,
|
||||||
|
uint256 flashFeePpm
|
||||||
|
) internal view returns (uint256[] memory repaymentAmounts) {
|
||||||
|
repaymentAmounts = new uint256[](state.tokens.length);
|
||||||
|
for (uint256 i = 0; i < state.tokens.length; i++) {
|
||||||
|
uint256 amount = loanAmounts[i];
|
||||||
|
if (amount > 0) {
|
||||||
|
repaymentAmounts[i] = amount + _ceilFee(amount, flashFeePpm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Execute flash loan
|
||||||
|
function flash(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
address recipient,
|
||||||
|
uint256[] memory amounts,
|
||||||
|
bytes calldata data,
|
||||||
|
uint256 flashFeePpm
|
||||||
|
) internal {
|
||||||
|
require(recipient != address(0), "flash: zero recipient");
|
||||||
|
require(amounts.length == state.tokens.length, "flash: amounts length mismatch");
|
||||||
|
|
||||||
|
// Calculate repayment amounts for each token including fee
|
||||||
|
uint256[] memory repaymentAmounts = new uint256[](state.tokens.length);
|
||||||
|
|
||||||
|
// Store initial balances to verify repayment later
|
||||||
|
uint256[] memory initialBalances = new uint256[](state.tokens.length);
|
||||||
|
|
||||||
|
// Track if any token amount is non-zero
|
||||||
|
bool hasNonZeroAmount = false;
|
||||||
|
|
||||||
|
// Process each token, skipping those with zero amounts
|
||||||
|
for (uint256 i = 0; i < state.tokens.length; i++) {
|
||||||
|
uint256 amount = amounts[i];
|
||||||
|
|
||||||
|
if (amount > 0) {
|
||||||
|
hasNonZeroAmount = true;
|
||||||
|
|
||||||
|
// Calculate repayment amount with fee (ceiling)
|
||||||
|
repaymentAmounts[i] = amount + _ceilFee(amount, flashFeePpm);
|
||||||
|
|
||||||
|
// Record initial balance
|
||||||
|
initialBalances[i] = IERC20(state.tokens[i]).balanceOf(address(this));
|
||||||
|
|
||||||
|
// Transfer token to recipient
|
||||||
|
state.tokens[i].safeTransfer(recipient, amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure at least one token is being borrowed
|
||||||
|
require(hasNonZeroAmount, "flash: no tokens requested");
|
||||||
|
|
||||||
|
// Call flash callback with expected repayment amounts
|
||||||
|
IPartyFlashCallback(msg.sender).partyFlashCallback(amounts, repaymentAmounts, data);
|
||||||
|
|
||||||
|
// Verify repayment amounts for tokens that were borrowed
|
||||||
|
for (uint256 i = 0; i < state.tokens.length; i++) {
|
||||||
|
if (amounts[i] > 0) {
|
||||||
|
uint256 currentBalance = IERC20(state.tokens[i]).balanceOf(address(this));
|
||||||
|
|
||||||
|
// Verify repayment: current balance must be at least (initial balance + fee)
|
||||||
|
require(
|
||||||
|
currentBalance >= initialBalances[i] + _ceilFee(amounts[i], flashFeePpm),
|
||||||
|
"flash: repayment failed"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update cached balance
|
||||||
|
state.cachedUintBalances[i] = currentBalance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Get marginal price between two tokens
|
||||||
|
function price(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
uint256 baseTokenIndex,
|
||||||
|
uint256 quoteTokenIndex
|
||||||
|
) internal view returns (int128) {
|
||||||
|
uint256 n = state.tokens.length;
|
||||||
|
require(baseTokenIndex < n && quoteTokenIndex < n, "price: idx");
|
||||||
|
require(state.lmsr.nAssets > 0, "price: uninit");
|
||||||
|
return state.lmsr.price(baseTokenIndex, quoteTokenIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Get price of one LP token in quote asset
|
||||||
|
function poolPrice(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
uint256 quoteTokenIndex,
|
||||||
|
uint256 totalSupply
|
||||||
|
) internal view returns (int128) {
|
||||||
|
uint256 n = state.tokens.length;
|
||||||
|
require(quoteTokenIndex < n, "poolPrice: idx");
|
||||||
|
require(state.lmsr.nAssets > 0, "poolPrice: uninit");
|
||||||
|
|
||||||
|
// price per unit of qTotal (Q64.64) from LMSR
|
||||||
|
int128 pricePerQ = state.lmsr.poolPrice(quoteTokenIndex);
|
||||||
|
|
||||||
|
// total internal q (qTotal) as Q64.64
|
||||||
|
int128 qTotal = _computeSizeMetric(state.lmsr.qInternal);
|
||||||
|
require(qTotal > int128(0), "poolPrice: qTotal zero");
|
||||||
|
|
||||||
|
// totalSupply as Q64.64
|
||||||
|
require(totalSupply > 0, "poolPrice: zero supply");
|
||||||
|
int128 supplyQ64 = ABDKMath64x64.fromUInt(totalSupply);
|
||||||
|
|
||||||
|
// factor = totalSupply / qTotal (Q64.64)
|
||||||
|
int128 factor = supplyQ64.div(qTotal);
|
||||||
|
|
||||||
|
// price per LP token = pricePerQ * factor (Q64.64)
|
||||||
|
return pricePerQ.mul(factor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal helper functions
|
||||||
|
|
||||||
|
/// @notice Ceiling fee helper
|
||||||
|
function _ceilFee(uint256 x, uint256 feePpm) internal pure returns (uint256) {
|
||||||
|
if (feePpm == 0) return 0;
|
||||||
|
return (x * feePpm + 1_000_000 - 1) / 1_000_000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Compute fee and net amounts for a gross input
|
||||||
|
function _computeFee(uint256 gross, uint256 swapFeePpm) internal pure returns (uint256 feeUint, uint256 netUint) {
|
||||||
|
if (swapFeePpm == 0) {
|
||||||
|
return (0, gross);
|
||||||
|
}
|
||||||
|
feeUint = _ceilFee(gross, swapFeePpm);
|
||||||
|
netUint = gross - feeUint;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Add fee to net amount
|
||||||
|
function _addFee(uint256 netUint, uint256 swapFeePpm) internal pure returns (uint256 gross) {
|
||||||
|
if (swapFeePpm == 0) return netUint;
|
||||||
|
uint256 fee = _ceilFee(netUint, swapFeePpm);
|
||||||
|
return netUint + fee;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Internal quote for exact-input swap
|
||||||
|
function _quoteSwapExactIn(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
uint256 inputTokenIndex,
|
||||||
|
uint256 outputTokenIndex,
|
||||||
|
uint256 maxAmountIn,
|
||||||
|
int128 limitPrice,
|
||||||
|
uint256 swapFeePpm
|
||||||
|
)
|
||||||
|
internal
|
||||||
|
view
|
||||||
|
returns (
|
||||||
|
uint256 grossIn,
|
||||||
|
uint256 amountOutUint,
|
||||||
|
int128 amountInInternalUsed,
|
||||||
|
int128 amountOutInternal,
|
||||||
|
uint256 amountInUintNoFee,
|
||||||
|
uint256 feeUint
|
||||||
|
)
|
||||||
|
{
|
||||||
|
uint256 n = state.tokens.length;
|
||||||
|
require(inputTokenIndex < n && outputTokenIndex < n, "swap: idx");
|
||||||
|
require(maxAmountIn > 0, "swap: input zero");
|
||||||
|
require(state.lmsr.nAssets > 0, "swap: empty pool");
|
||||||
|
|
||||||
|
// Estimate max net input
|
||||||
|
(, uint256 netUintForSwap) = _computeFee(maxAmountIn, swapFeePpm);
|
||||||
|
|
||||||
|
// Convert to internal (floor)
|
||||||
|
int128 deltaInternalI = _uintToInternalFloor(netUintForSwap, state.bases[inputTokenIndex]);
|
||||||
|
require(deltaInternalI > int128(0), "swap: input too small after fee");
|
||||||
|
|
||||||
|
// Compute internal amounts using LMSR
|
||||||
|
(amountInInternalUsed, amountOutInternal) = state.lmsr.swapAmountsForExactInput(
|
||||||
|
inputTokenIndex, outputTokenIndex, deltaInternalI, limitPrice);
|
||||||
|
|
||||||
|
// Convert actual used input internal -> uint (ceil)
|
||||||
|
amountInUintNoFee = _internalToUintCeil(amountInInternalUsed, state.bases[inputTokenIndex]);
|
||||||
|
require(amountInUintNoFee > 0, "swap: input zero");
|
||||||
|
|
||||||
|
// Compute gross transfer including fee
|
||||||
|
feeUint = 0;
|
||||||
|
grossIn = amountInUintNoFee;
|
||||||
|
if (swapFeePpm > 0) {
|
||||||
|
feeUint = _ceilFee(amountInUintNoFee, swapFeePpm);
|
||||||
|
grossIn += feeUint;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure within user max
|
||||||
|
require(grossIn <= maxAmountIn, "swap: transfer exceeds max");
|
||||||
|
|
||||||
|
// Compute output (floor)
|
||||||
|
amountOutUint = _internalToUintFloor(amountOutInternal, state.bases[outputTokenIndex]);
|
||||||
|
require(amountOutUint > 0, "swap: output zero");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Internal quote for exact-input swap
|
||||||
|
function _quoteSwapExactIn_BalancedPair(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
uint256 inputTokenIndex,
|
||||||
|
uint256 outputTokenIndex,
|
||||||
|
uint256 maxAmountIn,
|
||||||
|
int128 limitPrice,
|
||||||
|
uint256 swapFeePpm
|
||||||
|
)
|
||||||
|
internal
|
||||||
|
view
|
||||||
|
returns (
|
||||||
|
uint256 grossIn,
|
||||||
|
uint256 amountOutUint,
|
||||||
|
int128 amountInInternalUsed,
|
||||||
|
int128 amountOutInternal,
|
||||||
|
uint256 amountInUintNoFee,
|
||||||
|
uint256 feeUint
|
||||||
|
)
|
||||||
|
{
|
||||||
|
uint256 n = state.tokens.length;
|
||||||
|
require(inputTokenIndex < n && outputTokenIndex < n, "swap: idx");
|
||||||
|
require(maxAmountIn > 0, "swap: input zero");
|
||||||
|
require(state.lmsr.nAssets > 0, "swap: empty pool");
|
||||||
|
|
||||||
|
// Estimate max net input
|
||||||
|
(, uint256 netUintForSwap) = _computeFee(maxAmountIn, swapFeePpm);
|
||||||
|
|
||||||
|
// Convert to internal (floor)
|
||||||
|
int128 deltaInternalI = _uintToInternalFloor(netUintForSwap, state.bases[inputTokenIndex]);
|
||||||
|
require(deltaInternalI > int128(0), "swap: input too small after fee");
|
||||||
|
|
||||||
|
// Compute internal amounts using LMSR
|
||||||
|
(amountInInternalUsed, amountOutInternal) = LMSRStabilizedBalancedPair.swapAmountsForExactInput(
|
||||||
|
state.lmsr, inputTokenIndex, outputTokenIndex, deltaInternalI, limitPrice);
|
||||||
|
|
||||||
|
// Convert actual used input internal -> uint (ceil)
|
||||||
|
amountInUintNoFee = _internalToUintCeil(amountInInternalUsed, state.bases[inputTokenIndex]);
|
||||||
|
require(amountInUintNoFee > 0, "swap: input zero");
|
||||||
|
|
||||||
|
// Compute gross transfer including fee
|
||||||
|
feeUint = 0;
|
||||||
|
grossIn = amountInUintNoFee;
|
||||||
|
if (swapFeePpm > 0) {
|
||||||
|
feeUint = _ceilFee(amountInUintNoFee, swapFeePpm);
|
||||||
|
grossIn += feeUint;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure within user max
|
||||||
|
require(grossIn <= maxAmountIn, "swap: transfer exceeds max");
|
||||||
|
|
||||||
|
// Compute output (floor)
|
||||||
|
amountOutUint = _internalToUintFloor(amountOutInternal, state.bases[outputTokenIndex]);
|
||||||
|
require(amountOutUint > 0, "swap: output zero");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Internal quote for swap-to-limit
|
||||||
|
function _quoteSwapToLimit(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
uint256 inputTokenIndex,
|
||||||
|
uint256 outputTokenIndex,
|
||||||
|
int128 limitPrice,
|
||||||
|
uint256 swapFeePpm
|
||||||
|
)
|
||||||
|
internal
|
||||||
|
view
|
||||||
|
returns (
|
||||||
|
uint256 grossIn,
|
||||||
|
uint256 amountOutUint,
|
||||||
|
int128 amountInInternal,
|
||||||
|
int128 amountOutInternal,
|
||||||
|
uint256 amountInUintNoFee,
|
||||||
|
uint256 feeUint
|
||||||
|
)
|
||||||
|
{
|
||||||
|
uint256 n = state.tokens.length;
|
||||||
|
require(inputTokenIndex < n && outputTokenIndex < n, "swapToLimit: idx");
|
||||||
|
require(limitPrice > int128(0), "swapToLimit: limit <= 0");
|
||||||
|
require(state.lmsr.nAssets > 0, "swapToLimit: pool uninitialized");
|
||||||
|
|
||||||
|
// Compute internal maxima at the price limit
|
||||||
|
(amountInInternal, amountOutInternal) = state.lmsr.swapAmountsForPriceLimit(inputTokenIndex, outputTokenIndex, limitPrice);
|
||||||
|
|
||||||
|
// Convert input to uint (ceil) and output to uint (floor)
|
||||||
|
amountInUintNoFee = _internalToUintCeil(amountInInternal, state.bases[inputTokenIndex]);
|
||||||
|
require(amountInUintNoFee > 0, "swapToLimit: input zero");
|
||||||
|
|
||||||
|
feeUint = 0;
|
||||||
|
grossIn = amountInUintNoFee;
|
||||||
|
if (swapFeePpm > 0) {
|
||||||
|
feeUint = _ceilFee(amountInUintNoFee, swapFeePpm);
|
||||||
|
grossIn += feeUint;
|
||||||
|
}
|
||||||
|
|
||||||
|
amountOutUint = _internalToUintFloor(amountOutInternal, state.bases[outputTokenIndex]);
|
||||||
|
require(amountOutUint > 0, "swapToLimit: output zero");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert uint token amount -> internal 64.64 (floor)
|
||||||
|
function _uintToInternalFloor(uint256 amount, uint256 base) internal pure returns (int128) {
|
||||||
|
return ABDKMath64x64.divu(amount, base);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert internal 64.64 -> uint token amount (floor)
|
||||||
|
function _internalToUintFloor(int128 internalAmount, uint256 base) internal pure returns (uint256) {
|
||||||
|
return ABDKMath64x64.mulu(internalAmount, base);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert internal 64.64 -> uint token amount (ceiling)
|
||||||
|
function _internalToUintCeil(int128 internalAmount, uint256 base) internal pure returns (uint256) {
|
||||||
|
// Get the floor value first
|
||||||
|
uint256 floorValue = ABDKMath64x64.mulu(internalAmount, base);
|
||||||
|
|
||||||
|
// Check if there was any fractional part by comparing to a reconstruction
|
||||||
|
int128 reconstructed = ABDKMath64x64.divu(floorValue, base);
|
||||||
|
|
||||||
|
// If reconstructed is less than original, there was a fractional part that was truncated
|
||||||
|
if (reconstructed < internalAmount) {
|
||||||
|
return floorValue + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return floorValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Helper to compute size metric (sum of all asset quantities) from internal balances
|
||||||
|
function _computeSizeMetric(int128[] memory qInternal_) internal pure returns (int128) {
|
||||||
|
int128 total = int128(0);
|
||||||
|
for (uint i = 0; i < qInternal_.length; ) {
|
||||||
|
total = total.add(qInternal_[i]);
|
||||||
|
unchecked { i++; }
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
}
|
||||||
139
src/PoolLibBalancedPair.sol
Normal file
139
src/PoolLibBalancedPair.sol
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
|
pragma solidity ^0.8.30;
|
||||||
|
|
||||||
|
import "@abdk/ABDKMath64x64.sol";
|
||||||
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||||
|
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||||
|
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||||
|
import "./LMSRStabilized.sol";
|
||||||
|
import "./LMSRStabilizedBalancedPair.sol";
|
||||||
|
import "./IPartyFlashCallback.sol";
|
||||||
|
import {PoolLib} from "./PoolLib.sol";
|
||||||
|
|
||||||
|
/// @title PoolLibBalancedPair - Library with optimized functions for stablecoin pair pools
|
||||||
|
/// @dev All functions are internal and accept State as the first parameter
|
||||||
|
library PoolLibBalancedPair {
|
||||||
|
using ABDKMath64x64 for int128;
|
||||||
|
using LMSRStabilized for LMSRStabilized.State;
|
||||||
|
using PoolLib for PoolLib.State;
|
||||||
|
using SafeERC20 for IERC20;
|
||||||
|
|
||||||
|
/// @notice Get swap amounts for exact input swap
|
||||||
|
function swapAmounts(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
uint256 inputTokenIndex,
|
||||||
|
uint256 outputTokenIndex,
|
||||||
|
uint256 maxAmountIn,
|
||||||
|
int128 limitPrice,
|
||||||
|
uint256 swapFeePpm
|
||||||
|
) internal view returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
|
||||||
|
(uint256 grossIn, uint256 outUint,,,, uint256 feeUint) = _quoteSwapExactIn(
|
||||||
|
state, inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice, swapFeePpm
|
||||||
|
);
|
||||||
|
return (grossIn, outUint, feeUint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Internal quote for exact-input swap
|
||||||
|
function _quoteSwapExactIn(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
uint256 inputTokenIndex,
|
||||||
|
uint256 outputTokenIndex,
|
||||||
|
uint256 maxAmountIn,
|
||||||
|
int128 limitPrice,
|
||||||
|
uint256 swapFeePpm
|
||||||
|
)
|
||||||
|
internal
|
||||||
|
view
|
||||||
|
returns (
|
||||||
|
uint256 grossIn,
|
||||||
|
uint256 amountOutUint,
|
||||||
|
int128 amountInInternalUsed,
|
||||||
|
int128 amountOutInternal,
|
||||||
|
uint256 amountInUintNoFee,
|
||||||
|
uint256 feeUint
|
||||||
|
)
|
||||||
|
{
|
||||||
|
uint256 n = state.tokens.length;
|
||||||
|
require(inputTokenIndex < n && outputTokenIndex < n, "swap: idx");
|
||||||
|
require(maxAmountIn > 0, "swap: input zero");
|
||||||
|
require(state.lmsr.nAssets > 0, "swap: empty pool");
|
||||||
|
|
||||||
|
// Estimate max net input
|
||||||
|
(, uint256 netUintForSwap) = PoolLib._computeFee(maxAmountIn, swapFeePpm);
|
||||||
|
|
||||||
|
// Convert to internal (floor)
|
||||||
|
int128 deltaInternalI = PoolLib._uintToInternalFloor(netUintForSwap, state.bases[inputTokenIndex]);
|
||||||
|
require(deltaInternalI > int128(0), "swap: input too small after fee");
|
||||||
|
|
||||||
|
// Compute internal amounts using LMSR
|
||||||
|
(amountInInternalUsed, amountOutInternal) = LMSRStabilizedBalancedPair.swapAmountsForExactInput(state.lmsr,
|
||||||
|
inputTokenIndex, outputTokenIndex, deltaInternalI, limitPrice);
|
||||||
|
|
||||||
|
// Convert actual used input internal -> uint (ceil)
|
||||||
|
amountInUintNoFee = PoolLib._internalToUintCeil(amountInInternalUsed, state.bases[inputTokenIndex]);
|
||||||
|
require(amountInUintNoFee > 0, "swap: input zero");
|
||||||
|
|
||||||
|
// Compute gross transfer including fee
|
||||||
|
feeUint = 0;
|
||||||
|
grossIn = amountInUintNoFee;
|
||||||
|
if (swapFeePpm > 0) {
|
||||||
|
feeUint = PoolLib._ceilFee(amountInUintNoFee, swapFeePpm);
|
||||||
|
grossIn += feeUint;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure within user max
|
||||||
|
require(grossIn <= maxAmountIn, "swap: transfer exceeds max");
|
||||||
|
|
||||||
|
// Compute output (floor)
|
||||||
|
amountOutUint = PoolLib._internalToUintFloor(amountOutInternal, state.bases[outputTokenIndex]);
|
||||||
|
require(amountOutUint > 0, "swap: output zero");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Execute exact input swap
|
||||||
|
function swap(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
address payer,
|
||||||
|
address receiver,
|
||||||
|
uint256 inputTokenIndex,
|
||||||
|
uint256 outputTokenIndex,
|
||||||
|
uint256 maxAmountIn,
|
||||||
|
int128 limitPrice,
|
||||||
|
uint256 deadline,
|
||||||
|
uint256 swapFeePpm
|
||||||
|
) internal returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
|
||||||
|
uint256 n = state.tokens.length;
|
||||||
|
require(inputTokenIndex < n && outputTokenIndex < n, "swap: idx");
|
||||||
|
require(maxAmountIn > 0, "swap: input zero");
|
||||||
|
require(deadline == 0 || block.timestamp <= deadline, "swap: deadline exceeded");
|
||||||
|
|
||||||
|
// Read previous balances
|
||||||
|
uint256 prevBalI = IERC20(state.tokens[inputTokenIndex]).balanceOf(address(this));
|
||||||
|
uint256 prevBalJ = IERC20(state.tokens[outputTokenIndex]).balanceOf(address(this));
|
||||||
|
|
||||||
|
// Compute amounts
|
||||||
|
(uint256 totalTransferAmount, uint256 amountOutUint, int128 amountInInternalUsed, int128 amountOutInternal, , uint256 feeUint) =
|
||||||
|
_quoteSwapExactIn(state, inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice, swapFeePpm);
|
||||||
|
|
||||||
|
// Transfer exact amount from payer
|
||||||
|
state.tokens[inputTokenIndex].safeTransferFrom(payer, address(this), totalTransferAmount);
|
||||||
|
uint256 balIAfter = IERC20(state.tokens[inputTokenIndex]).balanceOf(address(this));
|
||||||
|
require(balIAfter == prevBalI + totalTransferAmount, "swap: non-standard tokenIn");
|
||||||
|
|
||||||
|
// Transfer output to receiver
|
||||||
|
state.tokens[outputTokenIndex].safeTransfer(receiver, amountOutUint);
|
||||||
|
uint256 balJAfter = IERC20(state.tokens[outputTokenIndex]).balanceOf(address(this));
|
||||||
|
require(balJAfter == prevBalJ - amountOutUint, "swap: non-standard tokenOut");
|
||||||
|
|
||||||
|
// Update cached balances
|
||||||
|
state.cachedUintBalances[inputTokenIndex] = balIAfter;
|
||||||
|
state.cachedUintBalances[outputTokenIndex] = balJAfter;
|
||||||
|
|
||||||
|
// Apply swap to LMSR state
|
||||||
|
state.lmsr.applySwap(inputTokenIndex, outputTokenIndex, amountInInternalUsed, amountOutInternal);
|
||||||
|
|
||||||
|
emit PoolLib.Swap(payer, receiver, state.tokens[inputTokenIndex], state.tokens[outputTokenIndex], totalTransferAmount, amountOutUint);
|
||||||
|
|
||||||
|
return (totalTransferAmount, amountOutUint, feeUint);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
173
test/GasTest.sol
173
test/GasTest.sol
@@ -1,77 +1,111 @@
|
|||||||
// SPDX-License-Identifier: UNLICENSED
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
pragma solidity ^0.8.30;
|
pragma solidity ^0.8.30;
|
||||||
/* solhint-disable erc20-unchecked-transfer */
|
|
||||||
|
|
||||||
import "forge-std/Test.sol";
|
import "../src/Deploy.sol";
|
||||||
import "@abdk/ABDKMath64x64.sol";
|
import "../src/IPartyFlashCallback.sol";
|
||||||
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
|
||||||
import "../src/LMSRStabilized.sol";
|
import "../src/LMSRStabilized.sol";
|
||||||
import "../src/PartyPool.sol";
|
import "../src/PartyPool.sol";
|
||||||
import "../src/PartyPlanner.sol";
|
import "@abdk/ABDKMath64x64.sol";
|
||||||
import "../lib/openzeppelin-contracts/contracts/interfaces/IERC3156FlashBorrower.sol";
|
|
||||||
import {Deploy} from "../src/Deploy.sol";
|
// Import the flash callback interface
|
||||||
|
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||||
|
import "forge-std/Test.sol";
|
||||||
|
|
||||||
/// @notice Test contract that implements the flash callback for testing flash loans
|
/// @notice Test contract that implements the flash callback for testing flash loans
|
||||||
contract FlashBorrower is IERC3156FlashBorrower {
|
contract FlashBorrower is IPartyFlashCallback {
|
||||||
enum Action {
|
enum Action {
|
||||||
NORMAL, // Normal repayment
|
NORMAL, // Normal repayment
|
||||||
REPAY_NONE, // Don't repay anything
|
REPAY_NONE, // Don't repay anything
|
||||||
REPAY_PARTIAL, // Repay less than required
|
REPAY_PARTIAL, // Repay less than required
|
||||||
REPAY_NO_FEE, // Repay only the principal without fee
|
REPAY_NO_FEE, // Repay only the principal without fee
|
||||||
REPAY_EXACT // Repay exactly the required amount
|
REPAY_EXACT, // Repay exactly the required amount
|
||||||
|
REPAY_EXTRA // Repay more than required (donation)
|
||||||
}
|
}
|
||||||
|
|
||||||
Action public action;
|
Action public action;
|
||||||
address public pool;
|
address public pool;
|
||||||
address public payer;
|
address public recipient;
|
||||||
|
address[] public tokens;
|
||||||
|
|
||||||
constructor(address _pool) {
|
constructor(address _pool, IERC20[] memory _tokens) {
|
||||||
pool = _pool;
|
pool = _pool;
|
||||||
|
tokens = new address[](_tokens.length);
|
||||||
|
for (uint i = 0; i < _tokens.length; i++) {
|
||||||
|
tokens[i] = address(_tokens[i]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setAction(Action _action, address _payer) external {
|
function setAction(Action _action, address _recipient) external {
|
||||||
action = _action;
|
action = _action;
|
||||||
payer = _payer;
|
recipient = _recipient;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onFlashLoan(
|
function flash(uint256[] memory amounts) external {
|
||||||
address /*initiator*/,
|
PartyPool(pool).flash(recipient, amounts, "");
|
||||||
address token,
|
}
|
||||||
uint256 amount,
|
|
||||||
uint256 fee,
|
function partyFlashCallback(
|
||||||
|
uint256[] memory loanAmounts,
|
||||||
|
uint256[] memory repaymentAmounts,
|
||||||
bytes calldata /* data */
|
bytes calldata /* data */
|
||||||
) external override returns (bytes32) {
|
) external override {
|
||||||
require(msg.sender == pool, "Callback not called by pool");
|
require(msg.sender == pool, "Callback not called by pool");
|
||||||
|
|
||||||
if (action == Action.NORMAL) {
|
if (action == Action.NORMAL || action == Action.REPAY_EXTRA) {
|
||||||
// Normal repayment
|
// Normal or extra repayment - transfer required amounts back to pool
|
||||||
// We received 'amount' from the pool, need to pay back amount + fee
|
for (uint256 i = 0; i < loanAmounts.length; i++) {
|
||||||
uint256 repaymentAmount = amount + fee;
|
if (loanAmounts[i] > 0) {
|
||||||
|
uint256 repaymentAmount = repaymentAmounts[i];
|
||||||
|
|
||||||
// Transfer the fee from payer to this contract
|
// For REPAY_EXTRA, add 1 to each repayment
|
||||||
// (we already have the principal 'amount' from the flash loan)
|
if (action == Action.REPAY_EXTRA) {
|
||||||
TestERC20(token).transferFrom(payer, address(this), fee);
|
repaymentAmount += 1;
|
||||||
|
}
|
||||||
|
|
||||||
// Approve pool to pull back the full repayment
|
// Transfer from recipient back to pool
|
||||||
TestERC20(token).approve(pool, repaymentAmount);
|
TestERC20(tokens[i]).transferFrom(
|
||||||
|
recipient,
|
||||||
|
pool,
|
||||||
|
repaymentAmount
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (action == Action.REPAY_PARTIAL) {
|
} else if (action == Action.REPAY_PARTIAL) {
|
||||||
// Repay half of the required amount
|
// Repay half of the required amounts
|
||||||
uint256 partialRepayment = (amount + fee) / 2;
|
for (uint256 i = 0; i < loanAmounts.length; i++) {
|
||||||
TestERC20(token).approve(pool, partialRepayment);
|
if (loanAmounts[i] > 0) {
|
||||||
|
uint256 partialRepayment = repaymentAmounts[i] / 2;
|
||||||
|
TestERC20(tokens[i]).transferFrom(
|
||||||
|
recipient,
|
||||||
|
pool,
|
||||||
|
partialRepayment
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (action == Action.REPAY_NO_FEE) {
|
} else if (action == Action.REPAY_NO_FEE) {
|
||||||
// Repay only the principal without fee (we already have it from the loan)
|
// Repay only the principal without fee
|
||||||
TestERC20(token).approve(pool, amount);
|
for (uint256 i = 0; i < loanAmounts.length; i++) {
|
||||||
|
if (loanAmounts[i] > 0) {
|
||||||
|
TestERC20(tokens[i]).transferFrom(
|
||||||
|
recipient,
|
||||||
|
pool,
|
||||||
|
loanAmounts[i]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (action == Action.REPAY_EXACT) {
|
} else if (action == Action.REPAY_EXACT) {
|
||||||
// Repay exactly what was required
|
// Repay exactly what was required
|
||||||
uint256 repaymentAmount = amount + fee;
|
for (uint256 i = 0; i < loanAmounts.length; i++) {
|
||||||
// Transfer the fee from payer (we have the principal from the loan)
|
if (loanAmounts[i] > 0) {
|
||||||
TestERC20(token).transferFrom(payer, address(this), fee);
|
TestERC20(tokens[i]).transferFrom(
|
||||||
// Approve pool to pull back the full repayment
|
recipient,
|
||||||
TestERC20(token).approve(pool, repaymentAmount);
|
pool,
|
||||||
|
repaymentAmounts[i]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// For REPAY_NONE, do nothing (don't approve repayment)
|
// For REPAY_NONE, do nothing (don't repay)
|
||||||
|
|
||||||
return keccak256("ERC3156FlashBorrower.onFlashLoan");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,23 +130,21 @@ contract TestERC20 is ERC20 {
|
|||||||
/// @notice Gas testing contract for PartyPool - contains all gas measurement tests
|
/// @notice Gas testing contract for PartyPool - contains all gas measurement tests
|
||||||
contract GasTest is Test {
|
contract GasTest is Test {
|
||||||
using ABDKMath64x64 for int128;
|
using ABDKMath64x64 for int128;
|
||||||
using SafeERC20 for TestERC20;
|
|
||||||
|
|
||||||
PartyPlanner internal planner;
|
PartyPool pool2;
|
||||||
PartyPool internal pool2;
|
PartyPool pool10;
|
||||||
PartyPool internal pool10;
|
PartyPool pool20;
|
||||||
PartyPool internal pool20;
|
PartyPool pool50;
|
||||||
PartyPool internal pool50;
|
|
||||||
|
|
||||||
address internal alice;
|
address alice;
|
||||||
address internal bob;
|
address bob;
|
||||||
|
|
||||||
// Common parameters
|
// Common parameters
|
||||||
int128 internal tradeFrac;
|
int128 tradeFrac;
|
||||||
int128 internal targetSlippage;
|
int128 targetSlippage;
|
||||||
|
|
||||||
uint256 constant internal INIT_BAL = 1_000_000; // initial token units for each token (internal==amount when base==1)
|
uint256 constant INIT_BAL = 1_000_000; // initial token units for each token (internal==amount when base==1)
|
||||||
uint256 constant internal BASE = 1; // use base=1 so internal amounts correspond to raw integers (Q64.64 units)
|
uint256 constant BASE = 1; // use base=1 so internal amounts correspond to raw integers (Q64.64 units)
|
||||||
|
|
||||||
/// @notice Helper function to create a pool with the specified number of tokens
|
/// @notice Helper function to create a pool with the specified number of tokens
|
||||||
function createPool(uint256 numTokens) internal returns (PartyPool) {
|
function createPool(uint256 numTokens) internal returns (PartyPool) {
|
||||||
@@ -198,8 +230,6 @@ contract GasTest is Test {
|
|||||||
alice = address(0xA11ce);
|
alice = address(0xA11ce);
|
||||||
bob = address(0xB0b);
|
bob = address(0xB0b);
|
||||||
|
|
||||||
planner = Deploy.newPartyPlanner();
|
|
||||||
|
|
||||||
// Configure LMSR parameters similar to other tests: trade size 1% of asset -> 0.01, slippage 0.001
|
// Configure LMSR parameters similar to other tests: trade size 1% of asset -> 0.01, slippage 0.001
|
||||||
tradeFrac = ABDKMath64x64.divu(100, 10_000); // 0.01
|
tradeFrac = ABDKMath64x64.divu(100, 10_000); // 0.01
|
||||||
targetSlippage = ABDKMath64x64.divu(10, 10_000); // 0.001
|
targetSlippage = ABDKMath64x64.divu(10, 10_000); // 0.001
|
||||||
@@ -213,11 +243,13 @@ contract GasTest is Test {
|
|||||||
|
|
||||||
/// @notice Setup a flash borrower for testing
|
/// @notice Setup a flash borrower for testing
|
||||||
function setupFlashBorrower() internal returns (FlashBorrower borrower) {
|
function setupFlashBorrower() internal returns (FlashBorrower borrower) {
|
||||||
|
// Get token addresses from the 2-token pool
|
||||||
|
IERC20[] memory tokenAddresses = pool2.allTokens();
|
||||||
|
|
||||||
// Deploy the borrower contract
|
// Deploy the borrower contract
|
||||||
borrower = new FlashBorrower(address(pool2));
|
borrower = new FlashBorrower(address(pool2), tokenAddresses);
|
||||||
|
|
||||||
// Mint tokens to alice to be used for repayments and approve borrower
|
// Mint tokens to alice to be used for repayments and approve borrower
|
||||||
IERC20[] memory tokenAddresses = pool2.allTokens();
|
|
||||||
vm.startPrank(alice);
|
vm.startPrank(alice);
|
||||||
for (uint256 i = 0; i < tokenAddresses.length; i++) {
|
for (uint256 i = 0; i < tokenAddresses.length; i++) {
|
||||||
TestERC20(address(tokenAddresses[i])).mint(alice, INIT_BAL * 2);
|
TestERC20(address(tokenAddresses[i])).mint(alice, INIT_BAL * 2);
|
||||||
@@ -405,14 +437,33 @@ contract GasTest is Test {
|
|||||||
// Configure borrower
|
// Configure borrower
|
||||||
borrower.setAction(FlashBorrower.Action.NORMAL, alice);
|
borrower.setAction(FlashBorrower.Action.NORMAL, alice);
|
||||||
|
|
||||||
// Get first token from pool
|
// Create loan request for single token (get array size from pool)
|
||||||
IERC20[] memory poolTokens = pool2.allTokens();
|
IERC20[] memory poolTokens = pool2.allTokens();
|
||||||
address token = address(poolTokens[0]);
|
uint256[] memory amounts = new uint256[](poolTokens.length);
|
||||||
uint256 amount = 1000;
|
amounts[0] = 1000;
|
||||||
|
|
||||||
// Execute flash loan 10 times to measure gas
|
// Execute flash loan 10 times to measure gas
|
||||||
for (uint256 i = 0; i < 10; i++) {
|
for (uint256 i = 0; i < 10; i++) {
|
||||||
pool2.flashLoan(borrower, token, amount, "");
|
borrower.flash(amounts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Gas measurement: flash with multiple tokens
|
||||||
|
function testFlashGasMultipleTokens() public {
|
||||||
|
FlashBorrower borrower = setupFlashBorrower();
|
||||||
|
|
||||||
|
// Configure borrower
|
||||||
|
borrower.setAction(FlashBorrower.Action.NORMAL, alice);
|
||||||
|
|
||||||
|
// Create loan request for multiple tokens (get array size from pool)
|
||||||
|
IERC20[] memory poolTokens = pool2.allTokens();
|
||||||
|
uint256[] memory amounts = new uint256[](poolTokens.length);
|
||||||
|
amounts[0] = 1000;
|
||||||
|
amounts[1] = 2000;
|
||||||
|
|
||||||
|
// Execute flash loan 10 times to measure gas
|
||||||
|
for (uint256 i = 0; i < 10; i++) {
|
||||||
|
borrower.flash(amounts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
pragma solidity ^0.8.20;
|
pragma solidity ^0.8.20;
|
||||||
|
|
||||||
import "forge-std/Test.sol";
|
import "forge-std/Test.sol";
|
||||||
import "forge-std/console2.sol";
|
import "forge-std/console.sol";
|
||||||
import "@openzeppelin/contracts/interfaces/IERC20Metadata.sol";
|
import "@openzeppelin/contracts/interfaces/IERC20Metadata.sol";
|
||||||
import "../src/LMSRStabilized.sol";
|
import "../src/LMSRStabilized.sol";
|
||||||
import "../src/LMSRStabilizedBalancedPair.sol";
|
import "../src/LMSRStabilizedBalancedPair.sol";
|
||||||
@@ -711,7 +711,7 @@ contract LMSRStabilizedTest is Test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Path 1: Direct swap from asset 0 to asset 2
|
// Path 1: Direct swap from asset 0 to asset 2
|
||||||
(/*int128 directAmountIn*/, int128 directAmountOut) = s.swapAmountsForExactInput(0, 2, directSwapAmount, 0);
|
(/* int128 directAmountIn */, int128 directAmountOut) = s.swapAmountsForExactInput(0, 2, directSwapAmount, 0);
|
||||||
|
|
||||||
// Restore original state for second path
|
// Restore original state for second path
|
||||||
_updateCachedQInternal(backupQ);
|
_updateCachedQInternal(backupQ);
|
||||||
@@ -724,7 +724,7 @@ contract LMSRStabilizedTest is Test {
|
|||||||
s.qInternal[1] = s.qInternal[1].add(indirectAmountOut1);
|
s.qInternal[1] = s.qInternal[1].add(indirectAmountOut1);
|
||||||
|
|
||||||
// Second swap: asset 1 -> asset 2
|
// Second swap: asset 1 -> asset 2
|
||||||
(/*int128 indirectAmountIn2*/, int128 indirectAmountOut2) = s.swapAmountsForExactInput(1, 2, indirectAmountOut1, 0);
|
(/* int128 indirectAmountIn2 */, int128 indirectAmountOut2) = s.swapAmountsForExactInput(1, 2, indirectAmountOut1, 0);
|
||||||
|
|
||||||
// The path independence property isn't perfect due to discrete swap mechanics,
|
// The path independence property isn't perfect due to discrete swap mechanics,
|
||||||
// but the difference should be within reasonable bounds
|
// but the difference should be within reasonable bounds
|
||||||
@@ -765,7 +765,7 @@ contract LMSRStabilizedTest is Test {
|
|||||||
s.qInternal[1] = s.qInternal[1].add(amountOut1);
|
s.qInternal[1] = s.qInternal[1].add(amountOut1);
|
||||||
|
|
||||||
// Step 2: Swap back asset 1 -> asset 0
|
// Step 2: Swap back asset 1 -> asset 0
|
||||||
(/*int128 amountIn2*/, int128 amountOut2) = s.swapAmountsForExactInput(1, 0, amountOut1, 0);
|
(/* int128 amountIn2 */, int128 amountOut2) = s.swapAmountsForExactInput(1, 0, amountOut1, 0);
|
||||||
|
|
||||||
// Calculate round-trip slippage: (initial amount - final amount) / initial amount
|
// Calculate round-trip slippage: (initial amount - final amount) / initial amount
|
||||||
int128 roundTripSlippage = (amountIn1.sub(amountOut2)).div(amountIn1);
|
int128 roundTripSlippage = (amountIn1.sub(amountOut2)).div(amountIn1);
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ pragma solidity ^0.8.30;
|
|||||||
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||||
|
|
||||||
contract MockERC20 is ERC20 {
|
contract MockERC20 is ERC20 {
|
||||||
uint8 private immutable DECIMALS;
|
uint8 private immutable _decimals;
|
||||||
|
|
||||||
constructor(string memory name, string memory symbol, uint8 decimals_) ERC20(name, symbol) {DECIMALS = decimals_;}
|
constructor(string memory name, string memory symbol, uint8 decimals_) ERC20(name, symbol) {_decimals = decimals_;}
|
||||||
|
|
||||||
function decimals() public view virtual override returns (uint8) {return DECIMALS;}
|
function decimals() public view virtual override returns (uint8) {return _decimals;}
|
||||||
function mint(address account, uint256 amount) external {_mint(account, amount);}
|
function mint(address account, uint256 amount) external {_mint(account, amount);}
|
||||||
function burn(address account, uint256 amount) external {_burn(account, amount);}
|
function burn(address account, uint256 amount) external {_burn(account, amount);}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,11 @@
|
|||||||
// SPDX-License-Identifier: UNLICENSED
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
pragma solidity ^0.8.30;
|
pragma solidity ^0.8.30;
|
||||||
|
|
||||||
import {CommonBase} from "../lib/forge-std/src/Base.sol";
|
import "forge-std/Test.sol";
|
||||||
import {StdAssertions} from "../lib/forge-std/src/StdAssertions.sol";
|
import "../src/LMSRStabilized.sol";
|
||||||
import {StdChains} from "../lib/forge-std/src/StdChains.sol";
|
import "../src/PartyPlanner.sol";
|
||||||
import {StdCheats, StdCheatsSafe} from "../lib/forge-std/src/StdCheats.sol";
|
import "../src/PartyPool.sol";
|
||||||
import {StdUtils} from "../lib/forge-std/src/StdUtils.sol";
|
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||||
import {Test} from "../lib/forge-std/src/Test.sol";
|
|
||||||
import {ERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
|
|
||||||
import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
|
|
||||||
import {Deploy} from "../src/Deploy.sol";
|
|
||||||
import {IPartyPool} from "../src/IPartyPool.sol";
|
|
||||||
import {LMSRStabilized} from "../src/LMSRStabilized.sol";
|
|
||||||
import {PartyPlanner} from "../src/PartyPlanner.sol";
|
|
||||||
import {PartyPool} from "../src/PartyPool.sol";
|
|
||||||
import {MockERC20} from "./PartyPlanner.t.sol";
|
|
||||||
|
|
||||||
// Mock ERC20 token for testing
|
// Mock ERC20 token for testing
|
||||||
contract MockERC20 is ERC20 {
|
contract MockERC20 is ERC20 {
|
||||||
@@ -47,7 +38,7 @@ contract PartyPlannerTest is Test {
|
|||||||
|
|
||||||
function setUp() public {
|
function setUp() public {
|
||||||
// Deploy PartyPlanner
|
// Deploy PartyPlanner
|
||||||
planner = Deploy.newPartyPlanner();
|
planner = new PartyPlanner();
|
||||||
|
|
||||||
// Deploy mock tokens
|
// Deploy mock tokens
|
||||||
tokenA = new MockERC20("Token A", "TKNA", 18);
|
tokenA = new MockERC20("Token A", "TKNA", 18);
|
||||||
@@ -96,7 +87,7 @@ contract PartyPlannerTest is Test {
|
|||||||
// Compute kappa then create pool via kappa overload
|
// Compute kappa then create pool via kappa overload
|
||||||
int128 computedKappa = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage);
|
int128 computedKappa = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage);
|
||||||
|
|
||||||
(IPartyPool pool, uint256 lpAmount) = planner.newPool(
|
(PartyPool pool, uint256 lpAmount) = planner.createPool(
|
||||||
name,
|
name,
|
||||||
symbol,
|
symbol,
|
||||||
tokens,
|
tokens,
|
||||||
@@ -125,7 +116,7 @@ contract PartyPlannerTest is Test {
|
|||||||
assertEq(planner.poolsByTokenCount(IERC20(address(tokenB))), initialTokenBCount + 1, "TokenB pool count should increase");
|
assertEq(planner.poolsByTokenCount(IERC20(address(tokenB))), initialTokenBCount + 1, "TokenB pool count should increase");
|
||||||
|
|
||||||
// Verify pools can be retrieved
|
// Verify pools can be retrieved
|
||||||
IPartyPool[] memory allPools = planner.getAllPools(0, 10);
|
PartyPool[] memory allPools = planner.getAllPools(0, 10);
|
||||||
bool poolFound = false;
|
bool poolFound = false;
|
||||||
for (uint256 i = 0; i < allPools.length; i++) {
|
for (uint256 i = 0; i < allPools.length; i++) {
|
||||||
if (allPools[i] == pool) {
|
if (allPools[i] == pool) {
|
||||||
@@ -136,7 +127,7 @@ contract PartyPlannerTest is Test {
|
|||||||
assertTrue(poolFound, "Created pool should be in getAllPools result");
|
assertTrue(poolFound, "Created pool should be in getAllPools result");
|
||||||
|
|
||||||
// Verify pool appears in token-specific queries
|
// Verify pool appears in token-specific queries
|
||||||
IPartyPool[] memory tokenAPools = planner.getPoolsByToken(IERC20(address(tokenA)), 0, 10);
|
PartyPool[] memory tokenAPools = planner.getPoolsByToken(IERC20(address(tokenA)), 0, 10);
|
||||||
bool poolInTokenA = false;
|
bool poolInTokenA = false;
|
||||||
for (uint256 i = 0; i < tokenAPools.length; i++) {
|
for (uint256 i = 0; i < tokenAPools.length; i++) {
|
||||||
if (tokenAPools[i] == pool) {
|
if (tokenAPools[i] == pool) {
|
||||||
@@ -146,7 +137,7 @@ contract PartyPlannerTest is Test {
|
|||||||
}
|
}
|
||||||
assertTrue(poolInTokenA, "Pool should be indexed under tokenA");
|
assertTrue(poolInTokenA, "Pool should be indexed under tokenA");
|
||||||
|
|
||||||
IPartyPool[] memory tokenBPools = planner.getPoolsByToken(IERC20(address(tokenB)), 0, 10);
|
PartyPool[] memory tokenBPools = planner.getPoolsByToken(IERC20(address(tokenB)), 0, 10);
|
||||||
bool poolInTokenB = false;
|
bool poolInTokenB = false;
|
||||||
for (uint256 i = 0; i < tokenBPools.length; i++) {
|
for (uint256 i = 0; i < tokenBPools.length; i++) {
|
||||||
if (tokenBPools[i] == pool) {
|
if (tokenBPools[i] == pool) {
|
||||||
@@ -175,7 +166,7 @@ contract PartyPlannerTest is Test {
|
|||||||
deposits1[1] = INITIAL_DEPOSIT_AMOUNT;
|
deposits1[1] = INITIAL_DEPOSIT_AMOUNT;
|
||||||
|
|
||||||
int128 kappa1 = LMSRStabilized.computeKappaFromSlippage(tokens1.length, int128((1 << 64) - 1), int128(1 << 62));
|
int128 kappa1 = LMSRStabilized.computeKappaFromSlippage(tokens1.length, int128((1 << 64) - 1), int128(1 << 62));
|
||||||
(IPartyPool pool1,) = planner.newPool(
|
(PartyPool pool1,) = planner.createPool(
|
||||||
"Pool 1", "LP1", tokens1, bases1,
|
"Pool 1", "LP1", tokens1, bases1,
|
||||||
kappa1, 3000, 5000, false,
|
kappa1, 3000, 5000, false,
|
||||||
payer, receiver, deposits1, 1000e18, 0
|
payer, receiver, deposits1, 1000e18, 0
|
||||||
@@ -195,7 +186,7 @@ contract PartyPlannerTest is Test {
|
|||||||
deposits2[1] = INITIAL_DEPOSIT_AMOUNT / 1e12; // Adjust for 6 decimals
|
deposits2[1] = INITIAL_DEPOSIT_AMOUNT / 1e12; // Adjust for 6 decimals
|
||||||
|
|
||||||
int128 kappa2 = LMSRStabilized.computeKappaFromSlippage(tokens2.length, int128((1 << 64) - 1), int128(1 << 62));
|
int128 kappa2 = LMSRStabilized.computeKappaFromSlippage(tokens2.length, int128((1 << 64) - 1), int128(1 << 62));
|
||||||
(IPartyPool pool2,) = planner.newPool(
|
(PartyPool pool2,) = planner.createPool(
|
||||||
"Pool 2", "LP2", tokens2, bases2,
|
"Pool 2", "LP2", tokens2, bases2,
|
||||||
kappa2, 3000, 5000, false,
|
kappa2, 3000, 5000, false,
|
||||||
payer, receiver, deposits2, 1000e18, 0
|
payer, receiver, deposits2, 1000e18, 0
|
||||||
@@ -211,7 +202,7 @@ contract PartyPlannerTest is Test {
|
|||||||
assertEq(planner.poolsByTokenCount(IERC20(address(tokenC))), 1, "TokenC should be in 1 pool");
|
assertEq(planner.poolsByTokenCount(IERC20(address(tokenC))), 1, "TokenC should be in 1 pool");
|
||||||
|
|
||||||
// Verify tokenB appears in both pools
|
// Verify tokenB appears in both pools
|
||||||
IPartyPool[] memory tokenBPools = planner.getPoolsByToken(IERC20(address(tokenB)), 0, 10);
|
PartyPool[] memory tokenBPools = planner.getPoolsByToken(IERC20(address(tokenB)), 0, 10);
|
||||||
assertEq(tokenBPools.length, 2, "TokenB should have 2 pools");
|
assertEq(tokenBPools.length, 2, "TokenB should have 2 pools");
|
||||||
|
|
||||||
bool pool1Found = false;
|
bool pool1Found = false;
|
||||||
@@ -239,7 +230,7 @@ contract PartyPlannerTest is Test {
|
|||||||
// Test token/deposit length mismatch
|
// Test token/deposit length mismatch
|
||||||
vm.expectRevert("Planner: tokens and deposits length mismatch");
|
vm.expectRevert("Planner: tokens and deposits length mismatch");
|
||||||
// call old-signature convenience (it will still exist) for the mismatched-length revert check
|
// call old-signature convenience (it will still exist) for the mismatched-length revert check
|
||||||
planner.newPool(
|
planner.createPool(
|
||||||
"Test Pool", "TESTLP", tokens, bases,
|
"Test Pool", "TESTLP", tokens, bases,
|
||||||
int128((1 << 64) - 1), int128(1 << 62), 3000, 5000, false,
|
int128((1 << 64) - 1), int128(1 << 62), 3000, 5000, false,
|
||||||
payer, receiver, deposits, 1000e18, 0
|
payer, receiver, deposits, 1000e18, 0
|
||||||
@@ -253,7 +244,7 @@ contract PartyPlannerTest is Test {
|
|||||||
int128 kappaErr = LMSRStabilized.computeKappaFromSlippage(tokens.length, int128((1 << 64) - 1), int128(1 << 62));
|
int128 kappaErr = LMSRStabilized.computeKappaFromSlippage(tokens.length, int128((1 << 64) - 1), int128(1 << 62));
|
||||||
|
|
||||||
vm.expectRevert("Planner: payer cannot be zero address");
|
vm.expectRevert("Planner: payer cannot be zero address");
|
||||||
planner.newPool(
|
planner.createPool(
|
||||||
"Test Pool", "TESTLP", tokens, bases,
|
"Test Pool", "TESTLP", tokens, bases,
|
||||||
kappaErr, 3000, 5000, false,
|
kappaErr, 3000, 5000, false,
|
||||||
address(0), receiver, validDeposits, 1000e18, 0
|
address(0), receiver, validDeposits, 1000e18, 0
|
||||||
@@ -261,7 +252,7 @@ contract PartyPlannerTest is Test {
|
|||||||
|
|
||||||
// Test zero receiver address
|
// Test zero receiver address
|
||||||
vm.expectRevert("Planner: receiver cannot be zero address");
|
vm.expectRevert("Planner: receiver cannot be zero address");
|
||||||
planner.newPool(
|
planner.createPool(
|
||||||
"Test Pool", "TESTLP", tokens, bases,
|
"Test Pool", "TESTLP", tokens, bases,
|
||||||
kappaErr, 3000, 5000, false,
|
kappaErr, 3000, 5000, false,
|
||||||
payer, address(0), validDeposits, 1000e18, 0
|
payer, address(0), validDeposits, 1000e18, 0
|
||||||
@@ -272,7 +263,7 @@ contract PartyPlannerTest is Test {
|
|||||||
int128 kappaDeadline = LMSRStabilized.computeKappaFromSlippage(tokens.length, int128((1 << 64) - 1), int128(1 << 62));
|
int128 kappaDeadline = LMSRStabilized.computeKappaFromSlippage(tokens.length, int128((1 << 64) - 1), int128(1 << 62));
|
||||||
vm.warp(1000);
|
vm.warp(1000);
|
||||||
vm.expectRevert("Planner: deadline exceeded");
|
vm.expectRevert("Planner: deadline exceeded");
|
||||||
planner.newPool(
|
planner.createPool(
|
||||||
"Test Pool", "TESTLP", tokens, bases,
|
"Test Pool", "TESTLP", tokens, bases,
|
||||||
kappaDeadline, 3000, 5000, false,
|
kappaDeadline, 3000, 5000, false,
|
||||||
payer, receiver, validDeposits, 1000e18, block.timestamp - 1
|
payer, receiver, validDeposits, 1000e18, block.timestamp - 1
|
||||||
@@ -282,7 +273,7 @@ contract PartyPlannerTest is Test {
|
|||||||
function test_poolIndexing_Pagination() public {
|
function test_poolIndexing_Pagination() public {
|
||||||
// Create multiple pools for pagination testing
|
// Create multiple pools for pagination testing
|
||||||
uint256 numPools = 5;
|
uint256 numPools = 5;
|
||||||
IPartyPool[] memory createdPools = new IPartyPool[](numPools);
|
PartyPool[] memory createdPools = new PartyPool[](numPools);
|
||||||
|
|
||||||
for (uint256 i = 0; i < numPools; i++) {
|
for (uint256 i = 0; i < numPools; i++) {
|
||||||
IERC20[] memory tokens = new IERC20[](2);
|
IERC20[] memory tokens = new IERC20[](2);
|
||||||
@@ -298,7 +289,7 @@ contract PartyPlannerTest is Test {
|
|||||||
deposits[1] = INITIAL_DEPOSIT_AMOUNT;
|
deposits[1] = INITIAL_DEPOSIT_AMOUNT;
|
||||||
|
|
||||||
int128 kappaLoop = LMSRStabilized.computeKappaFromSlippage(tokens.length, int128((1 << 64) - 1), int128(1 << 62));
|
int128 kappaLoop = LMSRStabilized.computeKappaFromSlippage(tokens.length, int128((1 << 64) - 1), int128(1 << 62));
|
||||||
(IPartyPool pool,) = planner.newPool(
|
(PartyPool pool,) = planner.createPool(
|
||||||
string(abi.encodePacked("Pool ", vm.toString(i))),
|
string(abi.encodePacked("Pool ", vm.toString(i))),
|
||||||
string(abi.encodePacked("LP", vm.toString(i))),
|
string(abi.encodePacked("LP", vm.toString(i))),
|
||||||
tokens, bases,
|
tokens, bases,
|
||||||
@@ -312,19 +303,19 @@ contract PartyPlannerTest is Test {
|
|||||||
assertEq(planner.poolCount(), numPools, "Should have created all pools");
|
assertEq(planner.poolCount(), numPools, "Should have created all pools");
|
||||||
|
|
||||||
// Test pagination - get first 3 pools
|
// Test pagination - get first 3 pools
|
||||||
IPartyPool[] memory page1 = planner.getAllPools(0, 3);
|
PartyPool[] memory page1 = planner.getAllPools(0, 3);
|
||||||
assertEq(page1.length, 3, "First page should have 3 pools");
|
assertEq(page1.length, 3, "First page should have 3 pools");
|
||||||
|
|
||||||
// Test pagination - get next 2 pools
|
// Test pagination - get next 2 pools
|
||||||
IPartyPool[] memory page2 = planner.getAllPools(3, 3);
|
PartyPool[] memory page2 = planner.getAllPools(3, 3);
|
||||||
assertEq(page2.length, 2, "Second page should have 2 pools");
|
assertEq(page2.length, 2, "Second page should have 2 pools");
|
||||||
|
|
||||||
// Test pagination - offset beyond bounds
|
// Test pagination - offset beyond bounds
|
||||||
IPartyPool[] memory emptyPage = planner.getAllPools(10, 3);
|
PartyPool[] memory emptyPage = planner.getAllPools(10, 3);
|
||||||
assertEq(emptyPage.length, 0, "Should return empty array for out of bounds offset");
|
assertEq(emptyPage.length, 0, "Should return empty array for out of bounds offset");
|
||||||
|
|
||||||
// Verify all pools are accessible through pagination
|
// Verify all pools are accessible through pagination
|
||||||
IPartyPool[] memory allPools = planner.getAllPools(0, 10);
|
PartyPool[] memory allPools = planner.getAllPools(0, 10);
|
||||||
assertEq(allPools.length, numPools, "Should return all pools");
|
assertEq(allPools.length, numPools, "Should return all pools");
|
||||||
|
|
||||||
for (uint256 i = 0; i < numPools; i++) {
|
for (uint256 i = 0; i < numPools; i++) {
|
||||||
|
|||||||
@@ -1,80 +1,108 @@
|
|||||||
// SPDX-License-Identifier: UNLICENSED
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
/* solhint-disable */
|
|
||||||
pragma solidity ^0.8.30;
|
pragma solidity ^0.8.30;
|
||||||
|
|
||||||
import "forge-std/Test.sol";
|
import "../src/Deploy.sol";
|
||||||
import "@abdk/ABDKMath64x64.sol";
|
import "../src/IPartyFlashCallback.sol";
|
||||||
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
|
||||||
import "../src/LMSRStabilized.sol";
|
import "../src/LMSRStabilized.sol";
|
||||||
import "../src/PartyPool.sol";
|
import "../src/PartyPool.sol";
|
||||||
|
import "@abdk/ABDKMath64x64.sol";
|
||||||
|
|
||||||
// Import the flash callback interface
|
// Import the flash callback interface
|
||||||
import "../lib/openzeppelin-contracts/contracts/interfaces/IERC3156FlashBorrower.sol";
|
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||||
import {PartyPlanner} from "../src/PartyPlanner.sol";
|
import "forge-std/Test.sol";
|
||||||
import {Deploy} from "../src/Deploy.sol";
|
|
||||||
import {PartyPoolViewer} from "../src/PartyPoolViewer.sol";
|
|
||||||
|
|
||||||
/// @notice Test contract that implements the flash callback for testing flash loans
|
/// @notice Test contract that implements the flash callback for testing flash loans
|
||||||
contract FlashBorrower is IERC3156FlashBorrower {
|
contract FlashBorrower is IPartyFlashCallback {
|
||||||
enum Action {
|
enum Action {
|
||||||
NORMAL, // Normal repayment
|
NORMAL, // Normal repayment
|
||||||
REPAY_NONE, // Don't repay anything
|
REPAY_NONE, // Don't repay anything
|
||||||
REPAY_PARTIAL, // Repay less than required
|
REPAY_PARTIAL, // Repay less than required
|
||||||
REPAY_NO_FEE, // Repay only the principal without fee
|
REPAY_NO_FEE, // Repay only the principal without fee
|
||||||
REPAY_EXACT // Repay exactly the required amount
|
REPAY_EXACT, // Repay exactly the required amount
|
||||||
|
REPAY_EXTRA // Repay more than required (donation)
|
||||||
}
|
}
|
||||||
|
|
||||||
Action public action;
|
Action public action;
|
||||||
address public pool;
|
address public pool;
|
||||||
address public payer;
|
address public recipient;
|
||||||
|
address[] public tokens;
|
||||||
|
|
||||||
constructor(address _pool) {
|
constructor(address _pool, address[] memory _tokens) {
|
||||||
pool = _pool;
|
pool = _pool;
|
||||||
|
tokens = _tokens;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setAction(Action _action, address _payer) external {
|
function setAction(Action _action, address _recipient) external {
|
||||||
action = _action;
|
action = _action;
|
||||||
payer = _payer;
|
recipient = _recipient;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onFlashLoan(
|
function flash(uint256[] memory amounts) external {
|
||||||
address /*initiator*/,
|
PartyPool(pool).flash(recipient, amounts, "");
|
||||||
address token,
|
}
|
||||||
uint256 amount,
|
|
||||||
uint256 fee,
|
function partyFlashCallback(
|
||||||
|
uint256[] memory loanAmounts,
|
||||||
|
uint256[] memory repaymentAmounts,
|
||||||
bytes calldata /* data */
|
bytes calldata /* data */
|
||||||
) external override returns (bytes32) {
|
) external override {
|
||||||
require(msg.sender == pool, "Callback not called by pool");
|
require(msg.sender == pool, "Callback not called by pool");
|
||||||
|
|
||||||
if (action == Action.NORMAL) {
|
if (action == Action.NORMAL || action == Action.REPAY_EXTRA) {
|
||||||
// Normal repayment
|
// Normal or extra repayment - transfer required amounts back to pool
|
||||||
// We received 'amount' from the pool, need to pay back amount + fee
|
for (uint256 i = 0; i < loanAmounts.length; i++) {
|
||||||
uint256 repaymentAmount = amount + fee;
|
if (loanAmounts[i] > 0) {
|
||||||
|
uint256 repaymentAmount = repaymentAmounts[i];
|
||||||
|
|
||||||
// Transfer the fee from payer to this contract
|
// For REPAY_EXTRA, add 1 to each repayment
|
||||||
// (we already have the principal 'amount' from the flash loan)
|
if (action == Action.REPAY_EXTRA) {
|
||||||
TestERC20(token).transferFrom(payer, address(this), fee);
|
repaymentAmount += 1;
|
||||||
|
}
|
||||||
|
|
||||||
// Approve pool to pull back the full repayment
|
// Transfer from recipient back to pool
|
||||||
TestERC20(token).approve(pool, repaymentAmount);
|
TestERC20(tokens[i]).transferFrom(
|
||||||
|
recipient,
|
||||||
|
pool,
|
||||||
|
repaymentAmount
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (action == Action.REPAY_PARTIAL) {
|
} else if (action == Action.REPAY_PARTIAL) {
|
||||||
// Repay half of the required amount
|
// Repay half of the required amounts
|
||||||
uint256 partialRepayment = (amount + fee) / 2;
|
for (uint256 i = 0; i < loanAmounts.length; i++) {
|
||||||
TestERC20(token).approve(pool, partialRepayment);
|
if (loanAmounts[i] > 0) {
|
||||||
|
uint256 partialRepayment = repaymentAmounts[i] / 2;
|
||||||
|
TestERC20(tokens[i]).transferFrom(
|
||||||
|
recipient,
|
||||||
|
pool,
|
||||||
|
partialRepayment
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (action == Action.REPAY_NO_FEE) {
|
} else if (action == Action.REPAY_NO_FEE) {
|
||||||
// Repay only the principal without fee (we already have it from the loan)
|
// Repay only the principal without fee
|
||||||
TestERC20(token).approve(pool, amount);
|
for (uint256 i = 0; i < loanAmounts.length; i++) {
|
||||||
|
if (loanAmounts[i] > 0) {
|
||||||
|
TestERC20(tokens[i]).transferFrom(
|
||||||
|
recipient,
|
||||||
|
pool,
|
||||||
|
loanAmounts[i]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (action == Action.REPAY_EXACT) {
|
} else if (action == Action.REPAY_EXACT) {
|
||||||
// Repay exactly what was required
|
// Repay exactly what was required
|
||||||
uint256 repaymentAmount = amount + fee;
|
for (uint256 i = 0; i < loanAmounts.length; i++) {
|
||||||
// Transfer the fee from payer (we have the principal from the loan)
|
if (loanAmounts[i] > 0) {
|
||||||
TestERC20(token).transferFrom(payer, address(this), fee);
|
TestERC20(tokens[i]).transferFrom(
|
||||||
// Approve pool to pull back the full repayment
|
recipient,
|
||||||
TestERC20(token).approve(pool, repaymentAmount);
|
pool,
|
||||||
|
repaymentAmounts[i]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// For REPAY_NONE, do nothing (don't approve repayment)
|
// For REPAY_NONE, do nothing (don't repay)
|
||||||
|
|
||||||
return keccak256("ERC3156FlashBorrower.onFlashLoan");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,10 +138,8 @@ contract PartyPoolTest is Test {
|
|||||||
TestERC20 token7;
|
TestERC20 token7;
|
||||||
TestERC20 token8;
|
TestERC20 token8;
|
||||||
TestERC20 token9;
|
TestERC20 token9;
|
||||||
PartyPlanner planner;
|
|
||||||
PartyPool pool;
|
PartyPool pool;
|
||||||
PartyPool pool10;
|
PartyPool pool10;
|
||||||
PartyPoolViewer viewer;
|
|
||||||
|
|
||||||
address alice;
|
address alice;
|
||||||
address bob;
|
address bob;
|
||||||
@@ -126,7 +152,6 @@ contract PartyPoolTest is Test {
|
|||||||
uint256 constant BASE = 1; // use base=1 so internal amounts correspond to raw integers (Q64.64 units)
|
uint256 constant BASE = 1; // use base=1 so internal amounts correspond to raw integers (Q64.64 units)
|
||||||
|
|
||||||
function setUp() public {
|
function setUp() public {
|
||||||
planner = Deploy.newPartyPlanner();
|
|
||||||
alice = address(0xA11ce);
|
alice = address(0xA11ce);
|
||||||
bob = address(0xB0b);
|
bob = address(0xB0b);
|
||||||
|
|
||||||
@@ -254,8 +279,6 @@ contract PartyPoolTest is Test {
|
|||||||
token7.mint(bob, INIT_BAL);
|
token7.mint(bob, INIT_BAL);
|
||||||
token8.mint(bob, INIT_BAL);
|
token8.mint(bob, INIT_BAL);
|
||||||
token9.mint(bob, INIT_BAL);
|
token9.mint(bob, INIT_BAL);
|
||||||
|
|
||||||
viewer = Deploy.newViewer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Basic sanity: initial mint should have produced LP tokens for this contract and the pool holds tokens.
|
/// @notice Basic sanity: initial mint should have produced LP tokens for this contract and the pool holds tokens.
|
||||||
@@ -297,7 +320,7 @@ contract PartyPoolTest is Test {
|
|||||||
token2.approve(address(pool), type(uint256).max);
|
token2.approve(address(pool), type(uint256).max);
|
||||||
|
|
||||||
// Inspect the deposit amounts that the pool will require (these are rounded up)
|
// Inspect the deposit amounts that the pool will require (these are rounded up)
|
||||||
uint256[] memory deposits = viewer.mintAmounts(pool, 1);
|
uint256[] memory deposits = pool.mintDepositAmounts(1);
|
||||||
|
|
||||||
// Basic sanity: deposits array length must match token count and not all zero necessarily
|
// Basic sanity: deposits array length must match token count and not all zero necessarily
|
||||||
assertEq(deposits.length, 3);
|
assertEq(deposits.length, 3);
|
||||||
@@ -339,7 +362,7 @@ contract PartyPoolTest is Test {
|
|||||||
uint256 totalLpBefore = pool.totalSupply();
|
uint256 totalLpBefore = pool.totalSupply();
|
||||||
|
|
||||||
// Compute required deposits and perform mint for 1 wei
|
// Compute required deposits and perform mint for 1 wei
|
||||||
uint256[] memory deposits = viewer.mintAmounts(pool, 1);
|
uint256[] memory deposits = pool.mintDepositAmounts(1);
|
||||||
|
|
||||||
// Sum deposits as deposited_value
|
// Sum deposits as deposited_value
|
||||||
uint256 depositedValue = 0;
|
uint256 depositedValue = 0;
|
||||||
@@ -373,14 +396,14 @@ contract PartyPoolTest is Test {
|
|||||||
vm.stopPrank();
|
vm.stopPrank();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice mintAmounts should round up deposit amounts to protect the pool.
|
/// @notice mintDepositAmounts should round up deposit amounts to protect the pool.
|
||||||
function testMintDepositAmountsRoundingUp() public view {
|
function testMintDepositAmountsRoundingUp() public view {
|
||||||
uint256 totalLp = pool.totalSupply();
|
uint256 totalLp = pool.totalSupply();
|
||||||
assertTrue(totalLp > 0, "precondition: total supply > 0");
|
assertTrue(totalLp > 0, "precondition: total supply > 0");
|
||||||
|
|
||||||
// Request half of LP supply
|
// Request half of LP supply
|
||||||
uint256 want = totalLp / 2;
|
uint256 want = totalLp / 2;
|
||||||
uint256[] memory deposits = viewer.mintAmounts(pool, want);
|
uint256[] memory deposits = pool.mintDepositAmounts(want);
|
||||||
|
|
||||||
// We expect each deposit to be roughly half the pool balance, but due to rounding up it should satisfy:
|
// We expect each deposit to be roughly half the pool balance, but due to rounding up it should satisfy:
|
||||||
// deposits[i] * 2 >= cached balance (i.e., rounding up)
|
// deposits[i] * 2 >= cached balance (i.e., rounding up)
|
||||||
@@ -397,7 +420,7 @@ contract PartyPoolTest is Test {
|
|||||||
assertTrue(totalLp > 0, "precondition: LP > 0");
|
assertTrue(totalLp > 0, "precondition: LP > 0");
|
||||||
|
|
||||||
// Compute amounts required to redeem entire supply (should be current balances)
|
// Compute amounts required to redeem entire supply (should be current balances)
|
||||||
uint256[] memory withdrawAmounts = viewer.burnAmounts(pool, totalLp);
|
uint256[] memory withdrawAmounts = pool.burnReceiveAmounts(totalLp);
|
||||||
|
|
||||||
// Sanity: withdrawAmounts should equal pool balances (or very close due to rounding)
|
// Sanity: withdrawAmounts should equal pool balances (or very close due to rounding)
|
||||||
for (uint i = 0; i < withdrawAmounts.length; i++) {
|
for (uint i = 0; i < withdrawAmounts.length; i++) {
|
||||||
@@ -487,7 +510,7 @@ contract PartyPoolTest is Test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// @notice Verify mintAmounts matches the actual token transfers performed by mint()
|
/// @notice Verify mintDepositAmounts matches the actual token transfers performed by mint()
|
||||||
function testMintDepositAmountsMatchesMint_3TokenPool() public {
|
function testMintDepositAmountsMatchesMint_3TokenPool() public {
|
||||||
// Use a range of LP requests (tiny to large fraction)
|
// Use a range of LP requests (tiny to large fraction)
|
||||||
uint256 totalLp = pool.totalSupply();
|
uint256 totalLp = pool.totalSupply();
|
||||||
@@ -501,7 +524,7 @@ contract PartyPoolTest is Test {
|
|||||||
if (req == 0) req = 1;
|
if (req == 0) req = 1;
|
||||||
|
|
||||||
// Compute expected deposit amounts via view
|
// Compute expected deposit amounts via view
|
||||||
uint256[] memory expected = viewer.mintAmounts(pool, req);
|
uint256[] memory expected = pool.mintDepositAmounts(req);
|
||||||
|
|
||||||
// Ensure alice has tokens and approve pool
|
// Ensure alice has tokens and approve pool
|
||||||
vm.startPrank(alice);
|
vm.startPrank(alice);
|
||||||
@@ -515,7 +538,7 @@ contract PartyPoolTest is Test {
|
|||||||
uint256 a2Before = token2.balanceOf(alice);
|
uint256 a2Before = token2.balanceOf(alice);
|
||||||
|
|
||||||
// Perform mint (may revert for zero-request; ensure req>0 above)
|
// Perform mint (may revert for zero-request; ensure req>0 above)
|
||||||
// Guard: if mintAmounts returned all zeros, skip (nothing to transfer)
|
// Guard: if mintDepositAmounts returned all zeros, skip (nothing to transfer)
|
||||||
bool allZero = (expected[0] == 0 && expected[1] == 0 && expected[2] == 0);
|
bool allZero = (expected[0] == 0 && expected[1] == 0 && expected[2] == 0);
|
||||||
if (!allZero) {
|
if (!allZero) {
|
||||||
uint256 lpBefore = pool.balanceOf(alice);
|
uint256 lpBefore = pool.balanceOf(alice);
|
||||||
@@ -534,7 +557,7 @@ contract PartyPoolTest is Test {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Verify mintAmounts matches the actual token transfers performed by mint() for 10-token pool
|
/// @notice Verify mintDepositAmounts matches the actual token transfers performed by mint() for 10-token pool
|
||||||
function testMintDepositAmountsMatchesMint_10TokenPool() public {
|
function testMintDepositAmountsMatchesMint_10TokenPool() public {
|
||||||
uint256 totalLp = pool10.totalSupply();
|
uint256 totalLp = pool10.totalSupply();
|
||||||
uint256[] memory requests = new uint256[](4);
|
uint256[] memory requests = new uint256[](4);
|
||||||
@@ -546,7 +569,7 @@ contract PartyPoolTest is Test {
|
|||||||
uint256 req = requests[k];
|
uint256 req = requests[k];
|
||||||
if (req == 0) req = 1;
|
if (req == 0) req = 1;
|
||||||
|
|
||||||
uint256[] memory expected = viewer.mintAmounts(pool10, req);
|
uint256[] memory expected = pool10.mintDepositAmounts(req);
|
||||||
|
|
||||||
// Approve all tokens from alice
|
// Approve all tokens from alice
|
||||||
vm.startPrank(alice);
|
vm.startPrank(alice);
|
||||||
@@ -597,7 +620,7 @@ contract PartyPoolTest is Test {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Verify burnAmounts matches actual transfers performed by burn() for 3-token pool
|
/// @notice Verify burnReceiveAmounts matches actual transfers performed by burn() for 3-token pool
|
||||||
function testBurnReceiveAmountsMatchesBurn_3TokenPool() public {
|
function testBurnReceiveAmountsMatchesBurn_3TokenPool() public {
|
||||||
// Use address(this) as payer (holds initial LP from setUp)
|
// Use address(this) as payer (holds initial LP from setUp)
|
||||||
uint256 totalLp = pool.totalSupply();
|
uint256 totalLp = pool.totalSupply();
|
||||||
@@ -624,7 +647,7 @@ contract PartyPoolTest is Test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Recompute withdraw amounts via view after any top-up
|
// Recompute withdraw amounts via view after any top-up
|
||||||
uint256[] memory expected = viewer.burnAmounts(pool, req);
|
uint256[] memory expected = pool.burnReceiveAmounts(req);
|
||||||
|
|
||||||
// If expected withdraws are all zero (rounding edge), skip this iteration
|
// If expected withdraws are all zero (rounding edge), skip this iteration
|
||||||
if (expected[0] == 0 && expected[1] == 0 && expected[2] == 0) {
|
if (expected[0] == 0 && expected[1] == 0 && expected[2] == 0) {
|
||||||
@@ -650,7 +673,7 @@ contract PartyPoolTest is Test {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Verify burnAmounts matches actual transfers performed by burn() for 10-token pool
|
/// @notice Verify burnReceiveAmounts matches actual transfers performed by burn() for 10-token pool
|
||||||
function testBurnReceiveAmountsMatchesBurn_10TokenPool() public {
|
function testBurnReceiveAmountsMatchesBurn_10TokenPool() public {
|
||||||
uint256 totalLp = pool10.totalSupply();
|
uint256 totalLp = pool10.totalSupply();
|
||||||
uint256[] memory burns = new uint256[](4);
|
uint256[] memory burns = new uint256[](4);
|
||||||
@@ -681,7 +704,7 @@ contract PartyPoolTest is Test {
|
|||||||
vm.stopPrank();
|
vm.stopPrank();
|
||||||
}
|
}
|
||||||
|
|
||||||
uint256[] memory expected = viewer.burnAmounts(pool10, req);
|
uint256[] memory expected = pool10.burnReceiveAmounts(req);
|
||||||
|
|
||||||
// If expected withdraws are all zero (rounding edge), skip this iteration
|
// If expected withdraws are all zero (rounding edge), skip this iteration
|
||||||
bool allZero = true;
|
bool allZero = true;
|
||||||
@@ -815,8 +838,14 @@ contract PartyPoolTest is Test {
|
|||||||
|
|
||||||
/// @notice Setup a flash borrower for testing
|
/// @notice Setup a flash borrower for testing
|
||||||
function setupFlashBorrower() internal returns (FlashBorrower borrower) {
|
function setupFlashBorrower() internal returns (FlashBorrower borrower) {
|
||||||
|
// Create array of token addresses for borrower
|
||||||
|
address[] memory tokenAddresses = new address[](3);
|
||||||
|
tokenAddresses[0] = address(token0);
|
||||||
|
tokenAddresses[1] = address(token1);
|
||||||
|
tokenAddresses[2] = address(token2);
|
||||||
|
|
||||||
// Deploy the borrower contract
|
// Deploy the borrower contract
|
||||||
borrower = new FlashBorrower(address(pool));
|
borrower = new FlashBorrower(address(pool), tokenAddresses);
|
||||||
|
|
||||||
// Mint tokens to alice to be used for repayments
|
// Mint tokens to alice to be used for repayments
|
||||||
token0.mint(alice, INIT_BAL * 2);
|
token0.mint(alice, INIT_BAL * 2);
|
||||||
@@ -839,17 +868,18 @@ contract PartyPoolTest is Test {
|
|||||||
borrower.setAction(FlashBorrower.Action.NORMAL, alice);
|
borrower.setAction(FlashBorrower.Action.NORMAL, alice);
|
||||||
|
|
||||||
// Create loan request for token0 only
|
// Create loan request for token0 only
|
||||||
uint256 amount = 1000;
|
uint256[] memory amounts = new uint256[](3);
|
||||||
|
amounts[0] = 1000; // Only borrow token0
|
||||||
|
|
||||||
// Record balances before flash
|
// Record balances before flash
|
||||||
uint256 aliceToken0Before = token0.balanceOf(alice);
|
uint256 aliceToken0Before = token0.balanceOf(alice);
|
||||||
uint256 poolToken0Before = token0.balanceOf(address(pool));
|
uint256 poolToken0Before = token0.balanceOf(address(pool));
|
||||||
|
|
||||||
// Execute flash loan
|
// Execute flash loan
|
||||||
pool.flashLoan(borrower, address(token0), amount, "");
|
borrower.flash(amounts);
|
||||||
|
|
||||||
// Net change for alice should equal the flash fee (principal is returned during repayment)
|
// Net change for alice should equal the flash fee (principal is returned during repayment)
|
||||||
uint256 fee = (amount * pool.flashFeePpm() + 1_000_000 - 1) / 1_000_000; // ceil fee calculation
|
uint256 fee = (amounts[0] * pool.flashFeePpm() + 1_000_000 - 1) / 1_000_000; // ceil fee calculation
|
||||||
uint256 expectedAliceDecrease = fee;
|
uint256 expectedAliceDecrease = fee;
|
||||||
assertEq(
|
assertEq(
|
||||||
aliceToken0Before - token0.balanceOf(alice),
|
aliceToken0Before - token0.balanceOf(alice),
|
||||||
@@ -865,6 +895,126 @@ contract PartyPoolTest is Test {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// @notice Test flash loan with multiple tokens
|
||||||
|
function testFlashLoanMultipleTokens() public {
|
||||||
|
FlashBorrower borrower = setupFlashBorrower();
|
||||||
|
|
||||||
|
// Configure borrower to repay normally
|
||||||
|
borrower.setAction(FlashBorrower.Action.NORMAL, alice);
|
||||||
|
|
||||||
|
// Create loan request for all tokens
|
||||||
|
uint256[] memory amounts = new uint256[](3);
|
||||||
|
amounts[0] = 1000;
|
||||||
|
amounts[1] = 2000;
|
||||||
|
amounts[2] = 3000;
|
||||||
|
|
||||||
|
// Record balances before flash
|
||||||
|
uint256[] memory aliceBalancesBefore = new uint256[](3);
|
||||||
|
uint256[] memory poolBalancesBefore = new uint256[](3);
|
||||||
|
|
||||||
|
aliceBalancesBefore[0] = token0.balanceOf(alice);
|
||||||
|
aliceBalancesBefore[1] = token1.balanceOf(alice);
|
||||||
|
aliceBalancesBefore[2] = token2.balanceOf(alice);
|
||||||
|
|
||||||
|
poolBalancesBefore[0] = token0.balanceOf(address(pool));
|
||||||
|
poolBalancesBefore[1] = token1.balanceOf(address(pool));
|
||||||
|
poolBalancesBefore[2] = token2.balanceOf(address(pool));
|
||||||
|
|
||||||
|
// Execute flash loan
|
||||||
|
borrower.flash(amounts);
|
||||||
|
|
||||||
|
// Check balances for each token
|
||||||
|
for (uint256 i = 0; i < 3; i++) {
|
||||||
|
uint256 fee = (amounts[i] * pool.flashFeePpm() + 1_000_000 - 1) / 1_000_000; // ceil fee calculation
|
||||||
|
uint256 expectedAliceDecrease = fee;
|
||||||
|
|
||||||
|
IERC20 token;
|
||||||
|
if (i == 0) token = token0;
|
||||||
|
else if (i == 1) token = token1;
|
||||||
|
else token = token2;
|
||||||
|
|
||||||
|
// Net change for Alice should equal the flash fee for this token (principal was returned)
|
||||||
|
assertEq(
|
||||||
|
aliceBalancesBefore[i] - token.balanceOf(alice),
|
||||||
|
expectedAliceDecrease,
|
||||||
|
"Alice should pay flash fee for token"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Pool's balance increased by fee
|
||||||
|
assertEq(
|
||||||
|
token.balanceOf(address(pool)),
|
||||||
|
poolBalancesBefore[i] + fee,
|
||||||
|
"Pool should receive fee for token"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Test flash loan with some zero amounts (should be skipped)
|
||||||
|
function testFlashLoanWithZeroAmounts() public {
|
||||||
|
FlashBorrower borrower = setupFlashBorrower();
|
||||||
|
|
||||||
|
// Configure borrower to repay normally
|
||||||
|
borrower.setAction(FlashBorrower.Action.NORMAL, alice);
|
||||||
|
|
||||||
|
// Create loan request with mix of zero and non-zero amounts
|
||||||
|
uint256[] memory amounts = new uint256[](3);
|
||||||
|
amounts[0] = 0; // Zero - should be skipped
|
||||||
|
amounts[1] = 2000; // Non-zero
|
||||||
|
amounts[2] = 0; // Zero - should be skipped
|
||||||
|
|
||||||
|
// Record balances before flash
|
||||||
|
uint256 aliceToken1Before = token1.balanceOf(alice);
|
||||||
|
uint256 poolToken1Before = token1.balanceOf(address(pool));
|
||||||
|
|
||||||
|
// Tokens that should remain unchanged
|
||||||
|
uint256 aliceToken0Before = token0.balanceOf(alice);
|
||||||
|
uint256 aliceToken2Before = token2.balanceOf(alice);
|
||||||
|
uint256 poolToken0Before = token0.balanceOf(address(pool));
|
||||||
|
uint256 poolToken2Before = token2.balanceOf(address(pool));
|
||||||
|
|
||||||
|
// Execute flash loan
|
||||||
|
borrower.flash(amounts);
|
||||||
|
|
||||||
|
// Check token1 balances changed appropriately
|
||||||
|
uint256 fee = (amounts[1] * pool.flashFeePpm() + 1_000_000 - 1) / 1_000_000; // ceil fee calculation
|
||||||
|
uint256 expectedAliceDecrease = fee;
|
||||||
|
|
||||||
|
assertEq(
|
||||||
|
aliceToken1Before - token1.balanceOf(alice),
|
||||||
|
expectedAliceDecrease,
|
||||||
|
"Alice should pay flash fee for token1"
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEq(
|
||||||
|
token1.balanceOf(address(pool)),
|
||||||
|
poolToken1Before + fee,
|
||||||
|
"Pool should receive fee for token1"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check token0 and token2 balances remained unchanged
|
||||||
|
assertEq(token0.balanceOf(alice), aliceToken0Before, "Alice token0 balance should be unchanged");
|
||||||
|
assertEq(token2.balanceOf(alice), aliceToken2Before, "Alice token2 balance should be unchanged");
|
||||||
|
assertEq(token0.balanceOf(address(pool)), poolToken0Before, "Pool token0 balance should be unchanged");
|
||||||
|
assertEq(token2.balanceOf(address(pool)), poolToken2Before, "Pool token2 balance should be unchanged");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Test that flash reverts when all amounts are zero
|
||||||
|
function testFlashLoanAllZeroAmountsReverts() public {
|
||||||
|
FlashBorrower borrower = setupFlashBorrower();
|
||||||
|
|
||||||
|
// Configure borrower to repay normally
|
||||||
|
borrower.setAction(FlashBorrower.Action.NORMAL, alice);
|
||||||
|
|
||||||
|
// Create loan request with all zeros
|
||||||
|
uint256[] memory amounts = new uint256[](3);
|
||||||
|
amounts[0] = 0;
|
||||||
|
amounts[1] = 0;
|
||||||
|
amounts[2] = 0;
|
||||||
|
|
||||||
|
// Execute flash loan - should revert
|
||||||
|
vm.expectRevert(bytes("flash: no tokens requested"));
|
||||||
|
borrower.flash(amounts);
|
||||||
|
}
|
||||||
|
|
||||||
/// @notice Test flash loan with incorrect repayment (none)
|
/// @notice Test flash loan with incorrect repayment (none)
|
||||||
function testFlashLoanNoRepaymentReverts() public {
|
function testFlashLoanNoRepaymentReverts() public {
|
||||||
@@ -874,11 +1024,12 @@ contract PartyPoolTest is Test {
|
|||||||
borrower.setAction(FlashBorrower.Action.REPAY_NONE, alice);
|
borrower.setAction(FlashBorrower.Action.REPAY_NONE, alice);
|
||||||
|
|
||||||
// Create loan request
|
// Create loan request
|
||||||
uint256 amount = 1000;
|
uint256[] memory amounts = new uint256[](3);
|
||||||
|
amounts[0] = 1000;
|
||||||
|
|
||||||
// Execute flash loan - should revert due to insufficient allowance when pool tries to pull repayment
|
// Execute flash loan - should revert on validation
|
||||||
vm.expectRevert();
|
vm.expectRevert(bytes("flash: repayment failed"));
|
||||||
pool.flashLoan(borrower, address(token0), amount, "");
|
borrower.flash(amounts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Test flash loan with partial repayment (should revert)
|
/// @notice Test flash loan with partial repayment (should revert)
|
||||||
@@ -889,11 +1040,12 @@ contract PartyPoolTest is Test {
|
|||||||
borrower.setAction(FlashBorrower.Action.REPAY_PARTIAL, alice);
|
borrower.setAction(FlashBorrower.Action.REPAY_PARTIAL, alice);
|
||||||
|
|
||||||
// Create loan request
|
// Create loan request
|
||||||
uint256 amount = 1000;
|
uint256[] memory amounts = new uint256[](3);
|
||||||
|
amounts[0] = 1000;
|
||||||
|
|
||||||
// Execute flash loan - should revert due to insufficient allowance when pool tries to pull full repayment
|
// Execute flash loan - should revert on validation
|
||||||
vm.expectRevert();
|
vm.expectRevert(bytes("flash: repayment failed"));
|
||||||
pool.flashLoan(borrower, address(token0), amount, "");
|
borrower.flash(amounts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Test flash loan with principal repayment but no fee (should revert)
|
/// @notice Test flash loan with principal repayment but no fee (should revert)
|
||||||
@@ -904,15 +1056,16 @@ contract PartyPoolTest is Test {
|
|||||||
borrower.setAction(FlashBorrower.Action.REPAY_NO_FEE, alice);
|
borrower.setAction(FlashBorrower.Action.REPAY_NO_FEE, alice);
|
||||||
|
|
||||||
// Create loan request
|
// Create loan request
|
||||||
uint256 amount = 1000;
|
uint256[] memory amounts = new uint256[](3);
|
||||||
|
amounts[0] = 1000;
|
||||||
|
|
||||||
// Execute flash loan - should revert due to insufficient allowance if fee > 0
|
// Execute flash loan - should revert on validation if fee > 0
|
||||||
if (pool.flashFeePpm() > 0) {
|
if (pool.flashFeePpm() > 0) {
|
||||||
vm.expectRevert();
|
vm.expectRevert(bytes("flash: repayment failed"));
|
||||||
pool.flashLoan(borrower, address(token0), amount, "");
|
borrower.flash(amounts);
|
||||||
} else {
|
} else {
|
||||||
// If fee is zero, this should succeed
|
// If fee is zero, this should succeed
|
||||||
pool.flashLoan(borrower, address(token0), amount, "");
|
borrower.flash(amounts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -924,17 +1077,18 @@ contract PartyPoolTest is Test {
|
|||||||
borrower.setAction(FlashBorrower.Action.REPAY_EXACT, alice);
|
borrower.setAction(FlashBorrower.Action.REPAY_EXACT, alice);
|
||||||
|
|
||||||
// Create loan request
|
// Create loan request
|
||||||
uint256 amount = 1000;
|
uint256[] memory amounts = new uint256[](3);
|
||||||
|
amounts[0] = 1000;
|
||||||
|
|
||||||
// Record balances before flash
|
// Record balances before flash
|
||||||
uint256 aliceToken0Before = token0.balanceOf(alice);
|
uint256 aliceToken0Before = token0.balanceOf(alice);
|
||||||
uint256 poolToken0Before = token0.balanceOf(address(pool));
|
uint256 poolToken0Before = token0.balanceOf(address(pool));
|
||||||
|
|
||||||
// Execute flash loan
|
// Execute flash loan
|
||||||
pool.flashLoan(borrower, address(token0), amount, "");
|
borrower.flash(amounts);
|
||||||
|
|
||||||
// Check balances: net change for alice should equal the fee
|
// Check balances: net change for alice should equal the fee
|
||||||
uint256 fee = (amount * pool.flashFeePpm() + 1_000_000 - 1) / 1_000_000; // ceil fee calculation
|
uint256 fee = (amounts[0] * pool.flashFeePpm() + 1_000_000 - 1) / 1_000_000; // ceil fee calculation
|
||||||
uint256 expectedAliceDecrease = fee;
|
uint256 expectedAliceDecrease = fee;
|
||||||
|
|
||||||
assertEq(
|
assertEq(
|
||||||
@@ -950,29 +1104,115 @@ contract PartyPoolTest is Test {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Test flashFee view function matches flash implementation
|
/// @notice Test flash loan with extra repayment (donation, should succeed)
|
||||||
function testFlashFee() public view {
|
function testFlashLoanExtraRepayment() public {
|
||||||
// Test different loan amounts
|
FlashBorrower borrower = setupFlashBorrower();
|
||||||
uint256[] memory testAmounts = new uint256[](3);
|
|
||||||
testAmounts[0] = 1000;
|
|
||||||
testAmounts[1] = 2000;
|
|
||||||
testAmounts[2] = 3000;
|
|
||||||
|
|
||||||
for (uint256 i = 0; i < testAmounts.length; i++) {
|
// Configure borrower to repay more than required
|
||||||
uint256 amount = testAmounts[i];
|
borrower.setAction(FlashBorrower.Action.REPAY_EXTRA, alice);
|
||||||
uint256 fee = viewer.flashFee(pool, address(token0), amount);
|
|
||||||
|
|
||||||
// Calculate expected fee
|
// Create loan request
|
||||||
uint256 expectedFee = (amount * pool.flashFeePpm() + 1_000_000 - 1) / 1_000_000; // ceiling
|
uint256[] memory amounts = new uint256[](3);
|
||||||
|
amounts[0] = 1000;
|
||||||
|
|
||||||
assertEq(
|
// Record balances before flash
|
||||||
fee,
|
uint256 aliceToken0Before = token0.balanceOf(alice);
|
||||||
expectedFee,
|
uint256 poolToken0Before = token0.balanceOf(address(pool));
|
||||||
"Flash fee calculation mismatch"
|
|
||||||
);
|
// Execute flash loan
|
||||||
|
borrower.flash(amounts);
|
||||||
|
|
||||||
|
// Check balances - net change for alice should equal fee + extra donation (principal returned)
|
||||||
|
uint256 fee = (amounts[0] * pool.flashFeePpm() + 1_000_000 - 1) / 1_000_000; // ceil fee calculation
|
||||||
|
uint256 extra = 1; // borrower donates +1 per token in REPAY_EXTRA
|
||||||
|
uint256 expectedAliceDecrease = fee + extra; // fee plus donation
|
||||||
|
|
||||||
|
assertEq(
|
||||||
|
aliceToken0Before - token0.balanceOf(alice),
|
||||||
|
expectedAliceDecrease,
|
||||||
|
"Alice should pay fee + extra"
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEq(
|
||||||
|
token0.balanceOf(address(pool)),
|
||||||
|
poolToken0Before + fee + extra,
|
||||||
|
"Pool should receive fee + extra"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Test flashRepaymentAmounts matches flash implementation
|
||||||
|
function testFlashRepaymentAmounts() public view {
|
||||||
|
// Create different loan amount scenarios
|
||||||
|
uint256[][] memory testCases = new uint256[][](3);
|
||||||
|
|
||||||
|
// Case 1: Single token
|
||||||
|
testCases[0] = new uint256[](3);
|
||||||
|
testCases[0][0] = 1000;
|
||||||
|
testCases[0][1] = 0;
|
||||||
|
testCases[0][2] = 0;
|
||||||
|
|
||||||
|
// Case 2: Multiple tokens
|
||||||
|
testCases[1] = new uint256[](3);
|
||||||
|
testCases[1][0] = 1000;
|
||||||
|
testCases[1][1] = 2000;
|
||||||
|
testCases[1][2] = 3000;
|
||||||
|
|
||||||
|
// Case 3: Mix of zero and non-zero
|
||||||
|
testCases[2] = new uint256[](3);
|
||||||
|
testCases[2][0] = 0;
|
||||||
|
testCases[2][1] = 2000;
|
||||||
|
testCases[2][2] = 0;
|
||||||
|
|
||||||
|
for (uint256 i = 0; i < testCases.length; i++) {
|
||||||
|
uint256[] memory loanAmounts = testCases[i];
|
||||||
|
uint256[] memory repaymentAmounts = pool.flashRepaymentAmounts(loanAmounts);
|
||||||
|
|
||||||
|
// Verify each repayment amount is correctly calculated
|
||||||
|
for (uint256 j = 0; j < loanAmounts.length; j++) {
|
||||||
|
if (loanAmounts[j] == 0) {
|
||||||
|
// Zero loans should have zero repayment
|
||||||
|
assertEq(repaymentAmounts[j], 0, "Zero loan should have zero repayment");
|
||||||
|
} else {
|
||||||
|
// Calculate expected repayment with fee
|
||||||
|
uint256 fee = (loanAmounts[j] * pool.flashFeePpm() + 1_000_000 - 1) / 1_000_000; // ceiling
|
||||||
|
uint256 expectedRepayment = loanAmounts[j] + fee;
|
||||||
|
|
||||||
|
assertEq(
|
||||||
|
repaymentAmounts[j],
|
||||||
|
expectedRepayment,
|
||||||
|
"Repayment calculation mismatch"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// @notice Test flash with invalid recipient
|
||||||
|
function testFlashWithZeroRecipientReverts() public {
|
||||||
|
FlashBorrower borrower = setupFlashBorrower();
|
||||||
|
|
||||||
|
// Configure borrower with zero recipient
|
||||||
|
borrower.setAction(FlashBorrower.Action.NORMAL, address(0));
|
||||||
|
|
||||||
|
// Create loan request
|
||||||
|
uint256[] memory amounts = new uint256[](3);
|
||||||
|
amounts[0] = 1000;
|
||||||
|
|
||||||
|
// Execute flash loan - should revert due to zero recipient
|
||||||
|
vm.expectRevert(bytes("flash: zero recipient"));
|
||||||
|
borrower.flash(amounts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Test flash with incorrect amounts length
|
||||||
|
function testFlashWithIncorrectLengthReverts() public {
|
||||||
|
// Call flash directly with incorrect length
|
||||||
|
uint256[] memory wrongLengthAmounts = new uint256[](2); // Pool has 3 tokens
|
||||||
|
wrongLengthAmounts[0] = 1000;
|
||||||
|
wrongLengthAmounts[1] = 2000;
|
||||||
|
|
||||||
|
vm.expectRevert(bytes("flash: amounts length mismatch"));
|
||||||
|
pool.flash(alice, wrongLengthAmounts, "");
|
||||||
|
}
|
||||||
|
|
||||||
/// @notice Test that passing nonzero lpTokens to initialMint doesn't affect swap results
|
/// @notice Test that passing nonzero lpTokens to initialMint doesn't affect swap results
|
||||||
/// compared to pools initialized with default lpTokens (0)
|
/// compared to pools initialized with default lpTokens (0)
|
||||||
@@ -1090,7 +1330,7 @@ contract PartyPoolTest is Test {
|
|||||||
uint256 lpDefault = poolDefault.initialMint(address(this), 0);
|
uint256 lpDefault = poolDefault.initialMint(address(this), 0);
|
||||||
uint256 scaleFactor = 3;
|
uint256 scaleFactor = 3;
|
||||||
uint256 customLpAmount = lpDefault * scaleFactor;
|
uint256 customLpAmount = lpDefault * scaleFactor;
|
||||||
poolCustom.initialMint(address(this), customLpAmount);
|
uint256 lpCustom = poolCustom.initialMint(address(this), customLpAmount);
|
||||||
|
|
||||||
// Verify initial LP supplies
|
// Verify initial LP supplies
|
||||||
assertEq(poolDefault.totalSupply(), lpDefault, "Default pool should have default LP supply");
|
assertEq(poolDefault.totalSupply(), lpDefault, "Default pool should have default LP supply");
|
||||||
@@ -1117,8 +1357,8 @@ contract PartyPoolTest is Test {
|
|||||||
token2.approve(address(poolCustom), type(uint256).max);
|
token2.approve(address(poolCustom), type(uint256).max);
|
||||||
|
|
||||||
// Get required deposit amounts for both pools
|
// Get required deposit amounts for both pools
|
||||||
uint256[] memory depositsDefault = viewer.mintAmounts(poolDefault, lpRequestDefault);
|
uint256[] memory depositsDefault = poolDefault.mintDepositAmounts(lpRequestDefault);
|
||||||
uint256[] memory depositsCustom = viewer.mintAmounts(poolCustom, lpRequestCustom);
|
uint256[] memory depositsCustom = poolCustom.mintDepositAmounts(lpRequestCustom);
|
||||||
|
|
||||||
// Deposits should be identical (same proportion of identical balances)
|
// Deposits should be identical (same proportion of identical balances)
|
||||||
assertEq(depositsDefault[0], depositsCustom[0], "Token0 deposits should be identical");
|
assertEq(depositsDefault[0], depositsCustom[0], "Token0 deposits should be identical");
|
||||||
@@ -1146,4 +1386,3 @@ contract PartyPoolTest is Test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
/* solhint-enable */
|
|
||||||
|
|||||||
Reference in New Issue
Block a user