additive fees; burnSwapAmounts fix

This commit is contained in:
tim
2025-11-04 16:58:16 -04:00
parent 590acdd4dc
commit dc2e186331
10 changed files with 103 additions and 100 deletions

24
doc/launch_list.md Normal file
View File

@@ -0,0 +1,24 @@
| Symbol | Address |
|-----------------|----------------------------------------------|
| **Stablecoins** | |
| USDT | `0xdAC17F958D2ee523a2206206994597C13D831ec7` |
| USDC | `0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48` |
| USDe | `0x4c9EDD5852cd905f086C759E8383e09bff1E68B3` |
| DAI | `0x6B175474E89094C44Da98b954EedeAC495271d0F` |
| **Chain Coins** | |
| WETH | `0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2` |
| WBTC | `0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599` |
| WSOL | `0xd1D82d3Ab815E0B47e38EC2d666c5b8AA05Ae501` |
| BNB | `0xB8c77482e45F1F44dE1745F52C74426C631bDD52` |
| TRX | `0x50327c6c5a14DCaDE707ABad2E27eB517df87AB5` |
| POL | `0x455e53CBB86018Ac2B8092FdCd39d8444aFFC3F6` |
| DOT | `0x21c2c96Dbfa137E23946143c71AC8330F9B44001` |
| NEAR | `0x85F17Cf997934a597031b2E18a9aB6ebD4B9f6a4` |
| **Others** | |
| WBETH | `0xa2E3356610840701BDf5611a53974510Ae27E2e1` |
| ARB | `0xB50721BCf8d664c30412Cfbc6cf7a15145234ad1` |
| UNI | `0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984` |
| LINK | `0x514910771AF9Ca656af840dff83E8264EcF986CA` |
| AAVE | `0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9` |
| PEPE | `0x6982508145454Ce325dDbE47a25d4ec3d2311933` |
| SHIB | `0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE` |

View File

@@ -117,20 +117,19 @@ if __name__ == "__main__":
if alerts: if alerts:
log.warning("Some fee configuration may produce large values (bps): %s", alerts) log.warning("Some fee configuration may produce large values (bps): %s", alerts)
print("\nPair fees [per-asset fee -> total]:") # Per-asset diagnostic: show per-asset fee (interpreted vs USDC)
for n, w in weights.items():
f_token = per_asset_fee(w)
print(f"{n:>12} {bps(f_token):>12}")
print()
for (name_a, w_a), (name_b, w_b) in combinations(weights.items(), 2): for (name_a, w_a), (name_b, w_b) in combinations(weights.items(), 2):
f_eff_ab, f_i_ab, f_j_ab = fee_for_pair(w_a, w_b) f_eff_ab, f_i_ab, f_j_ab = fee_for_pair(w_a, w_b)
print( print(
f"{name_a}/{name_b}: per-asset_in={bps(f_i_ab)}, per-asset_out={bps(f_j_ab)}, total={bps(f_eff_ab)}" f"{name_a+'-'+name_b:>12} {bps(f_eff_ab):>12}"
) )
# Per-asset diagnostic: show per-asset fee (interpreted vs USDC)
print("\nPer-asset diagnostics (fees interpreted vs USDC):")
for n, w in weights.items():
f_token = per_asset_fee(w)
print(f"{n}: target_vs_USDC={w:.6f} bps, per_asset_fee={bps(f_token)}")
# -- Solidity (ABDK Q64.64) output -- # -- Solidity (ABDK Q64.64) output --
# Print derived per-asset fees and cap as Solidity int128 Q64.64 constants that can be pasted # Print derived per-asset fees and cap as Solidity int128 Q64.64 constants that can be pasted
# into a Solidity file using ABDK's 64.64 fixed-point representation (int128 constants). # into a Solidity file using ABDK's 64.64 fixed-point representation (int128 constants).

View File

