From c01dc680dd00a56b3c1492a839b7a912fc354ea2 Mon Sep 17 00:00:00 2001 From: Kevin Yang <5478483+k-yang@users.noreply.github.com> Date: Fri, 14 Jul 2023 11:36:45 -0400 Subject: [PATCH] feat(spot): add balancer swap predictor (#181) --- packages/nibijs/CHANGELOG.md | 5 + packages/nibijs/package.json | 2 +- packages/nibijs/src/balancer/balancer.ts | 146 ++++++++++++++++++++++ packages/nibijs/src/balancer/index.ts | 1 + packages/nibijs/src/test/balancer.test.ts | 108 ++++++++++++++++ 5 files changed, 261 insertions(+), 1 deletion(-) create mode 100644 packages/nibijs/src/balancer/balancer.ts create mode 100644 packages/nibijs/src/balancer/index.ts create mode 100644 packages/nibijs/src/test/balancer.test.ts diff --git a/packages/nibijs/CHANGELOG.md b/packages/nibijs/CHANGELOG.md index 9fd178d1..501d9563 100644 --- a/packages/nibijs/CHANGELOG.md +++ b/packages/nibijs/CHANGELOG.md @@ -7,6 +7,11 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline - . +## v0.21.5 + +- Refactor stableswap class +- Add balancer class for predicting outputs + ## v0.21.4 - Actually fix build diff --git a/packages/nibijs/package.json b/packages/nibijs/package.json index c0eccee5..f7f89439 100644 --- a/packages/nibijs/package.json +++ b/packages/nibijs/package.json @@ -1,7 +1,7 @@ { "name": "@nibiruchain/nibijs", "description": "The TypeScript SDK for the Nibiru blockchain.", - "version": "0.21.4", + "version": "0.21.5", "license": "MIT", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/nibijs/src/balancer/balancer.ts b/packages/nibijs/src/balancer/balancer.ts new file mode 100644 index 00000000..c2cb1b6e --- /dev/null +++ b/packages/nibijs/src/balancer/balancer.ts @@ -0,0 +1,146 @@ +import { BigNumber } from "bignumber.js" + +BigNumber.set({ ROUNDING_MODE: BigNumber.ROUND_FLOOR }) + +/** + * Swap contains the result of a swap + * + * @export + * @class Swap + * @property {BalancerPool} poolStart the pool before the swap + * @property {BalancerPool} poolEnd the pool after the swap + * @property {BigNumber} dxAmm the amount of x added to the pool + * @property {BigNumber} dxUser the amount of x removed from the pool + * @property {BigNumber} dyAmm the amount of y removed from the pool + * @property {BigNumber} dyUser the amount of y added to the pool + * @property {BigNumber} priceImpact the price impact of the swap + */ +export class Swap { + public poolStart: BalancerPool + public poolEnd: BalancerPool + + public dxAmm: BigNumber + public dxUser: BigNumber + public dyAmm: BigNumber + public dyUser: BigNumber + public priceImpact: BigNumber + + constructor(poolStart: BalancerPool, poolEnd: BalancerPool) { + this.poolStart = poolStart + this.poolEnd = poolEnd + this.dxAmm = poolEnd.x.minus(poolStart.x) + this.dyAmm = poolEnd.y.minus(poolStart.y) + this.dxUser = this.dxAmm.negated() + this.dyUser = this.dyAmm.negated() + + const startPrice = poolStart.spotPrice() + const endPrice = poolEnd.spotPrice() + this.priceImpact = endPrice.minus(startPrice).dividedBy(startPrice) + } +} + +/** + * Balancer contains the logic for exchanging tokens in a traditional xy=k AMM pool + * + * Constructor: + * @param {BigNumber} x + * @param {BigNumber} y + * @param {BigNumber} swapFee + * + * @export + * @class BalancerPool + * @property {BigNumber} x the amount of x in the pool + * @property {BigNumber} y the amount of y in the pool + * @property {BigNumber} swapFee the swap fee expressed as a ratio + */ +export class BalancerPool { + public x: BigNumber + public y: BigNumber + public swapFee: BigNumber + + constructor(x: BigNumber, y: BigNumber, fee: BigNumber) { + this.swapFee = fee + this.x = x + this.y = y + } + + k() { + return this.x.multipliedBy(this.y) + } + + spotPrice() { + return this.y.dividedBy(this.x) + } + + /** + * Calculates the result of adding x to the pool + * + * @param dxAmm the amount of x to add to the pool. Could be negative. + * @returns a Swap object representing the result of the swap + */ + swapX(dxAmm: BigNumber): Swap | undefined { + if (!this.x.plus(dxAmm).isPositive()) return undefined + + let dxAmmEffective = dxAmm + if (dxAmm.isPositive()) { + dxAmmEffective = dxAmmEffective.multipliedBy( + BigNumber(1).minus(this.swapFee) + ) + } + + let dyAmmNeeded = this.k() + .dividedBy(this.x.plus(dxAmmEffective)) + .minus(this.y) + if (dxAmm.isNegative()) { + // mutually exclusive from reducing dxAmmEffective + dyAmmNeeded = dyAmmNeeded.dividedBy(BigNumber(1).minus(this.swapFee)) + } + + const poolEnd = new BalancerPool( + this.x.plus(dxAmm), + this.y.plus(dyAmmNeeded), + this.swapFee + ) + + if (poolEnd.x.isLessThanOrEqualTo(0) || poolEnd.y.isLessThanOrEqualTo(0)) + return undefined + + return new Swap(this, poolEnd) + } + + /** + * Calculates the result of adding y to the pool + * + * @param dyAmm the amount of y to add to the pool. Could be negative. + * @returns a Swap object representing the result of the swap + */ + swapY(dyAmm: BigNumber): Swap | undefined { + if (!this.y.plus(dyAmm).isPositive()) return undefined + + let dyAmmEffective = dyAmm + if (dyAmm.isPositive()) { + dyAmmEffective = dyAmmEffective.multipliedBy( + BigNumber(1).minus(this.swapFee) + ) + } + + let dxAmmNeeded = this.k() + .dividedBy(this.y.plus(dyAmmEffective)) + .minus(this.x) + if (dyAmm.isNegative()) { + // mutually exclusive from reducing dyAmmEffective + dxAmmNeeded = dxAmmNeeded.dividedBy(BigNumber(1).minus(this.swapFee)) + } + + const poolEnd = new BalancerPool( + this.x.plus(dxAmmNeeded), + this.y.plus(dyAmm), + this.swapFee + ) + + if (poolEnd.x.isLessThanOrEqualTo(0) || poolEnd.y.isLessThanOrEqualTo(0)) + return undefined + + return new Swap(this, poolEnd) + } +} diff --git a/packages/nibijs/src/balancer/index.ts b/packages/nibijs/src/balancer/index.ts new file mode 100644 index 00000000..71d307b6 --- /dev/null +++ b/packages/nibijs/src/balancer/index.ts @@ -0,0 +1 @@ +export * from "./balancer" diff --git a/packages/nibijs/src/test/balancer.test.ts b/packages/nibijs/src/test/balancer.test.ts new file mode 100644 index 00000000..5f6f44ff --- /dev/null +++ b/packages/nibijs/src/test/balancer.test.ts @@ -0,0 +1,108 @@ +import { BigNumber } from "bignumber.js" +import { BalancerPool, Swap } from "../balancer" + +describe("balancer tests", () => { + test("add x", () => { + const balancerPool = new BalancerPool( + BigNumber(100), + BigNumber(100), + BigNumber(0.5) // 50% fee + ) + + expect(balancerPool.swapX(BigNumber(50))).toEqual({ + poolStart: { + x: BigNumber(100), + y: BigNumber(100), + swapFee: BigNumber(0.5), + }, + poolEnd: { + x: BigNumber(150), + y: BigNumber(80), + swapFee: BigNumber(0.5), + }, + dxAmm: BigNumber(50), + dxUser: BigNumber(-50), + dyAmm: BigNumber(-20), + dyUser: BigNumber(20), + priceImpact: BigNumber("-0.46666666666666666667"), + } as Swap) + }) + + test("remove x", () => { + const balancerPool = new BalancerPool( + BigNumber(100), + BigNumber(100), + BigNumber(0.5) // 50% fee + ) + + expect(balancerPool.swapX(BigNumber(-50))).toEqual({ + poolStart: { + x: BigNumber(100), + y: BigNumber(100), + swapFee: BigNumber(0.5), + }, + poolEnd: { + x: BigNumber(50), + y: BigNumber(300), // the user needs to deposit 200 y to get 50 x because of the swap fee + swapFee: BigNumber(0.5), + }, + dxAmm: BigNumber(-50), + dxUser: BigNumber(50), + dyAmm: BigNumber(200), + dyUser: BigNumber(-200), + priceImpact: BigNumber(5), + } as Swap) + }) + + test("add y", () => { + const balancerPool = new BalancerPool( + BigNumber(100), + BigNumber(100), + BigNumber(0.5) // 50% fee + ) + + expect(balancerPool.swapY(BigNumber(50))).toEqual({ + poolStart: { + x: BigNumber(100), + y: BigNumber(100), + swapFee: BigNumber(0.5), + }, + poolEnd: { + x: BigNumber(80), + y: BigNumber(150), + swapFee: BigNumber(0.5), + }, + dxAmm: BigNumber(-20), + dxUser: BigNumber(20), + dyAmm: BigNumber(50), + dyUser: BigNumber(-50), + priceImpact: BigNumber(0.875), + } as Swap) + }) + + test("remove y", () => { + const balancerPool = new BalancerPool( + BigNumber(100), + BigNumber(100), + BigNumber(0.5) // 50% fee + ) + + expect(balancerPool.swapY(BigNumber(-50))).toEqual({ + poolStart: { + x: BigNumber(100), + y: BigNumber(100), + swapFee: BigNumber(0.5), + }, + poolEnd: { + x: BigNumber(300), // the user needs to deposit 200 x to get 50 y because of the swap fee + y: BigNumber(50), + swapFee: BigNumber(0.5), + }, + dxAmm: BigNumber(200), + dxUser: BigNumber(-200), + dyAmm: BigNumber(-50), + dyUser: BigNumber(50), + priceImpact: BigNumber("-0.83333333333333333334"), + } as Swap) + }) +})