Skip to content

Commit

Permalink
feat: Add integration test of tickSpacing>1 (#1745)
Browse files Browse the repository at this point in the history
* fix(test): Do not run tests in parallel as everyone disables it
* feat(test): Open a tickSpacing=100 market
* feat(test): Add integration test of tickSpacing=100 to kandel, trade, and resting order
* feat(tickPriceHelper): Introduce tickOffsetFromRawRatio
* fix(trade): Coerce tick after slippage
* fix(restingOrder): Coerce tick on update offer
* fix(liquidityProvider): normalizeOfferParams coerces tick
* fix(liquidityProvider): updateOffer should work with eoa
  • Loading branch information
lnist authored Jan 19, 2024
1 parent 19a8567 commit 6dfc67f
Show file tree
Hide file tree
Showing 16 changed files with 608 additions and 211 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Next version

- Upgrade `examples/tutorials/on-the-fly-offer.js` to new Mangrove core protocol and SDK
- fix: Coerce ticks to tickSpacing when given as arguments
- feat: Add integration test of tickSpacing>1

# 2.0.4

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
"make-cli-executable": "shx chmod u+x ./dist/nodejs/cli/mgv.js",
"clean": "rimraf dist",
"test": "npm-run-all --parallel test:unit test:integration",
"test:integration": "cross-env NODE_ENV=test nyc --reporter=lcov mocha --parallel --config test/mocha/config/integration-tests.json --exit",
"test:unit": "cross-env NODE_ENV=test nyc --reporter=lcov mocha --parallel --config test/mocha/config/unit-tests.json --exit",
"test:integration": "cross-env NODE_ENV=test nyc --reporter=lcov mocha --config test/mocha/config/integration-tests.json --exit",
"test:unit": "cross-env NODE_ENV=test nyc --reporter=lcov mocha --config test/mocha/config/unit-tests.json --exit",
"test:coverage": "cross-env NODE_ENV=test nyc --reporter=lcov mocha --forbid-only --config test/mocha/config/coverage-tests.json --exit ",
"typechain": "ts-node --transpileOnly src/util/runTypechain.ts",
"write-contract-package-versions": "ts-node --transpileOnly src/util/writeContractPackageVersions.ts",
Expand Down
4 changes: 4 additions & 0 deletions src/constants/kandelConfiguration.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@
"1": {
"minimumBasePerOfferFactor": 10,
"minimumQuotePerOfferFactor": 10
},
"100": {
"minimumBasePerOfferFactor": 10,
"minimumQuotePerOfferFactor": 10
}
}
}
Expand Down
11 changes: 2 additions & 9 deletions src/kandel/geometricKandel/geometricKandelDistributionHelper.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import Market from "../../market";
import { MAX_TICK, MIN_TICK } from "../../util/coreCalculations/Constants";
import Big from "big.js";
import { ethers } from "ethers";
import { Bigish } from "../../util";
import KandelDistributionHelper from "../kandelDistributionHelper";

Expand Down Expand Up @@ -96,15 +95,9 @@ class GeometricKandelDistributionHelper {
throw Error("priceRatio must be larger than 1");
}

const { outbound_tkn, inbound_tkn } = Market.getOutboundInbound(
"asks",
this.helper.market.base,
this.helper.market.quote,
);
// round down to ensure ratio is not exceeded
return this.helper.askTickPriceHelper.tickFromVolumes(
inbound_tkn.fromUnits(ethers.constants.WeiPerEther).mul(priceRatio),
outbound_tkn.fromUnits(ethers.constants.WeiPerEther),
return this.helper.askTickPriceHelper.tickFromRawRatio(
priceRatio,
"roundDown",
);
}
Expand Down
9 changes: 3 additions & 6 deletions src/liquidityProvider.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { logger } from "./util/logger";
import * as ethers from "ethers";
import util from "util";

