mirror of
https://github.com/luxfi/uma.git
synced 2026-10-30 15:14:44 +00:00
543 lines
27 KiB
JavaScript
543 lines
27 KiB
JavaScript
const { web3, getContract } = require("hardhat");
|
|
const { assert } = require("chai");
|
|
const { toWei, hexToUtf8, utf8ToHex, padRight } = web3.utils;
|
|
const winston = require("winston");
|
|
const sinon = require("sinon");
|
|
const {
|
|
interfaceName,
|
|
parseFixed,
|
|
runTestForVersion,
|
|
createConstructorParamsForContractVersion,
|
|
TESTED_CONTRACT_VERSIONS,
|
|
TEST_DECIMAL_COMBOS,
|
|
getContractsNodePackageAliasForVerion,
|
|
} = require("@uma/common");
|
|
|
|
// Script to test
|
|
const { CRMonitor } = require("../src/CRMonitor");
|
|
|
|
// Helpers and custom winston transport module to monitor winston log outputs
|
|
const {
|
|
FinancialContractClient,
|
|
PriceFeedMock,
|
|
SpyTransport,
|
|
lastSpyLogIncludes,
|
|
lastSpyLogLevel,
|
|
} = require("@uma/financial-templates-lib");
|
|
|
|
let iterationTestVersion; // store the test version between tests that is currently being tested.
|
|
const startTime = "15798990420";
|
|
|
|
// Contracts
|
|
let collateralToken;
|
|
let financialContract;
|
|
let syntheticToken;
|
|
let mockOracle;
|
|
let identifierWhitelist;
|
|
let finder;
|
|
let store;
|
|
let timer;
|
|
let optimisticOracle;
|
|
let configStore;
|
|
let collateralWhitelist;
|
|
let multicall;
|
|
|
|
// Price feed mock
|
|
let priceFeedMock;
|
|
let spyLogger;
|
|
let spy;
|
|
let financialContractProps;
|
|
let monitorConfig;
|
|
|
|
// re-used variables
|
|
let identifier;
|
|
let fundingRateIdentifier;
|
|
let currentTime;
|
|
let financialContractClient;
|
|
let crMonitor;
|
|
|
|
let convertDecimals;
|
|
|
|
let accounts;
|
|
let owner;
|
|
let monitoredTrader;
|
|
let monitoredSponsor;
|
|
|
|
// Set the funding rate and advances time by 10k seconds.
|
|
const _setFundingRateAndAdvanceTime = async (fundingRate) => {
|
|
currentTime = parseInt(await financialContract.methods.getCurrentTime().call());
|
|
await financialContract.methods.proposeFundingRate({ rawValue: fundingRate }, currentTime).send({ from: owner });
|
|
await financialContract.methods.setCurrentTime(currentTime + 10000).send({ from: owner });
|
|
};
|
|
|
|
// If the current version being executed is part of the `supportedVersions` array then return `it` to run the test.
|
|
// Else, do nothing. Can be used exactly in place of a normal `it` to parameterize contract types and versions supported
|
|
// for a given test.eg: versionedIt([{ contractType: "any", contractVersion: "any" }])(["Perpetual-latest"])("test name", async function () { assert.isTrue(true) })
|
|
// Note that a second param can be provided to make the test an `it.only` thereby ONLY running that single test, on
|
|
// the provided version. This is very useful for debugging and writing single unit tests without having ro run all tests.
|
|
const versionedIt = function (supportedVersions, shouldBeItOnly = false) {
|
|
if (shouldBeItOnly)
|
|
return runTestForVersion(supportedVersions, TESTED_CONTRACT_VERSIONS, iterationTestVersion) ? it.only : () => {};
|
|
return runTestForVersion(supportedVersions, TESTED_CONTRACT_VERSIONS, iterationTestVersion) ? it : () => {};
|
|
};
|
|
|
|
const Convert = (decimals) => (number) => parseFixed(number.toString(), decimals).toString();
|
|
|
|
describe("CRMonitor.js", function () {
|
|
before(async function () {
|
|
accounts = await web3.eth.getAccounts();
|
|
[owner, monitoredTrader, monitoredSponsor] = accounts;
|
|
});
|
|
|
|
TESTED_CONTRACT_VERSIONS.forEach(function (contractVersion) {
|
|
// Store the contractVersion.contractVersion, type and version being tested
|
|
iterationTestVersion = contractVersion;
|
|
|
|
const { getAbi, getBytecode } = require(getContractsNodePackageAliasForVerion(contractVersion.contractVersion));
|
|
|
|
const createContract = (name) => {
|
|
const abi = getAbi(name);
|
|
const bytecode = getBytecode(name);
|
|
return getContract(name, { abi, bytecode });
|
|
};
|
|
|
|
// Import the tested versions of contracts. note that financialContract is either an ExpiringMultiParty or a
|
|
// Perpetual depending on the current iteration version.
|
|
const FinancialContract = createContract(contractVersion.contractType);
|
|
const Finder = createContract("Finder");
|
|
const IdentifierWhitelist = createContract("IdentifierWhitelist");
|
|
const AddressWhitelist = createContract("AddressWhitelist");
|
|
const MockOracle = createContract("MockOracle");
|
|
const Token = createContract("ExpandedERC20");
|
|
const SyntheticToken = createContract("SyntheticToken");
|
|
const Timer = createContract("Timer");
|
|
const Store = createContract("Store");
|
|
const ConfigStore = createContract("ConfigStore");
|
|
const OptimisticOracle = createContract("OptimisticOracle");
|
|
const MulticallMock = createContract("MulticallMock");
|
|
|
|
for (let testConfig of TEST_DECIMAL_COMBOS) {
|
|
describe(`${testConfig.collateralDecimals} collateral, ${testConfig.syntheticDecimals} synthetic & ${testConfig.priceFeedDecimals} pricefeed decimals, for smart contract version ${contractVersion.contractType} @ ${contractVersion.contractVersion}`, function () {
|
|
before(async function () {
|
|
identifier = `${testConfig.tokenSymbol}TEST`;
|
|
fundingRateIdentifier = `${testConfig.tokenName}_FUNDING`;
|
|
convertDecimals = Convert(testConfig.collateralDecimals);
|
|
collateralToken = await Token.new(
|
|
testConfig.tokenSymbol + " Token",
|
|
testConfig.tokenSymbol,
|
|
testConfig.collateralDecimals
|
|
).send({ from: owner });
|
|
|
|
identifierWhitelist = await IdentifierWhitelist.new().send({ from: owner });
|
|
await identifierWhitelist.methods.addSupportedIdentifier(utf8ToHex(identifier)).send({ from: owner });
|
|
|
|
finder = await Finder.new().send({ from: owner });
|
|
timer = await Timer.new().send({ from: owner });
|
|
store = await Store.new({ rawValue: "0" }, { rawValue: "0" }, timer.options.address).send({ from: owner });
|
|
await finder.methods
|
|
.changeImplementationAddress(utf8ToHex(interfaceName.Store), store.options.address)
|
|
.send({ from: owner });
|
|
|
|
await finder.methods
|
|
.changeImplementationAddress(
|
|
utf8ToHex(interfaceName.IdentifierWhitelist),
|
|
identifierWhitelist.options.address
|
|
)
|
|
.send({ from: owner });
|
|
await identifierWhitelist.methods
|
|
.addSupportedIdentifier(utf8ToHex(testConfig.tokenSymbol + " Identifier"))
|
|
.send({ from: owner });
|
|
|
|
// Create a mockOracle and finder. Register the mockOracle with the finder.
|
|
mockOracle = await MockOracle.new(finder.options.address, timer.options.address).send({ from: owner });
|
|
const mockOracleInterfaceName = utf8ToHex(interfaceName.Oracle);
|
|
await finder.methods
|
|
.changeImplementationAddress(mockOracleInterfaceName, mockOracle.options.address)
|
|
.send({ from: owner });
|
|
|
|
collateralWhitelist = await AddressWhitelist.new().send({ from: owner });
|
|
await finder.methods
|
|
.changeImplementationAddress(
|
|
utf8ToHex(interfaceName.CollateralWhitelist),
|
|
collateralWhitelist.options.address
|
|
)
|
|
.send({ from: owner });
|
|
await collateralWhitelist.methods.addToWhitelist(collateralToken.options.address).send({ from: owner });
|
|
|
|
multicall = await MulticallMock.new().send({ from: owner });
|
|
});
|
|
|
|
beforeEach(async function () {
|
|
await timer.methods.setCurrentTime(startTime - 1).send({ from: owner });
|
|
currentTime = await mockOracle.methods.getCurrentTime().call();
|
|
|
|
// Create a new synthetic token
|
|
syntheticToken = await SyntheticToken.new(
|
|
"Test Synthetic Token",
|
|
"SYNTH",
|
|
testConfig.syntheticDecimals
|
|
).send({ from: owner });
|
|
|
|
// If we are testing a perpetual then we need to also deploy a config store, an optimistic oracle and set the funding rate identifier.
|
|
if (contractVersion.contractType == "Perpetual") {
|
|
configStore = await ConfigStore.new(
|
|
{
|
|
timelockLiveness: 86400, // 1 day
|
|
rewardRatePerSecond: { rawValue: "0" },
|
|
proposerBondPercentage: { rawValue: "0" },
|
|
maxFundingRate: { rawValue: toWei("0.00001") },
|
|
minFundingRate: { rawValue: toWei("-0.00001") },
|
|
proposalTimePastLimit: 0,
|
|
},
|
|
timer.options.address
|
|
).send({ from: owner });
|
|
|
|
await identifierWhitelist.methods
|
|
.addSupportedIdentifier(padRight(utf8ToHex(fundingRateIdentifier)))
|
|
.send({ from: owner });
|
|
optimisticOracle = await OptimisticOracle.new(7200, finder.options.address, timer.options.address).send({
|
|
from: owner,
|
|
});
|
|
await finder.methods
|
|
.changeImplementationAddress(utf8ToHex(interfaceName.OptimisticOracle), optimisticOracle.options.address)
|
|
.send({ from: owner });
|
|
}
|
|
|
|
const constructorParams = await createConstructorParamsForContractVersion(
|
|
contractVersion,
|
|
{
|
|
convertDecimals,
|
|
finder,
|
|
collateralToken,
|
|
syntheticToken,
|
|
identifier,
|
|
fundingRateIdentifier,
|
|
timer,
|
|
store,
|
|
configStore: configStore || {}, // if the contract type is not a perp this will be null.
|
|
},
|
|
{
|
|
minSponsorTokens: { rawValue: convertDecimals("1") },
|
|
collateralRequirement: { rawValue: toWei("1.5") },
|
|
withdrawalLiveness: "10",
|
|
liquidationLiveness: "10",
|
|
}
|
|
);
|
|
// Deploy a new expiring multi party OR perpetual, depending on the test version.
|
|
financialContract = await FinancialContract.new(constructorParams).send({ from: owner });
|
|
|
|
// If we are testing a perpetual then we need to apply the initial funding rate to start the timer.
|
|
await financialContract.methods.setCurrentTime(startTime).send({ from: owner });
|
|
|
|
// Create a sinon spy and give it to the SpyTransport as the winston logger. Use this to check all winston
|
|
// logs the correct text based on interactions with the financialContract. Note that only `info` level messages are captured.
|
|
spy = sinon.spy();
|
|
spyLogger = winston.createLogger({
|
|
level: "info",
|
|
transports: [new SpyTransport({ level: "info" }, { spy: spy })],
|
|
});
|
|
|
|
await syntheticToken.methods.addMinter(financialContract.options.address).send({ from: owner });
|
|
await syntheticToken.methods.addBurner(financialContract.options.address).send({ from: owner });
|
|
financialContractClient = new FinancialContractClient(
|
|
spyLogger,
|
|
FinancialContract.abi,
|
|
web3,
|
|
financialContract.options.address,
|
|
multicall.options.address,
|
|
testConfig.collateralDecimals,
|
|
testConfig.syntheticDecimals,
|
|
testConfig.priceFeedDecimals,
|
|
contractVersion.contractType
|
|
);
|
|
priceFeedMock = new PriceFeedMock();
|
|
|
|
monitorConfig = {
|
|
walletsToMonitor: [
|
|
{
|
|
name: "Monitored trader wallet",
|
|
address: monitoredTrader,
|
|
crAlert: 2.0, // if the collateralization ratio of this wallet drops below 200% send an alert
|
|
},
|
|
{
|
|
name: "Monitored sponsor wallet",
|
|
address: monitoredSponsor,
|
|
crAlert: 1.5, // if the collateralization ratio of this wallet drops below 150% send an alert
|
|
},
|
|
],
|
|
};
|
|
syntheticToken = await Token.at(await financialContract.methods.tokenCurrency().call());
|
|
|
|
financialContractProps = {
|
|
collateralSymbol: await collateralToken.methods.symbol().call(),
|
|
collateralDecimals: testConfig.collateralDecimals,
|
|
syntheticDecimals: testConfig.syntheticDecimals,
|
|
priceFeedDecimals: testConfig.priceFeedDecimals,
|
|
syntheticSymbol: await syntheticToken.methods.symbol().call(),
|
|
priceIdentifier: hexToUtf8(await financialContract.methods.priceIdentifier().call()),
|
|
networkId: await web3.eth.net.getId(),
|
|
};
|
|
|
|
crMonitor = new CRMonitor({
|
|
logger: spyLogger,
|
|
financialContractClient: financialContractClient,
|
|
priceFeed: priceFeedMock,
|
|
monitorConfig,
|
|
financialContractProps,
|
|
});
|
|
|
|
await collateralToken.methods.addMember(1, owner).send({ from: owner });
|
|
|
|
// Bulk mint and approve for all wallets
|
|
for (let i = 1; i < 3; i++) {
|
|
await collateralToken.methods.mint(accounts[i], convertDecimals("100000000")).send({ from: owner });
|
|
await collateralToken.methods
|
|
.approve(financialContract.options.address, convertDecimals("100000000"))
|
|
.send({ from: accounts[i] });
|
|
await syntheticToken.methods
|
|
.approve(financialContract.options.address, convertDecimals("100000000"))
|
|
.send({ from: accounts[i] });
|
|
}
|
|
|
|
// Create positions for the monitoredTrader and monitoredSponsor accounts
|
|
await financialContract.methods
|
|
.create({ rawValue: convertDecimals("250") }, { rawValue: convertDecimals("100") })
|
|
.send({ from: monitoredTrader });
|
|
await financialContract.methods
|
|
.create({ rawValue: convertDecimals("300") }, { rawValue: convertDecimals("100") })
|
|
.send({ from: monitoredSponsor });
|
|
});
|
|
|
|
versionedIt([{ contractType: "any", contractVersion: "any" }])(
|
|
"Winston correctly emits collateralization ratio message",
|
|
async function () {
|
|
// No messages created if safely above the CR threshold
|
|
await financialContractClient.update();
|
|
priceFeedMock.setCurrentPrice(toWei("1"));
|
|
await crMonitor.checkWalletCrRatio();
|
|
assert.equal(spy.callCount, 0);
|
|
|
|
// Emits a message if below the CR threshold. At a price of 1.3 only the monitoredTrader should be undercollateralized
|
|
// with a CR of 250 / (100 * 1.3) =1.923 which is below this addresses threshold of 200 and should emit a message.
|
|
await financialContractClient.update();
|
|
priceFeedMock.setCurrentPrice(toWei("1.3"));
|
|
await crMonitor.checkWalletCrRatio();
|
|
assert.equal(spy.callCount, 1);
|
|
assert.isTrue(lastSpyLogIncludes(spy, "Collateralization ratio alert"));
|
|
assert.isTrue(lastSpyLogIncludes(spy, "Monitored trader wallet")); // Monitored wallet name from `monitorConfig`
|
|
assert.isTrue(lastSpyLogIncludes(spy, `https://etherscan.io/address/${monitoredTrader}`)); // liquidator address
|
|
assert.isTrue(lastSpyLogIncludes(spy, `https://etherscan.io/address/${monitoredTrader}`)); // liquidator address
|
|
assert.isTrue(lastSpyLogIncludes(spy, "192.30%")); // calculated CR ratio for this position
|
|
assert.isTrue(lastSpyLogIncludes(spy, "200%")); // calculated CR ratio threshold for this address
|
|
assert.isTrue(lastSpyLogIncludes(spy, "1.30")); // Current price of the identifer
|
|
assert.isTrue(lastSpyLogIncludes(spy, hexToUtf8(await financialContract.methods.priceIdentifier().call()))); // Synthetic identifier
|
|
assert.isTrue(lastSpyLogIncludes(spy, "150.00%")); // Collateralization requirement
|
|
assert.isTrue(lastSpyLogIncludes(spy, "1.66")); // Liquidation price
|
|
assert.equal(lastSpyLogLevel(spy), "warn");
|
|
|
|
// The message should be sent every time the bot is polled and there is a crossing of the threshold line. At a price
|
|
// of 1.2 monitoredTrader's CR = 250/(100*1.2) = 2.083 and monitoredSponsor's CR = 300/(100*1.2) = 2.5 which places
|
|
// both monitored wallets above their thresholds. As a result no new message should be sent.
|
|
await financialContractClient.update();
|
|
priceFeedMock.setCurrentPrice(toWei("1.2"));
|
|
await crMonitor.checkWalletCrRatio();
|
|
assert.equal(spy.callCount, 1); // no new message.
|
|
|
|
// Crossing the price threshold for both sponsors should emit exactly 2 new messages. At a price of 2.1
|
|
// monitoredTrader's CR = 250/(100*2.1) = 1.1904 and monitoredSponsor's CR = 300/(100*2.1) = 1.42857. At these CRs
|
|
// Both bots are below their thresholds
|
|
await financialContractClient.update();
|
|
priceFeedMock.setCurrentPrice(toWei("2.1"));
|
|
await crMonitor.checkWalletCrRatio();
|
|
assert.equal(spy.callCount, 3); // two new messages
|
|
|
|
// A second check below this threshold should again trigger messages for both sponsors.
|
|
await financialContractClient.update();
|
|
priceFeedMock.setCurrentPrice(toWei("2.1"));
|
|
await crMonitor.checkWalletCrRatio();
|
|
assert.equal(spy.callCount, 5);
|
|
|
|
// Reset the price to over collateralized state for both accounts by moving the price into the lower value. This
|
|
// should not emit any events as both correctly collateralized.
|
|
await financialContractClient.update();
|
|
priceFeedMock.setCurrentPrice(toWei("1"));
|
|
await crMonitor.checkWalletCrRatio();
|
|
assert.equal(spy.callCount, 5);
|
|
|
|
// In addition to the price moving of the synthetic, adding/removing collateral or creating/redeeming debt can also
|
|
// impact a positions collateralization ratio. If monitoredTrader was to withdraw some collateral after waiting the
|
|
// withdrawal liveness they can place their position's collateralization under the threshold. Say monitoredTrader
|
|
// withdraws 75 units of collateral. Given price is 1 unit of synthetic for each unit of debt. This would place
|
|
// their position at a collateralization ratio of 175/(100*1)=1.75. monitoredSponsor is at 300/(100*1)=3.00.
|
|
await financialContract.methods
|
|
.requestWithdrawal({ rawValue: convertDecimals("75") })
|
|
.send({ from: monitoredTrader });
|
|
|
|
// The wallet CR should reflect the requested withdrawal amount.
|
|
await financialContractClient.update();
|
|
await crMonitor.checkWalletCrRatio();
|
|
await crMonitor.checkWalletCrRatio();
|
|
assert.equal(spy.callCount, 7); // a new message is sent.
|
|
assert.isTrue(lastSpyLogIncludes(spy, "Monitored trader wallet")); // Monitored wallet name from `MonitorConfig`
|
|
assert.isTrue(lastSpyLogIncludes(spy, `https://etherscan.io/address/${monitoredTrader}`)); // liquidator address
|
|
assert.isTrue(lastSpyLogIncludes(spy, `https://etherscan.io/address/${monitoredTrader}`)); // liquidator address
|
|
assert.isTrue(lastSpyLogIncludes(spy, "175.00%")); // calculated CR ratio for this position
|
|
assert.isTrue(lastSpyLogIncludes(spy, "200%")); // calculated CR ratio threshold for this address
|
|
assert.isTrue(lastSpyLogIncludes(spy, "1.00")); // Current price of the identifer
|
|
assert.isTrue(lastSpyLogIncludes(spy, hexToUtf8(await financialContract.methods.priceIdentifier().call()))); // Synthetic identifier
|
|
|
|
// Advance time after withdrawal liveness. Check that CR detected is the same post withdrawal execution
|
|
currentTime = await timer.methods.getCurrentTime().call();
|
|
await timer.methods.setCurrentTime(parseInt(currentTime) + 11).send({ from: owner });
|
|
await financialContract.methods.withdrawPassedRequest().send({ from: monitoredTrader });
|
|
|
|
await financialContractClient.update();
|
|
await crMonitor.checkWalletCrRatio();
|
|
assert.equal(spy.callCount, 8); // a new message is sent.
|
|
assert.isTrue(lastSpyLogIncludes(spy, "Monitored trader wallet")); // Monitored wallet name from `MonitorConfig`
|
|
assert.isTrue(lastSpyLogIncludes(spy, `https://etherscan.io/address/${monitoredTrader}`)); // liquidator address
|
|
assert.isTrue(lastSpyLogIncludes(spy, `https://etherscan.io/address/${monitoredTrader}`)); // liquidator address
|
|
assert.isTrue(lastSpyLogIncludes(spy, "175.00%")); // calculated CR ratio for this position
|
|
assert.isTrue(lastSpyLogIncludes(spy, "200%")); // calculated CR ratio threshold for this address
|
|
assert.isTrue(lastSpyLogIncludes(spy, "1.00")); // Current price of the identifer
|
|
assert.isTrue(lastSpyLogIncludes(spy, hexToUtf8(await financialContract.methods.priceIdentifier().call()))); // Synthetic identifier
|
|
}
|
|
);
|
|
versionedIt([{ contractType: "Perpetual", contractVersion: "2.0.1" }])(
|
|
"Winston correctly emits collateralization ratio message considering perpetual funding rates",
|
|
async function () {
|
|
// No messages created if safely above the CR threshold
|
|
await financialContractClient.update();
|
|
priceFeedMock.setCurrentPrice(toWei("1"));
|
|
await crMonitor.checkWalletCrRatio();
|
|
assert.equal(spy.callCount, 0);
|
|
|
|
// Emits a message if below the CR threshold. Introduce some funding rate multiplier to test the monitors ability
|
|
// the adjust alerts based on this input. Setting the funding rate multiplier to 1.1, results in modifying
|
|
// sponsor's debt. This becomes 100*1.1 = 110. Also, let's set the price to 1.1
|
|
// The sponsor CR is now 250 / (100 * 1.1 * 1.1) = 2.066
|
|
await _setFundingRateAndAdvanceTime(toWei("0.00001"));
|
|
|
|
// Note: no need to call `applyFundingRate()` on Perpetual contract because client should
|
|
// be able to use Multicall contract to simulate calling that and anticipating what the
|
|
// effective funding rate charge will be.
|
|
await financialContractClient.update();
|
|
assert.equal(financialContractClient.getLatestCumulativeFundingRateMultiplier(), toWei("1.1"));
|
|
priceFeedMock.setCurrentPrice(toWei("1.1"));
|
|
await crMonitor.checkWalletCrRatio();
|
|
assert.equal(spy.callCount, 0);
|
|
|
|
// Next, further add additional funding rate to push the sponsors position below the expected CR. Lets say
|
|
// another 0.05 is added onto the funding rate. The cumlative rate will become 1.1 * (1 + 0.000005 * 10000) = 1.155.
|
|
// This will place the sponsors CR at 250 / (100 * 1.155 * 1.1) = 1.9677 which is below the 2 alerting threshold.
|
|
await _setFundingRateAndAdvanceTime(toWei("0.000005"));
|
|
await financialContractClient.update();
|
|
assert.equal(financialContractClient.getLatestCumulativeFundingRateMultiplier(), toWei("1.155"));
|
|
|
|
await crMonitor.checkWalletCrRatio();
|
|
assert.equal(spy.callCount, 1);
|
|
assert.isTrue(lastSpyLogIncludes(spy, "Collateralization ratio alert"));
|
|
assert.isTrue(lastSpyLogIncludes(spy, "Monitored trader wallet")); // Monitored wallet name from `monitorConfig`
|
|
assert.isTrue(lastSpyLogIncludes(spy, `https://etherscan.io/address/${monitoredTrader}`)); // liquidator address
|
|
assert.isTrue(lastSpyLogIncludes(spy, `https://etherscan.io/address/${monitoredTrader}`)); // liquidator address
|
|
assert.isTrue(lastSpyLogIncludes(spy, "196.77%")); // calculated CR ratio for this position
|
|
assert.isTrue(lastSpyLogIncludes(spy, "200%")); // calculated CR ratio threshold for this address
|
|
assert.isTrue(lastSpyLogIncludes(spy, "1.10")); // Current price of the identifer
|
|
assert.isTrue(lastSpyLogIncludes(spy, hexToUtf8(await financialContract.methods.priceIdentifier().call()))); // Synthetic identifier
|
|
assert.isTrue(lastSpyLogIncludes(spy, "150.00%")); // Collateralization requirement
|
|
assert.isTrue(lastSpyLogIncludes(spy, "1.44")); // Liquidation price
|
|
assert.isTrue(lastSpyLogIncludes(spy, "cumulative funding rate multiplier is 1.15")); // correctly reports funding rate multiplier.
|
|
assert.equal(lastSpyLogLevel(spy), "warn");
|
|
}
|
|
);
|
|
versionedIt([{ contractType: "any", contractVersion: "any" }])("Cannot set invalid config", async function () {
|
|
let errorThrown1;
|
|
try {
|
|
// Create an invalid config. A valid config expects an array of objects with keys in the object of `name` `address`
|
|
// and `crAlert`.
|
|
const invalidMonitorConfig1 = {
|
|
// Config missing `crAlert`.
|
|
walletsToMonitor: [{ name: "Sponsor wallet", address: owner }],
|
|
};
|
|
|
|
crMonitor = new CRMonitor({
|
|
logger: spyLogger,
|
|
financialContractClient: financialContractClient,
|
|
priceFeed: priceFeedMock,
|
|
monitorConfig: invalidMonitorConfig1,
|
|
financialContractProps,
|
|
});
|
|
errorThrown1 = false;
|
|
} catch (err) {
|
|
errorThrown1 = true;
|
|
}
|
|
assert.isTrue(errorThrown1);
|
|
|
|
let errorThrown2;
|
|
try {
|
|
// Create an invalid config. A valid config expects an array of objects with keys in the object of `name` `address`
|
|
// `collateralThreshold`, `etherThreshold`. The value of `address` must be of type address.
|
|
const invalidMonitorConfig2 = {
|
|
// Config has an invalid address for the monitored bot.
|
|
walletsToMonitor: [{ name: "Sponsor wallet", address: "INVALID_ADDRESS", crAlert: 1.5 }],
|
|
};
|
|
|
|
crMonitor = new CRMonitor({
|
|
logger: spyLogger,
|
|
financialContractClient: financialContractClient,
|
|
priceFeed: priceFeedMock,
|
|
monitorConfig: invalidMonitorConfig2,
|
|
financialContractProps,
|
|
});
|
|
errorThrown2 = false;
|
|
} catch (err) {
|
|
errorThrown2 = true;
|
|
}
|
|
assert.isTrue(errorThrown2);
|
|
});
|
|
versionedIt([{ contractType: "any", contractVersion: "any" }])(
|
|
"Can correctly CR Monitor and check wallet CR Ratios with no config provided",
|
|
async function () {
|
|
const emptyConfig = {};
|
|
let errorThrown;
|
|
try {
|
|
crMonitor = new CRMonitor({
|
|
logger: spyLogger,
|
|
financialContractClient: financialContractClient,
|
|
priceFeed: priceFeedMock,
|
|
monitorConfig: emptyConfig,
|
|
financialContractProps,
|
|
});
|
|
await crMonitor.checkWalletCrRatio();
|
|
errorThrown = false;
|
|
} catch (err) {
|
|
errorThrown = true;
|
|
}
|
|
assert.isFalse(errorThrown);
|
|
}
|
|
);
|
|
versionedIt([{ contractType: "any", contractVersion: "any" }])(
|
|
"Can override the synthetic-threshold log level",
|
|
async function () {
|
|
const alertOverrideConfig = { ...monitorConfig, logOverrides: { crThreshold: "error" } };
|
|
crMonitor = new CRMonitor({
|
|
logger: spyLogger,
|
|
financialContractClient: financialContractClient,
|
|
priceFeed: priceFeedMock,
|
|
monitorConfig: alertOverrideConfig,
|
|
financialContractProps,
|
|
});
|
|
|
|
// Increase price to lower wallet CR below threshold
|
|
await financialContractClient.update();
|
|
priceFeedMock.setCurrentPrice(toWei("1.3"));
|
|
await crMonitor.checkWalletCrRatio();
|
|
assert.isTrue(lastSpyLogIncludes(spy, "Collateralization ratio alert"));
|
|
assert.equal(lastSpyLogLevel(spy), "error");
|
|
}
|
|
);
|
|
});
|
|
}
|
|
});
|
|
});
|