Skip to content

Commit ea87558

Browse files
committed
fix: damm v2 zap in to single sided pool
1 parent 064c58b commit ea87558

File tree

4 files changed

+194
-12
lines changed

4 files changed

+194
-12
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2121

2222
### Breaking Changes
2323

24+
## zap [0.2.2] [PR #46](https://github.com/MeteoraAg/zap-program/pull/46)
25+
26+
### Fixed
27+
28+
- Fix `zap_in_damm_v2` endpoint when the damm-v2 pool is single-sided, meaning it only has token A or only has token B
29+
2430
## zap [0.2.1] [PR #41](https://github.com/MeteoraAg/zap-program/pull/41)
2531

2632
### Fixed

programs/zap/src/utils/damm_v2_utils.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,10 @@ pub fn get_liquidity_from_amount_a(
459459
sqrt_max_price: u128,
460460
sqrt_price: u128,
461461
) -> Result<u128> {
462+
if sqrt_price == sqrt_max_price {
463+
// Single-sided B position: no token A needed, return max so A is always surplus
464+
return Ok(u128::MAX);
465+
}
462466
let price_delta = U512::from(sqrt_max_price.safe_sub(sqrt_price)?);
463467
let prod = U512::from(amount_a)
464468
.safe_mul(U512::from(sqrt_price))?
@@ -473,6 +477,10 @@ pub fn get_liquidity_from_amount_b(
473477
sqrt_min_price: u128,
474478
sqrt_price: u128,
475479
) -> Result<u128> {
480+
if sqrt_price == sqrt_min_price {
481+
// Single-sided A position: no token B needed, return max so B is always surplus
482+
return Ok(u128::MAX);
483+
}
476484
let price_delta = U256::from(sqrt_price.safe_sub(sqrt_min_price)?);
477485
let quote_amount = U256::from(amount_b).safe_shl(128)?;
478486
let liquidity = quote_amount.safe_div(price_delta)?; // round down

tests/common/damm_v2.ts

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -169,10 +169,15 @@ export async function createDammV2Pool(
169169
tokenBMint: PublicKey,
170170
amountA?: BN,
171171
amountB?: BN,
172-
baseFeeParams?: Buffer
172+
baseFeeParams?: Buffer,
173+
customInitSqrtPrice?: BN
173174
): Promise<PublicKey> {
174175
const program = createDammV2Program();
175176

177+
const sqrtMinPrice = MIN_SQRT_PRICE;
178+
const sqrtMaxPrice = MAX_SQRT_PRICE;
179+
const sqrtPrice = customInitSqrtPrice ?? INIT_PRICE;
180+
176181
const poolAuthority = deriveDammV2PoolAuthority();
177182
const pool = deriveDammV2CustomizablePoolAddress(tokenAMint, tokenBMint);
178183

@@ -205,17 +210,29 @@ export async function createDammV2Pool(
205210
if (amountA && amountB) {
206211
const liquidityFromA = getLiquidityDeltaFromAmountA(
207212
amountA,
208-
INIT_PRICE,
209-
MAX_SQRT_PRICE
213+
sqrtPrice,
214+
sqrtMaxPrice
210215
);
211-
212216
const liquidityFromB = getLiquidityDeltaFromAmountB(
213217
amountB,
214-
MIN_SQRT_PRICE,
215-
INIT_PRICE
218+
sqrtMinPrice,
219+
sqrtPrice
216220
);
217-
218221
liquidityDelta = BN.min(liquidityFromA, liquidityFromB);
222+
} else if (amountA) {
223+
// one sided pool A
224+
liquidityDelta = getLiquidityDeltaFromAmountA(
225+
amountA,
226+
sqrtPrice,
227+
sqrtMaxPrice
228+
);
229+
} else if (amountB) {
230+
// one sided pool B
231+
liquidityDelta = getLiquidityDeltaFromAmountB(
232+
amountB,
233+
sqrtMinPrice,
234+
sqrtPrice
235+
);
219236
}
220237

221238
const baseFee = {
@@ -237,11 +254,11 @@ export async function createDammV2Pool(
237254
baseFee,
238255
dynamicFee: null,
239256
},
240-
sqrtMinPrice: MIN_SQRT_PRICE,
241-
sqrtMaxPrice: MAX_SQRT_PRICE,
257+
sqrtMinPrice,
258+
sqrtMaxPrice,
242259
hasAlphaVault: false,
243260
liquidity: liquidityDelta,
244-
sqrtPrice: INIT_PRICE,
261+
sqrtPrice,
245262
activationType: 0,
246263
collectFeeMode: 1,
247264
activationPoint: null,
@@ -280,8 +297,12 @@ export async function createDammV2Pool(
280297

281298
const vaultBBalance = Number(AccountLayout.decode(tokenBVaultData).amount);
282299

283-
expect(vaultABalance).greaterThan(0);
284-
expect(vaultBBalance).greaterThan(0);
300+
if (!sqrtPrice.eq(sqrtMaxPrice)) {
301+
expect(vaultABalance).greaterThan(0);
302+
}
303+
if (!sqrtPrice.eq(sqrtMinPrice)) {
304+
expect(vaultBBalance).greaterThan(0);
305+
}
285306

286307
return pool;
287308
}

tests/test_zapin/zapin_dammv2.test.ts

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import {
2424
TOKEN_DECIMALS,
2525
U64_MAX,
2626
U32_MAX,
27+
MIN_SQRT_PRICE,
28+
MAX_SQRT_PRICE,
2729
} from "../common";
2830

2931
import ZapIDL from "../../target/idl/zap.json";
@@ -409,6 +411,151 @@ describe("Zap In damm V2", () => {
409411
expect(result).instanceOf(TransactionMetadata);
410412
});
411413

414+
it("zap in when sqrt_price is at sqrt_min_price (single-sided A)", async () => {
415+
const pool = await createDammV2Pool(
416+
svm,
417+
admin,
418+
tokenAMint,
419+
tokenBMint,
420+
new BN(LAMPORTS_PER_SOL),
421+
undefined,
422+
undefined,
423+
MIN_SQRT_PRICE
424+
);
425+
426+
const poolState = getDammV2Pool(svm, pool);
427+
428+
expect(poolState.sqrtPrice.eq(MIN_SQRT_PRICE)).to.be.true;
429+
430+
const { position, positionNftAccount } = await createDammV2Position(
431+
svm,
432+
user,
433+
pool
434+
);
435+
436+
const initializeLedgerTx = await initializeLedgerAccount(user.publicKey);
437+
438+
const totalAmountA = new BN(LAMPORTS_PER_SOL / 2);
439+
const setLedgerBalanceTx = await setLedgerBalance(
440+
user.publicKey,
441+
totalAmountA,
442+
true
443+
);
444+
445+
const tokenBAccount = getAssociatedTokenAddressSync(
446+
tokenBMint,
447+
user.publicKey
448+
);
449+
const preTokenBBalance = getTokenBalance(svm, tokenBAccount);
450+
const updateLedgerBalanceAfterSwapTx = await updateLedgerBalanceAfterSwap(
451+
user.publicKey,
452+
tokenBAccount,
453+
preTokenBBalance,
454+
U64_MAX,
455+
false
456+
);
457+
458+
const zapInTx = await zapInDammv2({
459+
svm,
460+
user: user.publicKey,
461+
pool,
462+
position,
463+
positionNftAccount,
464+
preSqrtPrice: poolState.sqrtPrice,
465+
maxSqrtPriceChangeBps: U32_MAX.toNumber(),
466+
});
467+
468+
const closeLedgerTx = await closeLedgerAccount(user.publicKey);
469+
470+
const finalTx = new Transaction()
471+
.add(initializeLedgerTx)
472+
.add(setLedgerBalanceTx)
473+
.add(updateLedgerBalanceAfterSwapTx)
474+
.add(zapInTx)
475+
.add(closeLedgerTx);
476+
finalTx.recentBlockhash = svm.latestBlockhash();
477+
finalTx.sign(user);
478+
479+
const result = svm.sendTransaction(finalTx);
480+
if (result instanceof FailedTransactionMetadata) {
481+
console.log(result.meta().logs());
482+
}
483+
expect(result).instanceOf(TransactionMetadata);
484+
});
485+
486+
it("zap in when sqrt_price is at sqrt_max_price (single-sided B)", async () => {
487+
const pool = await createDammV2Pool(
488+
svm,
489+
admin,
490+
tokenAMint,
491+
tokenBMint,
492+
undefined,
493+
new BN(LAMPORTS_PER_SOL),
494+
undefined,
495+
MAX_SQRT_PRICE
496+
);
497+
498+
const poolState = getDammV2Pool(svm, pool);
499+
500+
expect(poolState.sqrtPrice.eq(MAX_SQRT_PRICE)).to.be.true;
501+
502+
const { position, positionNftAccount } = await createDammV2Position(
503+
svm,
504+
user,
505+
pool
506+
);
507+
508+
const initializeLedgerTx = await initializeLedgerAccount(user.publicKey);
509+
510+
const totalAmountB = new BN(LAMPORTS_PER_SOL / 2);
511+
const setLedgerBalanceTx = await setLedgerBalance(
512+
user.publicKey,
513+
totalAmountB,
514+
false
515+
);
516+
517+
const tokenAAccount = getAssociatedTokenAddressSync(
518+
tokenAMint,
519+
user.publicKey
520+
);
521+
const preTokenABalance = getTokenBalance(svm, tokenAAccount);
522+
const updateLedgerBalanceAfterSwapTx = await updateLedgerBalanceAfterSwap(
523+
user.publicKey,
524+
tokenAAccount,
525+
preTokenABalance,
526+
U64_MAX,
527+
true
528+
);
529+
530+
const zapInTx = await zapInDammv2({
531+
svm,
532+
user: user.publicKey,
533+
pool,
534+
position,
535+
positionNftAccount,
536+
preSqrtPrice: poolState.sqrtPrice,
537+
maxSqrtPriceChangeBps: U32_MAX.toNumber(),
538+
});
539+
540+
const closeLedgerTx = await closeLedgerAccount(user.publicKey);
541+
542+
const finalTx = new Transaction()
543+
.add(initializeLedgerTx)
544+
.add(setLedgerBalanceTx)
545+
.add(updateLedgerBalanceAfterSwapTx)
546+
.add(zapInTx)
547+
.add(closeLedgerTx);
548+
549+
finalTx.recentBlockhash = svm.latestBlockhash();
550+
finalTx.sign(user);
551+
552+
const result = svm.sendTransaction(finalTx);
553+
if (result instanceof FailedTransactionMetadata) {
554+
console.log(result.meta().logs());
555+
}
556+
expect(result).instanceOf(TransactionMetadata);
557+
});
558+
412559
it("zap in without external swap with rate limiter and remaining accounts", async () => {
413560
const baseFee = encodeFeeRateLimiterParams(
414561
new BN(10_000_00), // 1% cliff fee

0 commit comments

Comments
 (0)