mirror of
https://github.com/luxfi/oracle.git
synced 2026-11-03 09:43:36 +00:00
521 lines
19 KiB
JavaScript
521 lines
19 KiB
JavaScript
const { assert } = require("chai");
|
|
const { web3 } = require("hardhat");
|
|
const winston = require("winston");
|
|
|
|
const { toWei, toBN } = web3.utils;
|
|
|
|
const { MedianizerPriceFeed } = require("../../dist/price-feed/MedianizerPriceFeed");
|
|
const { BasketSpreadPriceFeed } = require("../../dist/price-feed/BasketSpreadPriceFeed");
|
|
const { PriceFeedMock } = require("../../dist/price-feed/PriceFeedMock");
|
|
|
|
describe("BasketSpreadPriceFeed.js", function () {
|
|
let baselinePriceFeeds;
|
|
let experimentalPriceFeeds;
|
|
let denominatorPriceFeed;
|
|
let dummyLogger;
|
|
let basketSpreadPriceFeed;
|
|
|
|
beforeEach(async function () {
|
|
dummyLogger = winston.createLogger({ level: "info", transports: [new winston.transports.Console()] });
|
|
});
|
|
it("Update", async function () {
|
|
const priceFeeds = [new PriceFeedMock()];
|
|
baselinePriceFeeds = [new MedianizerPriceFeed(priceFeeds), new MedianizerPriceFeed(priceFeeds)];
|
|
experimentalPriceFeeds = [new MedianizerPriceFeed(priceFeeds), new MedianizerPriceFeed(priceFeeds)];
|
|
denominatorPriceFeed = new MedianizerPriceFeed(priceFeeds);
|
|
|
|
basketSpreadPriceFeed = new BasketSpreadPriceFeed(
|
|
web3,
|
|
dummyLogger,
|
|
baselinePriceFeeds,
|
|
experimentalPriceFeeds,
|
|
denominatorPriceFeed
|
|
);
|
|
|
|
await basketSpreadPriceFeed.update();
|
|
|
|
// On the basket spread's update call, `priceFeeds[0]` should have been updated once for each
|
|
// Medianizer price feed that it is incorporated in. This is because the basket spread price feed
|
|
// updates its imported medianizer price feeds.
|
|
// Check work: 2x for baseline update, 2x experimental udpate, 1x for denominator = 5 total.
|
|
assert.equal(priceFeeds[0].updateCalled, 5);
|
|
});
|
|
describe("Computing basket spreads when the spread is within the range [0,2]", function () {
|
|
function _constructPriceFeedsWithPrecision(precision, noDenominator = false) {
|
|
// First let's construct the constituent pricefeeds of the baskets.
|
|
const baselineFeeds1 = new MedianizerPriceFeed(
|
|
[
|
|
// currentPrice historicalPrice lastUpdatedTime
|
|
new PriceFeedMock(
|
|
toBN(toWei("1")).div(toBN(10).pow(toBN(18 - precision))),
|
|
toBN(toWei("0.2")).div(toBN(10).pow(toBN(18 - precision))),
|
|
200,
|
|
precision
|
|
),
|
|
new PriceFeedMock(
|
|
toBN(toWei("1.5")).div(toBN(10).pow(toBN(18 - precision))),
|
|
toBN(toWei("1.3")).div(toBN(10).pow(toBN(18 - precision))),
|
|
55000,
|
|
precision
|
|
),
|
|
new PriceFeedMock(
|
|
toBN(toWei("9")).div(toBN(10).pow(toBN(18 - precision))),
|
|
toBN(toWei("2")).div(toBN(10).pow(toBN(18 - precision))),
|
|
50,
|
|
precision
|
|
),
|
|
],
|
|
false
|
|
);
|
|
// Computes the median:
|
|
// current: 1.5
|
|
// historical: 1.3
|
|
const baselineFeeds2 = new MedianizerPriceFeed(
|
|
[
|
|
// currentPrice historicalPrice lastUpdatedTime
|
|
new PriceFeedMock(
|
|
toBN(toWei("1.1")).div(toBN(10).pow(toBN(18 - precision))),
|
|
toBN(toWei("0.6")).div(toBN(10).pow(toBN(18 - precision))),
|
|
200,
|
|
precision
|
|
),
|
|
new PriceFeedMock(
|
|
toBN(toWei("2")).div(toBN(10).pow(toBN(18 - precision))),
|
|
toBN(toWei("0.8")).div(toBN(10).pow(toBN(18 - precision))),
|
|
55000,
|
|
precision
|
|
),
|
|
new PriceFeedMock(
|
|
toBN(toWei("2.3")).div(toBN(10).pow(toBN(18 - precision))),
|
|
toBN(toWei("2.2")).div(toBN(10).pow(toBN(18 - precision))),
|
|
50,
|
|
precision
|
|
),
|
|
],
|
|
true
|
|
);
|
|
// Computes the mean:
|
|
// current: 1.8
|
|
// historical: 1.2
|
|
|
|
baselinePriceFeeds = [baselineFeeds1, baselineFeeds2];
|
|
// Average basket price:
|
|
// current: 1.65
|
|
// historical: 1.25
|
|
|
|
const experimentalFeeds1 = new MedianizerPriceFeed(
|
|
[
|
|
// currentPrice historicalPrice lastUpdatedTime
|
|
new PriceFeedMock(
|
|
toBN(toWei("1.1")).div(toBN(10).pow(toBN(18 - precision))),
|
|
toBN(toWei("0.6")).div(toBN(10).pow(toBN(18 - precision))),
|
|
400,
|
|
precision
|
|
),
|
|
new PriceFeedMock(
|
|
toBN(toWei("1.2")).div(toBN(10).pow(toBN(18 - precision))),
|
|
toBN(toWei("2")).div(toBN(10).pow(toBN(18 - precision))),
|
|
60000,
|
|
precision
|
|
),
|
|
new PriceFeedMock(
|
|
toBN(toWei("1.3")).div(toBN(10).pow(toBN(18 - precision))),
|
|
toBN(toWei("66")).div(toBN(10).pow(toBN(18 - precision))),
|
|
100,
|
|
precision
|
|
),
|
|
],
|
|
false
|
|
);
|
|
// Computes the median:
|
|
// current: 1.2
|
|
// historical: 2
|
|
const experimentalFeeds2 = new MedianizerPriceFeed(
|
|
[
|
|
// currentPrice historicalPrice lastUpdatedTime
|
|
new PriceFeedMock(
|
|
toBN(toWei("0.9")).div(toBN(10).pow(toBN(18 - precision))),
|
|
toBN(toWei("0.25")).div(toBN(10).pow(toBN(18 - precision))),
|
|
800,
|
|
precision
|
|
),
|
|
new PriceFeedMock(
|
|
toBN(toWei("1.3")).div(toBN(10).pow(toBN(18 - precision))),
|
|
toBN(toWei("0.75")).div(toBN(10).pow(toBN(18 - precision))),
|
|
650000,
|
|
precision
|
|
),
|
|
new PriceFeedMock(
|
|
toBN(toWei("2")).div(toBN(10).pow(toBN(18 - precision))),
|
|
toBN(toWei("2")).div(toBN(10).pow(toBN(18 - precision))),
|
|
200,
|
|
precision
|
|
),
|
|
],
|
|
true
|
|
);
|
|
// Computes the mean:
|
|
// current: 1.4
|
|
// historical: 1
|
|
|
|
experimentalPriceFeeds = [experimentalFeeds1, experimentalFeeds2];
|
|
// Average basket price:
|
|
// current: 1.3
|
|
// historical: 1.5
|
|
|
|
denominatorPriceFeed = new MedianizerPriceFeed([
|
|
// currentPrice historicalPrice lastUpdatedTime
|
|
new PriceFeedMock(
|
|
toBN(toWei("1")).div(toBN(10).pow(toBN(18 - precision))),
|
|
toBN(toWei("8")).div(toBN(10).pow(toBN(18 - precision))),
|
|
6,
|
|
precision
|
|
),
|
|
new PriceFeedMock(
|
|
toBN(toWei("9")).div(toBN(10).pow(toBN(18 - precision))),
|
|
toBN(toWei("12")).div(toBN(10).pow(toBN(18 - precision))),
|
|
7,
|
|
precision
|
|
),
|
|
]);
|
|
// Computes the median:
|
|
// current: 5
|
|
// historical: 10
|
|
basketSpreadPriceFeed = new BasketSpreadPriceFeed(
|
|
web3,
|
|
dummyLogger,
|
|
baselinePriceFeeds,
|
|
experimentalPriceFeeds,
|
|
noDenominator ? null : denominatorPriceFeed
|
|
);
|
|
}
|
|
it("Default price precision", async function () {
|
|
_constructPriceFeedsWithPrecision(18);
|
|
|
|
// Current price calculation:
|
|
// - Basket averaged prices:
|
|
// - baseline = 1.65
|
|
// - experimental = 1.3
|
|
// - Spread price: 1 + 1.3 - 1.65 = 0.65
|
|
// - Denominator price: 5
|
|
// ===> Spread price divided by denominator: 0.13
|
|
assert.equal(basketSpreadPriceFeed.getCurrentPrice().toString(), toWei("0.13"));
|
|
|
|
// Historical price calculation (because we're using mocks, the timestamp doesn't matter).:
|
|
// - Basket averaged prices:
|
|
// - baseline = 1.25
|
|
// - experimental = 1.5
|
|
// - Spread price: 1 + 1.5 - 1.25 = 1.25
|
|
// - Denominator price: 10
|
|
// ===> Spread price divided by denominator: 0.125
|
|
const arbitraryHistoricalTimestamp = 1000;
|
|
assert.equal(await basketSpreadPriceFeed.getHistoricalPrice(arbitraryHistoricalTimestamp), toWei("0.125"));
|
|
|
|
// Should return the *maximum* lastUpdatedTime.
|
|
assert.equal(basketSpreadPriceFeed.getLastUpdateTime(), 650000);
|
|
assert.equal(basketSpreadPriceFeed.getLookback(), 3600);
|
|
});
|
|
it("Custom price precision", async function () {
|
|
// (same calculations and results as previous test, but precision should be different)
|
|
_constructPriceFeedsWithPrecision(8);
|
|
|
|
assert.equal(
|
|
basketSpreadPriceFeed.getCurrentPrice().toString(),
|
|
toBN(toWei("0.13"))
|
|
.div(toBN(10).pow(toBN(18 - 8)))
|
|
.toString()
|
|
);
|
|
const arbitraryHistoricalTimestamp = 1000;
|
|
assert.equal(
|
|
await basketSpreadPriceFeed.getHistoricalPrice(arbitraryHistoricalTimestamp),
|
|
toBN(toWei("0.125"))
|
|
.div(toBN(10).pow(toBN(18 - 8)))
|
|
.toString()
|
|
);
|
|
assert.equal(basketSpreadPriceFeed.getLastUpdateTime(), 650000);
|
|
});
|
|
it("Skipping denominator price feed", async function () {
|
|
// Same computation as first test except for last step where you divide by denominator, this
|
|
// should skip that step. Recall that the denominator's current and historical price are:
|
|
// 5 and 10 respectively.
|
|
_constructPriceFeedsWithPrecision(6, true);
|
|
|
|
assert.equal(
|
|
basketSpreadPriceFeed.getCurrentPrice().toString(),
|
|
toBN(toWei("0.65"))
|
|
.div(toBN(10).pow(toBN(18 - 6)))
|
|
.toString()
|
|
);
|
|
const arbitraryHistoricalTimestamp = 1000;
|
|
assert.equal(
|
|
await basketSpreadPriceFeed.getHistoricalPrice(arbitraryHistoricalTimestamp),
|
|
toBN(toWei("1.25"))
|
|
.div(toBN(10).pow(toBN(18 - 6)))
|
|
.toString()
|
|
);
|
|
assert.equal(basketSpreadPriceFeed.getLastUpdateTime(), 650000);
|
|
});
|
|
});
|
|
describe("Returns floored value when spread is below 0", function () {
|
|
// Basket averaged prices:
|
|
// - baseline = 2.1
|
|
// - experimental = 1
|
|
// Spread price: 1 + 1 - 2.1 = -0.1
|
|
// Denominator price: 5
|
|
// Basket spread divided by denominator = 0
|
|
|
|
function _constructPriceFeedsWithPrecision(precision) {
|
|
const baselineFeeds1 = new MedianizerPriceFeed([
|
|
// currentPrice historicalPrice lastUpdatedTime
|
|
new PriceFeedMock(
|
|
toBN(toWei("2.1")).div(toBN(10).pow(toBN(18 - precision))),
|
|
toBN(toWei("2.1")).div(toBN(10).pow(toBN(18 - precision))),
|
|
200,
|
|
precision
|
|
),
|
|
]);
|
|
baselinePriceFeeds = [baselineFeeds1];
|
|
// Average basket price = 2.1
|
|
|
|
const experimentalFeeds1 = new MedianizerPriceFeed([
|
|
// currentPrice historicalPrice lastUpdatedTime
|
|
new PriceFeedMock(
|
|
toBN(toWei("1")).div(toBN(10).pow(toBN(18 - precision))),
|
|
toBN(toWei("1")).div(toBN(10).pow(toBN(18 - precision))),
|
|
400,
|
|
precision
|
|
),
|
|
]);
|
|
experimentalPriceFeeds = [experimentalFeeds1];
|
|
// Average basket price = 1
|
|
|
|
denominatorPriceFeed = new MedianizerPriceFeed([
|
|
// currentPrice historicalPrice lastUpdatedTime
|
|
new PriceFeedMock(
|
|
toBN(toWei("1")).div(toBN(10).pow(toBN(18 - precision))),
|
|
toBN(toWei("8")).div(toBN(10).pow(toBN(18 - precision))),
|
|
6,
|
|
precision
|
|
),
|
|
new PriceFeedMock(
|
|
toBN(toWei("9")).div(toBN(10).pow(toBN(18 - precision))),
|
|
toBN(toWei("12")).div(toBN(10).pow(toBN(18 - precision))),
|
|
7,
|
|
precision
|
|
),
|
|
]);
|
|
// Computes the median:
|
|
// current: 5
|
|
// historical: 10
|
|
basketSpreadPriceFeed = new BasketSpreadPriceFeed(
|
|
web3,
|
|
dummyLogger,
|
|
baselinePriceFeeds,
|
|
experimentalPriceFeeds,
|
|
denominatorPriceFeed,
|
|
precision
|
|
);
|
|
}
|
|
it("Default price precision", async function () {
|
|
_constructPriceFeedsWithPrecision(18);
|
|
|
|
// Should return 0
|
|
assert.equal(basketSpreadPriceFeed.getCurrentPrice().toString(), "0");
|
|
|
|
// Should return 0 for historical price as well (because we're using mocks, the timestamp doesn't matter).
|
|
const arbitraryHistoricalTimestamp = 1000;
|
|
assert.equal(await basketSpreadPriceFeed.getHistoricalPrice(arbitraryHistoricalTimestamp), "0");
|
|
|
|
// Should return the *maximum* lastUpdatedTime.
|
|
assert.equal(basketSpreadPriceFeed.getLastUpdateTime(), 400);
|
|
});
|
|
it("Custom price precision", async function () {
|
|
_constructPriceFeedsWithPrecision(8);
|
|
|
|
// Should return the basket spread price divided by denominator
|
|
assert.equal(basketSpreadPriceFeed.getCurrentPrice().toString(), "0");
|
|
|
|
// Should return the same for historical price (because we're using mocks, the timestamp doesn't matter).
|
|
const arbitraryHistoricalTimestamp = 1000;
|
|
assert.equal(await basketSpreadPriceFeed.getHistoricalPrice(arbitraryHistoricalTimestamp), "0");
|
|
|
|
// Should return the *maximum* lastUpdatedTime.
|
|
assert.equal(basketSpreadPriceFeed.getLastUpdateTime(), 400);
|
|
});
|
|
});
|
|
describe("Returns ceiling value when spread is above 2 0", function () {
|
|
// Basket averaged prices:
|
|
// - baseline = 1
|
|
// - experimental = 2.1
|
|
// Spread price: 1 + 2.1 - 1 = 2.1, which gets ceil'd to 2
|
|
// Denominator price: 5 for current, 10 for historical
|
|
// Basket spread divided by denominator = 0.4 for current, 0.2 for historical
|
|
|
|
function _constructPriceFeedsWithPrecision(precision) {
|
|
const baselineFeeds1 = new MedianizerPriceFeed([
|
|
// currentPrice historicalPrice lastUpdatedTime
|
|
new PriceFeedMock(
|
|
toBN(toWei("1")).div(toBN(10).pow(toBN(18 - precision))),
|
|
toBN(toWei("1")).div(toBN(10).pow(toBN(18 - precision))),
|
|
200,
|
|
precision
|
|
),
|
|
]);
|
|
baselinePriceFeeds = [baselineFeeds1];
|
|
// Average basket price = 1
|
|
|
|
const experimentalFeeds1 = new MedianizerPriceFeed([
|
|
// currentPrice historicalPrice lastUpdatedTime
|
|
new PriceFeedMock(
|
|
toBN(toWei("2.1")).div(toBN(10).pow(toBN(18 - precision))),
|
|
toBN(toWei("2.1")).div(toBN(10).pow(toBN(18 - precision))),
|
|
400,
|
|
precision
|
|
),
|
|
]);
|
|
experimentalPriceFeeds = [experimentalFeeds1];
|
|
// Average basket price = 2.1
|
|
|
|
denominatorPriceFeed = new MedianizerPriceFeed([
|
|
// currentPrice historicalPrice lastUpdatedTime
|
|
new PriceFeedMock(
|
|
toBN(toWei("1")).div(toBN(10).pow(toBN(18 - precision))),
|
|
toBN(toWei("8")).div(toBN(10).pow(toBN(18 - precision))),
|
|
6,
|
|
precision
|
|
),
|
|
new PriceFeedMock(
|
|
toBN(toWei("9")).div(toBN(10).pow(toBN(18 - precision))),
|
|
toBN(toWei("12")).div(toBN(10).pow(toBN(18 - precision))),
|
|
7,
|
|
precision
|
|
),
|
|
]); // Computes the median: 5
|
|
basketSpreadPriceFeed = new BasketSpreadPriceFeed(
|
|
web3,
|
|
dummyLogger,
|
|
baselinePriceFeeds,
|
|
experimentalPriceFeeds,
|
|
denominatorPriceFeed,
|
|
precision
|
|
);
|
|
}
|
|
it("Default price precision", async function () {
|
|
_constructPriceFeedsWithPrecision(18);
|
|
|
|
// Should return 0.4
|
|
assert.equal(basketSpreadPriceFeed.getCurrentPrice().toString(), toWei("0.4"));
|
|
|
|
// Should return the same for historical price (because we're using mocks, the timestamp doesn't matter).
|
|
const arbitraryHistoricalTimestamp = 1000;
|
|
assert.equal(await basketSpreadPriceFeed.getHistoricalPrice(arbitraryHistoricalTimestamp), toWei("0.2"));
|
|
|
|
// Should return the *maximum* lastUpdatedTime.
|
|
assert.equal(basketSpreadPriceFeed.getLastUpdateTime(), 400);
|
|
});
|
|
it("Custom price precision", async function () {
|
|
_constructPriceFeedsWithPrecision(8);
|
|
|
|
// Should return 0.4 in desired precision
|
|
assert.equal(
|
|
basketSpreadPriceFeed.getCurrentPrice().toString(),
|
|
toBN(toWei("0.4"))
|
|
.div(toBN(10).pow(toBN(18 - 8)))
|
|
.toString()
|
|
);
|
|
|
|
// Should return a historical price that is adjusted for the historical denominator price
|
|
// (because we're using mocks, the timestamp doesn't matter).
|
|
const arbitraryHistoricalTimestamp = 1000;
|
|
assert.equal(
|
|
await basketSpreadPriceFeed.getHistoricalPrice(arbitraryHistoricalTimestamp),
|
|
toBN(toWei("0.2"))
|
|
.div(toBN(10).pow(toBN(18 - 8)))
|
|
.toString()
|
|
);
|
|
|
|
// Should return the *maximum* lastUpdatedTime.
|
|
assert.equal(basketSpreadPriceFeed.getLastUpdateTime(), 400);
|
|
});
|
|
});
|
|
it("Constituent price feeds fail to return price", async function () {
|
|
const priceFeeds = [new PriceFeedMock()];
|
|
baselinePriceFeeds = [new MedianizerPriceFeed(priceFeeds), new MedianizerPriceFeed(priceFeeds)];
|
|
experimentalPriceFeeds = [new MedianizerPriceFeed(priceFeeds), new MedianizerPriceFeed(priceFeeds)];
|
|
denominatorPriceFeed = new MedianizerPriceFeed(priceFeeds);
|
|
|
|
basketSpreadPriceFeed = new BasketSpreadPriceFeed(
|
|
web3,
|
|
dummyLogger,
|
|
baselinePriceFeeds,
|
|
experimentalPriceFeeds,
|
|
denominatorPriceFeed
|
|
);
|
|
|
|
// Should return null.
|
|
assert.equal(basketSpreadPriceFeed.getCurrentPrice(), null);
|
|
|
|
// Should throw an error for each null price output.
|
|
const arbitraryHistoricalTimestamp = 1000;
|
|
try {
|
|
basketSpreadPriceFeed.getHistoricalPrice(arbitraryHistoricalTimestamp);
|
|
} catch (err) {
|
|
// Error messages should reflect a missing price
|
|
assert.equal(err[0][0].message, "PriceFeedMock expected error thrown");
|
|
assert.equal(err[1][0].message, "PriceFeedMock expected error thrown");
|
|
assert.equal(err[2][0].message, "PriceFeedMock expected error thrown");
|
|
assert.equal(err[3][0].message, "PriceFeedMock expected error thrown");
|
|
assert.equal(err[4][0].message, "PriceFeedMock expected error thrown");
|
|
assert.equal(err.length, 5);
|
|
}
|
|
|
|
// Should return null.
|
|
assert.equal(basketSpreadPriceFeed.getLastUpdateTime(), null);
|
|
});
|
|
it("Validates constituent price feed decimals", async function () {
|
|
// Test that the BasketSpreadPriceFeed rejects any constituent price feeds where the decimals do not match up with the
|
|
// denominator price feed.
|
|
const priceFeeds = [new PriceFeedMock()];
|
|
const differentPrecisionPriceFeeds = [new PriceFeedMock(undefined, undefined, 0, 8)];
|
|
baselinePriceFeeds = [new MedianizerPriceFeed(priceFeeds), new MedianizerPriceFeed(priceFeeds)];
|
|
experimentalPriceFeeds = [new MedianizerPriceFeed(priceFeeds), new MedianizerPriceFeed(priceFeeds)];
|
|
denominatorPriceFeed = new MedianizerPriceFeed(priceFeeds);
|
|
|
|
const validBasketPriceFeed = new BasketSpreadPriceFeed(
|
|
web3,
|
|
dummyLogger,
|
|
baselinePriceFeeds,
|
|
experimentalPriceFeeds,
|
|
denominatorPriceFeed
|
|
);
|
|
|
|
let didThrow = false;
|
|
try {
|
|
const feedDecimals = validBasketPriceFeed.getPriceFeedDecimals();
|
|
assert.equal(feedDecimals, 18);
|
|
} catch (error) {
|
|
didThrow = true;
|
|
}
|
|
|
|
assert.isFalse(didThrow);
|
|
|
|
const invalidExperimentalPriceFeeds = [
|
|
new MedianizerPriceFeed(differentPrecisionPriceFeeds),
|
|
new MedianizerPriceFeed(differentPrecisionPriceFeeds),
|
|
];
|
|
const invalidBasketPriceFeed = new BasketSpreadPriceFeed(
|
|
web3,
|
|
dummyLogger,
|
|
baselinePriceFeeds,
|
|
invalidExperimentalPriceFeeds,
|
|
denominatorPriceFeed
|
|
);
|
|
try {
|
|
invalidBasketPriceFeed.getPriceFeedDecimals();
|
|
} catch (error) {
|
|
didThrow = true;
|
|
}
|
|
assert.isTrue(didThrow);
|
|
});
|
|
});
|