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
168 changes: 167 additions & 1 deletion src/chaincode/dex/createPool.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,15 @@ import { currency, fixture, users, writesMap } from "@gala-chain/test";
import BigNumber from "bignumber.js";
import { plainToInstance } from "class-transformer";

import { CreatePoolDto, CreatePoolResDto, DexFeeConfig, DexFeePercentageTypes, Pool } from "../../api/";
import {
CreatePoolDto,
CreatePoolResDto,
DexFeeConfig,
DexFeePercentageTypes,
Pool,
TickData
} from "../../api/";
import { tickToSqrtPrice } from "../../api/utils/dex/tick.helper";
import { DexV3Contract } from "../DexV3Contract";
import dexTestUtils from "../test/dex";
import { generateKeyFromClassKey } from "./dexUtils";
Expand Down Expand Up @@ -151,4 +159,162 @@ describe("createPool", () => {
expect(response).toEqual(GalaChainResponse.Success(expectedResponse));
expect(getWrites()).toEqual(writesMap(expectedFeeThresholdUses, expectedPool));
});

describe("sqrtPrice validation", () => {
it("should reject sqrtPrice = 0", async () => {
const currencyClass: TokenClass = currency.tokenClass();
const currencyClassKey: TokenClassKey = currency.tokenClassKey();

const dexClass: TokenClass = dexTestUtils.tokenClass();
const dexClassKey: TokenClassKey = dexTestUtils.tokenClassKey();

const dexFeeConfig: DexFeeConfig = new DexFeeConfig([asValidUserAlias(users.admin.identityKey)], 0.1);

const { ctx, contract } = fixture<GalaChainContext, DexV3Contract>(DexV3Contract)
.registeredUsers(users.testUser1)
.savedState(currencyClass, dexFeeConfig, dexClass);

const dto = new CreatePoolDto(
dexClassKey,
currencyClassKey,
DexFeePercentageTypes.FEE_1_PERCENT,
new BigNumber(0) // Zero sqrtPrice - should be rejected
);
dto.uniqueKey = "test-zero";
dto.sign(users.testUser1.privateKey);

// When
const response = await contract.CreatePool(ctx, dto);

// Then - should fail with validation error
expect(response.Status).toBe(0);
expect(response.Message).toContain("initialSqrtPrice must be greater than 0");
});

it("should reject sqrtPrice below MIN_SQRT_RATIO", async () => {
const currencyClass: TokenClass = currency.tokenClass();
const currencyClassKey: TokenClassKey = currency.tokenClassKey();

const dexClass: TokenClass = dexTestUtils.tokenClass();
const dexClassKey: TokenClassKey = dexTestUtils.tokenClassKey();

const dexFeeConfig: DexFeeConfig = new DexFeeConfig([asValidUserAlias(users.admin.identityKey)], 0.1);

const { ctx, contract } = fixture<GalaChainContext, DexV3Contract>(DexV3Contract)
.registeredUsers(users.testUser1)
.savedState(currencyClass, dexFeeConfig, dexClass);

const belowMinSqrtPrice = new BigNumber("1e-50"); // Way below MIN_SQRT_RATIO

const dto = new CreatePoolDto(
dexClassKey,
currencyClassKey,
DexFeePercentageTypes.FEE_1_PERCENT,
belowMinSqrtPrice
);
dto.uniqueKey = "test-below-min";
dto.sign(users.testUser1.privateKey);

// When
const response = await contract.CreatePool(ctx, dto);

// Then - should fail with validation error
expect(response.Status).toBe(0);
expect(response.Message).toContain("initialSqrtPrice must be between");
});

it("should reject sqrtPrice above MAX_SQRT_RATIO", async () => {
const currencyClass: TokenClass = currency.tokenClass();
const currencyClassKey: TokenClassKey = currency.tokenClassKey();

const dexClass: TokenClass = dexTestUtils.tokenClass();
const dexClassKey: TokenClassKey = dexTestUtils.tokenClassKey();

const dexFeeConfig: DexFeeConfig = new DexFeeConfig([asValidUserAlias(users.admin.identityKey)], 0.1);

const { ctx, contract } = fixture<GalaChainContext, DexV3Contract>(DexV3Contract)
.registeredUsers(users.testUser1)
.savedState(currencyClass, dexFeeConfig, dexClass);

const aboveMaxSqrtPrice = new BigNumber("1e50"); // Way above MAX_SQRT_RATIO

const dto = new CreatePoolDto(
dexClassKey,
currencyClassKey,
DexFeePercentageTypes.FEE_1_PERCENT,
aboveMaxSqrtPrice
);
dto.uniqueKey = "test-above-max";
dto.sign(users.testUser1.privateKey);

// When
const response = await contract.CreatePool(ctx, dto);

// Then - should fail with validation error
expect(response.Status).toBe(0);
expect(response.Message).toContain("initialSqrtPrice must be between");
});

it("should accept sqrtPrice at exactly MIN_SQRT_RATIO", async () => {
const currencyClass: TokenClass = currency.tokenClass();
const currencyClassKey: TokenClassKey = currency.tokenClassKey();

const dexClass: TokenClass = dexTestUtils.tokenClass();
const dexClassKey: TokenClassKey = dexTestUtils.tokenClassKey();

const dexFeeConfig: DexFeeConfig = new DexFeeConfig([asValidUserAlias(users.admin.identityKey)], 0.1);

const { ctx, contract } = fixture<GalaChainContext, DexV3Contract>(DexV3Contract)
.registeredUsers(users.testUser1)
.savedState(currencyClass, dexFeeConfig, dexClass);

const minSqrtPrice = tickToSqrtPrice(TickData.MIN_TICK);

const dto = new CreatePoolDto(
dexClassKey,
currencyClassKey,
DexFeePercentageTypes.FEE_1_PERCENT,
minSqrtPrice
);
dto.uniqueKey = "test-min";
dto.sign(users.testUser1.privateKey);

// When
const response = await contract.CreatePool(ctx, dto);

// Then
expect(response.Status).toBe(1); // Success
});

it("should accept sqrtPrice at exactly MAX_SQRT_RATIO", async () => {
const currencyClass: TokenClass = currency.tokenClass();
const currencyClassKey: TokenClassKey = currency.tokenClassKey();

const dexClass: TokenClass = dexTestUtils.tokenClass();
const dexClassKey: TokenClassKey = dexTestUtils.tokenClassKey();

const dexFeeConfig: DexFeeConfig = new DexFeeConfig([asValidUserAlias(users.admin.identityKey)], 0.1);

const { ctx, contract } = fixture<GalaChainContext, DexV3Contract>(DexV3Contract)
.registeredUsers(users.testUser1)
.savedState(currencyClass, dexFeeConfig, dexClass);

const maxSqrtPrice = tickToSqrtPrice(TickData.MAX_TICK);

const dto = new CreatePoolDto(
dexClassKey,
currencyClassKey,
DexFeePercentageTypes.FEE_0_05_PERCENT, // Different fee to avoid duplicate pool
maxSqrtPrice
);
dto.uniqueKey = "test-max";
dto.sign(users.testUser1.privateKey);

// When
const response = await contract.CreatePool(ctx, dto);

// Then
expect(response.Status).toBe(1); // Success
});
});
});
18 changes: 17 additions & 1 deletion src/chaincode/dex/createPool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
import { ConflictError, ValidationFailedError } from "@gala-chain/api";
import { GalaChainContext, fetchTokenClass, getObjectByKey, putChainObject } from "@gala-chain/chaincode";

