@fleupold/dex-contracts
Version:
Contracts for dFusion multi-token batch auction exchange
197 lines (196 loc) • 9.12 kB
JavaScript
;
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;