dxod repo init
This commit is contained in:
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# Compiler files
|
||||
cache/
|
||||
out/
|
||||
|
||||
docs/
|
||||
log/
|
||||
.env
|
||||
.idea
|
||||
|
||||
# Ignores development broadcast logs
|
||||
!/broadcast
|
||||
/broadcast/*/31337/
|
||||
/broadcast/**/dry-run/
|
||||
9
.gitmodules
vendored
Normal file
9
.gitmodules
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
[submodule "lib/forge-std"]
|
||||
path = lib/forge-std
|
||||
url = https://github.com/foundry-rs/forge-std
|
||||
[submodule "lib/abdk-libraries-solidity"]
|
||||
path = lib/abdk-libraries-solidity
|
||||
url = https://github.com/abdk-consulting/abdk-libraries-solidity
|
||||
[submodule "lib/openzeppelin-contracts"]
|
||||
path = lib/openzeppelin-contracts
|
||||
url = https://github.com/OpenZeppelin/openzeppelin-contracts
|
||||
49
bin/mock
Executable file
49
bin/mock
Executable file
@@ -0,0 +1,49 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Function to cleanup processes
|
||||
cleanup() {
|
||||
kill $ANVIL_PID 2>/dev/null
|
||||
}
|
||||
|
||||
# Set up trap to handle script exit
|
||||
trap cleanup EXIT
|
||||
|
||||
# Create log directory if it doesn't exist
|
||||
mkdir -p log
|
||||
|
||||
# Run anvil in background and redirect output to log file
|
||||
anvil | tee log/anvil.txt &
|
||||
ANVIL_PID=$!
|
||||
|
||||
# Function to check if string exists in file
|
||||
check_string() {
|
||||
grep -q "$1" "$2"
|
||||
return $?
|
||||
}
|
||||
|
||||
# Wait for anvil to start (max 30 seconds)
|
||||
echo "Waiting for anvil to start..."
|
||||
counter=0
|
||||
while ! check_string "Listening on" "log/anvil.txt"; do
|
||||
sleep 1
|
||||
counter=$((counter + 1))
|
||||
if [ $counter -ge 5 ]; then
|
||||
echo "Timeout waiting for anvil to start"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Extract bytecode using jq
|
||||
BYTECODE=$(jq -r '.bytecode.object' out/PartyPool.sol/PartyPool.json)
|
||||
if [ $? -ne 0 ] || [ -z "$BYTECODE" ]; then
|
||||
echo "Failed to extract bytecode from PartyPool.json"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export BYTECODE
|
||||
forge script DeployMock --broadcast
|
||||
|
||||
echo "Press Ctrl+C to exit..."
|
||||
while true; do
|
||||
sleep 1
|
||||
done
|
||||
47
doc/stablecoins.csv
Normal file
47
doc/stablecoins.csv
Normal file
@@ -0,0 +1,47 @@
|
||||
Token Name,Token Symbol,Price,Volume (24h)
|
||||
Tether,USDT,1.00,124178611175
|
||||
USDC,USDC,0.9996,15873101191
|
||||
First Digital USD,FDUSD,0.9981,6001738498
|
||||
World Liberty Financial USD,USD1,0.9994,443670921
|
||||
Ethena USDe,USDe,1.00,203389879
|
||||
Dai,DAI,0.9998,107786336
|
||||
Ripple USD,RLUSD,0.9997,95900714
|
||||
PayPal USD,PYUSD,0.9994,84926338
|
||||
Falcon USD,USDf,1.00,75128301
|
||||
EURC,EURC,1.17,50655783
|
||||
TrueUSD,TUSD,0.9976,41243610
|
||||
StabiR USD,USDR,0.9980,37757919
|
||||
EUR CoinVertible,EURCV,1.17,36001785
|
||||
StraitsX USD,XUSD,1.00,30768737
|
||||
Global Dollar,USDG,1.00,26395021
|
||||
AUSD,AUSD,0.9999,23425385
|
||||
Quantoz EURQ,EURQ,1.17,15986967
|
||||
Quantoz USDQ,USDQ,0.9993,11283404
|
||||
Elixir deUSD,DEUSD,0.9992,11801718
|
||||
Gemini Dollar,GUSD,0.9997,10831936
|
||||
Eurite,EURi,1.17,8463732
|
||||
USDD,USDD,0.9998,6457661
|
||||
BUSD,BUSD,0.9998,4320760
|
||||
Ondo US Dollar Yield,USDY,1.09,3876681
|
||||
JUSDJ,JUSDJ,1.32,3243537
|
||||
Pax Dollar,USDP,0.9998,3010075
|
||||
StabiR Euro,EURR,1.16,2581109
|
||||
Steem Dollars,SBD,0.8492,2517238
|
||||
Bucket Protocol BUCK Stablecoin,BUCK,0.9990,2375173
|
||||
Hyper USD,USDHL,0.9995,2227470
|
||||
USDP Stablecoin,USDP,0.9998,2126519
|
||||
AllUnity EUR,EURAU,1.17,1919081
|
||||
Celo Dollar,CUSD,1.00,1809631
|
||||
Worldwide USD,WUSD,1.00,1306544
|
||||
Usual USD,USD0,0.9981,3897930
|
||||
GHO,GHO,0.9997,675336
|
||||
Legacy Frax Dollar,FRAX,0.9979,513159
|
||||
STASIS EURO,EURS,1.16,23449869
|
||||
Noble Dollar,USDN,0.9996,
|
||||
Frax USD,FRXUSD,0.9986,
|
||||
lisUSD,lisUSD,0.9990,34118
|
||||
USDB,USDB,0.9987,952802
|
||||
Venus BUSD,vBUSD,0.02229,
|
||||
MNEE,MNEE,0.9974,86346
|
||||
Anchored Coins,AEUR,1.10,26168
|
||||
Lift Dollar,USDL,0.9987,76398
|
||||
|
17
foundry.toml
Normal file
17
foundry.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
[profile.default]
|
||||
src = "src"
|
||||
out = "out"
|
||||
libs = ["lib"]
|
||||
remappings = [
|
||||
'@openzeppelin/=lib/openzeppelin-contracts/',
|
||||
'@abdk/=lib/abdk-libraries-solidity/',
|
||||
]
|
||||
optimizer=true
|
||||
optimizer_runs=999999999
|
||||
viaIR=true
|
||||
gas_reports = ['PartyPool']
|
||||
|
||||
[lint]
|
||||
exclude_lints=['mixed-case-variable', 'unaliased-plain-import', ]
|
||||
|
||||
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
|
||||
1
lib/abdk-libraries-solidity
Submodule
1
lib/abdk-libraries-solidity
Submodule
Submodule lib/abdk-libraries-solidity added at 5e1e7c11b3
1
lib/forge-std
Submodule
1
lib/forge-std
Submodule
Submodule lib/forge-std added at 8bbcf6e3f8
1
lib/openzeppelin-contracts
Submodule
1
lib/openzeppelin-contracts
Submodule
Submodule lib/openzeppelin-contracts added at c64a1edb67
354
research/LMSRComparisonAnalysis.py
Normal file
354
research/LMSRComparisonAnalysis.py
Normal file
@@ -0,0 +1,354 @@
|
||||
# lmsr_vs_cp_sim.py
|
||||
# Requires: Python 3.9+, numpy, matplotlib
|
||||
# Optional: seaborn (for prettier plots)
|
||||
|
||||
import math
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
try:
|
||||
import seaborn as sns
|
||||
sns.set_context("talk")
|
||||
sns.set_style("whitegrid")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# Core AMM primitives
|
||||
# ---------------------------
|
||||
|
||||
def cp_price(X, Y):
|
||||
# Instantaneous marginal price p = dy/dx for constant product
|
||||
return Y / X
|
||||
|
||||
def cp_trade_y_out(X, Y, qx_in, fee=0.0):
|
||||
"""
|
||||
Swap x-in (amount qx_in) for y-out on a constant-product pool with fee.
|
||||
Fee taken on input (Uniswap-style): only (1-fee)*qx_in enters the invariant.
|
||||
Returns (dy_out, X_new, Y_new, fee_value).
|
||||
"""
|
||||
assert X > 0 and Y > 0 and qx_in >= 0 and 0 <= fee < 1
|
||||
k = X * Y
|
||||
effective_in = qx_in * (1 - fee)
|
||||
X_new = X + effective_in
|
||||
Y_new = k / X_new
|
||||
dy_out = Y - Y_new
|
||||
fee_value = qx_in * fee # in x-units
|
||||
return dy_out, X_new, Y_new, fee_value
|
||||
|
||||
def cp_avg_y_per_x(X, Y, qx_in, fee=0.0):
|
||||
if qx_in == 0:
|
||||
return cp_price(X, Y)
|
||||
dy, *_ = cp_trade_y_out(X, Y, qx_in, fee)
|
||||
return dy / qx_in
|
||||
|
||||
def lmsr_calibrate_b(X, Y, mode="thin_side", factor=0.5):
|
||||
"""
|
||||
Calibrate LMSR 'b' to match CP local log-price stiffness at the current state.
|
||||
- thin_side: use min(X, Y) * factor where factor≈0.5 matches ds/dq at that side
|
||||
- x_side: use X * factor
|
||||
- y_side: use Y * factor (useful if measuring q in y-space)
|
||||
For our use (q measured in x), 'x_side' or 'thin_side' with factor=0.5 are typical.
|
||||
"""
|
||||
if mode == "thin_side":
|
||||
return max(1e-12, factor * min(X, Y))
|
||||
elif mode == "x_side":
|
||||
return max(1e-12, factor * X)
|
||||
elif mode == "y_side":
|
||||
return max(1e-12, factor * Y)
|
||||
else:
|
||||
raise ValueError("mode must be one of ['thin_side','x_side','y_side']")
|
||||
|
||||
def lmsr_y_out(qx_in, p0, b, fee=0.0):
|
||||
"""
|
||||
Symmetric 2-asset LMSR approximation in x-measure:
|
||||
- We model log-price s moving linearly in q_x: ds/dq_x = 1/b
|
||||
- Then price path p(q) = p0 * exp(q/b)
|
||||
- Cumulative y received for x-in q is integral of p(q) dq: y(q) = p0 * b * (exp(q/b) - 1)
|
||||
Fee applied to input reduces effective q: q_eff = q * (1 - fee)
|
||||
Returns dy_out (in y-units) and fee_value (in x-units)
|
||||
"""
|
||||
assert qx_in >= 0 and b > 0 and p0 > 0 and 0 <= fee < 1
|
||||
q_eff = qx_in * (1 - fee)
|
||||
dy = p0 * b * (math.exp(q_eff / b) - 1.0)
|
||||
fee_value = qx_in * fee
|
||||
return dy, fee_value
|
||||
|
||||
def lmsr_avg_y_per_x(qx_in, p0, b, fee=0.0):
|
||||
if qx_in == 0:
|
||||
return p0
|
||||
dy, _ = lmsr_y_out(qx_in, p0, b, fee)
|
||||
return dy / qx_in
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# Static comparison utilities
|
||||
# ---------------------------
|
||||
|
||||
def static_compare(X, Y, fee_cp=0.0005, fee_lmsr=0.0005, b=None, b_mode="x_side", b_factor=0.5,
|
||||
q_grid=None):
|
||||
"""
|
||||
Compute average execution (y per x), penalties vs spot, and welfare gaps over a grid of trade sizes.
|
||||
Returns dict with arrays.
|
||||
"""
|
||||
p0 = cp_price(X, Y)
|
||||
if b is None:
|
||||
b = lmsr_calibrate_b(X, Y, mode=b_mode, factor=b_factor)
|
||||
|
||||
if q_grid is None:
|
||||
# span from tiny to a meaningful fraction of X
|
||||
q_grid = np.geomspace(1e-9 * X, 0.3 * X, 60)
|
||||
|
||||
avg_cp = np.array([cp_avg_y_per_x(X, Y, q, fee_cp) for q in q_grid])
|
||||
avg_lm = np.array([lmsr_avg_y_per_x(q, p0, b, fee_lmsr) for q in q_grid])
|
||||
|
||||
# Slippage penalty vs spot p0 (in y-per-x)
|
||||
pen_cp = p0 - avg_cp
|
||||
pen_lm = p0 - avg_lm
|
||||
|
||||
# Taker welfare difference: LMSR minus CP (positive means LMSR better for taker)
|
||||
welfare_gap = avg_lm - avg_cp
|
||||
|
||||
return {
|
||||
"q_grid": q_grid,
|
||||
"avg_cp": avg_cp,
|
||||
"avg_lm": avg_lm,
|
||||
"pen_cp": pen_cp,
|
||||
"pen_lm": pen_lm,
|
||||
"welfare_gap": welfare_gap,
|
||||
"p0": p0,
|
||||
"b": b
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# Imbalance sweep
|
||||
# ---------------------------
|
||||
|
||||
def sweep_imbalance(X, rho_grid, fee_cp=0.0005, fee_lmsr=0.0005, q_frac=0.01,
|
||||
b_mode="thin_side", b_factor=0.5):
|
||||
"""
|
||||
For a fixed X (x-reserve), sweep imbalance rho = Y/X and measure welfare advantage at a chosen trade size q = q_frac * X.
|
||||
Returns arrays over rho.
|
||||
"""
|
||||
q = q_frac * X
|
||||
wgap = []
|
||||
pen_cp_list, pen_lm_list, p0_list, b_list = [], [], [], []
|
||||
for rho in rho_grid:
|
||||
Y = rho * X
|
||||
res = static_compare(X, Y, fee_cp, fee_lmsr, b=None, b_mode=b_mode, b_factor=b_factor, q_grid=np.array([q]))
|
||||
wgap.append(res["welfare_gap"][0])
|
||||
pen_cp_list.append(res["pen_cp"][0])
|
||||
pen_lm_list.append(res["pen_lm"][0])
|
||||
p0_list.append(res["p0"])
|
||||
b_list.append(res["b"])
|
||||
return {
|
||||
"rho_grid": rho_grid,
|
||||
"q": q,
|
||||
"welfare_gap": np.array(wgap),
|
||||
"pen_cp": np.array(pen_cp_list),
|
||||
"pen_lm": np.array(pen_lm_list),
|
||||
"p0": np.array(p0_list),
|
||||
"b": np.array(b_list)
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# Sequential Monte Carlo simulation
|
||||
# ---------------------------
|
||||
|
||||
def sample_trade_sizes(n, X, dist="lognormal", mean_frac=0.005, std_frac=0.01, seed=None):
|
||||
"""
|
||||
Return non-negative trade sizes q in x-units.
|
||||
- lognormal: parameters chosen so mean ≈ mean_frac*X
|
||||
- uniform: U(0, 2*mean_frac*X)
|
||||
"""
|
||||
rng = np.random.default_rng(seed)
|
||||
if dist == "lognormal":
|
||||
mean = mean_frac * X
|
||||
std = std_frac * X
|
||||
# Map mean/std to lognormal parameters
|
||||
# mean = exp(mu + sigma^2/2), var = (exp(sigma^2)-1)exp(2mu+sigma^2)
|
||||
# Let CV = std/mean => sigma^2 = ln(1+CV^2), mu = ln(mean) - sigma^2/2
|
||||
cv = std / max(1e-12, mean)
|
||||
sigma2 = math.log(1 + cv * cv)
|
||||
sigma = math.sqrt(sigma2)
|
||||
mu = math.log(max(1e-12, mean)) - sigma2 / 2
|
||||
q = rng.lognormal(mean=mu, sigma=sigma, size=n)
|
||||
return q
|
||||
elif dist == "uniform":
|
||||
return rng.uniform(0, 2 * mean_frac * X, size=n)
|
||||
else:
|
||||
raise ValueError("Unknown dist")
|
||||
|
||||
|
||||
def sequential_sim(X0, Y0, n_trades=2000, direction_bias=0.6,
|
||||
fee_cp=0.0005, fee_lmsr=0.0005,
|
||||
b_mode="thin_side", b_factor=0.5,
|
||||
dist="lognormal", mean_frac=0.005, std_frac=0.01, seed=42):
|
||||
"""
|
||||
Run a sequential simulation where each trade either buys y with x (prob=direction_bias)
|
||||
or buys x with y (prob=1-direction_bias). We compare CP vs LMSR per-trade and accumulate:
|
||||
- taker surplus difference (y-per-x times q, converted to y-units)
|
||||
- fee revenue for LPs (x- or y-denominated; we also track value at spot)
|
||||
- pool state evolution (for CP only; LMSR is state-less under this approximation)
|
||||
Note: We approximate LMSR as state-less with ds/dq = 1/b, b recalibrated each step to the thin side.
|
||||
"""
|
||||
rng = np.random.default_rng(seed)
|
||||
X_cp, Y_cp = X0, Y0
|
||||
p_history = []
|
||||
b_history = []
|
||||
|
||||
taker_y_advantage = 0.0
|
||||
lp_fee_x_cp = 0.0
|
||||
lp_fee_x_lm = 0.0
|
||||
|
||||
# Track realized slippage stats at fixed snapshot prices for comparability
|
||||
for t in range(n_trades):
|
||||
p_cp = cp_price(X_cp, Y_cp)
|
||||
p_history.append(p_cp)
|
||||
|
||||
# Calibrate LMSR b to thin side each step
|
||||
b = lmsr_calibrate_b(X_cp, Y_cp, mode=b_mode, factor=b_factor)
|
||||
b_history.append(b)
|
||||
|
||||
# Decide direction: True => use x to buy y; False => use y to buy x
|
||||
buy_y = rng.random() < direction_bias
|
||||
|
||||
if buy_y:
|
||||
# Draw trade size in x-units
|
||||
qx = float(sample_trade_sizes(1, X_cp, dist, mean_frac, std_frac, seed=rng.integers(1e9))[0])
|
||||
|
||||
# CP execution
|
||||
dy_cp, X_cp_new, Y_cp_new, fee_x_cp = cp_trade_y_out(X_cp, Y_cp, qx, fee_cp)
|
||||
|
||||
# LMSR execution (approximate with current spot p_cp)
|
||||
dy_lm, fee_x_lm = lmsr_y_out(qx, p_cp, b, fee_lmsr)
|
||||
|
||||
# Update CP state
|
||||
X_cp, Y_cp = X_cp_new, Y_cp_new
|
||||
|
||||
# Accumulate
|
||||
taker_y_advantage += (dy_lm - dy_cp)
|
||||
lp_fee_x_cp += fee_x_cp
|
||||
lp_fee_x_lm += fee_x_lm
|
||||
|
||||
else:
|
||||
# Buy x with y. Mirror the model by symmetry:
|
||||
# We'll convert problem by swapping roles of (x,y) and using same formulas.
|
||||
# Draw trade size in y-units proportional to Y
|
||||
qy = float(sample_trade_sizes(1, Y_cp, dist, mean_frac, std_frac, seed=rng.integers(1e9))[0])
|
||||
|
||||
# CP: y-in, x-out
|
||||
# Symmetric function by swapping X<->Y and interpreting result
|
||||
dy_dummy, Y_new, X_new, fee_y_cp = cp_trade_y_out(Y_cp, X_cp, qy, fee_cp) # returns x-out in "dy_dummy"
|
||||
dx_cp = dy_dummy # interpret as x-out
|
||||
X_cp, Y_cp = X_new, Y_new
|
||||
|
||||
# LMSR: state-less approx using ds/dq_y = 1/b_y; we map via price and symmetry.
|
||||
# For buy-x with y-in, average x per y uses 1/p along exponential path in y-space.
|
||||
# For simplicity, we mirror by computing y-per-x with LMSR at price p, then invert locally:
|
||||
# Expected x received ≈ qy / p * (e^{(qy_eff/b_y)/?} - 1)/((qy)/?) ... too detailed.
|
||||
# Instead, use symmetry by swapping labels and reusing the function with p_inv = 1/p.
|
||||
p_inv = 1.0 / p_cp
|
||||
b_y = lmsr_calibrate_b(X_cp, Y_cp, mode="y_side", factor=b_factor)
|
||||
dx_lm, fee_y_lm = lmsr_y_out(qy, p_inv, b_y, fee_lmsr) # gives x-out
|
||||
|
||||
# Taker advantage in x-units; convert to y-units for aggregation using pre-trade spot
|
||||
taker_y_advantage += (dx_lm - dx_cp) * p_cp # multiply by p to convert x to y at current spot
|
||||
lp_fee_x_cp += fee_y_cp * p_inv # convert y-fee to x using p_inv
|
||||
lp_fee_x_lm += fee_y_lm * p_inv
|
||||
|
||||
return {
|
||||
"taker_y_advantage": taker_y_advantage,
|
||||
"lp_fee_x_cp": lp_fee_x_cp,
|
||||
"lp_fee_x_lm": lp_fee_x_lm,
|
||||
"p_history": np.array(p_history),
|
||||
"b_history": np.array(b_history),
|
||||
"final_state_cp": (X_cp, Y_cp)
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# Plotting helpers
|
||||
# ---------------------------
|
||||
|
||||
def plot_static(res):
|
||||
q = res["q_grid"]
|
||||
p0 = res["p0"]
|
||||
plt.figure(figsize=(10, 6))
|
||||
plt.plot(q, res["avg_cp"], label="CP avg y-per-x")
|
||||
plt.plot(q, res["avg_lm"], label="LMSR avg y-per-x")
|
||||
plt.axhline(p0, color="gray", linestyle="--", alpha=0.6, label="Spot p0")
|
||||
plt.xscale("log")
|
||||
plt.title("Average execution (y per x) vs trade size")
|
||||
plt.xlabel("q_x (trade size)")
|
||||
plt.ylabel("y per x")
|
||||
plt.legend()
|
||||
plt.tight_layout()
|
||||
|
||||
plt.figure(figsize=(10, 6))
|
||||
plt.plot(q, res["welfare_gap"] / res["p0"] * 1e4)
|
||||
plt.xscale("log")
|
||||
plt.axhline(0, color="gray", linestyle="--")
|
||||
plt.title("Taker welfare advantage LMSR−CP (in bp of price)")
|
||||
plt.xlabel("q_x (trade size)")
|
||||
plt.ylabel("bp")
|
||||
plt.tight_layout()
|
||||
|
||||
def plot_imbalance(sweep):
|
||||
rho = sweep["rho_grid"]
|
||||
plt.figure(figsize=(10, 6))
|
||||
plt.plot(rho, sweep["welfare_gap"] / sweep["p0"] * 1e4)
|
||||
plt.axhline(0, color="gray", linestyle="--")
|
||||
plt.title(f"LMSR−CP taker advantage vs imbalance (q={sweep['q']:.4g}·X)")
|
||||
plt.xlabel("rho = Y/X (imbalance)")
|
||||
plt.ylabel("bp of price")
|
||||
plt.tight_layout()
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# Demo main
|
||||
# ---------------------------
|
||||
|
||||
def demo():
|
||||
# Baseline pool
|
||||
X, Y = 10_000.0, 10_000.0
|
||||
fee_cp = 0.0005 # 5 bp
|
||||
fee_lmsr = 0.0005 # 5 bp
|
||||
b_mode = "thin_side"
|
||||
b_factor = 0.5
|
||||
|
||||
# Static comparison across trade sizes
|
||||
res = static_compare(
|
||||
X, Y, fee_cp=fee_cp, fee_lmsr=fee_lmsr,
|
||||
b=None, b_mode=b_mode, b_factor=b_factor
|
||||
)
|
||||
print(f"Spot p0={res['p0']:.6f}, calibrated b={res['b']:.6f}")
|
||||
plot_static(res)
|
||||
|
||||
# Imbalance sweep
|
||||
rho_grid = np.linspace(0.2, 5.0, 60) # from thin-y to thin-x
|
||||
sw = sweep_imbalance(
|
||||
X, rho_grid, fee_cp=fee_cp, fee_lmsr=fee_lmsr,
|
||||
q_frac=0.01, b_mode=b_mode, b_factor=b_factor
|
||||
)
|
||||
plot_imbalance(sw)
|
||||
|
||||
# Sequential simulation
|
||||
sim = sequential_sim(
|
||||
X0=X, Y0=Y, n_trades=2000, direction_bias=0.6,
|
||||
fee_cp=fee_cp, fee_lmsr=fee_lmsr,
|
||||
b_mode=b_mode, b_factor=b_factor,
|
||||
dist="lognormal", mean_frac=0.005, std_frac=0.01, seed=7
|
||||
)
|
||||
adv_bp_equiv = sim["taker_y_advantage"] / (X + Y / res["p0"]) * 1e4
|
||||
print(f"Sequential sim taker Y-advantage (absolute): {sim['taker_y_advantage']:.6f} y-units")
|
||||
print(f"LP fee (CP) in x-units: {sim['lp_fee_x_cp']:.6f}")
|
||||
print(f"LP fee (LMSR) in x-units: {sim['lp_fee_x_lm']:.6f}")
|
||||
|
||||
plt.show()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
demo()
|
||||
71
script/DeployMock.sol
Normal file
71
script/DeployMock.sol
Normal file
@@ -0,0 +1,71 @@
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import "forge-std/Script.sol";
|
||||
import "forge-std/console2.sol";
|
||||
import "@abdk/ABDKMath64x64.sol";
|
||||
import "../test/MockERC20.sol";
|
||||
import "../src/IPartyPool.sol";
|
||||
import "../src/PartyPool.sol";
|
||||
|
||||
contract DeployMock is Script {
|
||||
|
||||
// private key 0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356
|
||||
address constant devAccount7 = 0x14dC79964da2C08b23698B3D3cc7Ca32193d9955;
|
||||
|
||||
function run() public {
|
||||
vm.startBroadcast(deployer);
|
||||
|
||||
// create mock tokens
|
||||
usxd = new MockERC20('Joke Currency', 'USXD', 6);
|
||||
fusd = new MockERC20('Fake USD', 'FUSD', 6);
|
||||
dive = new MockERC20('DAI Virtually Equal', 'DIVE', 18);
|
||||
|
||||
string memory name = 'Mock Pool';
|
||||
string memory symbol = 'MP';
|
||||
address[] memory tokens = new address[](3);
|
||||
tokens[0] = address(usxd);
|
||||
tokens[1] = address(fusd);
|
||||
tokens[2] = address(dive);
|
||||
uint256[] memory _bases = new uint256[](3);
|
||||
_bases[0] = 6;
|
||||
_bases[1] = 6;
|
||||
_bases[2] = 18;
|
||||
int128 _tradeFrac = ABDKMath64x64.divu(1, 10);
|
||||
int128 _targetSlippage = ABDKMath64x64.divu(1,10000);
|
||||
uint256 _feePpm = 100;
|
||||
|
||||
IPartyPool pool = new PartyPool();
|
||||
bytes memory args = abi.encode(name, symbol, tokens, _bases, _tradeFrac, _targetSlippage, _feePpm);
|
||||
bytes memory deployCode = abi.encodePacked(bytecode,args);
|
||||
vm.etch(pool, deployCode);
|
||||
|
||||
console2.log('PartyPool', pool);
|
||||
|
||||
// initial mint
|
||||
mintAll(pool, 10_000);
|
||||
IPartyPool(pool).mint(deployer, deployer, 0, 0);
|
||||
|
||||
console2.log('USXD', address(usxd));
|
||||
console2.log('FUSD', address(fusd));
|
||||
console2.log('DIVE', address(dive));
|
||||
|
||||
// give tokens to dev7
|
||||
mintAll(devAccount7, 1_000_000);
|
||||
|
||||
vm.stopBroadcast();
|
||||
}
|
||||
|
||||
address constant deployer = address(0x472358699872673459876); // anything
|
||||
|
||||
MockERC20 private usxd;
|
||||
MockERC20 private fusd;
|
||||
MockERC20 private dive;
|
||||
|
||||
function mintAll(address who, uint256 amount) internal {
|
||||
usxd.mint(who, amount * 1e6);
|
||||
fusd.mint(who, amount * 1e6);
|
||||
dive.mint(who, amount * 1e18);
|
||||
}
|
||||
|
||||
}
|
||||
6
src/IPartyFlashCallback.sol
Normal file
6
src/IPartyFlashCallback.sol
Normal file
@@ -0,0 +1,6 @@
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
interface IPartyFlashCallback {
|
||||
function partyFlashCallback(uint256[] memory loanAmounts, uint256[] memory repaymentAmounts, bytes calldata data) external;
|
||||
}
|
||||
149
src/IPartyPool.sol
Normal file
149
src/IPartyPool.sol
Normal file
@@ -0,0 +1,149 @@
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
|
||||
|
||||
/// @title PartyPool - LMSR-backed multi-asset pool with LP ERC20 token
|
||||
/// @notice Uses LMSRStabilized library; stores per-token uint bases to convert to/from 64.64 fixed point.
|
||||
/// - Caches qInternal[] (int128 64.64) and cachedUintBalances[] to minimize balanceOf() calls.
|
||||
/// - swap and swapToLimit mimic core lib; mint/burn call updateForProportionalChange() and manage LP tokens.
|
||||
interface IPartyPool is IERC20Metadata {
|
||||
// All int128's are ABDKMath64x64 format
|
||||
|
||||
// Events
|
||||
|
||||
event Mint(address payer, address indexed receiver, uint256[] amounts, uint256 lpMinted);
|
||||
|
||||
event Burn(address payer, address indexed receiver, uint256[] amounts, uint256 lpBurned);
|
||||
|
||||
event Swap(
|
||||
address payer,
|
||||
address indexed receiver,
|
||||
address indexed tokenIn,
|
||||
address indexed tokenOut,
|
||||
uint256 amountIn,
|
||||
uint256 amountOut
|
||||
);
|
||||
|
||||
/// @notice Emitted when a single-token swapMint is executed.
|
||||
/// Records payer/receiver, input token index, gross transfer (net+fee), net input and fee taken.
|
||||
event SwapMint(
|
||||
address indexed payer,
|
||||
address indexed receiver,
|
||||
uint256 indexed inputTokenIndex,
|
||||
uint256 grossTransfer, // total tokens transferred (net + fee)
|
||||
uint256 netInput, // net input credited to swaps (after fee)
|
||||
uint256 feeTaken // fee taken (ceil)
|
||||
);
|
||||
|
||||
/// @notice Emitted when a burnSwap is executed.
|
||||
/// Records payer/receiver, target token index and the uint payout sent to the receiver.
|
||||
event BurnSwap(
|
||||
address indexed payer,
|
||||
address indexed receiver,
|
||||
uint256 indexed targetTokenIndex,
|
||||
uint256 payoutUint
|
||||
);
|
||||
|
||||
|
||||
// Immutable pool configuration (public getters)
|
||||
function tokens(uint256) external view returns (address); // get single token
|
||||
function numTokens() external view returns (uint256);
|
||||
function allTokens() external view returns (address[] memory);
|
||||
function tradeFrac() external view returns (int128); // ABDK 64x64
|
||||
function targetSlippage() external view returns (int128); // ABDK 64x64
|
||||
function swapFeePpm() external view returns (uint256);
|
||||
function tokenAddressToIndexPlusOne(address) external view returns (uint);
|
||||
|
||||
// Initialization / Mint / Burn (LP token managed)
|
||||
|
||||
/// @notice Calculate the proportional deposit amounts required for a given LP token amount
|
||||
/// @param lpTokenAmount The amount of LP tokens desired
|
||||
/// @return depositAmounts Array of token amounts to deposit (rounded up)
|
||||
function computeMintAmounts(uint256 lpTokenAmount) external view returns (uint256[] memory depositAmounts);
|
||||
|
||||
/// @notice Proportional mint (or initial supply if first call).
|
||||
/// For initial supply: assumes tokens have already been transferred to the pool
|
||||
/// For subsequent mints: payer must approve tokens beforehand, receiver gets the LP tokens
|
||||
/// @param payer address that provides the input tokens (ignored for initial deposit)
|
||||
/// @param receiver address that receives the LP tokens
|
||||
/// @param lpTokenAmount desired amount of LP tokens to mint (ignored for initial deposit)
|
||||
/// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore.
|
||||
function mint(address payer, address receiver, uint256 lpTokenAmount, uint256 deadline) external;
|
||||
|
||||
/// @notice Calculate the proportional withdrawal amounts for a given LP token amount
|
||||
/// @param lpTokenAmount The amount of LP tokens to burn
|
||||
/// @return withdrawAmounts Array of token amounts to withdraw (rounded down)
|
||||
function computeBurnAmounts(uint256 lpTokenAmount) external view returns (uint256[] memory withdrawAmounts);
|
||||
|
||||
/// @notice Burn LP tokens and withdraw the proportional basket to receiver.
|
||||
/// Payer must own the LP tokens; withdraw amounts are computed from current proportions.
|
||||
/// @param payer address that provides the LP tokens to burn
|
||||
/// @param receiver address that receives the withdrawn tokens
|
||||
/// @param lpAmount amount of LP tokens to burn (proportional withdrawal)
|
||||
/// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore.
|
||||
function burn(address payer, address receiver, uint256 lpAmount, uint256 deadline) external;
|
||||
|
||||
|
||||
// Swaps
|
||||
function swap(
|
||||
address payer,
|
||||
address receiver,
|
||||
uint256 i,
|
||||
uint256 j,
|
||||
uint256 maxAmountIn,
|
||||
int128 limitPrice,
|
||||
uint256 deadline
|
||||
) external returns (uint256 amountIn, uint256 amountOut);
|
||||
|
||||
function swapToLimit(
|
||||
address payer,
|
||||
address receiver,
|
||||
uint256 i,
|
||||
uint256 j,
|
||||
int128 limitPrice,
|
||||
uint256 deadline
|
||||
) external returns (uint256 amountInUsed, uint256 amountOut);
|
||||
|
||||
/// @notice Single-token mint: deposit a single token, charge swap-LMSR cost, and mint LP.
|
||||
/// @param payer who transfers the input token
|
||||
/// @param receiver who receives the minted LP tokens
|
||||
/// @param i index of the input token
|
||||
/// @param maxAmountIn maximum uint token input (inclusive of fee)
|
||||
/// @param deadline optional deadline
|
||||
/// @return lpMinted actual LP minted (uint)
|
||||
function swapMint(
|
||||
address payer,
|
||||
address receiver,
|
||||
uint256 i,
|
||||
uint256 maxAmountIn,
|
||||
uint256 deadline
|
||||
) external returns (uint256 lpMinted);
|
||||
|
||||
/// @notice Burn LP tokens then swap the redeemed proportional basket into a single asset `i` and send to receiver.
|
||||
/// @param payer who burns LP tokens
|
||||
/// @param receiver who receives the single asset
|
||||
/// @param lpAmount amount of LP tokens to burn
|
||||
/// @param i index of target asset to receive
|
||||
/// @param deadline optional deadline
|
||||
/// @return amountOutUint uint amount of asset i sent to receiver
|
||||
function burnSwap(
|
||||
address payer,
|
||||
address receiver,
|
||||
uint256 lpAmount,
|
||||
uint256 i,
|
||||
uint256 deadline
|
||||
) external returns (uint256 amountOutUint);
|
||||
|
||||
/// @notice Receive token0 and/or token1 and pay it back, plus a fee, in the callback
|
||||
/// @dev The caller of this method receives a callback in the form of IPartyFlashCallback#partyFlashCallback
|
||||
/// @param recipient The address which will receive the token amounts
|
||||
/// @param amounts The amount of each token to send
|
||||
/// @param data Any data to be passed through to the callback
|
||||
function flash(
|
||||
address recipient,
|
||||
uint256[] memory amounts,
|
||||
bytes calldata data
|
||||
) external;
|
||||
|
||||
}
|
||||
1138
src/LMSRStabilized.sol
Normal file
1138
src/LMSRStabilized.sol
Normal file
File diff suppressed because it is too large
Load Diff
818
src/PartyPool.sol
Normal file
818
src/PartyPool.sol
Normal file
@@ -0,0 +1,818 @@
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import "@abdk/ABDKMath64x64.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
||||
import "./LMSRStabilized.sol";
|
||||
import "./IPartyPool.sol";
|
||||
import "./IPartyFlashCallback.sol";
|
||||
|
||||
/// @title PartyPool - LMSR-backed multi-asset pool with LP ERC20 token
|
||||
/// @notice Uses LMSRStabilized library; stores per-token uint bases to convert to/from 64.64 fixed point.
|
||||
/// - Caches qInternal[] (int128 64.64) and cachedUintBalances[] to minimize balanceOf() calls.
|
||||
/// - swap and swapToLimit mimic core lib; mint/burn call updateForProportionalChange() and manage LP tokens.
|
||||
contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
|
||||
using ABDKMath64x64 for int128;
|
||||
using LMSRStabilized for LMSRStabilized.State;
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
|
||||
//
|
||||
// Immutable pool configuration
|
||||
//
|
||||
|
||||
address[] public tokens; // effectively immutable since there is no interface to change the tokens
|
||||
function numTokens() external view returns (uint256) { return tokens.length; }
|
||||
function allTokens() external view returns (address[] memory) { return tokens; }
|
||||
|
||||
// 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.
|
||||
int128 public immutable tradeFrac; // slippage target trade size as a fraction of one asset's inventory
|
||||
int128 public immutable targetSlippage; // target slippage applied to that trade size
|
||||
|
||||
// fee in parts-per-million (ppm), taken from inputs before swaps
|
||||
uint256 public immutable swapFeePpm;
|
||||
|
||||
// flash loan fee in parts-per-million (ppm)
|
||||
uint256 public immutable flashFeePpm;
|
||||
|
||||
//
|
||||
// Internal state
|
||||
//
|
||||
|
||||
LMSRStabilized.State internal lmsr;
|
||||
|
||||
// Cached on-chain balances (uint) and internal 64.64 representation
|
||||
// balance / base = internal
|
||||
uint256[] internal cachedUintBalances;
|
||||
uint256[] internal bases; // per-token uint base used to scale token amounts <-> internal
|
||||
|
||||
mapping(address=>uint) public tokenAddressToIndexPlusOne; // Uses index+1 so a result of 0 indicates a failed lookup
|
||||
|
||||
uint256 public constant LP_SCALE = 1e18; // Scale used to convert LMSR lastTotal (Q64.64) into LP token units (uint)
|
||||
|
||||
/// @param name_ LP token name
|
||||
/// @param symbol_ LP token symbol
|
||||
/// @param _tokens token addresses (n)
|
||||
/// @param _bases scaling bases for each token (n) - used when converting to/from internal 64.64 amounts
|
||||
/// @param _tradeFrac trade fraction in 64.64 fixed-point (as used by LMSR)
|
||||
/// @param _targetSlippage target slippage in 64.64 fixed-point (as used by LMSR)
|
||||
/// @param _swapFeePpm fee in parts-per-million, taken from swap input amounts before LMSR calculations
|
||||
/// @param _flashFeePpm fee in parts-per-million, taken for flash loans
|
||||
constructor(
|
||||
string memory name_,
|
||||
string memory symbol_,
|
||||
address[] memory _tokens,
|
||||
uint256[] memory _bases,
|
||||
int128 _tradeFrac,
|
||||
int128 _targetSlippage,
|
||||
uint256 _swapFeePpm,
|
||||
uint256 _flashFeePpm
|
||||
) ERC20(name_, symbol_) {
|
||||
require(_tokens.length > 1, "Pool: need >1 asset");
|
||||
require(_tokens.length == _bases.length, "Pool: lengths mismatch");
|
||||
tokens = _tokens;
|
||||
bases = _bases;
|
||||
tradeFrac = _tradeFrac;
|
||||
targetSlippage = _targetSlippage;
|
||||
require(_swapFeePpm < 1_000_000, "Pool: fee >= ppm");
|
||||
swapFeePpm = _swapFeePpm;
|
||||
require(_flashFeePpm < 1_000_000, "Pool: flash fee >= ppm");
|
||||
flashFeePpm = _flashFeePpm;
|
||||
|
||||
uint256 n = _tokens.length;
|
||||
|
||||
// Initialize LMSR state nAssets; full init occurs on first mint when quantities are known.
|
||||
lmsr.nAssets = n;
|
||||
|
||||
// Initialize token address to index mapping
|
||||
for (uint i = 0; i < n;) {
|
||||
tokenAddressToIndexPlusOne[_tokens[i]] = i + 1;
|
||||
unchecked {i++;}
|
||||
}
|
||||
|
||||
// Initialize caches to zero
|
||||
cachedUintBalances = new uint256[](n);
|
||||
}
|
||||
|
||||
|
||||
/* ----------------------
|
||||
Initialization / Mint / Burn (LP token managed)
|
||||
---------------------- */
|
||||
|
||||
/// @notice Calculate the proportional deposit amounts required for a given LP token amount
|
||||
/// @param lpTokenAmount The amount of LP tokens desired
|
||||
/// @return depositAmounts Array of token amounts to deposit (rounded up)
|
||||
function computeMintAmounts(uint256 lpTokenAmount) public view returns (uint256[] memory depositAmounts) {
|
||||
uint256 n = tokens.length;
|
||||
depositAmounts = new uint256[](n);
|
||||
|
||||
// If this is the first mint or pool is empty, return zeros
|
||||
// For first mint, tokens should already be transferred to the pool
|
||||
if (totalSupply() == 0 || lmsr.nAssets == 0) {
|
||||
return depositAmounts; // Return zeros, initial deposit handled differently
|
||||
}
|
||||
|
||||
// Calculate deposit based on current proportions
|
||||
uint256 totalLpSupply = totalSupply();
|
||||
|
||||
// lpTokenAmount / totalLpSupply = depositAmount / currentBalance
|
||||
// Therefore: depositAmount = (lpTokenAmount * currentBalance) / totalLpSupply
|
||||
// We round up to protect the pool
|
||||
for (uint i = 0; i < n; i++) {
|
||||
uint256 currentBalance = cachedUintBalances[i];
|
||||
// Calculate with rounding up: (a * b + c - 1) / c
|
||||
depositAmounts[i] = (lpTokenAmount * currentBalance + totalLpSupply - 1) / totalLpSupply;
|
||||
}
|
||||
|
||||
return depositAmounts;
|
||||
}
|
||||
|
||||
/// @notice Calculate the proportional withdrawal amounts for a given LP token amount
|
||||
/// @param lpTokenAmount The amount of LP tokens to burn
|
||||
/// @return withdrawAmounts Array of token amounts to withdraw (rounded down)
|
||||
function computeBurnAmounts(uint256 lpTokenAmount) external view returns (uint256[] memory withdrawAmounts) {
|
||||
return _computeBurnAmounts(lpTokenAmount);
|
||||
}
|
||||
|
||||
function _computeBurnAmounts(uint256 lpTokenAmount) internal view returns (uint256[] memory withdrawAmounts) {
|
||||
uint256 n = tokens.length;
|
||||
withdrawAmounts = new uint256[](n);
|
||||
|
||||
// If supply is zero or pool uninitialized, return zeros
|
||||
if (totalSupply() == 0 || lmsr.nAssets == 0) {
|
||||
return withdrawAmounts; // Return zeros, nothing to withdraw
|
||||
}
|
||||
|
||||
// Calculate withdrawal amounts based on current proportions
|
||||
uint256 totalLpSupply = totalSupply();
|
||||
|
||||
// withdrawAmount = floor(lpTokenAmount * currentBalance / totalLpSupply)
|
||||
for (uint i = 0; i < n; i++) {
|
||||
uint256 currentBalance = cachedUintBalances[i];
|
||||
withdrawAmounts[i] = (lpTokenAmount * currentBalance) / totalLpSupply;
|
||||
}
|
||||
|
||||
return withdrawAmounts;
|
||||
}
|
||||
|
||||
/// @notice Proportional mint (or initial supply if first call).
|
||||
/// For initial supply: assumes tokens have already been transferred to the pool
|
||||
/// For subsequent mints: payer must approve tokens beforehand, receiver gets the LP tokens
|
||||
/// @param payer address that provides the input tokens (ignored for initial deposit)
|
||||
/// @param receiver address that receives the LP tokens
|
||||
/// @param lpTokenAmount desired amount of LP tokens to mint (ignored for initial deposit)
|
||||
/// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore.
|
||||
function mint(address payer, address receiver, uint256 lpTokenAmount, uint256 deadline) external nonReentrant {
|
||||
require(deadline == 0 || block.timestamp <= deadline, "mint: deadline exceeded");
|
||||
uint256 n = tokens.length;
|
||||
// Check if this is initial deposit
|
||||
bool isInitialDeposit = totalSupply() == 0 || lmsr.nAssets == 0;
|
||||
|
||||
require(lpTokenAmount > 0 || isInitialDeposit, "mint: zero LP amount");
|
||||
|
||||
// Capture old pool size metric (scaled) by computing from current balances
|
||||
uint256 oldScaled = 0;
|
||||
if (!isInitialDeposit) {
|
||||
int128 oldTotal = _computeSizeMetric(lmsr.qInternal);
|
||||
oldScaled = ABDKMath64x64.mulu(oldTotal, LP_SCALE);
|
||||
}
|
||||
|
||||
// For non-initial deposits, transfer tokens from payer
|
||||
uint256[] memory depositAmounts = new uint256[](n);
|
||||
|
||||
if (!isInitialDeposit) {
|
||||
// Calculate required deposit amounts for the desired LP tokens
|
||||
depositAmounts = computeMintAmounts(lpTokenAmount);
|
||||
|
||||
// Transfer in all token amounts
|
||||
for (uint i = 0; i < n; ) {
|
||||
if (depositAmounts[i] > 0) {
|
||||
_safeTransferFrom(tokens[i], payer, address(this), depositAmounts[i]);
|
||||
}
|
||||
unchecked { i++; }
|
||||
}
|
||||
}
|
||||
|
||||
// Update cached balances for all assets
|
||||
int128[] memory newQInternal = new int128[](n);
|
||||
for (uint i = 0; i < n; ) {
|
||||
uint256 bal = IERC20(tokens[i]).balanceOf(address(this));
|
||||
cachedUintBalances[i] = bal;
|
||||
newQInternal[i] = _uintToInternalFloor(bal, bases[i]);
|
||||
|
||||
// For initial deposit, record the actual deposited amounts
|
||||
if (isInitialDeposit) {
|
||||
depositAmounts[i] = bal;
|
||||
}
|
||||
|
||||
unchecked { i++; }
|
||||
}
|
||||
|
||||
// If first time, call init, otherwise update proportional change.
|
||||
if (isInitialDeposit) {
|
||||
// Initialize the stabilized LMSR state
|
||||
lmsr.init(newQInternal, tradeFrac, targetSlippage);
|
||||
} else {
|
||||
// Update for proportional change
|
||||
lmsr.updateForProportionalChange(newQInternal);
|
||||
}
|
||||
|
||||
// Compute actual LP tokens to mint based on change in size metric (scaled)
|
||||
// floor truncation rounds in favor of the pool
|
||||
int128 newTotal = _computeSizeMetric(newQInternal);
|
||||
uint256 newScaled = ABDKMath64x64.mulu(newTotal, LP_SCALE);
|
||||
uint256 actualLpToMint;
|
||||
|
||||
if (isInitialDeposit) {
|
||||
// Initial provisioning: mint newScaled (as LP units)
|
||||
actualLpToMint = newScaled;
|
||||
} else {
|
||||
require(oldScaled > 0, "mint: oldScaled zero");
|
||||
uint256 delta = (newScaled > oldScaled) ? (newScaled - oldScaled) : 0;
|
||||
// Proportional issuance: totalSupply * delta / oldScaled
|
||||
if (delta > 0) {
|
||||
// floor truncation rounds in favor of the pool
|
||||
actualLpToMint = (totalSupply() * delta) / oldScaled;
|
||||
} else {
|
||||
actualLpToMint = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// For subsequent mints, ensure the calculated LP amount is not too different from requested
|
||||
if (!isInitialDeposit) {
|
||||
// Allow for some rounding error but ensure we're not far off from requested amount
|
||||
require(actualLpToMint > 0, "mint: zero LP minted");
|
||||
|
||||
// Allow actual amount to be at most 0.00001% less than requested
|
||||
// This accounts for rounding in deposit calculations
|
||||
uint256 minAcceptable = lpTokenAmount * 99_999 / 100_000;
|
||||
require(actualLpToMint >= minAcceptable, "mint: insufficient LP minted");
|
||||
}
|
||||
|
||||
console2.log('actualLpToMint', actualLpToMint);
|
||||
require( actualLpToMint > 0, "mint: zero LP amount");
|
||||
_mint(receiver, actualLpToMint);
|
||||
emit Mint(payer, receiver, depositAmounts, actualLpToMint);
|
||||
}
|
||||
|
||||
/// @notice Burn LP tokens and withdraw the proportional basket to receiver.
|
||||
/// Payer must own the LP tokens; withdraw amounts are computed from current proportions.
|
||||
/// @param payer address that provides the LP tokens to burn
|
||||
/// @param receiver address that receives the withdrawn tokens
|
||||
/// @param lpAmount amount of LP tokens to burn (proportional withdrawal)
|
||||
/// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore.
|
||||
function burn(address payer, address receiver, uint256 lpAmount, uint256 deadline) external nonReentrant {
|
||||
require(deadline == 0 || block.timestamp <= deadline, "burn: deadline exceeded");
|
||||
uint256 n = tokens.length;
|
||||
require(lpAmount > 0, "burn: zero lp");
|
||||
|
||||
uint256 supply = totalSupply();
|
||||
require(supply > 0, "burn: empty supply");
|
||||
require(lmsr.nAssets > 0, "burn: uninit pool");
|
||||
require(balanceOf(payer) >= lpAmount, "burn: insufficient LP");
|
||||
|
||||
// Refresh cached balances to reflect current on-chain balances before computing withdrawal amounts
|
||||
for (uint i = 0; i < n; ) {
|
||||
uint256 bal = IERC20(tokens[i]).balanceOf(address(this));
|
||||
cachedUintBalances[i] = bal;
|
||||
unchecked { i++; }
|
||||
}
|
||||
|
||||
// Compute proportional withdrawal amounts for the requested LP amount (rounded down)
|
||||
uint256[] memory withdrawAmounts = _computeBurnAmounts(lpAmount);
|
||||
|
||||
// Transfer underlying tokens out to receiver according to computed proportions
|
||||
for (uint i = 0; i < n; ) {
|
||||
if (withdrawAmounts[i] > 0) {
|
||||
_safeTransfer(tokens[i], receiver, withdrawAmounts[i]);
|
||||
}
|
||||
unchecked { i++; }
|
||||
}
|
||||
|
||||
// Update cached balances and internal q for all assets
|
||||
int128[] memory newQInternal = new int128[](n);
|
||||
for (uint i = 0; i < n; ) {
|
||||
uint256 bal = IERC20(tokens[i]).balanceOf(address(this));
|
||||
cachedUintBalances[i] = bal;
|
||||
newQInternal[i] = _uintToInternalFloor(bal, bases[i]);
|
||||
unchecked { i++; }
|
||||
}
|
||||
|
||||
// Apply proportional update or deinitialize if drained
|
||||
bool allZero = true;
|
||||
for (uint i = 0; i < n; ) {
|
||||
if (newQInternal[i] != int128(0)) {
|
||||
allZero = false;
|
||||
break;
|
||||
}
|
||||
unchecked { i++; }
|
||||
}
|
||||
|
||||
if (allZero) {
|
||||
lmsr.deinit();
|
||||
} else {
|
||||
lmsr.updateForProportionalChange(newQInternal);
|
||||
}
|
||||
|
||||
// Burn exactly the requested LP amount from payer (authorization via allowance)
|
||||
if (msg.sender != payer) {
|
||||
uint256 allowed = allowance(payer, msg.sender);
|
||||
require(allowed >= lpAmount, "burn: allowance insufficient");
|
||||
_approve(payer, msg.sender, allowed - lpAmount);
|
||||
}
|
||||
_burn(payer, lpAmount);
|
||||
|
||||
emit Burn(payer, receiver, withdrawAmounts, lpAmount);
|
||||
}
|
||||
|
||||
/* ----------------------
|
||||
Swaps
|
||||
---------------------- */
|
||||
|
||||
/// @notice Swap input token i -> token j. Payer must approve token i.
|
||||
/// @param payer address of the account that pays for the swap
|
||||
/// @param receiver address that will receive the output tokens
|
||||
/// @param i index of input asset
|
||||
/// @param j index of output asset
|
||||
/// @param maxAmountIn maximum amount of token i (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)
|
||||
function swap(
|
||||
address payer,
|
||||
address receiver,
|
||||
uint256 i,
|
||||
uint256 j,
|
||||
uint256 maxAmountIn,
|
||||
int128 limitPrice,
|
||||
uint256 deadline
|
||||
) external nonReentrant returns (uint256 amountIn, uint256 amountOut) {
|
||||
uint256 n = tokens.length;
|
||||
require(i < n && j < n, "swap: idx");
|
||||
require(maxAmountIn > 0, "swap: input zero");
|
||||
require(deadline == 0 || block.timestamp <= deadline, "swap: deadline exceeded");
|
||||
|
||||
// Read previous balances for affected assets
|
||||
uint256 prevBalI = IERC20(tokens[i]).balanceOf(address(this));
|
||||
uint256 prevBalJ = IERC20(tokens[j]).balanceOf(address(this));
|
||||
|
||||
// Calculate fee (ceiling) and net amount
|
||||
(, uint256 netUintForSwap) = _computeFee(maxAmountIn);
|
||||
|
||||
// Convert the net amount to internal (floor)
|
||||
int128 deltaInternalI = _uintToInternalFloor(netUintForSwap, bases[i]);
|
||||
require(deltaInternalI > int128(0), "swap: input too small after fee");
|
||||
|
||||
// Make sure LMSR state exists
|
||||
require(lmsr.nAssets > 0, "swap: empty pool");
|
||||
|
||||
// Compute swap amounts in internal space using exact-input logic (with limitPrice)
|
||||
(int128 amountInInternalUsed, int128 amountOutInternal) = lmsr.swapAmountsForExactInput(
|
||||
i,
|
||||
j,
|
||||
deltaInternalI,
|
||||
limitPrice
|
||||
);
|
||||
|
||||
// Convert actual used input internal -> uint (ceiling to protect the pool)
|
||||
uint256 amountInUint = _internalToUintCeil(amountInInternalUsed, bases[i]);
|
||||
|
||||
// Total transfer amount includes fee calculated on the actual used input (ceiling)
|
||||
uint256 totalTransferAmount = amountInUint;
|
||||
if (swapFeePpm > 0) {
|
||||
uint256 feeOnUsed = _ceilFee(amountInUint, swapFeePpm);
|
||||
totalTransferAmount += feeOnUsed;
|
||||
}
|
||||
|
||||
// Ensure we do not attempt to transfer more than the caller specified as maximum
|
||||
require(totalTransferAmount > 0, 'swap: input zero');
|
||||
require(totalTransferAmount <= maxAmountIn, "swap: transfer exceeds max");
|
||||
|
||||
// Transfer the exact amount from payer and require exact receipt (revert on fee-on-transfer)
|
||||
_safeTransferFrom(tokens[i], payer, address(this), totalTransferAmount);
|
||||
uint256 balIAfter = IERC20(tokens[i]).balanceOf(address(this));
|
||||
require(balIAfter == prevBalI + totalTransferAmount, "swap: non-standard tokenIn");
|
||||
|
||||
// Compute output uint amount (floor)
|
||||
uint256 amountOutUint = _internalToUintFloor(amountOutInternal, bases[j]);
|
||||
require(amountOutUint > 0, "swap: output zero");
|
||||
|
||||
// Transfer output to receiver and verify exact decrease
|
||||
_safeTransfer(tokens[j], receiver, amountOutUint);
|
||||
uint256 balJAfter = IERC20(tokens[j]).balanceOf(address(this));
|
||||
require(balJAfter == prevBalJ - amountOutUint, "swap: non-standard tokenOut");
|
||||
|
||||
// Update cached uint balances for i and j using actual balances
|
||||
cachedUintBalances[i] = balIAfter;
|
||||
cachedUintBalances[j] = balJAfter;
|
||||
|
||||
// Apply swap to LMSR state with the internal amounts actually used
|
||||
// (fee is already accounted for in the reduced input amount)
|
||||
lmsr.applySwap(i, j, amountInInternalUsed, amountOutInternal);
|
||||
|
||||
emit Swap(payer, receiver, tokens[i], tokens[j], totalTransferAmount, amountOutUint);
|
||||
|
||||
return (totalTransferAmount, amountOutUint);
|
||||
}
|
||||
|
||||
/// @notice Swap up to the price limit; computes max input to reach limit then performs swap.
|
||||
/// If the pool can't fill entirely because of balances, it caps appropriately and returns actuals.
|
||||
/// Payer must approve token i for the exact computed input amount.
|
||||
/// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore.
|
||||
function swapToLimit(
|
||||
address payer,
|
||||
address receiver,
|
||||
uint256 i,
|
||||
uint256 j,
|
||||
int128 limitPrice,
|
||||
uint256 deadline
|
||||
) external returns (uint256 amountInUsed, uint256 amountOut) {
|
||||
uint256 n = tokens.length;
|
||||
require(i < n && j < n, "swapToLimit: idx");
|
||||
require(limitPrice > int128(0), "swapToLimit: limit <= 0");
|
||||
require(deadline == 0 || block.timestamp <= deadline, "swapToLimit: deadline exceeded");
|
||||
|
||||
// Ensure LMSR state exists
|
||||
require(lmsr.nAssets > 0, "swapToLimit: pool uninitialized");
|
||||
|
||||
// Read previous balances for affected assets
|
||||
uint256 prevBalI = IERC20(tokens[i]).balanceOf(address(this));
|
||||
uint256 prevBalJ = IERC20(tokens[j]).balanceOf(address(this));
|
||||
|
||||
// Compute maxima in internal space using library
|
||||
(int128 amountInInternalMax, int128 amountOutInternal) = lmsr.swapAmountsForPriceLimit(i, j, limitPrice);
|
||||
|
||||
// Calculate how much input will be needed with fee included (ceiling to protect the pool)
|
||||
uint256 amountInUsedUint = _internalToUintCeil(amountInInternalMax, bases[i]);
|
||||
require(amountInUsedUint > 0, "swapToLimit: input zero");
|
||||
|
||||
// Total transfer amount is the input amount including what will be taken as fee (ceiling)
|
||||
uint256 totalTransferAmount = amountInUsedUint;
|
||||
|
||||
if (swapFeePpm > 0) {
|
||||
uint256 feeOnUsed = _ceilFee(amountInUsedUint, swapFeePpm);
|
||||
totalTransferAmount += feeOnUsed;
|
||||
}
|
||||
|
||||
// Transfer the exact amount needed from payer and require exact receipt (revert on fee-on-transfer)
|
||||
_safeTransferFrom(tokens[i], payer, address(this), totalTransferAmount);
|
||||
uint256 balIAfter = IERC20(tokens[i]).balanceOf(address(this));
|
||||
require(balIAfter == prevBalI + totalTransferAmount, "swapToLimit: non-standard tokenIn");
|
||||
|
||||
// Compute output amount (floor)
|
||||
uint256 amountOutUint = _internalToUintFloor(amountOutInternal, bases[j]);
|
||||
require(amountOutUint > 0, "swapToLimit: output zero");
|
||||
|
||||
// Transfer output to receiver and verify exact decrease
|
||||
_safeTransfer(tokens[j], receiver, amountOutUint);
|
||||
uint256 balJAfter = IERC20(tokens[j]).balanceOf(address(this));
|
||||
require(balJAfter == prevBalJ - amountOutUint, "swapToLimit: non-standard tokenOut");
|
||||
|
||||
// Update caches to actual balances
|
||||
cachedUintBalances[i] = balIAfter;
|
||||
cachedUintBalances[j] = balJAfter;
|
||||
|
||||
// Apply swap to LMSR state with the internal amounts
|
||||
// (fee is already part of the reduced effective input)
|
||||
lmsr.applySwap(i, j, amountInInternalMax, amountOutInternal);
|
||||
|
||||
emit Swap(payer, receiver, tokens[i], tokens[j], amountInUsedUint, amountOutUint);
|
||||
|
||||
return (amountInUsedUint, amountOutUint);
|
||||
}
|
||||
|
||||
/// @notice Ceiling fee helper: computes ceil(x * feePpm / 1_000_000)
|
||||
function _ceilFee(uint256 x, uint256 feePpm) internal pure returns (uint256) {
|
||||
if (feePpm == 0) return 0;
|
||||
// ceil division: (num + denom - 1) / denom
|
||||
return (x * feePpm + 1_000_000 - 1) / 1_000_000;
|
||||
}
|
||||
|
||||
/// @notice Compute fee and net amounts for a gross input (fee rounded up to favor the pool).
|
||||
/// @return feeUint fee taken (uint) and netUint remaining for protocol use (uint)
|
||||
function _computeFee(uint256 gross) internal view returns (uint256 feeUint, uint256 netUint) {
|
||||
if (swapFeePpm == 0) {
|
||||
return (0, gross);
|
||||
}
|
||||
feeUint = _ceilFee(gross, swapFeePpm);
|
||||
netUint = gross - feeUint;
|
||||
}
|
||||
|
||||
/// @notice Convenience: return gross = net + fee(net) using ceiling for fee.
|
||||
function _addFee(uint256 netUint) internal view returns (uint256 gross) {
|
||||
if (swapFeePpm == 0) return netUint;
|
||||
uint256 fee = _ceilFee(netUint, swapFeePpm);
|
||||
return netUint + fee;
|
||||
}
|
||||
|
||||
// --- New events for single-token mint/burn flows ---
|
||||
// Note: events intentionally avoid exposing internal ΔS and avoid duplicating LP mint/burn data
|
||||
// which is already present in the standard Mint/Burn events.
|
||||
|
||||
/// @notice Single-token mint: deposit a single token, charge swap-LMSR cost, and mint LP.
|
||||
/// @param payer who transfers the input token
|
||||
/// @param receiver who receives the minted LP tokens
|
||||
/// @param i index of the input token
|
||||
/// @param maxAmountIn maximum uint token input (inclusive of fee)
|
||||
/// @param deadline optional deadline
|
||||
/// @return lpMinted actual LP minted (uint)
|
||||
function swapMint(
|
||||
address payer,
|
||||
address receiver,
|
||||
uint256 i,
|
||||
uint256 maxAmountIn,
|
||||
uint256 deadline
|
||||
) external nonReentrant returns (uint256 lpMinted) {
|
||||
uint256 n = tokens.length;
|
||||
require(i < n, "swapMint: idx");
|
||||
require(maxAmountIn > 0, "swapMint: input zero");
|
||||
require(deadline == 0 || block.timestamp <= deadline, "swapMint: deadline");
|
||||
|
||||
// Ensure pool initialized
|
||||
require(lmsr.nAssets > 0, "swapMint: uninit pool");
|
||||
|
||||
// compute fee on gross maxAmountIn to get an initial net estimate (we'll recompute based on actual used)
|
||||
(, uint256 netUintGuess) = _computeFee(maxAmountIn);
|
||||
|
||||
// Convert the net guess to internal (floor)
|
||||
int128 netInternalGuess = _uintToInternalFloor(netUintGuess, bases[i]);
|
||||
require(netInternalGuess > int128(0), "swapMint: input too small after fee");
|
||||
|
||||
// Use LMSR view to determine actual internal consumed and size-increase (ΔS) for mint
|
||||
(int128 amountInInternalUsed, int128 sizeIncreaseInternal) = lmsr.swapAmountsForMint(i, netInternalGuess);
|
||||
|
||||
// amountInInternalUsed may be <= netInternalGuess. Convert to uint (ceil) to determine actual transfer
|
||||
uint256 amountInUint = _internalToUintCeil(amountInInternalUsed, bases[i]);
|
||||
require(amountInUint > 0, "swapMint: input zero after internal conversion");
|
||||
|
||||
// Compute fee on the actual used input and total transfer amount (ceiling)
|
||||
uint256 feeUintActual = _ceilFee(amountInUint, swapFeePpm);
|
||||
uint256 totalTransfer = amountInUint + feeUintActual;
|
||||
require(totalTransfer > 0 && totalTransfer <= maxAmountIn, "swapMint: transfer exceeds max");
|
||||
|
||||
// Record pre-balance and transfer tokens from payer, require exact receipt (revert on fee-on-transfer)
|
||||
uint256 prevBalI = IERC20(tokens[i]).balanceOf(address(this));
|
||||
_safeTransferFrom(tokens[i], payer, address(this), totalTransfer);
|
||||
uint256 balIAfter = IERC20(tokens[i]).balanceOf(address(this));
|
||||
require(balIAfter == prevBalI + totalTransfer, "swapMint: non-standard tokenIn");
|
||||
|
||||
// Update cached uint balances for token i (only i changed externally)
|
||||
cachedUintBalances[i] = balIAfter;
|
||||
|
||||
// Compute old and new scaled size metrics to determine LP minted
|
||||
int128 oldTotal = _computeSizeMetric(lmsr.qInternal);
|
||||
require(oldTotal > int128(0), "swapMint: zero total");
|
||||
uint256 oldScaled = ABDKMath64x64.mulu(oldTotal, LP_SCALE);
|
||||
|
||||
int128 newTotal = oldTotal.add(sizeIncreaseInternal);
|
||||
uint256 newScaled = ABDKMath64x64.mulu(newTotal, LP_SCALE);
|
||||
|
||||
uint256 actualLpToMint;
|
||||
if (totalSupply() == 0) {
|
||||
// If somehow supply zero (shouldn't happen as lmsr.nAssets>0), mint newScaled
|
||||
actualLpToMint = newScaled;
|
||||
} else {
|
||||
require(oldScaled > 0, "swapMint: oldScaled zero");
|
||||
uint256 delta = (newScaled > oldScaled) ? (newScaled - oldScaled) : 0;
|
||||
if (delta > 0) {
|
||||
// floor truncation rounds in favor of pool
|
||||
actualLpToMint = (totalSupply() * delta) / oldScaled;
|
||||
} else {
|
||||
actualLpToMint = 0;
|
||||
}
|
||||
}
|
||||
|
||||
require(actualLpToMint > 0, "swapMint: zero LP minted");
|
||||
|
||||
// Update LMSR internal state: scale qInternal proportionally by newTotal/oldTotal
|
||||
int128[] memory newQInternal = new int128[](n);
|
||||
for (uint256 idx = 0; idx < n; idx++) {
|
||||
// newQInternal[idx] = qInternal[idx] * (newTotal / oldTotal)
|
||||
newQInternal[idx] = lmsr.qInternal[idx].mul(newTotal).div(oldTotal);
|
||||
}
|
||||
|
||||
// Update cached internal and kappa via updateForProportionalChange
|
||||
lmsr.updateForProportionalChange(newQInternal);
|
||||
|
||||
// Note: we updated cachedUintBalances[i] above via reading balance; other token uint balances did not
|
||||
// change externally (they were not transferred in). We keep cachedUintBalances for others unchanged.
|
||||
// Mint LP tokens to receiver
|
||||
_mint(receiver, actualLpToMint);
|
||||
|
||||
// Emit SwapMint event with gross transfer, net input and fee (planned exact-in)
|
||||
emit SwapMint(payer, receiver, i, totalTransfer, amountInUint, feeUintActual);
|
||||
|
||||
// Emit standard Mint event which records deposit amounts and LP minted
|
||||
emit Mint(payer, receiver, new uint256[](n), actualLpToMint);
|
||||
// Note: depositAmounts array omitted (empty) since swapMint uses single-token input
|
||||
|
||||
return actualLpToMint;
|
||||
}
|
||||
|
||||
/// @notice Burn LP tokens then swap the redeemed proportional basket into a single asset `i` and send to receiver.
|
||||
/// @param payer who burns LP tokens
|
||||
/// @param receiver who receives the single asset
|
||||
/// @param lpAmount amount of LP tokens to burn
|
||||
/// @param i index of target asset to receive
|
||||
/// @param deadline optional deadline
|
||||
/// @return amountOutUint uint amount of asset i sent to receiver
|
||||
function burnSwap(
|
||||
address payer,
|
||||
address receiver,
|
||||
uint256 lpAmount,
|
||||
uint256 i,
|
||||
uint256 deadline
|
||||
) external nonReentrant returns (uint256 amountOutUint) {
|
||||
uint256 n = tokens.length;
|
||||
require(i < n, "burnSwap: idx");
|
||||
require(lpAmount > 0, "burnSwap: zero lp");
|
||||
require(deadline == 0 || block.timestamp <= deadline, "burnSwap: deadline");
|
||||
|
||||
uint256 supply = totalSupply();
|
||||
require(supply > 0, "burnSwap: empty supply");
|
||||
require(balanceOf(payer) >= lpAmount, "burnSwap: insufficient LP");
|
||||
|
||||
// alpha = lpAmount / supply as Q64.64
|
||||
int128 alpha = ABDKMath64x64.divu(lpAmount, supply);
|
||||
|
||||
// Use LMSR view to compute single-asset payout and burned size-metric
|
||||
(int128 payoutInternal, ) = lmsr.swapAmountsForBurn(i, alpha);
|
||||
|
||||
// Convert payoutInternal -> uint (floor) to favor pool
|
||||
amountOutUint = _internalToUintFloor(payoutInternal, bases[i]);
|
||||
require(amountOutUint > 0, "burnSwap: output zero");
|
||||
|
||||
// Transfer the payout to receiver
|
||||
_safeTransfer(tokens[i], receiver, amountOutUint);
|
||||
|
||||
// Burn LP tokens from payer (authorization via allowance)
|
||||
if (msg.sender != payer) {
|
||||
uint256 allowed = allowance(payer, msg.sender);
|
||||
require(allowed >= lpAmount, "burnSwap: allowance insufficient");
|
||||
_approve(payer, msg.sender, allowed - lpAmount);
|
||||
}
|
||||
_burn(payer, lpAmount);
|
||||
|
||||
// Update cached balances by reading on-chain balances for all tokens
|
||||
int128[] memory newQInternal = new int128[](n);
|
||||
for (uint256 idx = 0; idx < n; idx++) {
|
||||
uint256 bal = IERC20(tokens[idx]).balanceOf(address(this));
|
||||
cachedUintBalances[idx] = bal;
|
||||
newQInternal[idx] = _uintToInternalFloor(bal, bases[idx]);
|
||||
}
|
||||
|
||||
// Emit BurnSwap with public-facing info only (do not expose ΔS or LP burned)
|
||||
emit BurnSwap(payer, receiver, i, amountOutUint);
|
||||
|
||||
// If entire pool drained, deinit; else update proportionally
|
||||
bool allZero = true;
|
||||
for (uint256 idx = 0; idx < n; idx++) {
|
||||
if (newQInternal[idx] != int128(0)) { allZero = false; break; }
|
||||
}
|
||||
if (allZero) {
|
||||
lmsr.deinit();
|
||||
} else {
|
||||
lmsr.updateForProportionalChange(newQInternal);
|
||||
}
|
||||
|
||||
emit Burn(payer, receiver, new uint256[](n), lpAmount);
|
||||
return amountOutUint;
|
||||
}
|
||||
|
||||
|
||||
function computeFlashRepaymentAmounts(uint256[] memory loanAmounts) external view
|
||||
returns (uint256[] memory repaymentAmounts) {
|
||||
repaymentAmounts = new uint256[](tokens.length);
|
||||
for (uint256 i = 0; i < tokens.length; i++) {
|
||||
uint256 amount = loanAmounts[i];
|
||||
if (amount > 0) {
|
||||
repaymentAmounts[i] = amount + _ceilFee(amount, flashFeePpm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// @notice Receive token0 and/or token1 and pay it back, plus a fee, in the callback
|
||||
/// @dev The caller of this method receives a callback in the form of IPartyFlashCallback#partyFlashCallback
|
||||
/// @param recipient The address which will receive the token amounts
|
||||
/// @param amounts The amount of each token to send
|
||||
/// @param data Any data to be passed through to the callback
|
||||
function flash(
|
||||
address recipient,
|
||||
uint256[] memory amounts,
|
||||
bytes calldata data
|
||||
) external nonReentrant {
|
||||
require(recipient != address(0), "flash: zero recipient");
|
||||
require(amounts.length == tokens.length, "flash: amounts length mismatch");
|
||||
|
||||
// Calculate repayment amounts for each token including fee
|
||||
uint256[] memory repaymentAmounts = new uint256[](tokens.length);
|
||||
|
||||
// Store initial balances to verify repayment later
|
||||
uint256[] memory initialBalances = new uint256[](tokens.length);
|
||||
|
||||
// Track if any token amount is non-zero
|
||||
bool hasNonZeroAmount = false;
|
||||
|
||||
// Process each token, skipping those with zero amounts
|
||||
for (uint256 i = 0; i < tokens.length; i++) {
|
||||
uint256 amount = amounts[i];
|
||||
|
||||
if (amount > 0) {
|
||||
hasNonZeroAmount = true;
|
||||
|
||||
// Calculate repayment amount with fee (ceiling)
|
||||
repaymentAmounts[i] = amount + _ceilFee(amount, flashFeePpm);
|
||||
|
||||
// Record initial balance
|
||||
initialBalances[i] = IERC20(tokens[i]).balanceOf(address(this));
|
||||
|
||||
// Transfer token to recipient
|
||||
_safeTransfer(tokens[i], recipient, amount);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure at least one token is being borrowed
|
||||
require(hasNonZeroAmount, "flash: no tokens requested");
|
||||
|
||||
// Call flash callback with expected repayment amounts
|
||||
IPartyFlashCallback(msg.sender).partyFlashCallback(amounts, repaymentAmounts, data);
|
||||
|
||||
// Verify repayment amounts for tokens that were borrowed
|
||||
for (uint256 i = 0; i < tokens.length; i++) {
|
||||
if (amounts[i] > 0) {
|
||||
uint256 currentBalance = IERC20(tokens[i]).balanceOf(address(this));
|
||||
|
||||
// Verify repayment: current balance must be at least (initial balance + fee)
|
||||
require(
|
||||
currentBalance >= initialBalances[i] + _ceilFee(amounts[i], flashFeePpm),
|
||||
"flash: repayment failed"
|
||||
);
|
||||
|
||||
// Update cached balance
|
||||
cachedUintBalances[i] = currentBalance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* ----------------------
|
||||
Conversion helpers
|
||||
---------------------- */
|
||||
|
||||
// Convert uint token amount -> internal 64.64 (floor). Uses ABDKMath64x64.divu which truncates.
|
||||
function _uintToInternalFloor(uint256 amount, uint256 base) internal pure returns (int128) {
|
||||
// internal = amount / base (as Q64.64)
|
||||
return ABDKMath64x64.divu(amount, base);
|
||||
}
|
||||
|
||||
// Convert internal 64.64 -> uint token amount (floor). Uses ABDKMath64x64.mulu which floors the product.
|
||||
function _internalToUintFloor(int128 internalAmount, uint256 base) internal pure returns (uint256) {
|
||||
// uint = internal * base (floored)
|
||||
return ABDKMath64x64.mulu(internalAmount, base);
|
||||
}
|
||||
|
||||
// Convert internal 64.64 -> uint token amount (ceiling). Rounds up to protect the pool.
|
||||
function _internalToUintCeil(int128 internalAmount, uint256 base) internal pure returns (uint256) {
|
||||
// Get the floor value first
|
||||
uint256 floorValue = ABDKMath64x64.mulu(internalAmount, base);
|
||||
|
||||
// Check if there was any fractional part by comparing to a reconstruction of the original
|
||||
int128 reconstructed = ABDKMath64x64.divu(floorValue, base);
|
||||
|
||||
// If reconstructed is less than original, there was a fractional part that was truncated
|
||||
if (reconstructed < internalAmount) {
|
||||
return floorValue + 1;
|
||||
}
|
||||
|
||||
return floorValue;
|
||||
}
|
||||
|
||||
/* ----------------------
|
||||
ERC20 helpers (minimal)
|
||||
---------------------- */
|
||||
|
||||
function _safeTransferFrom(address token, address from, address to, uint256 amt) internal {
|
||||
IERC20(token).safeTransferFrom(from, to, amt);
|
||||
}
|
||||
|
||||
function _safeTransfer(address token, address to, uint256 amt) internal {
|
||||
IERC20(token).safeTransfer(to, amt);
|
||||
}
|
||||
|
||||
/// @notice Helper to compute size metric (sum of all asset quantities) from internal balances
|
||||
function _computeSizeMetric(int128[] memory qInternal_) private pure returns (int128) {
|
||||
int128 total = int128(0);
|
||||
for (uint i = 0; i < qInternal_.length; ) {
|
||||
total = total.add(qInternal_[i]);
|
||||
unchecked { i++; }
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
}
|
||||
24
test/Counter.t.sol
Normal file
24
test/Counter.t.sol
Normal file
@@ -0,0 +1,24 @@
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.13;
|
||||
|
||||
import {Test} from "forge-std/Test.sol";
|
||||
import {Counter} from "../src/Counter.sol";
|
||||
|
||||
contract CounterTest is Test {
|
||||
Counter public counter;
|
||||
|
||||
function setUp() public {
|
||||
counter = new Counter();
|
||||
counter.setNumber(0);
|
||||
}
|
||||
|
||||
function test_Increment() public {
|
||||
counter.increment();
|
||||
assertEq(counter.number(), 1);
|
||||
}
|
||||
|
||||
function testFuzz_SetNumber(uint256 x) public {
|
||||
counter.setNumber(x);
|
||||
assertEq(counter.number(), x);
|
||||
}
|
||||
}
|
||||
861
test/LMSRStabilized.t.sol
Normal file
861
test/LMSRStabilized.t.sol
Normal file
@@ -0,0 +1,861 @@
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import "forge-std/Test.sol";
|
||||
import "forge-std/console.sol";
|
||||
import "@openzeppelin/contracts/interfaces/IERC20Metadata.sol";
|
||||
import "../src/LMSRStabilized.sol";
|
||||
|
||||
|
||||
/// @notice Forge tests for LMSRStabilized
|
||||
contract LMSRStabilizedTest is Test {
|
||||
using LMSRStabilized for LMSRStabilized.State;
|
||||
using ABDKMath64x64 for int128;
|
||||
|
||||
LMSRStabilized.State internal s;
|
||||
|
||||
int128 stdTradeSize;
|
||||
int128 stdSlippage;
|
||||
|
||||
|
||||
function setUp() public {
|
||||
// 0.10% slippage when taking 1.00% of the assets
|
||||
stdTradeSize = ABDKMath64x64.divu(100,10_000);
|
||||
stdSlippage = ABDKMath64x64.divu(10,10_000);
|
||||
}
|
||||
|
||||
function initBalanced() internal {
|
||||
int128[] memory q = new int128[](3);
|
||||
q[0] = ABDKMath64x64.fromUInt(1_000_000);
|
||||
q[1] = ABDKMath64x64.fromUInt(1_000_000);
|
||||
q[2] = ABDKMath64x64.fromUInt(1_000_000);
|
||||
s.init(q, stdTradeSize, stdSlippage);
|
||||
}
|
||||
|
||||
function initAlmostBalanced() internal {
|
||||
int128[] memory q = new int128[](3);
|
||||
q[0] = ABDKMath64x64.fromUInt(999_999);
|
||||
q[1] = ABDKMath64x64.fromUInt(1_000_000);
|
||||
q[2] = ABDKMath64x64.fromUInt(1_000_001);
|
||||
s.init(q, stdTradeSize, stdSlippage);
|
||||
}
|
||||
|
||||
function initImbalanced() internal {
|
||||
int128[] memory q = new int128[](4);
|
||||
q[0] = ABDKMath64x64.fromUInt(1);
|
||||
q[1] = ABDKMath64x64.fromUInt(1e9);
|
||||
q[2] = ABDKMath64x64.fromUInt(1);
|
||||
q[3] = ABDKMath64x64.divu(1, 1e9);
|
||||
s.init(q, stdTradeSize, stdSlippage);
|
||||
}
|
||||
|
||||
|
||||
function testInitBalanced() public {
|
||||
// Test 1: Balanced Pool Initialization
|
||||
initBalanced();
|
||||
|
||||
// Create mock qInternal for testing
|
||||
int128[] memory mockQInternal = new int128[](3);
|
||||
mockQInternal[0] = ABDKMath64x64.fromUInt(1_000_000);
|
||||
mockQInternal[1] = ABDKMath64x64.fromUInt(1_000_000);
|
||||
mockQInternal[2] = ABDKMath64x64.fromUInt(1_000_000);
|
||||
|
||||
// Update the state's cached qInternal
|
||||
_updateCachedQInternal(mockQInternal);
|
||||
|
||||
// Verify slippage by performing asset swaps and checking price impact
|
||||
int128 tradeAmount = mockQInternal[0].mul(stdTradeSize);
|
||||
|
||||
// For a balanced pool, test asset 0 -> asset 1 swap
|
||||
(int128 amountIn, int128 amountOut) = s.swapAmountsForExactInput(0, 1, tradeAmount, 0);
|
||||
|
||||
// Verify amountIn and amountOut are reasonable
|
||||
assertTrue(amountIn > 0, "amountIn should be positive");
|
||||
assertTrue(amountOut > 0, "amountOut should be positive");
|
||||
|
||||
// Calculate slippage = (initialPrice/finalPrice - 1)
|
||||
// Compute e values dynamically for price ratio
|
||||
int128 b = _computeB(mockQInternal);
|
||||
int128[] memory eValues = _computeE(b, mockQInternal);
|
||||
|
||||
// For balanced pool, initial price ratio is 1:1
|
||||
int128 initialRatio = eValues[0].div(eValues[1]);
|
||||
|
||||
// Verify initial ratio for balanced pool is approximately 1:1
|
||||
assertTrue((initialRatio.sub(ABDKMath64x64.fromInt(1))).abs() < ABDKMath64x64.divu(1, 10000),
|
||||
"Initial price ratio should be close to 1:1");
|
||||
|
||||
// After trade, the new e values would be different
|
||||
int128 newE0 = eValues[0].mul(_exp(tradeAmount.div(b)));
|
||||
int128 slippageRatio = newE0.div(eValues[0]).div(eValues[1].div(eValues[1]));
|
||||
int128 slippage = slippageRatio.sub(ABDKMath64x64.fromInt(1));
|
||||
console2.log('slippage', slippage);
|
||||
|
||||
// Slippage should be close to stdSlippage (within 1% relative error)
|
||||
int128 relativeError = slippage.sub(stdSlippage).abs().div(stdSlippage);
|
||||
assertLt(relativeError, ABDKMath64x64.divu(1, 100), "Balanced pool slippage error too high");
|
||||
}
|
||||
|
||||
function testInitAlmostBalanced() public {
|
||||
// Test 2: Almost Balanced Pool Initialization
|
||||
initAlmostBalanced();
|
||||
|
||||
// Create mock qInternal for testing
|
||||
int128[] memory mockQInternal = new int128[](3);
|
||||
mockQInternal[0] = ABDKMath64x64.fromUInt(999_999);
|
||||
mockQInternal[1] = ABDKMath64x64.fromUInt(1_000_000);
|
||||
mockQInternal[2] = ABDKMath64x64.fromUInt(1_000_001);
|
||||
|
||||
// Update the state's cached qInternal
|
||||
_updateCachedQInternal(mockQInternal);
|
||||
|
||||
// Verify slippage for almost balanced pool
|
||||
int128 tradeAmount = mockQInternal[0].mul(stdTradeSize);
|
||||
|
||||
(int128 amountIn, int128 amountOut) = s.swapAmountsForExactInput(0, 1, tradeAmount, 0);
|
||||
|
||||
// Verify amountIn and amountOut are reasonable
|
||||
assertTrue(amountIn > 0, "amountIn should be positive");
|
||||
assertTrue(amountOut > 0, "amountOut should be positive");
|
||||
|
||||
// Compute e values dynamically for price ratio
|
||||
int128 b = _computeB(mockQInternal);
|
||||
int128[] memory eValues = _computeE(b, mockQInternal);
|
||||
int128 initialRatio = eValues[0].div(eValues[1]);
|
||||
int128 relDiff = (initialRatio.sub(ABDKMath64x64.fromInt(1))).abs();
|
||||
// Verify the initial ratio is close to but not exactly 1:1
|
||||
assertTrue(relDiff < ABDKMath64x64.divu(1, 1000),
|
||||
"Initial ratio should be close to 1:1 for almost balanced pool");
|
||||
assertTrue(relDiff > ABDKMath64x64.divu(1, 10000000),
|
||||
"Initial ratio should not be exactly 1:1 for almost balanced pool");
|
||||
|
||||
int128 newE0 = eValues[0].mul(_exp(tradeAmount.div(b)));
|
||||
int128 slippageRatio = newE0.div(eValues[0]).div(eValues[1].div(eValues[1]));
|
||||
int128 slippage = slippageRatio.sub(ABDKMath64x64.fromInt(1));
|
||||
console2.log('slippage', slippage);
|
||||
int128 relativeError = slippage.sub(stdSlippage).abs().div(stdSlippage);
|
||||
assertLt(relativeError, ABDKMath64x64.divu(1, 100), "Almost balanced pool slippage error too high");
|
||||
}
|
||||
|
||||
function testInitImbalanced() public {
|
||||
// Test 3: Imbalanced Pool Initialization
|
||||
initImbalanced();
|
||||
|
||||
// Create mock qInternal for testing
|
||||
int128[] memory mockQInternal = new int128[](4);
|
||||
mockQInternal[0] = ABDKMath64x64.fromUInt(1);
|
||||
mockQInternal[1] = ABDKMath64x64.fromUInt(1e9);
|
||||
mockQInternal[2] = ABDKMath64x64.fromUInt(1);
|
||||
mockQInternal[3] = ABDKMath64x64.divu(1, 1e9);
|
||||
|
||||
// Update the state's cached qInternal
|
||||
_updateCachedQInternal(mockQInternal);
|
||||
|
||||
// For imbalanced pool, we need to try an "average" swap
|
||||
// We'll use asset 0 -> asset 2 as it's more balanced than asset 0 -> asset 1
|
||||
int128 tradeAmount = mockQInternal[0].mul(stdTradeSize);
|
||||
|
||||
// Compute e values dynamically for price ratio
|
||||
int128 b = _computeB(mockQInternal);
|
||||
int128[] memory eValues = _computeE(b, mockQInternal);
|
||||
|
||||
// Verify the ratios between small and large assets is different
|
||||
int128 initialRatio = eValues[0].div(eValues[3]); // Assets 0 and 2 match, and assets 1 and 3 match. 0 and 3 differ.
|
||||
int128 relDiff = (initialRatio.sub(ABDKMath64x64.fromInt(1))).abs();
|
||||
// Verify initial ratio shows significant imbalance
|
||||
assertTrue(relDiff != 0, "Initial ratio should show imbalance");
|
||||
|
||||
(int128 amountIn, int128 amountOut) = s.swapAmountsForExactInput(0, 2, tradeAmount, 0);
|
||||
|
||||
// Verify amountIn and amountOut are reasonable
|
||||
assertTrue(amountIn > 0, "amountIn should be positive");
|
||||
assertTrue(amountOut > 0, "amountOut should be positive");
|
||||
|
||||
int128 newE0 = eValues[0].mul(_exp(tradeAmount.div(b)));
|
||||
int128 slippageRatio = newE0.div(eValues[0]).div(eValues[2].div(eValues[2]));
|
||||
int128 slippage = slippageRatio.sub(ABDKMath64x64.fromInt(1));
|
||||
console2.log('slippage', slippage);
|
||||
|
||||
// Since the imbalance is extreme, with one coin worth lots more than the others, the actual slippage for
|
||||
// this swap is actually off by about 100%
|
||||
// When we configure kappa, it is a best case slippage (worst case AMM loss) that only occurs with balanced
|
||||
// assets
|
||||
int128 relativeError = slippage.sub(stdSlippage).abs().div(stdSlippage);
|
||||
console2.log('relative error', relativeError);
|
||||
assertLt(relativeError, ABDKMath64x64.divu(100, 100), "Imbalanced pool slippage error too high");
|
||||
}
|
||||
|
||||
function testRecentering() public {
|
||||
// Recentering functionality has been removed since we no longer cache intermediate values
|
||||
// This test is now a no-op but kept for API compatibility
|
||||
initAlmostBalanced();
|
||||
|
||||
// Verify basic state is still functional
|
||||
assertTrue(s.nAssets > 0, "State should still be initialized");
|
||||
assertTrue(s.kappa > int128(0), "Kappa should still be positive");
|
||||
}
|
||||
|
||||
function testRescalingAfterDeposit() public {
|
||||
// Initialize pool with almost balanced assets
|
||||
initAlmostBalanced();
|
||||
|
||||
// Create initial asset quantities
|
||||
int128[] memory initialQ = new int128[](3);
|
||||
initialQ[0] = ABDKMath64x64.fromUInt(999_999);
|
||||
initialQ[1] = ABDKMath64x64.fromUInt(1_000_000);
|
||||
initialQ[2] = ABDKMath64x64.fromUInt(1_000_001);
|
||||
|
||||
// Update the state's cached qInternal
|
||||
_updateCachedQInternal(initialQ);
|
||||
|
||||
// Store initial parameters
|
||||
int128 initialB = _computeB(initialQ);
|
||||
int128 initialKappa = s.kappa;
|
||||
|
||||
// Simulate a deposit by increasing all asset quantities by 50%
|
||||
int128[] memory newQ = new int128[](s.nAssets);
|
||||
for (uint i = 0; i < s.nAssets; i++) {
|
||||
// Increase by 50%
|
||||
newQ[i] = initialQ[i].mul(ABDKMath64x64.fromUInt(3).div(ABDKMath64x64.fromUInt(2))); // 1.5x
|
||||
}
|
||||
|
||||
// Apply the update for proportional change
|
||||
s.updateForProportionalChange(newQ);
|
||||
|
||||
// Verify that b has been rescaled proportionally
|
||||
int128 newB = _computeB(s.qInternal);
|
||||
int128 expectedRatio = ABDKMath64x64.fromUInt(3).div(ABDKMath64x64.fromUInt(2)); // 1.5x
|
||||
int128 actualRatio = newB.div(initialB);
|
||||
|
||||
int128 tolerance = ABDKMath64x64.divu(1, 1000); // 0.1% tolerance
|
||||
assertTrue((actualRatio.sub(expectedRatio)).abs() < tolerance, "b did not scale proportionally after deposit");
|
||||
|
||||
// Verify kappa remained unchanged
|
||||
assertTrue((s.kappa.sub(initialKappa)).abs() < tolerance, "kappa should not change after deposit");
|
||||
|
||||
// Verify slippage target is still met by performing a trade
|
||||
int128 tradeAmount = s.qInternal[0].mul(stdTradeSize);
|
||||
(int128 amountIn, int128 amountOut) = s.swapAmountsForExactInput(0, 1, tradeAmount, 0);
|
||||
|
||||
// Verify computed swap amounts
|
||||
assertTrue(amountIn > 0, "Swap amountIn should be positive");
|
||||
assertTrue(amountOut > 0, "Swap amountOut should be positive");
|
||||
// Verify amountOut is reasonable compared to amountIn (not a severe loss)
|
||||
assertTrue(amountOut.div(amountIn) > ABDKMath64x64.divu(9, 10), "Swap should not incur severe loss");
|
||||
|
||||
int128[] memory eValues = _computeE(newB, s.qInternal);
|
||||
int128 newE0 = eValues[0].mul(_exp(tradeAmount.div(newB)));
|
||||
int128 slippageRatio = newE0.div(eValues[0]).div(eValues[1].div(eValues[1]));
|
||||
int128 slippage = slippageRatio.sub(ABDKMath64x64.fromInt(1));
|
||||
console2.log('post-deposit slippage', slippage);
|
||||
|
||||
int128 relativeError = slippage.sub(stdSlippage).abs().div(stdSlippage);
|
||||
assertLt(relativeError, ABDKMath64x64.divu(1, 100), "Slippage target not met after deposit");
|
||||
}
|
||||
|
||||
function testRescalingAfterWithdrawal() public {
|
||||
// Initialize pool with almost balanced assets
|
||||
initAlmostBalanced();
|
||||
|
||||
// Create initial asset quantities
|
||||
int128[] memory initialQ = new int128[](3);
|
||||
initialQ[0] = ABDKMath64x64.fromUInt(999_999);
|
||||
initialQ[1] = ABDKMath64x64.fromUInt(1_000_000);
|
||||
initialQ[2] = ABDKMath64x64.fromUInt(1_000_001);
|
||||
|
||||
// Update the state's cached qInternal
|
||||
_updateCachedQInternal(initialQ);
|
||||
|
||||
// Store initial parameters
|
||||
int128 initialB = _computeB(initialQ);
|
||||
int128 initialKappa = s.kappa;
|
||||
|
||||
// Simulate a withdrawal by decreasing all asset quantities by 30%
|
||||
int128[] memory newQ = new int128[](s.nAssets);
|
||||
for (uint i = 0; i < s.nAssets; i++) {
|
||||
// Decrease by 30%
|
||||
newQ[i] = initialQ[i].mul(ABDKMath64x64.fromUInt(7).div(ABDKMath64x64.fromUInt(10))); // 0.7x
|
||||
}
|
||||
|
||||
// Apply the update for proportional change
|
||||
s.updateForProportionalChange(newQ);
|
||||
|
||||
// Verify that b has been rescaled proportionally
|
||||
int128 newB = _computeB(s.qInternal);
|
||||
int128 expectedRatio = ABDKMath64x64.fromUInt(7).div(ABDKMath64x64.fromUInt(10)); // 0.7x
|
||||
int128 actualRatio = newB.div(initialB);
|
||||
|
||||
int128 tolerance = ABDKMath64x64.divu(1, 1000); // 0.1% tolerance
|
||||
assertTrue((actualRatio.sub(expectedRatio)).abs() < tolerance, "b did not scale proportionally after withdrawal");
|
||||
|
||||
// Verify kappa remained unchanged
|
||||
assertTrue((s.kappa.sub(initialKappa)).abs() < tolerance, "kappa should not change after withdrawal");
|
||||
|
||||
// Verify slippage target is still met by performing a trade
|
||||
int128 tradeAmount = s.qInternal[0].mul(stdTradeSize);
|
||||
(int128 amountIn, int128 amountOut) = s.swapAmountsForExactInput(0, 1, tradeAmount, 0);
|
||||
|
||||
// Verify computed swap amounts
|
||||
assertTrue(amountIn > 0, "Swap amountIn should be positive");
|
||||
assertTrue(amountOut > 0, "Swap amountOut should be positive");
|
||||
// Verify amountOut is reasonable compared to amountIn (not a severe loss)
|
||||
assertTrue(amountOut.div(amountIn) > ABDKMath64x64.divu(9, 10), "Swap should not incur severe loss");
|
||||
|
||||
int128[] memory eValues = _computeE(newB, s.qInternal);
|
||||
int128 newE0 = eValues[0].mul(_exp(tradeAmount.div(newB)));
|
||||
int128 slippageRatio = newE0.div(eValues[0]).div(eValues[1].div(eValues[1]));
|
||||
int128 slippage = slippageRatio.sub(ABDKMath64x64.fromInt(1));
|
||||
console2.log('post-withdrawal slippage', slippage);
|
||||
|
||||
int128 relativeError = slippage.sub(stdSlippage).abs().div(stdSlippage);
|
||||
assertLt(relativeError, ABDKMath64x64.divu(1, 100), "Slippage target not met after withdrawal");
|
||||
}
|
||||
|
||||
// --- tests probing numerical stability and boundary conditions ---
|
||||
|
||||
/// @notice Recentering functionality has been removed - this test is now a no-op
|
||||
function testRecenterShiftTooLargeReverts() public {
|
||||
initAlmostBalanced();
|
||||
// Recentering has been removed, so this test now just verifies basic functionality
|
||||
assertTrue(s.nAssets > 0, "State should still be initialized");
|
||||
}
|
||||
|
||||
/// @notice limitPrice <= current price should revert (no partial fill)
|
||||
function testLimitPriceRevertWhenAtOrBelowCurrent() public {
|
||||
initBalanced();
|
||||
|
||||
// Create mock qInternal for testing
|
||||
int128[] memory mockQInternal = new int128[](3);
|
||||
mockQInternal[0] = ABDKMath64x64.fromUInt(1_000_000);
|
||||
mockQInternal[1] = ABDKMath64x64.fromUInt(1_000_000);
|
||||
mockQInternal[2] = ABDKMath64x64.fromUInt(1_000_000);
|
||||
|
||||
// Update the state's cached qInternal
|
||||
_updateCachedQInternal(mockQInternal);
|
||||
|
||||
// For balanced pool r0 = 1. Use limitPrice == 1 which should revert.
|
||||
int128 tradeAmount = mockQInternal[0].mul(stdTradeSize);
|
||||
|
||||
vm.expectRevert(bytes("LMSR: limitPrice <= current price"));
|
||||
this.externalSwapAmountsForExactInput(0, 1, tradeAmount, ABDKMath64x64.fromInt(1));
|
||||
}
|
||||
|
||||
/// @notice If e_j == 0 we should revert early to avoid div-by-zero
|
||||
function testEJZeroReverts() public {
|
||||
initBalanced();
|
||||
|
||||
// Create mock qInternal where asset 1 has zero quantity
|
||||
int128[] memory mockQInternal = new int128[](3);
|
||||
mockQInternal[0] = ABDKMath64x64.fromUInt(1_000_000);
|
||||
mockQInternal[1] = int128(0); // Zero quantity for asset 1
|
||||
mockQInternal[2] = ABDKMath64x64.fromUInt(1_000_000);
|
||||
|
||||
// Update the state's cached qInternal
|
||||
_updateCachedQInternal(mockQInternal);
|
||||
|
||||
int128 tradeAmount = mockQInternal[0].mul(stdTradeSize);
|
||||
|
||||
vm.expectRevert(bytes("LMSR: e_j==0"));
|
||||
this.externalSwapAmountsForExactInput(0, 1, tradeAmount, 0);
|
||||
}
|
||||
|
||||
/// @notice swapAmountsForPriceLimit returns zero if limit equals current price
|
||||
function testSwapAmountsForPriceLimitZeroWhenLimitEqualsPrice() public {
|
||||
initBalanced();
|
||||
|
||||
// Create mock qInternal for testing
|
||||
int128[] memory mockQInternal = new int128[](3);
|
||||
mockQInternal[0] = ABDKMath64x64.fromUInt(1_000_000);
|
||||
mockQInternal[1] = ABDKMath64x64.fromUInt(1_000_000);
|
||||
mockQInternal[2] = ABDKMath64x64.fromUInt(1_000_000);
|
||||
|
||||
// Update the state's cached qInternal
|
||||
_updateCachedQInternal(mockQInternal);
|
||||
|
||||
// For balanced pool r0 = 1. swapAmountsForPriceLimit with limit==1 should be zero
|
||||
vm.expectRevert('LMSR: limitPrice <= current price');
|
||||
this.externalSwapAmountsForPriceLimit(0, 1, ABDKMath64x64.fromInt(1));
|
||||
|
||||
// Try with a limit price slightly above 1, which should not revert
|
||||
try this.externalSwapAmountsForPriceLimit(0, 1, ABDKMath64x64.fromInt(1).add(ABDKMath64x64.divu(1, 1000))) returns (int128 _amountIn, int128 _maxOut) {
|
||||
// Verify that the returned values are reasonable
|
||||
assertTrue(_amountIn > 0, "amountIn should be positive for valid limit price");
|
||||
assertTrue(_maxOut > 0, "maxOut should be positive for valid limit price");
|
||||
} catch {
|
||||
fail("Should not revert with limit price > current price");
|
||||
}
|
||||
}
|
||||
|
||||
function externalSwapAmountsForPriceLimit(uint256 i, uint256 j, int128 limitPrice) external view
|
||||
returns (int128, int128) {
|
||||
return s.swapAmountsForPriceLimit(i, j, limitPrice);
|
||||
}
|
||||
|
||||
/// @notice Gas/throughput test: perform 100 alternating swaps between asset 0 and 1
|
||||
function testSwapGas() public {
|
||||
// Initialize the almost-balanced pool
|
||||
initAlmostBalanced();
|
||||
|
||||
// Create mock qInternal that we'll update through swaps
|
||||
int128[] memory currentQ = new int128[](3);
|
||||
currentQ[0] = ABDKMath64x64.fromUInt(999_999);
|
||||
currentQ[1] = ABDKMath64x64.fromUInt(1_000_000);
|
||||
currentQ[2] = ABDKMath64x64.fromUInt(1_000_001);
|
||||
|
||||
// Update the state's cached qInternal
|
||||
_updateCachedQInternal(currentQ);
|
||||
|
||||
// Perform 100 swaps, alternating between asset 0 -> 1 and 1 -> 0
|
||||
for (uint256 iter = 0; iter < 100; iter++) {
|
||||
uint256 from = (iter % 2 == 0) ? 0 : 1;
|
||||
uint256 to = (from == 0) ? 1 : 0;
|
||||
|
||||
// Use standard trade size applied to the 'from' asset's current quantity
|
||||
int128 tradeAmount = s.qInternal[from].mul(stdTradeSize);
|
||||
|
||||
// Compute swap amounts and apply to state
|
||||
(int128 amountIn, int128 amountOut) = s.swapAmountsForExactInput(from, to, tradeAmount, 0);
|
||||
|
||||
// applySwap now updates the internal qInternal directly
|
||||
s.applySwap(from, to, amountIn, amountOut);
|
||||
}
|
||||
}
|
||||
|
||||
/// @notice Extremely large a that makes a/b exceed expLimit should revert
|
||||
function testAmountOutABOverflowReverts() public {
|
||||
initBalanced();
|
||||
|
||||
// Create mock qInternal for testing
|
||||
int128[] memory mockQInternal = new int128[](3);
|
||||
mockQInternal[0] = ABDKMath64x64.fromUInt(1_000_000);
|
||||
mockQInternal[1] = ABDKMath64x64.fromUInt(1_000_000);
|
||||
mockQInternal[2] = ABDKMath64x64.fromUInt(1_000_000);
|
||||
|
||||
// Update the state's cached qInternal
|
||||
_updateCachedQInternal(mockQInternal);
|
||||
|
||||
int128 b = _computeB(mockQInternal);
|
||||
// Pick a such that a/b = 33 (expLimit is 32). a = b * 33
|
||||
int128 aOverB_target = ABDKMath64x64.fromInt(33);
|
||||
int128 a = b.mul(aOverB_target);
|
||||
|
||||
vm.expectRevert(bytes("LMSR: a/b too large (would overflow exp)"));
|
||||
this.externalSwapAmountsForExactInput(0, 1, a, 0);
|
||||
}
|
||||
|
||||
// Helper function to compute b from qInternal (either from provided array or state)
|
||||
function _computeB(int128[] memory qInternal) internal view returns (int128) {
|
||||
int128 sizeMetric = _computeSizeMetric(qInternal);
|
||||
require(sizeMetric > int128(0), "LMSR: size metric zero");
|
||||
return s.kappa.mul(sizeMetric);
|
||||
}
|
||||
|
||||
// Overload that uses state's cached qInternal
|
||||
function _computeB() internal view returns (int128) {
|
||||
int128 sizeMetric = _computeSizeMetric(s.qInternal);
|
||||
require(sizeMetric > int128(0), "LMSR: size metric zero");
|
||||
return s.kappa.mul(sizeMetric);
|
||||
}
|
||||
|
||||
// Helper function to compute size metric (sum of all asset quantities)
|
||||
function _computeSizeMetric(int128[] memory qInternal) internal pure returns (int128) {
|
||||
int128 total = int128(0);
|
||||
for (uint i = 0; i < qInternal.length; ) {
|
||||
total = total.add(qInternal[i]);
|
||||
unchecked { i++; }
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
// Helper function to update the state's cached qInternal
|
||||
function _updateCachedQInternal(int128[] memory mockQInternal) internal {
|
||||
// First ensure qInternal array exists with the right size
|
||||
if (s.qInternal.length != mockQInternal.length) {
|
||||
s.qInternal = new int128[](mockQInternal.length);
|
||||
}
|
||||
|
||||
// Copy values from mockQInternal to state's qInternal
|
||||
for (uint i = 0; i < mockQInternal.length; ) {
|
||||
s.qInternal[i] = mockQInternal[i];
|
||||
unchecked { i++; }
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to compute M and Z dynamically
|
||||
function _computeMAndZ(int128 b, int128[] memory qInternal) internal pure returns (int128 M, int128 Z) {
|
||||
require(qInternal.length > 0, "LMSR: no assets");
|
||||
|
||||
// Compute y_i = q_i / b for numerical stability
|
||||
int128[] memory y = new int128[](qInternal.length);
|
||||
for (uint i = 0; i < qInternal.length; ) {
|
||||
y[i] = qInternal[i].div(b);
|
||||
unchecked { i++; }
|
||||
}
|
||||
|
||||
// Find max y for centering (M = maxY)
|
||||
M = y[0];
|
||||
for (uint i = 1; i < qInternal.length; ) {
|
||||
if (y[i] > M) M = y[i];
|
||||
unchecked { i++; }
|
||||
}
|
||||
|
||||
// Compute Z = sum of exp(z_i) where z_i = y_i - M
|
||||
Z = int128(0);
|
||||
for (uint i = 0; i < qInternal.length; ) {
|
||||
int128 z_i = y[i].sub(M);
|
||||
int128 e_i = _exp(z_i);
|
||||
Z = Z.add(e_i);
|
||||
unchecked { i++; }
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to compute all e[i] = exp(z[i]) values dynamically
|
||||
function _computeE(int128 b, int128[] memory qInternal) internal pure returns (int128[] memory e) {
|
||||
(int128 M, ) = _computeMAndZ(b, qInternal);
|
||||
e = new int128[](qInternal.length);
|
||||
|
||||
for (uint i = 0; i < qInternal.length; ) {
|
||||
int128 y_i = qInternal[i].div(b);
|
||||
int128 z_i = y_i.sub(M);
|
||||
e[i] = _exp(z_i);
|
||||
unchecked { i++; }
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to calculate exp (copied from LMSRStabilized library)
|
||||
function _exp(int128 x) internal pure returns (int128) {
|
||||
return ABDKMath64x64.exp(x);
|
||||
}
|
||||
|
||||
// External helper function that wraps swapAmountsForExactInput to properly handle reverts in tests
|
||||
function externalSwapAmountsForExactInput(
|
||||
uint i,
|
||||
uint j,
|
||||
int128 a,
|
||||
int128 limitPrice
|
||||
) external view returns (int128 amountIn, int128 amountOut) {
|
||||
return s.swapAmountsForExactInput(i, j, a, limitPrice);
|
||||
}
|
||||
|
||||
// External helper function that wraps recenterIfNeeded to properly handle reverts in tests
|
||||
function externalRecenterIfNeeded() external {
|
||||
// Recentering has been removed - this is now a no-op
|
||||
}
|
||||
|
||||
// External helper function that wraps applySwap to properly handle reverts in tests
|
||||
function externalApplySwap(
|
||||
uint i,
|
||||
uint j,
|
||||
int128 amountIn,
|
||||
int128 amountOut
|
||||
) external {
|
||||
s.applySwap(i, j, amountIn, amountOut);
|
||||
}
|
||||
|
||||
// Small helper: convert a Q64.64 int128 into micro-units (value * 1e6) as an int256 for readable logging.
|
||||
// Example: if x represents 0.001 (Q64.64), _toMicro(x) will return ~1000.
|
||||
function _toMicro(int128 x) internal pure returns (int256) {
|
||||
int256 ONE = int256(uint256(0x10000000000000000)); // 2^64
|
||||
return (int256(x) * 1_000_000) / ONE;
|
||||
}
|
||||
|
||||
/// @notice Test that applySwap correctly validates swap parameters and updates qInternal
|
||||
function testApplySwap() public {
|
||||
// Initialize with balanced assets
|
||||
initBalanced();
|
||||
|
||||
// Create mock qInternal for testing
|
||||
int128[] memory mockQInternal = new int128[](3);
|
||||
mockQInternal[0] = ABDKMath64x64.fromUInt(1_000_000);
|
||||
mockQInternal[1] = ABDKMath64x64.fromUInt(1_000_000);
|
||||
mockQInternal[2] = ABDKMath64x64.fromUInt(1_000_000);
|
||||
|
||||
// Update the state's cached qInternal
|
||||
_updateCachedQInternal(mockQInternal);
|
||||
|
||||
// Save original values for comparison
|
||||
int128 originalQ0 = s.qInternal[0];
|
||||
int128 originalQ1 = s.qInternal[1];
|
||||
|
||||
// Calculate swap amounts from asset 0 to asset 1
|
||||
int128 tradeAmount = mockQInternal[0].mul(stdTradeSize);
|
||||
|
||||
(int128 amountIn, int128 amountOut) = s.swapAmountsForExactInput(0, 1, tradeAmount, 0);
|
||||
|
||||
// Verify basic swap calculation worked
|
||||
assertTrue(amountIn > 0, "amountIn should be positive");
|
||||
assertTrue(amountOut > 0, "amountOut should be positive");
|
||||
|
||||
// Apply the swap - should not revert for valid inputs
|
||||
s.applySwap(0, 1, amountIn, amountOut);
|
||||
|
||||
// Verify qInternal is correctly updated
|
||||
// Input asset should increase by amountIn
|
||||
assertEq(s.qInternal[0], originalQ0.add(amountIn), "qInternal[0] should be updated");
|
||||
// Output asset should decrease by amountOut
|
||||
assertEq(s.qInternal[1], originalQ1.sub(amountOut), "qInternal[1] should be updated");
|
||||
}
|
||||
|
||||
/// @notice Test path independence by comparing direct vs indirect swaps
|
||||
function testPathIndependence() public {
|
||||
// Start with a balanced pool
|
||||
initBalanced();
|
||||
|
||||
// Create initial quantities
|
||||
int128[] memory initialQValues = new int128[](s.nAssets);
|
||||
initialQValues[0] = ABDKMath64x64.fromUInt(1_000_000);
|
||||
initialQValues[1] = ABDKMath64x64.fromUInt(1_000_000);
|
||||
initialQValues[2] = ABDKMath64x64.fromUInt(1_000_000);
|
||||
|
||||
// Update the state's cached qInternal
|
||||
_updateCachedQInternal(initialQValues);
|
||||
|
||||
// Test path independence by computing swap outcomes without state changes
|
||||
int128 directSwapAmount = initialQValues[0].mul(stdTradeSize);
|
||||
|
||||
// Store a backup of the original values to restore between swaps
|
||||
int128[] memory backupQ = new int128[](s.nAssets);
|
||||
for (uint i = 0; i < s.nAssets; i++) {
|
||||
backupQ[i] = s.qInternal[i];
|
||||
}
|
||||
|
||||
// Path 1: Direct swap from asset 0 to asset 2
|
||||
(int128 directAmountIn, int128 directAmountOut) = s.swapAmountsForExactInput(0, 2, directSwapAmount, 0);
|
||||
|
||||
// Restore original state for second path
|
||||
_updateCachedQInternal(backupQ);
|
||||
|
||||
// Path 2: Swap from asset 0 to asset 1, then from asset 1 to asset 2
|
||||
(int128 indirectAmountIn1, int128 indirectAmountOut1) = s.swapAmountsForExactInput(0, 1, directSwapAmount, 0);
|
||||
|
||||
// Update state for second leg of indirect path
|
||||
s.qInternal[0] = s.qInternal[0].sub(indirectAmountIn1);
|
||||
s.qInternal[1] = s.qInternal[1].add(indirectAmountOut1);
|
||||
|
||||
// Second swap: asset 1 -> asset 2
|
||||
(int128 indirectAmountIn2, int128 indirectAmountOut2) = s.swapAmountsForExactInput(1, 2, indirectAmountOut1, 0);
|
||||
|
||||
// The path independence property isn't perfect due to discrete swap mechanics,
|
||||
// but the difference should be within reasonable bounds
|
||||
console2.log("Direct swap output:");
|
||||
console2.logInt(directAmountOut);
|
||||
console2.log("Indirect swap total output:");
|
||||
console2.logInt(indirectAmountOut2);
|
||||
|
||||
// Basic verification that both paths produce positive outputs
|
||||
assertTrue(directAmountOut > 0, "Direct swap should produce positive output");
|
||||
assertTrue(indirectAmountOut2 > 0, "Indirect swap should produce positive output");
|
||||
}
|
||||
|
||||
/// @notice Test round-trip trades to verify near-zero slippage
|
||||
function testRoundTripTradesAcrossAllPools() public {
|
||||
// Test with balanced pool only since we removed state caching
|
||||
initBalanced();
|
||||
|
||||
// Create mock qInternal
|
||||
int128[] memory initialQ = new int128[](3);
|
||||
initialQ[0] = ABDKMath64x64.fromUInt(1_000_000);
|
||||
initialQ[1] = ABDKMath64x64.fromUInt(1_000_000);
|
||||
initialQ[2] = ABDKMath64x64.fromUInt(1_000_000);
|
||||
|
||||
// Update the state's cached qInternal
|
||||
_updateCachedQInternal(initialQ);
|
||||
|
||||
console2.log("Testing round-trip trades for balanced pool");
|
||||
|
||||
// Use standard trade size
|
||||
int128 tradeAmount = s.qInternal[0].mul(stdTradeSize);
|
||||
|
||||
// Step 1: Swap asset 0 -> asset 1
|
||||
(int128 amountIn1, int128 amountOut1) = s.swapAmountsForExactInput(0, 1, tradeAmount, 0);
|
||||
|
||||
// Update quantities for step 2
|
||||
s.qInternal[0] = s.qInternal[0].sub(amountIn1);
|
||||
s.qInternal[1] = s.qInternal[1].add(amountOut1);
|
||||
|
||||
// Step 2: Swap back asset 1 -> asset 0
|
||||
(int128 amountIn2, int128 amountOut2) = s.swapAmountsForExactInput(1, 0, amountOut1, 0);
|
||||
|
||||
// Calculate round-trip slippage: (initial amount - final amount) / initial amount
|
||||
int128 roundTripSlippage = (amountIn1.sub(amountOut2)).div(amountIn1);
|
||||
|
||||
console2.log("Round-trip slippage (micro-units):");
|
||||
console2.logInt(_toMicro(roundTripSlippage));
|
||||
|
||||
// Verify round-trip slippage is reasonable
|
||||
int128 tolerance = ABDKMath64x64.divu(1, 100000); // 0.001% tolerance
|
||||
assertLt(roundTripSlippage.abs(), tolerance, "Round-trip slippage should be near zero");
|
||||
}
|
||||
|
||||
/// @notice Test that slippage is approximately equal in both directions for small swaps
|
||||
function testBidirectionalSlippageSymmetry() public {
|
||||
// Initialize with balanced assets for clearest slippage measurement
|
||||
initBalanced();
|
||||
|
||||
// Create mock qInternal
|
||||
int128[] memory initialQ = new int128[](3);
|
||||
initialQ[0] = ABDKMath64x64.fromUInt(1_000_000);
|
||||
initialQ[1] = ABDKMath64x64.fromUInt(1_000_000);
|
||||
initialQ[2] = ABDKMath64x64.fromUInt(1_000_000);
|
||||
|
||||
// Update the state's cached qInternal
|
||||
_updateCachedQInternal(initialQ);
|
||||
|
||||
// Use small trade size for clear slippage measurement
|
||||
int128 tradeSize = ABDKMath64x64.divu(5, 10_000); // 0.05% of pool
|
||||
int128 tradeAmount0 = s.qInternal[0].mul(tradeSize);
|
||||
int128 tradeAmount1 = s.qInternal[1].mul(tradeSize);
|
||||
|
||||
// Store original state to restore between tests
|
||||
int128[] memory backupQ = new int128[](s.nAssets);
|
||||
for (uint i = 0; i < s.nAssets; i++) {
|
||||
backupQ[i] = s.qInternal[i];
|
||||
}
|
||||
|
||||
// First direction: asset 0 -> asset 1
|
||||
(int128 amountIn0to1, int128 amountOut0to1) = s.swapAmountsForExactInput(0, 1, tradeAmount0, 0);
|
||||
|
||||
// Restore original state
|
||||
_updateCachedQInternal(backupQ);
|
||||
|
||||
// Second direction: asset 1 -> asset 0
|
||||
(int128 amountIn1to0, int128 amountOut1to0) = s.swapAmountsForExactInput(1, 0, tradeAmount1, 0);
|
||||
|
||||
console2.log("0->1 swap amountIn:");
|
||||
console2.logInt(amountIn0to1);
|
||||
console2.log("0->1 swap amountOut:");
|
||||
console2.logInt(amountOut0to1);
|
||||
console2.log("1->0 swap amountIn:");
|
||||
console2.logInt(amountIn1to0);
|
||||
console2.log("1->0 swap amountOut:");
|
||||
console2.logInt(amountOut1to0);
|
||||
|
||||
// For balanced pools, the swap ratios should be approximately symmetric
|
||||
int128 ratio0to1 = amountOut0to1.div(amountIn0to1);
|
||||
int128 ratio1to0 = amountOut1to0.div(amountIn1to0);
|
||||
|
||||
// Calculate relative difference between the ratios
|
||||
int128 ratioDifference = (ratio0to1.sub(ratio1to0)).abs();
|
||||
int128 relativeRatioDiff = ratioDifference.div(ratio0to1.add(ratio1to0).div(ABDKMath64x64.fromInt(2)));
|
||||
|
||||
console2.log("Relative ratio difference (micro-units):");
|
||||
console2.logInt(_toMicro(relativeRatioDiff));
|
||||
|
||||
// Assert that the relative difference between ratios is small
|
||||
int128 tolerance = ABDKMath64x64.divu(5, 100); // 5% tolerance
|
||||
assertLt(relativeRatioDiff, tolerance,
|
||||
"Swap ratios should be approximately equal in both directions");
|
||||
}
|
||||
|
||||
/// @notice Test that basic swap functionality works across multiple operations
|
||||
function testZConsistencyAfterMultipleSwaps() public {
|
||||
// Initialize with balanced assets
|
||||
initBalanced();
|
||||
|
||||
// Create mock qInternal that we'll update through swaps
|
||||
int128[] memory initialQ = new int128[](3);
|
||||
initialQ[0] = ABDKMath64x64.fromUInt(1_000_000);
|
||||
initialQ[1] = ABDKMath64x64.fromUInt(1_000_000);
|
||||
initialQ[2] = ABDKMath64x64.fromUInt(1_000_000);
|
||||
|
||||
// Update the state's cached qInternal
|
||||
_updateCachedQInternal(initialQ);
|
||||
|
||||
// Perform multiple swaps in different directions
|
||||
for (uint i = 0; i < 5; i++) {
|
||||
// Swap from asset i%3 to asset (i+1)%3
|
||||
uint from = i % 3;
|
||||
uint to = (i + 1) % 3;
|
||||
|
||||
int128 tradeAmount = s.qInternal[from].mul(stdTradeSize);
|
||||
|
||||
(int128 amountIn, int128 amountOut) = s.swapAmountsForExactInput(from, to, tradeAmount, 0);
|
||||
|
||||
// Apply swap to update internal state
|
||||
s.applySwap(from, to, amountIn, amountOut);
|
||||
|
||||
// Basic validation that swap worked
|
||||
assertTrue(amountIn > 0, "amountIn should be positive");
|
||||
assertTrue(amountOut > 0, "amountOut should be positive");
|
||||
}
|
||||
}
|
||||
|
||||
// --- New tests for single-token mint/burn helpers ---
|
||||
|
||||
/// @notice Basic sanity check for swapAmountsForMint: small single-token input
|
||||
function testSwapAmountsForMintBasic() public {
|
||||
initBalanced();
|
||||
|
||||
// Use a small single-token input (stdTradeSize fraction of asset 0)
|
||||
int128 a = s.qInternal[0].mul(stdTradeSize);
|
||||
|
||||
(int128 consumed, int128 lpIncrease) = s.swapAmountsForMint(0, a);
|
||||
|
||||
// consumed must be non-negative and <= provided a (partial-fill allowed)
|
||||
assertTrue(consumed > 0, "consumed should be positive");
|
||||
assertTrue(consumed <= a, "consumed should not exceed provided input");
|
||||
|
||||
// lpIncrease should be positive
|
||||
assertTrue(lpIncrease > 0, "lpIncrease should be positive");
|
||||
}
|
||||
|
||||
/// @notice Large input for swapAmountsForMint should return a valid partial fill (consumed <= provided)
|
||||
function testSwapAmountsForMintLargeInputPartial() public {
|
||||
initAlmostBalanced();
|
||||
|
||||
// Provide a large input far above stdTradeSize to exercise cap logic
|
||||
int128 a = s.qInternal[0].mul(ABDKMath64x64.fromUInt(1000)); // 1000x one-asset quantity
|
||||
|
||||
(int128 consumed, int128 lpIncrease) = s.swapAmountsForMint(0, a);
|
||||
|
||||
// Should not consume more than provided
|
||||
assertTrue(consumed <= a, "consumed must be <= provided");
|
||||
|
||||
// If nothing could be consumed, the helper should revert earlier; otherwise positive
|
||||
assertTrue(consumed > 0, "consumed should be positive for large input in normal pools");
|
||||
assertTrue(lpIncrease > 0, "lpIncrease should be positive for large input");
|
||||
}
|
||||
|
||||
/// @notice Basic swapAmountsForBurn sanity: small alpha should return positive single-asset payout
|
||||
function testSwapAmountsForBurnBasic() public {
|
||||
initBalanced();
|
||||
|
||||
// Burn alpha fraction of pool
|
||||
int128 alpha = ABDKMath64x64.divu(1, 100); // 1%
|
||||
int128 S = _computeSizeMetric(s.qInternal);
|
||||
|
||||
(int128 payout, int128 burned) = s.swapAmountsForBurn(0, alpha);
|
||||
|
||||
// burned should equal alpha * S
|
||||
assertEq(burned, alpha.mul(S), "burned size-metric mismatch");
|
||||
|
||||
// payout should be positive
|
||||
assertTrue(payout > 0, "payout must be positive for balanced pool burn");
|
||||
}
|
||||
|
||||
/// @notice If some assets have zero quantity, burn should skip them but still return payout when possible
|
||||
function testSwapAmountsForBurnWithZeroAsset() public {
|
||||
initBalanced();
|
||||
|
||||
// Make asset 1 empty; others non-zero
|
||||
int128[] memory mockQInternal = new int128[](3);
|
||||
mockQInternal[0] = ABDKMath64x64.fromUInt(1_000_000);
|
||||
mockQInternal[1] = int128(0); // zero
|
||||
mockQInternal[2] = ABDKMath64x64.fromUInt(1_000_000);
|
||||
_updateCachedQInternal(mockQInternal);
|
||||
|
||||
int128 alpha = ABDKMath64x64.divu(1, 100); // 1%
|
||||
(int128 payout, int128 burned) = s.swapAmountsForBurn(0, alpha);
|
||||
|
||||
// Should still burn the size metric
|
||||
int128 S = _computeSizeMetric(mockQInternal);
|
||||
assertEq(burned, alpha.mul(S), "burned size-metric mismatch with zero asset present");
|
||||
|
||||
// Payout should be at least the direct redeemed portion (alpha * q_i)
|
||||
assertTrue(payout >= alpha.mul(mockQInternal[0]), "payout should be >= direct redeemed portion");
|
||||
|
||||
// Payout must be positive
|
||||
assertTrue(payout > 0, "payout must be positive even when one asset is zero");
|
||||
}
|
||||
|
||||
}
|
||||
14
test/MockERC20.sol
Normal file
14
test/MockERC20.sol
Normal file
@@ -0,0 +1,14 @@
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||
|
||||
contract MockERC20 is ERC20 {
|
||||
uint8 private immutable _decimals;
|
||||
|
||||
constructor(string memory name, string memory symbol, uint8 decimals_) ERC20(name, symbol) {_decimals = decimals_;}
|
||||
|
||||
function decimals() public view virtual override returns (uint8) {return _decimals;}
|
||||
function mint(address account, uint256 amount) external {_mint(account, amount);}
|
||||
function burn(address account, uint256 amount) external {_burn(account, amount);}
|
||||
}
|
||||
1423
test/PartyPool.t.sol
Normal file
1423
test/PartyPool.t.sol
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user