import { CreatePoolDto, CreatePoolResDto, DexFeeConfig, Pool } from "../../api/";
import { CreatePoolDto, CreatePoolResDto, DexFeeConfig, Pool, TickData } from "../../api/";
import { tickToSqrtPrice } from "../../api/utils/dex/tick.helper";
import { generateKeyFromClassKey } from "./dexUtils";

/**
Expand All @@ -38,6 +39,21 @@ export async function createPool(ctx: GalaChainContext, dto: CreatePoolDto): Pro
);
}

// Validate sqrtPrice is greater than zero
if (dto.initialSqrtPrice.lte(0)) {
throw new ValidationFailedError("initialSqrtPrice must be greater than 0");
}

// Validate sqrtPrice is within valid tick range
const MIN_SQRT_RATIO = tickToSqrtPrice(TickData.MIN_TICK);
const MAX_SQRT_RATIO = tickToSqrtPrice(TickData.MAX_TICK);

if (dto.initialSqrtPrice.lt(MIN_SQRT_RATIO) || dto.initialSqrtPrice.gt(MAX_SQRT_RATIO)) {
throw new ValidationFailedError(
`initialSqrtPrice must be between ${MIN_SQRT_RATIO.toExponential(6)} and ${MAX_SQRT_RATIO.toExponential(6)}`
);
}

const key = ctx.stub.createCompositeKey(DexFeeConfig.INDEX_KEY, []);
let protocolFee = 0.1; // default
const protocolFeeConfig = await getObjectByKey(ctx, DexFeeConfig, key).catch(() => null);
Expand Down
76 changes: 76 additions & 0 deletions test/shared/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright (c) Gala Games Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { DexFeePercentageTypes } from "../../src/api";

/**
* Fee tiers available for pool creation
* Maps to DexFeePercentageTypes enum values
*/
export const FEE_TIERS = {
LOW: DexFeePercentageTypes.FEE_0_05_PERCENT, // 500 (0.05%)
MEDIUM: DexFeePercentageTypes.FEE_0_3_PERCENT, // 3000 (0.30%)
HIGH: DexFeePercentageTypes.FEE_1_PERCENT // 10000 (1.00%)
} as const;

