diff --git a/check-dex-status.sh b/check-dex-status.sh new file mode 100755 index 0000000..5db9613 --- /dev/null +++ b/check-dex-status.sh @@ -0,0 +1,89 @@ +#!/bin/bash +# GalaSwap DEX Status Check Script +# Usage: ./check-dex-status.sh + +echo "๐Ÿ” GALASWAP DEX STATUS CHECK" +echo "============================" +echo "Timestamp: $(date)" +echo "" + +# TODO: Replace these with your actual GalaChain connection details +CHANNEL_NAME="product-channel" +CHAINCODE_NAME="basic-product" +PEER_ADDRESS="peer0.your-org.com:7051" + +# Method 1: Using gala-cli (if available) +if command -v gala-cli &> /dev/null; then + echo "๐Ÿ“ก Checking DEX status via gala-cli..." + + # Try to call checkPaused - if it throws an error, DEX is paused + if gala-cli chaincode query \ + --channel-name "$CHANNEL_NAME" \ + --chaincode-name "$CHAINCODE_NAME" \ + --function-name "checkPaused" \ + --args "" \ + --peer-addresses "$PEER_ADDRESS" 2>/dev/null; then + + echo "โœ… DEX STATUS: OPERATIONAL" + echo "๐ŸŸข All trading functions are active" + else + echo "๐Ÿ”ด DEX STATUS: PAUSED" + echo "๐Ÿšซ All trading functions are blocked" + fi + +# Method 2: Using peer CLI (fallback) +elif command -v peer &> /dev/null; then + echo "๐Ÿ“ก Checking DEX status via peer CLI..." + + if peer chaincode query \ + -C "$CHANNEL_NAME" \ + -n "$CHAINCODE_NAME" \ + -c "{\"function\":\"checkPaused\",\"Args\":[]}" 2>/dev/null; then + + echo "โœ… DEX STATUS: OPERATIONAL" + echo "๐ŸŸข All trading functions are active" + else + echo "๐Ÿ”ด DEX STATUS: PAUSED" + echo "๐Ÿšซ All trading functions are blocked" + fi + +# Method 3: API call (if CLI tools not available) +else + echo "๐Ÿ“ก Checking DEX status via API..." + + # TODO: Replace with your actual API endpoint and auth + API_ENDPOINT="https://your-galachain-api.com/api/chaincode/query" + AUTH_TOKEN="your-admin-token" + + if curl -X POST "$API_ENDPOINT" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $AUTH_TOKEN" \ + -d "{ + \"chaincodeName\": \"$CHAINCODE_NAME\", + \"functionName\": \"checkPaused\", + \"args\": [] + }" \ + --fail \ + --silent \ + --show-error 2>/dev/null; then + + echo "โœ… DEX STATUS: OPERATIONAL" + echo "๐ŸŸข All trading functions are active" + else + echo "๐Ÿ”ด DEX STATUS: PAUSED" + echo "๐Ÿšซ All trading functions are blocked" + fi +fi + +echo "" +echo "๐Ÿ“‹ AVAILABLE COMMANDS:" +echo " ๐Ÿ›‘ Emergency pause: ./emergency-stop.sh" +echo " โ–ถ๏ธ Emergency resume: ./emergency-resume.sh" +echo " ๐Ÿ“Š Check status: ./check-dex-status.sh" +echo "" + +# Show recent emergency log entries +if [ -f "emergency.log" ]; then + echo "๐Ÿ“œ RECENT EMERGENCY ACTIONS:" + tail -5 emergency.log +fi diff --git a/emergency-resume.sh b/emergency-resume.sh new file mode 100755 index 0000000..0af90b6 --- /dev/null +++ b/emergency-resume.sh @@ -0,0 +1,130 @@ +#!/bin/bash +# GalaSwap DEX Emergency Resume Script +# Usage: ./emergency-resume.sh + +set -e # Exit on any error + +echo "๐Ÿ”„ GALASWAP DEX EMERGENCY RESUME INITIATED" +echo "==========================================" +echo "Timestamp: $(date)" +echo "Executed by: $(whoami)" +echo "Host: $(hostname)" +echo "" + +# Safety warnings +echo "โš ๏ธ CRITICAL SAFETY CHECKS:" +echo "๐Ÿ” Have you verified the security issue is resolved?" +echo "๐Ÿงช Have you tested the fix thoroughly?" +echo "๐Ÿ‘ฅ Has the security team approved resume?" +echo "๐Ÿ“‹ Have you documented the incident resolution?" +echo "" + +# Multiple confirmation prompts for safety +echo "๐Ÿšจ WARNING: This will resume ALL DEX operations!" +echo "Only proceed if you are absolutely certain it is safe." +echo "" +echo "Type 'RESUME' to confirm you want to resume operations:" +read CONFIRMATION + +if [ "$CONFIRMATION" != "RESUME" ]; then + echo "โŒ Resume cancelled - confirmation not received" + exit 1 +fi + +echo "" +echo "Final confirmation: Resume DEX operations now? (y/N)" +read FINAL_CONFIRMATION + +if [ "$FINAL_CONFIRMATION" != "y" ] && [ "$FINAL_CONFIRMATION" != "Y" ]; then + echo "โŒ Resume cancelled by user" + exit 1 +fi + +echo "" +echo "๐Ÿ”„ Executing emergency resume..." + +# TODO: Replace these with your actual GalaChain connection details +CHANNEL_NAME="product-channel" +CHAINCODE_NAME="basic-product" +PEER_ADDRESS="peer0.your-org.com:7051" + +# Method 1: Using gala-cli (if available) +if command -v gala-cli &> /dev/null; then + echo "๐Ÿ“ก Using gala-cli to resume DEX..." + + gala-cli chaincode invoke \ + --channel-name "$CHANNEL_NAME" \ + --chaincode-name "$CHAINCODE_NAME" \ + --function-name "resumeDex" \ + --args "" \ + --peer-addresses "$PEER_ADDRESS" + + RESUME_RESULT=$? + +# Method 2: Using peer CLI (fallback) +elif command -v peer &> /dev/null; then + echo "๐Ÿ“ก Using peer CLI to resume DEX..." + + peer chaincode invoke \ + -o orderer.your-domain.com:7050 \ + -C "$CHANNEL_NAME" \ + -n "$CHAINCODE_NAME" \ + --peerAddresses "$PEER_ADDRESS" \ + -c "{\"function\":\"resumeDex\",\"Args\":[]}" + + RESUME_RESULT=$? + +# Method 3: API call (if CLI tools not available) +else + echo "๐Ÿ“ก Using API call to resume DEX..." + + # TODO: Replace with your actual API endpoint and auth + API_ENDPOINT="https://your-galachain-api.com/api/chaincode/invoke" + AUTH_TOKEN="your-admin-token" + + curl -X POST "$API_ENDPOINT" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $AUTH_TOKEN" \ + -d "{ + \"chaincodeName\": \"$CHAINCODE_NAME\", + \"functionName\": \"resumeDex\", + \"args\": [] + }" \ + --fail \ + --silent \ + --show-error + + RESUME_RESULT=$? +fi + +# Check if resume was successful +if [ $RESUME_RESULT -eq 0 ]; then + echo "" + echo "โœ… DEX SUCCESSFULLY RESUMED!" + echo "๐ŸŸข All trading operations are now active" + echo "" + echo "๐Ÿ“‹ NEXT STEPS:" + echo " 1. ๐Ÿ“Š Monitor system closely for 30 minutes" + echo " 2. ๐Ÿ“ฑ Notify team of successful resume" + echo " 3. ๐Ÿ“ข Update user communications" + echo " 4. ๐Ÿ“ Complete incident report" + echo " 5. ๐Ÿ” Review incident response process" + echo "" + echo "๐Ÿ“ˆ Monitor: ./check-dex-status.sh" + echo "" + + # Log the resume action + echo "$(date): EMERGENCY RESUME - By: $(whoami)" >> emergency.log + +else + echo "" + echo "โŒ EMERGENCY RESUME FAILED!" + echo "๐Ÿšจ DEX STATUS UNKNOWN - INVESTIGATE IMMEDIATELY" + echo "" + echo "๐Ÿ”„ IMMEDIATE ACTIONS:" + echo " 1. ๐Ÿ“ž Contact technical team" + echo " 2. ๐Ÿ” Check system logs" + echo " 3. ๐Ÿงช Verify current pause status" + echo "" + exit 1 +fi diff --git a/emergency-stop.sh b/emergency-stop.sh new file mode 100755 index 0000000..f77b30f --- /dev/null +++ b/emergency-stop.sh @@ -0,0 +1,128 @@ +#!/bin/bash +# GalaSwap DEX Emergency Stop Script +# Usage: ./emergency-stop.sh [reason] + +set -e # Exit on any error + +echo "๐Ÿšจ GALASWAP DEX EMERGENCY PAUSE INITIATED" +echo "==========================================" +echo "Timestamp: $(date)" +echo "Executed by: $(whoami)" +echo "Host: $(hostname)" + +# Get emergency reason +if [ -n "$1" ]; then + REASON="$1" +else + echo "" + echo "Enter emergency reason (or press Enter for default):" + read REASON +fi + +# Set default reason if none provided +if [ -z "$REASON" ]; then + REASON="EMERGENCY_STOP_$(date +%Y%m%d_%H%M%S)" +fi + +echo "" +echo "๐Ÿ”ด PAUSING DEX WITH REASON: $REASON" +echo "โš ๏ธ This will block ALL DEX operations immediately!" +echo "" + +# Confirmation prompt +echo "Continue with emergency pause? (y/N)" +read CONFIRMATION + +if [ "$CONFIRMATION" != "y" ] && [ "$CONFIRMATION" != "Y" ]; then + echo "โŒ Emergency pause cancelled by user" + exit 1 +fi + +echo "" +echo "๐Ÿ”„ Executing emergency pause..." + +# TODO: Replace these with your actual GalaChain connection details +CHANNEL_NAME="product-channel" +CHAINCODE_NAME="basic-product" +PEER_ADDRESS="peer0.your-org.com:7051" + +# Method 1: Using gala-cli (if available) +if command -v gala-cli &> /dev/null; then + echo "๐Ÿ“ก Using gala-cli to pause DEX..." + + gala-cli chaincode invoke \ + --channel-name "$CHANNEL_NAME" \ + --chaincode-name "$CHAINCODE_NAME" \ + --function-name "pauseDex" \ + --args "\"$REASON\"" \ + --peer-addresses "$PEER_ADDRESS" + + PAUSE_RESULT=$? + +# Method 2: Using peer CLI (fallback) +elif command -v peer &> /dev/null; then + echo "๐Ÿ“ก Using peer CLI to pause DEX..." + + peer chaincode invoke \ + -o orderer.your-domain.com:7050 \ + -C "$CHANNEL_NAME" \ + -n "$CHAINCODE_NAME" \ + --peerAddresses "$PEER_ADDRESS" \ + -c "{\"function\":\"pauseDex\",\"Args\":[\"$REASON\"]}" + + PAUSE_RESULT=$? + +# Method 3: API call (if CLI tools not available) +else + echo "๐Ÿ“ก Using API call to pause DEX..." + + # TODO: Replace with your actual API endpoint and auth + API_ENDPOINT="https://your-galachain-api.com/api/chaincode/invoke" + AUTH_TOKEN="your-admin-token" + + curl -X POST "$API_ENDPOINT" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $AUTH_TOKEN" \ + -d "{ + \"chaincodeName\": \"$CHAINCODE_NAME\", + \"functionName\": \"pauseDex\", + \"args\": [\"$REASON\"] + }" \ + --fail \ + --silent \ + --show-error + + PAUSE_RESULT=$? +fi + +# Check if pause was successful +if [ $PAUSE_RESULT -eq 0 ]; then + echo "" + echo "โœ… DEX SUCCESSFULLY PAUSED!" + echo "๐Ÿ”ด All trading operations are now blocked" + echo "" + echo "๐Ÿ“‹ NEXT STEPS:" + echo " 1. ๐Ÿ“ฑ Notify emergency team immediately" + echo " 2. ๐Ÿ“Š Check system status and logs" + echo " 3. ๐Ÿ” Begin incident investigation" + echo " 4. ๐Ÿ“ Document incident details" + echo " 5. ๐Ÿ“ข Prepare user communication" + echo "" + echo "๐Ÿ’ก To resume operations: ./emergency-resume.sh" + echo "" + + # Log the emergency action + echo "$(date): EMERGENCY PAUSE - Reason: $REASON - By: $(whoami)" >> emergency.log + +else + echo "" + echo "โŒ EMERGENCY PAUSE FAILED!" + echo "๐Ÿšจ DEX MAY STILL BE OPERATIONAL - CRITICAL ISSUE" + echo "" + echo "๐Ÿ”„ IMMEDIATE ACTIONS:" + echo " 1. ๐Ÿ“ž Contact technical team immediately" + echo " 2. ๐Ÿ”„ Try alternative pause method" + echo " 3. ๐Ÿ›‘ Consider network-level intervention" + echo "" + exit 1 +fi diff --git a/package-lock.json b/package-lock.json index 12874cf..aaf83da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "@gala-chain/client": "~2.3.4", "@gala-chain/test": "~2.3.4", "@trivago/prettier-plugin-sort-imports": "^4.3.0", + "@types/decimal.js": "^0.0.32", "@types/jest": "^29.5.12", "@types/node": "18.11.9", "@typescript-eslint/eslint-plugin": "^5.47.1", @@ -2076,6 +2077,13 @@ "@types/node": "*" } }, + "node_modules/@types/decimal.js": { + "version": "0.0.32", + "resolved": "https://registry.npmjs.org/@types/decimal.js/-/decimal.js-0.0.32.tgz", + "integrity": "sha512-qiZoeFWRa6SaedYkSV8VrGV8xDGV3C6usFlUKOOl/fpvVdKVx+eHm+yHjJbGIuHgNsoe24wUddwLJGVBZFM5Ow==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -9930,6 +9938,12 @@ "@types/node": "*" } }, + "@types/decimal.js": { + "version": "0.0.32", + "resolved": "https://registry.npmjs.org/@types/decimal.js/-/decimal.js-0.0.32.tgz", + "integrity": "sha512-qiZoeFWRa6SaedYkSV8VrGV8xDGV3C6usFlUKOOl/fpvVdKVx+eHm+yHjJbGIuHgNsoe24wUddwLJGVBZFM5Ow==", + "dev": true + }, "@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", diff --git a/package.json b/package.json index a62c8b7..df916ca 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@gala-chain/client": "~2.3.4", "@gala-chain/test": "~2.3.4", "@trivago/prettier-plugin-sort-imports": "^4.3.0", + "@types/decimal.js": "^0.0.32", "@types/jest": "^29.5.12", "@types/node": "18.11.9", "@typescript-eslint/eslint-plugin": "^5.47.1", diff --git a/src/chaincode/DexV3Contract.ts b/src/chaincode/DexV3Contract.ts index 9dfd390..6197628 100644 --- a/src/chaincode/DexV3Contract.ts +++ b/src/chaincode/DexV3Contract.ts @@ -112,6 +112,9 @@ import { transferDexPositionFeeGate } from "./dexLaunchpadFeeGate"; + +import { EmergencyControl } from "./dex/emergencyControl"; + /** * DexV3Contract provides Uniswap V3-style decentralized exchange functionality * including concentrated liquidity, limit orders, and automated market making. @@ -570,4 +573,42 @@ export class DexV3Contract extends GalaContract { ): Promise { return await getBatchSubmitAuthorities(ctx, dto); } + + +/** + * Emergency pause function - stops all DEX operations + * @param ctx - The GalaChain context + * @param reason - Reason for the pause + * @returns Confirmation of pause + */ + @GalaTransaction({ + type: EVALUATE, + in: class { + @IsString() + @IsNotEmpty() + reason: string; + }, + out: "string", + description: "Emergency pause of all DEX operations" + }) + public async emergencyPause(ctx: GalaChainContext, dto: { reason: string }): Promise { + await EmergencyControl.pauseDex(ctx, dto.reason); + return `DEX paused successfully at ${ctx.txUnixTime}`; + } + + /** + * Emergency resume function - resumes DEX operations after pause + * @param ctx - The GalaChain context + * @returns Confirmation of resume + */ + @GalaTransaction({ + type: EVALUATE, + out: "string", + description: "Resume DEX operations after emergency pause" + }) + public async emergencyResume(ctx: GalaChainContext): Promise { + await EmergencyControl.resumeDex(ctx); + return `DEX resumed successfully at ${ctx.txUnixTime}`; + } + } diff --git a/src/chaincode/dex/addLiquidity.ts b/src/chaincode/dex/addLiquidity.ts index 332bac9..6ed8c7f 100644 --- a/src/chaincode/dex/addLiquidity.ts +++ b/src/chaincode/dex/addLiquidity.ts @@ -35,6 +35,7 @@ import { NegativeAmountError } from "./dexError"; import { getTokenDecimalsFromPool, roundTokenAmount, validateTokenOrder } from "./dexUtils"; import { fetchOrCreateDexPosition } from "./position.helper"; import { fetchOrCreateTickDataPair } from "./tickData.helper"; +import { EmergencyControl } from "./emergencyControl"; /** * @dev Function to add Liqudity to v3 pool. The addLiquidity function facilitates the addition of liquidity to a Decentralized exchange pool within the GalaChain ecosystem. It takes in the blockchain context, liquidity parameters, and an optional launchpad address, then executes the necessary operations to deposit assets into the specified liquidity pool. @@ -48,6 +49,9 @@ export async function addLiquidity( dto: AddLiquidityDTO, launchpadAddress?: UserAlias | undefined ): Promise { + // Emergency pause check + + await EmergencyControl.checkPaused(ctx); const [token0, token1] = validateTokenOrder(dto.token0, dto.token1); const key = ctx.stub.createCompositeKey(Pool.INDEX_KEY, [token0, token1, dto.fee.toString()]); diff --git a/src/chaincode/dex/burn.ts b/src/chaincode/dex/burn.ts index e14df69..27de218 100644 --- a/src/chaincode/dex/burn.ts +++ b/src/chaincode/dex/burn.ts @@ -38,6 +38,7 @@ import { getTokenDecimalsFromPool, roundTokenAmount, validateTokenOrder } from " import { fetchUserPositionInTickRange } from "./position.helper"; import { fetchOrCreateTickDataPair } from "./tickData.helper"; import { updateOrRemovePosition } from "./updateOrRemovePosition"; +import { EmergencyControl } from "./emergencyControl"; /** * @dev The burn function is responsible for removing liquidity from a Decentralized exchange pool within the GalaChain ecosystem. It executes the necessary operations to burn the liquidity position and transfer the corresponding tokens back to the user. @@ -46,6 +47,11 @@ import { updateOrRemovePosition } from "./updateOrRemovePosition"; * @returns DexOperationResDto */ export async function burn(ctx: GalaChainContext, dto: BurnDto): Promise { + + // Emergency pause check + await EmergencyControl.checkPaused(ctx); + + // Fetch pool and user position const [token0, token1] = validateTokenOrder(dto.token0, dto.token1); diff --git a/src/chaincode/dex/collect.ts b/src/chaincode/dex/collect.ts index d626844..6571b6b 100644 --- a/src/chaincode/dex/collect.ts +++ b/src/chaincode/dex/collect.ts @@ -28,6 +28,7 @@ import { getTokenDecimalsFromPool, roundTokenAmount, validateTokenOrder } from " import { fetchUserPositionInTickRange } from "./position.helper"; import { fetchOrCreateTickDataPair } from "./tickData.helper"; import { updateOrRemovePosition } from "./updateOrRemovePosition"; +import { EmergencyControl } from "./emergencyControl"; /** * @dev The collect function allows a user to claim and withdraw accrued fee tokens from a specific liquidity position in a Decentralized exchange pool within the GalaChain ecosystem. It retrieves earned fees based on the user's position details and transfers them to the user's account. @@ -37,6 +38,9 @@ import { updateOrRemovePosition } from "./updateOrRemovePosition"; * @returns DexOperationResDto */ export async function collect(ctx: GalaChainContext, dto: CollectDto): Promise { + // Emergency pause check + await EmergencyControl.checkPaused(ctx); + // Validate token order and fetch pool and positions const [token0, token1] = validateTokenOrder(dto.token0, dto.token1); const key = ctx.stub.createCompositeKey(Pool.INDEX_KEY, [token0, token1, dto.fee.toString()]); diff --git a/src/chaincode/dex/createPool.ts b/src/chaincode/dex/createPool.ts index 4f63775..35848af 100644 --- a/src/chaincode/dex/createPool.ts +++ b/src/chaincode/dex/createPool.ts @@ -17,6 +17,10 @@ import { GalaChainContext, fetchTokenClass, getObjectByKey, putChainObject } fro import { CreatePoolDto, CreatePoolResDto, DexFeeConfig, Pool } from "../../api/"; import { generateKeyFromClassKey } from "./dexUtils"; +import { EmergencyControl } from "./emergencyControl"; + + + /** * @dev The createPool function initializes a new Decentralized exchange liquidity pool within the GalaChain ecosystem. It sets up the pool with the specified token pair, initial price, fee structure, and protocol fee settings. @@ -28,6 +32,10 @@ import { generateKeyFromClassKey } from "./dexUtils"; - Protocol fees โ€“ The percentage of fees collected by the protocol. */ export async function createPool(ctx: GalaChainContext, dto: CreatePoolDto): Promise { + + // Emergency pause check +await EmergencyControl.checkPaused(ctx); + // sort the tokens in an order const [token0, token1] = [dto.token0, dto.token1].map(generateKeyFromClassKey); if (token0.localeCompare(token1) > 0) { diff --git a/src/chaincode/dex/debug.spec.ts b/src/chaincode/dex/debug.spec.ts new file mode 100644 index 0000000..f6181e7 --- /dev/null +++ b/src/chaincode/dex/debug.spec.ts @@ -0,0 +1,61 @@ +// Debug test - Save as: src/chaincode/dex/debug.spec.ts +// This will help us see what's going wrong with state storage + +import { EmergencyControl, EmergencyPauseState } from "./emergencyControl"; +import { GalaChainContext } from "@gala-chain/chaincode"; + +describe("Emergency Control Debug", () => { + let ctx: GalaChainContext; + let mockState: Map = new Map(); + + beforeEach(() => { + mockState.clear(); + + ctx = { + callingUser: "admin-user", + txUnixTime: Date.now(), + clientIdentity: { + getMSPID: jest.fn().mockReturnValue("GalaAdminMSP") + }, + stub: { + createCompositeKey: jest.fn().mockImplementation((objectType: string, attributes: string[]) => { + return `${objectType}:${attributes.join(':')}`; + }), + setEvent: jest.fn(), + putState: jest.fn().mockImplementation(async (key: string, value: Buffer) => { + console.log(`๐Ÿ” DEBUG: Storing state - Key: ${key}, Value: ${value.toString()}`); + mockState.set(key, value); + }), + getState: jest.fn().mockImplementation(async (key: string) => { + console.log(`๐Ÿ” DEBUG: Reading state - Key: ${key}`); + const value = mockState.get(key); + console.log(`๐Ÿ” DEBUG: Found value: ${value ? value.toString() : 'null'}`); + return value || Buffer.from(''); + }) + } + } as any; + }); + + it("should store and retrieve pause state correctly", async () => { + console.log("\n=== DEBUGGING STATE STORAGE ==="); + + // Step 1: Pause the DEX + console.log("1๏ธโƒฃ Pausing DEX..."); + await EmergencyControl.pauseDex(ctx, "Testing emergency pause"); + + // Step 2: Check what was stored + console.log("2๏ธโƒฃ Current state in mock storage:"); + for (const [key, value] of mockState.entries()) { + console.log(` Key: ${key} -> Value: ${value.toString()}`); + } + + // Step 3: Try to read the pause state + console.log("3๏ธโƒฃ Checking if paused..."); + try { + await EmergencyControl.checkPaused(ctx); + console.log("โŒ ERROR: checkPaused did not throw an error!"); + } catch (error) { + console.log(`โœ… SUCCESS: checkPaused correctly threw: ${error.message}`); + } + }); +}); \ No newline at end of file diff --git a/src/chaincode/dex/dexEmergency.spec.ts b/src/chaincode/dex/dexEmergency.spec.ts new file mode 100644 index 0000000..5525bf9 --- /dev/null +++ b/src/chaincode/dex/dexEmergency.spec.ts @@ -0,0 +1,109 @@ +// DEX Emergency Integration Test +// Save as: src/chaincode/dex/dexEmergency.spec.ts + +import { GalaChainContext } from "@gala-chain/chaincode"; +import { ForbiddenError } from "@gala-chain/api"; +import { EmergencyControl } from "./emergencyControl"; +import { swap } from "./swap"; +import { addLiquidity } from "./addLiquidity"; +import { burn } from "./burn"; +import { collect } from "./collect"; +import { createPool } from "./createPool"; + +describe("DEX Emergency Integration Tests", () => { + let ctx: GalaChainContext; + let mockState: Map; + + beforeEach(() => { + mockState = new Map(); + + ctx = { + callingUser: "admin-user", + txUnixTime: Date.now(), + clientIdentity: { + getMSPID: jest.fn().mockReturnValue("GalaAdminMSP") + }, + stub: { + createCompositeKey: jest.fn().mockImplementation((objectType: string, attributes: string[]) => { + return `${objectType}${attributes.join('')}`; + }), + setEvent: jest.fn(), + putState: jest.fn().mockImplementation(async (key: string, value: Buffer) => { + mockState.set(key, value); + }), + getState: jest.fn().mockImplementation(async (key: string) => { + const value = mockState.get(key); + return value || Buffer.from(''); + }) + } + } as any; + }); + + describe("Emergency Controls Block DEX Operations", () => { + it("should block swap when DEX is paused", async () => { + // Pause the DEX + await EmergencyControl.pauseDex(ctx, "Emergency security patch"); + + // Try to swap - should be blocked + await expect(swap(ctx, {} as any)) + .rejects.toThrow("DEX operations are currently paused"); + }); + + it("should block addLiquidity when DEX is paused", async () => { + // Pause the DEX + await EmergencyControl.pauseDex(ctx, "Emergency maintenance"); + + // Try to add liquidity - should be blocked + await expect(addLiquidity(ctx, {} as any)) + .rejects.toThrow("DEX operations are currently paused"); + }); + + it("should block burn when DEX is paused", async () => { + // Pause the DEX + await EmergencyControl.pauseDex(ctx, "Security audit"); + + // Try to burn liquidity - should be blocked + await expect(burn(ctx, {} as any)) + .rejects.toThrow("DEX operations are currently paused"); + }); + + it("should block collect when DEX is paused", async () => { + // Pause the DEX + await EmergencyControl.pauseDex(ctx, "Fee calculation fix"); + + // Try to collect fees - should be blocked + await expect(collect(ctx, {} as any)) + .rejects.toThrow("DEX operations are currently paused"); + }); + + it("should block createPool when DEX is paused", async () => { + // Pause the DEX + await EmergencyControl.pauseDex(ctx, "Pool creation security review"); + + // Try to create pool - should be blocked + await expect(createPool(ctx, {} as any)) + .rejects.toThrow("DEX operations are currently paused"); + }); + }); + + describe("DEX Operations Work When Not Paused", () => { + it("should allow operations when DEX is not paused", async () => { + // These should NOT throw emergency pause errors + // (They may throw other validation errors, which is fine) + + try { + await swap(ctx, {} as any); + } catch (error) { + // Should NOT be an emergency pause error + expect(error.message).not.toContain("DEX operations are currently paused"); + } + + try { + await addLiquidity(ctx, {} as any); + } catch (error) { + // Should NOT be an emergency pause error + expect(error.message).not.toContain("DEX operations are currently paused"); + } + }); + }); +}); \ No newline at end of file diff --git a/src/chaincode/dex/emergencyControl.spec.ts b/src/chaincode/dex/emergencyControl.spec.ts new file mode 100644 index 0000000..e4af1c5 --- /dev/null +++ b/src/chaincode/dex/emergencyControl.spec.ts @@ -0,0 +1,89 @@ +/* + * Emergency Control Test Suite + */ +import { GalaChainContext } from "@gala-chain/chaincode"; +import { ForbiddenError } from "@gala-chain/api"; +import { EmergencyControl, EmergencyPauseState } from "./emergencyControl"; +import { swap } from "./swap"; +import { addLiquidity } from "./addLiquidity"; +import { burn } from "./burn"; +import { collect } from "./collect"; + +describe("Emergency Control Tests", () => { + let ctx: GalaChainContext; + let mockState: Map; + + beforeEach(() => { + mockState = new Map(); + + // Mock context with proper state management + ctx = { + callingUser: "admin-user", + txUnixTime: Date.now(), + clientIdentity: { + getMSPID: jest.fn().mockReturnValue("GalaAdminMSP") + }, + stub: { + createCompositeKey: jest.fn().mockImplementation((objectType: string, attributes: string[]) => { + return `${objectType}${attributes.join('')}`; + }), + setEvent: jest.fn(), + putState: jest.fn().mockImplementation(async (key: string, value: Buffer) => { + mockState.set(key, value); + }), + getState: jest.fn().mockImplementation(async (key: string) => { + const value = mockState.get(key); + return value || Buffer.from(''); + }) + } + } as any; + }); + + describe("Emergency Pause", () => { + it("should allow admin to pause the DEX", async () => { + await expect(EmergencyControl.pauseDex(ctx, "Security vulnerability detected")) + .resolves.not.toThrow(); + + expect(ctx.stub.setEvent).toHaveBeenCalledWith( + "EmergencyPause", + expect.any(Buffer) + ); + }); + + it("should prevent non-admin from pausing", async () => { + ctx.clientIdentity.getMSPID = jest.fn().mockReturnValue("UserMSP"); + + await expect(EmergencyControl.pauseDex(ctx, "Trying to pause")) + .rejects.toThrow(ForbiddenError); + }); + }); + + describe("Pause Check", () => { + it("should throw error when DEX is paused", async () => { + // First pause the DEX using our pauseDex method + await EmergencyControl.pauseDex(ctx, "Emergency maintenance"); + + // Now check if it properly detects the paused state + await expect(EmergencyControl.checkPaused(ctx)) + .rejects.toThrow("DEX operations are currently paused"); + }); + + it("should not throw when DEX is not paused", async () => { + // mockState is empty by default, so system should not be paused + await expect(EmergencyControl.checkPaused(ctx)) + .resolves.not.toThrow(); + }); + }); + + describe("Resume Functionality", () => { + it("should allow admin to resume operations", async () => { + await expect(EmergencyControl.resumeDex(ctx)) + .resolves.not.toThrow(); + + expect(ctx.stub.setEvent).toHaveBeenCalledWith( + "EmergencyResume", + expect.any(Buffer) + ); + }); + }); +}); \ No newline at end of file diff --git a/src/chaincode/dex/emergencyControl.ts b/src/chaincode/dex/emergencyControl.ts new file mode 100644 index 0000000..7817a96 --- /dev/null +++ b/src/chaincode/dex/emergencyControl.ts @@ -0,0 +1,156 @@ +/* + * 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 { ChainKey, ChainObject, ForbiddenError } from "@gala-chain/api"; +import { GalaChainContext, getObjectByKey, putChainObject } from "@gala-chain/chaincode"; +import { IsBoolean, IsInt, IsOptional, IsString } from "class-validator"; + +export class EmergencyPauseState extends ChainObject { + public static readonly INDEX_KEY = "GCDEMERGENCYPAUSE"; + + @ChainKey({ position: 0 }) + @IsString() + public readonly key: string = "SINGLETON"; + + @IsBoolean() + public isPaused: boolean; + + @IsOptional() + @IsString() + public pausedBy?: string; + + @IsOptional() + @IsInt() + public pausedAt?: number; + + @IsOptional() + @IsString() + public reason?: string; + + constructor() { + super(); + this.isPaused = false; + } +} + +export class EmergencyControl { + /** + * Pauses all DEX operations in case of emergency + * Requires admin privileges + */ + public static async pauseDex(ctx: GalaChainContext, reason: string): Promise { + // Check admin permission + const callingUser = ctx.callingUser; + if (!this.isAdmin(ctx, callingUser)) { + throw new ForbiddenError("Only admins can pause the DEX"); + } + + // Create pause state data + const pauseState = { + isPaused: true, + key: "SINGLETON", + pausedBy: callingUser, + pausedAt: ctx.txUnixTime, + reason: reason + }; + + // Store using the same method as checkPaused uses to read + const key = ctx.stub.createCompositeKey(EmergencyPauseState.INDEX_KEY, []); + await ctx.stub.putState(key, Buffer.from(JSON.stringify(pauseState))); + + // Emit event + ctx.stub.setEvent("EmergencyPause", Buffer.from(JSON.stringify({ + pausedBy: callingUser, + timestamp: ctx.txUnixTime, + reason + }))); + } + + /** + * Resumes DEX operations after emergency + * Requires admin privileges + */ + public static async resumeDex(ctx: GalaChainContext): Promise { + // Check admin permission + const callingUser = ctx.callingUser; + if (!this.isAdmin(ctx, callingUser)) { + throw new ForbiddenError("Only admins can resume the DEX"); + } + + // Clear pause state using the same method + const key = ctx.stub.createCompositeKey(EmergencyPauseState.INDEX_KEY, []); + const pauseState = { + isPaused: false, + key: "SINGLETON", + pausedBy: undefined, + pausedAt: undefined, + reason: undefined + }; + + await ctx.stub.putState(key, Buffer.from(JSON.stringify(pauseState))); + + // Emit event + ctx.stub.setEvent("EmergencyResume", Buffer.from(JSON.stringify({ + resumedBy: callingUser, + timestamp: ctx.txUnixTime + }))); + } + + /** + * Checks if DEX is currently paused + * Should be called at the start of every state-changing function + */ + public static async checkPaused(ctx: GalaChainContext): Promise { + try { + const key = ctx.stub.createCompositeKey(EmergencyPauseState.INDEX_KEY, []); + + // Get raw state data + const stateBytes = await ctx.stub.getState(key); + + if (stateBytes && stateBytes.length > 0) { + // Parse the stored JSON + const stateData = JSON.parse(stateBytes.toString()); + + if (stateData && stateData.isPaused === true) { + throw new ForbiddenError(`DEX operations are currently paused: ${stateData.reason || "Emergency maintenance"}`); + } + } + } catch (error) { + // If it's a ForbiddenError (pause error), re-throw it + if (error instanceof ForbiddenError) { + throw error; + } + // If it's any other error (like JSON parse error), assume system is not paused + // Log the error for debugging but don't throw + console.log(`DEBUG: Error checking pause state: ${error.message}`); + } + } + + /** + * Check if user is admin + * TODO: Implement your actual admin check logic here + */ + private static isAdmin(ctx: GalaChainContext, user: string): boolean { + // For now, you can hardcode admin users or check MSP + // This should be replaced with your actual admin logic + + // Example: Check if user belongs to admin organization + const mspId = ctx.clientIdentity.getMSPID(); + + // Replace these with your actual admin MSPs + const adminMSPs = ["GalaAdminMSP", "Org1MSP"]; + + return adminMSPs.includes(mspId); + } +} \ No newline at end of file diff --git a/src/chaincode/dex/swap.ts b/src/chaincode/dex/swap.ts index 7a33871..c9ccc8b 100644 --- a/src/chaincode/dex/swap.ts +++ b/src/chaincode/dex/swap.ts @@ -33,6 +33,7 @@ import { } from "../../api/"; import { roundTokenAmount, validateTokenOrder } from "./dexUtils"; import { processSwapSteps } from "./swap.helper"; +import { EmergencyControl } from "./emergencyControl"; /** * @dev The swap function executes a token swap in a Dex liquidity pool within the GalaChain ecosystem. @@ -49,6 +50,8 @@ import { processSwapSteps } from "./swap.helper"; * @returns */ export async function swap(ctx: GalaChainContext, dto: SwapDto): Promise { + // Emergency pause check + await EmergencyControl.checkPaused(ctx); const [token0, token1] = validateTokenOrder(dto.token0, dto.token1); const zeroForOne = dto.zeroForOne; const sqrtPriceLimit = dto.sqrtPriceLimit;