@@ -240,7 +240,8 @@ interface IPartyPool is IERC20Metadata, IOwnable {
/// @param lpAmount amount of LP tokens to burn /// @param lpAmount amount of LP tokens to burn
/// @param outputTokenIndex index of target asset to receive /// @param outputTokenIndex index of target asset to receive
/// @param deadline optional deadline /// @param deadline optional deadline
/// @return amountOutUint uint amount of asset outputTokenIndex sent to receiver /// @return amountOut uint amount of asset outputTokenIndex sent to receiver
/// @return outFee uint amount of output asset kept by the LP's and protocol as a fee
function burnSwap( function burnSwap(
address payer, address payer,
address receiver, address receiver,
@@ -248,7 +249,7 @@ interface IPartyPool is IERC20Metadata, IOwnable {
uint256 outputTokenIndex, uint256 outputTokenIndex,
uint256 deadline, uint256 deadline,
bool unwrap bool unwrap
) external returns (uint256 amountOutUint); ) external returns (uint256 amountOut, uint256 outFee);
/// @dev Initiate a flash loan. /// @dev Initiate a flash loan.
/// @param receiver The receiver of the tokens in the loan, and the receiver of the callback. /// @param receiver The receiver of the tokens in the loan, and the receiver of the callback.

View File

@@ -519,7 +519,7 @@ library LMSRStabilized {
State storage s, State storage s,
uint256 i, uint256 i,
int128 alpha int128 alpha
) internal view returns (int128 amountOut, int128 amountIn) { ) internal view returns (int128 amountIn, int128 amountOut) {
return swapAmountsForBurn(s.kappa, s.qInternal, i, alpha); return swapAmountsForBurn(s.kappa, s.qInternal, i, alpha);
} }
@@ -536,14 +536,14 @@ library LMSRStabilized {
/// @param qInternal Cached internal balances in 64.64 fixed-point format /// @param qInternal Cached internal balances in 64.64 fixed-point format
/// @param i Index of output asset /// @param i Index of output asset
/// @param alpha Proportional share to burn (0 < alpha <= 1) /// @param alpha Proportional share to burn (0 < alpha <= 1)
/// @return amountOut Amount of asset i received (in 64.64 fixed-point)
/// @return amountIn LP size-metric redeemed (alpha * S) /// @return amountIn LP size-metric redeemed (alpha * S)
/// @return amountOut Amount of asset i received (in 64.64 fixed-point)
function swapAmountsForBurn( function swapAmountsForBurn(
int128 kappa, int128 kappa,
int128[] memory qInternal, int128[] memory qInternal,
uint256 i, uint256 i,
int128 alpha int128 alpha
) internal pure returns (int128 amountOut, int128 amountIn) { ) internal pure returns (int128 amountIn, int128 amountOut) {
require(alpha > int128(0) && alpha <= ONE, "LMSR: alpha"); require(alpha > int128(0) && alpha <= ONE, "LMSR: alpha");
int128 sizeMetric = _computeSizeMetric(qInternal); int128 sizeMetric = _computeSizeMetric(qInternal);
@@ -566,10 +566,10 @@ library LMSRStabilized {
} }
// Start totalOut with direct portion of asset i redeemed // Start totalOut with direct portion of asset i redeemed
int128 totalOut = alpha.mul(qInternal[i]); amountOut = alpha.mul(qInternal[i]);
// Track whether any non-zero contribution was produced // Track whether any non-zero contribution was produced
bool anyNonZero = (totalOut > int128(0)); bool anyNonZero = (amountOut > int128(0));
// For each asset j != i, swap the withdrawn a_j := alpha * q_j into i // For each asset j != i, swap the withdrawn a_j := alpha * q_j into i
for (uint256 j = 0; j < n; ) { for (uint256 j = 0; j < n; ) {
@@ -621,7 +621,7 @@ library LMSRStabilized {
// Update q_local: pool receives amountInUsed on asset j, and loses qLocal[i] // Update q_local: pool receives amountInUsed on asset j, and loses qLocal[i]
qLocal[j] = qLocal[j].add(amountInUsed); qLocal[j] = qLocal[j].add(amountInUsed);
// subtract capped output from qLocal[i] (becomes zero) // subtract capped output from qLocal[i] (becomes zero)
totalOut = totalOut.add(qLocal[i]); amountOut = amountOut.add(qLocal[i]);
qLocal[i] = int128(0); qLocal[i] = int128(0);
anyNonZero = true; anyNonZero = true;
unchecked { j++; } unchecked { j++; }
@@ -632,7 +632,7 @@ library LMSRStabilized {
// Update q_local accordingly: pool receives aj on j, and loses y on i // Update q_local accordingly: pool receives aj on j, and loses y on i
qLocal[j] = qLocal[j].add(aj); qLocal[j] = qLocal[j].add(aj);
qLocal[i] = qLocal[i].sub(y); qLocal[i] = qLocal[i].sub(y);
totalOut = totalOut.add(y); amountOut = amountOut.add(y);
anyNonZero = true; anyNonZero = true;
} }
} }
@@ -640,12 +640,9 @@ library LMSRStabilized {
} }
// If no asset contributed (totalOut == 0) treat as no-trade and revert // If no asset contributed (totalOut == 0) treat as no-trade and revert
if (!anyNonZero || totalOut <= int128(0)) { if (!anyNonZero || amountOut <= int128(0)) {
revert("LMSR: zero output"); revert("LMSR: zero output");
} }
amountOut = totalOut;
return (amountOut, amountIn);
} }