/**
* Tick spacing for each fee tier
* Lower fee = tighter tick spacing = more precision
*/
export const TICK_SPACINGS: Record<number, number> = {
[FEE_TIERS.LOW]: 10,
[FEE_TIERS.MEDIUM]: 60,
[FEE_TIERS.HIGH]: 200
};

/**
* Minimum tick value (from Uniswap V3)
*/
export const MIN_TICK = -887272;

/**
* Maximum tick value (from Uniswap V3)
*/
export const MAX_TICK = 887272;

/**
* Approximate MIN_SQRT_RATIO (sqrt(1.0001^MIN_TICK))
* Calculated from tickToSqrtPrice(MIN_TICK) with full precision
*/
export const MIN_SQRT_RATIO = "5.4212146310449513864e-20";

/**
* Approximate MAX_SQRT_RATIO (sqrt(1.0001^MAX_TICK))
* Calculated from tickToSqrtPrice(MAX_TICK) with full precision
*/
export const MAX_SQRT_RATIO = "18446050711097703530";

/**
* Helper to get minimum usable tick for a given tick spacing
*/
export const getMinTick = (tickSpacing: number): number => Math.ceil(MIN_TICK / tickSpacing) * tickSpacing;

/**
* Helper to get maximum usable tick for a given tick spacing
*/
export const getMaxTick = (tickSpacing: number): number => Math.floor(MAX_TICK / tickSpacing) * tickSpacing;

/**
* Array of all fee tiers for parametric testing
*/
export const ALL_FEE_TIERS = [
{ name: "LOW", fee: FEE_TIERS.LOW, tickSpacing: TICK_SPACINGS[FEE_TIERS.LOW] },
{ name: "MEDIUM", fee: FEE_TIERS.MEDIUM, tickSpacing: TICK_SPACINGS[FEE_TIERS.MEDIUM] },
{ name: "HIGH", fee: FEE_TIERS.HIGH, tickSpacing: TICK_SPACINGS[FEE_TIERS.HIGH] }
];
Loading
Loading