Skip to content

Commit

Permalink
feat: slippage calculations and validations
Browse files Browse the repository at this point in the history
  • Loading branch information
LukasDeco committed May 2, 2024
1 parent c085ece commit e559214
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 39 deletions.
12 changes: 8 additions & 4 deletions lib/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
import { SwapType } from "@metadaoproject/futarchy-ts";
import { Observable } from "rxjs";
import { SpotObservation, TwapObservation } from "@/types/prices";
import { BN } from "@coral-xyz/anchor";

export interface FutarchyClient {
daos: FutarchyDaoClient;
Expand Down Expand Up @@ -95,17 +96,20 @@ export interface FutarchyAmmMarketsClient {
outputAmountMin: number
): Promise<string[]>;
removeLiquidity(
ammAddr: AmmMarket,
lpTokensToBurn: number
ammMarket: AmmMarket,
lpTokensToBurn: BN,
slippage: BN
): Promise<string[]>;
validateAddLiquidity(
ammMarket: AmmMarket,
quoteAmount: number,
maxBaseAmount: number
maxBaseAmount: number,
slippage: number
): LiquidityAddError | null;
addLiquidity(
ammAddr: AmmMarket,
quoteAmount: number,
maxBaseAmount: number
maxBaseAmount: number,
slippage: number
): Promise<string[] | LiquidityAddError>;
}
26 changes: 18 additions & 8 deletions lib/client/indexer/market-clients/ammMarkets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from "@/types";
import { PublicKey } from "@solana/web3.js";
import { SwapType } from "@metadaoproject/futarchy-ts";
import { BN } from "@coral-xyz/anchor";

export class FutarchyIndexerAmmMarketsClient
implements FutarchyAmmMarketsClient
Expand All @@ -29,25 +30,29 @@ export class FutarchyIndexerAmmMarketsClient

validateAddLiquidity(
ammMarket: AmmMarket,
quoteAmount: number,
maxBaseAmount: number
quoteAmount: BN,
maxBaseAmount: BN,
slippage: BN
): LiquidityAddError | null {
return this.rpcMarketsClient.validateAddLiquidity(
ammMarket,
quoteAmount,
maxBaseAmount
maxBaseAmount,
slippage
);
}

async addLiquidity(
ammMarket: AmmMarket,
quoteAmount: number,
maxBaseAmount: number
quoteAmount: BN,
maxBaseAmount: BN,
slippage: BN
): Promise<string[] | LiquidityAddError> {
return this.rpcMarketsClient.addLiquidity(
ammMarket,
quoteAmount,
maxBaseAmount
maxBaseAmount,
slippage
);
}

Expand All @@ -66,9 +71,14 @@ export class FutarchyIndexerAmmMarketsClient

async removeLiquidity(
ammMarket: AmmMarket,
lpTokensToBurn: number
lpTokensToBurn: BN,
slippage: BN
): Promise<string[]> {
return this.rpcMarketsClient.removeLiquidity(ammMarket, lpTokensToBurn);
return this.rpcMarketsClient.removeLiquidity(
ammMarket,
lpTokensToBurn,
slippage
);
}

