Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/angry-ideas-heal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@venusprotocol/evm": patch
---

fix an issue where certain profitable positions can't be closed
Original file line number Diff line number Diff line change
Expand Up @@ -264,4 +264,64 @@ describe('getTradeReduceSwapQuotes', () => {
).toBe('10');
expect(result.pnlDsaTokens.toFixed()).toBe('3');
});

it('returns direct positive pnl in dsa tokens without a profit swap when the long token matches the dsa token', async () => {
(getSwapQuote as Mock)
.mockResolvedValueOnce({
swapQuote: makeApproximateOutSwapQuote({
fromToken: busd,
toToken: usdc,
fromTokenAmountSoldMantissa: 3_000_000_000_000_000_000n,
minimumToTokenAmountReceivedMantissa: 5_000_000n,
}),
})
.mockResolvedValueOnce({
swapQuote: makeExactInSwapQuote({
fromToken: busd,
toToken: usdc,
fromTokenAmountSoldMantissa: 10_000_000_000_000_000_000n,
minimumToTokenAmountReceivedMantissa: 5_000_000n,
}),
});

const result = await getTradeReduceSwapQuotes({
...sharedInput,
dsaToken: busd,
closeFractionPercentage: 50,
});

expect(getSwapQuote).toHaveBeenCalledTimes(2);
expect(result.pnlDsaTokens.toFixed()).toBe('7');
expect(result.profitSwapQuote).toBeUndefined();
});

it('returns direct negative dsa pnl without a loss swap when the short token matches the dsa token', async () => {
(getSwapQuote as Mock)
.mockResolvedValueOnce({
swapQuote: makeApproximateOutSwapQuote({
fromToken: busd,
toToken: usdc,
fromTokenAmountSoldMantissa: 10_000_000_000_000_000_000n,
minimumToTokenAmountReceivedMantissa: 5_000_000n,
}),
})
.mockResolvedValueOnce({
swapQuote: makeExactInSwapQuote({
fromToken: busd,
toToken: usdc,
fromTokenAmountSoldMantissa: 10_000_000_000_000_000_000n,
minimumToTokenAmountReceivedMantissa: 4_000_000n,
}),
});

const result = await getTradeReduceSwapQuotes({
...sharedInput,
dsaToken: usdc,
closeFractionPercentage: 50,
});

expect(getSwapQuote).toHaveBeenCalledTimes(2);
expect(result.pnlDsaTokens.toFixed()).toBe('-1');
expect(result.lossSwapQuote).toBeUndefined();
});
});
14 changes: 11 additions & 3 deletions apps/evm/src/clients/api/queries/getTradeReduceSwapQuotes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,11 +163,19 @@ export const getTradeReduceSwapQuotes = async ({
// Calculate actual PnL based on swaps
let pnlDsaTokens = new BigNumber(0);

// Closing/Reducing with profit
if (profitSwapQuote?.direction === 'exact-in') {
// Closing/Reducing with profit when long token = DSA token
if (areTokensEqual(longToken, dsaToken) && longProfitAmountDeltaTokens?.isGreaterThan(0)) {
pnlDsaTokens = longProfitAmountDeltaTokens;
}
// Closing/Reducing with loss when short token = DSA token
else if (areTokensEqual(shortToken, dsaToken) && shortLossAmountDeltaTokens?.isGreaterThan(0)) {
pnlDsaTokens = shortLossAmountDeltaTokens.multipliedBy(-1);
}
Comment thread
therealemjy marked this conversation as resolved.
// Closing/Reducing with profit when long token ≠ DSA token
else if (profitSwapQuote?.direction === 'exact-in') {
pnlDsaTokens = getSwapToTokenAmount(profitSwapQuote);
}
// Closing/Reducing with loss
// Closing/Reducing with loss when short token ≠ DSA token
else if (lossSwapQuote?.direction === 'approximate-out') {
pnlDsaTokens = convertMantissaToTokens({
value: lossSwapQuote.fromTokenAmountSoldMantissa,
Expand Down
2 changes: 1 addition & 1 deletion apps/evm/src/libs/errors/handleError/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BaseError } from 'viem';
import type { BaseError } from 'viem';

import { displayNotification } from 'libs/notifications';

Expand Down
29 changes: 15 additions & 14 deletions apps/evm/src/pages/Trade/ReduceForm/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,8 @@ export const ReduceForm: React.FC<ReduceFormProps> = ({ position, closePosition
}

const isReducingWithProfit = pnlDsaTokens?.isGreaterThan(0);
const isReducingWithLoss = pnlDsaTokens?.isLessThan(0);
const isReducingWithoutPnl = pnlDsaTokens?.isZero();

// Reduce with profit
if (
Expand Down Expand Up @@ -251,44 +253,43 @@ export const ReduceForm: React.FC<ReduceFormProps> = ({ position, closePosition
});
}

const isReducingWithLoss =
pnlDsaTokens?.isLessThan(0) &&
repayWithLossSwapQuote?.direction === 'exact-in' &&
lossSwapQuote?.direction === 'approximate-out';

const isReducingWithoutPnl =
pnlDsaTokens?.isZero() && repayWithLossSwapQuote?.direction === 'exact-in';

const repayShortAmountMantissa = convertTokensToMantissa({
token: position.shortAsset.vToken.underlyingToken,
value: debouncedShortAmountTokens,
});

const sanitizedLossSwapQuote =
lossSwapQuote?.direction === 'approximate-out' ? lossSwapQuote : undefined;

// Reduce with loss
if (isReducingWithLoss && !closePosition) {
if (isReducingWithLoss && !closePosition && repayWithLossSwapQuote?.direction === 'exact-in') {
return reducePositionWithLoss({
longVTokenAddress: position.longAsset.vToken.address,
shortVTokenAddress: position.shortAsset.vToken.address,
closeFractionPercentage,
repaySwapQuote: repayWithLossSwapQuote,
lossSwapQuote,
lossSwapQuote: sanitizedLossSwapQuote,
repayShortAmountMantissa,
});
}

// Close with loss
if (isReducingWithLoss && closePosition) {
if (isReducingWithLoss && closePosition && repayWithLossSwapQuote?.direction === 'exact-in') {
return closePositionWithLoss({
longVTokenAddress: position.longAsset.vToken.address,
shortVTokenAddress: position.shortAsset.vToken.address,
repaySwapQuote: repayWithLossSwapQuote,
lossSwapQuote,
lossSwapQuote: sanitizedLossSwapQuote,
repayShortAmountMantissa,
});
}

// Reduce without PnL
if (isReducingWithoutPnl && !closePosition) {
if (
isReducingWithoutPnl &&
!closePosition &&
repayWithLossSwapQuote?.direction === 'exact-in'
) {
return reducePositionWithLoss({
longVTokenAddress: position.longAsset.vToken.address,
shortVTokenAddress: position.shortAsset.vToken.address,
Expand All @@ -299,7 +300,7 @@ export const ReduceForm: React.FC<ReduceFormProps> = ({ position, closePosition
}

// Close without PnL
if (isReducingWithoutPnl && closePosition) {
if (isReducingWithoutPnl && closePosition && repayWithLossSwapQuote?.direction === 'exact-in') {
return closePositionWithLoss({
longVTokenAddress: position.longAsset.vToken.address,
shortVTokenAddress: position.shortAsset.vToken.address,
Expand Down
Loading