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