import Market from "./market";
// syntactic sugar
Expand Down Expand Up @@ -211,7 +210,8 @@ class LiquidityProvider {
const tickPriceHelper = new TickPriceHelper(p.ba, market);
let tick: number, gives: Big;
if ("tick" in p) {
tick = p.tick;
// round up to ensure we get at least the tick we want
tick = tickPriceHelper.coerceTick(p.tick, "roundUp");
gives = Big(p.gives);
} else if ("price" in p) {
// deduce tick & gives from volume & price
Expand Down Expand Up @@ -383,10 +383,7 @@ class LiquidityProvider {
if (typeof offer === "undefined") {
throw Error(`No offer in market with id ${id}.`);
}
if (typeof this.logic == "undefined") {
throw new Error(`${util.inspect(this)} must be defined`);
}
const thisMaker = this.eoa ? this.eoa : this.logic.address;
const thisMaker = this.logic ? this.logic.address : this.eoa;
const offerMakerAddress = offer.maker;
if (offerMakerAddress != thisMaker) {
throw Error(
Expand Down
1 change: 1 addition & 0 deletions src/util/test/emptyChainDeployer.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ contract EmptyChainDeployer is Deployer {
ActivateMarket activateMarket = new ActivateMarket();

activateMarket.innerRun(mgv, mgvReader, Market(address(tokenA), address(tokenB), 1), 2 * 1e12, 3 * 1e12, 250);
activateMarket.innerRun(mgv, mgvReader, Market(address(tokenA), address(tokenB), 100), 2 * 1e12, 3 * 1e12, 250);
activateMarket.innerRun(mgv, mgvReader, Market(dai, usdc, 1), 1e12 / 1000, 1e12 / 1000, 0);
activateMarket.innerRun(mgv, mgvReader, Market(weth, dai, 1), 1e12, 1e12 / 1000, 0);
activateMarket.innerRun(mgv, mgvReader, Market(weth, usdc, 1), 1e12, 1e12 / 1000, 0);
Expand Down
3 changes: 2 additions & 1 deletion src/util/test/mgvIntegrationTestUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export type Balances = {
};

export const bidsAsks: Market.BA[] = ["bids", "asks"];
export const buySell: Market.BS[] = ["buy", "sell"];

export type AddressAndSigner = { address: string; signer: string };

Expand All @@ -44,7 +45,7 @@ const signers: any = {};

// A safe minimum to be above density requirement.
export const rawMinGivesBase = BigNumber.from("1000000000000000000");
export const rawMinGivesQuote = BigNumber.from("1000000000000000000");
export const rawMinGivesQuote = BigNumber.from("100000");

// With the removal of hardhat, there is no "default chain" anymore
// (it used to be implicit since we ran the ethereum local server in-process).
Expand Down
10 changes: 8 additions & 2 deletions src/util/tickPriceHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,12 +339,18 @@ class TickPriceHelper {
* NB: This is a lossy conversions since ticks are discrete and ratios are not.
*
* @param rawRatio inbound/outbound ratio to calculate the tick for
* @param roundingMode the rounding mode for coercing tick to a representable tick. See {@link RoundingMode}
* @param roundingMode the rounding mode for coercing tick to a representable tick. See {@link RoundingMode}. `noCoercion` does not coerce to a representable tick, i.e., as if tickSpacing=1
* @returns a tick (coerced to nearest bin) that approximates the given ratio.
*/
public tickFromRawRatio(rawRatio: Big, roundingMode: RoundingMode): number {
public tickFromRawRatio(
rawRatio: Big,
roundingMode: RoundingMode | "noCoercion",
): number {
const { man, exp } = TickPriceHelper.rawRatioToMantissaExponent(rawRatio);
const tick = TickLib.tickFromRatio(man, exp);
if (roundingMode === "noCoercion") {
return tick.toNumber();
}
let binnedTick = this.nearestRepresentableTick(tick, roundingMode);
// Since the `tick` can be off, the `binnedTick` can be off, so we correct it.
if (roundingMode === "roundDown") {
Expand Down
28 changes: 10 additions & 18 deletions src/util/trade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,26 +62,17 @@ class Trade {
// in this case, we're merely asking to get the tick adjusted for slippage
fillVolume = Big(params.fillVolume);
fillWants = params.fillWants ?? fillWants;
maxTick = params.maxTick;
if (slippage > 0) {
// round down to not exceed the price when buying, and up to get at least price for when selling
const limitPrice = tickPriceHelper.priceFromTick(
params.maxTick,
bs === "buy" ? "roundDown" : "roundUp",
// add slippage to the tick with an offset given by the tick representing the same slippage as a ratio (not coerced to tickSpacing, we do that after the addition)
// the slippage is added for both buy and sell, since a higher tick means a worse price.
maxTick += tickPriceHelper.tickFromRawRatio(
Big(100 + slippage).div(100),
"noCoercion",
);
const limitPriceWithSlippage = this.adjustForSlippage(
limitPrice,
slippage,
bs,
);
// round down to not exceed the price expectations
maxTick = tickPriceHelper.tickFromPrice(
limitPriceWithSlippage,
"roundDown",
);
} else {
// if slippage is 0, we don't need to do anything
maxTick = params.maxTick;
}
// coerce tick - round down to not exceed the price expectations
maxTick = tickPriceHelper.coerceTick(maxTick, "roundDown");
} else {
let wants = Big(params.wants);
let gives = Big(params.gives);
Expand Down Expand Up @@ -352,7 +343,8 @@ class Trade {
const tickPriceHelper = new TickPriceHelper(ba, market);

if ("tick" in params && params.tick !== undefined) {
tick = params.tick;
// round up to ensure we as a maker get what we want for the offer.
tick = tickPriceHelper.coerceTick(params.tick, "roundUp");
}

if ("price" in params && params.price !== undefined) {
Expand Down
137 changes: 135 additions & 2 deletions test/integration/kandel/geometricKandelInstance.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,13 @@ describe(`${GeometricKandelInstance.prototype.constructor.name} integration test
let kandel: GeometricKandelInstance;
let kandelStrategies: KandelStrategies;

async function createKandel(onAave: boolean) {
async function createKandel(onAave: boolean, tickSpacing: number = 1) {
kandelStrategies = new KandelStrategies(mgv);
const seeder = new KandelStrategies(mgv).seeder;
const market = await mgv.market({
base: "TokenA",
quote: "TokenB",
tickSpacing: 1,
tickSpacing,
});
const kandelAddress = (
await (
Expand Down Expand Up @@ -728,6 +728,139 @@ describe(`${GeometricKandelInstance.prototype.constructor.name} integration test
});
});

describe("tickSpacing=100", function () {
beforeEach(async function () {
kandel = await createKandel(false, 100);
});
it(`populate for tickSpacing=100 populates a market`, async function () {
// Arrange
const market = kandel.market;
const priceRatio = new Big(1.08);
const firstBase = Big(1);
const firstQuote = Big(1000);
const pricePoints = 6;
const midPrice = Big(1200);
const distribution =
await kandel.geometricGenerator.calculateDistribution({
distributionParams: {
minPrice: firstQuote.div(firstBase),
priceRatio,
pricePoints,
midPrice,
generateFromMid: true,
stepSize: 1,
},
initialAskGives: firstBase,
});

const { requiredBase, requiredQuote } =
distribution.getOfferedVolumeForDistribution();

const approvalTxs = await kandel.approveIfHigher();
await approvalTxs[0]?.wait();
await approvalTxs[1]?.wait();

// Act
const receipts = await waitForTransactions(
await kandel.populateGeometricDistribution({
distribution,
depositBaseAmount: requiredBase,
depositQuoteAmount: requiredQuote,
}),
);

// Assert
await mgvTestUtil.waitForBlock(
market.mgv,
receipts[receipts.length - 1].blockNumber,
);

// assert parameters are updated
const params = await kandel.getParameters();

assert.equal(
params.pricePoints,
pricePoints,
"pricePoints should have been updated",
);
assert.equal(
(await kandel.getBaseQuoteTickOffset()).baseQuoteTickOffset,
kandel.geometricGenerator.geometricDistributionHelper.calculateBaseQuoteTickOffset(
priceRatio,
),
"ratio should have been updated",
);

assert.ok(
(await kandel.getBaseQuoteTickOffset()).baseQuoteTickOffset % 100 == 0,
"tickSpacing should be a multiple of 100",
);

// assert expected offer writes
const book = market.getBook();
const asks = [...book.asks];
const bids = [...book.bids];

// assert asks
assert.equal(asks.length, 3, "3 live asks should be populated");
for (let i = 0; i < asks.length; i++) {
const offer = asks[i];
const d = distribution.getOfferAtIndex(
"asks",
distribution.getFirstLiveAskIndex() + i,
);
assert.ok(d !== undefined);
assert.equal(
offer.gives.toString(),
d.gives.toString(),
"gives should be base for ask",
);
assert.equal(
offer.tick.toString(),
d.tick.toString(),
"tick should be correct for ask",
);
assert.ok(offer.tick % 100 == 0, "tick should be a multiple of 100");
assert.equal(offer.id, await kandel.getOfferIdAtIndex("asks", d.index));
assert.equal(d.index, await kandel.getIndexOfOfferId("asks", offer.id));
}
// assert bids
assert.equal(bids.length, 2, "2 bids should be populated, 1 hole");
for (let i = 0; i < bids.length; i++) {
const offer = bids[bids.length - 1 - i];
const d = distribution.getOfferAtIndex("bids", i);
assert.ok(d !== undefined);
assert.equal(
offer.gives.toString(),
d.gives.toString(),
"gives should be quote for bid",
);
assert.equal(
offer.tick.toString(),
d.tick.toString(),
"tick should be correct for bid",
);
assert.ok(offer.tick % 100 == 0, "tick should be a multiple of 100");
assert.equal(offer.id, await kandel.getOfferIdAtIndex("bids", d.index));
assert.equal(d.index, await kandel.getIndexOfOfferId("bids", offer.id));
}

// assert provisions transferred is done by offers being able to be posted

// assert deposits
assert.equal(
(await kandel.getBalance("asks")).toString(),
requiredBase.toString(),
"Base should be deposited",
);
assert.equal(
(await kandel.getBalance("bids")).toString(),
requiredQuote.toString(),
"Quote should be deposited",
);
});
});

[true, false].forEach((onAave) =>
describe(`onAave=${onAave}`, function () {
beforeEach(async function () {
Expand Down
12 changes: 12 additions & 0 deletions test/integration/mangrove.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,18 @@ describe("Mangrove integration tests suite", function () {
assert.deepEqual(tokenToData(marketData[0].base), tokenBData);
assert.deepEqual(tokenToData(marketData[0].quote), tokenAData);
});

it("gets two open TokenB/TokenA markets", async function () {
// Act
const markets = (await mgv.openMarkets()).filter(
(m) => m.base.symbol === "TokenB" && m.quote.symbol === "TokenA",
);

// Assert
assert.equal(markets.length, 2);
assert.equal(markets.filter((m) => m.tickSpacing === 1).length, 1);
assert.equal(markets.filter((m) => m.tickSpacing !== 1).length, 1);
});
});
describe("node utils", () => {
it("can deal a test token", async () => {
Expand Down
Loading

0 comments on commit 6dfc67f

Please sign in to comment.