View File

@@ -408,14 +408,7 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
return abi.decode(result, (uint256, uint256, uint256)); return abi.decode(result, (uint256, uint256, uint256));
} }
/// @notice Burn LP _tokens then swap the redeemed proportional basket into a single asset `inputTokenIndex` and send to receiver. /// @inheritdoc IPartyPool
/// @dev This function forwards the call to the burnSwap implementation via delegatecall
/// @param payer who burns LP _tokens
/// @param receiver who receives the single asset
/// @param lpAmount amount of LP _tokens to burn
/// @param outputTokenIndex index of target asset to receive
/// @param deadline optional deadline
/// @return amountOutUint uint amount of asset i sent to receiver
function burnSwap( function burnSwap(
address payer, address payer,
address receiver, address receiver,
@@ -423,7 +416,7 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
uint256 outputTokenIndex, uint256 outputTokenIndex,
uint256 deadline, uint256 deadline,
bool unwrap bool unwrap
) external returns (uint256 amountOutUint) { ) external returns (uint256 amountOut, uint256 outFee) {
bytes memory data = abi.encodeWithSelector( bytes memory data = abi.encodeWithSelector(
PartyPoolMintImpl.burnSwap.selector, PartyPoolMintImpl.burnSwap.selector,
payer, payer,
@@ -437,7 +430,7 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
); );
bytes memory result = Address.functionDelegateCall(address(MINT_IMPL), data); bytes memory result = Address.functionDelegateCall(address(MINT_IMPL), data);
return abi.decode(result, (uint256)); return abi.decode(result, (uint256,uint256 ));
} }

View File

