Skip to content
Merged
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
2 changes: 1 addition & 1 deletion typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"publishConfig": {
"access": "public"
},
"version": "0.0.40",
"version": "0.0.41",
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
Expand Down
42 changes: 33 additions & 9 deletions typescript/src/reClamm/reClammPool.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import { MathSol } from '../utils/math';
import {
MaxSwapParams,
type PoolBase,
SwapKind,
type SwapParams,
} from '../vault/types';
import { toRawUndoRateRoundDown } from '../vault/utils';
import { ReClammMutable } from './reClammData';
import {
computeCurrentVirtualBalances,
computeInGivenOut,
computeOutGivenIn,
} from './reClammMath';

// Apply a 99% safety margin to the token out balance, consistent with other pool types, to avoid
// edge-case reverts caused by rounding/curve constraints when swapping at the theoretical max.
const _MAX_TOKEN_OUT_RATIO = 990000000000000000n;

export class ReClamm implements PoolBase {
public reClammState: ReClammMutable;
constructor(reClammState: ReClammMutable) {
Expand All @@ -35,8 +41,20 @@ export class ReClamm implements PoolBase {
* @returns GivenIn: Returns the max amount in. GivenOut: Returns the max amount out.
*/
getMaxSwapAmount(maxSwapParams: MaxSwapParams): bigint {
const { balancesLiveScaled18, indexIn, indexOut, swapKind } =
maxSwapParams;
const {
balancesLiveScaled18,
indexIn,
indexOut,
tokenRates,
scalingFactors,
swapKind,
} = maxSwapParams;

// 99% of token out balance
const maxAmountOutScaled18 = MathSol.mulDownFixed(
_MAX_TOKEN_OUT_RATIO,
balancesLiveScaled18[indexOut],
);

if (swapKind === SwapKind.GivenIn) {
const computeResult =
Expand All @@ -47,15 +65,21 @@ export class ReClamm implements PoolBase {
computeResult.currentVirtualBalanceB,
indexIn,
indexOut,
balancesLiveScaled18[indexOut],
maxAmountOutScaled18,
);
// Scale to token in (and remove rate)
return toRawUndoRateRoundDown(
maxAmountIn,
scalingFactors[indexIn],
tokenRates[indexIn],
);
const maxAmountInWithTolerance = maxAmountIn - 10n; // 10 is a tolerance for rounding
return maxAmountInWithTolerance < 0n
? 0n
: maxAmountInWithTolerance;
}
const maxAmountOutWithTolerance = balancesLiveScaled18[indexOut] - 10n; // 10 is a tolerance for rounding
return maxAmountOutWithTolerance < 0n ? 0n : maxAmountOutWithTolerance;
// Scale to token out (and remove rate)
return toRawUndoRateRoundDown(
maxAmountOutScaled18,
scalingFactors[indexOut],
tokenRates[indexOut],
);
}

getMaxSingleTokenAddAmount(): bigint {
Expand Down
42 changes: 33 additions & 9 deletions typescript/src/reClammV2/reClammV2Pool.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import { MathSol } from '../utils/math';
import {
MaxSwapParams,
type PoolBase,
SwapKind,
type SwapParams,
} from '../vault/types';
import { toRawUndoRateRoundDown } from '../vault/utils';
import { ReClammV2Mutable } from './reClammV2Data';
import {
computeCurrentVirtualBalances,
computeInGivenOut,
computeOutGivenIn,
} from './reClammV2Math';

// Apply a 99% safety margin to the token out balance, consistent with other pool types, to avoid
// edge-case reverts caused by rounding/curve constraints when swapping at the theoretical max.
const _MAX_TOKEN_OUT_RATIO = 990000000000000000n;

export class ReClammV2 implements PoolBase {
public reClammState: ReClammV2Mutable;
constructor(reClammState: ReClammV2Mutable) {
Expand All @@ -35,8 +41,20 @@ export class ReClammV2 implements PoolBase {
* @returns GivenIn: Returns the max amount in. GivenOut: Returns the max amount out.
*/
getMaxSwapAmount(maxSwapParams: MaxSwapParams): bigint {
const { balancesLiveScaled18, indexIn, indexOut, swapKind } =
maxSwapParams;
const {
balancesLiveScaled18,
indexIn,
indexOut,
tokenRates,
scalingFactors,
swapKind,
} = maxSwapParams;

// 99% of token out balance
const maxAmountOutScaled18 = MathSol.mulDownFixed(
_MAX_TOKEN_OUT_RATIO,
balancesLiveScaled18[indexOut],
);

if (swapKind === SwapKind.GivenIn) {
const computeResult =
Expand All @@ -47,15 +65,21 @@ export class ReClammV2 implements PoolBase {
computeResult.currentVirtualBalanceB,
indexIn,
indexOut,
balancesLiveScaled18[indexOut],
maxAmountOutScaled18,
);
// Scale to token in (and remove rate)
return toRawUndoRateRoundDown(
maxAmountIn,
scalingFactors[indexIn],
tokenRates[indexIn],
);
const maxAmountInWithTolerance = maxAmountIn - 10n; // 10 is a tolerance for rounding
return maxAmountInWithTolerance < 0n
? 0n
: maxAmountInWithTolerance;
}
const maxAmountOutWithTolerance = balancesLiveScaled18[indexOut] - 10n; // 10 is a tolerance for rounding
return maxAmountOutWithTolerance < 0n ? 0n : maxAmountOutWithTolerance;
// Scale to token out (and remove rate)
return toRawUndoRateRoundDown(
maxAmountOutScaled18,
scalingFactors[indexOut],
tokenRates[indexOut],
);
}

getMaxSingleTokenAddAmount(): bigint {
Expand Down
110 changes: 106 additions & 4 deletions typescript/test/reClammPool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { describe, expect, test } from 'vitest';
import { SwapKind } from '../src/index';
import { ReClamm } from '../src/reClamm';
import { toScaled18ApplyRateRoundDown } from '../src/vault/utils';

describe('reClamm pool', () => {
const pool = new ReClamm({
Expand Down Expand Up @@ -29,8 +30,15 @@ describe('reClamm pool', () => {
indexIn: 0,
indexOut: 1,
};
// getMaxSwapAmount returns a raw amount, so it must be scaled back to 18 decimals
// (applying the rate) before being used as `amountGivenScaled18` in onSwap.
const maxSwapAmount = pool.getMaxSwapAmount(swapParams);
const sp = { ...swapParams, amountGivenScaled18: maxSwapAmount };
const amountGivenScaled18 = toScaled18ApplyRateRoundDown(
maxSwapAmount,
swapParams.scalingFactors[swapParams.indexIn],
swapParams.tokenRates[swapParams.indexIn],
);
const sp = { ...swapParams, amountGivenScaled18 };
expect(() => pool.onSwap(sp)).not.toThrow();
});
test('exact out, 6 decimals', () => {
Expand All @@ -47,7 +55,12 @@ describe('reClamm pool', () => {
indexOut: 1,
};
const maxSwapAmount = pool.getMaxSwapAmount(swapParams);
const sp = { ...swapParams, amountGivenScaled18: maxSwapAmount };
const amountGivenScaled18 = toScaled18ApplyRateRoundDown(
maxSwapAmount,
swapParams.scalingFactors[swapParams.indexOut],
swapParams.tokenRates[swapParams.indexOut],
);
const sp = { ...swapParams, amountGivenScaled18 };
expect(() => pool.onSwap(sp)).not.toThrow();
});
test('exact in, 6 decimals', () => {
Expand All @@ -64,7 +77,12 @@ describe('reClamm pool', () => {
indexOut: 0,
};
const maxSwapAmount = pool.getMaxSwapAmount(swapParams);
const sp = { ...swapParams, amountGivenScaled18: maxSwapAmount };
const amountGivenScaled18 = toScaled18ApplyRateRoundDown(
maxSwapAmount,
swapParams.scalingFactors[swapParams.indexIn],
swapParams.tokenRates[swapParams.indexIn],
);
const sp = { ...swapParams, amountGivenScaled18 };
expect(() => pool.onSwap(sp)).not.toThrow();
});
test('exact out, 18 decimals', () => {
Expand All @@ -81,8 +99,92 @@ describe('reClamm pool', () => {
indexOut: 0,
};
const maxSwapAmount = pool.getMaxSwapAmount(swapParams);
const sp = { ...swapParams, amountGivenScaled18: maxSwapAmount };
const amountGivenScaled18 = toScaled18ApplyRateRoundDown(
maxSwapAmount,
swapParams.scalingFactors[swapParams.indexOut],
swapParams.tokenRates[swapParams.indexOut],
);
const sp = { ...swapParams, amountGivenScaled18 };
expect(() => pool.onSwap(sp)).not.toThrow();
});
describe('with rate', () => {
test('exact in, returns raw amount (not scaled18)', () => {
const swapParams = {
swapKind: SwapKind.GivenIn,
amountGivenScaled18: 0n,
balancesLiveScaled18: [
14552907646299798n,
174459788000000000000n,
],
// indexOut token has a 1.5 rate, so balancesLiveScaled18 != raw balances
tokenRates: [1000000000000000000n, 1500000000000000000n],
scalingFactors: [1n, 1n],
indexIn: 0,
indexOut: 1,
};
const maxSwapAmount = pool.getMaxSwapAmount(swapParams);

// The result must be a raw amount: scaling it back up by the indexIn token's
// rate/scalingFactor should reproduce a valid scaled18 swap input.
const amountGivenScaled18 = toScaled18ApplyRateRoundDown(
maxSwapAmount,
swapParams.scalingFactors[swapParams.indexIn],
swapParams.tokenRates[swapParams.indexIn],
);
const sp = { ...swapParams, amountGivenScaled18 };
expect(() => pool.onSwap(sp)).not.toThrow();

// Sanity check: with a rate of 1 on indexIn, raw == scaled18 here, so the max
// amount in must be well below the full indexOut balance (since only 99% of it
// is targeted, and the rate on indexOut is > 1).
expect(maxSwapAmount).toBeGreaterThan(0n);
expect(maxSwapAmount).toBeLessThan(
swapParams.balancesLiveScaled18[swapParams.indexOut],
);
});
test('exact out, returns raw amount (not scaled18)', () => {
const swapParams = {
swapKind: SwapKind.GivenOut,
amountGivenScaled18: 0n,
balancesLiveScaled18: [
14552907646299798n,
174459788000000000000n,
],
// indexOut token has a 1.5 rate, so balancesLiveScaled18 != raw balance
tokenRates: [1000000000000000000n, 1500000000000000000n],
scalingFactors: [1n, 1n],
indexIn: 0,
indexOut: 1,
};
const maxSwapAmount = pool.getMaxSwapAmount(swapParams);

// Expected raw amount: 99% of balancesLiveScaled18[indexOut], converted back to
// raw by removing the indexOut token's rate.
const expectedMaxAmountOutScaled18 =
(990000000000000000n *
swapParams.balancesLiveScaled18[
swapParams.indexOut
]) /
1000000000000000000n;
const expectedRaw =
(expectedMaxAmountOutScaled18 * 1000000000000000000n) /
swapParams.tokenRates[swapParams.indexOut];
expect(maxSwapAmount).toEqual(expectedRaw);

// It must be strictly less than balancesLiveScaled18[indexOut], proving the
// rate has been removed (previously this returned the scaled18 value directly).
expect(maxSwapAmount).toBeLessThan(
swapParams.balancesLiveScaled18[swapParams.indexOut],
);

const amountGivenScaled18 = toScaled18ApplyRateRoundDown(
maxSwapAmount,
swapParams.scalingFactors[swapParams.indexOut],
swapParams.tokenRates[swapParams.indexOut],
);
const sp = { ...swapParams, amountGivenScaled18 };
expect(() => pool.onSwap(sp)).not.toThrow();
});
});
});
});
Loading