per-asset fees
This commit is contained in:
@@ -191,7 +191,46 @@ and at $\delta=0$ the symmetry reduces to $y(a)\approx \tfrac{a}{2} - \tfrac{a^2
|
||||
- Regardless of path, our policy prioritizes monotonicity, domain safety, and conservative decisions at boundaries.
|
||||
|
||||
## 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$.
|
||||
Fees are applied outside the fee-free LMSR kernel and are retained in the pool so that fee accrual increases the size metric $S(\mathbf{q})$ (and thereby raises LP share value under $L\propto S$). We extend the single global swap-fee to a per-token fee vector $f = (f_0, f_1, \dots, f_{n-1})$, where $f_i$ denotes the canonical fee rate associated with token $i$ (expressed as a fractional rate, e.g., ppm or bps). For a trade that takes token $i$ as input and token $j$ as output, the pool computes an effective pair fee and applies it to the user-submitted input before invoking the fee-free kernel.
|
||||
|
||||
Effective pair fee composition
|
||||
- The effective fee for an asset-to-asset swap is an exact multiplicative composition:
|
||||
$$
|
||||
f_{\mathrm{eff}} \;=\; 1 - (1 - f_i)\,(1 - f_j),
|
||||
$$
|
||||
|
||||
Asset-to-asset policy
|
||||
- Charge on input: compute the kernel input as
|
||||
$$
|
||||
a_{\mathrm{eff}} \;=\; a_{\mathrm{user}}\,(1 - f_{\mathrm{eff}})
|
||||
$$
|
||||
and pass $a_{\mathrm{eff}}$ to the fee-free LMSR kernel; collect the retained fee in the input token. Charging on input keeps gas logic simple and makes effective price shifts monotone and predictable for takers.
|
||||
- Protocol fees are taken as a fraction of LP fee earnings, and immediately moved to a separate account that does not participate in liquidity operations.
|
||||
- Limit semantics: all limitPrice and cap computations refer to the fee-free kernel path (i.e., are evaluated against the kernel output computed from $a_{\mathrm{eff}}$). Aggregators should compute $f_{\mathrm{eff}}$ externally and present accurate expected outputs to users.
|
||||
|
||||
SwapMint and BurnSwap: single-asset fee policy
|
||||
- Overview: swapMint (single-asset mint) and burnSwap (single-asset redeem) are single-asset-facing operations that interface the pool with one external token and the LP shares on the other side. To keep semantics simple and predictable, only the single asset’s fee is applied for these operations; there is no separate “LP token” fee ($f_{\mathrm{LP}} = 0$).
|
||||
- swapMint (deposit one token to mint LP):
|
||||
- Apply only the deposited token’s fee $f_i$. Compute the effective kernel input as
|
||||
$$
|
||||
a_{\mathrm{eff}} \;=\; a_{\mathrm{user}}\,(1 - f_i).
|
||||
$$
|
||||
The fee is collected in the deposited token and split by the protocol share $\phi$; the remainder increases the pool (raising $S$ and accruing value to LPs implicitly).
|
||||
- burnSwap (burn LP to receive one token):
|
||||
- Apply only the withdrawn token’s fee $f_j$. Compute the gross payout via the fee-free kernel for the burned LP fraction, and remit to the user:
|
||||
$$
|
||||
\text{payout}_{\mathrm{net}} \;=\; \text{payout}_{\mathrm{gross}}\,(1 - f_j).
|
||||
$$
|
||||
The collected fee is retained in the withdrawn token, the protocol takes $\phi$ of that fee, and the remainder increases pool-held balances (raising $S$ for remaining LPs).
|
||||
|
||||
Rationale and interactions with pair-fee rules
|
||||
- Conceptual consistency: single-asset operations touch a single external token, so applying only that token’s fee is the most natural mapping from the per-token fee vector to operation-level economics. For asset-to-asset swaps the two-token multiplicative composition remains the canonical rule.
|
||||
- $f_{\mathrm{LP}} = 0$: keeping LP-side fees at zero simplifies LP accounting and avoids double charging when LP shares are the other leg of a transaction. LPs still receive value via retained fees that increase $S$.
|
||||
|
||||
Rounding
|
||||
Our policies aim to always protect the LPs value.
|
||||
- Total fees are rounded up for the benefit of the pool and against takers.
|
||||
- The Protocol fee is rounded down for the benefit of LPs and against the protocol developers.
|
||||
|
||||
## Risk Management and Bounded Loss
|
||||
Under constant $b$, classical LMSR admits a worst-case loss bound of $b \ln n$ in the payoff numéraire. With $b(\mathbf{q})=\kappa S(\mathbf{q})$, the per-state bound scales with the current size metric, giving an instantaneous worst-case loss $b(\mathbf{q}) \ln n = \kappa\,S(\mathbf{q})\,\ln n$; per unit of size $S$ this is $\kappa \ln n$. Because $b$ varies with state, global worst-case loss along an arbitrary path depends on how $S$ evolves, but the proportionality clarifies how risk scales with liquidity. Operational mitigations include user price limits (swap-to-limit), capacity caps on outputs ($y \le q_j$), minimum bootstrap $S^{(0)}$ to avoid thin-liquidity regimes, and strict numerical guards (e.g., positivity of inner logarithm arguments) to prevent nonphysical states.
|
||||
@@ -206,7 +245,7 @@ $$
|
||||
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.
|
||||
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)
|
||||
|
||||
172
research/fees.py
Normal file
172
research/fees.py
Normal file
@@ -0,0 +1,172 @@
|
||||
import logging
|
||||
import math
|
||||
from itertools import combinations
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
# Now interpret the `weights` mapping as the desired pair fee (in bps) vs USDC.
|
||||
# Examples:
|
||||
# - USDC: 0.0 -> trading USDC<>USDC costs 0 bps
|
||||
# - USDT: 0.10 -> trading USDT<>USDC costs ~0.1 bps
|
||||
# - WIF: 30.0 -> trading WIF<>USDC costs ~30 bps
|
||||
#
|
||||
# For arbitrary pair A<>B we compose per-asset fees multiplicatively:
|
||||
# f_eff = 1 - (1 - f_A) * (1 - f_B)
|
||||
# which is ≈ f_A + f_B for small fees.
|
||||
|
||||
STABLECOIN_RATE = 0.01 # bps
|
||||
X0 = STABLECOIN_RATE / 2
|
||||
targets = {
|
||||
'USDT': STABLECOIN_RATE,
|
||||
'USDC': STABLECOIN_RATE,
|
||||
'WBTC': 3.0, # ~3 bps vs USDC
|
||||
'WETH': 5.0, # ~5 bps vs USDC
|
||||
'WIF': 30.0, # ~30 bps vs USDC
|
||||
}
|
||||
weights = {k:v-X0 for k,v in targets.items()}
|
||||
|
||||
# UI / safety parameters (bps)
|
||||
# cap_pair_fee_bps: global cap on the composed pair fee to avoid degenerate values (set to None to disable)
|
||||
cap_pair_fee_bps = None # safety cap (bps); adjust or set to None to disable
|
||||
|
||||
# Compute geometric mean "base" of weights (kept for diagnostics)
|
||||
def geometric_mean(vals):
|
||||
# Guard: all weights must be positive (allow zero for USDC)
|
||||
for v in vals:
|
||||
if v < 0:
|
||||
raise ValueError("All weights must be non-negative")
|
||||
# exclude zeros when computing geometric mean to avoid exact zero product
|
||||
pos_vals = [v for v in vals if v > 0]
|
||||
if not pos_vals:
|
||||
return 0.0
|
||||
return math.prod(pos_vals) ** (1.0 / len(pos_vals))
|
||||
|
||||
base = geometric_mean(list(weights.values()))
|
||||
ref_asset = 'USDC'
|
||||
|
||||
# Convert UI bps -> fractional form for internal calculations
|
||||
def bps_to_frac(bps_val):
|
||||
return bps_val / 10_000.0
|
||||
|
||||
log.info(
|
||||
f"Fee model (weights are target bps vs USDC): ref_asset={ref_asset}, base_geo={base:.6f}"
|
||||
)
|
||||
|
||||
def clamp_nonneg(x):
|
||||
return x if x >= 0.0 else 0.0
|
||||
|
||||
def clamp_cap_frac(x_frac):
|
||||
"""Clamp a fractional fee to configured cap_pair_fee_bps if set"""
|
||||
if cap_pair_fee_bps is None:
|
||||
return x_frac
|
||||
cap_frac = bps_to_frac(cap_pair_fee_bps)
|
||||
return min(x_frac, cap_frac)
|
||||
|
||||
def per_asset_fee(w_bps):
|
||||
"""
|
||||
Interpret `w_bps` directly as the target bps vs USDC for that asset and
|
||||
return the fractional per-asset fee (clamped).
|
||||
|
||||
So for asset A:
|
||||
f_A = bps_to_frac(weights[A])
|
||||
"""
|
||||
return clamp_nonneg(bps_to_frac(w_bps))
|
||||
|
||||
def fee_for_pair(w_in_bps, w_out_bps):
|
||||
"""
|
||||
Compute effective fee fraction for a swap i->j using multiplicative composition
|
||||
of per-asset fees:
|
||||
|
||||
f_i = per_asset_fee(w_in_bps)
|
||||
f_j = per_asset_fee(w_out_bps)
|
||||
f_eff = 1 - (1 - f_i) * (1 - f_j)
|
||||
|
||||
Returns tuple (f_eff_frac, f_i_frac, f_j_frac).
|
||||
"""
|
||||
f_i = per_asset_fee(w_in_bps)
|
||||
f_j = per_asset_fee(w_out_bps)
|
||||
|
||||
# multiplicative composition (accurate and gas-cheap counterpart)
|
||||
f_eff = 1.0 - (1.0 - f_i) * (1.0 - f_j)
|
||||
|
||||
# apply global cap if configured
|
||||
f_eff = clamp_cap_frac(f_eff)
|
||||
return clamp_nonneg(f_eff), f_i, f_j
|
||||
|
||||
def pct(x):
|
||||
return f"{100.0 * x:.6f}%"
|
||||
|
||||
def bps(x):
|
||||
# x expected as fraction; convert to bps (1 bps = 1/10000)
|
||||
return f"{10_000.0 * x:.4f} bps"
|
||||
|
||||
if __name__ == "__main__":
|
||||
names = list(weights.keys())
|
||||
vals = list(weights.values())
|
||||
|
||||
# Warn if any single-asset implied fee would produce large numbers
|
||||
alert_threshold_bps = 1000.0
|
||||
alerts = []
|
||||
max_w = max(weights.values())
|
||||
min_w = min(weights.values())
|
||||
max_diff = max_w - min_w
|
||||
if max_w > alert_threshold_bps:
|
||||
alerts.append(("single_asset_too_large_bps", max_w))
|
||||
if alerts:
|
||||
log.warning("Some fee configuration may produce large values (bps): %s", alerts)
|
||||
|
||||
print("\nPair fees [per-asset fee -> total]:")
|
||||
for (name_a, w_a), (name_b, w_b) in combinations(weights.items(), 2):
|
||||
f_eff_ab, f_i_ab, f_j_ab = fee_for_pair(w_a, w_b)
|
||||
|
||||
print(
|
||||
f"{name_a}/{name_b}: per-asset_in={bps(f_i_ab)}, per-asset_out={bps(f_j_ab)}, total={bps(f_eff_ab)}"
|
||||
)
|
||||
|
||||
# Per-asset diagnostic: show per-asset fee (interpreted vs USDC)
|
||||
print("\nPer-asset diagnostics (fees interpreted vs USDC):")
|
||||
for n, w in weights.items():
|
||||
f_token = per_asset_fee(w)
|
||||
print(f"{n}: target_vs_USDC={w:.6f} bps, per_asset_fee={bps(f_token)}")
|
||||
|
||||
# -- Solidity (ABDK Q64.64) output --
|
||||
# Print derived per-asset fees and cap as Solidity int128 Q64.64 constants that can be pasted
|
||||
# into a Solidity file using ABDK's 64.64 fixed-point representation (int128 constants).
|
||||
print("\n// Solidity (ABDK Q64.64) constants - generated by research/fees.py")
|
||||
print("// Per-asset fees (FEE_<SYMBOL>) are Q64.64 int128 literals representing the fractional fee")
|
||||
print("// Example: int128 internal constant FEE_USDT = 0x012345...; // comment\n")
|
||||
|
||||
# Helper: order tokens consistently for array examples
|
||||
token_names = list(weights.keys())
|
||||
|
||||
for name in token_names:
|
||||
w_bps = weights[name]
|
||||
t_bps = targets[name]
|
||||
f_frac = per_asset_fee(w_bps)
|
||||
q64_int = int(round(f_frac * (1 << 64)))
|
||||
hexstr = hex(q64_int)
|
||||
print(f"int128 internal constant FEE_{name} = {hexstr}; // {bps(f_frac)}")
|
||||
|
||||
# Cap constant if configured
|
||||
if cap_pair_fee_bps is not None:
|
||||
cap_frac = bps_to_frac(cap_pair_fee_bps)
|
||||
cap_q64 = int(round(cap_frac * (1 << 64)))
|
||||
print(f"int128 internal constant CAP_PAIR_FEE_Q64 = {hex(cap_q64)}; // cap {cap_pair_fee_bps} bps")
|
||||
|
||||
# Example how to assemble into an array in the same order as `token_names`.
|
||||
print("\n// Example: build a per-asset fee array (same ordering as token_names list above)")
|
||||
print(f"// token order: {token_names}")
|
||||
print(f"int128[] memory perAssetFeesQ64 = new int128[]({len(token_names)});")
|
||||
for idx, name in enumerate(token_names):
|
||||
print(f"perAssetFeesQ64[{idx}] = FEE_{name};")
|
||||
|
||||
print("\n// End of generated Solidity constants\n")
|
||||
|
||||
# Notes:
|
||||
# - weights[name] should be set to the desired bps (not fraction) vs USDC.
|
||||
# - Per-asset fraction f_token = bps_to_frac(weights[name]) and
|
||||
# composed pair fee is f_eff = 1 - (1 - f_i) * (1 - f_j).
|
||||
# - This keeps USDC as the reference (weights['USDC'] == 0.0).
|
||||
|
||||
@@ -2,7 +2,9 @@ import { ethers } from 'ethers';
|
||||
import { Token } from '@uniswap/sdk-core';
|
||||
import fs from 'fs';
|
||||
|
||||
// Token definitions
|
||||
//
|
||||
// TOKEN DEFINITIONS
|
||||
//
|
||||
const ChainId = {
|
||||
MAINNET: 1
|
||||
};
|
||||
@@ -31,11 +33,18 @@ const USDT_TOKEN= new Token(
|
||||
'USDT'
|
||||
);
|
||||
|
||||
|
||||
//
|
||||
// POOL DEFINITION
|
||||
//
|
||||
|
||||
const tokenA = USDC_TOKEN
|
||||
const tokenB = WETH_TOKEN
|
||||
// Pool ID to fetch pool key from
|
||||
// const POOL_ID = '0x8aa4e11cbdf30eedc92100f4c8a31ff748e201d44712cc8c90d189edaa8e4e47';
|
||||
const POOL_ID = '0xdce6394339af00981949f5f3baf27e3610c76326a700af57e4b3e3ae4977f78d';
|
||||
// const POOL_ID = '0x8aa4e11cbdf30eedc92100f4c8a31ff748e201d44712cc8c90d189edaa8e4e47'; // USDT-USDC 0.0010%
|
||||
// const POOL_ID = '0xdce6394339af00981949f5f3baf27e3610c76326a700af57e4b3e3ae4977f78d'; // USDC-WETH 0.30%
|
||||
const POOL_ID = '0xdce6394339af00981949f5f3baf27e3610c76326a700af57e4b3e3ae4977f78d'; // USDC-WETH 0.05%
|
||||
|
||||
|
||||
// Configuration
|
||||
const QUOTER_ADDRESS = '0x52f0e24d1c21c8a0cb1e5a5dd6198556bd9e1203';
|
||||
|
||||
@@ -61,8 +61,8 @@ interface IPartyPool is IERC20Metadata, IOwnable {
|
||||
IERC20 indexed tokenOut,
|
||||
uint256 amountIn,
|
||||
uint256 amountOut,
|
||||
uint256 lpFee,
|
||||
uint256 protocolFee
|
||||
uint256 lpFee, // taken from the output token
|
||||
uint256 protocolFee // taken from the output token
|
||||
);
|
||||
|
||||
event Flash(
|
||||
@@ -97,8 +97,11 @@ interface IPartyPool is IERC20Metadata, IOwnable {
|
||||
/// @dev denominators()[i] is the base for tokens[i]. These bases are chosen by deployer and must match token decimals.
|
||||
function denominators() external view returns (uint256[] memory);
|
||||
|
||||
/// @notice Per-swap fee in parts-per-million (ppm). Fee is taken from input amounts before LMSR computations.
|
||||
function swapFeePpm() external view returns (uint256);
|
||||
/// @notice Per-asset swap fees in ppm. Fees are applied on input; for asset-to-asset swaps, the effective pair fee is 1 - (1 - f_i)(1 - f_j).
|
||||
function fees() external view returns (uint256[] memory);
|
||||
|
||||
/// @notice Effective combined fee in ppm for the given asset pair (i as input, j as output).
|
||||
function fee(uint256 i, uint256 j) external view returns (uint256);
|
||||
|
||||
/// @notice Flash-loan fee in parts-per-million (ppm) applied to flash borrow amounts.
|
||||
function flashFeePpm() external view returns (uint256);
|
||||
@@ -163,13 +166,13 @@ interface IPartyPool is IERC20Metadata, IOwnable {
|
||||
/// @param outputTokenIndex index of output token
|
||||
/// @param maxAmountIn maximum gross input allowed (inclusive of fee)
|
||||
/// @param limitPrice maximum acceptable marginal price (pass 0 to ignore)
|
||||
/// @return amountIn gross input amount to transfer (includes fee), amountOut output amount user would receive, fee fee amount taken
|
||||
/// @return amountIn gross input amount to transfer (includes fee), amountOut output amount user would receive, inFee fee taken from input amount
|
||||
function swapAmounts(
|
||||
uint256 inputTokenIndex,
|
||||
uint256 outputTokenIndex,
|
||||
uint256 maxAmountIn,
|
||||
int128 limitPrice
|
||||
) external view returns (uint256 amountIn, uint256 amountOut, uint256 fee);
|
||||
) external view returns (uint256 amountIn, uint256 amountOut, uint256 inFee);
|
||||
|
||||
/// @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.
|
||||
@@ -181,7 +184,7 @@ interface IPartyPool is IERC20Metadata, IOwnable {
|
||||
/// @param maxAmountIn maximum amount of token inputTokenIndex (uint256) to transfer in (inclusive of fees)
|
||||
/// @param limitPrice maximum acceptable marginal price (64.64 fixed point). Pass 0 to ignore.
|
||||
/// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore.
|
||||
/// @return amountIn actual input used (uint256), amountOut actual output sent (uint256), fee fee taken from the input (uint256)
|
||||
/// @return amountIn actual input used (uint256), amountOut actual output sent (uint256), inFee fee taken from the input (uint256)
|
||||
function swap(
|
||||
address payer,
|
||||
address receiver,
|
||||
@@ -191,7 +194,7 @@ interface IPartyPool is IERC20Metadata, IOwnable {
|
||||
int128 limitPrice,
|
||||
uint256 deadline,
|
||||
bool unwrap
|
||||
) external payable returns (uint256 amountIn, uint256 amountOut, uint256 fee);
|
||||
) external payable returns (uint256 amountIn, uint256 amountOut, uint256 inFee);
|
||||
|
||||
/// @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.
|
||||
@@ -202,7 +205,7 @@ interface IPartyPool is IERC20Metadata, IOwnable {
|
||||
/// @param outputTokenIndex index of output asset
|
||||
/// @param limitPrice target marginal price to reach (must be > 0)
|
||||
/// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore.
|
||||
/// @return amountInUsed actual input used excluding fee (uint256), amountOut actual output sent (uint256), fee fee taken from the input (uint256)
|
||||
/// @return amountInUsed actual input used excluding fee (uint256), amountOut actual output sent (uint256), inFee fee taken from the input (uint256)
|
||||
function swapToLimit(
|
||||
address payer,
|
||||
address receiver,
|
||||
@@ -211,7 +214,7 @@ interface IPartyPool is IERC20Metadata, IOwnable {
|
||||
int128 limitPrice,
|
||||
uint256 deadline,
|
||||
bool unwrap
|
||||
) external payable returns (uint256 amountInUsed, uint256 amountOut, uint256 fee);
|
||||
) external payable returns (uint256 amountInUsed, uint256 amountOut, uint256 inFee);
|
||||
|
||||
/// @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.
|
||||
@@ -221,14 +224,14 @@ interface IPartyPool is IERC20Metadata, IOwnable {
|
||||
/// @param inputTokenIndex index of the input token
|
||||
/// @param maxAmountIn maximum uint token input (inclusive of fee)
|
||||
/// @param deadline optional deadline
|
||||
/// @return lpMinted actual LP minted (uint)
|
||||
/// @return amountInUsed actual input used (uint256), lpMinted actual LP minted (uint256), inFee fee taken from the input (uint256)
|
||||
function swapMint(
|
||||
address payer,
|
||||
address receiver,
|
||||
uint256 inputTokenIndex,
|
||||
uint256 maxAmountIn,
|
||||
uint256 deadline
|
||||
) external payable returns (uint256 lpMinted);
|
||||
) external payable returns (uint256 amountInUsed, uint256 lpMinted, uint256 inFee);
|
||||
|
||||
/// @notice Burn LP tokens then swap the redeemed proportional basket into a single asset `outputTokenIndex` and send to receiver.
|
||||
/// @dev The function burns LP tokens (authorization via allowance if needed), sends the single-asset payout and updates LMSR state.
|
||||
|
||||
@@ -34,27 +34,27 @@ interface IPartyPoolViewer {
|
||||
/// @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
|
||||
/// @return amountIn gross input amount to transfer (includes fee), amountOut output amount user would receive, inFee fee taken from input amount
|
||||
function swapToLimitAmounts(
|
||||
IPartyPool pool,
|
||||
uint256 inputTokenIndex,
|
||||
uint256 outputTokenIndex,
|
||||
int128 limitPrice
|
||||
) external view returns (uint256 amountIn, uint256 amountOut, uint256 fee);
|
||||
) external view returns (uint256 amountIn, uint256 amountOut, uint256 inFee);
|
||||
|
||||
/// @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)
|
||||
function swapMintAmounts(IPartyPool pool, uint256 inputTokenIndex, uint256 maxAmountIn) external view
|
||||
returns (uint256 amountInUsed, uint256 fee, uint256 lpMinted);
|
||||
returns (uint256 amountInUsed, uint256 lpMinted, uint256 inFee);
|
||||
|
||||
/// @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 outputTokenIndex index of target asset to receive
|
||||
function burnSwapAmounts(IPartyPool pool, uint256 lpAmount, uint256 outputTokenIndex) external view
|
||||
returns (uint256 amountOut);
|
||||
returns (uint256 amountOut, uint256 outFee);
|
||||
|
||||
/// @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.
|
||||
|
||||
@@ -131,8 +131,6 @@ library LMSRStabilized {
|
||||
int128 a,
|
||||
int128 limitPrice
|
||||
) internal pure returns (int128 amountIn, int128 amountOut) {
|
||||
require(i < nAssets && j < nAssets, "LMSR: idx");
|
||||
|
||||
// Initialize amountIn to full amount (will be adjusted if limit price is hit)
|
||||
amountIn = a;
|
||||
|
||||
@@ -140,48 +138,37 @@ library LMSRStabilized {
|
||||
int128 sizeMetric = _computeSizeMetric(qInternal);
|
||||
require(sizeMetric > int128(0), "LMSR: size metric zero");
|
||||
int128 b = kappa.mul(sizeMetric);
|
||||
require(b > int128(0), "LMSR: b<=0");
|
||||
|
||||
// Precompute reciprocal of b to avoid repeated divisions
|
||||
int128 invB = ABDKMath64x64.div(ONE, b);
|
||||
|
||||
// Guard: output asset must have non-zero effective weight to avoid degenerate/div-by-zero-like conditions
|
||||
require(qInternal[j] > int128(0), "LMSR: e_j==0");
|
||||
|
||||
// Compute r0 = exp((q_i - q_j) / b) directly using invB
|
||||
int128 r0 = _exp(qInternal[i].sub(qInternal[j]).mul(invB));
|
||||
require(r0 > int128(0), "LMSR: r0<=0"); // equivalent to e_j > 0 check
|
||||
|
||||
// If a positive limitPrice is given, determine whether the full `a` would
|
||||
// push the marginal price p_i/p_j beyond the limit; if so, truncate `a`.
|
||||
// Marginal price ratio evolves as r(t) = r0 * exp(t/b) (since e_i multiplies by exp(t/b))
|
||||
if (limitPrice > int128(0)) {
|
||||
// r0 must be positive; if r0 == 0 then no risk of exceeding limit by increasing r.
|
||||
require(r0 >= int128(0), "LMSR: r0<0");
|
||||
if (r0 == int128(0)) {
|
||||
// console2.log("r0 == 0 (input asset has zero weight), no limit truncation needed");
|
||||
// If limitPrice <= current price, we revert (caller must choose a limit > current price to allow any fill)
|
||||
if (limitPrice <= r0) {
|
||||
revert("LMSR: limitPrice <= current price");
|
||||
}
|
||||
|
||||
// Compute a_limit directly from ln(limit / r0): a_limit = b * ln(limit / r0)
|
||||
int128 ratioLimitOverR0 = limitPrice.div(r0);
|
||||
require(ratioLimitOverR0 > int128(0), "LMSR: ratio<=0");
|
||||
|
||||
int128 aLimitOverB = _ln(ratioLimitOverR0); // > 0
|
||||
|
||||
// aLimit = b * aLimitOverB
|
||||
int128 aLimit64 = b.mul(aLimitOverB);
|
||||
|
||||
// If computed aLimit is less than the requested a, use the truncated value.
|
||||
if (aLimit64 < a) {
|
||||
amountIn = aLimit64; // Store the truncated input amount
|
||||
a = aLimit64; // Use truncated amount for calculations
|
||||
} else {
|
||||
// If limitPrice <= current price, we revert (caller must choose a limit > current price to allow any fill)
|
||||
if (limitPrice <= r0) {
|
||||
revert("LMSR: limitPrice <= current price");
|
||||
}
|
||||
|
||||
// Compute a_limit directly from ln(limit / r0): a_limit = b * ln(limit / r0)
|
||||
int128 ratioLimitOverR0 = limitPrice.div(r0);
|
||||
require(ratioLimitOverR0 > int128(0), "LMSR: ratio<=0");
|
||||
|
||||
int128 aLimitOverB = _ln(ratioLimitOverR0); // > 0
|
||||
|
||||
// aLimit = b * aLimitOverB
|
||||
int128 aLimit64 = b.mul(aLimitOverB);
|
||||
|
||||
// If computed aLimit is less than the requested a, use the truncated value.
|
||||
if (aLimit64 < a) {
|
||||
amountIn = aLimit64; // Store the truncated input amount
|
||||
a = aLimit64; // Use truncated amount for calculations
|
||||
} else {
|
||||
// console2.log("Not truncating: aLimit64 >= a");
|
||||
}
|
||||
// no truncation needed
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,28 +242,19 @@ library LMSRStabilized {
|
||||
uint256 j,
|
||||
int128 limitPrice
|
||||
) internal pure returns (int128 amountIn, int128 amountOut) {
|
||||
require(i < nAssets && j < nAssets, "LMSR: idx");
|
||||
require(limitPrice > int128(0), "LMSR: limitPrice <= 0");
|
||||
|
||||
// Compute b and ensure positivity before deriving invB
|
||||
int128 sizeMetric = _computeSizeMetric(qInternal);
|
||||
require(sizeMetric > int128(0), "LMSR: size metric zero");
|
||||
int128 b = kappa.mul(sizeMetric);
|
||||
require(b > int128(0), "LMSR: b<=0");
|
||||
|
||||
// Precompute reciprocal of b to avoid repeated divisions
|
||||
int128 invB = ABDKMath64x64.div(ONE, b);
|
||||
|
||||
// Guard: output asset must have non-zero effective weight to avoid degenerate/div-by-zero-like conditions
|
||||
require(qInternal[j] > int128(0), "LMSR: e_j==0");
|
||||
|
||||
// Compute r0 = exp((q_i - q_j) / b) directly using invB
|
||||
int128 r0 = _exp(qInternal[i].sub(qInternal[j]).mul(invB));
|
||||
|
||||
// Mirror swapAmountsForExactInput behavior: treat invalid r0 as an error condition.
|
||||
// Revert if r0 is non-positive (no finite trade under a price limit).
|
||||
require(r0 > int128(0), "LMSR: r0<=0");
|
||||
|
||||
// If current price already exceeds or equals limit, revert the same way swapAmountsForExactInput does.
|
||||
if (r0 >= limitPrice) {
|
||||
revert("LMSR: limitPrice <= current price");
|
||||
|
||||
@@ -81,14 +81,14 @@ contract PartyPlanner is OwnableExternal, IPartyPlanner {
|
||||
protocolFeeAddress = protocolFeeAddress_;
|
||||
}
|
||||
|
||||
/// Main newPool variant: accepts kappa directly (preferred).
|
||||
/// Main newPool variant: accepts kappa directly (preferred) and a per-asset fee vector.
|
||||
function newPool(
|
||||
// Pool constructor args
|
||||
string memory name_,
|
||||
string memory symbol_,
|
||||
IERC20[] memory tokens_,
|
||||
int128 kappa_,
|
||||
uint256 swapFeePpm_,
|
||||
uint256[] memory swapFeesPpm_,
|
||||
uint256 flashFeePpm_,
|
||||
bool stable_,
|
||||
// Initial deposit information
|
||||
@@ -107,6 +107,9 @@ contract PartyPlanner is OwnableExternal, IPartyPlanner {
|
||||
// Validate kappa > 0 (Q64.64)
|
||||
require(kappa_ > int128(0), "Planner: kappa must be > 0");
|
||||
|
||||
// Validate fees vector length matches number of tokens
|
||||
require(swapFeesPpm_.length == tokens_.length, "Planner: fees and tokens length mismatch");
|
||||
|
||||
// Create a new PartyPool instance (kappa-based constructor)
|
||||
IPartyPoolDeployer deployer = stable_ && tokens_.length == 2 ? BALANCED_PAIR_DEPLOYER : NORMAL_POOL_DEPLOYER;
|
||||
pool = deployer.deploy(
|
||||
@@ -115,7 +118,7 @@ contract PartyPlanner is OwnableExternal, IPartyPlanner {
|
||||
symbol_,
|
||||
tokens_,
|
||||
kappa_,
|
||||
swapFeePpm_,
|
||||
swapFeesPpm_,
|
||||
flashFeePpm_,
|
||||
PROTOCOL_FEE_PPM,
|
||||
protocolFeeAddress,
|
||||
@@ -155,6 +158,48 @@ contract PartyPlanner is OwnableExternal, IPartyPlanner {
|
||||
lpAmount = pool.initialMint(receiver, initialLpAmount);
|
||||
}
|
||||
|
||||
/// Convenience overload: legacy single-fee signature — repeat the scalar for every asset and delegate.
|
||||
function newPool(
|
||||
// Pool constructor args (legacy single-fee)
|
||||
string memory name_,
|
||||
string memory symbol_,
|
||||
IERC20[] memory tokens_,
|
||||
int128 kappa_,
|
||||
uint256 swapFeePpm_,
|
||||
uint256 flashFeePpm_,
|
||||
bool stable_,
|
||||
// Initial deposit information
|
||||
address payer,
|
||||
address receiver,
|
||||
uint256[] memory initialDeposits,
|
||||
uint256 initialLpAmount,
|
||||
uint256 deadline
|
||||
) public onlyOwner returns (IPartyPool pool, uint256 lpAmount) {
|
||||
// Build per-asset fee vector by repeating the scalar swapFeePpm_
|
||||
uint256[] memory feesArr = new uint256[](tokens_.length);
|
||||
for (uint256 i = 0; i < tokens_.length; i++) {
|
||||
// We divide by two, because the new per-asset fee semantics charges both the in-asset fee and
|
||||
// out-asset fee. This should be a square-root for exactness.
|
||||
feesArr[i] = swapFeePpm_ / 2;
|
||||
}
|
||||
|
||||
// Delegate to the vector-based newPool variant
|
||||
return newPool(
|
||||
name_,
|
||||
symbol_,
|
||||
tokens_,
|
||||
kappa_,
|
||||
feesArr,
|
||||
flashFeePpm_,
|
||||
stable_,
|
||||
payer,
|
||||
receiver,
|
||||
initialDeposits,
|
||||
initialLpAmount,
|
||||
deadline
|
||||
);
|
||||
}
|
||||
|
||||
// NOTE that the slippage target is only exactly achieved in completely balanced pools where all assets are
|
||||
// priced the same. This target is actually a minimum slippage that the pool imposes on traders, and the actual
|
||||
// slippage cost can be multiples bigger in practice due to pool inventory imbalances.
|
||||
|
||||
@@ -54,9 +54,11 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
|
||||
int128 private 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.
|
||||
uint256 private immutable SWAP_FEE_PPM;
|
||||
function swapFeePpm() external view returns (uint256) { return SWAP_FEE_PPM; }
|
||||
/// @notice Per-asset swap fees in ppm.
|
||||
function fees() external view returns (uint256[] memory) { return _fees; }
|
||||
|
||||
/// @notice Effective combined fee in ppm for (i as input, j as output)
|
||||
function fee(uint256 i, uint256 j) external view returns (uint256) { return _pairFeePpm(i,j); }
|
||||
|
||||
/// @notice Flash-loan fee in parts-per-million (ppm) applied to flash borrow amounts.
|
||||
uint256 private immutable FLASH_FEE_PPM;
|
||||
@@ -100,7 +102,7 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
|
||||
/// @param symbol_ LP token symbol
|
||||
/// @param tokens_ token addresses (n)
|
||||
/// @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 fees_ per-asset swap fees in ppm (length must equal tokens_.length)
|
||||
/// @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
|
||||
@@ -110,7 +112,7 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
|
||||
string memory symbol_,
|
||||
IERC20[] memory tokens_,
|
||||
int128 kappa_,
|
||||
uint256 swapFeePpm_,
|
||||
uint256[] memory fees_,
|
||||
uint256 flashFeePpm_,
|
||||
uint256 protocolFeePpm_,
|
||||
address protocolFeeAddress_,
|
||||
@@ -126,11 +128,17 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
|
||||
require(tokens_.length > 1, "Pool: need >1 asset");
|
||||
_tokens = tokens_;
|
||||
KAPPA = kappa_;
|
||||
require(swapFeePpm_ < 1_000_000, "Pool: fee >= ppm");
|
||||
SWAP_FEE_PPM = swapFeePpm_;
|
||||
require(flashFeePpm_ < 1_000_000, "Pool: flash fee >= ppm");
|
||||
require(fees_.length == tokens_.length, "Pool: fees length");
|
||||
// validate ppm bounds and assign
|
||||
_fees = new uint256[](fees_.length);
|
||||
for (uint256 i = 0; i < fees_.length; i++) {
|
||||
// Cap all fees at 1%
|
||||
require(fees_[i] < 10_000, "Pool: fee >= 1%");
|
||||
_fees[i] = fees_[i];
|
||||
}
|
||||
require(flashFeePpm_ < 10_000, "Pool: flash fee >= 1%");
|
||||
FLASH_FEE_PPM = flashFeePpm_;
|
||||
require(protocolFeePpm_ < 1_000_000, "Pool: protocol fee >= ppm");
|
||||
require(protocolFeePpm_ < 400_000, "Pool: protocol fee >= 40%");
|
||||
// If the protocolFeePpm_ is set, then also require the fee address to be nonzero
|
||||
require(protocolFeePpm_ == 0 || protocolFeeAddress_ != address(0));
|
||||
PROTOCOL_FEE_PPM = protocolFeePpm_;
|
||||
@@ -168,8 +176,10 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
|
||||
/// @notice If a security problem is found, the vault owner may call this function to permanently disable swap and
|
||||
/// mint functionality, leaving only burns (withdrawals) working.
|
||||
function kill() external onlyOwner {
|
||||
_killed = true;
|
||||
emit Killed();
|
||||
if( !_killed ) {
|
||||
_killed = true;
|
||||
emit Killed();
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------------------
|
||||
@@ -232,7 +242,7 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
|
||||
uint256 outputTokenIndex,
|
||||
uint256 maxAmountIn,
|
||||
int128 limitPrice
|
||||
) external view returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
|
||||
) external view returns (uint256 amountIn, uint256 amountOut, uint256 inFee) {
|
||||
(uint256 grossIn, uint256 outUint,,,, uint256 feeUint) = _quoteSwapExactIn(inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice);
|
||||
return (grossIn, outUint, feeUint);
|
||||
}
|
||||
@@ -247,7 +257,7 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
|
||||
int128 limitPrice,
|
||||
uint256 deadline,
|
||||
bool unwrap
|
||||
) external payable native nonReentrant killable returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
|
||||
) external payable native nonReentrant killable returns (uint256 amountIn, uint256 amountOut, uint256 inFee) {
|
||||
require(deadline == 0 || block.timestamp <= deadline, "swap: deadline exceeded");
|
||||
|
||||
// Compute amounts using the same path as views
|
||||
@@ -303,10 +313,8 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
|
||||
uint256 outputTokenIndex,
|
||||
uint256 maxAmountIn,
|
||||
int128 limitPrice
|
||||
)
|
||||
internal
|
||||
view
|
||||
returns (
|
||||
) internal view
|
||||
returns (
|
||||
uint256 grossIn,
|
||||
uint256 amountOutUint,
|
||||
int128 amountInInternalUsed,
|
||||
@@ -316,7 +324,8 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
|
||||
)
|
||||
{
|
||||
// Estimate max net input (fee on gross rounded up, then subtract)
|
||||
(, uint256 netUintForSwap) = _computeFee(maxAmountIn, SWAP_FEE_PPM);
|
||||
uint256 pairFeePpm = _pairFeePpm(inputTokenIndex, outputTokenIndex);
|
||||
(, uint256 netUintForSwap) = _computeFee(maxAmountIn, pairFeePpm);
|
||||
|
||||
// Convert to internal (floor)
|
||||
int128 deltaInternalI = _uintToInternalFloor(netUintForSwap, _bases[inputTokenIndex]);
|
||||
@@ -332,8 +341,8 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
|
||||
// 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);
|
||||
if (pairFeePpm > 0) {
|
||||
feeUint = _ceilFee(amountInUintNoFee, pairFeePpm);
|
||||
grossIn += feeUint;
|
||||
}
|
||||
|
||||
@@ -354,7 +363,7 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
|
||||
int128 limitPrice,
|
||||
uint256 deadline,
|
||||
bool unwrap
|
||||
) external payable returns (uint256 amountInUsed, uint256 amountOut, uint256 fee) {
|
||||
) external payable returns (uint256 amountInUsed, uint256 amountOut, uint256 inFee) {
|
||||
bytes memory data = abi.encodeWithSelector(
|
||||
PartyPoolSwapImpl.swapToLimit.selector,
|
||||
payer,
|
||||
@@ -364,7 +373,7 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
|
||||
limitPrice,
|
||||
deadline,
|
||||
unwrap,
|
||||
SWAP_FEE_PPM,
|
||||
_pairFeePpm(inputTokenIndex, outputTokenIndex),
|
||||
PROTOCOL_FEE_PPM
|
||||
);
|
||||
bytes memory result = Address.functionDelegateCall(address(SWAP_IMPL), data);
|
||||
@@ -379,14 +388,14 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
|
||||
/// @param inputTokenIndex index of the input token
|
||||
/// @param maxAmountIn maximum uint token input (inclusive of fee)
|
||||
/// @param deadline optional deadline
|
||||
/// @return lpMinted actual LP minted (uint)
|
||||
/// @return amountInUsed actual input used (uint256), lpMinted actual LP minted (uint256), inFee fee taken from the input (uint256)
|
||||
function swapMint(
|
||||
address payer,
|
||||
address receiver,
|
||||
uint256 inputTokenIndex,
|
||||
uint256 maxAmountIn,
|
||||
uint256 deadline
|
||||
) external payable returns (uint256 lpMinted) {
|
||||
) external payable returns (uint256 amountInUsed, uint256 lpMinted, uint256 inFee) {
|
||||
bytes memory data = abi.encodeWithSelector(
|
||||
PartyPoolMintImpl.swapMint.selector,
|
||||
payer,
|
||||
@@ -394,12 +403,12 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
|
||||
inputTokenIndex,
|
||||
maxAmountIn,
|
||||
deadline,
|
||||
SWAP_FEE_PPM,
|
||||
_assetFeePpm(inputTokenIndex),
|
||||
PROTOCOL_FEE_PPM
|
||||
);
|
||||
|
||||
bytes memory result = Address.functionDelegateCall(address(MINT_IMPL), data);
|
||||
return abi.decode(result, (uint256));
|
||||
return abi.decode(result, (uint256, uint256, uint256));
|
||||
}
|
||||
|
||||
/// @notice Burn LP _tokens then swap the redeemed proportional basket into a single asset `inputTokenIndex` and send to receiver.
|
||||
@@ -426,7 +435,7 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
|
||||
outputTokenIndex,
|
||||
deadline,
|
||||
unwrap,
|
||||
SWAP_FEE_PPM,
|
||||
_assetFeePpm(outputTokenIndex),
|
||||
PROTOCOL_FEE_PPM
|
||||
);
|
||||
|
||||
@@ -435,13 +444,11 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
|
||||
}
|
||||
|
||||
|
||||
bytes32 internal constant FLASH_CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan");
|
||||
|
||||
/**
|
||||
* @dev Loan `amount` _tokens to `receiver`, and takes it back plus a `flashFee` after the callback.
|
||||
* @param receiver The contract receiving the _tokens, needs to implement the `onFlashLoan(address user, uint256 amount, uint256 fee, bytes calldata)` interface.
|
||||
* @dev Loan `amount` tokens to `receiver`, and takes it back plus a `flashFee` after the callback.
|
||||
* @param receiver The contract receiving the tokens, needs to implement the `onFlashLoan(address user, uint256 amount, uint256 fee, bytes calldata)` interface.
|
||||
* @param tokenAddr The loan currency.
|
||||
* @param amount The amount of _tokens lent.
|
||||
* @param amount The amount of tokens lent.
|
||||
* @param data A data parameter to be passed on to the `receiver` for any custom use.
|
||||
*/
|
||||
function flashLoan(
|
||||
@@ -449,37 +456,19 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
|
||||
address tokenAddr,
|
||||
uint256 amount,
|
||||
bytes calldata data
|
||||
) external nonReentrant killable returns (bool)
|
||||
) external returns (bool)
|
||||
{
|
||||
IERC20 token = IERC20(tokenAddr);
|
||||
require(amount <= token.balanceOf(address(this)));
|
||||
uint256 tokenIndex = _tokenAddressToIndexPlusOne[token];
|
||||
require(tokenIndex != 0, 'flash: token not in pool');
|
||||
tokenIndex -= 1;
|
||||
(uint256 fee, ) = _computeFee(amount, FLASH_FEE_PPM);
|
||||
|
||||
// Compute protocol share of flash fee
|
||||
uint256 protoShare = 0;
|
||||
if (PROTOCOL_FEE_PPM > 0 && fee > 0) {
|
||||
protoShare = (fee * PROTOCOL_FEE_PPM) / 1_000_000; // floor
|
||||
if (protoShare > 0) {
|
||||
_protocolFeesOwed[tokenIndex] += protoShare;
|
||||
}
|
||||
}
|
||||
|
||||
_sendTokenTo(token, address(receiver), amount, false);
|
||||
require(receiver.onFlashLoan(msg.sender, address(token), amount, fee, data) == FLASH_CALLBACK_SUCCESS);
|
||||
_receiveTokenFrom(address(receiver), token, amount + fee);
|
||||
|
||||
// Update cached balance for the borrowed token
|
||||
uint256 balAfter = token.balanceOf(address(this));
|
||||
// Inline _recordCachedBalance logic
|
||||
require(balAfter >= _protocolFeesOwed[tokenIndex], "balance < protocol owed");
|
||||
_cachedUintBalances[tokenIndex] = balAfter - _protocolFeesOwed[tokenIndex];
|
||||
|
||||
emit Flash(msg.sender, receiver, token, amount, fee-protoShare, protoShare);
|
||||
|
||||
return true;
|
||||
bytes memory payload = abi.encodeWithSelector(
|
||||
PartyPoolSwapImpl.flashLoan.selector,
|
||||
receiver,
|
||||
tokenAddr,
|
||||
amount,
|
||||
data,
|
||||
FLASH_FEE_PPM,
|
||||
PROTOCOL_FEE_PPM
|
||||
);
|
||||
bytes memory result = Address.functionDelegateCall(address(SWAP_IMPL), payload);
|
||||
return abi.decode(result, (bool));
|
||||
}
|
||||
|
||||
|
||||
@@ -490,7 +479,7 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
|
||||
PartyPoolSwapImpl.collectProtocolFees.selector,
|
||||
protocolFeeAddress
|
||||
);
|
||||
Address.functionDelegateCall(address(MINT_IMPL), data);
|
||||
Address.functionDelegateCall(address(SWAP_IMPL), data);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ contract PartyPoolBalancedPair is PartyPool {
|
||||
string memory symbol_,
|
||||
IERC20[] memory tokens_,
|
||||
int128 kappa_,
|
||||
uint256 swapFeePpm_,
|
||||
uint256[] memory fees_,
|
||||
uint256 flashFeePpm_,
|
||||
uint256 protocolFeePpm_, // NEW: protocol share of fees (ppm)
|
||||
address protocolFeeAddress_, // NEW: recipient for collected protocol tokens
|
||||
@@ -24,7 +24,7 @@ contract PartyPoolBalancedPair is PartyPool {
|
||||
PartyPoolSwapImpl swapMintImpl_,
|
||||
PartyPoolMintImpl mintImpl_
|
||||
)
|
||||
PartyPool(owner_, name_, symbol_, tokens_, kappa_, swapFeePpm_, flashFeePpm_, protocolFeePpm_, protocolFeeAddress_, wrapperToken_, swapMintImpl_, mintImpl_)
|
||||
PartyPool(owner_, name_, symbol_, tokens_, kappa_, fees_, flashFeePpm_, protocolFeePpm_, protocolFeeAddress_, wrapperToken_, swapMintImpl_, mintImpl_)
|
||||
{}
|
||||
|
||||
function _swapAmountsForExactInput(uint256 i, uint256 j, int128 a, int128 limitPrice) internal virtual override view
|
||||
|
||||
@@ -24,6 +24,9 @@ abstract contract PartyPoolBase is OwnableInternal, ERC20Internal, ReentrancyGua
|
||||
WRAPPER_TOKEN = wrapper_;
|
||||
}
|
||||
|
||||
/// @notice Per-asset swap fees in ppm. Fees are applied on input for swaps; see helpers for composition rules.
|
||||
uint256[] internal _fees;
|
||||
|
||||
//
|
||||
// Internal state
|
||||
//
|
||||
@@ -78,6 +81,23 @@ abstract contract PartyPoolBase is OwnableInternal, ERC20Internal, ReentrancyGua
|
||||
Conversion & fee helpers (internal)
|
||||
---------------------- */
|
||||
|
||||
// Per-asset fee getters and composition
|
||||
function _assetFeePpm(uint256 i) internal view returns (uint256) {
|
||||
if (_fees.length == 0) return 0;
|
||||
return _fees[i];
|
||||
}
|
||||
|
||||
// Effective pair fee: 1 - (1-fi)(1-fj) in ppm, rounding in favor of the pool, and guarding
|
||||
// overflows by using 1e6 ppm base.
|
||||
// We implement this as: ceil( fi + fj - (fi*fj)/1e6 ) for the real-valued expression.
|
||||
// For integer arithmetic with fi,fj in ppm this is equal to: fi + fj - floor( (fi*fj)/1e6 ).
|
||||
// So we compute prod = fi * fj, prodDiv = prod / 1e6 (floor), and return fi + fj - prodDiv.
|
||||
function _pairFeePpm(uint256 i, uint256 j) internal view returns (uint256) {
|
||||
uint256 fi = _fees[i];
|
||||
uint256 fj = _fees[j];
|
||||
return fi + fj - fi * fj / 1_000_000;
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
@@ -16,7 +16,7 @@ interface IPartyPoolDeployer {
|
||||
string memory symbol_,
|
||||
IERC20[] memory tokens_,
|
||||
int128 kappa_,
|
||||
uint256 swapFeePpm_,
|
||||
uint256[] memory fees_,
|
||||
uint256 flashFeePpm_,
|
||||
uint256 protocolFeePpm_,
|
||||
address protocolFeeAddress_,
|
||||
@@ -33,7 +33,7 @@ contract PartyPoolDeployer is IPartyPoolDeployer {
|
||||
string memory symbol_,
|
||||
IERC20[] memory tokens_,
|
||||
int128 kappa_,
|
||||
uint256 swapFeePpm_,
|
||||
uint256[] memory fees_,
|
||||
uint256 flashFeePpm_,
|
||||
uint256 protocolFeePpm_,
|
||||
address protocolFeeAddress_,
|
||||
@@ -47,7 +47,7 @@ contract PartyPoolDeployer is IPartyPoolDeployer {
|
||||
symbol_,
|
||||
tokens_,
|
||||
kappa_,
|
||||
swapFeePpm_,
|
||||
fees_,
|
||||
flashFeePpm_,
|
||||
protocolFeePpm_,
|
||||
protocolFeeAddress_,
|
||||
@@ -65,7 +65,7 @@ contract PartyPoolBalancedPairDeployer is IPartyPoolDeployer {
|
||||
string memory symbol_,
|
||||
IERC20[] memory tokens_,
|
||||
int128 kappa_,
|
||||
uint256 swapFeePpm_,
|
||||
uint256[] memory fees_,
|
||||
uint256 flashFeePpm_,
|
||||
uint256 protocolFeePpm_,
|
||||
address protocolFeeAddress_,
|
||||
@@ -79,7 +79,7 @@ contract PartyPoolBalancedPairDeployer is IPartyPoolDeployer {
|
||||
symbol_,
|
||||
tokens_,
|
||||
kappa_,
|
||||
swapFeePpm_,
|
||||
fees_,
|
||||
flashFeePpm_,
|
||||
protocolFeePpm_,
|
||||
protocolFeeAddress_,
|
||||
|
||||
@@ -268,8 +268,8 @@ contract PartyPoolMintImpl is PartyPoolBase {
|
||||
/// @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
|
||||
/// @return lpMinted LP tokens that would be minted
|
||||
/// @return inFee fee amount charged
|
||||
function swapMintAmounts(
|
||||
uint256 inputTokenIndex,
|
||||
uint256 maxAmountIn,
|
||||
@@ -277,7 +277,7 @@ contract PartyPoolMintImpl is PartyPoolBase {
|
||||
LMSRStabilized.State memory lmsrState,
|
||||
uint256[] memory bases_,
|
||||
uint256 totalSupply_
|
||||
) public pure returns (uint256 amountInUsed, uint256 fee, uint256 lpMinted) {
|
||||
) public pure returns (uint256 amountInUsed, uint256 lpMinted, uint256 inFee) {
|
||||
require(inputTokenIndex < bases_.length, "swapMintAmounts: idx");
|
||||
require(maxAmountIn > 0, "swapMintAmounts: input zero");
|
||||
require(lmsrState.nAssets > 0, "swapMintAmounts: uninit pool");
|
||||
@@ -304,11 +304,11 @@ contract PartyPoolMintImpl is PartyPoolBase {
|
||||
require(amountInUsed > 0, "swapMintAmounts: input zero after internal conversion");
|
||||
|
||||
// Compute fee on the actual used input (ceiling)
|
||||
fee = 0;
|
||||
inFee = 0;
|
||||
if (swapFeePpm > 0) {
|
||||
fee = (amountInUsed * swapFeePpm + 999999) / 1000000; // ceil fee
|
||||
inFee = (amountInUsed * swapFeePpm + 999999) / 1000000; // ceil fee
|
||||
}
|
||||
uint256 totalTransfer = amountInUsed + fee;
|
||||
uint256 totalTransfer = amountInUsed + inFee;
|
||||
require(totalTransfer > 0 && totalTransfer <= maxAmountIn, "swapMintAmounts: transfer exceeds max");
|
||||
|
||||
// Compute old and new scaled size metrics to determine LP minted
|
||||
@@ -345,7 +345,7 @@ contract PartyPoolMintImpl is PartyPoolBase {
|
||||
/// @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)
|
||||
/// @return amountInUsed actual input used (uint256), lpMinted actual LP minted (uint256), inFee fee taken from the input (uint256)
|
||||
function swapMint(
|
||||
address payer,
|
||||
address receiver,
|
||||
@@ -354,7 +354,7 @@ contract PartyPoolMintImpl is PartyPoolBase {
|
||||
uint256 deadline,
|
||||
uint256 swapFeePpm,
|
||||
uint256 protocolFeePpm
|
||||
) external payable native killable nonReentrant returns (uint256 lpMinted) {
|
||||
) external payable native killable nonReentrant returns (uint256 amountInUsed, uint256 lpMinted, uint256 inFee) {
|
||||
uint256 n = _tokens.length;
|
||||
require(inputTokenIndex < n, "swapMint: idx");
|
||||
require(maxAmountIn > 0, "swapMint: input zero");
|
||||
@@ -435,7 +435,10 @@ contract PartyPoolMintImpl is PartyPoolBase {
|
||||
emit IPartyPool.SwapMint(payer, receiver, _tokens[inputTokenIndex],
|
||||
totalTransfer, actualLpToMint, feeUintActual-protoShare, protoShare);
|
||||
|
||||
return actualLpToMint;
|
||||
amountInUsed = amountInUint;
|
||||
lpMinted = actualLpToMint;
|
||||
inFee = feeUintActual;
|
||||
return (amountInUsed, lpMinted, inFee);
|
||||
}
|
||||
|
||||
/// @notice Calculate the amounts for a burn swap operation
|
||||
@@ -454,7 +457,7 @@ contract PartyPoolMintImpl is PartyPoolBase {
|
||||
LMSRStabilized.State memory lmsrState,
|
||||
uint256[] memory bases_,
|
||||
uint256 totalSupply_
|
||||
) public pure returns (uint256 amountOut) {
|
||||
) public pure returns (uint256 amountOut, uint256 outFee) {
|
||||
require(outputTokenIndex < bases_.length, "burnSwapAmounts: idx");
|
||||
require(lpAmount > 0, "burnSwapAmounts: zero lp");
|
||||
require(totalSupply_ > 0, "burnSwapAmounts: empty supply");
|
||||
@@ -470,6 +473,13 @@ contract PartyPoolMintImpl is PartyPoolBase {
|
||||
// Convert payoutInternal -> uint (floor) to favor pool
|
||||
amountOut = _internalToUintFloorPure(payoutInternal, bases_[outputTokenIndex]);
|
||||
require(amountOut > 0, "burnSwapAmounts: output zero");
|
||||
|
||||
// Compute gross payout (no swap fee) to derive token-side fee = gross - net
|
||||
int128 alphaGross = ABDKMath64x64.divu(lpAmount, totalSupply_); // gross fraction (no swap fee)
|
||||
(int128 payoutGrossInternal, ) = LMSRStabilized.swapAmountsForBurn(lmsrState.nAssets, lmsrState.kappa, lmsrState.qInternal,
|
||||
outputTokenIndex, alphaGross);
|
||||
uint256 payoutGrossUint = _internalToUintFloorPure(payoutGrossInternal, bases_[outputTokenIndex]);
|
||||
outFee = (payoutGrossUint > amountOut) ? (payoutGrossUint - amountOut) : 0;
|
||||
}
|
||||
|
||||
/// @notice Burn LP _tokens then swap the redeemed proportional basket into a single asset `outputTokenIndex` and send to receiver.
|
||||
|
||||
@@ -8,6 +8,7 @@ import {IPartyPool} from "./IPartyPool.sol";
|
||||
import {NativeWrapper} from "./NativeWrapper.sol";
|
||||
import {LMSRStabilized} from "./LMSRStabilized.sol";
|
||||
import {PartyPoolBase} from "./PartyPoolBase.sol";
|
||||
import {IERC3156FlashBorrower} from "../lib/openzeppelin-contracts/contracts/interfaces/IERC3156FlashBorrower.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
|
||||
@@ -19,6 +20,48 @@ contract PartyPoolSwapImpl is PartyPoolBase {
|
||||
|
||||
constructor(NativeWrapper wrapper_) PartyPoolBase(wrapper_) {}
|
||||
|
||||
bytes32 internal constant FLASH_CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan");
|
||||
|
||||
function flashLoan(
|
||||
IERC3156FlashBorrower receiver,
|
||||
address tokenAddr,
|
||||
uint256 amount,
|
||||
bytes calldata data,
|
||||
uint256 flashFeePpm,
|
||||
uint256 protocolFeePpm
|
||||
) external nonReentrant killable returns (bool) {
|
||||
IERC20 token = IERC20(tokenAddr);
|
||||
require(amount <= token.balanceOf(address(this)), "flash: amount > balance");
|
||||
uint256 tokenIndex = _tokenAddressToIndexPlusOne[token];
|
||||
require(tokenIndex != 0, 'flash: token not in pool');
|
||||
tokenIndex -= 1;
|
||||
(uint256 flashFee, ) = _computeFee(amount, flashFeePpm);
|
||||
|
||||
// Compute protocol share of flash fee
|
||||
uint256 protoShare = 0;
|
||||
if (protocolFeePpm > 0 && flashFee > 0) {
|
||||
protoShare = (flashFee * protocolFeePpm) / 1_000_000; // floor
|
||||
if (protoShare > 0) {
|
||||
_protocolFeesOwed[tokenIndex] += protoShare;
|
||||
}
|
||||
}
|
||||
|
||||
_sendTokenTo(token, address(receiver), amount, false);
|
||||
require(
|
||||
receiver.onFlashLoan(msg.sender, address(token), amount, flashFee, data) == FLASH_CALLBACK_SUCCESS,
|
||||
'flash: callback'
|
||||
);
|
||||
_receiveTokenFrom(address(receiver), token, amount + flashFee);
|
||||
|
||||
// Update cached balance for the borrowed token
|
||||
uint256 balAfter = token.balanceOf(address(this));
|
||||
require(balAfter >= _protocolFeesOwed[tokenIndex], "balance < protocol owed");
|
||||
_cachedUintBalances[tokenIndex] = balAfter - _protocolFeesOwed[tokenIndex];
|
||||
|
||||
emit IPartyPool.Flash(msg.sender, receiver, token, amount, flashFee - protoShare, protoShare);
|
||||
return true;
|
||||
}
|
||||
|
||||
function swapToLimitAmounts(
|
||||
uint256 inputTokenIndex,
|
||||
uint256 outputTokenIndex,
|
||||
@@ -27,7 +70,7 @@ contract PartyPoolSwapImpl is PartyPoolBase {
|
||||
int128 kappa,
|
||||
int128[] memory qInternal,
|
||||
uint256 swapFeePpm
|
||||
) external pure returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
|
||||
) external pure returns (uint256 amountIn, uint256 amountOut, uint256 inFee) {
|
||||
// Compute internal maxima at the price limit
|
||||
(int128 amountInInternal, int128 amountOutInternal) = LMSRStabilized.swapAmountsForPriceLimit(
|
||||
bases.length, kappa, qInternal,
|
||||
@@ -37,11 +80,11 @@ contract PartyPoolSwapImpl is PartyPoolBase {
|
||||
uint256 amountInUintNoFee = _internalToUintCeil(amountInInternal, bases[inputTokenIndex]);
|
||||
require(amountInUintNoFee > 0, "swapToLimit: input zero");
|
||||
|
||||
fee = 0;
|
||||
inFee = 0;
|
||||
amountIn = amountInUintNoFee;
|
||||
if (swapFeePpm > 0) {
|
||||
fee = _ceilFee(amountInUintNoFee, swapFeePpm);
|
||||
amountIn += fee;
|
||||
inFee = _ceilFee(amountInUintNoFee, swapFeePpm);
|
||||
amountIn += inFee;
|
||||
}
|
||||
|
||||
amountOut = _internalToUintFloor(amountOutInternal, bases[outputTokenIndex]);
|
||||
@@ -59,7 +102,7 @@ contract PartyPoolSwapImpl is PartyPoolBase {
|
||||
bool unwrap,
|
||||
uint256 swapFeePpm,
|
||||
uint256 protocolFeePpm
|
||||
) external payable native killable nonReentrant returns (uint256 amountInUsed, uint256 amountOut, uint256 fee) {
|
||||
) external payable native killable nonReentrant returns (uint256 amountInUsed, uint256 amountOut, uint256 inFee) {
|
||||
uint256 n = _tokens.length;
|
||||
require(inputTokenIndex < n && outputTokenIndex < n, "swapToLimit: idx");
|
||||
require(limitPrice > int128(0), "swapToLimit: limit <= 0");
|
||||
|
||||
@@ -91,13 +91,13 @@ contract PartyPoolViewer is PartyPoolHelpers, IPartyPoolViewer {
|
||||
/// @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
|
||||
/// @return amountIn gross input amount to transfer (includes fee), amountOut output amount user would receive, inFee fee amount taken
|
||||
function swapToLimitAmounts(
|
||||
IPartyPool pool,
|
||||
uint256 inputTokenIndex,
|
||||
uint256 outputTokenIndex,
|
||||
int128 limitPrice
|
||||
) external view returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
|
||||
) external view returns (uint256 amountIn, uint256 amountOut, uint256 inFee) {
|
||||
LMSRStabilized.State memory lmsr = pool.LMSR();
|
||||
require(inputTokenIndex < lmsr.nAssets && outputTokenIndex < lmsr.nAssets, "swapToLimit: idx");
|
||||
require(limitPrice > int128(0), "swapToLimit: limit <= 0");
|
||||
@@ -105,17 +105,17 @@ contract PartyPoolViewer is PartyPoolHelpers, IPartyPoolViewer {
|
||||
|
||||
return SWAP_IMPL.swapToLimitAmounts(
|
||||
inputTokenIndex, outputTokenIndex, limitPrice,
|
||||
pool.denominators(), pool.kappa(), lmsr.qInternal, pool.swapFeePpm());
|
||||
pool.denominators(), pool.kappa(), lmsr.qInternal, pool.fee(inputTokenIndex, outputTokenIndex));
|
||||
}
|
||||
|
||||
|
||||
function swapMintAmounts(IPartyPool pool, uint256 inputTokenIndex, uint256 maxAmountIn) external view
|
||||
returns (uint256 amountInUsed, uint256 fee, uint256 lpMinted) {
|
||||
returns (uint256 amountInUsed, uint256 lpMinted, uint256 inFee) {
|
||||
LMSRStabilized.State memory lmsr = pool.LMSR();
|
||||
return MINT_IMPL.swapMintAmounts(
|
||||
inputTokenIndex,
|
||||
maxAmountIn,
|
||||
pool.swapFeePpm(),
|
||||
pool.fees()[inputTokenIndex],
|
||||
lmsr,
|
||||
pool.denominators(),
|
||||
pool.totalSupply()
|
||||
@@ -124,12 +124,12 @@ contract PartyPoolViewer is PartyPoolHelpers, IPartyPoolViewer {
|
||||
|
||||
|
||||
function burnSwapAmounts(IPartyPool pool, uint256 lpAmount, uint256 outputTokenIndex) external view
|
||||
returns (uint256 amountOut) {
|
||||
returns (uint256 amountOut, uint256 outFee) {
|
||||
LMSRStabilized.State memory lmsr = pool.LMSR();
|
||||
return MINT_IMPL.burnSwapAmounts(
|
||||
lpAmount,
|
||||
outputTokenIndex,
|
||||
pool.swapFeePpm(),
|
||||
pool.fees()[outputTokenIndex],
|
||||
lmsr,
|
||||
pool.denominators(),
|
||||
pool.totalSupply()
|
||||
|
||||
@@ -64,6 +64,9 @@ library Deploy {
|
||||
NativeWrapper wrapper,
|
||||
bool _stable
|
||||
) internal returns (PartyPool) {
|
||||
// Build per-asset fee vector from scalar for tests
|
||||
uint256[] memory feesArr = new uint256[](tokens_.length);
|
||||
for (uint256 i = 0; i < tokens_.length; i++) { feesArr[i] = _swapFeePpm; }
|
||||
return _stable && tokens_.length == 2 ?
|
||||
new PartyPoolBalancedPair(
|
||||
owner_,
|
||||
@@ -71,7 +74,7 @@ library Deploy {
|
||||
symbol_,
|
||||
tokens_,
|
||||
_kappa,
|
||||
_swapFeePpm,
|
||||
feesArr,
|
||||
_flashFeePpm,
|
||||
PROTOCOL_FEE_PPM,
|
||||
PROTOCOL_FEE_RECEIVER,
|
||||
@@ -85,7 +88,7 @@ library Deploy {
|
||||
symbol_,
|
||||
tokens_,
|
||||
_kappa,
|
||||
_swapFeePpm,
|
||||
feesArr,
|
||||
_flashFeePpm,
|
||||
PROTOCOL_FEE_PPM,
|
||||
PROTOCOL_FEE_RECEIVER,
|
||||
|
||||
@@ -304,7 +304,7 @@ contract GasTest is Test {
|
||||
|
||||
for (uint256 k = 0; k < iterations; k++) {
|
||||
// Mint LP by providing single-token input; receive LP minted
|
||||
uint256 minted = testPool.swapMint(alice, alice, 0, input, 0);
|
||||
(, uint256 minted, ) = testPool.swapMint(alice, alice, 0, input, 0);
|
||||
// If nothing minted (numerical edge), skip burn step
|
||||
if (minted == 0) continue;
|
||||
// Immediately burn the minted LP back to _tokens, targeting the same token index
|
||||
|
||||
@@ -426,7 +426,7 @@ contract NativeTest is Test {
|
||||
uint256 aliceLpBefore = pool.balanceOf(alice);
|
||||
|
||||
// Call swapMint with native currency: deposit ETH as WETH (index 2)
|
||||
uint256 lpMinted = pool.swapMint{value: maxIn}(
|
||||
(, uint256 lpMinted, ) = pool.swapMint{value: maxIn}(
|
||||
alice, // payer
|
||||
alice, // receiver
|
||||
2, // inputTokenIndex (WETH)
|
||||
@@ -457,7 +457,7 @@ contract NativeTest is Test {
|
||||
uint256 aliceEthBefore = alice.balance;
|
||||
|
||||
// Send excess native currency
|
||||
uint256 lpMinted = pool.swapMint{value: totalSent}(
|
||||
(, uint256 lpMinted, ) = pool.swapMint{value: totalSent}(
|
||||
alice,
|
||||
alice,
|
||||
2, // WETH
|
||||
|
||||
@@ -721,7 +721,7 @@ contract PartyPoolTest is Test {
|
||||
|
||||
uint256 input = 10_000;
|
||||
// Call swapMint as alice, receive LP to alice
|
||||
uint256 minted = pool.swapMint(alice, alice, 0, input, 0);
|
||||
(, uint256 minted, ) = pool.swapMint(alice, alice, 0, input, 0);
|
||||
|
||||
// minted should be > 0
|
||||
assertTrue(minted > 0, "swapMint should mint LP");
|
||||
@@ -751,7 +751,7 @@ contract PartyPoolTest is Test {
|
||||
|
||||
uint256 aliceBalBefore = token0.balanceOf(alice);
|
||||
|
||||
uint256 minted = pool.swapMint(alice, alice, 0, largeInput, 0);
|
||||
(, uint256 minted, ) = pool.swapMint(alice, alice, 0, largeInput, 0);
|
||||
|
||||
// minted should be > 0
|
||||
assertTrue(minted > 0, "swapMint large input should still mint LP");
|
||||
|
||||
Reference in New Issue
Block a user