Compare commits

...

7 Commits

6 changed files with 193 additions and 46 deletions

View File

@@ -0,0 +1,4 @@
cast call 0x8f98B899F4135408Fe03228cE942Ad6BF8E40f22 \
"working(address)" \
0x3EDE1eE859A72aEc85ff04d305B6Ffe89f2Cb4eb \
--rpc-url https://eth-sepolia.g.alchemy.com/v2/demo

View File

@@ -139,13 +139,13 @@ const TEST_TOKENS = currentConfig.tokens;
// Default pool parameters
const DEFAULT_POOL_PARAMS = {
name: 'Liquidity Party POC',
symbol: 'POC.LP',
name: 'Original Genesis of Liquidity Party',
symbol: 'OG.LP',
kappa: ethers.BigNumber.from('184467440737095520'), //0.01 * 2^64
swapFeesPpm: Object.values(TEST_TOKENS).map(t => t.feePpm),
flashFeePpm: 5, // 0.0005%
stable: false,
initialLpAmount: ethers.utils.parseUnits('100', 18) // 100 USD in 18 decimals
initialLpAmount: ethers.utils.parseUnits('1', 18) // 100 USD in 18 decimals
};
// Input amount in USD
@@ -289,13 +289,13 @@ async function approveTokens(provider, tokenAmounts, receiverPrivateKey) {
console.log(` [~] Approving ${symbol} ${tokenInfo.address} ${approvalAmount} (1% buffer)...`);
try {
// // USDT and some tokens require setting allowance to 0 before setting a new value
// // Skip for BNB as it has a broken approve function
// if (symbol !== 'BNB') {
// const resetTx = await tokenContract.approve(PARTY_PLANNER_ADDRESS, 0);
// await resetTx.wait();
// console.log(` [+] ${symbol} allowance reset to 0`);
// }
// USDT and some tokens require setting allowance to 0 before setting a new value
// Skip for BNB as it has a broken approve function
if (symbol == 'USDT') {
const resetTx = await tokenContract.approve(PARTY_PLANNER_ADDRESS, 0);
await resetTx.wait();
console.log(` [+] ${symbol} allowance reset to 0`);
}
const tx = await tokenContract.approve(PARTY_PLANNER_ADDRESS, approvalAmount);
await tx.wait();
@@ -445,14 +445,14 @@ async function main() {
// Step 4: Check balances
await checkBalances(provider, wallet, tokenAmounts, RECEIVER_ADDRESS);
// // // Step 5: Approve tokens
// if (NETWORK === 'mockchain' && currentConfig.receiverPrivateKey) {
// // On mockchain, use receiver wallet for approvals
// await approveTokens(provider, tokenAmounts, currentConfig.receiverPrivateKey);
// } else if (NETWORK === 'mainnet') {
// // On mainnet, use the main wallet (payer and receiver are the same)
// await approveTokens(provider, tokenAmounts, PRIVATE_KEY);
// }
// // Step 5: Approve tokens
if (NETWORK === 'mockchain' && currentConfig.receiverPrivateKey) {
// On mockchain, use receiver wallet for approvals
await approveTokens(provider, tokenAmounts, currentConfig.receiverPrivateKey);
} else if (NETWORK === 'mainnet') {
// On mainnet, use the main wallet (payer and receiver are the same)
await approveTokens(provider, tokenAmounts, PRIVATE_KEY);
}
// Step 6: Create pool
await createPool(wallet, tokenAmounts);

View File

@@ -151,9 +151,14 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
const { burnSwapAmounts, loading: burnSwapLoading } = useBurnSwapAmounts(
mode === 'unstake' && !redeemAll ? selectedPool?.address : undefined,
mode === 'unstake' && !redeemAll ? maxAmountIn : undefined,
mode === 'unstake' && !redeemAll ? inputTokenIndex : undefined
mode === 'unstake' && !redeemAll ? inputTokenIndex : undefined,
undefined, // lpTokenPrice - not needed for burnSwap custom calculation
mode === 'unstake' && !redeemAll && selectedToken ? selectedToken.decimals : undefined
);
console.log('burn swap maounts', burnSwapAmounts)
// Fetch burn amounts (for unstake mode when redeeming all)
const { burnAmounts, loading: burnAmountsLoading } = useBurnAmounts(
mode === 'unstake' && redeemAll ? selectedPool?.address : undefined,

View File

@@ -73,6 +73,14 @@ export function SwapForm() {
}
}, [selectedFromToken, fromAmount]);
// Check if calculated slippage exceeds 5%
const slippageExceedsLimit = useMemo(() => {
if (!swapAmounts || swapAmounts.length === 0 || swapAmounts[0].calculatedSlippage === undefined) {
return false;
}
return Math.abs(swapAmounts[0].calculatedSlippage) > 5;
}, [swapAmounts]);
// Initialize swap hook
const { executeSwap, estimateGas, isSwapping, gasEstimate, isEstimatingGas } = useSwap();
@@ -301,6 +309,7 @@ export function SwapForm() {
onChange={(e) => setToAmount(e.target.value)}
className="text-2xl h-16"
disabled={!selectedFromToken}
readOnly
/>
<div className="relative min-w-[160px] space-y-1" ref={toDropdownRef}>
<Button
@@ -370,9 +379,20 @@ export function SwapForm() {
</div>
)}
{/* High slippage warning - show if calculated slippage exceeds max slippage OR 5% */}
{swapAmounts && swapAmounts.length > 0 && swapAmounts[0].calculatedSlippage !== undefined && (
Math.abs(swapAmounts[0].calculatedSlippage) > Math.max(currentSlippage, 5)
{/* Error message for slippage exceeding 5% */}
{slippageExceedsLimit && (
<div className="px-4 py-3 bg-destructive/10 border border-destructive/20 rounded-lg">
<p className="text-sm text-destructive font-medium"> Slippage Exceeds 5%</p>
<p className="text-xs text-destructive/80 mt-1">
The estimated slippage for this swap is {Math.abs(swapAmounts![0].calculatedSlippage!).toFixed(2)}%.
We cannot process this swap as you may lose too much money due to the high slippage.
</p>
</div>
)}
{/* High slippage warning - show if calculated slippage exceeds max slippage but is under 5% */}
{!slippageExceedsLimit && swapAmounts && swapAmounts.length > 0 && swapAmounts[0].calculatedSlippage !== undefined && (
Math.abs(swapAmounts[0].calculatedSlippage) > currentSlippage
) && (
<div className="px-4 py-3 bg-yellow-500/10 border border-yellow-500/20 rounded-lg">
<p className="text-sm text-yellow-600 dark:text-yellow-500 font-medium"> High Slippage Warning</p>
@@ -420,12 +440,14 @@ export function SwapForm() {
<Button
className="w-full h-14 text-lg"
onClick={() => setIsReviewModalOpen(true)}
disabled={!isConnected || !fromAmount || !toAmount || !!poolsError || hasInsufficientBalance}
disabled={!isConnected || !fromAmount || !toAmount || !!poolsError || hasInsufficientBalance || slippageExceedsLimit}
>
{!isConnected
? t('swap.connectWalletToSwap')
: hasInsufficientBalance
? 'Insufficient Balance'
: slippageExceedsLimit
? 'Slippage Too High'
: 'Review'}
</Button>
</CardContent>

View File

@@ -8,6 +8,7 @@ import IPartyPoolABI from '@/contracts/IPartyPoolABI';
import IPartyPoolViewerABI from '@/contracts/IPartyPoolViewerABI';
import IPartyInfoABI from '@/contracts/IPartyInfoABI';
import { ERC20ABI } from '@/contracts/ERC20ABI';
import { calculateSlippage } from './usePartyPool';
// Helper function to format large numbers with K, M, B suffixes
function formatTVL(value: number): string {
@@ -50,7 +51,7 @@ export function useGetAllTokens(offset: number = 0, limit: number = 100) {
// Get chain ID and contract address
const chainId = await publicClient.getChainId();
// @ts-ignore
const address = (chainInfo as Record<string, { v1: { PartyPlanner: string; PartyPoolViewer: string } }>)[chainId.toString()]?.v1?.PartyPlanner;
const address = (chainInfo as Record<string, { v1: { PartyPlanner: string } }>)[chainId.toString()]?.v1?.PartyPlanner;
if (!address) {
setError('IPartyPlanner contract not found for current chain');
@@ -465,6 +466,7 @@ export function useGetAllPools(offset: number = 0, limit: number = 100) {
if (isWorking) {
// Fetch pool price (use first token as quote, index 0)
console.log('fetching pool price')
try {
const priceRaw = await publicClient.readContract({
address: partyInfoAddress as `0x${string}`,
@@ -565,17 +567,21 @@ export interface SwapMintAmounts {
amountInUsed: bigint;
fee: bigint;
lpMinted: bigint;
calculatedSlippage?: number; // Percentage, e.g. 5.5 means 5.5%
}
export interface BurnSwapAmounts {
amountOut: bigint;
outFee: bigint;
calculatedSlippage?: number; // Percentage, e.g. 5.5 means 5.5%
}
export function useSwapMintAmounts(
poolAddress: `0x${string}` | undefined,
inputTokenIndex: number | undefined,
maxAmountIn: bigint | undefined
maxAmountIn: bigint | undefined,
lpTokenPrice?: number, // Market price of the LP token in decimal format
inputTokenDecimals?: number // Decimals of the input token
) {
const publicClient = usePublicClient();
const [mounted, setMounted] = useState(false);
@@ -608,26 +614,68 @@ export function useSwapMintAmounts(
// Get chain ID and contract address
const chainId = await publicClient.getChainId();
// @ts-ignore
const viewerAddress = (chainInfo as Record<string, { v1: { PartyPlanner: string; PartyPoolViewer: string } }>)[chainId.toString()]?.v1?.PartyPoolViewer;
const partyInfoAddress = (chainInfo as Record<string, { v1: { PartyInfo: string } }>)[chainId.toString()]?.v1?.PartyInfo;
if (!viewerAddress) {
setError('IPartyPoolViewer contract not found for current chain');
if (!partyInfoAddress) {
setError('PartyInfo contract not found for current chain');
setSwapMintAmounts(null);
return;
}
// Call swapMintAmounts function
const result = await publicClient.readContract({
address: viewerAddress as `0x${string}`,
address: partyInfoAddress as `0x${string}`,
abi: IPartyPoolViewerABI,
functionName: 'swapMintAmounts',
args: [poolAddress, BigInt(inputTokenIndex), maxAmountIn],
}) as readonly [bigint, bigint, bigint];
// Fetch the market price if not provided
let marketPrice = lpTokenPrice;
if (marketPrice === undefined) {
try {
console.log('input token index', inputTokenIndex)
// Get the pool price (price of the pool in terms of the input token)
const poolPriceInt128 = await publicClient.readContract({
address: partyInfoAddress as `0x${string}`,
abi: IPartyInfoABI,
functionName: 'poolPrice',
args: [poolAddress, BigInt(inputTokenIndex)],
}) as bigint;
// Convert Q64 format to decimal (price = priceValue / 2^64)
// poolPrice returns how much 1 LP token is worth in terms of the input token
marketPrice = Number(poolPriceInt128) / 2 ** 64;
console.log('swapMintAmounts fetched marketPrice (poolPrice)', marketPrice);
} catch (priceErr) {
console.error('Error fetching poolPrice:', priceErr);
}
}
// Calculate slippage if market price is available
let calculatedSlippage: number | undefined;
if (marketPrice !== undefined) {
try {
// For swapMint:
// - marketPrice: how much 1 LP token is worth in input tokens (from poolPrice)
// - swapOutputAmount: LP tokens minted (result[2])
// - swapInputAmount: input token amount used (maxAmountIn)
// - inFee: fee charged (result[1])
console.log('LP minted', result[1])
// Convert result[1] to token price using poolPrice: result[1] * poolPrice
const lpMintedInTokenPrice = BigInt(Math.floor(Number(result[1]) * marketPrice));
calculatedSlippage = calculateSlippage(marketPrice, lpMintedInTokenPrice, result[0], result[2]);
console.log('🎯 swapMint calculatedSlippage: ' + calculatedSlippage.toFixed(4) + '%');
} catch (slippageErr) {
console.error(`Error calculating slippage for swapMint:`, slippageErr);
}
}
setSwapMintAmounts({
amountInUsed: result[0],
fee: result[1],
lpMinted: result[2],
fee: result[2],
lpMinted: result[1],
calculatedSlippage,
});
} catch (err) {
console.error('Error calling swapMintAmounts:', err);
@@ -639,7 +687,7 @@ export function useSwapMintAmounts(
};
fetchSwapMintAmounts();
}, [publicClient, mounted, poolAddress, inputTokenIndex, maxAmountIn]);
}, [publicClient, mounted, poolAddress, inputTokenIndex, maxAmountIn, lpTokenPrice]);
return {
swapMintAmounts,
@@ -652,7 +700,9 @@ export function useSwapMintAmounts(
export function useBurnSwapAmounts(
poolAddress: `0x${string}` | undefined,
lpAmount: bigint | undefined,
inputTokenIndex: number | undefined
inputTokenIndex: number | undefined,
lpTokenPrice?: number, // Market price of the LP token in decimal format
tokenDecimals?: number // Decimals of the output token
) {
const publicClient = usePublicClient();
const [mounted, setMounted] = useState(false);
@@ -681,18 +731,21 @@ export function useBurnSwapAmounts(
try {
setLoading(true);
setError(null);
console.log('fetching swap amounts')
// Get chain ID and contract address
const chainId = await publicClient.getChainId();
// @ts-ignore
const viewerAddress = (chainInfo as Record<string, { v1: { PartyPlanner: string; PartyPoolViewer: string } }>)[chainId.toString()]?.v1?.PartyPoolViewer;
const partyInfoAddress = (chainInfo as Record<string, { v1: { PartyInfo: string } }>)[chainId.toString()]?.v1?.PartyInfo;
if (!viewerAddress) {
setError('IPartyPoolViewer contract not found for current chain');
if (!partyInfoAddress) {
console.log('errores here')
setError('PartyInfo contract not found for current chain');
setBurnSwapAmounts(null);
return;
}
console.log('fetching swap amounts 2')
// Log inputs
console.log('🔍 burnSwapAmounts INPUTS:', {
chainId: chainId.toString(),
@@ -700,12 +753,12 @@ export function useBurnSwapAmounts(
poolAddress,
lpAmount: lpAmount.toString(),
inputTokenIndex,
viewerAddress,
partyInfoAddress,
});
// Call burnSwapAmounts function - returns [amountOut, outFee]
const result = await publicClient.readContract({
address: viewerAddress as `0x${string}`,
address: partyInfoAddress as `0x${string}`,
abi: IPartyPoolViewerABI,
functionName: 'burnSwapAmounts',
args: [poolAddress, lpAmount, BigInt(inputTokenIndex)],
@@ -717,16 +770,59 @@ export function useBurnSwapAmounts(
amountOut: result[0].toString(),
outFee: result[1].toString(),
});
// Calculate slippage for burnSwap using poolPrice
let calculatedSlippage: number | undefined;
if (tokenDecimals !== undefined) {
try {
// Get the market price from poolPrice (quoteTokenIndex = 0)
const poolPriceInt128 = await publicClient.readContract({
address: partyInfoAddress as `0x${string}`,
abi: IPartyInfoABI,
functionName: 'poolPrice',
args: [poolAddress, BigInt(inputTokenIndex)],
}) as bigint;
// Convert Q64 format to decimal (price = priceValue / 2^64)
const marketPrice = Number(poolPriceInt128) / 2 ** 64;
// For burnSwap: swapPrice = (outAmount + fee) / lpAmount
// Need to apply decimal corrections: outAmount and fee are in token decimals, lpAmount is in 18 decimals
const outAmountDecimal = Number(result[0]) / Math.pow(10, tokenDecimals);
const feeDecimal = Number(result[1]) / Math.pow(10, tokenDecimals);
const lpAmountDecimal = Number(lpAmount) / Math.pow(10, 18); // LP tokens have 18 decimals
const swapPrice = (outAmountDecimal + feeDecimal) / lpAmountDecimal;
// Calculate slippage percentage: ((swapPrice - marketPrice) / marketPrice) * 100
calculatedSlippage = ((swapPrice - marketPrice) / marketPrice) * 100;
console.log('burnSwap slippage calculation:', {
marketPrice,
swapPrice,
calculatedSlippage,
outAmountDecimal,
feeDecimal,
lpAmountDecimal,
outAmount: result[0].toString(),
fee: result[1].toString(),
lpAmount: lpAmount.toString(),
});
} catch (slippageErr) {
console.error(`Error calculating slippage for burnSwap:`, slippageErr);
}
}
const parsedAmounts = {
amountOut: result[0],
outFee: result[1],
calculatedSlippage,
};
// Log parsed result
console.log('✅ burnSwapAmounts PARSED:', {
amountOut: parsedAmounts.amountOut.toString(),
outFee: parsedAmounts.outFee.toString(),
calculatedSlippage: parsedAmounts.calculatedSlippage,
});
setBurnSwapAmounts(parsedAmounts);
@@ -740,7 +836,7 @@ export function useBurnSwapAmounts(
};
fetchBurnSwapAmounts();
}, [publicClient, mounted, poolAddress, lpAmount, inputTokenIndex]);
}, [publicClient, mounted, poolAddress, lpAmount, inputTokenIndex, lpTokenPrice, tokenDecimals]);
return {
burnSwapAmounts,

View File

@@ -11,6 +11,29 @@ import type { AvailableToken } from './usePartyPlanner';
// Q96 constant for price calculations
const Q96 = 1n << 96n;
/**
* Calculate slippage percentage based on market price and actual swap execution price
* @param marketPrice The current market price from the pool (in Q64 format, already converted to decimal)
* @param swapOutputAmount The output amount from the swap
* @param swapInputAmount The input amount for the swap
* @param swapFee The fee charged for the swap
* @returns Slippage as a percentage (e.g., 5.5 means 5.5%)
*/
export function calculateSlippage(
marketPrice: number,
swapOutputAmount: bigint,
swapInputAmount: bigint,
swapFee: bigint
): number {
// Calculate actual swap price with decimal correction
const swapPrice = Number(swapOutputAmount) / (Number(swapInputAmount) - Number(swapFee));
// Calculate slippage percentage: ((swapPrice - marketPrice) / marketPrice) * 100
const slippage = ((marketPrice - swapPrice) / marketPrice) * 100;
return slippage;
}
export interface SwapAmountResult {
tokenAddress: `0x${string}`;
tokenSymbol: string;
@@ -172,13 +195,10 @@ export function useSwapAmounts(
}) as bigint;
// Convert Q64 format to decimal (price = priceValue / 2^64)
const price = Number(priceInt128) / 2 ** 64;
const marketPrice = Number(priceInt128) / 2 ** 64;
// Calculate actual swap price with decimal correction
const swapPrice = Number(swapOutputAmount) / ((Number(swapInputAmount)) - Number(inFee));
// Calculate slippage: 1 - (actualPrice / marketPrice)
const slippage = 1 - (swapPrice / price);
calculatedSlippage = slippage * 100; // Convert to percentage
// Calculate slippage using the reusable function
calculatedSlippage = calculateSlippage(marketPrice, swapOutputAmount, swapInputAmount, inFee);
console.log('calculatedSlippage', calculatedSlippage)
} catch (slippageErr) {
console.error(`Error calculating slippage for ${token.symbol}:`, slippageErr);