async swap(
Expand Down
74 changes: 53 additions & 21 deletions lib/client/rpc/market-clients/ammMarkets.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { FutarchyAmmMarketsClient } from "@/client";
import { enrichTokenMetadata } from "@/tokens";
import { calculateMaxWithSlippage, calculateMinWithSlippage } from "@/trading";
import { TransactionSender } from "@/transactions";
import { TokenWithBalance } from "@/types";
import {
Expand Down Expand Up @@ -77,7 +78,8 @@ export class FutarchyAmmMarketsRPCClient implements FutarchyAmmMarketsClient {
validateAddLiquidity(
ammMarket: AmmMarket,
quoteAmount: number,
maxBaseAmount: number
maxBaseAmount: number,
slippage: number
): LiquidityAddError | null {
const quoteAmountArg = new BN(
quoteAmount *
Expand All @@ -87,17 +89,25 @@ export class FutarchyAmmMarketsRPCClient implements FutarchyAmmMarketsClient {
maxBaseAmount *
new BN(10).pow(new BN(ammMarket.baseToken.decimals)).toNumber()
);
const quoteAmountWithSlippage = calculateMaxWithSlippage(
quoteAmountArg,
slippage
);
const baseAmountWithSlippage = calculateMaxWithSlippage(
baseAmountArg,
slippage
);
// base passed in should be ammBaseAmount honestly...
const baseReserve = new BN(ammMarket.baseAmount);
const quoteReserve = new BN(ammMarket.quoteAmount);
const baseReserve = ammMarket.baseAmount;
const quoteReserve = ammMarket.quoteAmount;

const ammBaseAmount = quoteAmountArg
const ammBaseAmount = new BN(quoteAmountWithSlippage)
.mul(baseReserve)
.div(quoteReserve)
.add(new BN(1));
if (baseAmountArg.toNumber() < ammBaseAmount.toNumber()) {
console.error(
`liquidity max base exceeded. baseAmountArg: ${baseAmountArg.toNumber()}. quoteAmountArg: ${quoteAmountArg.toNumber()}, ammBaseAmount: ${ammBaseAmount.toNumber()}`
if (baseAmountWithSlippage < ammBaseAmount.toNumber()) {
console.warn(
`liquidity max base exceeded. baseAmountArg: ${baseAmountWithSlippage}. quoteAmountArg: ${quoteAmountWithSlippage}, ammBaseAmount: ${ammBaseAmount.toNumber()}`
);
return "AddLiquidityMaxBaseExceeded";
}
Expand All @@ -107,14 +117,16 @@ export class FutarchyAmmMarketsRPCClient implements FutarchyAmmMarketsClient {
async addLiquidity(
ammMarket: AmmMarket,
quoteAmount: number,
maxBaseAmount: number
maxBaseAmount: number,
slippage: number
): Promise<string[] | LiquidityAddError> {
if (!this.transactionSender) return [];

const validationError = this.validateAddLiquidity(
ammMarket,
quoteAmount,
maxBaseAmount
maxBaseAmount,
slippage
);
if (validationError) {
return validationError;
Expand All @@ -124,25 +136,36 @@ export class FutarchyAmmMarketsRPCClient implements FutarchyAmmMarketsClient {
quoteAmount *
new BN(10).pow(new BN(ammMarket.quoteToken.decimals)).toNumber()
);
const maxBaseAmountArg = new BN(
maxBaseAmount *
new BN(10).pow(new BN(ammMarket.baseToken.decimals)).toNumber()
);

// we just pass this in as min LP tokens and this replicates the calculation in the program
// TODO multiple this times slippage
const minLpTokensToMint = quoteAmountArg
const quoteAmountWithSlippage = calculateMaxWithSlippage(
quoteAmountArg,
slippage
);
const maxBaseAmountWithSlippage = calculateMaxWithSlippage(
maxBaseAmountArg,
slippage
);

const minLpTokensToMint = new BN(quoteAmountWithSlippage)
.mul(new BN(ammMarket.lpMintSupply))
.div(ammMarket.quoteAmount);

const maxBaseAmountArg = new BN(
maxBaseAmount *
new BN(10).pow(new BN(ammMarket.baseToken.decimals)).toNumber()
const minLpTokensWithSlippage = calculateMinWithSlippage(
minLpTokensToMint.toNumber(),
slippage
);

const ix = this.ammClient.addLiquidityIx(
ammMarket.publicKey,
ammMarket.baseMint,
ammMarket.quoteMint,
quoteAmountArg,
maxBaseAmountArg,
minLpTokensToMint,
new BN(quoteAmountWithSlippage),
new BN(maxBaseAmountWithSlippage),
new BN(minLpTokensWithSlippage),
this.rpcProvider.publicKey
);
const tx = await ix.transaction();
Expand All @@ -168,9 +191,18 @@ export class FutarchyAmmMarketsRPCClient implements FutarchyAmmMarketsClient {
return simulation;
}

async removeLiquidity(ammMarket: AmmMarket, lpTokensToBurn: BN) {
const minBaseAmount = new BN(0);
const minQuoteAmount = new BN(0);
async removeLiquidity(
ammMarket: AmmMarket,
lpTokensToBurn: BN,
slippage: BN
) {
const lpRatio = lpTokensToBurn.div(new BN(ammMarket.lpMintSupply));
const minQuoteAmount = lpRatio
.mul(new BN(ammMarket.quoteAmount))
.mul(new BN(1 - slippage.toNumber()));
const minBaseAmount = lpRatio
.mul(new BN(ammMarket.baseAmount))
.mul(new BN(1 - slippage.toNumber()));
const ix = this.ammClient.removeLiquidityIx(
ammMarket.publicKey,
ammMarket.baseMint,
Expand Down
25 changes: 25 additions & 0 deletions lib/trading.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* Calculates the maximum value considering the slippage percentage.
*
* @param {number} maxValue - The original maximum value before slippage adjustment.
* @param {number} slippage - The slippage rate as a whole percentage (not decimal). i.e. if your slippage is 0.3%, pass in 0.3.
* This value will be divided by 100 internally to calculate the slippage percentage.
* @returns {number} The maximum value adjusted for slippage.
*/
export function calculateMaxWithSlippage(maxValue: number, slippage: number) {
const slippagePercent = slippage / 100;
return maxValue * (1 + slippagePercent);
}

/**
* Calculates the minimum value considering the slippage percentage.
*
* @param {number} minValue - The original minimum value before slippage adjustment.
* @param {number} slippage - The slippage rate as a whole percentage (not decimal). i.e. if your slippage is 0.3%, pass in 0.3.
* This value will be divided by 100 internally to calculate the slippage percentage.
* @returns {number} The minimum value adjusted for slippage.
*/
export function calculateMinWithSlippage(minValue: number, slippage: number) {
const slippagePercent = slippage / 100;
return minValue * (1 - slippagePercent);
}
4 changes: 2 additions & 2 deletions lib/types/amm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ export class AmmMarketFetchRequest implements MarketFetchRequest {

export type AmmMarket = Market & {
type: "amm";
baseAmount: number;
quoteAmount: number;
baseAmount: BN;
quoteAmount: BN;
lpMintSupply: number;
};

Expand Down
20 changes: 16 additions & 4 deletions tests/client/rpc/markets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { FutarchyRPCClient } from "@/client";
import { autocratVersionToTwapMap } from "@/constants";
import { TransactionSender } from "@/transactions";
import { AmmMarketFetchRequest, OpenbookMarketFetchRequest } from "@/types";
import { AnchorProvider, Program } from "@coral-xyz/anchor";
import { AnchorProvider, BN, Program } from "@coral-xyz/anchor";
import { Connection, PublicKey } from "@solana/web3.js";
import { describe, test, expect, beforeAll } from "bun:test";
import { createMockWallet } from "tests/test-utils";
Expand Down Expand Up @@ -51,21 +51,33 @@ describe("FutarchyRPCClient Integration Test", () => {
console.log(marketData); // Log to verify data or perform assertions
expect(marketData).toBeDefined(); // Simple check, adjust according to expected data structure
}, 60000);
test.skip("addLiquidity test. This should likely not run in CI for now", async () => {
test("addLiquidity test. This should likely not run in CI for now", async () => {
const request = new AmmMarketFetchRequest(
new PublicKey("HbSYiZ8JRKqNHTx2EJUr6c5wQMvMjNx1rmHkTUtVi9qC")
);
const marketData = await rpcClient.markets.fetchMarket(request);
if (marketData?.type === "amm") {
const txs = await rpcClient.markets.amm.addLiquidity(
marketData,
0.1,
0.1
0.01,
20,
0.3
);
console.log(txs); // Log to verify data or perform assertions
expect(txs).toBeDefined(); // Simple check, adjust according to expected data structure
}
}, 60000);
test.skip("remove liquidity test. This should likely not run in CI for now", async () => {
const request = new AmmMarketFetchRequest(
new PublicKey("HbSYiZ8JRKqNHTx2EJUr6c5wQMvMjNx1rmHkTUtVi9qC")
);
const marketData = await rpcClient.markets.fetchMarket(request);
if (marketData?.type === "amm") {
const txs = await rpcClient.markets.amm.removeLiquidity(marketData, 10);
console.log(txs); // Log to verify data or perform assertions
expect(txs).toBeDefined(); // Simple check, adjust according to expected data structure
}
}, 60000);

// Add more tests for other methods like cancelOrder, fetchOrderBook, etc.
});

0 comments on commit e559214

Please sign in to comment.