@@ -96,7 +96,10 @@ abstract contract PartyPoolBase is OwnableInternal, ERC20Internal, ReentrancyGua
function _pairFeePpmView(uint256 i, uint256 j) internal view returns (uint256) { function _pairFeePpmView(uint256 i, uint256 j) internal view returns (uint256) {
uint256 fi = _fees[i]; uint256 fi = _fees[i];
uint256 fj = _fees[j]; uint256 fj = _fees[j];
return fi + fj - fi * fj / 1_000_000; // multiplicative combination, while mathematically correct, is more confusing to users
// return fi + fj - fi * fj / 1_000_000;
// additive fees are easy to understand and very very close to the multiplicative combination.
return fi + fj;
} }
function _pairFeePpm(uint256 i, uint256 j) internal returns (uint256 fee) { function _pairFeePpm(uint256 i, uint256 j) internal returns (uint256 fee) {

View File

@@ -269,7 +269,7 @@ contract PartyPoolMintImpl is PartyPoolBase {
/// @param lmsrState current LMSR state /// @param lmsrState current LMSR state
/// @param bases_ scaling _bases for each token /// @param bases_ scaling _bases for each token
/// @param totalSupply_ current total LP token supply /// @param totalSupply_ current total LP token supply
/// @return amountInUsed actual input amount used (excluding fee) /// @return amountIn actual input amount used (excluding fee)
/// @return lpMinted LP tokens that would be minted /// @return lpMinted LP tokens that would be minted
/// @return inFee fee amount charged /// @return inFee fee amount charged
function swapMintAmounts( function swapMintAmounts(
@@ -279,7 +279,7 @@ contract PartyPoolMintImpl is PartyPoolBase {
LMSRStabilized.State memory lmsrState, LMSRStabilized.State memory lmsrState,
uint256[] memory bases_, uint256[] memory bases_,
uint256 totalSupply_ uint256 totalSupply_
) public pure returns (uint256 amountInUsed, uint256 lpMinted, uint256 inFee) { ) public pure returns (uint256 amountIn, uint256 lpMinted, uint256 inFee) {
require(inputTokenIndex < bases_.length, "swapMintAmounts: idx"); require(inputTokenIndex < bases_.length, "swapMintAmounts: idx");
require(maxAmountIn > 0, "swapMintAmounts: input zero"); require(maxAmountIn > 0, "swapMintAmounts: input zero");
require(lmsrState.qInternal.length > 0, "swapMintAmounts: uninit pool"); require(lmsrState.qInternal.length > 0, "swapMintAmounts: uninit pool");
@@ -302,7 +302,7 @@ contract PartyPoolMintImpl is PartyPoolBase {
inputTokenIndex, netInternalGuess); inputTokenIndex, netInternalGuess);
// amountInInternalUsed may be <= netInternalGuess. Convert to uint (ceil) to determine actual transfer // amountInInternalUsed may be <= netInternalGuess. Convert to uint (ceil) to determine actual transfer
amountInUsed = _internalToUintCeilPure(amountInInternalUsed, bases_[inputTokenIndex]); uint256 amountInUsed = _internalToUintCeilPure(amountInInternalUsed, bases_[inputTokenIndex]);
require(amountInUsed > 0, "swapMintAmounts: input zero after internal conversion"); require(amountInUsed > 0, "swapMintAmounts: input zero after internal conversion");
// Compute fee on the actual used input (ceiling) // Compute fee on the actual used input (ceiling)
@@ -310,8 +310,8 @@ contract PartyPoolMintImpl is PartyPoolBase {
if (swapFeePpm > 0) { if (swapFeePpm > 0) {
inFee = (amountInUsed * swapFeePpm + 999999) / 1000000; // ceil fee inFee = (amountInUsed * swapFeePpm + 999999) / 1000000; // ceil fee
} }
uint256 totalTransfer = amountInUsed + inFee; amountIn = amountInUsed + inFee;
require(totalTransfer > 0 && totalTransfer <= maxAmountIn, "swapMintAmounts: transfer exceeds max"); require(amountIn > 0 && amountIn <= maxAmountIn, "swapMintAmounts: transfer exceeds max");
// Compute old and new scaled size metrics to determine LP minted // Compute old and new scaled size metrics to determine LP minted
int128 oldTotal = _computeSizeMetricPure(lmsrState.qInternal); int128 oldTotal = _computeSizeMetricPure(lmsrState.qInternal);
@@ -347,7 +347,7 @@ contract PartyPoolMintImpl is PartyPoolBase {
/// @param maxAmountIn maximum uint token input (inclusive of fee) /// @param maxAmountIn maximum uint token input (inclusive of fee)
/// @param deadline optional deadline /// @param deadline optional deadline
/// @param swapFeePpm fee in parts-per-million for this pool /// @param swapFeePpm fee in parts-per-million for this pool
/// @return amountInUsed actual input used (uint256), lpMinted actual LP minted (uint256), inFee fee taken from the input (uint256) /// @return amountIn actual input used (uint256), lpMinted actual LP minted (uint256), inFee fee taken from the input (uint256)
function swapMint( function swapMint(
address payer, address payer,
address receiver, address receiver,
@@ -356,7 +356,7 @@ contract PartyPoolMintImpl is PartyPoolBase {
uint256 deadline, uint256 deadline,
uint256 swapFeePpm, uint256 swapFeePpm,
uint256 protocolFeePpm uint256 protocolFeePpm
) external payable native killable nonReentrant returns (uint256 amountInUsed, uint256 lpMinted, uint256 inFee) { ) external payable native killable nonReentrant returns (uint256 amountIn, uint256 lpMinted, uint256 inFee) {
uint256 n = _tokens.length; uint256 n = _tokens.length;
require(inputTokenIndex < n, "swapMint: idx"); require(inputTokenIndex < n, "swapMint: idx");
require(maxAmountIn > 0, "swapMint: input zero"); require(maxAmountIn > 0, "swapMint: input zero");
@@ -374,27 +374,27 @@ contract PartyPoolMintImpl is PartyPoolBase {
(int128 amountInInternalUsed, int128 sizeIncreaseInternal) = _lmsr.swapAmountsForMint(inputTokenIndex, netInternalGuess); (int128 amountInInternalUsed, int128 sizeIncreaseInternal) = _lmsr.swapAmountsForMint(inputTokenIndex, netInternalGuess);
// amountInInternalUsed may be <= netInternalGuess. Convert to uint (ceil) to determine actual transfer // amountInInternalUsed may be <= netInternalGuess. Convert to uint (ceil) to determine actual transfer
uint256 amountInUint = _internalToUintCeil(amountInInternalUsed, _bases[inputTokenIndex]); uint256 amountInUsed = _internalToUintCeil(amountInInternalUsed, _bases[inputTokenIndex]);
require(amountInUint > 0, "swapMint: input zero after internal conversion"); require(amountInUsed > 0, "swapMint: input zero after internal conversion");
// Compute fee on the actual used input and total transfer amount (ceiling) // Compute fee on the actual used input and total transfer amount (ceiling)
uint256 feeUintActual = _ceilFee(amountInUint, swapFeePpm); inFee = _ceilFee(amountInUsed, swapFeePpm);
uint256 totalTransfer = amountInUint + feeUintActual; amountIn = amountInUsed + inFee;
require(totalTransfer > 0 && totalTransfer <= maxAmountIn, "swapMint: transfer exceeds max"); require(amountIn > 0 && amountIn <= maxAmountIn, "swapMint: transfer exceeds max");
// Transfer _tokens from payer (assume standard ERC20 without transfer fees) via helper // Transfer _tokens from payer (assume standard ERC20 without transfer fees) via helper
_receiveTokenFrom(payer, _tokens[inputTokenIndex], totalTransfer); _receiveTokenFrom(payer, _tokens[inputTokenIndex], amountIn);
// Accrue protocol share (floor) from the fee on the input token // Accrue protocol share (floor) from the fee on the input token
uint256 protoShare = 0; uint256 protoShare = 0;
if (protocolFeePpm > 0 && feeUintActual > 0) { if (protocolFeePpm > 0 && inFee > 0) {
protoShare = (feeUintActual * protocolFeePpm) / 1_000_000; protoShare = (inFee * protocolFeePpm) / 1_000_000;
if (protoShare > 0) { if (protoShare > 0) {
_protocolFeesOwed[inputTokenIndex] += protoShare; _protocolFeesOwed[inputTokenIndex] += protoShare;
} }
} }
// Update cached effective balance directly: add totalTransfer minus protocol share // Update cached effective balance directly: add totalTransfer minus protocol share
_cachedUintBalances[inputTokenIndex] += (totalTransfer - protoShare); _cachedUintBalances[inputTokenIndex] += (amountIn - protoShare);
// Compute old and new scaled size metrics to determine LP minted // Compute old and new scaled size metrics to determine LP minted
int128 oldTotal = _computeSizeMetric(_lmsr.qInternal); int128 oldTotal = _computeSizeMetric(_lmsr.qInternal);
@@ -403,23 +403,22 @@ contract PartyPoolMintImpl is PartyPoolBase {
int128 newTotal = oldTotal.add(sizeIncreaseInternal); int128 newTotal = oldTotal.add(sizeIncreaseInternal);
uint256 newScaled = ABDKMath64x64.mulu(newTotal, LP_SCALE); uint256 newScaled = ABDKMath64x64.mulu(newTotal, LP_SCALE);
uint256 actualLpToMint;
// Use natural ERC20 function since base contract inherits from ERC20 // Use natural ERC20 function since base contract inherits from ERC20
uint256 currentSupply = _totalSupply; uint256 currentSupply = _totalSupply;
if (currentSupply == 0) { if (currentSupply == 0) {
// If somehow supply zero (shouldn't happen as _lmsr.nAssets>0), mint newScaled // If somehow supply zero (shouldn't happen as _lmsr.nAssets>0), mint newScaled
actualLpToMint = newScaled; lpMinted = newScaled;
} else { } else {
uint256 delta = (newScaled > oldScaled) ? (newScaled - oldScaled) : 0; uint256 delta = (newScaled > oldScaled) ? (newScaled - oldScaled) : 0;
if (delta > 0) { if (delta > 0) {
// floor truncation rounds in favor of pool // floor truncation rounds in favor of pool
actualLpToMint = (currentSupply * delta) / oldScaled; lpMinted = (currentSupply * delta) / oldScaled;
} else { } else {
actualLpToMint = 0; lpMinted = 0;
} }
} }
require(actualLpToMint > 0, "swapMint: zero LP minted"); require(lpMinted > 0, "swapMint: zero LP minted");
// Update LMSR internal state: scale qInternal proportionally by newTotal/oldTotal // Update LMSR internal state: scale qInternal proportionally by newTotal/oldTotal
int128[] memory newQInternal = new int128[](n); int128[] memory newQInternal = new int128[](n);
@@ -432,14 +431,11 @@ contract PartyPoolMintImpl is PartyPoolBase {
_lmsr.updateForProportionalChange(newQInternal); _lmsr.updateForProportionalChange(newQInternal);
// Use natural ERC20 function since base contract inherits from ERC20 // Use natural ERC20 function since base contract inherits from ERC20
_mint(receiver, actualLpToMint); _mint(receiver, lpMinted);
emit IPartyPool.SwapMint(payer, receiver, _tokens[inputTokenIndex], emit IPartyPool.SwapMint(payer, receiver, _tokens[inputTokenIndex],
totalTransfer, actualLpToMint, feeUintActual-protoShare, protoShare); amountIn, lpMinted, inFee -protoShare, protoShare);
amountInUsed = amountInUint;
lpMinted = actualLpToMint;
inFee = feeUintActual;
return (amountInUsed, lpMinted, inFee); return (amountInUsed, lpMinted, inFee);
} }
@@ -465,23 +461,17 @@ contract PartyPoolMintImpl is PartyPoolBase {
require(totalSupply_ > 0, "burnSwapAmounts: empty supply"); require(totalSupply_ > 0, "burnSwapAmounts: empty supply");
// alpha = lpAmount / supply as Q64.64 // alpha = lpAmount / supply as Q64.64
int128 alpha = ABDKMath64x64.divu(lpAmount, totalSupply_) // fraction of total supply to burn int128 alpha = ABDKMath64x64.divu(lpAmount, totalSupply_);
.mul(ABDKMath64x64.divu(1000000-swapFeePpm, 1000000)); // adjusted for fee
// Use LMSR view to compute single-asset payout and burned size-metric // Use LMSR view to compute single-asset payout and burned size-metric
(int128 payoutInternal, ) = LMSRStabilized.swapAmountsForBurn(lmsrState.kappa, lmsrState.qInternal, (, int128 payoutInternal) = LMSRStabilized.swapAmountsForBurn(lmsrState.kappa, lmsrState.qInternal,
outputTokenIndex, alpha); outputTokenIndex, alpha);
// Convert payoutInternal -> uint (floor) to favor pool // Convert payoutInternal -> uint (floor) to favor pool
amountOut = _internalToUintFloorPure(payoutInternal, bases_[outputTokenIndex]); uint256 grossAmountOut = _internalToUintFloorPure(payoutInternal, bases_[outputTokenIndex]);
require(amountOut > 0, "burnSwapAmounts: output zero"); (outFee,) = _computeFee(grossAmountOut, swapFeePpm);
require(grossAmountOut > outFee, "burnSwapAmounts: output zero");
// Compute gross payout (no swap fee) to derive token-side fee = gross - net amountOut = grossAmountOut - outFee;
int128 alphaGross = ABDKMath64x64.divu(lpAmount, totalSupply_); // gross fraction (no swap fee)
(int128 payoutGrossInternal, ) = LMSRStabilized.swapAmountsForBurn(lmsrState.kappa, lmsrState.qInternal,
outputTokenIndex, alphaGross);
uint256 payoutGrossUint = _internalToUintFloorPure(payoutGrossInternal, bases_[outputTokenIndex]);
outFee = (payoutGrossUint > amountOut) ? (payoutGrossUint - amountOut) : 0;
} }
/// @notice Burn LP _tokens then swap the redeemed proportional basket into a single asset `outputTokenIndex` and send to receiver. /// @notice Burn LP _tokens then swap the redeemed proportional basket into a single asset `outputTokenIndex` and send to receiver.
@@ -494,7 +484,8 @@ contract PartyPoolMintImpl is PartyPoolBase {
/// @param outputTokenIndex index of target asset to receive /// @param outputTokenIndex index of target asset to receive
/// @param deadline optional deadline /// @param deadline optional deadline
/// @param swapFeePpm fee in parts-per-million for this pool (may be used for future fee logic) /// @param swapFeePpm fee in parts-per-million for this pool (may be used for future fee logic)
/// @return amountOutUint uint amount of asset i sent to receiver /// @return amountOut uint amount of asset i sent to receiver
/// @return outFee uint amount of asset i kept as an LP and protocol fee
function burnSwap( function burnSwap(
address payer, address payer,
address receiver, address receiver,
@@ -504,7 +495,7 @@ contract PartyPoolMintImpl is PartyPoolBase {
bool unwrap, bool unwrap,
uint256 swapFeePpm, uint256 swapFeePpm,
uint256 protocolFeePpm uint256 protocolFeePpm
) external nonReentrant killable returns (uint256 amountOutUint) { ) external nonReentrant killable returns (uint256 amountOut, uint256 outFee) {
uint256 n = _tokens.length; uint256 n = _tokens.length;
require(outputTokenIndex < n, "burnSwap: idx"); require(outputTokenIndex < n, "burnSwap: idx");
require(lpAmount > 0, "burnSwap: zero lp"); require(lpAmount > 0, "burnSwap: zero lp");
@@ -514,35 +505,26 @@ contract PartyPoolMintImpl is PartyPoolBase {
require(supply > 0, "burnSwap: empty supply"); require(supply > 0, "burnSwap: empty supply");
// alpha = lpAmount / supply as Q64.64 (adjusted for fee) // alpha = lpAmount / supply as Q64.64 (adjusted for fee)
int128 alpha = ABDKMath64x64.divu(lpAmount, supply) // fraction of total supply to burn int128 alpha = ABDKMath64x64.divu(lpAmount, supply); // fraction of total supply to burn
.mul(ABDKMath64x64.divu(1000000-swapFeePpm, 1000000)); // adjusted for fee
// Use LMSR view to compute single-asset payout and burned size-metric // Use LMSR view to compute single-asset payout and burned size-metric
(int128 payoutInternal, ) = _lmsr.swapAmountsForBurn(outputTokenIndex, alpha); (, int128 payoutInternal) = _lmsr.swapAmountsForBurn(outputTokenIndex, alpha);
// Convert payoutInternal -> uint (floor) to favor pool // Convert payoutInternal -> uint (floor) to favor pool
amountOutUint = _internalToUintFloor(payoutInternal, _bases[outputTokenIndex]); uint256 payoutGrossUint = _internalToUintFloorPure(payoutInternal, _bases[outputTokenIndex]);
require(amountOutUint > 0, "burnSwap: output zero"); (outFee,) = _computeFee(payoutGrossUint, swapFeePpm);
require(payoutGrossUint > outFee, "burnSwapAmounts: output zero");
// Compute gross payout (no swap fee) so we can determine token-side fee = gross - net amountOut = payoutGrossUint - outFee;
int128 alphaGross = ABDKMath64x64.divu(lpAmount, supply); // gross fraction (no swap fee)
(int128 payoutGrossInternal, ) = _lmsr.swapAmountsForBurn(outputTokenIndex, alphaGross);
uint256 payoutGrossUint = _internalToUintFloor(payoutGrossInternal, _bases[outputTokenIndex]);
uint256 feeTokenUint = (payoutGrossUint > amountOutUint) ? (payoutGrossUint - amountOutUint) : 0;
// Accrue protocol share (floor) from the token-side fee // Accrue protocol share (floor) from the token-side fee
uint256 protoShare = 0; uint256 protoShare = 0;
if (protocolFeePpm > 0 && feeTokenUint > 0) { if (protocolFeePpm > 0 && outFee > 0) {
protoShare = (feeTokenUint * protocolFeePpm) / 1_000_000; protoShare = (outFee * protocolFeePpm) / 1_000_000;
if (protoShare > 0) { if (protoShare > 0) {
_protocolFeesOwed[outputTokenIndex] += protoShare; _protocolFeesOwed[outputTokenIndex] += protoShare;
} }
} }
// Transfer the payout to receiver via centralized helper
IERC20 outputToken = _tokens[outputTokenIndex];
_sendTokenTo(outputToken, receiver, amountOutUint, unwrap);
// Burn LP _tokens from payer (authorization via allowance) // Burn LP _tokens from payer (authorization via allowance)
if (msg.sender != payer) { if (msg.sender != payer) {
uint256 allowed = _allowances[payer][msg.sender]; uint256 allowed = _allowances[payer][msg.sender];
@@ -550,13 +532,19 @@ contract PartyPoolMintImpl is PartyPoolBase {
} }
_burn(payer, lpAmount); _burn(payer, lpAmount);
// Transfer the payout to receiver via centralized helper
IERC20 outputToken = _tokens[outputTokenIndex];
_sendTokenTo(outputToken, receiver, amountOut, unwrap);
// Update cached balances using computed payout and protocol fee; no on-chain reads // Update cached balances using computed payout and protocol fee; no on-chain reads
int128[] memory newQInternal = new int128[](n); int128[] memory newQInternal = new int128[](n);
for (uint256 idx = 0; idx < n; idx++) { for (uint256 idx = 0; idx < n; idx++) {
uint256 newBal = _cachedUintBalances[idx]; uint256 newBal = _cachedUintBalances[idx];
if (idx == outputTokenIndex) { if (idx == outputTokenIndex) {
// Effective LP balance decreases by net payout and increased protocol owed // Effective LP balance decreases by net payout and increased protocol owed
newBal = newBal - amountOutUint - protoShare; newBal = newBal - amountOut - protoShare;
} }
_cachedUintBalances[idx] = newBal; _cachedUintBalances[idx] = newBal;
newQInternal[idx] = _uintToInternalFloor(newBal, _bases[idx]); newQInternal[idx] = _uintToInternalFloor(newBal, _bases[idx]);
@@ -573,10 +561,8 @@ contract PartyPoolMintImpl is PartyPoolBase {
_lmsr.updateForProportionalChange(newQInternal); _lmsr.updateForProportionalChange(newQInternal);
} }
emit IPartyPool.BurnSwap(payer, receiver, outputToken, lpAmount, amountOutUint, emit IPartyPool.BurnSwap(payer, receiver, outputToken, lpAmount, amountOut,
feeTokenUint-protoShare, protoShare); outFee-protoShare, protoShare);
return amountOutUint;
} }
/// @notice Pure version of _uintToInternalFloor for use in view functions /// @notice Pure version of _uintToInternalFloor for use in view functions

View File

@@ -900,7 +900,7 @@ contract LMSRStabilizedTest is Test {
int128 alpha = ABDKMath64x64.divu(1, 100); // 1% int128 alpha = ABDKMath64x64.divu(1, 100); // 1%
int128 S = _computeSizeMetric(s.qInternal); int128 S = _computeSizeMetric(s.qInternal);
(int128 payout, int128 burned) = s.swapAmountsForBurn(0, alpha); (int128 burned, int128 payout) = s.swapAmountsForBurn(0, alpha);
// burned should equal alpha * S // burned should equal alpha * S
assertEq(burned, alpha.mul(S), "burned size-metric mismatch"); assertEq(burned, alpha.mul(S), "burned size-metric mismatch");
@@ -921,7 +921,7 @@ contract LMSRStabilizedTest is Test {
_updateCachedQInternal(mockQInternal); _updateCachedQInternal(mockQInternal);
int128 alpha = ABDKMath64x64.divu(1, 100); // 1% int128 alpha = ABDKMath64x64.divu(1, 100); // 1%
(int128 payout, int128 burned) = s.swapAmountsForBurn(0, alpha); (int128 burned, int128 payout) = s.swapAmountsForBurn(0, alpha);
// Should still burn the size metric // Should still burn the size metric
int128 S = _computeSizeMetric(mockQInternal); int128 S = _computeSizeMetric(mockQInternal);

View File

@@ -484,7 +484,7 @@ contract NativeTest is Test {
uint256 thisEthBefore = address(this).balance; uint256 thisEthBefore = address(this).balance;
// Burn LP and receive all proceeds as native currency (WETH unwrapped) // Burn LP and receive all proceeds as native currency (WETH unwrapped)
uint256 payout = pool.burnSwap( (uint256 payout, ) = pool.burnSwap(
address(this), // payer (holds LP) address(this), // payer (holds LP)
address(this), // receiver address(this), // receiver
lpToBurn, // lpAmount lpToBurn, // lpAmount
@@ -506,7 +506,7 @@ contract NativeTest is Test {
uint256 bobEthBefore = bob.balance; uint256 bobEthBefore = bob.balance;
// Burn LP and send native currency to bob // Burn LP and send native currency to bob
uint256 payout = pool.burnSwap( (uint256 payout, ) = pool.burnSwap(
address(this), // payer address(this), // payer
bob, // receiver bob, // receiver
lpToBurn, lpToBurn,
@@ -555,7 +555,7 @@ contract NativeTest is Test {
// 4. Burn LP to native currency // 4. Burn LP to native currency
uint256 lpToBurn = lpMinted / 2; uint256 lpToBurn = lpMinted / 2;
uint256 payout = pool.burnSwap(alice, alice, lpToBurn, 2, 0, true); (uint256 payout, ) = pool.burnSwap(alice, alice, lpToBurn, 2, 0, true);
assertTrue(payout > 0, "Should receive payout in native"); assertTrue(payout > 0, "Should receive payout in native");
// Alice should have some ETH back (maybe more or less depending on slippage) // Alice should have some ETH back (maybe more or less depending on slippage)

View File

@@ -784,7 +784,7 @@ contract PartyPoolTest is Test {
uint256 bobBefore = token0.balanceOf(bob); uint256 bobBefore = token0.balanceOf(bob);
// Call burnSwap where this contract is the payer (it holds initial LP from setUp) // Call burnSwap where this contract is the payer (it holds initial LP from setUp)
uint256 payout = pool.burnSwap(address(this), bob, lpToBurn, target, 0, false); (uint256 payout, ) = pool.burnSwap(address(this), bob, lpToBurn, target, 0, false);
// Payout must be > 0 // Payout must be > 0
assertTrue(payout > 0, "burnSwap should produce a payout"); assertTrue(payout > 0, "burnSwap should produce a payout");