204 lines
9.8 KiB
Solidity
204 lines
9.8 KiB
Solidity
// 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 "./PartyPoolBase.sol";
|
|
import "./LMSRStabilized.sol";
|
|
|
|
/// @title PartyPoolSwapMintImpl - Implementation contract for swapMint and burnSwap functions
|
|
/// @notice This contract contains the swapMint and burnSwap implementation that will be called via delegatecall
|
|
/// @dev This contract inherits from PartyPoolBase to access storage and internal functions
|
|
contract PartyPoolSwapMintImpl is PartyPoolBase {
|
|
using ABDKMath64x64 for int128;
|
|
using LMSRStabilized for LMSRStabilized.State;
|
|
using SafeERC20 for IERC20;
|
|
|
|
// Events that mirror the main contract events
|
|
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);
|
|
event Mint(address indexed payer, address indexed receiver, uint256[] depositAmounts, uint256 lpMinted);
|
|
event Burn(address indexed payer, address indexed receiver, uint256[] withdrawAmounts, uint256 lpBurned);
|
|
|
|
constructor() PartyPoolBase('','') {}
|
|
|
|
/// @notice Single-token mint: deposit a single token, charge swap-LMSR cost, and mint LP.
|
|
/// @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.
|
|
/// @param payer who transfers the input token
|
|
/// @param receiver who receives the minted LP tokens
|
|
/// @param inputTokenIndex index of the input token
|
|
/// @param maxAmountIn maximum uint token input (inclusive of fee)
|
|
/// @param deadline optional deadline
|
|
/// @param swapFeePpm fee in parts-per-million for this pool
|
|
/// @return lpMinted actual LP minted (uint)
|
|
function swapMint(
|
|
address payer,
|
|
address receiver,
|
|
uint256 inputTokenIndex,
|
|
uint256 maxAmountIn,
|
|
uint256 deadline,
|
|
uint256 swapFeePpm
|
|
) external returns (uint256 lpMinted) {
|
|
uint256 n = tokens.length;
|
|
require(inputTokenIndex < n, "swapMint: idx");
|
|
require(maxAmountIn > 0, "swapMint: input zero");
|
|
require(deadline == 0 || block.timestamp <= deadline, "swapMint: deadline");
|
|
|
|
// Ensure pool initialized
|
|
require(lmsr.nAssets > 0, "swapMint: uninit pool");
|
|
|
|
// compute fee on gross maxAmountIn to get an initial net estimate (we'll recompute based on actual used)
|
|
(, uint256 netUintGuess) = _computeFee(maxAmountIn, swapFeePpm);
|
|
|
|
// 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;
|
|
// Use natural ERC20 function since base contract inherits from ERC20
|
|
uint256 currentSupply = totalSupply();
|
|
if (currentSupply == 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 = (currentSupply * 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);
|
|
|
|
// Use natural ERC20 function since base contract inherits from ERC20
|
|
_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.
|
|
/// @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 receiver who receives the single asset
|
|
/// @param lpAmount amount of LP tokens to burn
|
|
/// @param inputTokenIndex index of target asset to receive
|
|
/// @param deadline optional deadline
|
|
/// @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
|
|
// todo fee!?
|
|
function burnSwap(
|
|
address payer,
|
|
address receiver,
|
|
uint256 lpAmount,
|
|
uint256 inputTokenIndex,
|
|
uint256 deadline,
|
|
uint256 swapFeePpm
|
|
) external returns (uint256 amountOutUint) {
|
|
uint256 n = tokens.length;
|
|
require(inputTokenIndex < n, "burnSwap: idx");
|
|
require(lpAmount > 0, "burnSwap: zero lp");
|
|
require(deadline == 0 || block.timestamp <= deadline, "burnSwap: deadline");
|
|
|
|
uint256 supply = totalSupply();
|
|
require(supply > 0, "burnSwap: empty supply");
|
|
require(balanceOf(payer) >= lpAmount, "burnSwap: insufficient LP");
|
|
|
|
// alpha = lpAmount / supply as Q64.64
|
|
int128 alpha = ABDKMath64x64.divu(lpAmount, supply);
|
|
|
|
// Use LMSR view to compute single-asset payout and burned size-metric
|
|
(int128 payoutInternal, ) = lmsr.swapAmountsForBurn(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) {
|
|
uint256 allowed = allowance(payer, msg.sender);
|
|
require(allowed >= lpAmount, "burnSwap: allowance insufficient");
|
|
_approve(payer, msg.sender, allowed - lpAmount);
|
|
}
|
|
_burn(payer, lpAmount);
|
|
|
|
// Update cached balances by reading on-chain balances for all tokens
|
|
int128[] memory newQInternal = new int128[](n);
|
|
for (uint256 idx = 0; idx < n; idx++) {
|
|
uint256 bal = IERC20(tokens[idx]).balanceOf(address(this));
|
|
cachedUintBalances[idx] = bal;
|
|
newQInternal[idx] = _uintToInternalFloor(bal, bases[idx]);
|
|
}
|
|
|
|
// Emit BurnSwap with public-facing info only (do not expose ΔS or LP burned)
|
|
emit BurnSwap(payer, receiver, 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;
|
|
}
|
|
}
|