UNPKG

@fleupold/dex-contracts

Version:

Contracts for dFusion multi-token batch auction exchange

197 lines (196 loc) 9.12 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.solutionObjectiveValueComputation = exports.solutionObjectiveValue = exports.orderDisregardedUtility = exports.orderUtility = exports.getExecutedSellAmount = exports.ERROR_EPSILON = exports.feeAdded = exports.feeSubtracted = exports.toETH = void 0; const assert_1 = __importDefault(require("assert")); const bn_js_1 = __importDefault(require("bn.js")); const array_shims_1 = require("./array-shims"); /** * Converts the amount value to `ether` unit. * @param value - - The amount to convert * @returns The value in `ether` as a bignum */ function toETH(value) { const GWEI = 1000000000; return new bn_js_1.default(value * GWEI).mul(new bn_js_1.default(GWEI)); } exports.toETH = toETH; /** * The fee denominator used for calculating fees. */ const FEE_DENOMINATOR = new bn_js_1.default(1000); /** * The fee denominator minus one. */ const FEE_DENOMINATOR_MINUS_ONE = FEE_DENOMINATOR.sub(new bn_js_1.default(1)); /** * Removes fees to the specified value `n` times. * @param x - - The value to apply the fee to * @param n - - The number of times to apply the fee, must be greater than 0 * @returns The value minus fees */ function feeSubtracted(x, n = 1) { const result = x.mul(FEE_DENOMINATOR_MINUS_ONE).div(FEE_DENOMINATOR); return n === 1 ? result : feeSubtracted(result, n - 1); } exports.feeSubtracted = feeSubtracted; /** * Adds fees to the specified. * @param x - The value to apply the fee to * @returns The value plus fees */ function feeAdded(x) { return x.mul(FEE_DENOMINATOR).div(FEE_DENOMINATOR_MINUS_ONE); } exports.feeAdded = feeAdded; /** * The error epsilon required for buy/sell amounts to account for rounding * errors. */ exports.ERROR_EPSILON = new bn_js_1.default(999000); /** * Calculates the executed buy amout given a buy volume and the settled buy and * sell prices. * @param executedBuyAmount - The executed buy amount * @param buyTokenPrice - The buy token price * @param sellTokenPrice - The sell token price * @returns The value plus fees */ function getExecutedSellAmount(executedBuyAmount, buyTokenPrice, sellTokenPrice) { return executedBuyAmount .mul(buyTokenPrice) .div(FEE_DENOMINATOR_MINUS_ONE) .mul(FEE_DENOMINATOR) .div(sellTokenPrice); } exports.getExecutedSellAmount = getExecutedSellAmount; /** * Calculates the utility of an order given an executed buy amount and settled * solution prices. * @param order - The order * @param executedBuyAmount - The executed buy amount * @param prices - The prices * @returns The order's utility */ function orderUtility(order, executedBuyAmount, prices) { assert_1.default(prices.length > order.buyToken, "order buy token not included in prices"); assert_1.default(prices.length > order.sellToken, "order sell token not included in prices"); const executedSellAmount = getExecutedSellAmount(executedBuyAmount, prices[order.buyToken], prices[order.sellToken]); const execSellTimesBuy = executedSellAmount.mul(order.buyAmount); const roundedUtility = executedBuyAmount .sub(execSellTimesBuy.div(order.sellAmount)) .mul(prices[order.buyToken]); const utilityError = execSellTimesBuy .mod(order.sellAmount) .mul(prices[order.buyToken]) .div(order.sellAmount); return roundedUtility.sub(utilityError); } exports.orderUtility = orderUtility; /** * Calculates the disregarded utility of an order given an executed buy amount * and settled solution prices. * @param order - The order * @param executedBuyAmount - The executed buy amount * @param prices - The prices * @returns The order's disregarded utility */ function orderDisregardedUtility(order, executedBuyAmount, prices) { assert_1.default(prices.length > order.buyToken, "order buy token not included in prices"); assert_1.default(prices.length > order.sellToken, "order sell token not included in prices"); const executedSellAmount = getExecutedSellAmount(executedBuyAmount, prices[order.buyToken], prices[order.sellToken]); // TODO: account for balances here. // Contract evaluates as: MIN(sellAmount - executedSellAmount, user.balance.sellToken) const leftoverSellAmount = order.sellAmount.sub(executedSellAmount); const limitTermLeft = prices[order.sellToken].mul(order.sellAmount); const limitTermRight = prices[order.buyToken] .mul(order.buyAmount) .mul(FEE_DENOMINATOR) .div(FEE_DENOMINATOR_MINUS_ONE); let limitTerm = toETH(0); if (limitTermLeft.gt(limitTermRight)) { limitTerm = limitTermLeft.sub(limitTermRight); } return leftoverSellAmount.mul(limitTerm).div(order.sellAmount); } exports.orderDisregardedUtility = orderDisregardedUtility; /** * Calculates the total objective value for the specified solution given the * order book. * @param orders - The orders * @param solution - The solution * @returns The solution's objective value */ function solutionObjectiveValue(orders, solution) { return solutionObjectiveValueComputation(orders, solution, true).result; } exports.solutionObjectiveValue = solutionObjectiveValue; /** * Calculates the solutions objective value returning a computation object with * all the intermediate values - useful for debugging. * @param orders - The orders * @param solution - The solution * @param strict - Throw when solution is determined to be invalid * @returns The solution's objective value computation object */ function solutionObjectiveValueComputation(orders, solution, strict = true) { const tokenCount = Math.max(...array_shims_1.flat(orders.map((o) => [o.buyToken, o.sellToken]))) + 1; assert_1.default(orders.length === solution.buyVolumes.length, "solution buy volumes do not match orders"); assert_1.default(tokenCount === solution.prices.length, "solution prices does not include all tokens"); assert_1.default(toETH(1).eq(solution.prices[0]), "fee token price is not 1 ether"); const touchedOrders = orders .map((o, i) => (solution.buyVolumes[i].isZero() ? null : [o, i])) .filter((pair) => !!pair); const orderExecutedAmounts = orders.map(() => { return { buy: new bn_js_1.default(0), sell: new bn_js_1.default(0) }; }); const orderTokenConservation = orders.map(() => solution.prices.map(() => new bn_js_1.default(0))); const tokenConservation = solution.prices.map(() => new bn_js_1.default(0)); const utilities = orders.map(() => new bn_js_1.default(0)); const disregardedUtilities = orders.map(() => new bn_js_1.default(0)); for (const [order, i] of touchedOrders) { const buyVolume = solution.buyVolumes[i]; const sellVolume = getExecutedSellAmount(solution.buyVolumes[i], solution.prices[order.buyToken], solution.prices[order.sellToken]); orderExecutedAmounts[i] = { buy: buyVolume, sell: sellVolume }; orderTokenConservation[i][order.buyToken].isub(buyVolume); orderTokenConservation[i][order.sellToken].iadd(sellVolume); tokenConservation[order.buyToken].isub(buyVolume); tokenConservation[order.sellToken].iadd(sellVolume); utilities[i] = orderUtility(order, solution.buyVolumes[i], solution.prices); disregardedUtilities[i] = orderDisregardedUtility(order, solution.buyVolumes[i], solution.prices); } if (strict) { const feeTokenTouched = orders.findIndex((o, i) => !solution.buyVolumes[i].isZero() && (o.buyToken === 0 || o.sellToken === 0)) !== -1; assert_1.default(feeTokenTouched, "fee token is not touched"); assert_1.default(!tokenConservation[0].isNeg(), "fee token conservation is negative"); tokenConservation .slice(1) .forEach((conservation, i) => assert_1.default(conservation.isZero(), `token conservation not respected for token ${i + 1}`)); touchedOrders.forEach(([, id], i) => { assert_1.default(!utilities[i].isNeg(), `utility for order ${id} is negative`); assert_1.default(!disregardedUtilities[i].isNeg(), `disregarded utility for order ${id} is negative`); }); } const totalUtility = utilities.reduce((acc, du) => acc.iadd(du), toETH(0)); const totalDisregardedUtility = disregardedUtilities.reduce((acc, du) => acc.iadd(du), toETH(0)); const burntFees = tokenConservation[0].div(new bn_js_1.default(2)); const result = totalUtility.sub(totalDisregardedUtility).add(burntFees); if (strict) { assert_1.default(!result.isNeg() && !result.isZero(), "objective value negative or zero"); } return { orderExecutedAmounts, orderTokenConservation, tokenConservation, utilities, disregardedUtilities, totalUtility, totalDisregardedUtility, burntFees, result, }; } exports.solutionObjectiveValueComputation = solutionObjectiveValueComputation;