Compare commits
5 Commits
6edad6e510
...
43fb62c47c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43fb62c47c | ||
|
|
a43c893609 | ||
|
|
e5b2577ba9 | ||
|
|
8e69bfac5c | ||
|
|
77683555e8 |
@@ -9,7 +9,7 @@ remappings = [
|
|||||||
optimizer=true
|
optimizer=true
|
||||||
optimizer_runs=999999999
|
optimizer_runs=999999999
|
||||||
viaIR=true
|
viaIR=true
|
||||||
gas_reports = ['PartyPool', 'PartyPlanner']
|
gas_reports = ['PartyPlanner', 'PartyPoolBalancedPair', 'PartyPool', ]
|
||||||
fs_permissions = [{ access = "write", path = "chain.json"}]
|
fs_permissions = [{ access = "write", path = "chain.json"}]
|
||||||
|
|
||||||
[lint]
|
[lint]
|
||||||
|
|||||||
30
src/Deploy.sol
Normal file
30
src/Deploy.sol
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
|
pragma solidity ^0.8.30;
|
||||||
|
|
||||||
|
import "./PartyPoolBalancedPair.sol";
|
||||||
|
import {PartyPlanner} from "./PartyPlanner.sol";
|
||||||
|
import {PartyPool} from "./PartyPool.sol";
|
||||||
|
|
||||||
|
|
||||||
|
library Deploy {
|
||||||
|
|
||||||
|
function newPartyPlanner() internal returns (PartyPlanner) {
|
||||||
|
return new PartyPlanner();
|
||||||
|
}
|
||||||
|
|
||||||
|
function newPartyPool(
|
||||||
|
string memory name_,
|
||||||
|
string memory symbol_,
|
||||||
|
IERC20[] memory tokens_,
|
||||||
|
uint256[] memory bases_,
|
||||||
|
int128 kappa_,
|
||||||
|
uint256 swapFeePpm_,
|
||||||
|
uint256 flashFeePpm_,
|
||||||
|
bool stablePair_
|
||||||
|
) internal returns (PartyPool) {
|
||||||
|
return stablePair_ ?
|
||||||
|
new PartyPoolBalancedPair(name_, symbol_, tokens_, bases_, kappa_, swapFeePpm_, flashFeePpm_) :
|
||||||
|
new PartyPool(name_, symbol_, tokens_, bases_, kappa_, swapFeePpm_, flashFeePpm_);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -188,6 +188,15 @@ interface IPartyPool is IERC20Metadata {
|
|||||||
uint256 deadline
|
uint256 deadline
|
||||||
) external returns (uint256 amountInUsed, uint256 amountOut, uint256 fee);
|
) external returns (uint256 amountInUsed, uint256 amountOut, uint256 fee);
|
||||||
|
|
||||||
|
/// @notice External view to quote swapMint amounts, matching swapMint() computations
|
||||||
|
/// @param inputTokenIndex index of input token to deposit
|
||||||
|
/// @param maxAmountIn maximum gross input allowed (inclusive of fee)
|
||||||
|
/// @return totalTransfer gross input amount to transfer (includes fee), amountIn net input amount used for minting, fee fee amount taken, lpMinted LP tokens that would be minted
|
||||||
|
function swapMintAmounts(
|
||||||
|
uint256 inputTokenIndex,
|
||||||
|
uint256 maxAmountIn
|
||||||
|
) external view returns (uint256 totalTransfer, uint256 amountIn, uint256 fee, uint256 lpMinted);
|
||||||
|
|
||||||
/// @notice Single-token mint: deposit a single token, charge swap-LMSR cost, and mint LP.
|
/// @notice Single-token mint: deposit a single token, charge swap-LMSR cost, and mint LP.
|
||||||
/// @dev swapMint executes as an exact-in planned swap followed by proportional scaling of qInternal.
|
/// @dev swapMint executes as an exact-in planned swap followed by proportional scaling of qInternal.
|
||||||
/// The function emits SwapMint (gross, net, fee) and also emits Mint for LP issuance.
|
/// The function emits SwapMint (gross, net, fee) and also emits Mint for LP issuance.
|
||||||
@@ -205,6 +214,15 @@ interface IPartyPool is IERC20Metadata {
|
|||||||
uint256 deadline
|
uint256 deadline
|
||||||
) external returns (uint256 lpMinted);
|
) external returns (uint256 lpMinted);
|
||||||
|
|
||||||
|
/// @notice External view to quote burnSwap amounts, matching burnSwap() computations
|
||||||
|
/// @param lpAmount amount of LP tokens to burn
|
||||||
|
/// @param inputTokenIndex index of target asset to receive
|
||||||
|
/// @return amountOut output amount user would receive after fees
|
||||||
|
function burnSwapAmounts(
|
||||||
|
uint256 lpAmount,
|
||||||
|
uint256 inputTokenIndex
|
||||||
|
) external view returns (uint256 amountOut);
|
||||||
|
|
||||||
/// @notice Burn LP tokens then swap the redeemed proportional basket into a single asset `inputTokenIndex` and send to receiver.
|
/// @notice Burn LP tokens then swap the redeemed proportional basket into a single asset `inputTokenIndex` and send to receiver.
|
||||||
/// @dev The function burns LP tokens (authorization via allowance if needed), sends the single-asset payout and updates LMSR state.
|
/// @dev The function burns LP tokens (authorization via allowance if needed), sends the single-asset payout and updates LMSR state.
|
||||||
/// @param payer who burns LP tokens
|
/// @param payer who burns LP tokens
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
// SPDX-License-Identifier: UNLICENSED
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
pragma solidity ^0.8.30;
|
pragma solidity ^0.8.30;
|
||||||
|
|
||||||
import "forge-std/console2.sol";
|
|
||||||
import "@abdk/ABDKMath64x64.sol";
|
import "@abdk/ABDKMath64x64.sol";
|
||||||
|
|
||||||
/// @notice Stabilized LMSR library with incremental exp(z) caching for gas efficiency.
|
/// @notice Stabilized LMSR library with incremental exp(z) caching for gas efficiency.
|
||||||
@@ -41,21 +40,11 @@ library LMSRStabilized {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int128 total = _computeSizeMetric(s.qInternal);
|
int128 total = _computeSizeMetric(s.qInternal);
|
||||||
console2.log("total (internal 64.64)");
|
|
||||||
console2.logInt(total);
|
|
||||||
require(total > int128(0), "LMSR: total zero");
|
require(total > int128(0), "LMSR: total zero");
|
||||||
|
|
||||||
console2.log("LMSR.init: start");
|
|
||||||
console2.log("nAssets", s.nAssets);
|
|
||||||
console2.log("qInternal.length", s.qInternal.length);
|
|
||||||
|
|
||||||
// Set kappa directly (caller provides kappa)
|
// Set kappa directly (caller provides kappa)
|
||||||
s.kappa = kappa;
|
s.kappa = kappa;
|
||||||
console2.log("kappa (64x64)");
|
|
||||||
console2.logInt(s.kappa);
|
|
||||||
require(s.kappa > int128(0), "LMSR: kappa>0");
|
require(s.kappa > int128(0), "LMSR: kappa>0");
|
||||||
|
|
||||||
console2.log("LMSR.init: done");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --------------------
|
/* --------------------
|
||||||
@@ -125,49 +114,29 @@ library LMSRStabilized {
|
|||||||
// push the marginal price p_i/p_j beyond the limit; if so, truncate `a`.
|
// push the marginal price p_i/p_j beyond the limit; if so, truncate `a`.
|
||||||
// Marginal price ratio evolves as r(t) = r0 * exp(t/b) (since e_i multiplies by exp(t/b))
|
// Marginal price ratio evolves as r(t) = r0 * exp(t/b) (since e_i multiplies by exp(t/b))
|
||||||
if (limitPrice > int128(0)) {
|
if (limitPrice > int128(0)) {
|
||||||
console2.log("\n=== LimitPrice Logic Debug ===");
|
|
||||||
console2.log("Received limitPrice (64x64):");
|
|
||||||
console2.logInt(limitPrice);
|
|
||||||
|
|
||||||
console2.log("Current price ratio r0 (e_i/e_j, 64x64):");
|
|
||||||
console2.logInt(r0);
|
|
||||||
|
|
||||||
// r0 must be positive; if r0 == 0 then no risk of exceeding limit by increasing r.
|
// r0 must be positive; if r0 == 0 then no risk of exceeding limit by increasing r.
|
||||||
require(r0 >= int128(0), "LMSR: r0<0");
|
require(r0 >= int128(0), "LMSR: r0<0");
|
||||||
if (r0 == int128(0)) {
|
if (r0 == int128(0)) {
|
||||||
console2.log("r0 == 0 (input asset has zero weight), no limit truncation needed");
|
// console2.log("r0 == 0 (input asset has zero weight), no limit truncation needed");
|
||||||
} else {
|
} else {
|
||||||
// If limitPrice <= current price, we revert (caller must choose a limit > current price to allow any fill)
|
// If limitPrice <= current price, we revert (caller must choose a limit > current price to allow any fill)
|
||||||
if (limitPrice <= r0) {
|
require(limitPrice > r0, "LMSR: limitPrice <= current price");
|
||||||
console2.log("Limit price is <= current price: reverting");
|
|
||||||
revert("LMSR: limitPrice <= current price");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute a_limit directly from ln(limit / r0): a_limit = b * ln(limit / r0)
|
// Compute a_limit directly from ln(limit / r0): a_limit = b * ln(limit / r0)
|
||||||
int128 ratioLimitOverR0 = limitPrice.div(r0);
|
int128 ratioLimitOverR0 = limitPrice.div(r0);
|
||||||
console2.log("limitPrice/r0 (64x64):");
|
|
||||||
console2.logInt(ratioLimitOverR0);
|
|
||||||
require(ratioLimitOverR0 > int128(0), "LMSR: ratio<=0");
|
require(ratioLimitOverR0 > int128(0), "LMSR: ratio<=0");
|
||||||
|
|
||||||
int128 aLimitOverB = _ln(ratioLimitOverR0); // > 0
|
int128 aLimitOverB = _ln(ratioLimitOverR0); // > 0
|
||||||
console2.log("ln(limitPrice/r0) (64x64):");
|
|
||||||
console2.logInt(aLimitOverB);
|
|
||||||
|
|
||||||
// aLimit = b * aLimitOverB
|
// aLimit = b * aLimitOverB
|
||||||
int128 aLimit64 = b.mul(aLimitOverB);
|
int128 aLimit64 = b.mul(aLimitOverB);
|
||||||
console2.log("aLimit in 64x64 format:");
|
|
||||||
console2.logInt(aLimit64);
|
|
||||||
|
|
||||||
// If computed aLimit is less than the requested a, use the truncated value.
|
// If computed aLimit is less than the requested a, use the truncated value.
|
||||||
if (aLimit64 < a) {
|
if (aLimit64 < a) {
|
||||||
console2.log("TRUNCATING: a reduced from 64.64 value");
|
|
||||||
console2.logInt(a);
|
|
||||||
console2.log("to 64.64 value");
|
|
||||||
console2.logInt(aLimit64);
|
|
||||||
amountIn = aLimit64; // Store the truncated input amount
|
amountIn = aLimit64; // Store the truncated input amount
|
||||||
a = aLimit64; // Use truncated amount for calculations
|
a = aLimit64; // Use truncated amount for calculations
|
||||||
} else {
|
} else {
|
||||||
console2.log("Not truncating: aLimit64 >= a");
|
// console2.log("Not truncating: aLimit64 >= a");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -177,56 +146,24 @@ library LMSRStabilized {
|
|||||||
// Protect exp from enormous inputs (consistent with recenter thresholds)
|
// Protect exp from enormous inputs (consistent with recenter thresholds)
|
||||||
require(aOverB <= EXP_LIMIT, "LMSR: a/b too large (would overflow exp)");
|
require(aOverB <= EXP_LIMIT, "LMSR: a/b too large (would overflow exp)");
|
||||||
|
|
||||||
console2.log("\n=== AmountOut Calculation Debug ===");
|
|
||||||
console2.log("Input amount (64.64):");
|
|
||||||
console2.logInt(a);
|
|
||||||
console2.log("a/b (64x64):");
|
|
||||||
console2.logInt(aOverB);
|
|
||||||
|
|
||||||
// Use the closed-form fee-free formula:
|
// Use the closed-form fee-free formula:
|
||||||
// y = b * ln(1 + r0 * (1 - exp(-a/b)))
|
// y = b * ln(1 + r0 * (1 - exp(-a/b)))
|
||||||
console2.log("r0_for_calc (e_i/e_j):");
|
|
||||||
console2.logInt(r0);
|
|
||||||
|
|
||||||
int128 expNeg = _exp(aOverB.neg()); // exp(-a/b)
|
int128 expNeg = _exp(aOverB.neg()); // exp(-a/b)
|
||||||
console2.log("exp(-a/b):");
|
|
||||||
console2.logInt(expNeg);
|
|
||||||
|
|
||||||
int128 oneMinusExpNeg = ONE.sub(expNeg);
|
int128 oneMinusExpNeg = ONE.sub(expNeg);
|
||||||
console2.log("1 - exp(-a/b):");
|
|
||||||
console2.logInt(oneMinusExpNeg);
|
|
||||||
|
|
||||||
int128 inner = ONE.add(r0.mul(oneMinusExpNeg));
|
int128 inner = ONE.add(r0.mul(oneMinusExpNeg));
|
||||||
console2.log("inner = 1 + r0 * (1 - exp(-a/b)):");
|
|
||||||
console2.logInt(inner);
|
|
||||||
|
|
||||||
// If inner <= 0 then cap output to the current balance q_j (cannot withdraw more than q_j)
|
// If inner <= 0 then cap output to the current balance q_j (cannot withdraw more than q_j)
|
||||||
if (inner <= int128(0)) {
|
if (inner <= int128(0)) {
|
||||||
console2.log("WARNING: inner <= 0, capping output to balance q_j");
|
|
||||||
int128 qj64 = s.qInternal[j];
|
int128 qj64 = s.qInternal[j];
|
||||||
console2.log("Capped output (64.64):");
|
|
||||||
console2.logInt(qj64);
|
|
||||||
return (amountIn, qj64);
|
return (amountIn, qj64);
|
||||||
}
|
}
|
||||||
|
|
||||||
int128 lnInner = _ln(inner);
|
int128 lnInner = _ln(inner);
|
||||||
console2.log("ln(inner):");
|
|
||||||
console2.logInt(lnInner);
|
|
||||||
|
|
||||||
int128 b_lnInner = b.mul(lnInner);
|
int128 b_lnInner = b.mul(lnInner);
|
||||||
console2.log("b*ln(inner):");
|
|
||||||
console2.logInt(b_lnInner);
|
|
||||||
|
|
||||||
amountOut = b_lnInner;
|
amountOut = b_lnInner;
|
||||||
console2.log("amountOut = b*ln(inner):");
|
|
||||||
console2.logInt(amountOut);
|
|
||||||
|
|
||||||
console2.log("amountOut (final 64.64 amount):");
|
|
||||||
console2.logInt(amountOut);
|
|
||||||
|
|
||||||
// Safety check
|
// Safety check
|
||||||
if (amountOut <= 0) {
|
if (amountOut <= 0) {
|
||||||
console2.log("WARNING: x64 <= 0, returning 0");
|
|
||||||
return (0, 0);
|
return (0, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -265,60 +202,37 @@ library LMSRStabilized {
|
|||||||
// Compute r0 = exp((q_i - q_j) / b) directly using invB
|
// Compute r0 = exp((q_i - q_j) / b) directly using invB
|
||||||
int128 r0 = _exp(s.qInternal[i].sub(s.qInternal[j]).mul(invB));
|
int128 r0 = _exp(s.qInternal[i].sub(s.qInternal[j]).mul(invB));
|
||||||
|
|
||||||
console2.log("\n=== Max Input/Output Calculation ===");
|
|
||||||
console2.log("Limit price (64x64):");
|
|
||||||
console2.logInt(limitPrice);
|
|
||||||
console2.log("Current price ratio r0 (e_i/e_j, 64x64):");
|
|
||||||
console2.logInt(r0);
|
|
||||||
|
|
||||||
// Mirror swapAmountsForExactInput behavior: treat invalid r0 as an error condition.
|
// Mirror swapAmountsForExactInput behavior: treat invalid r0 as an error condition.
|
||||||
// Revert if r0 is non-positive (no finite trade under a price limit).
|
// Revert if r0 is non-positive (no finite trade under a price limit).
|
||||||
require(r0 > int128(0), "LMSR: r0<=0");
|
require(r0 > int128(0), "LMSR: r0<=0");
|
||||||
|
|
||||||
// If current price already exceeds or equals limit, revert the same way swapAmountsForExactInput does.
|
// If current price already exceeds or equals limit, revert the same way swapAmountsForExactInput does.
|
||||||
if (r0 >= limitPrice) {
|
if (r0 >= limitPrice) {
|
||||||
console2.log("Limit price is <= current price: reverting");
|
|
||||||
revert("LMSR: limitPrice <= current price");
|
revert("LMSR: limitPrice <= current price");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate the price change factor: limitPrice/r0
|
// Calculate the price change factor: limitPrice/r0
|
||||||
int128 priceChangeFactor = limitPrice.div(r0);
|
int128 priceChangeFactor = limitPrice.div(r0);
|
||||||
console2.log("Price change factor (limitPrice/r0):");
|
|
||||||
console2.logInt(priceChangeFactor);
|
|
||||||
|
|
||||||
// ln(priceChangeFactor) gives us the maximum allowed delta in the exponent
|
// ln(priceChangeFactor) gives us the maximum allowed delta in the exponent
|
||||||
int128 maxDeltaExponent = _ln(priceChangeFactor);
|
int128 maxDeltaExponent = _ln(priceChangeFactor);
|
||||||
console2.log("Max delta exponent ln(priceChangeFactor):");
|
|
||||||
console2.logInt(maxDeltaExponent);
|
|
||||||
|
|
||||||
// Maximum input capable of reaching the price limit:
|
// Maximum input capable of reaching the price limit:
|
||||||
// x_max = b * ln(limitPrice / r0)
|
// x_max = b * ln(limitPrice / r0)
|
||||||
int128 amountInMax = b.mul(maxDeltaExponent);
|
int128 amountInMax = b.mul(maxDeltaExponent);
|
||||||
console2.log("Max input to reach limit (64.64):");
|
|
||||||
console2.logInt(amountInMax);
|
|
||||||
|
|
||||||
// The maximum output y corresponding to that input:
|
// The maximum output y corresponding to that input:
|
||||||
// y = b * ln(1 + (e_i/e_j) * (1 - exp(-x_max/b)))
|
// y = b * ln(1 + (e_i/e_j) * (1 - exp(-x_max/b)))
|
||||||
int128 expTerm = ONE.sub(_exp(maxDeltaExponent.neg()));
|
int128 expTerm = ONE.sub(_exp(maxDeltaExponent.neg()));
|
||||||
console2.log("1 - exp(-maxDeltaExponent):");
|
|
||||||
console2.logInt(expTerm);
|
|
||||||
|
|
||||||
int128 innerTerm = r0.mul(expTerm);
|
int128 innerTerm = r0.mul(expTerm);
|
||||||
console2.log("e_i/e_j * expTerm:");
|
|
||||||
console2.logInt(innerTerm);
|
|
||||||
|
|
||||||
int128 lnTerm = _ln(ONE.add(innerTerm));
|
int128 lnTerm = _ln(ONE.add(innerTerm));
|
||||||
console2.log("ln(1 + innerTerm):");
|
|
||||||
console2.logInt(lnTerm);
|
|
||||||
|
|
||||||
int128 maxOutput = b.mul(lnTerm);
|
int128 maxOutput = b.mul(lnTerm);
|
||||||
console2.log("Max output (b * lnTerm):");
|
|
||||||
console2.logInt(maxOutput);
|
|
||||||
|
|
||||||
// Current balance of asset j (in 64.64)
|
// Current balance of asset j (in 64.64)
|
||||||
int128 qj64 = s.qInternal[j];
|
int128 qj64 = s.qInternal[j];
|
||||||
console2.log("Current j balance (64.64):");
|
|
||||||
console2.logInt(qj64);
|
|
||||||
|
|
||||||
// Initialize outputs to the computed maxima
|
// Initialize outputs to the computed maxima
|
||||||
amountIn = amountInMax;
|
amountIn = amountInMax;
|
||||||
@@ -326,7 +240,6 @@ library LMSRStabilized {
|
|||||||
|
|
||||||
// If the calculated maximum output exceeds the balance, cap output and solve for input.
|
// If the calculated maximum output exceeds the balance, cap output and solve for input.
|
||||||
if (maxOutput > qj64) {
|
if (maxOutput > qj64) {
|
||||||
console2.log("Max output exceeds balance, capping to balance");
|
|
||||||
amountOut = qj64;
|
amountOut = qj64;
|
||||||
|
|
||||||
// Solve inverse relation for input given capped output:
|
// Solve inverse relation for input given capped output:
|
||||||
@@ -336,19 +249,12 @@ library LMSRStabilized {
|
|||||||
// a = -b * ln( (r0 + 1 - E) / r0 ) = b * ln( r0 / (r0 + 1 - E) )
|
// a = -b * ln( (r0 + 1 - E) / r0 ) = b * ln( r0 / (r0 + 1 - E) )
|
||||||
int128 E = _exp(amountOut.mul(invB)); // exp(y/b)
|
int128 E = _exp(amountOut.mul(invB)); // exp(y/b)
|
||||||
int128 rhs = r0.add(ONE).sub(E); // r0 + 1 - E
|
int128 rhs = r0.add(ONE).sub(E); // r0 + 1 - E
|
||||||
console2.log("E = exp(y/b):");
|
|
||||||
console2.logInt(E);
|
|
||||||
console2.log("rhs = r0 + 1 - E:");
|
|
||||||
console2.logInt(rhs);
|
|
||||||
|
|
||||||
// If rhs <= 0 due to numerical issues, fall back to amountInMax
|
// If rhs <= 0 due to numerical issues, fall back to amountInMax
|
||||||
if (rhs <= int128(0)) {
|
if (rhs <= int128(0)) {
|
||||||
console2.log("Numerical issue solving inverse; using amountInMax as fallback");
|
|
||||||
amountIn = amountInMax;
|
amountIn = amountInMax;
|
||||||
} else {
|
} else {
|
||||||
amountIn = b.mul(_ln(r0.div(rhs)));
|
amountIn = b.mul(_ln(r0.div(rhs)));
|
||||||
console2.log("Computed input required for capped output (64.64):");
|
|
||||||
console2.logInt(amountIn);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -674,19 +580,9 @@ library LMSRStabilized {
|
|||||||
require(amountIn > int128(0), "LMSR: amountIn <= 0");
|
require(amountIn > int128(0), "LMSR: amountIn <= 0");
|
||||||
require(amountOut > int128(0), "LMSR: amountOut <= 0");
|
require(amountOut > int128(0), "LMSR: amountOut <= 0");
|
||||||
|
|
||||||
console2.log("\n=== Applying Swap ===");
|
|
||||||
console2.log("Input asset:", i);
|
|
||||||
console2.log("Output asset:", j);
|
|
||||||
console2.log("Amount in (64.64):");
|
|
||||||
console2.logInt(amountIn);
|
|
||||||
console2.log("Amount out (64.64):");
|
|
||||||
console2.logInt(amountOut);
|
|
||||||
|
|
||||||
// Update internal balances
|
// Update internal balances
|
||||||
s.qInternal[i] = s.qInternal[i].add(amountIn);
|
s.qInternal[i] = s.qInternal[i].add(amountIn);
|
||||||
s.qInternal[j] = s.qInternal[j].sub(amountOut);
|
s.qInternal[j] = s.qInternal[j].sub(amountOut);
|
||||||
|
|
||||||
console2.log("=== Swap Applied (qInternal updated) ===\n");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -697,27 +593,16 @@ library LMSRStabilized {
|
|||||||
function updateForProportionalChange(State storage s, int128[] memory newQInternal) internal {
|
function updateForProportionalChange(State storage s, int128[] memory newQInternal) internal {
|
||||||
require(newQInternal.length == s.nAssets, "LMSR: length mismatch");
|
require(newQInternal.length == s.nAssets, "LMSR: length mismatch");
|
||||||
|
|
||||||
console2.log("LMSR.updateForProportionalChange: start");
|
|
||||||
|
|
||||||
// Compute new total for validation
|
// Compute new total for validation
|
||||||
int128 newTotal = _computeSizeMetric(newQInternal);
|
int128 newTotal = _computeSizeMetric(newQInternal);
|
||||||
console2.log("new total");
|
|
||||||
console2.logInt(newTotal);
|
|
||||||
|
|
||||||
require(newTotal > int128(0), "LMSR: new total zero");
|
require(newTotal > int128(0), "LMSR: new total zero");
|
||||||
|
|
||||||
// With kappa formulation, b automatically scales with pool size
|
|
||||||
int128 newB = s.kappa.mul(newTotal);
|
|
||||||
console2.log("new effective b");
|
|
||||||
console2.logInt(newB);
|
|
||||||
|
|
||||||
// Update the cached qInternal with new values
|
// Update the cached qInternal with new values
|
||||||
for (uint i = 0; i < s.nAssets; ) {
|
for (uint i = 0; i < s.nAssets; ) {
|
||||||
s.qInternal[i] = newQInternal[i];
|
s.qInternal[i] = newQInternal[i];
|
||||||
unchecked { i++; }
|
unchecked { i++; }
|
||||||
}
|
}
|
||||||
|
|
||||||
console2.log("LMSR.updateForProportionalChange: end");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Price-share of asset i: exp(z_i) / Z (64.64)
|
/// @notice Price-share of asset i: exp(z_i) / Z (64.64)
|
||||||
@@ -904,8 +789,6 @@ library LMSRStabilized {
|
|||||||
/// @notice De-initialize the LMSR state when the entire pool is drained.
|
/// @notice De-initialize the LMSR state when the entire pool is drained.
|
||||||
/// This resets the state so the pool can be re-initialized by init(...) on next mint.
|
/// This resets the state so the pool can be re-initialized by init(...) on next mint.
|
||||||
function deinit(State storage s) internal {
|
function deinit(State storage s) internal {
|
||||||
console2.log("LMSR.deinit: resetting state");
|
|
||||||
|
|
||||||
// Reset core state
|
// Reset core state
|
||||||
s.nAssets = 0;
|
s.nAssets = 0;
|
||||||
s.kappa = int128(0);
|
s.kappa = int128(0);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
// SPDX-License-Identifier: UNLICENSED
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
pragma solidity ^0.8.30;
|
pragma solidity ^0.8.30;
|
||||||
|
|
||||||
import "forge-std/console2.sol";
|
|
||||||
import "@abdk/ABDKMath64x64.sol";
|
import "@abdk/ABDKMath64x64.sol";
|
||||||
import "./LMSRStabilized.sol";
|
import "./LMSRStabilized.sol";
|
||||||
|
|
||||||
@@ -40,7 +39,6 @@ library LMSRStabilizedBalancedPair {
|
|||||||
|
|
||||||
// If not exactly a two-asset pool, fall back to the general routine.
|
// If not exactly a two-asset pool, fall back to the general routine.
|
||||||
if (s.nAssets != 2) {
|
if (s.nAssets != 2) {
|
||||||
console2.log('balanced2: fallback nAssets!=n2');
|
|
||||||
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
|
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,7 +46,6 @@ library LMSRStabilizedBalancedPair {
|
|||||||
int128 b = LMSRStabilized._computeB(s);
|
int128 b = LMSRStabilized._computeB(s);
|
||||||
// Guard: if b not positive, fallback to exact implementation (will revert there if necessary)
|
// Guard: if b not positive, fallback to exact implementation (will revert there if necessary)
|
||||||
if (!(b > int128(0))) {
|
if (!(b > int128(0))) {
|
||||||
console2.log("balanced2: fallback b<=0");
|
|
||||||
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
|
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
|
||||||
}
|
}
|
||||||
int128 invB = ABDKMath64x64.div(ONE, b);
|
int128 invB = ABDKMath64x64.div(ONE, b);
|
||||||
@@ -58,8 +55,6 @@ library LMSRStabilizedBalancedPair {
|
|||||||
|
|
||||||
// If a positive limitPrice is given, attempt a 2-asset near-parity polynomial solution
|
// If a positive limitPrice is given, attempt a 2-asset near-parity polynomial solution
|
||||||
if (limitPrice > int128(0)) {
|
if (limitPrice > int128(0)) {
|
||||||
console2.log("balanced2: handling limitPrice via small-delta approx");
|
|
||||||
|
|
||||||
// Approximate r0 = exp(delta) using Taylor: 1 + δ + δ^2/2 + δ^3/6
|
// Approximate r0 = exp(delta) using Taylor: 1 + δ + δ^2/2 + δ^3/6
|
||||||
int128 delta_sq = delta.mul(delta);
|
int128 delta_sq = delta.mul(delta);
|
||||||
int128 delta_cu = delta_sq.mul(delta);
|
int128 delta_cu = delta_sq.mul(delta);
|
||||||
@@ -68,19 +63,13 @@ library LMSRStabilizedBalancedPair {
|
|||||||
.add(delta_sq.div(ABDKMath64x64.fromUInt(2)))
|
.add(delta_sq.div(ABDKMath64x64.fromUInt(2)))
|
||||||
.add(delta_cu.div(ABDKMath64x64.fromUInt(6)));
|
.add(delta_cu.div(ABDKMath64x64.fromUInt(6)));
|
||||||
|
|
||||||
console2.log("r0_approx:");
|
|
||||||
console2.logInt(r0_approx);
|
|
||||||
|
|
||||||
// If limitPrice <= r0 (current price) we must revert (same semantic as original)
|
// If limitPrice <= r0 (current price) we must revert (same semantic as original)
|
||||||
if (limitPrice <= r0_approx) {
|
if (limitPrice <= r0_approx) {
|
||||||
console2.log("balanced2: limitPrice <= r0_approx -> revert");
|
|
||||||
revert("LMSR: limitPrice <= current price");
|
revert("LMSR: limitPrice <= current price");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ratio = limitPrice / r0_approx
|
// Ratio = limitPrice / r0_approx
|
||||||
int128 ratio = limitPrice.div(r0_approx);
|
int128 ratio = limitPrice.div(r0_approx);
|
||||||
console2.log("limitPrice/r0_approx:");
|
|
||||||
console2.logInt(ratio);
|
|
||||||
|
|
||||||
// x = ratio - 1; use Taylor for ln(1+x) when |x| is small
|
// x = ratio - 1; use Taylor for ln(1+x) when |x| is small
|
||||||
int128 x = ratio.sub(ONE);
|
int128 x = ratio.sub(ONE);
|
||||||
@@ -90,7 +79,6 @@ library LMSRStabilizedBalancedPair {
|
|||||||
int128 X_MAX = ABDKMath64x64.divu(1, 10); // 0.1
|
int128 X_MAX = ABDKMath64x64.divu(1, 10); // 0.1
|
||||||
if (absX > X_MAX) {
|
if (absX > X_MAX) {
|
||||||
// Too large to safely approximate; fall back to exact computation
|
// Too large to safely approximate; fall back to exact computation
|
||||||
console2.log("balanced2: fallback limitPrice ratio too far from 1");
|
|
||||||
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
|
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,63 +89,34 @@ library LMSRStabilizedBalancedPair {
|
|||||||
.sub(x_sq.div(ABDKMath64x64.fromUInt(2)))
|
.sub(x_sq.div(ABDKMath64x64.fromUInt(2)))
|
||||||
.add(x_cu.div(ABDKMath64x64.fromUInt(3)));
|
.add(x_cu.div(ABDKMath64x64.fromUInt(3)));
|
||||||
|
|
||||||
console2.log("lnRatioApprox (64x64):");
|
|
||||||
console2.logInt(lnRatioApprox);
|
|
||||||
|
|
||||||
// aLimitOverB = ln(limitPrice / r0) approximated
|
// aLimitOverB = ln(limitPrice / r0) approximated
|
||||||
int128 aLimitOverB = lnRatioApprox;
|
int128 aLimitOverB = lnRatioApprox;
|
||||||
|
|
||||||
// Must be > 0; otherwise fall back
|
// Must be > 0; otherwise fall back
|
||||||
if (!(aLimitOverB > int128(0))) {
|
if (!(aLimitOverB > int128(0))) {
|
||||||
console2.log("balanced2: fallback non-positive aLimitOverB");
|
|
||||||
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
|
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
|
||||||
}
|
}
|
||||||
|
|
||||||
// aLimit = b * aLimitOverB (in Q64.64)
|
// aLimit = b * aLimitOverB (in Q64.64)
|
||||||
int128 aLimit64 = b.mul(aLimitOverB);
|
int128 aLimit64 = b.mul(aLimitOverB);
|
||||||
console2.log("aLimit64 (64x64):");
|
|
||||||
console2.logInt(aLimit64);
|
|
||||||
|
|
||||||
// If computed aLimit is less than requested a, use the truncated value.
|
// If computed aLimit is less than requested a, use the truncated value.
|
||||||
if (aLimit64 < a) {
|
if (aLimit64 < a) {
|
||||||
console2.log("balanced2: truncating input a to aLimit64 due to limitPrice");
|
|
||||||
console2.log("original a:");
|
|
||||||
console2.logInt(a);
|
|
||||||
console2.log("truncated aLimit64:");
|
|
||||||
console2.logInt(aLimit64);
|
|
||||||
a = aLimit64;
|
a = aLimit64;
|
||||||
} else {
|
} else {
|
||||||
console2.log("balanced2: limitPrice does not truncate input");
|
// console2.log("balanced2: limitPrice does not truncate input");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: after potential truncation we continue with the polynomial approximation below
|
// Note: after potential truncation we continue with the polynomial approximation below
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug: entry trace
|
|
||||||
console2.log("balanced2: enter");
|
|
||||||
console2.log("i", i);
|
|
||||||
console2.log("j", j);
|
|
||||||
console2.log("nAssets", s.nAssets);
|
|
||||||
console2.log("a (64x64):");
|
|
||||||
console2.logInt(a);
|
|
||||||
console2.log("b (64x64):");
|
|
||||||
console2.logInt(b);
|
|
||||||
console2.log("invB (64x64):");
|
|
||||||
console2.logInt(invB);
|
|
||||||
|
|
||||||
// Small-signal delta already computed above; reuse it
|
// Small-signal delta already computed above; reuse it
|
||||||
int128 absDelta = delta >= int128(0) ? delta : delta.neg();
|
int128 absDelta = delta >= int128(0) ? delta : delta.neg();
|
||||||
|
|
||||||
console2.log("delta (q_i - q_j)/b:");
|
|
||||||
console2.logInt(delta);
|
|
||||||
console2.log("absDelta:");
|
|
||||||
console2.logInt(absDelta);
|
|
||||||
|
|
||||||
// Allow balanced pools only: require |delta| <= 1% (approx ln(1.01) ~ 0.00995; we use conservative 0.01)
|
// Allow balanced pools only: require |delta| <= 1% (approx ln(1.01) ~ 0.00995; we use conservative 0.01)
|
||||||
int128 DELTA_MAX = ABDKMath64x64.divu(1, 100); // 0.01
|
int128 DELTA_MAX = ABDKMath64x64.divu(1, 100); // 0.01
|
||||||
if (absDelta > DELTA_MAX) {
|
if (absDelta > DELTA_MAX) {
|
||||||
// Not balanced within 1% -> use exact routine
|
// Not balanced within 1% -> use exact routine
|
||||||
console2.log("balanced2: fallback delta too large");
|
|
||||||
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
|
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,18 +124,13 @@ library LMSRStabilizedBalancedPair {
|
|||||||
int128 u = a.mul(invB);
|
int128 u = a.mul(invB);
|
||||||
if (u <= int128(0)) {
|
if (u <= int128(0)) {
|
||||||
// Non-positive input -> behave like exact implementation (will revert if invalid)
|
// Non-positive input -> behave like exact implementation (will revert if invalid)
|
||||||
console2.log("balanced2: fallback u<=0");
|
|
||||||
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
|
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
|
||||||
}
|
}
|
||||||
|
|
||||||
console2.log("u = a/b (64x64):");
|
|
||||||
console2.logInt(u);
|
|
||||||
|
|
||||||
// Restrict to a conservative polynomial radius for accuracy; fallback otherwise.
|
// Restrict to a conservative polynomial radius for accuracy; fallback otherwise.
|
||||||
// We choose u <= 0.5 (0.5 in Q64.64) as safe for cubic approximation in typical parameters.
|
// We choose u <= 0.5 (0.5 in Q64.64) as safe for cubic approximation in typical parameters.
|
||||||
int128 U_MAX = ABDKMath64x64.divu(1, 2); // 0.5
|
int128 U_MAX = ABDKMath64x64.divu(1, 2); // 0.5
|
||||||
if (u > U_MAX) {
|
if (u > U_MAX) {
|
||||||
console2.log("balanced2: fallback u too large");
|
|
||||||
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
|
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,39 +154,26 @@ library LMSRStabilizedBalancedPair {
|
|||||||
if (u <= U_TIER1) {
|
if (u <= U_TIER1) {
|
||||||
// Cheap quadratic ln(1+X) ≈ X - X^2/2
|
// Cheap quadratic ln(1+X) ≈ X - X^2/2
|
||||||
lnApprox = X.sub(X2.div(ABDKMath64x64.fromUInt(2)));
|
lnApprox = X.sub(X2.div(ABDKMath64x64.fromUInt(2)));
|
||||||
console2.log("balanced2: using tier1 quadratic approx");
|
|
||||||
} else if (u <= U_MAX_LOCAL) {
|
} else if (u <= U_MAX_LOCAL) {
|
||||||
// Secondary cubic correction: ln(1+X) ≈ X - X^2/2 + X^3/3
|
// Secondary cubic correction: ln(1+X) ≈ X - X^2/2 + X^3/3
|
||||||
int128 X3 = X2.mul(X);
|
int128 X3 = X2.mul(X);
|
||||||
lnApprox = X.sub(X2.div(ABDKMath64x64.fromUInt(2))).add(X3.div(ABDKMath64x64.fromUInt(3)));
|
lnApprox = X.sub(X2.div(ABDKMath64x64.fromUInt(2))).add(X3.div(ABDKMath64x64.fromUInt(3)));
|
||||||
console2.log("balanced2: using tier2 cubic approx");
|
|
||||||
} else {
|
} else {
|
||||||
// u beyond allowed range - fallback
|
// u beyond allowed range - fallback
|
||||||
console2.log("balanced2: fallback u too large for approximation");
|
|
||||||
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
|
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
|
||||||
}
|
}
|
||||||
|
|
||||||
console2.log("lnApprox (64x64):");
|
|
||||||
console2.logInt(lnApprox);
|
|
||||||
|
|
||||||
int128 approxOut = b.mul(lnApprox);
|
int128 approxOut = b.mul(lnApprox);
|
||||||
|
|
||||||
console2.log("approxOut (64x64):");
|
|
||||||
console2.logInt(approxOut);
|
|
||||||
|
|
||||||
// Safety sanity: approximation must be > 0
|
// Safety sanity: approximation must be > 0
|
||||||
if (approxOut <= int128(0)) {
|
if (approxOut <= int128(0)) {
|
||||||
console2.log("balanced2: fallback approxOut <= 0");
|
|
||||||
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
|
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cap to available j balance: if approximated output exceeds q_j, it's likely approximation break;
|
// Cap to available j balance: if approximated output exceeds q_j, it's likely approximation break;
|
||||||
// fall back to the exact solver to handle capping/edge cases.
|
// fall back to the exact solver to handle capping/edge cases.
|
||||||
int128 qj64 = s.qInternal[j];
|
int128 qj64 = s.qInternal[j];
|
||||||
console2.log("qj64 (64x64):");
|
|
||||||
console2.logInt(qj64);
|
|
||||||
if (approxOut >= qj64) {
|
if (approxOut >= qj64) {
|
||||||
console2.log("balanced2: fallback approxOut >= qj");
|
|
||||||
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
|
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,15 +181,8 @@ library LMSRStabilizedBalancedPair {
|
|||||||
amountIn = a;
|
amountIn = a;
|
||||||
amountOut = approxOut;
|
amountOut = approxOut;
|
||||||
|
|
||||||
console2.log("balanced2: returning approx results");
|
|
||||||
console2.log("amountIn (64x64):");
|
|
||||||
console2.logInt(amountIn);
|
|
||||||
console2.log("amountOut (64x64):");
|
|
||||||
console2.logInt(amountOut);
|
|
||||||
|
|
||||||
// Final guard: ensure output is sensible and not NaN-like (rely on positivity checks above)
|
// Final guard: ensure output is sensible and not NaN-like (rely on positivity checks above)
|
||||||
if (amountOut < int128(0)) {
|
if (amountOut < int128(0)) {
|
||||||
console2.log("balanced2: fallback final guard amountOut<0");
|
|
||||||
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
|
return LMSRStabilized.swapAmountsForExactInput(s, i, j, a, limitPrice);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
pragma solidity ^0.8.30;
|
pragma solidity ^0.8.30;
|
||||||
|
|
||||||
import "./IPartyPlanner.sol";
|
import "./IPartyPlanner.sol";
|
||||||
import "./PartyPool.sol";
|
|
||||||
import "./LMSRStabilized.sol";
|
import "./LMSRStabilized.sol";
|
||||||
|
import "./PartyPool.sol";
|
||||||
|
import "./PartyPoolBalancedPair.sol";
|
||||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||||
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||||
|
|
||||||
@@ -48,16 +49,9 @@ contract PartyPlanner is IPartyPlanner {
|
|||||||
require(_kappa > int128(0), "Planner: kappa must be > 0");
|
require(_kappa > int128(0), "Planner: kappa must be > 0");
|
||||||
|
|
||||||
// Create a new PartyPool instance (kappa-based constructor)
|
// Create a new PartyPool instance (kappa-based constructor)
|
||||||
pool = new PartyPool(
|
pool = _stable && _tokens.length == 2 ?
|
||||||
name_,
|
new PartyPoolBalancedPair(name_, symbol_, _tokens, _bases, _kappa, _swapFeePpm, _flashFeePpm) :
|
||||||
symbol_,
|
new PartyPool(name_, symbol_, _tokens, _bases, _kappa, _swapFeePpm, _flashFeePpm);
|
||||||
_tokens,
|
|
||||||
_bases,
|
|
||||||
_kappa,
|
|
||||||
_swapFeePpm,
|
|
||||||
_flashFeePpm,
|
|
||||||
_stable
|
|
||||||
);
|
|
||||||
|
|
||||||
_allPools.push(pool);
|
_allPools.push(pool);
|
||||||
_poolSupported[pool] = true;
|
_poolSupported[pool] = true;
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
// SPDX-License-Identifier: UNLICENSED
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
pragma solidity ^0.8.30;
|
pragma solidity ^0.8.30;
|
||||||
|
|
||||||
import "forge-std/console2.sol";
|
|
||||||
import "@abdk/ABDKMath64x64.sol";
|
|
||||||
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||||
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
||||||
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
||||||
import "./LMSRStabilized.sol";
|
import "./PoolLib.sol";
|
||||||
import "./LMSRStabilizedBalancedPair.sol";
|
|
||||||
import "./IPartyPool.sol";
|
import "./IPartyPool.sol";
|
||||||
import "./IPartyFlashCallback.sol";
|
import {PoolBase} from "./PoolBase.sol";
|
||||||
|
|
||||||
/// @title PartyPool - LMSR-backed multi-asset pool with LP ERC20 token
|
/// @title PartyPool - LMSR-backed multi-asset pool with LP ERC20 token
|
||||||
/// @notice A multi-asset liquidity pool backed by the LMSRStabilized pricing model.
|
/// @notice A multi-asset liquidity pool backed by the LMSRStabilized pricing model.
|
||||||
@@ -21,37 +17,13 @@ import "./IPartyFlashCallback.sol";
|
|||||||
/// - Exact-input swaps and swaps-to-price-limits,
|
/// - Exact-input swaps and swaps-to-price-limits,
|
||||||
/// - Flash loans via a callback interface.
|
/// - Flash loans via a callback interface.
|
||||||
///
|
///
|
||||||
/// @dev The contract stores per-token uint "bases" used to scale token units into the internal Q64.64
|
/// @dev The contract uses PoolLib for all implementation logic and maintains state in a PoolLib.State struct
|
||||||
/// representation used by the LMSR library. Cached on-chain uint balances are kept to reduce balanceOf calls.
|
contract PartyPool is PoolBase, IPartyPool {
|
||||||
/// The contract uses ceiling/floor rules described in function comments to bias rounding in favor of the pool
|
using PoolLib for PoolLib.State;
|
||||||
/// (i.e., floor outputs to users, ceil inputs/fees where appropriate).
|
|
||||||
contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
|
|
||||||
using ABDKMath64x64 for int128;
|
|
||||||
using LMSRStabilized for LMSRStabilized.State;
|
|
||||||
using SafeERC20 for IERC20;
|
|
||||||
|
|
||||||
|
|
||||||
//
|
|
||||||
// Immutable pool configuration
|
|
||||||
//
|
|
||||||
|
|
||||||
/// @notice Token addresses comprising the pool. Effectively immutable after construction.
|
|
||||||
/// @dev tokens[i] corresponds to the i-th asset and maps to index i in the internal LMSR arrays.
|
|
||||||
IERC20[] public tokens; // effectively immutable since there is no interface to change the tokens
|
|
||||||
|
|
||||||
/// @inheritdoc IPartyPool
|
|
||||||
function numTokens() external view returns (uint256) { return tokens.length; }
|
|
||||||
|
|
||||||
/// @inheritdoc IPartyPool
|
|
||||||
function allTokens() external view returns (IERC20[] 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.
|
|
||||||
|
|
||||||
/// @notice Liquidity parameter κ (Q64.64) used by the LMSR kernel: b = κ * S(q)
|
/// @notice Liquidity parameter κ (Q64.64) used by the LMSR kernel: b = κ * S(q)
|
||||||
/// @dev Pool is constructed with a fixed κ. Clients that previously passed tradeFrac/targetSlippage
|
/// @dev Pool is constructed with a fixed κ. Clients may use LMSRStabilized.computeKappaFromSlippage(...) to
|
||||||
/// should use LMSRStabilized.computeKappaFromSlippage(...) to derive κ and pass it here.
|
/// derive κ and pass it here.
|
||||||
int128 public immutable kappa; // kappa in Q64.64
|
int128 public immutable kappa; // kappa in Q64.64
|
||||||
|
|
||||||
/// @notice Per-swap fee in parts-per-million (ppm). Fee is taken from input amounts before LMSR computations.
|
/// @notice Per-swap fee in parts-per-million (ppm). Fee is taken from input amounts before LMSR computations.
|
||||||
@@ -60,29 +32,24 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
|
|||||||
/// @notice Flash-loan fee in parts-per-million (ppm) applied to flash borrow amounts.
|
/// @notice Flash-loan fee in parts-per-million (ppm) applied to flash borrow amounts.
|
||||||
uint256 public immutable flashFeePpm;
|
uint256 public immutable flashFeePpm;
|
||||||
|
|
||||||
//
|
|
||||||
// Internal state
|
|
||||||
//
|
|
||||||
|
|
||||||
LMSRStabilized.State internal lmsr;
|
|
||||||
|
|
||||||
/// @notice If true and there are exactly two assets, an optimized 2-asset stable-pair path is used for some computations.
|
|
||||||
bool immutable private _stablePair; // if true, the optimized LMSRStabilizedBalancedPair optimization path is enabled
|
|
||||||
|
|
||||||
// Cached on-chain balances (uint) and internal 64.64 representation
|
|
||||||
// balance / base = internal
|
|
||||||
uint256[] internal cachedUintBalances;
|
|
||||||
|
|
||||||
/// @notice Per-token uint base denominators used to convert uint token amounts <-> internal Q64.64 representation.
|
|
||||||
/// @dev denominators()[i] is the base for tokens[i]. These bases are chosen by deployer and must match token decimals.
|
|
||||||
uint256[] internal bases; // per-token uint base used to scale token amounts <-> internal
|
|
||||||
|
|
||||||
/// @inheritdoc IPartyPool
|
/// @inheritdoc IPartyPool
|
||||||
function denominators() external view returns (uint256[] memory) { return bases; }
|
function tokens(uint256 i) external view returns (IERC20) { return s.tokens[i]; }
|
||||||
|
|
||||||
|
/// @inheritdoc IPartyPool
|
||||||
|
function numTokens() external view returns (uint256) { return s.tokens.length; }
|
||||||
|
|
||||||
|
/// @inheritdoc IPartyPool
|
||||||
|
function allTokens() external view returns (IERC20[] memory) { return s.tokens; }
|
||||||
|
|
||||||
|
/// @inheritdoc IPartyPool
|
||||||
|
function denominators() external view returns (uint256[] memory) { return s.bases; }
|
||||||
|
|
||||||
/// @notice Mapping from token address => (index+1). A zero value indicates the token is not in the pool.
|
/// @notice Mapping from token address => (index+1). A zero value indicates the token is not in the pool.
|
||||||
/// @dev Use index = tokenAddressToIndexPlusOne[token] - 1 when non-zero.
|
/// @dev Use index = tokenAddressToIndexPlusOne[token] - 1 when non-zero.
|
||||||
mapping(IERC20=>uint) public tokenAddressToIndexPlusOne; // Uses index+1 so a result of 0 indicates a failed lookup
|
function tokenAddressToIndexPlusOne(IERC20 token) external view returns (uint256) {
|
||||||
|
return s.tokenAddressToIndexPlusOne[token];
|
||||||
|
}
|
||||||
|
|
||||||
/// @notice Scale factor used when converting LMSR Q64.64 totals to LP token units (uint).
|
/// @notice Scale factor used when converting LMSR Q64.64 totals to LP token units (uint).
|
||||||
/// @dev LP tokens are minted in units equal to ABDK.mulu(lastTotalQ64x64, LP_SCALE).
|
/// @dev LP tokens are minted in units equal to ABDK.mulu(lastTotalQ64x64, LP_SCALE).
|
||||||
@@ -90,77 +57,37 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
|
|||||||
|
|
||||||
/// @param name_ LP token name
|
/// @param name_ LP token name
|
||||||
/// @param symbol_ LP token symbol
|
/// @param symbol_ LP token symbol
|
||||||
/// @param _tokens token addresses (n)
|
/// @param tokens_ token addresses (n)
|
||||||
/// @param _bases scaling bases for each token (n) - used when converting to/from internal 64.64 amounts
|
/// @param bases_ scaling bases for each token (n) - used when converting to/from internal 64.64 amounts
|
||||||
/// @param _kappa liquidity parameter κ (Q64.64) used to derive b = κ * S(q)
|
/// @param kappa_ liquidity parameter κ (Q64.64) used to derive b = κ * S(q)
|
||||||
/// @param _swapFeePpm fee in parts-per-million, taken from swap input amounts before LMSR calculations
|
/// @param swapFeePpm_ fee in parts-per-million, taken from swap input amounts before LMSR calculations
|
||||||
/// @param _flashFeePpm fee in parts-per-million, taken for flash loans
|
/// @param flashFeePpm_ fee in parts-per-million, taken for flash loans
|
||||||
/// @param _stable if true and assets.length==2, then the optimization for 2-asset stablecoin pools is activated.
|
|
||||||
constructor(
|
constructor(
|
||||||
string memory name_,
|
string memory name_,
|
||||||
string memory symbol_,
|
string memory symbol_,
|
||||||
IERC20[] memory _tokens,
|
IERC20[] memory tokens_,
|
||||||
uint256[] memory _bases,
|
uint256[] memory bases_,
|
||||||
int128 _kappa,
|
int128 kappa_,
|
||||||
uint256 _swapFeePpm,
|
uint256 swapFeePpm_,
|
||||||
uint256 _flashFeePpm,
|
uint256 flashFeePpm_
|
||||||
bool _stable
|
|
||||||
) ERC20(name_, symbol_) {
|
) ERC20(name_, symbol_) {
|
||||||
require(_tokens.length > 1, "Pool: need >1 asset");
|
kappa = kappa_;
|
||||||
require(_tokens.length == _bases.length, "Pool: lengths mismatch");
|
require(swapFeePpm_ < 1_000_000, "Pool: fee >= ppm");
|
||||||
tokens = _tokens;
|
swapFeePpm = swapFeePpm_;
|
||||||
bases = _bases;
|
require(flashFeePpm_ < 1_000_000, "Pool: flash fee >= ppm");
|
||||||
kappa = _kappa;
|
flashFeePpm = flashFeePpm_;
|
||||||
require(_swapFeePpm < 1_000_000, "Pool: fee >= ppm");
|
|
||||||
swapFeePpm = _swapFeePpm;
|
|
||||||
require(_flashFeePpm < 1_000_000, "Pool: flash fee >= ppm");
|
|
||||||
flashFeePpm = _flashFeePpm;
|
|
||||||
_stablePair = _stable && _tokens.length == 2;
|
|
||||||
|
|
||||||
uint256 n = _tokens.length;
|
// Initialize state using library
|
||||||
|
s.initialize(tokens_, bases_);
|
||||||
// Initialize LMSR state nAssets; full init occurs on first mint when quantities are known.
|
|
||||||
lmsr.nAssets = n;
|
|
||||||
|
|
||||||
// Initialize token address to index mapping
|
|
||||||
for (uint i = 0; i < n;) {
|
|
||||||
tokenAddressToIndexPlusOne[_tokens[i]] = i + 1;
|
|
||||||
unchecked {i++;}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize caches to zero
|
|
||||||
cachedUintBalances = new uint256[](n);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ----------------------
|
/* ----------------------
|
||||||
Initialization / Mint / Burn (LP token managed)
|
Initialization / Mint / Burn (LP token managed)
|
||||||
---------------------- */
|
---------------------- */
|
||||||
|
|
||||||
/// @inheritdoc IPartyPool
|
/// @inheritdoc IPartyPool
|
||||||
function mintDepositAmounts(uint256 lpTokenAmount) public view returns (uint256[] memory depositAmounts) {
|
function mintDepositAmounts(uint256 lpTokenAmount) public view returns (uint256[] memory depositAmounts) {
|
||||||
uint256 n = tokens.length;
|
return s.mintDepositAmounts(lpTokenAmount, totalSupply());
|
||||||
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 Initial mint to set up pool for the first time.
|
/// @notice Initial mint to set up pool for the first time.
|
||||||
@@ -170,37 +97,8 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
|
|||||||
/// @param lpTokens The number of LP tokens to issue for this mint. If 0, then the number of tokens returned will equal the LMSR internal q total
|
/// @param lpTokens The number of LP tokens to issue for this mint. If 0, then the number of tokens returned will equal the LMSR internal q total
|
||||||
function initialMint(address receiver, uint256 lpTokens) external nonReentrant
|
function initialMint(address receiver, uint256 lpTokens) external nonReentrant
|
||||||
returns (uint256 lpMinted) {
|
returns (uint256 lpMinted) {
|
||||||
uint256 n = tokens.length;
|
lpMinted = s.initialMint(receiver, lpTokens, kappa, totalSupply());
|
||||||
|
|
||||||
// Check if this is initial deposit - revert if not
|
|
||||||
bool isInitialDeposit = totalSupply() == 0 || lmsr.nAssets == 0;
|
|
||||||
require(isInitialDeposit, "initialMint: pool already initialized");
|
|
||||||
|
|
||||||
// Update cached balances for all assets
|
|
||||||
int128[] memory newQInternal = new int128[](n);
|
|
||||||
uint256[] memory depositAmounts = new uint256[](n);
|
|
||||||
for (uint i = 0; i < n; ) {
|
|
||||||
uint256 bal = IERC20(tokens[i]).balanceOf(address(this));
|
|
||||||
cachedUintBalances[i] = bal;
|
|
||||||
newQInternal[i] = _uintToInternalFloor(bal, bases[i]);
|
|
||||||
depositAmounts[i] = bal;
|
|
||||||
unchecked { i++; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize the stabilized LMSR state with provided kappa
|
|
||||||
lmsr.init(newQInternal, kappa);
|
|
||||||
|
|
||||||
// Compute actual LP tokens to mint based on size metric (scaled)
|
|
||||||
if( lpTokens != 0 )
|
|
||||||
lpMinted = lpTokens;
|
|
||||||
else {
|
|
||||||
int128 newTotal = _computeSizeMetric(newQInternal);
|
|
||||||
lpMinted = ABDKMath64x64.mulu(newTotal, LP_SCALE);
|
|
||||||
}
|
|
||||||
|
|
||||||
require(lpMinted > 0, "initialMint: zero LP amount");
|
|
||||||
_mint(receiver, lpMinted);
|
_mint(receiver, lpMinted);
|
||||||
emit Mint(address(0), receiver, depositAmounts, lpMinted);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Proportional mint for existing pool.
|
/// @notice Proportional mint for existing pool.
|
||||||
@@ -213,94 +111,13 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
|
|||||||
/// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore.
|
/// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore.
|
||||||
function mint(address payer, address receiver, uint256 lpTokenAmount, uint256 deadline) external nonReentrant
|
function mint(address payer, address receiver, uint256 lpTokenAmount, uint256 deadline) external nonReentrant
|
||||||
returns (uint256 lpMinted) {
|
returns (uint256 lpMinted) {
|
||||||
require(deadline == 0 || block.timestamp <= deadline, "mint: deadline exceeded");
|
lpMinted = s.mint(payer, receiver, lpTokenAmount, deadline, totalSupply());
|
||||||
uint256 n = tokens.length;
|
_mint(receiver, lpMinted);
|
||||||
|
|
||||||
// Check if this is NOT initial deposit - revert if it is
|
|
||||||
bool isInitialDeposit = totalSupply() == 0 || lmsr.nAssets == 0;
|
|
||||||
require(!isInitialDeposit, "mint: use initialMint for pool initialization");
|
|
||||||
require(lpTokenAmount > 0, "mint: zero LP amount");
|
|
||||||
|
|
||||||
// Capture old pool size metric (scaled) by computing from current balances
|
|
||||||
int128 oldTotal = _computeSizeMetric(lmsr.qInternal);
|
|
||||||
uint256 oldScaled = ABDKMath64x64.mulu(oldTotal, LP_SCALE);
|
|
||||||
|
|
||||||
// Calculate required deposit amounts for the desired LP tokens
|
|
||||||
uint256[] memory depositAmounts = mintDepositAmounts(lpTokenAmount);
|
|
||||||
|
|
||||||
// Transfer in all token amounts
|
|
||||||
for (uint i = 0; i < n; ) {
|
|
||||||
if (depositAmounts[i] > 0) {
|
|
||||||
tokens[i].safeTransferFrom(payer, address(this), depositAmounts[i]);
|
|
||||||
}
|
|
||||||
unchecked { i++; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update cached balances 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++; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure the calculated LP amount is not too different from requested
|
|
||||||
require(actualLpToMint > 0, "mint: zero LP minted");
|
|
||||||
|
|
||||||
// Allow actual amount to be at most 0.00001% less than requested
|
|
||||||
// This accounts for rounding in deposit calculations
|
|
||||||
uint256 minAcceptable = lpTokenAmount * 99_999 / 100_000;
|
|
||||||
require(actualLpToMint >= minAcceptable, "mint: insufficient LP minted");
|
|
||||||
|
|
||||||
_mint(receiver, actualLpToMint);
|
|
||||||
emit Mint(payer, receiver, depositAmounts, actualLpToMint);
|
|
||||||
return actualLpToMint;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @inheritdoc IPartyPool
|
/// @inheritdoc IPartyPool
|
||||||
function burnReceiveAmounts(uint256 lpTokenAmount) external view returns (uint256[] memory withdrawAmounts) {
|
function burnReceiveAmounts(uint256 lpTokenAmount) external view returns (uint256[] memory withdrawAmounts) {
|
||||||
return _burnReceiveAmounts(lpTokenAmount);
|
return s.burnReceiveAmounts(lpTokenAmount, totalSupply());
|
||||||
}
|
|
||||||
|
|
||||||
function _burnReceiveAmounts(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 Burn LP tokens and withdraw the proportional basket to receiver.
|
/// @notice Burn LP tokens and withdraw the proportional basket to receiver.
|
||||||
@@ -311,67 +128,15 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
|
|||||||
/// @param lpAmount amount of LP tokens to burn (proportional withdrawal)
|
/// @param lpAmount amount of LP tokens to burn (proportional withdrawal)
|
||||||
/// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore.
|
/// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore.
|
||||||
function burn(address payer, address receiver, uint256 lpAmount, uint256 deadline) external nonReentrant {
|
function burn(address payer, address receiver, uint256 lpAmount, uint256 deadline) external nonReentrant {
|
||||||
require(deadline == 0 || block.timestamp <= deadline, "burn: deadline exceeded");
|
/* uint256[] memory withdrawAmounts = */ s.burn(payer, receiver, lpAmount, deadline, totalSupply(), balanceOf(payer));
|
||||||
uint256 n = tokens.length;
|
|
||||||
require(lpAmount > 0, "burn: zero lp");
|
|
||||||
|
|
||||||
uint256 supply = totalSupply();
|
// Handle LP token burning with allowance
|
||||||
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 = _burnReceiveAmounts(lpAmount);
|
|
||||||
|
|
||||||
// Transfer underlying tokens out to receiver according to computed proportions
|
|
||||||
for (uint i = 0; i < n; ) {
|
|
||||||
if (withdrawAmounts[i] > 0) {
|
|
||||||
tokens[i].safeTransfer(receiver, withdrawAmounts[i]);
|
|
||||||
}
|
|
||||||
unchecked { i++; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update cached balances and internal q for all assets
|
|
||||||
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) {
|
if (msg.sender != payer) {
|
||||||
uint256 allowed = allowance(payer, msg.sender);
|
uint256 allowed = allowance(payer, msg.sender);
|
||||||
require(allowed >= lpAmount, "burn: allowance insufficient");
|
require(allowed >= lpAmount, "burn: allowance insufficient");
|
||||||
_approve(payer, msg.sender, allowed - lpAmount);
|
_approve(payer, msg.sender, allowed - lpAmount);
|
||||||
}
|
}
|
||||||
_burn(payer, lpAmount);
|
_burn(payer, lpAmount);
|
||||||
|
|
||||||
emit Burn(payer, receiver, withdrawAmounts, lpAmount);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ----------------------
|
/* ----------------------
|
||||||
@@ -384,9 +149,8 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
|
|||||||
uint256 outputTokenIndex,
|
uint256 outputTokenIndex,
|
||||||
uint256 maxAmountIn,
|
uint256 maxAmountIn,
|
||||||
int128 limitPrice
|
int128 limitPrice
|
||||||
) external view returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
|
) virtual external view returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
|
||||||
(uint256 grossIn, uint256 outUint,,,, uint256 feeUint) = _quoteSwapExactIn(inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice);
|
return s.swapAmounts(inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice, swapFeePpm);
|
||||||
return (grossIn, outUint, feeUint);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @inheritdoc IPartyPool
|
/// @inheritdoc IPartyPool
|
||||||
@@ -395,22 +159,18 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
|
|||||||
uint256 outputTokenIndex,
|
uint256 outputTokenIndex,
|
||||||
int128 limitPrice
|
int128 limitPrice
|
||||||
) external view returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
|
) external view returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
|
||||||
(uint256 grossIn, uint256 outUint,,,, uint256 feeUint) = _quoteSwapToLimit(inputTokenIndex, outputTokenIndex, limitPrice);
|
return s.swapToLimitAmounts(inputTokenIndex, outputTokenIndex, limitPrice, swapFeePpm);
|
||||||
return (grossIn, outUint, feeUint);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// @inheritdoc IPartyPool
|
||||||
|
function swapMintAmounts(
|
||||||
|
uint256 inputTokenIndex,
|
||||||
|
uint256 maxAmountIn
|
||||||
|
) external view returns (uint256 totalTransfer, uint256 amountIn, uint256 fee, uint256 lpMinted) {
|
||||||
|
return s.swapMintAmounts(inputTokenIndex, maxAmountIn, swapFeePpm, totalSupply());
|
||||||
|
}
|
||||||
|
|
||||||
/// @notice Swap input token i -> token j. Payer must approve token i.
|
/// @inheritdoc IPartyPool
|
||||||
/// @dev This function transfers the exact gross input (including fee) from payer and sends the computed output to receiver.
|
|
||||||
/// Non-standard tokens (fee-on-transfer, rebasers) are rejected via balance checks.
|
|
||||||
/// @param payer address of the account that pays for the swap
|
|
||||||
/// @param receiver address that will receive the output tokens
|
|
||||||
/// @param inputTokenIndex index of input asset
|
|
||||||
/// @param outputTokenIndex 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), fee fee taken from the input (uint256)
|
|
||||||
function swap(
|
function swap(
|
||||||
address payer,
|
address payer,
|
||||||
address receiver,
|
address receiver,
|
||||||
@@ -419,40 +179,8 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
|
|||||||
uint256 maxAmountIn,
|
uint256 maxAmountIn,
|
||||||
int128 limitPrice,
|
int128 limitPrice,
|
||||||
uint256 deadline
|
uint256 deadline
|
||||||
) external nonReentrant returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
|
) virtual external nonReentrant returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
|
||||||
uint256 n = tokens.length;
|
return s.swap(payer, receiver, inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice, deadline, swapFeePpm);
|
||||||
require(inputTokenIndex < n && outputTokenIndex < n, "swap: idx");
|
|
||||||
require(maxAmountIn > 0, "swap: input zero");
|
|
||||||
require(deadline == 0 || block.timestamp <= deadline, "swap: deadline exceeded");
|
|
||||||
|
|
||||||
// Read previous balances for affected assets
|
|
||||||
uint256 prevBalI = IERC20(tokens[inputTokenIndex]).balanceOf(address(this));
|
|
||||||
uint256 prevBalJ = IERC20(tokens[outputTokenIndex]).balanceOf(address(this));
|
|
||||||
|
|
||||||
// Compute amounts using the same path as views
|
|
||||||
(uint256 totalTransferAmount, uint256 amountOutUint, int128 amountInInternalUsed, int128 amountOutInternal, , uint256 feeUint) =
|
|
||||||
_quoteSwapExactIn(inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice);
|
|
||||||
|
|
||||||
// Transfer the exact amount from payer and require exact receipt (revert on fee-on-transfer)
|
|
||||||
tokens[inputTokenIndex].safeTransferFrom(payer, address(this), totalTransferAmount);
|
|
||||||
uint256 balIAfter = IERC20(tokens[inputTokenIndex]).balanceOf(address(this));
|
|
||||||
require(balIAfter == prevBalI + totalTransferAmount, "swap: non-standard tokenIn");
|
|
||||||
|
|
||||||
// Transfer output to receiver and verify exact decrease
|
|
||||||
tokens[outputTokenIndex].safeTransfer(receiver, amountOutUint);
|
|
||||||
uint256 balJAfter = IERC20(tokens[outputTokenIndex]).balanceOf(address(this));
|
|
||||||
require(balJAfter == prevBalJ - amountOutUint, "swap: non-standard tokenOut");
|
|
||||||
|
|
||||||
// Update cached uint balances for i and j using actual balances
|
|
||||||
cachedUintBalances[inputTokenIndex] = balIAfter;
|
|
||||||
cachedUintBalances[outputTokenIndex] = balJAfter;
|
|
||||||
|
|
||||||
// Apply swap to LMSR state with the internal amounts actually used
|
|
||||||
lmsr.applySwap(inputTokenIndex, outputTokenIndex, amountInInternalUsed, amountOutInternal);
|
|
||||||
|
|
||||||
emit Swap(payer, receiver, tokens[inputTokenIndex], tokens[outputTokenIndex], totalTransferAmount, amountOutUint);
|
|
||||||
|
|
||||||
return (totalTransferAmount, amountOutUint, feeUint);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Swap up to the price limit; computes max input to reach limit then performs swap.
|
/// @notice Swap up to the price limit; computes max input to reach limit then performs swap.
|
||||||
@@ -467,176 +195,9 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
|
|||||||
int128 limitPrice,
|
int128 limitPrice,
|
||||||
uint256 deadline
|
uint256 deadline
|
||||||
) external returns (uint256 amountInUsed, uint256 amountOut, uint256 fee) {
|
) external returns (uint256 amountInUsed, uint256 amountOut, uint256 fee) {
|
||||||
uint256 n = tokens.length;
|
return s.swapToLimit(payer, receiver, inputTokenIndex, outputTokenIndex, limitPrice, deadline, swapFeePpm);
|
||||||
require(inputTokenIndex < n && outputTokenIndex < n, "swapToLimit: idx");
|
|
||||||
require(limitPrice > int128(0), "swapToLimit: limit <= 0");
|
|
||||||
require(deadline == 0 || block.timestamp <= deadline, "swapToLimit: deadline exceeded");
|
|
||||||
|
|
||||||
// Read previous balances for affected assets
|
|
||||||
uint256 prevBalI = IERC20(tokens[inputTokenIndex]).balanceOf(address(this));
|
|
||||||
uint256 prevBalJ = IERC20(tokens[outputTokenIndex]).balanceOf(address(this));
|
|
||||||
|
|
||||||
// Compute amounts using the same path as views
|
|
||||||
(uint256 totalTransferAmount, uint256 amountOutUint, int128 amountInInternalMax, int128 amountOutInternal, uint256 amountInUsedUint, uint256 feeUint) =
|
|
||||||
_quoteSwapToLimit(inputTokenIndex, outputTokenIndex, limitPrice);
|
|
||||||
|
|
||||||
// Transfer the exact amount needed from payer and require exact receipt (revert on fee-on-transfer)
|
|
||||||
tokens[inputTokenIndex].safeTransferFrom(payer, address(this), totalTransferAmount);
|
|
||||||
uint256 balIAfter = IERC20(tokens[inputTokenIndex]).balanceOf(address(this));
|
|
||||||
require(balIAfter == prevBalI + totalTransferAmount, "swapToLimit: non-standard tokenIn");
|
|
||||||
|
|
||||||
// Transfer output to receiver and verify exact decrease
|
|
||||||
tokens[outputTokenIndex].safeTransfer(receiver, amountOutUint);
|
|
||||||
uint256 balJAfter = IERC20(tokens[outputTokenIndex]).balanceOf(address(this));
|
|
||||||
require(balJAfter == prevBalJ - amountOutUint, "swapToLimit: non-standard tokenOut");
|
|
||||||
|
|
||||||
// Update caches to actual balances
|
|
||||||
cachedUintBalances[inputTokenIndex] = balIAfter;
|
|
||||||
cachedUintBalances[outputTokenIndex] = balJAfter;
|
|
||||||
|
|
||||||
// Apply swap to LMSR state with the internal amounts
|
|
||||||
lmsr.applySwap(inputTokenIndex, outputTokenIndex, amountInInternalMax, amountOutInternal);
|
|
||||||
|
|
||||||
// Maintain original event semantics (logs input without fee)
|
|
||||||
emit Swap(payer, receiver, tokens[inputTokenIndex], tokens[outputTokenIndex], amountInUsedUint, amountOutUint);
|
|
||||||
|
|
||||||
return (amountInUsedUint, amountOutUint, feeUint);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Ceiling fee helper: computes ceil(x * feePpm / 1_000_000)
|
|
||||||
/// @dev Internal helper; public-facing functions use this to ensure fees round up in favor of pool.
|
|
||||||
function _ceilFee(uint256 x, uint256 feePpm) internal pure returns (uint256) {
|
|
||||||
if (feePpm == 0) return 0;
|
|
||||||
// ceil division: (num + denom - 1) / denom
|
|
||||||
return (x * feePpm + 1_000_000 - 1) / 1_000_000;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @notice Internal quote for exact-input swap that mirrors swap() rounding and fee application
|
|
||||||
/// @dev Returns amounts consistent with swap() semantics: grossIn includes fees (ceil), amountOut is floored.
|
|
||||||
/// @return grossIn amount to transfer in (inclusive of fee), amountOutUint output amount (uint),
|
|
||||||
/// amountInInternalUsed and amountOutInternal (64.64), amountInUintNoFee input amount excluding fee (uint),
|
|
||||||
/// feeUint fee taken from the gross input (uint)
|
|
||||||
function _quoteSwapExactIn(
|
|
||||||
uint256 inputTokenIndex,
|
|
||||||
uint256 outputTokenIndex,
|
|
||||||
uint256 maxAmountIn,
|
|
||||||
int128 limitPrice
|
|
||||||
)
|
|
||||||
internal
|
|
||||||
view
|
|
||||||
returns (
|
|
||||||
uint256 grossIn,
|
|
||||||
uint256 amountOutUint,
|
|
||||||
int128 amountInInternalUsed,
|
|
||||||
int128 amountOutInternal,
|
|
||||||
uint256 amountInUintNoFee,
|
|
||||||
uint256 feeUint
|
|
||||||
)
|
|
||||||
{
|
|
||||||
uint256 n = tokens.length;
|
|
||||||
require(inputTokenIndex < n && outputTokenIndex < n, "swap: idx");
|
|
||||||
require(maxAmountIn > 0, "swap: input zero");
|
|
||||||
require(lmsr.nAssets > 0, "swap: empty pool");
|
|
||||||
|
|
||||||
// Estimate max net input (fee on gross rounded up, then subtract)
|
|
||||||
(, uint256 netUintForSwap) = _computeFee(maxAmountIn);
|
|
||||||
|
|
||||||
// Convert to internal (floor)
|
|
||||||
int128 deltaInternalI = _uintToInternalFloor(netUintForSwap, bases[inputTokenIndex]);
|
|
||||||
require(deltaInternalI > int128(0), "swap: input too small after fee");
|
|
||||||
|
|
||||||
// Compute internal amounts using LMSR (exact-input with price limit)
|
|
||||||
// if _stablePair is true, use the optimized path
|
|
||||||
console2.log('stablepair optimization?', _stablePair);
|
|
||||||
(amountInInternalUsed, amountOutInternal) =
|
|
||||||
_stablePair ? LMSRStabilizedBalancedPair.swapAmountsForExactInput(lmsr, inputTokenIndex, outputTokenIndex, deltaInternalI, limitPrice)
|
|
||||||
: lmsr.swapAmountsForExactInput(inputTokenIndex, outputTokenIndex, deltaInternalI, limitPrice);
|
|
||||||
|
|
||||||
// Convert actual used input internal -> uint (ceil)
|
|
||||||
amountInUintNoFee = _internalToUintCeil(amountInInternalUsed, bases[inputTokenIndex]);
|
|
||||||
require(amountInUintNoFee > 0, "swap: input zero");
|
|
||||||
|
|
||||||
// Compute gross transfer including fee on the used input (ceil)
|
|
||||||
feeUint = 0;
|
|
||||||
grossIn = amountInUintNoFee;
|
|
||||||
if (swapFeePpm > 0) {
|
|
||||||
feeUint = _ceilFee(amountInUintNoFee, swapFeePpm);
|
|
||||||
grossIn += feeUint;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure within user max
|
|
||||||
require(grossIn <= maxAmountIn, "swap: transfer exceeds max");
|
|
||||||
|
|
||||||
// Compute output (floor)
|
|
||||||
amountOutUint = _internalToUintFloor(amountOutInternal, bases[outputTokenIndex]);
|
|
||||||
require(amountOutUint > 0, "swap: output zero");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @notice Internal quote for swap-to-limit that mirrors swapToLimit() rounding and fee application
|
|
||||||
/// @dev Computes the input required to reach limitPrice and the resulting output; all rounding matches swapToLimit.
|
|
||||||
/// @return grossIn amount to transfer in (inclusive of fee), amountOutUint output amount (uint),
|
|
||||||
/// amountInInternal and amountOutInternal (64.64), amountInUintNoFee input amount excluding fee (uint),
|
|
||||||
/// feeUint fee taken from the gross input (uint)
|
|
||||||
function _quoteSwapToLimit(
|
|
||||||
uint256 inputTokenIndex,
|
|
||||||
uint256 outputTokenIndex,
|
|
||||||
int128 limitPrice
|
|
||||||
)
|
|
||||||
internal
|
|
||||||
view
|
|
||||||
returns (
|
|
||||||
uint256 grossIn,
|
|
||||||
uint256 amountOutUint,
|
|
||||||
int128 amountInInternal,
|
|
||||||
int128 amountOutInternal,
|
|
||||||
uint256 amountInUintNoFee,
|
|
||||||
uint256 feeUint
|
|
||||||
)
|
|
||||||
{
|
|
||||||
uint256 n = tokens.length;
|
|
||||||
require(inputTokenIndex < n && outputTokenIndex < n, "swapToLimit: idx");
|
|
||||||
require(limitPrice > int128(0), "swapToLimit: limit <= 0");
|
|
||||||
require(lmsr.nAssets > 0, "swapToLimit: pool uninitialized");
|
|
||||||
|
|
||||||
// Compute internal maxima at the price limit
|
|
||||||
(amountInInternal, amountOutInternal) = lmsr.swapAmountsForPriceLimit(inputTokenIndex, outputTokenIndex, limitPrice);
|
|
||||||
|
|
||||||
// Convert input to uint (ceil) and output to uint (floor)
|
|
||||||
amountInUintNoFee = _internalToUintCeil(amountInInternal, bases[inputTokenIndex]);
|
|
||||||
require(amountInUintNoFee > 0, "swapToLimit: input zero");
|
|
||||||
|
|
||||||
feeUint = 0;
|
|
||||||
grossIn = amountInUintNoFee;
|
|
||||||
if (swapFeePpm > 0) {
|
|
||||||
feeUint = _ceilFee(amountInUintNoFee, swapFeePpm);
|
|
||||||
grossIn += feeUint;
|
|
||||||
}
|
|
||||||
|
|
||||||
amountOutUint = _internalToUintFloor(amountOutInternal, bases[outputTokenIndex]);
|
|
||||||
require(amountOutUint > 0, "swapToLimit: output zero");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @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.
|
/// @notice Single-token mint: deposit a single token, charge swap-LMSR cost, and mint LP.
|
||||||
/// @dev swapMint executes as an exact-in planned swap followed by proportional scaling of qInternal.
|
/// @dev swapMint executes as an exact-in planned swap followed by proportional scaling of qInternal.
|
||||||
/// The function emits SwapMint (gross, net, fee) and also emits Mint for LP issuance.
|
/// The function emits SwapMint (gross, net, fee) and also emits Mint for LP issuance.
|
||||||
@@ -653,90 +214,16 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
|
|||||||
uint256 maxAmountIn,
|
uint256 maxAmountIn,
|
||||||
uint256 deadline
|
uint256 deadline
|
||||||
) external nonReentrant returns (uint256 lpMinted) {
|
) external nonReentrant returns (uint256 lpMinted) {
|
||||||
uint256 n = tokens.length;
|
lpMinted = s.swapMint(payer, receiver, inputTokenIndex, maxAmountIn, deadline, swapFeePpm, totalSupply());
|
||||||
require(inputTokenIndex < n, "swapMint: idx");
|
_mint(receiver, lpMinted);
|
||||||
require(maxAmountIn > 0, "swapMint: input zero");
|
}
|
||||||
require(deadline == 0 || block.timestamp <= deadline, "swapMint: deadline");
|
|
||||||
|
|
||||||
// Ensure pool initialized
|
/// @inheritdoc IPartyPool
|
||||||
require(lmsr.nAssets > 0, "swapMint: uninit pool");
|
function burnSwapAmounts(
|
||||||
|
uint256 lpAmount,
|
||||||
// compute fee on gross maxAmountIn to get an initial net estimate (we'll recompute based on actual used)
|
uint256 inputTokenIndex
|
||||||
(, uint256 netUintGuess) = _computeFee(maxAmountIn);
|
) external view returns (uint256 amountOut) {
|
||||||
|
return s.burnSwapAmounts(lpAmount, inputTokenIndex, swapFeePpm, totalSupply());
|
||||||
// Convert the net guess to internal (floor)
|
|
||||||
int128 netInternalGuess = _uintToInternalFloor(netUintGuess, bases[inputTokenIndex]);
|
|
||||||
require(netInternalGuess > int128(0), "swapMint: input too small after fee");
|
|
||||||
|
|
||||||
// Use LMSR view to determine actual internal consumed and size-increase (ΔS) for mint
|
|
||||||
(int128 amountInInternalUsed, int128 sizeIncreaseInternal) = lmsr.swapAmountsForMint(inputTokenIndex, netInternalGuess);
|
|
||||||
|
|
||||||
// amountInInternalUsed may be <= netInternalGuess. Convert to uint (ceil) to determine actual transfer
|
|
||||||
uint256 amountInUint = _internalToUintCeil(amountInInternalUsed, bases[inputTokenIndex]);
|
|
||||||
require(amountInUint > 0, "swapMint: input zero after internal conversion");
|
|
||||||
|
|
||||||
// Compute fee on the actual used input and total transfer amount (ceiling)
|
|
||||||
uint256 feeUintActual = _ceilFee(amountInUint, swapFeePpm);
|
|
||||||
uint256 totalTransfer = amountInUint + feeUintActual;
|
|
||||||
require(totalTransfer > 0 && totalTransfer <= maxAmountIn, "swapMint: transfer exceeds max");
|
|
||||||
|
|
||||||
// Record pre-balance and transfer tokens from payer, require exact receipt (revert on fee-on-transfer)
|
|
||||||
uint256 prevBalI = IERC20(tokens[inputTokenIndex]).balanceOf(address(this));
|
|
||||||
tokens[inputTokenIndex].safeTransferFrom(payer, address(this), totalTransfer);
|
|
||||||
uint256 balIAfter = IERC20(tokens[inputTokenIndex]).balanceOf(address(this));
|
|
||||||
require(balIAfter == prevBalI + totalTransfer, "swapMint: non-standard tokenIn");
|
|
||||||
|
|
||||||
// Update cached uint balances for token inputTokenIndex (only inputTokenIndex changed externally)
|
|
||||||
cachedUintBalances[inputTokenIndex] = 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[inputTokenIndex] 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, inputTokenIndex, 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 `inputTokenIndex` and send to receiver.
|
/// @notice Burn LP tokens then swap the redeemed proportional basket into a single asset `inputTokenIndex` and send to receiver.
|
||||||
@@ -754,76 +241,23 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
|
|||||||
uint256 inputTokenIndex,
|
uint256 inputTokenIndex,
|
||||||
uint256 deadline
|
uint256 deadline
|
||||||
) external nonReentrant returns (uint256 amountOutUint) {
|
) external nonReentrant returns (uint256 amountOutUint) {
|
||||||
uint256 n = tokens.length;
|
amountOutUint = s.burnSwap(payer, receiver, lpAmount, inputTokenIndex, deadline, swapFeePpm, totalSupply(), balanceOf(payer));
|
||||||
require(inputTokenIndex < n, "burnSwap: idx");
|
|
||||||
require(lpAmount > 0, "burnSwap: zero lp");
|
|
||||||
require(deadline == 0 || block.timestamp <= deadline, "burnSwap: deadline");
|
|
||||||
|
|
||||||
uint256 supply = totalSupply();
|
// Handle LP token burning with allowance
|
||||||
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(inputTokenIndex, alpha);
|
|
||||||
|
|
||||||
// Convert payoutInternal -> uint (floor) to favor pool
|
|
||||||
amountOutUint = _internalToUintFloor(payoutInternal, bases[inputTokenIndex]);
|
|
||||||
require(amountOutUint > 0, "burnSwap: output zero");
|
|
||||||
|
|
||||||
// Transfer the payout to receiver
|
|
||||||
tokens[inputTokenIndex].safeTransfer(receiver, amountOutUint);
|
|
||||||
|
|
||||||
// Burn LP tokens from payer (authorization via allowance)
|
|
||||||
if (msg.sender != payer) {
|
if (msg.sender != payer) {
|
||||||
uint256 allowed = allowance(payer, msg.sender);
|
uint256 allowed = allowance(payer, msg.sender);
|
||||||
require(allowed >= lpAmount, "burnSwap: allowance insufficient");
|
require(allowed >= lpAmount, "burnSwap: allowance insufficient");
|
||||||
_approve(payer, msg.sender, allowed - lpAmount);
|
_approve(payer, msg.sender, allowed - lpAmount);
|
||||||
}
|
}
|
||||||
_burn(payer, 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, inputTokenIndex, amountOutUint);
|
|
||||||
|
|
||||||
// If entire pool drained, deinit; else update proportionally
|
|
||||||
bool allZero = true;
|
|
||||||
for (uint256 idx = 0; idx < n; idx++) {
|
|
||||||
if (newQInternal[idx] != int128(0)) { allZero = false; break; }
|
|
||||||
}
|
|
||||||
if (allZero) {
|
|
||||||
lmsr.deinit();
|
|
||||||
} else {
|
|
||||||
lmsr.updateForProportionalChange(newQInternal);
|
|
||||||
}
|
|
||||||
|
|
||||||
emit Burn(payer, receiver, new uint256[](n), lpAmount);
|
|
||||||
return amountOutUint;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// @inheritdoc IPartyPool
|
/// @inheritdoc IPartyPool
|
||||||
function flashRepaymentAmounts(uint256[] memory loanAmounts) external view
|
function flashRepaymentAmounts(uint256[] memory loanAmounts) external view
|
||||||
returns (uint256[] memory repaymentAmounts) {
|
returns (uint256[] memory repaymentAmounts) {
|
||||||
repaymentAmounts = new uint256[](tokens.length);
|
return s.flashRepaymentAmounts(loanAmounts, flashFeePpm);
|
||||||
for (uint256 i = 0; i < tokens.length; i++) {
|
|
||||||
uint256 amount = loanAmounts[i];
|
|
||||||
if (amount > 0) {
|
|
||||||
repaymentAmounts[i] = amount + _ceilFee(amount, flashFeePpm);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// @notice Receive token amounts and require them to be repaid plus a fee inside a callback.
|
/// @notice Receive token amounts and require them to be repaid plus a fee inside a callback.
|
||||||
/// @dev The caller must implement IPartyFlashCallback#partyFlashCallback which receives (amounts, repaymentAmounts, data).
|
/// @dev The caller must implement IPartyFlashCallback#partyFlashCallback which receives (amounts, repaymentAmounts, data).
|
||||||
/// This function verifies that, after the callback returns, the pool's balances have increased by at least the fees
|
/// This function verifies that, after the callback returns, the pool's balances have increased by at least the fees
|
||||||
@@ -836,137 +270,19 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
|
|||||||
uint256[] memory amounts,
|
uint256[] memory amounts,
|
||||||
bytes calldata data
|
bytes calldata data
|
||||||
) external nonReentrant {
|
) external nonReentrant {
|
||||||
require(recipient != address(0), "flash: zero recipient");
|
s.flash(recipient, amounts, data, flashFeePpm);
|
||||||
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
|
|
||||||
tokens[i].safeTransfer(recipient, amount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure at least one token is being borrowed
|
|
||||||
require(hasNonZeroAmount, "flash: no tokens requested");
|
|
||||||
|
|
||||||
// Call flash callback with expected repayment amounts
|
|
||||||
IPartyFlashCallback(msg.sender).partyFlashCallback(amounts, repaymentAmounts, data);
|
|
||||||
|
|
||||||
// Verify repayment amounts for tokens that were borrowed
|
|
||||||
for (uint256 i = 0; i < 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Marginal price of `base` in terms of `quote` (p_quote / p_base) as Q64.64
|
/// @notice Marginal price of `base` in terms of `quote` (p_quote / p_base) as Q64.64
|
||||||
/// @dev Returns the LMSR marginal price directly (raw 64.64) for use by off-chain quoting logic.
|
/// @dev Returns the LMSR marginal price directly (raw 64.64) for use by off-chain quoting logic.
|
||||||
function price(uint256 baseTokenIndex, uint256 quoteTokenIndex) external view returns (int128) {
|
function price(uint256 baseTokenIndex, uint256 quoteTokenIndex) external view returns (int128) {
|
||||||
uint256 n = tokens.length;
|
return s.price(baseTokenIndex, quoteTokenIndex);
|
||||||
require(baseTokenIndex < n && quoteTokenIndex < n, "price: idx");
|
|
||||||
require(lmsr.nAssets > 0, "price: uninit");
|
|
||||||
return lmsr.price(baseTokenIndex, quoteTokenIndex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Price of one LP token denominated in `quote` asset as Q64.64
|
/// @notice Price of one LP token denominated in `quote` asset as Q64.64
|
||||||
/// @dev Computes LMSR poolPrice (quote per unit qTotal) and scales it by totalSupply() / qTotal
|
/// @dev Computes LMSR poolPrice (quote per unit qTotal) and scales it by totalSupply() / qTotal
|
||||||
/// to return price per LP token unit in quote asset (raw 64.64).
|
/// to return price per LP token unit in quote asset (raw 64.64).
|
||||||
function poolPrice(uint256 quoteTokenIndex) external view returns (int128) {
|
function poolPrice(uint256 quoteTokenIndex) external view returns (int128) {
|
||||||
uint256 n = tokens.length;
|
return s.poolPrice(quoteTokenIndex, totalSupply());
|
||||||
require(quoteTokenIndex < n, "poolPrice: idx");
|
|
||||||
require(lmsr.nAssets > 0, "poolPrice: uninit");
|
|
||||||
|
|
||||||
// price per unit of qTotal (Q64.64) from LMSR
|
|
||||||
int128 pricePerQ = lmsr.poolPrice(quoteTokenIndex);
|
|
||||||
|
|
||||||
// total internal q (qTotal) as Q64.64
|
|
||||||
int128 qTotal = _computeSizeMetric(lmsr.qInternal);
|
|
||||||
require(qTotal > int128(0), "poolPrice: qTotal zero");
|
|
||||||
|
|
||||||
// totalSupply as Q64.64
|
|
||||||
uint256 supply = totalSupply();
|
|
||||||
require(supply > 0, "poolPrice: zero supply");
|
|
||||||
int128 supplyQ64 = ABDKMath64x64.fromUInt(supply);
|
|
||||||
|
|
||||||
// factor = totalSupply / qTotal (Q64.64)
|
|
||||||
int128 factor = supplyQ64.div(qTotal);
|
|
||||||
|
|
||||||
// price per LP token = pricePerQ * factor (Q64.64)
|
|
||||||
return pricePerQ.mul(factor);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Helper to compute size metric (sum of all asset quantities) from internal balances
|
|
||||||
/// @dev Returns the sum of all provided qInternal_ entries as a Q64.64 value.
|
|
||||||
function _computeSizeMetric(int128[] memory qInternal_) private pure returns (int128) {
|
|
||||||
int128 total = int128(0);
|
|
||||||
for (uint i = 0; i < qInternal_.length; ) {
|
|
||||||
total = total.add(qInternal_[i]);
|
|
||||||
unchecked { i++; }
|
|
||||||
}
|
|
||||||
return total;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
42
src/PartyPoolBalancedPair.sol
Normal file
42
src/PartyPoolBalancedPair.sol
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
|
pragma solidity ^0.8.30;
|
||||||
|
|
||||||
|
import "./PartyPool.sol";
|
||||||
|
import {PoolLibBalancedPair} from "./PoolLibBalancedPair.sol";
|
||||||
|
|
||||||
|
contract PartyPoolBalancedPair is PartyPool {
|
||||||
|
constructor(
|
||||||
|
string memory name_,
|
||||||
|
string memory symbol_,
|
||||||
|
IERC20[] memory tokens_,
|
||||||
|
uint256[] memory bases_,
|
||||||
|
int128 kappa_,
|
||||||
|
uint256 swapFeePpm_,
|
||||||
|
uint256 flashFeePpm_
|
||||||
|
) PartyPool(name_, symbol_, tokens_, bases_, kappa_, swapFeePpm_, flashFeePpm_) {
|
||||||
|
}
|
||||||
|
|
||||||
|
function swapAmounts(
|
||||||
|
uint256 inputTokenIndex,
|
||||||
|
uint256 outputTokenIndex,
|
||||||
|
uint256 maxAmountIn,
|
||||||
|
int128 limitPrice
|
||||||
|
) virtual override external view returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
|
||||||
|
return PoolLibBalancedPair.swapAmounts(
|
||||||
|
s, inputTokenIndex, outputTokenIndex,
|
||||||
|
maxAmountIn, limitPrice, swapFeePpm);
|
||||||
|
}
|
||||||
|
|
||||||
|
function swap(
|
||||||
|
address payer,
|
||||||
|
address receiver,
|
||||||
|
uint256 inputTokenIndex,
|
||||||
|
uint256 outputTokenIndex,
|
||||||
|
uint256 maxAmountIn,
|
||||||
|
int128 limitPrice,
|
||||||
|
uint256 deadline
|
||||||
|
) virtual override external nonReentrant returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
|
||||||
|
return PoolLibBalancedPair.swap(s, payer, receiver, inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice, deadline, swapFeePpm);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
14
src/PoolBase.sol
Normal file
14
src/PoolBase.sol
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
|
pragma solidity ^0.8.30;
|
||||||
|
|
||||||
|
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||||
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||||
|
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
||||||
|
import "./PoolLib.sol";
|
||||||
|
|
||||||
|
/// @notice This contract has all the storage for PartyPool and can be inherited by implementation contracts that
|
||||||
|
/// are delegate-called
|
||||||
|
abstract contract PoolBase is ERC20, ReentrancyGuard {
|
||||||
|
/// @notice Pool state containing all storage variables
|
||||||
|
PoolLib.State internal s;
|
||||||
|
}
|
||||||
935
src/PoolLib.sol
Normal file
935
src/PoolLib.sol
Normal file
@@ -0,0 +1,935 @@
|
|||||||
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
|
pragma solidity ^0.8.30;
|
||||||
|
|
||||||
|
import "@abdk/ABDKMath64x64.sol";
|
||||||
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||||
|
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||||
|
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||||
|
import "./LMSRStabilized.sol";
|
||||||
|
import "./LMSRStabilizedBalancedPair.sol";
|
||||||
|
import "./IPartyFlashCallback.sol";
|
||||||
|
|
||||||
|
/// @title PoolLib - Library containing all PartyPool implementation logic
|
||||||
|
/// @notice This library contains the core implementation for LMSR-backed multi-asset pools
|
||||||
|
/// @dev All functions are internal and accept State as the first parameter
|
||||||
|
library PoolLib {
|
||||||
|
using ABDKMath64x64 for int128;
|
||||||
|
using LMSRStabilized for LMSRStabilized.State;
|
||||||
|
using SafeERC20 for IERC20;
|
||||||
|
|
||||||
|
/// @notice State struct containing all storage variables from PartyPool
|
||||||
|
/// @dev This struct is passed to all library functions as the first parameter
|
||||||
|
struct State {
|
||||||
|
/// @notice Token addresses comprising the pool
|
||||||
|
IERC20[] tokens;
|
||||||
|
|
||||||
|
/// @notice LMSR state for pricing computations
|
||||||
|
LMSRStabilized.State lmsr;
|
||||||
|
|
||||||
|
/// @notice Cached on-chain balances (uint) for each token
|
||||||
|
uint256[] cachedUintBalances;
|
||||||
|
|
||||||
|
/// @notice Per-token uint base denominators used to convert uint <-> internal Q64.64
|
||||||
|
uint256[] bases;
|
||||||
|
|
||||||
|
/// @notice Mapping from token address => (index+1). Zero means token not in pool.
|
||||||
|
mapping(IERC20=>uint) tokenAddressToIndexPlusOne;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Scale factor used when converting LMSR Q64.64 totals to LP token units (uint)
|
||||||
|
uint256 internal constant LP_SCALE = 1e18;
|
||||||
|
|
||||||
|
// Events that mirror the main contract events
|
||||||
|
event Mint(address indexed payer, address indexed receiver, uint256[] depositAmounts, uint256 lpMinted);
|
||||||
|
event Burn(address indexed payer, address indexed receiver, uint256[] withdrawAmounts, uint256 lpBurned);
|
||||||
|
event Swap(address indexed payer, address indexed receiver, IERC20 indexed tokenIn, IERC20 tokenOut, uint256 amountIn, uint256 amountOut);
|
||||||
|
event SwapMint(address indexed payer, address indexed receiver, uint256 indexed inputTokenIndex, uint256 totalTransfer, uint256 amountInUint, uint256 feeUintActual);
|
||||||
|
event BurnSwap(address indexed payer, address indexed receiver, uint256 indexed inputTokenIndex, uint256 amountOutUint);
|
||||||
|
|
||||||
|
/// @notice Initialize the pool state with tokens and bases
|
||||||
|
/// @param state The pool state
|
||||||
|
/// @param tokens_ Array of token addresses
|
||||||
|
/// @param bases_ Array of base denominators for each token
|
||||||
|
function initialize(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
IERC20[] memory tokens_,
|
||||||
|
uint256[] memory bases_
|
||||||
|
) internal {
|
||||||
|
require(tokens_.length > 1, "Pool: need >1 asset");
|
||||||
|
require(tokens_.length == bases_.length, "Pool: lengths mismatch");
|
||||||
|
|
||||||
|
state.tokens = tokens_;
|
||||||
|
state.bases = bases_;
|
||||||
|
|
||||||
|
uint256 n = tokens_.length;
|
||||||
|
|
||||||
|
// Initialize LMSR state nAssets; full init occurs on first mint
|
||||||
|
state.lmsr.nAssets = n;
|
||||||
|
|
||||||
|
// Initialize token address to index mapping
|
||||||
|
for (uint i = 0; i < n;) {
|
||||||
|
state.tokenAddressToIndexPlusOne[tokens_[i]] = i + 1;
|
||||||
|
unchecked {i++;}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize caches to zero
|
||||||
|
state.cachedUintBalances = new uint256[](n);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Get deposit amounts needed for minting LP tokens
|
||||||
|
function mintDepositAmounts(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
uint256 lpTokenAmount,
|
||||||
|
uint256 totalSupply
|
||||||
|
) internal view returns (uint256[] memory depositAmounts) {
|
||||||
|
uint256 n = state.tokens.length;
|
||||||
|
depositAmounts = new uint256[](n);
|
||||||
|
|
||||||
|
// If this is the first mint or pool is empty, return zeros
|
||||||
|
if (totalSupply == 0 || state.lmsr.nAssets == 0) {
|
||||||
|
return depositAmounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate deposit based on current proportions
|
||||||
|
for (uint i = 0; i < n; i++) {
|
||||||
|
uint256 currentBalance = state.cachedUintBalances[i];
|
||||||
|
// Calculate with rounding up: (a * b + c - 1) / c
|
||||||
|
depositAmounts[i] = (lpTokenAmount * currentBalance + totalSupply - 1) / totalSupply;
|
||||||
|
}
|
||||||
|
|
||||||
|
return depositAmounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Initial mint to set up pool for the first time
|
||||||
|
function initialMint(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
address receiver,
|
||||||
|
uint256 lpTokens,
|
||||||
|
int128 kappa,
|
||||||
|
uint256 totalSupply
|
||||||
|
) internal returns (uint256 lpMinted) {
|
||||||
|
uint256 n = state.tokens.length;
|
||||||
|
|
||||||
|
// Check if this is initial deposit
|
||||||
|
bool isInitialDeposit = totalSupply == 0 || state.lmsr.nAssets == 0;
|
||||||
|
require(isInitialDeposit, "initialMint: pool already initialized");
|
||||||
|
|
||||||
|
// Update cached balances for all assets
|
||||||
|
int128[] memory newQInternal = new int128[](n);
|
||||||
|
uint256[] memory depositAmounts = new uint256[](n);
|
||||||
|
for (uint i = 0; i < n; ) {
|
||||||
|
uint256 bal = IERC20(state.tokens[i]).balanceOf(address(this));
|
||||||
|
state.cachedUintBalances[i] = bal;
|
||||||
|
newQInternal[i] = _uintToInternalFloor(bal, state.bases[i]);
|
||||||
|
depositAmounts[i] = bal;
|
||||||
|
unchecked { i++; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the stabilized LMSR state with provided kappa
|
||||||
|
state.lmsr.init(newQInternal, kappa);
|
||||||
|
|
||||||
|
// Compute actual LP tokens to mint based on size metric (scaled)
|
||||||
|
if( lpTokens != 0 )
|
||||||
|
lpMinted = lpTokens;
|
||||||
|
else {
|
||||||
|
int128 newTotal = _computeSizeMetric(newQInternal);
|
||||||
|
lpMinted = ABDKMath64x64.mulu(newTotal, LP_SCALE);
|
||||||
|
}
|
||||||
|
|
||||||
|
require(lpMinted > 0, "initialMint: zero LP amount");
|
||||||
|
emit Mint(address(0), receiver, depositAmounts, lpMinted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Proportional mint for existing pool
|
||||||
|
function mint(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
address payer,
|
||||||
|
address receiver,
|
||||||
|
uint256 lpTokenAmount,
|
||||||
|
uint256 deadline,
|
||||||
|
uint256 totalSupply
|
||||||
|
) internal returns (uint256 lpMinted) {
|
||||||
|
require(deadline == 0 || block.timestamp <= deadline, "mint: deadline exceeded");
|
||||||
|
uint256 n = state.tokens.length;
|
||||||
|
|
||||||
|
// Check if this is NOT initial deposit
|
||||||
|
bool isInitialDeposit = totalSupply == 0 || state.lmsr.nAssets == 0;
|
||||||
|
require(!isInitialDeposit, "mint: use initialMint for pool initialization");
|
||||||
|
require(lpTokenAmount > 0, "mint: zero LP amount");
|
||||||
|
|
||||||
|
// Capture old pool size metric (scaled)
|
||||||
|
int128 oldTotal = _computeSizeMetric(state.lmsr.qInternal);
|
||||||
|
uint256 oldScaled = ABDKMath64x64.mulu(oldTotal, LP_SCALE);
|
||||||
|
|
||||||
|
// Calculate required deposit amounts
|
||||||
|
uint256[] memory depositAmounts = mintDepositAmounts(state, lpTokenAmount, totalSupply);
|
||||||
|
|
||||||
|
// Transfer in all token amounts
|
||||||
|
for (uint i = 0; i < n; ) {
|
||||||
|
if (depositAmounts[i] > 0) {
|
||||||
|
state.tokens[i].safeTransferFrom(payer, address(this), depositAmounts[i]);
|
||||||
|
}
|
||||||
|
unchecked { i++; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update cached balances for all assets
|
||||||
|
int128[] memory newQInternal = new int128[](n);
|
||||||
|
for (uint i = 0; i < n; ) {
|
||||||
|
uint256 bal = IERC20(state.tokens[i]).balanceOf(address(this));
|
||||||
|
state.cachedUintBalances[i] = bal;
|
||||||
|
newQInternal[i] = _uintToInternalFloor(bal, state.bases[i]);
|
||||||
|
unchecked { i++; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update for proportional change
|
||||||
|
state.lmsr.updateForProportionalChange(newQInternal);
|
||||||
|
|
||||||
|
// Compute actual LP tokens to mint
|
||||||
|
int128 newTotal = _computeSizeMetric(newQInternal);
|
||||||
|
uint256 newScaled = ABDKMath64x64.mulu(newTotal, LP_SCALE);
|
||||||
|
uint256 actualLpToMint;
|
||||||
|
|
||||||
|
require(oldScaled > 0, "mint: oldScaled zero");
|
||||||
|
uint256 delta = (newScaled > oldScaled) ? (newScaled - oldScaled) : 0;
|
||||||
|
if (delta > 0) {
|
||||||
|
actualLpToMint = (totalSupply * delta) / oldScaled;
|
||||||
|
} else {
|
||||||
|
actualLpToMint = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
require(actualLpToMint > 0, "mint: zero LP minted");
|
||||||
|
|
||||||
|
// Allow actual amount to be at most 0.00001% less than requested
|
||||||
|
uint256 minAcceptable = lpTokenAmount * 99_999 / 100_000;
|
||||||
|
require(actualLpToMint >= minAcceptable, "mint: insufficient LP minted");
|
||||||
|
|
||||||
|
emit Mint(payer, receiver, depositAmounts, actualLpToMint);
|
||||||
|
return actualLpToMint;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Get withdrawal amounts for burning LP tokens
|
||||||
|
function burnReceiveAmounts(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
uint256 lpTokenAmount,
|
||||||
|
uint256 totalSupply
|
||||||
|
) internal view returns (uint256[] memory withdrawAmounts) {
|
||||||
|
uint256 n = state.tokens.length;
|
||||||
|
withdrawAmounts = new uint256[](n);
|
||||||
|
|
||||||
|
if (totalSupply == 0 || state.lmsr.nAssets == 0) {
|
||||||
|
return withdrawAmounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint i = 0; i < n; i++) {
|
||||||
|
uint256 currentBalance = state.cachedUintBalances[i];
|
||||||
|
withdrawAmounts[i] = (lpTokenAmount * currentBalance) / totalSupply;
|
||||||
|
}
|
||||||
|
|
||||||
|
return withdrawAmounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Burn LP tokens and withdraw proportional basket
|
||||||
|
function burn(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
address payer,
|
||||||
|
address receiver,
|
||||||
|
uint256 lpAmount,
|
||||||
|
uint256 deadline,
|
||||||
|
uint256 totalSupply,
|
||||||
|
uint256 payerBalance
|
||||||
|
) internal returns (uint256[] memory withdrawAmounts) {
|
||||||
|
require(deadline == 0 || block.timestamp <= deadline, "burn: deadline exceeded");
|
||||||
|
uint256 n = state.tokens.length;
|
||||||
|
require(lpAmount > 0, "burn: zero lp");
|
||||||
|
require(totalSupply > 0, "burn: empty supply");
|
||||||
|
require(state.lmsr.nAssets > 0, "burn: uninit pool");
|
||||||
|
require(payerBalance >= lpAmount, "burn: insufficient LP");
|
||||||
|
|
||||||
|
// Refresh cached balances
|
||||||
|
for (uint i = 0; i < n; ) {
|
||||||
|
uint256 bal = IERC20(state.tokens[i]).balanceOf(address(this));
|
||||||
|
state.cachedUintBalances[i] = bal;
|
||||||
|
unchecked { i++; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute proportional withdrawal amounts
|
||||||
|
withdrawAmounts = burnReceiveAmounts(state, lpAmount, totalSupply);
|
||||||
|
|
||||||
|
// Transfer underlying tokens out
|
||||||
|
for (uint i = 0; i < n; ) {
|
||||||
|
if (withdrawAmounts[i] > 0) {
|
||||||
|
state.tokens[i].safeTransfer(receiver, withdrawAmounts[i]);
|
||||||
|
}
|
||||||
|
unchecked { i++; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update cached balances and internal q
|
||||||
|
int128[] memory newQInternal = new int128[](n);
|
||||||
|
for (uint i = 0; i < n; ) {
|
||||||
|
uint256 bal = IERC20(state.tokens[i]).balanceOf(address(this));
|
||||||
|
state.cachedUintBalances[i] = bal;
|
||||||
|
newQInternal[i] = _uintToInternalFloor(bal, state.bases[i]);
|
||||||
|
unchecked { i++; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply proportional update or deinitialize if drained
|
||||||
|
bool allZero = true;
|
||||||
|
for (uint i = 0; i < n; ) {
|
||||||
|
if (newQInternal[i] != int128(0)) {
|
||||||
|
allZero = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
unchecked { i++; }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allZero) {
|
||||||
|
state.lmsr.deinit();
|
||||||
|
} else {
|
||||||
|
state.lmsr.updateForProportionalChange(newQInternal);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit Burn(payer, receiver, withdrawAmounts, lpAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Get swap amounts for exact input swap
|
||||||
|
function swapAmounts(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
uint256 inputTokenIndex,
|
||||||
|
uint256 outputTokenIndex,
|
||||||
|
uint256 maxAmountIn,
|
||||||
|
int128 limitPrice,
|
||||||
|
uint256 swapFeePpm
|
||||||
|
) internal view returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
|
||||||
|
(uint256 grossIn, uint256 outUint,,,, uint256 feeUint) = _quoteSwapExactIn(
|
||||||
|
state, inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice, swapFeePpm
|
||||||
|
);
|
||||||
|
return (grossIn, outUint, feeUint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Get swap amounts for swap to price limit
|
||||||
|
function swapToLimitAmounts(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
uint256 inputTokenIndex,
|
||||||
|
uint256 outputTokenIndex,
|
||||||
|
int128 limitPrice,
|
||||||
|
uint256 swapFeePpm
|
||||||
|
) internal view returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
|
||||||
|
(uint256 grossIn, uint256 outUint,,,, uint256 feeUint) = _quoteSwapToLimit(
|
||||||
|
state, inputTokenIndex, outputTokenIndex, limitPrice, swapFeePpm
|
||||||
|
);
|
||||||
|
return (grossIn, outUint, feeUint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Get amounts for swapMint operation
|
||||||
|
function swapMintAmounts(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
uint256 inputTokenIndex,
|
||||||
|
uint256 maxAmountIn,
|
||||||
|
uint256 swapFeePpm,
|
||||||
|
uint256 totalSupply
|
||||||
|
) internal view returns (uint256 totalTransfer, uint256 amountIn, uint256 fee, uint256 lpMinted) {
|
||||||
|
uint256 n = state.tokens.length;
|
||||||
|
require(inputTokenIndex < n, "swapMintAmounts: idx");
|
||||||
|
require(maxAmountIn > 0, "swapMintAmounts: input zero");
|
||||||
|
require(state.lmsr.nAssets > 0, "swapMintAmounts: uninit pool");
|
||||||
|
|
||||||
|
// Compute fee on gross maxAmountIn to get initial net estimate
|
||||||
|
(, uint256 netUintGuess) = _computeFee(maxAmountIn, swapFeePpm);
|
||||||
|
|
||||||
|
// Convert the net guess to internal (floor)
|
||||||
|
int128 netInternalGuess = _uintToInternalFloor(netUintGuess, state.bases[inputTokenIndex]);
|
||||||
|
require(netInternalGuess > int128(0), "swapMintAmounts: input too small after fee");
|
||||||
|
|
||||||
|
// Use LMSR view to determine actual internal consumed and size-increase
|
||||||
|
(int128 amountInInternalUsed, int128 sizeIncreaseInternal) = state.lmsr.swapAmountsForMint(inputTokenIndex, netInternalGuess);
|
||||||
|
|
||||||
|
// Convert to uint (ceil) to determine actual transfer
|
||||||
|
uint256 amountInUint = _internalToUintCeil(amountInInternalUsed, state.bases[inputTokenIndex]);
|
||||||
|
require(amountInUint > 0, "swapMintAmounts: input zero after internal conversion");
|
||||||
|
|
||||||
|
// Compute fee on actual used input and total transfer amount
|
||||||
|
uint256 feeUintActual = _ceilFee(amountInUint, swapFeePpm);
|
||||||
|
uint256 totalTransferAmount = amountInUint + feeUintActual;
|
||||||
|
require(totalTransferAmount > 0 && totalTransferAmount <= maxAmountIn, "swapMintAmounts: transfer exceeds max");
|
||||||
|
|
||||||
|
// Compute old and new scaled size metrics
|
||||||
|
int128 oldTotal = _computeSizeMetric(state.lmsr.qInternal);
|
||||||
|
require(oldTotal > int128(0), "swapMintAmounts: zero total");
|
||||||
|
uint256 oldScaled = ABDKMath64x64.mulu(oldTotal, LP_SCALE);
|
||||||
|
|
||||||
|
int128 newTotal = oldTotal.add(sizeIncreaseInternal);
|
||||||
|
uint256 newScaled = ABDKMath64x64.mulu(newTotal, LP_SCALE);
|
||||||
|
|
||||||
|
uint256 actualLpToMint;
|
||||||
|
if (totalSupply == 0) {
|
||||||
|
actualLpToMint = newScaled;
|
||||||
|
} else {
|
||||||
|
require(oldScaled > 0, "swapMintAmounts: oldScaled zero");
|
||||||
|
uint256 delta = (newScaled > oldScaled) ? (newScaled - oldScaled) : 0;
|
||||||
|
if (delta > 0) {
|
||||||
|
actualLpToMint = (totalSupply * delta) / oldScaled;
|
||||||
|
} else {
|
||||||
|
actualLpToMint = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
require(actualLpToMint > 0, "swapMintAmounts: zero LP minted");
|
||||||
|
|
||||||
|
return (totalTransferAmount, amountInUint, feeUintActual, actualLpToMint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Execute exact input swap
|
||||||
|
function swap(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
address payer,
|
||||||
|
address receiver,
|
||||||
|
uint256 inputTokenIndex,
|
||||||
|
uint256 outputTokenIndex,
|
||||||
|
uint256 maxAmountIn,
|
||||||
|
int128 limitPrice,
|
||||||
|
uint256 deadline,
|
||||||
|
uint256 swapFeePpm
|
||||||
|
) internal returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
|
||||||
|
uint256 n = state.tokens.length;
|
||||||
|
require(inputTokenIndex < n && outputTokenIndex < n, "swap: idx");
|
||||||
|
require(maxAmountIn > 0, "swap: input zero");
|
||||||
|
require(deadline == 0 || block.timestamp <= deadline, "swap: deadline exceeded");
|
||||||
|
|
||||||
|
// Read previous balances
|
||||||
|
uint256 prevBalI = IERC20(state.tokens[inputTokenIndex]).balanceOf(address(this));
|
||||||
|
uint256 prevBalJ = IERC20(state.tokens[outputTokenIndex]).balanceOf(address(this));
|
||||||
|
|
||||||
|
// Compute amounts
|
||||||
|
(uint256 totalTransferAmount, uint256 amountOutUint, int128 amountInInternalUsed, int128 amountOutInternal, , uint256 feeUint) =
|
||||||
|
_quoteSwapExactIn(state, inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice, swapFeePpm);
|
||||||
|
|
||||||
|
// Transfer exact amount from payer
|
||||||
|
state.tokens[inputTokenIndex].safeTransferFrom(payer, address(this), totalTransferAmount);
|
||||||
|
uint256 balIAfter = IERC20(state.tokens[inputTokenIndex]).balanceOf(address(this));
|
||||||
|
require(balIAfter == prevBalI + totalTransferAmount, "swap: non-standard tokenIn");
|
||||||
|
|
||||||
|
// Transfer output to receiver
|
||||||
|
state.tokens[outputTokenIndex].safeTransfer(receiver, amountOutUint);
|
||||||
|
uint256 balJAfter = IERC20(state.tokens[outputTokenIndex]).balanceOf(address(this));
|
||||||
|
require(balJAfter == prevBalJ - amountOutUint, "swap: non-standard tokenOut");
|
||||||
|
|
||||||
|
// Update cached balances
|
||||||
|
state.cachedUintBalances[inputTokenIndex] = balIAfter;
|
||||||
|
state.cachedUintBalances[outputTokenIndex] = balJAfter;
|
||||||
|
|
||||||
|
// Apply swap to LMSR state
|
||||||
|
state.lmsr.applySwap(inputTokenIndex, outputTokenIndex, amountInInternalUsed, amountOutInternal);
|
||||||
|
|
||||||
|
emit Swap(payer, receiver, state.tokens[inputTokenIndex], state.tokens[outputTokenIndex], totalTransferAmount, amountOutUint);
|
||||||
|
|
||||||
|
return (totalTransferAmount, amountOutUint, feeUint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Execute swap to price limit
|
||||||
|
function swapToLimit(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
address payer,
|
||||||
|
address receiver,
|
||||||
|
uint256 inputTokenIndex,
|
||||||
|
uint256 outputTokenIndex,
|
||||||
|
int128 limitPrice,
|
||||||
|
uint256 deadline,
|
||||||
|
uint256 swapFeePpm
|
||||||
|
) internal returns (uint256 amountInUsed, uint256 amountOut, uint256 fee) {
|
||||||
|
uint256 n = state.tokens.length;
|
||||||
|
require(inputTokenIndex < n && outputTokenIndex < n, "swapToLimit: idx");
|
||||||
|
require(limitPrice > int128(0), "swapToLimit: limit <= 0");
|
||||||
|
require(deadline == 0 || block.timestamp <= deadline, "swapToLimit: deadline exceeded");
|
||||||
|
|
||||||
|
// Read previous balances
|
||||||
|
uint256 prevBalI = IERC20(state.tokens[inputTokenIndex]).balanceOf(address(this));
|
||||||
|
uint256 prevBalJ = IERC20(state.tokens[outputTokenIndex]).balanceOf(address(this));
|
||||||
|
|
||||||
|
// Compute amounts
|
||||||
|
(uint256 totalTransferAmount, uint256 amountOutUint, int128 amountInInternalMax, int128 amountOutInternal, uint256 amountInUsedUint, uint256 feeUint) =
|
||||||
|
_quoteSwapToLimit(state, inputTokenIndex, outputTokenIndex, limitPrice, swapFeePpm);
|
||||||
|
|
||||||
|
// Transfer exact amount from payer
|
||||||
|
state.tokens[inputTokenIndex].safeTransferFrom(payer, address(this), totalTransferAmount);
|
||||||
|
uint256 balIAfter = IERC20(state.tokens[inputTokenIndex]).balanceOf(address(this));
|
||||||
|
require(balIAfter == prevBalI + totalTransferAmount, "swapToLimit: non-standard tokenIn");
|
||||||
|
|
||||||
|
// Transfer output to receiver
|
||||||
|
state.tokens[outputTokenIndex].safeTransfer(receiver, amountOutUint);
|
||||||
|
uint256 balJAfter = IERC20(state.tokens[outputTokenIndex]).balanceOf(address(this));
|
||||||
|
require(balJAfter == prevBalJ - amountOutUint, "swapToLimit: non-standard tokenOut");
|
||||||
|
|
||||||
|
// Update caches
|
||||||
|
state.cachedUintBalances[inputTokenIndex] = balIAfter;
|
||||||
|
state.cachedUintBalances[outputTokenIndex] = balJAfter;
|
||||||
|
|
||||||
|
// Apply swap to LMSR state
|
||||||
|
state.lmsr.applySwap(inputTokenIndex, outputTokenIndex, amountInInternalMax, amountOutInternal);
|
||||||
|
|
||||||
|
emit Swap(payer, receiver, state.tokens[inputTokenIndex], state.tokens[outputTokenIndex], amountInUsedUint, amountOutUint);
|
||||||
|
|
||||||
|
return (amountInUsedUint, amountOutUint, feeUint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Get amounts for burnSwap operation
|
||||||
|
function burnSwapAmounts(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
uint256 lpAmount,
|
||||||
|
uint256 inputTokenIndex,
|
||||||
|
uint256 swapFeePpm,
|
||||||
|
uint256 totalSupply
|
||||||
|
) internal view returns (uint256 amountOut) {
|
||||||
|
uint256 n = state.tokens.length;
|
||||||
|
require(inputTokenIndex < n, "burnSwapAmounts: idx");
|
||||||
|
require(lpAmount > 0, "burnSwapAmounts: zero lp");
|
||||||
|
require(totalSupply > 0, "burnSwapAmounts: empty supply");
|
||||||
|
|
||||||
|
// alpha = lpAmount / supply as Q64.64
|
||||||
|
int128 alpha = ABDKMath64x64.divu(lpAmount, totalSupply);
|
||||||
|
|
||||||
|
// Use LMSR view to compute single-asset payout
|
||||||
|
(int128 payoutInternal, ) = state.lmsr.swapAmountsForBurn(inputTokenIndex, alpha);
|
||||||
|
|
||||||
|
// Convert payoutInternal -> uint (floor) to favor pool
|
||||||
|
uint256 amountOutUint = _internalToUintFloor(payoutInternal, state.bases[inputTokenIndex]);
|
||||||
|
require(amountOutUint > 0, "burnSwapAmounts: output zero");
|
||||||
|
|
||||||
|
// Apply swap fee to the output
|
||||||
|
if (swapFeePpm > 0) {
|
||||||
|
uint256 feeUint = _ceilFee(amountOutUint, swapFeePpm);
|
||||||
|
require(amountOutUint > feeUint, "burnSwapAmounts: fee exceeds output");
|
||||||
|
amountOutUint -= feeUint;
|
||||||
|
}
|
||||||
|
|
||||||
|
return amountOutUint;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Single-token mint (swapMint)
|
||||||
|
function swapMint(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
address payer,
|
||||||
|
address receiver,
|
||||||
|
uint256 inputTokenIndex,
|
||||||
|
uint256 maxAmountIn,
|
||||||
|
uint256 deadline,
|
||||||
|
uint256 swapFeePpm,
|
||||||
|
uint256 totalSupply
|
||||||
|
) internal returns (uint256 lpMinted) {
|
||||||
|
uint256 n = state.tokens.length;
|
||||||
|
require(inputTokenIndex < n, "swapMint: idx");
|
||||||
|
require(maxAmountIn > 0, "swapMint: input zero");
|
||||||
|
require(deadline == 0 || block.timestamp <= deadline, "swapMint: deadline");
|
||||||
|
require(state.lmsr.nAssets > 0, "swapMint: uninit pool");
|
||||||
|
|
||||||
|
// Calculate amounts using view function
|
||||||
|
(uint256 totalTransfer, uint256 amountInUint, uint256 feeUintActual, uint256 actualLpToMint) = swapMintAmounts(
|
||||||
|
state, inputTokenIndex, maxAmountIn, swapFeePpm, totalSupply
|
||||||
|
);
|
||||||
|
|
||||||
|
// Record pre-balance and transfer tokens
|
||||||
|
uint256 prevBalI = IERC20(state.tokens[inputTokenIndex]).balanceOf(address(this));
|
||||||
|
state.tokens[inputTokenIndex].safeTransferFrom(payer, address(this), totalTransfer);
|
||||||
|
uint256 balIAfter = IERC20(state.tokens[inputTokenIndex]).balanceOf(address(this));
|
||||||
|
require(balIAfter == prevBalI + totalTransfer, "swapMint: non-standard tokenIn");
|
||||||
|
|
||||||
|
// Update cached uint balances
|
||||||
|
state.cachedUintBalances[inputTokenIndex] = balIAfter;
|
||||||
|
|
||||||
|
// Update LMSR internal state
|
||||||
|
int128 oldTotal = _computeSizeMetric(state.lmsr.qInternal);
|
||||||
|
(, int128 sizeIncreaseInternal) = state.lmsr.swapAmountsForMint(inputTokenIndex, _uintToInternalFloor(amountInUint, state.bases[inputTokenIndex]));
|
||||||
|
int128 newTotal = oldTotal.add(sizeIncreaseInternal);
|
||||||
|
|
||||||
|
int128[] memory newQInternal = new int128[](n);
|
||||||
|
for (uint256 idx = 0; idx < n; idx++) {
|
||||||
|
newQInternal[idx] = state.lmsr.qInternal[idx].mul(newTotal).div(oldTotal);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.lmsr.updateForProportionalChange(newQInternal);
|
||||||
|
|
||||||
|
emit SwapMint(payer, receiver, inputTokenIndex, totalTransfer, amountInUint, feeUintActual);
|
||||||
|
emit Mint(payer, receiver, new uint256[](n), actualLpToMint);
|
||||||
|
|
||||||
|
return actualLpToMint;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Burn LP tokens and swap to single asset (burnSwap)
|
||||||
|
function burnSwap(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
address payer,
|
||||||
|
address receiver,
|
||||||
|
uint256 lpAmount,
|
||||||
|
uint256 inputTokenIndex,
|
||||||
|
uint256 deadline,
|
||||||
|
uint256 swapFeePpm,
|
||||||
|
uint256 totalSupply,
|
||||||
|
uint256 payerBalance
|
||||||
|
) internal returns (uint256 amountOutUint) {
|
||||||
|
uint256 n = state.tokens.length;
|
||||||
|
require(inputTokenIndex < n, "burnSwap: idx");
|
||||||
|
require(lpAmount > 0, "burnSwap: zero lp");
|
||||||
|
require(deadline == 0 || block.timestamp <= deadline, "burnSwap: deadline");
|
||||||
|
require(totalSupply > 0, "burnSwap: empty supply");
|
||||||
|
require(payerBalance >= lpAmount, "burnSwap: insufficient LP");
|
||||||
|
|
||||||
|
// Calculate amounts using view function
|
||||||
|
amountOutUint = burnSwapAmounts(state, lpAmount, inputTokenIndex, swapFeePpm, totalSupply);
|
||||||
|
|
||||||
|
// Transfer the payout to receiver
|
||||||
|
state.tokens[inputTokenIndex].safeTransfer(receiver, amountOutUint);
|
||||||
|
|
||||||
|
// Update cached balances by reading on-chain balances for all tokens
|
||||||
|
int128[] memory newQInternal = new int128[](n);
|
||||||
|
for (uint256 idx = 0; idx < n; idx++) {
|
||||||
|
uint256 bal = IERC20(state.tokens[idx]).balanceOf(address(this));
|
||||||
|
state.cachedUintBalances[idx] = bal;
|
||||||
|
newQInternal[idx] = _uintToInternalFloor(bal, state.bases[idx]);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit BurnSwap(payer, receiver, inputTokenIndex, amountOutUint);
|
||||||
|
|
||||||
|
// If entire pool drained, deinit; else update proportionally
|
||||||
|
bool allZero = true;
|
||||||
|
for (uint256 idx = 0; idx < n; idx++) {
|
||||||
|
if (newQInternal[idx] != int128(0)) { allZero = false; break; }
|
||||||
|
}
|
||||||
|
if (allZero) {
|
||||||
|
state.lmsr.deinit();
|
||||||
|
} else {
|
||||||
|
state.lmsr.updateForProportionalChange(newQInternal);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit Burn(payer, receiver, new uint256[](n), lpAmount);
|
||||||
|
return amountOutUint;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Calculate flash loan repayment amounts
|
||||||
|
function flashRepaymentAmounts(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
uint256[] memory loanAmounts,
|
||||||
|
uint256 flashFeePpm
|
||||||
|
) internal view returns (uint256[] memory repaymentAmounts) {
|
||||||
|
repaymentAmounts = new uint256[](state.tokens.length);
|
||||||
|
for (uint256 i = 0; i < state.tokens.length; i++) {
|
||||||
|
uint256 amount = loanAmounts[i];
|
||||||
|
if (amount > 0) {
|
||||||
|
repaymentAmounts[i] = amount + _ceilFee(amount, flashFeePpm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Execute flash loan
|
||||||
|
function flash(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
address recipient,
|
||||||
|
uint256[] memory amounts,
|
||||||
|
bytes calldata data,
|
||||||
|
uint256 flashFeePpm
|
||||||
|
) internal {
|
||||||
|
require(recipient != address(0), "flash: zero recipient");
|
||||||
|
require(amounts.length == state.tokens.length, "flash: amounts length mismatch");
|
||||||
|
|
||||||
|
// Calculate repayment amounts for each token including fee
|
||||||
|
uint256[] memory repaymentAmounts = new uint256[](state.tokens.length);
|
||||||
|
|
||||||
|
// Store initial balances to verify repayment later
|
||||||
|
uint256[] memory initialBalances = new uint256[](state.tokens.length);
|
||||||
|
|
||||||
|
// Track if any token amount is non-zero
|
||||||
|
bool hasNonZeroAmount = false;
|
||||||
|
|
||||||
|
// Process each token, skipping those with zero amounts
|
||||||
|
for (uint256 i = 0; i < state.tokens.length; i++) {
|
||||||
|
uint256 amount = amounts[i];
|
||||||
|
|
||||||
|
if (amount > 0) {
|
||||||
|
hasNonZeroAmount = true;
|
||||||
|
|
||||||
|
// Calculate repayment amount with fee (ceiling)
|
||||||
|
repaymentAmounts[i] = amount + _ceilFee(amount, flashFeePpm);
|
||||||
|
|
||||||
|
// Record initial balance
|
||||||
|
initialBalances[i] = IERC20(state.tokens[i]).balanceOf(address(this));
|
||||||
|
|
||||||
|
// Transfer token to recipient
|
||||||
|
state.tokens[i].safeTransfer(recipient, amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure at least one token is being borrowed
|
||||||
|
require(hasNonZeroAmount, "flash: no tokens requested");
|
||||||
|
|
||||||
|
// Call flash callback with expected repayment amounts
|
||||||
|
IPartyFlashCallback(msg.sender).partyFlashCallback(amounts, repaymentAmounts, data);
|
||||||
|
|
||||||
|
// Verify repayment amounts for tokens that were borrowed
|
||||||
|
for (uint256 i = 0; i < state.tokens.length; i++) {
|
||||||
|
if (amounts[i] > 0) {
|
||||||
|
uint256 currentBalance = IERC20(state.tokens[i]).balanceOf(address(this));
|
||||||
|
|
||||||
|
// Verify repayment: current balance must be at least (initial balance + fee)
|
||||||
|
require(
|
||||||
|
currentBalance >= initialBalances[i] + _ceilFee(amounts[i], flashFeePpm),
|
||||||
|
"flash: repayment failed"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update cached balance
|
||||||
|
state.cachedUintBalances[i] = currentBalance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Get marginal price between two tokens
|
||||||
|
function price(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
uint256 baseTokenIndex,
|
||||||
|
uint256 quoteTokenIndex
|
||||||
|
) internal view returns (int128) {
|
||||||
|
uint256 n = state.tokens.length;
|
||||||
|
require(baseTokenIndex < n && quoteTokenIndex < n, "price: idx");
|
||||||
|
require(state.lmsr.nAssets > 0, "price: uninit");
|
||||||
|
return state.lmsr.price(baseTokenIndex, quoteTokenIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Get price of one LP token in quote asset
|
||||||
|
function poolPrice(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
uint256 quoteTokenIndex,
|
||||||
|
uint256 totalSupply
|
||||||
|
) internal view returns (int128) {
|
||||||
|
uint256 n = state.tokens.length;
|
||||||
|
require(quoteTokenIndex < n, "poolPrice: idx");
|
||||||
|
require(state.lmsr.nAssets > 0, "poolPrice: uninit");
|
||||||
|
|
||||||
|
// price per unit of qTotal (Q64.64) from LMSR
|
||||||
|
int128 pricePerQ = state.lmsr.poolPrice(quoteTokenIndex);
|
||||||
|
|
||||||
|
// total internal q (qTotal) as Q64.64
|
||||||
|
int128 qTotal = _computeSizeMetric(state.lmsr.qInternal);
|
||||||
|
require(qTotal > int128(0), "poolPrice: qTotal zero");
|
||||||
|
|
||||||
|
// totalSupply as Q64.64
|
||||||
|
require(totalSupply > 0, "poolPrice: zero supply");
|
||||||
|
int128 supplyQ64 = ABDKMath64x64.fromUInt(totalSupply);
|
||||||
|
|
||||||
|
// factor = totalSupply / qTotal (Q64.64)
|
||||||
|
int128 factor = supplyQ64.div(qTotal);
|
||||||
|
|
||||||
|
// price per LP token = pricePerQ * factor (Q64.64)
|
||||||
|
return pricePerQ.mul(factor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal helper functions
|
||||||
|
|
||||||
|
/// @notice Ceiling fee helper
|
||||||
|
function _ceilFee(uint256 x, uint256 feePpm) internal pure returns (uint256) {
|
||||||
|
if (feePpm == 0) return 0;
|
||||||
|
return (x * feePpm + 1_000_000 - 1) / 1_000_000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Compute fee and net amounts for a gross input
|
||||||
|
function _computeFee(uint256 gross, uint256 swapFeePpm) internal pure returns (uint256 feeUint, uint256 netUint) {
|
||||||
|
if (swapFeePpm == 0) {
|
||||||
|
return (0, gross);
|
||||||
|
}
|
||||||
|
feeUint = _ceilFee(gross, swapFeePpm);
|
||||||
|
netUint = gross - feeUint;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Add fee to net amount
|
||||||
|
function _addFee(uint256 netUint, uint256 swapFeePpm) internal pure returns (uint256 gross) {
|
||||||
|
if (swapFeePpm == 0) return netUint;
|
||||||
|
uint256 fee = _ceilFee(netUint, swapFeePpm);
|
||||||
|
return netUint + fee;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Internal quote for exact-input swap
|
||||||
|
function _quoteSwapExactIn(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
uint256 inputTokenIndex,
|
||||||
|
uint256 outputTokenIndex,
|
||||||
|
uint256 maxAmountIn,
|
||||||
|
int128 limitPrice,
|
||||||
|
uint256 swapFeePpm
|
||||||
|
)
|
||||||
|
internal
|
||||||
|
view
|
||||||
|
returns (
|
||||||
|
uint256 grossIn,
|
||||||
|
uint256 amountOutUint,
|
||||||
|
int128 amountInInternalUsed,
|
||||||
|
int128 amountOutInternal,
|
||||||
|
uint256 amountInUintNoFee,
|
||||||
|
uint256 feeUint
|
||||||
|
)
|
||||||
|
{
|
||||||
|
uint256 n = state.tokens.length;
|
||||||
|
require(inputTokenIndex < n && outputTokenIndex < n, "swap: idx");
|
||||||
|
require(maxAmountIn > 0, "swap: input zero");
|
||||||
|
require(state.lmsr.nAssets > 0, "swap: empty pool");
|
||||||
|
|
||||||
|
// Estimate max net input
|
||||||
|
(, uint256 netUintForSwap) = _computeFee(maxAmountIn, swapFeePpm);
|
||||||
|
|
||||||
|
// Convert to internal (floor)
|
||||||
|
int128 deltaInternalI = _uintToInternalFloor(netUintForSwap, state.bases[inputTokenIndex]);
|
||||||
|
require(deltaInternalI > int128(0), "swap: input too small after fee");
|
||||||
|
|
||||||
|
// Compute internal amounts using LMSR
|
||||||
|
(amountInInternalUsed, amountOutInternal) = state.lmsr.swapAmountsForExactInput(
|
||||||
|
inputTokenIndex, outputTokenIndex, deltaInternalI, limitPrice);
|
||||||
|
|
||||||
|
// Convert actual used input internal -> uint (ceil)
|
||||||
|
amountInUintNoFee = _internalToUintCeil(amountInInternalUsed, state.bases[inputTokenIndex]);
|
||||||
|
require(amountInUintNoFee > 0, "swap: input zero");
|
||||||
|
|
||||||
|
// Compute gross transfer including fee
|
||||||
|
feeUint = 0;
|
||||||
|
grossIn = amountInUintNoFee;
|
||||||
|
if (swapFeePpm > 0) {
|
||||||
|
feeUint = _ceilFee(amountInUintNoFee, swapFeePpm);
|
||||||
|
grossIn += feeUint;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure within user max
|
||||||
|
require(grossIn <= maxAmountIn, "swap: transfer exceeds max");
|
||||||
|
|
||||||
|
// Compute output (floor)
|
||||||
|
amountOutUint = _internalToUintFloor(amountOutInternal, state.bases[outputTokenIndex]);
|
||||||
|
require(amountOutUint > 0, "swap: output zero");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Internal quote for exact-input swap
|
||||||
|
function _quoteSwapExactIn_BalancedPair(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
uint256 inputTokenIndex,
|
||||||
|
uint256 outputTokenIndex,
|
||||||
|
uint256 maxAmountIn,
|
||||||
|
int128 limitPrice,
|
||||||
|
uint256 swapFeePpm
|
||||||
|
)
|
||||||
|
internal
|
||||||
|
view
|
||||||
|
returns (
|
||||||
|
uint256 grossIn,
|
||||||
|
uint256 amountOutUint,
|
||||||
|
int128 amountInInternalUsed,
|
||||||
|
int128 amountOutInternal,
|
||||||
|
uint256 amountInUintNoFee,
|
||||||
|
uint256 feeUint
|
||||||
|
)
|
||||||
|
{
|
||||||
|
uint256 n = state.tokens.length;
|
||||||
|
require(inputTokenIndex < n && outputTokenIndex < n, "swap: idx");
|
||||||
|
require(maxAmountIn > 0, "swap: input zero");
|
||||||
|
require(state.lmsr.nAssets > 0, "swap: empty pool");
|
||||||
|
|
||||||
|
// Estimate max net input
|
||||||
|
(, uint256 netUintForSwap) = _computeFee(maxAmountIn, swapFeePpm);
|
||||||
|
|
||||||
|
// Convert to internal (floor)
|
||||||
|
int128 deltaInternalI = _uintToInternalFloor(netUintForSwap, state.bases[inputTokenIndex]);
|
||||||
|
require(deltaInternalI > int128(0), "swap: input too small after fee");
|
||||||
|
|
||||||
|
// Compute internal amounts using LMSR
|
||||||
|
(amountInInternalUsed, amountOutInternal) = LMSRStabilizedBalancedPair.swapAmountsForExactInput(
|
||||||
|
state.lmsr, inputTokenIndex, outputTokenIndex, deltaInternalI, limitPrice);
|
||||||
|
|
||||||
|
// Convert actual used input internal -> uint (ceil)
|
||||||
|
amountInUintNoFee = _internalToUintCeil(amountInInternalUsed, state.bases[inputTokenIndex]);
|
||||||
|
require(amountInUintNoFee > 0, "swap: input zero");
|
||||||
|
|
||||||
|
// Compute gross transfer including fee
|
||||||
|
feeUint = 0;
|
||||||
|
grossIn = amountInUintNoFee;
|
||||||
|
if (swapFeePpm > 0) {
|
||||||
|
feeUint = _ceilFee(amountInUintNoFee, swapFeePpm);
|
||||||
|
grossIn += feeUint;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure within user max
|
||||||
|
require(grossIn <= maxAmountIn, "swap: transfer exceeds max");
|
||||||
|
|
||||||
|
// Compute output (floor)
|
||||||
|
amountOutUint = _internalToUintFloor(amountOutInternal, state.bases[outputTokenIndex]);
|
||||||
|
require(amountOutUint > 0, "swap: output zero");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Internal quote for swap-to-limit
|
||||||
|
function _quoteSwapToLimit(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
uint256 inputTokenIndex,
|
||||||
|
uint256 outputTokenIndex,
|
||||||
|
int128 limitPrice,
|
||||||
|
uint256 swapFeePpm
|
||||||
|
)
|
||||||
|
internal
|
||||||
|
view
|
||||||
|
returns (
|
||||||
|
uint256 grossIn,
|
||||||
|
uint256 amountOutUint,
|
||||||
|
int128 amountInInternal,
|
||||||
|
int128 amountOutInternal,
|
||||||
|
uint256 amountInUintNoFee,
|
||||||
|
uint256 feeUint
|
||||||
|
)
|
||||||
|
{
|
||||||
|
uint256 n = state.tokens.length;
|
||||||
|
require(inputTokenIndex < n && outputTokenIndex < n, "swapToLimit: idx");
|
||||||
|
require(limitPrice > int128(0), "swapToLimit: limit <= 0");
|
||||||
|
require(state.lmsr.nAssets > 0, "swapToLimit: pool uninitialized");
|
||||||
|
|
||||||
|
// Compute internal maxima at the price limit
|
||||||
|
(amountInInternal, amountOutInternal) = state.lmsr.swapAmountsForPriceLimit(inputTokenIndex, outputTokenIndex, limitPrice);
|
||||||
|
|
||||||
|
// Convert input to uint (ceil) and output to uint (floor)
|
||||||
|
amountInUintNoFee = _internalToUintCeil(amountInInternal, state.bases[inputTokenIndex]);
|
||||||
|
require(amountInUintNoFee > 0, "swapToLimit: input zero");
|
||||||
|
|
||||||
|
feeUint = 0;
|
||||||
|
grossIn = amountInUintNoFee;
|
||||||
|
if (swapFeePpm > 0) {
|
||||||
|
feeUint = _ceilFee(amountInUintNoFee, swapFeePpm);
|
||||||
|
grossIn += feeUint;
|
||||||
|
}
|
||||||
|
|
||||||
|
amountOutUint = _internalToUintFloor(amountOutInternal, state.bases[outputTokenIndex]);
|
||||||
|
require(amountOutUint > 0, "swapToLimit: output zero");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert uint token amount -> internal 64.64 (floor)
|
||||||
|
function _uintToInternalFloor(uint256 amount, uint256 base) internal pure returns (int128) {
|
||||||
|
return ABDKMath64x64.divu(amount, base);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert internal 64.64 -> uint token amount (floor)
|
||||||
|
function _internalToUintFloor(int128 internalAmount, uint256 base) internal pure returns (uint256) {
|
||||||
|
return ABDKMath64x64.mulu(internalAmount, base);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert internal 64.64 -> uint token amount (ceiling)
|
||||||
|
function _internalToUintCeil(int128 internalAmount, uint256 base) internal pure returns (uint256) {
|
||||||
|
// Get the floor value first
|
||||||
|
uint256 floorValue = ABDKMath64x64.mulu(internalAmount, base);
|
||||||
|
|
||||||
|
// Check if there was any fractional part by comparing to a reconstruction
|
||||||
|
int128 reconstructed = ABDKMath64x64.divu(floorValue, base);
|
||||||
|
|
||||||
|
// If reconstructed is less than original, there was a fractional part that was truncated
|
||||||
|
if (reconstructed < internalAmount) {
|
||||||
|
return floorValue + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return floorValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Helper to compute size metric (sum of all asset quantities) from internal balances
|
||||||
|
function _computeSizeMetric(int128[] memory qInternal_) internal pure returns (int128) {
|
||||||
|
int128 total = int128(0);
|
||||||
|
for (uint i = 0; i < qInternal_.length; ) {
|
||||||
|
total = total.add(qInternal_[i]);
|
||||||
|
unchecked { i++; }
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
}
|
||||||
139
src/PoolLibBalancedPair.sol
Normal file
139
src/PoolLibBalancedPair.sol
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
|
pragma solidity ^0.8.30;
|
||||||
|
|
||||||
|
import "@abdk/ABDKMath64x64.sol";
|
||||||
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||||
|
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||||
|
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||||
|
import "./LMSRStabilized.sol";
|
||||||
|
import "./LMSRStabilizedBalancedPair.sol";
|
||||||
|
import "./IPartyFlashCallback.sol";
|
||||||
|
import {PoolLib} from "./PoolLib.sol";
|
||||||
|
|
||||||
|
/// @title PoolLibBalancedPair - Library with optimized functions for stablecoin pair pools
|
||||||
|
/// @dev All functions are internal and accept State as the first parameter
|
||||||
|
library PoolLibBalancedPair {
|
||||||
|
using ABDKMath64x64 for int128;
|
||||||
|
using LMSRStabilized for LMSRStabilized.State;
|
||||||
|
using PoolLib for PoolLib.State;
|
||||||
|
using SafeERC20 for IERC20;
|
||||||
|
|
||||||
|
/// @notice Get swap amounts for exact input swap
|
||||||
|
function swapAmounts(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
uint256 inputTokenIndex,
|
||||||
|
uint256 outputTokenIndex,
|
||||||
|
uint256 maxAmountIn,
|
||||||
|
int128 limitPrice,
|
||||||
|
uint256 swapFeePpm
|
||||||
|
) internal view returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
|
||||||
|
(uint256 grossIn, uint256 outUint,,,, uint256 feeUint) = _quoteSwapExactIn(
|
||||||
|
state, inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice, swapFeePpm
|
||||||
|
);
|
||||||
|
return (grossIn, outUint, feeUint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Internal quote for exact-input swap
|
||||||
|
function _quoteSwapExactIn(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
uint256 inputTokenIndex,
|
||||||
|
uint256 outputTokenIndex,
|
||||||
|
uint256 maxAmountIn,
|
||||||
|
int128 limitPrice,
|
||||||
|
uint256 swapFeePpm
|
||||||
|
)
|
||||||
|
internal
|
||||||
|
view
|
||||||
|
returns (
|
||||||
|
uint256 grossIn,
|
||||||
|
uint256 amountOutUint,
|
||||||
|
int128 amountInInternalUsed,
|
||||||
|
int128 amountOutInternal,
|
||||||
|
uint256 amountInUintNoFee,
|
||||||
|
uint256 feeUint
|
||||||
|
)
|
||||||
|
{
|
||||||
|
uint256 n = state.tokens.length;
|
||||||
|
require(inputTokenIndex < n && outputTokenIndex < n, "swap: idx");
|
||||||
|
require(maxAmountIn > 0, "swap: input zero");
|
||||||
|
require(state.lmsr.nAssets > 0, "swap: empty pool");
|
||||||
|
|
||||||
|
// Estimate max net input
|
||||||
|
(, uint256 netUintForSwap) = PoolLib._computeFee(maxAmountIn, swapFeePpm);
|
||||||
|
|
||||||
|
// Convert to internal (floor)
|
||||||
|
int128 deltaInternalI = PoolLib._uintToInternalFloor(netUintForSwap, state.bases[inputTokenIndex]);
|
||||||
|
require(deltaInternalI > int128(0), "swap: input too small after fee");
|
||||||
|
|
||||||
|
// Compute internal amounts using LMSR
|
||||||
|
(amountInInternalUsed, amountOutInternal) = LMSRStabilizedBalancedPair.swapAmountsForExactInput(state.lmsr,
|
||||||
|
inputTokenIndex, outputTokenIndex, deltaInternalI, limitPrice);
|
||||||
|
|
||||||
|
// Convert actual used input internal -> uint (ceil)
|
||||||
|
amountInUintNoFee = PoolLib._internalToUintCeil(amountInInternalUsed, state.bases[inputTokenIndex]);
|
||||||
|
require(amountInUintNoFee > 0, "swap: input zero");
|
||||||
|
|
||||||
|
// Compute gross transfer including fee
|
||||||
|
feeUint = 0;
|
||||||
|
grossIn = amountInUintNoFee;
|
||||||
|
if (swapFeePpm > 0) {
|
||||||
|
feeUint = PoolLib._ceilFee(amountInUintNoFee, swapFeePpm);
|
||||||
|
grossIn += feeUint;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure within user max
|
||||||
|
require(grossIn <= maxAmountIn, "swap: transfer exceeds max");
|
||||||
|
|
||||||
|
// Compute output (floor)
|
||||||
|
amountOutUint = PoolLib._internalToUintFloor(amountOutInternal, state.bases[outputTokenIndex]);
|
||||||
|
require(amountOutUint > 0, "swap: output zero");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Execute exact input swap
|
||||||
|
function swap(
|
||||||
|
PoolLib.State storage state,
|
||||||
|
address payer,
|
||||||
|
address receiver,
|
||||||
|
uint256 inputTokenIndex,
|
||||||
|
uint256 outputTokenIndex,
|
||||||
|
uint256 maxAmountIn,
|
||||||
|
int128 limitPrice,
|
||||||
|
uint256 deadline,
|
||||||
|
uint256 swapFeePpm
|
||||||
|
) internal returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
|
||||||
|
uint256 n = state.tokens.length;
|
||||||
|
require(inputTokenIndex < n && outputTokenIndex < n, "swap: idx");
|
||||||
|
require(maxAmountIn > 0, "swap: input zero");
|
||||||
|
require(deadline == 0 || block.timestamp <= deadline, "swap: deadline exceeded");
|
||||||
|
|
||||||
|
// Read previous balances
|
||||||
|
uint256 prevBalI = IERC20(state.tokens[inputTokenIndex]).balanceOf(address(this));
|
||||||
|
uint256 prevBalJ = IERC20(state.tokens[outputTokenIndex]).balanceOf(address(this));
|
||||||
|
|
||||||
|
// Compute amounts
|
||||||
|
(uint256 totalTransferAmount, uint256 amountOutUint, int128 amountInInternalUsed, int128 amountOutInternal, , uint256 feeUint) =
|
||||||
|
_quoteSwapExactIn(state, inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice, swapFeePpm);
|
||||||
|
|
||||||
|
// Transfer exact amount from payer
|
||||||
|
state.tokens[inputTokenIndex].safeTransferFrom(payer, address(this), totalTransferAmount);
|
||||||
|
uint256 balIAfter = IERC20(state.tokens[inputTokenIndex]).balanceOf(address(this));
|
||||||
|
require(balIAfter == prevBalI + totalTransferAmount, "swap: non-standard tokenIn");
|
||||||
|
|
||||||
|
// Transfer output to receiver
|
||||||
|
state.tokens[outputTokenIndex].safeTransfer(receiver, amountOutUint);
|
||||||
|
uint256 balJAfter = IERC20(state.tokens[outputTokenIndex]).balanceOf(address(this));
|
||||||
|
require(balJAfter == prevBalJ - amountOutUint, "swap: non-standard tokenOut");
|
||||||
|
|
||||||
|
// Update cached balances
|
||||||
|
state.cachedUintBalances[inputTokenIndex] = balIAfter;
|
||||||
|
state.cachedUintBalances[outputTokenIndex] = balJAfter;
|
||||||
|
|
||||||
|
// Apply swap to LMSR state
|
||||||
|
state.lmsr.applySwap(inputTokenIndex, outputTokenIndex, amountInInternalUsed, amountOutInternal);
|
||||||
|
|
||||||
|
emit PoolLib.Swap(payer, receiver, state.tokens[inputTokenIndex], state.tokens[outputTokenIndex], totalTransferAmount, amountOutUint);
|
||||||
|
|
||||||
|
return (totalTransferAmount, amountOutUint, feeUint);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
// SPDX-License-Identifier: UNLICENSED
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
pragma solidity ^0.8.30;
|
pragma solidity ^0.8.30;
|
||||||
|
|
||||||
import "forge-std/Test.sol";
|
import "../src/Deploy.sol";
|
||||||
import "@abdk/ABDKMath64x64.sol";
|
import "../src/IPartyFlashCallback.sol";
|
||||||
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
|
||||||
import "../src/LMSRStabilized.sol";
|
import "../src/LMSRStabilized.sol";
|
||||||
import "../src/PartyPool.sol";
|
import "../src/PartyPool.sol";
|
||||||
|
import "@abdk/ABDKMath64x64.sol";
|
||||||
|
|
||||||
// Import the flash callback interface
|
// Import the flash callback interface
|
||||||
import "../src/IPartyFlashCallback.sol";
|
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||||
|
import "forge-std/Test.sol";
|
||||||
|
|
||||||
/// @notice Test contract that implements the flash callback for testing flash loans
|
/// @notice Test contract that implements the flash callback for testing flash loans
|
||||||
contract FlashBorrower is IPartyFlashCallback {
|
contract FlashBorrower is IPartyFlashCallback {
|
||||||
@@ -172,7 +173,7 @@ contract GasTest is Test {
|
|||||||
}
|
}
|
||||||
// Compute kappa from slippage params and number of tokens, then construct pool with kappa
|
// Compute kappa from slippage params and number of tokens, then construct pool with kappa
|
||||||
int128 computedKappa = LMSRStabilized.computeKappaFromSlippage(ierc20Tokens.length, tradeFrac, targetSlippage);
|
int128 computedKappa = LMSRStabilized.computeKappaFromSlippage(ierc20Tokens.length, tradeFrac, targetSlippage);
|
||||||
PartyPool newPool = new PartyPool(poolName, poolName, ierc20Tokens, bases, computedKappa, feePpm, feePpm, false);
|
PartyPool newPool = Deploy.newPartyPool(poolName, poolName, ierc20Tokens, bases, computedKappa, feePpm, feePpm, false);
|
||||||
|
|
||||||
// Transfer initial deposit amounts into pool before initial mint
|
// Transfer initial deposit amounts into pool before initial mint
|
||||||
for (uint256 i = 0; i < numTokens; i++) {
|
for (uint256 i = 0; i < numTokens; i++) {
|
||||||
@@ -212,7 +213,7 @@ contract GasTest is Test {
|
|||||||
ierc20Tokens[i] = IERC20(tokens[i]);
|
ierc20Tokens[i] = IERC20(tokens[i]);
|
||||||
}
|
}
|
||||||
int128 computedKappa = LMSRStabilized.computeKappaFromSlippage(ierc20Tokens.length, tradeFrac, targetSlippage);
|
int128 computedKappa = LMSRStabilized.computeKappaFromSlippage(ierc20Tokens.length, tradeFrac, targetSlippage);
|
||||||
PartyPool newPool = new PartyPool(poolName, poolName, ierc20Tokens, bases, computedKappa, feePpm, feePpm, true);
|
PartyPool newPool = Deploy.newPartyPool(poolName, poolName, ierc20Tokens, bases, computedKappa, feePpm, feePpm, true);
|
||||||
|
|
||||||
// Transfer initial deposit amounts into pool before initial mint
|
// Transfer initial deposit amounts into pool before initial mint
|
||||||
for (uint256 i = 0; i < numTokens; i++) {
|
for (uint256 i = 0; i < numTokens; i++) {
|
||||||
|
|||||||
@@ -711,7 +711,7 @@ contract LMSRStabilizedTest is Test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Path 1: Direct swap from asset 0 to asset 2
|
// Path 1: Direct swap from asset 0 to asset 2
|
||||||
(int128 directAmountIn, int128 directAmountOut) = s.swapAmountsForExactInput(0, 2, directSwapAmount, 0);
|
(/* int128 directAmountIn */, int128 directAmountOut) = s.swapAmountsForExactInput(0, 2, directSwapAmount, 0);
|
||||||
|
|
||||||
// Restore original state for second path
|
// Restore original state for second path
|
||||||
_updateCachedQInternal(backupQ);
|
_updateCachedQInternal(backupQ);
|
||||||
@@ -724,7 +724,7 @@ contract LMSRStabilizedTest is Test {
|
|||||||
s.qInternal[1] = s.qInternal[1].add(indirectAmountOut1);
|
s.qInternal[1] = s.qInternal[1].add(indirectAmountOut1);
|
||||||
|
|
||||||
// Second swap: asset 1 -> asset 2
|
// Second swap: asset 1 -> asset 2
|
||||||
(int128 indirectAmountIn2, int128 indirectAmountOut2) = s.swapAmountsForExactInput(1, 2, indirectAmountOut1, 0);
|
(/* int128 indirectAmountIn2 */, int128 indirectAmountOut2) = s.swapAmountsForExactInput(1, 2, indirectAmountOut1, 0);
|
||||||
|
|
||||||
// The path independence property isn't perfect due to discrete swap mechanics,
|
// The path independence property isn't perfect due to discrete swap mechanics,
|
||||||
// but the difference should be within reasonable bounds
|
// but the difference should be within reasonable bounds
|
||||||
@@ -765,7 +765,7 @@ contract LMSRStabilizedTest is Test {
|
|||||||
s.qInternal[1] = s.qInternal[1].add(amountOut1);
|
s.qInternal[1] = s.qInternal[1].add(amountOut1);
|
||||||
|
|
||||||
// Step 2: Swap back asset 1 -> asset 0
|
// Step 2: Swap back asset 1 -> asset 0
|
||||||
(int128 amountIn2, int128 amountOut2) = s.swapAmountsForExactInput(1, 0, amountOut1, 0);
|
(/* int128 amountIn2 */, int128 amountOut2) = s.swapAmountsForExactInput(1, 0, amountOut1, 0);
|
||||||
|
|
||||||
// Calculate round-trip slippage: (initial amount - final amount) / initial amount
|
// Calculate round-trip slippage: (initial amount - final amount) / initial amount
|
||||||
int128 roundTripSlippage = (amountIn1.sub(amountOut2)).div(amountIn1);
|
int128 roundTripSlippage = (amountIn1.sub(amountOut2)).div(amountIn1);
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
// SPDX-License-Identifier: UNLICENSED
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
pragma solidity ^0.8.30;
|
pragma solidity ^0.8.30;
|
||||||
|
|
||||||
import "forge-std/Test.sol";
|
import "../src/Deploy.sol";
|
||||||
import "@abdk/ABDKMath64x64.sol";
|
import "../src/IPartyFlashCallback.sol";
|
||||||
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
|
||||||
import "../src/LMSRStabilized.sol";
|
import "../src/LMSRStabilized.sol";
|
||||||
import "../src/PartyPool.sol";
|
import "../src/PartyPool.sol";
|
||||||
|
import "@abdk/ABDKMath64x64.sol";
|
||||||
|
|
||||||
// Import the flash callback interface
|
// Import the flash callback interface
|
||||||
import "../src/IPartyFlashCallback.sol";
|
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||||
|
import "forge-std/Test.sol";
|
||||||
|
|
||||||
/// @notice Test contract that implements the flash callback for testing flash loans
|
/// @notice Test contract that implements the flash callback for testing flash loans
|
||||||
contract FlashBorrower is IPartyFlashCallback {
|
contract FlashBorrower is IPartyFlashCallback {
|
||||||
@@ -197,7 +198,7 @@ contract PartyPoolTest is Test {
|
|||||||
uint256 feePpm = 1000;
|
uint256 feePpm = 1000;
|
||||||
|
|
||||||
int128 kappa3 = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage);
|
int128 kappa3 = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage);
|
||||||
pool = new PartyPool("LP", "LP", tokens, bases, kappa3, feePpm, feePpm, false);
|
pool = Deploy.newPartyPool("LP", "LP", tokens, bases, kappa3, feePpm, feePpm, false);
|
||||||
|
|
||||||
// Transfer initial deposit amounts into pool before initial mint (pool expects tokens already in contract)
|
// Transfer initial deposit amounts into pool before initial mint (pool expects tokens already in contract)
|
||||||
// We deposit equal amounts INIT_BAL for each token
|
// We deposit equal amounts INIT_BAL for each token
|
||||||
@@ -227,7 +228,7 @@ contract PartyPoolTest is Test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int128 kappa10 = LMSRStabilized.computeKappaFromSlippage(tokens10.length, tradeFrac, targetSlippage);
|
int128 kappa10 = LMSRStabilized.computeKappaFromSlippage(tokens10.length, tradeFrac, targetSlippage);
|
||||||
pool10 = new PartyPool("LP10", "LP10", tokens10, bases10, kappa10, feePpm, feePpm, false);
|
pool10 = Deploy.newPartyPool("LP10", "LP10", tokens10, bases10, kappa10, feePpm, feePpm, false);
|
||||||
|
|
||||||
// Mint additional tokens for pool10 initial deposit
|
// Mint additional tokens for pool10 initial deposit
|
||||||
token0.mint(address(this), INIT_BAL);
|
token0.mint(address(this), INIT_BAL);
|
||||||
@@ -1231,11 +1232,11 @@ contract PartyPoolTest is Test {
|
|||||||
|
|
||||||
// Pool with default initialization (lpTokens = 0)
|
// Pool with default initialization (lpTokens = 0)
|
||||||
int128 kappaDefault = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage);
|
int128 kappaDefault = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage);
|
||||||
PartyPool poolDefault = new PartyPool("LP_DEFAULT", "LP_DEFAULT", tokens, bases, kappaDefault, feePpm, feePpm, false);
|
PartyPool poolDefault = Deploy.newPartyPool("LP_DEFAULT", "LP_DEFAULT", tokens, bases, kappaDefault, feePpm, feePpm, false);
|
||||||
|
|
||||||
// Pool with custom initialization (lpTokens = custom amount)
|
// Pool with custom initialization (lpTokens = custom amount)
|
||||||
int128 kappaCustom = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage);
|
int128 kappaCustom = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage);
|
||||||
PartyPool poolCustom = new PartyPool("LP_CUSTOM", "LP_CUSTOM", tokens, bases, kappaCustom, feePpm, feePpm, false);
|
PartyPool poolCustom = Deploy.newPartyPool("LP_CUSTOM", "LP_CUSTOM", tokens, bases, kappaCustom, feePpm, feePpm, false);
|
||||||
|
|
||||||
// Mint additional tokens for both pools
|
// Mint additional tokens for both pools
|
||||||
token0.mint(address(this), INIT_BAL * 2);
|
token0.mint(address(this), INIT_BAL * 2);
|
||||||
@@ -1307,9 +1308,9 @@ contract PartyPoolTest is Test {
|
|||||||
uint256 feePpm = 1000;
|
uint256 feePpm = 1000;
|
||||||
|
|
||||||
int128 kappaDefault2 = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage);
|
int128 kappaDefault2 = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage);
|
||||||
PartyPool poolDefault = new PartyPool("LP_DEFAULT", "LP_DEFAULT", tokens, bases, kappaDefault2, feePpm, feePpm, false);
|
PartyPool poolDefault = Deploy.newPartyPool("LP_DEFAULT", "LP_DEFAULT", tokens, bases, kappaDefault2, feePpm, feePpm, false);
|
||||||
int128 kappaCustom2 = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage);
|
int128 kappaCustom2 = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage);
|
||||||
PartyPool poolCustom = new PartyPool("LP_CUSTOM", "LP_CUSTOM", tokens, bases, kappaCustom2, feePpm, feePpm, false);
|
PartyPool poolCustom = Deploy.newPartyPool("LP_CUSTOM", "LP_CUSTOM", tokens, bases, kappaCustom2, feePpm, feePpm, false);
|
||||||
|
|
||||||
// Mint additional tokens
|
// Mint additional tokens
|
||||||
token0.mint(address(this), INIT_BAL * 4);
|
token0.mint(address(this), INIT_BAL * 4);
|
||||||
|
|||||||
Reference in New Issue
Block a user