mirror of
https://github.com/luxfi/uma.git
synced 2026-10-30 15:14:44 +00:00
456 lines
20 KiB
JavaScript
456 lines
20 KiB
JavaScript
const { web3, getContract } = require("hardhat");
|
|
const { assert } = require("chai");
|
|
|
|
const { toWei, utf8ToHex, padRight } = web3.utils;
|
|
const {
|
|
interfaceName,
|
|
addGlobalHardhatTestingAddress,
|
|
createConstructorParamsForContractVersion,
|
|
TESTED_CONTRACT_VERSIONS,
|
|
getContractsNodePackageAliasForVerion,
|
|
} = require("@uma/common");
|
|
|
|
// Custom winston transport module to monitor winston log outputs
|
|
const winston = require("winston");
|
|
const sinon = require("sinon");
|
|
const { SpyTransport, spyLogLevel, spyLogIncludes, OptimisticOracleType } = require("@uma/financial-templates-lib");
|
|
|
|
// Script to test
|
|
const Poll = require("../index.js");
|
|
|
|
let collateralToken;
|
|
let syntheticToken;
|
|
let financialContract;
|
|
let store;
|
|
let timer;
|
|
let mockOracle;
|
|
let finder;
|
|
let identifierWhitelist;
|
|
let configStore;
|
|
let collateralWhitelist;
|
|
let optimisticOracle;
|
|
let skinnyOptimisticOracle;
|
|
|
|
let constructorParams;
|
|
let defaultMonitorConfig;
|
|
let defaultTokenPricefeedConfig;
|
|
let defaultMedianizerPricefeedConfig;
|
|
|
|
let spy;
|
|
let spyLogger;
|
|
|
|
let pollingDelay = 0; // 0 polling delay creates a serverless bot that yields after one full execution.
|
|
let fromBlock = 0; // setting the from block to 0 will query all historic logs events.
|
|
let toBlock = null; // setting the to block to 0 will query up to the latest block Number.
|
|
let errorRetries = 1; // setting execution re-tried to 0 will exit as soon as the process encounters an error.
|
|
let errorRetriesTimeout = 0.1; // 100 milliseconds between performing retries
|
|
let identifier = "TEST_IDENTIFIER";
|
|
let fundingRateIdentifier = "TEST_FUNDING";
|
|
|
|
describe("index.js", function () {
|
|
let accounts;
|
|
let contractCreator;
|
|
|
|
before(async function () {
|
|
accounts = await web3.eth.getAccounts();
|
|
[contractCreator] = accounts;
|
|
});
|
|
|
|
TESTED_CONTRACT_VERSIONS.forEach(function (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 the perp
|
|
// 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");
|
|
// Note: OptimisticOracle always uses "2.0.1"
|
|
const OptimisticOracle = getContract("OptimisticOracle");
|
|
const SkinnyOptimisticOracle = getContract("SkinnyOptimisticOracle");
|
|
|
|
describe(`Tests running on smart contract version ${contractVersion.contractType} @ ${contractVersion.contractVersion}`, function () {
|
|
before(async function () {
|
|
collateralToken = await Token.new("Wrapped Ether", "WETH", 18).send({ from: contractCreator });
|
|
|
|
identifierWhitelist = await IdentifierWhitelist.new().send({ from: contractCreator });
|
|
await identifierWhitelist.methods
|
|
.addSupportedIdentifier(utf8ToHex("TEST_IDENTIFIER"))
|
|
.send({ from: contractCreator });
|
|
await identifierWhitelist.methods.addSupportedIdentifier(utf8ToHex("ETH/BTC")).send({ from: contractCreator });
|
|
|
|
// Create identifier whitelist and register the price tracking ticker with it.
|
|
finder = await Finder.new().send({ from: contractCreator });
|
|
timer = await Timer.new().send({ from: contractCreator });
|
|
store = await Store.new({ rawValue: "0" }, { rawValue: "0" }, timer.options.address).send({
|
|
from: contractCreator,
|
|
});
|
|
await finder.methods
|
|
.changeImplementationAddress(utf8ToHex(interfaceName.Store), store.options.address)
|
|
.send({ from: contractCreator });
|
|
|
|
await finder.methods
|
|
.changeImplementationAddress(
|
|
utf8ToHex(interfaceName.IdentifierWhitelist),
|
|
identifierWhitelist.options.address
|
|
)
|
|
.send({ from: contractCreator });
|
|
await identifierWhitelist.methods
|
|
.addSupportedIdentifier(utf8ToHex("TEST_IDENTIFIER"))
|
|
.send({ from: contractCreator });
|
|
|
|
mockOracle = await MockOracle.new(finder.options.address, timer.options.address).send({
|
|
from: contractCreator,
|
|
});
|
|
await finder.methods
|
|
.changeImplementationAddress(utf8ToHex(interfaceName.Oracle), mockOracle.options.address)
|
|
.send({ from: contractCreator });
|
|
// Set the address in the global name space to enable disputer's index.js to access it.
|
|
addGlobalHardhatTestingAddress("Voting", mockOracle.options.address);
|
|
});
|
|
|
|
beforeEach(async function () {
|
|
// Create a sinon spy and give it to the SpyTransport as the winston logger. Use this to check all winston logs.
|
|
spy = sinon.spy(); // Create a new spy for each test.
|
|
spyLogger = winston.createLogger({
|
|
level: "debug",
|
|
transports: [new SpyTransport({ level: "debug" }, { spy: spy })],
|
|
});
|
|
spyLogger.isFlushed = true; // exit instantly when requested to do so.
|
|
|
|
// Create a new synthetic token
|
|
syntheticToken = await SyntheticToken.new("Test Synthetic Token", "SYNTH", 18).send({ from: contractCreator });
|
|
|
|
collateralWhitelist = await AddressWhitelist.new().send({ from: contractCreator });
|
|
await finder.methods
|
|
.changeImplementationAddress(
|
|
web3.utils.utf8ToHex(interfaceName.CollateralWhitelist),
|
|
collateralWhitelist.options.address
|
|
)
|
|
.send({ from: contractCreator });
|
|
await collateralWhitelist.methods
|
|
.addToWhitelist(collateralToken.options.address)
|
|
.send({ from: contractCreator });
|
|
|
|
optimisticOracle = await OptimisticOracle.new(7200, finder.options.address, timer.options.address).send({
|
|
from: contractCreator,
|
|
});
|
|
skinnyOptimisticOracle = await SkinnyOptimisticOracle.new(
|
|
7200,
|
|
finder.options.address,
|
|
timer.options.address
|
|
).send({ from: contractCreator });
|
|
await finder.methods
|
|
.changeImplementationAddress(utf8ToHex(interfaceName.OptimisticOracle), optimisticOracle.options.address)
|
|
.send({ from: contractCreator });
|
|
|
|
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: contractCreator });
|
|
await identifierWhitelist.methods
|
|
.addSupportedIdentifier(padRight(utf8ToHex(fundingRateIdentifier)))
|
|
.send({ from: contractCreator });
|
|
}
|
|
// Deploy a new expiring multi party OR perpetual.
|
|
constructorParams = await createConstructorParamsForContractVersion(
|
|
contractVersion,
|
|
{
|
|
convertDecimals: toWei, // These tests do not use convertSynthetic. Override this with toWei
|
|
finder,
|
|
collateralToken,
|
|
syntheticToken,
|
|
identifier,
|
|
fundingRateIdentifier,
|
|
timer,
|
|
store,
|
|
configStore: configStore || {}, // if the contract type is not a perp this will be null.
|
|
},
|
|
// Note: an identifier which is part of the default config is required for this test.
|
|
{ priceFeedIdentifier: padRight(utf8ToHex("ETH/BTC"), 64) }
|
|
);
|
|
financialContract = await FinancialContract.new(constructorParams).send({ from: contractCreator });
|
|
await syntheticToken.methods.addMinter(financialContract.options.address).send({ from: contractCreator });
|
|
await syntheticToken.methods.addBurner(financialContract.options.address).send({ from: contractCreator });
|
|
|
|
defaultMonitorConfig = {};
|
|
defaultTokenPricefeedConfig = { type: "test", currentPrice: "1", historicalPrice: "1" };
|
|
defaultMedianizerPricefeedConfig = {};
|
|
});
|
|
|
|
it("FinancialContract monitor: Completes one iteration without logging any errors", async function () {
|
|
// Once specifying just the `financialContractAddress`, once specifying
|
|
// the `optimisticOracleAddress`, and once specifying both:
|
|
await Poll.run({
|
|
logger: spyLogger,
|
|
web3,
|
|
financialContractAddress: financialContract.options.address,
|
|
pollingDelay,
|
|
errorRetries,
|
|
errorRetriesTimeout,
|
|
startingBlock: fromBlock,
|
|
endingBlock: toBlock,
|
|
monitorConfig: defaultMonitorConfig,
|
|
tokenPriceFeedConfig: defaultTokenPricefeedConfig,
|
|
medianizerPriceFeedConfig: defaultMedianizerPricefeedConfig,
|
|
});
|
|
for (let i = 0; i < spy.callCount; i++) {
|
|
assert.notEqual(spyLogLevel(spy, i), "error");
|
|
}
|
|
});
|
|
it("OptimisticOracle monitor: Completes one iteration without logging any errors", async function () {
|
|
await Poll.run({
|
|
logger: spyLogger,
|
|
web3,
|
|
optimisticOracleAddress: optimisticOracle.options.address,
|
|
optimisticOracleType: OptimisticOracleType.OptimisticOracle,
|
|
pollingDelay,
|
|
errorRetries,
|
|
errorRetriesTimeout,
|
|
startingBlock: fromBlock,
|
|
endingBlock: toBlock,
|
|
monitorConfig: defaultMonitorConfig,
|
|
tokenPriceFeedConfig: defaultTokenPricefeedConfig,
|
|
medianizerPriceFeedConfig: defaultMedianizerPricefeedConfig,
|
|
});
|
|
for (let i = 0; i < spy.callCount; i++) {
|
|
assert.notEqual(spyLogLevel(spy, i), "error");
|
|
}
|
|
assert.equal(spy.getCall(0).lastArg.optimisticOracleType, OptimisticOracleType.OptimisticOracle);
|
|
});
|
|
it("SkinnyOptimisticOracle monitor: Completes one iteration without logging any errors", async function () {
|
|
await Poll.run({
|
|
logger: spyLogger,
|
|
web3,
|
|
optimisticOracleAddress: skinnyOptimisticOracle.options.address,
|
|
optimisticOracleType: OptimisticOracleType.SkinnyOptimisticOracle,
|
|
pollingDelay,
|
|
errorRetries,
|
|
errorRetriesTimeout,
|
|
startingBlock: fromBlock,
|
|
endingBlock: toBlock,
|
|
monitorConfig: defaultMonitorConfig,
|
|
tokenPriceFeedConfig: defaultTokenPricefeedConfig,
|
|
medianizerPriceFeedConfig: defaultMedianizerPricefeedConfig,
|
|
});
|
|
for (let i = 0; i < spy.callCount; i++) {
|
|
assert.notEqual(spyLogLevel(spy, i), "error");
|
|
}
|
|
assert.equal(spy.getCall(0).lastArg.optimisticOracleType, OptimisticOracleType.SkinnyOptimisticOracle);
|
|
});
|
|
it("Activating all monitors: Completes one iteration without logging any errors", async function () {
|
|
await Poll.run({
|
|
logger: spyLogger,
|
|
web3,
|
|
financialContractAddress: financialContract.options.address,
|
|
optimisticOracleAddress: optimisticOracle.options.address,
|
|
optimisticOracleType: OptimisticOracleType.OptimisticOracle,
|
|
pollingDelay,
|
|
errorRetries,
|
|
errorRetriesTimeout,
|
|
startingBlock: fromBlock,
|
|
endingBlock: toBlock,
|
|
monitorConfig: defaultMonitorConfig,
|
|
tokenPriceFeedConfig: defaultTokenPricefeedConfig,
|
|
medianizerPriceFeedConfig: defaultMedianizerPricefeedConfig,
|
|
});
|
|
for (let i = 0; i < spy.callCount; i++) {
|
|
assert.notEqual(spyLogLevel(spy, i), "error");
|
|
}
|
|
});
|
|
it("Detects price feed, collateral and synthetic decimals", async function () {
|
|
spy = sinon.spy(); // Create a new spy for each test.
|
|
spyLogger = winston.createLogger({
|
|
level: "debug",
|
|
transports: [new SpyTransport({ level: "debug" }, { spy: spy })],
|
|
});
|
|
spyLogger.isFlushed = true; // exit instantly when requested to do so.
|
|
|
|
collateralToken = await Token.new("USDC", "USDC", 6).send({ from: contractCreator });
|
|
syntheticToken = await SyntheticToken.new("Test Synthetic Token", "SYNTH", 6).send({ from: contractCreator });
|
|
constructorParams = {
|
|
...constructorParams,
|
|
collateralAddress: collateralToken.options.address,
|
|
tokenAddress: syntheticToken.options.address,
|
|
};
|
|
financialContract = await FinancialContract.new(constructorParams).send({ from: contractCreator });
|
|
await syntheticToken.methods.addMinter(financialContract.options.address);
|
|
await syntheticToken.methods.addBurner(financialContract.options.address);
|
|
|
|
await Poll.run({
|
|
logger: spyLogger,
|
|
web3,
|
|
financialContractAddress: financialContract.options.address,
|
|
pollingDelay,
|
|
errorRetries,
|
|
errorRetriesTimeout,
|
|
startingBlock: fromBlock,
|
|
endingBlock: toBlock,
|
|
monitorConfig: defaultMonitorConfig,
|
|
tokenPriceFeedConfig: defaultTokenPricefeedConfig,
|
|
medianizerPriceFeedConfig: defaultMedianizerPricefeedConfig,
|
|
});
|
|
|
|
for (let i = 0; i < spy.callCount; i++) {
|
|
assert.notEqual(spyLogLevel(spy, i), "error");
|
|
}
|
|
|
|
// Third log, which prints the decimal info, should include # of decimals for the price feed, collateral and synthetic
|
|
assert.isTrue(spyLogIncludes(spy, 9, '"collateralDecimals":6'));
|
|
assert.isTrue(spyLogIncludes(spy, 9, '"syntheticDecimals":6'));
|
|
assert.isTrue(spyLogIncludes(spy, 9, '"priceFeedDecimals":18'));
|
|
});
|
|
it("Correctly detects contract type and rejects unknown contract types", async function () {
|
|
spy = sinon.spy();
|
|
spyLogger = winston.createLogger({
|
|
level: "debug",
|
|
transports: [new SpyTransport({ level: "debug" }, { spy: spy })],
|
|
});
|
|
spyLogger.isFlushed = true; // exit instantly when requested to do so.
|
|
|
|
await Poll.run({
|
|
logger: spyLogger,
|
|
web3,
|
|
financialContractAddress: financialContract.options.address,
|
|
pollingDelay,
|
|
errorRetries,
|
|
errorRetriesTimeout,
|
|
startingBlock: fromBlock,
|
|
endingBlock: toBlock,
|
|
monitorConfig: defaultMonitorConfig,
|
|
tokenPriceFeedConfig: defaultTokenPricefeedConfig,
|
|
medianizerPriceFeedConfig: defaultMedianizerPricefeedConfig,
|
|
});
|
|
|
|
for (let i = 0; i < spy.callCount; i++) {
|
|
assert.notEqual(spyLogLevel(spy, i), "error");
|
|
}
|
|
|
|
// To verify contract type detection is correct for a standard feed, check the 9th log to see it matches expected.
|
|
assert.isTrue(spyLogIncludes(spy, 9, `"contractVersion":"${contractVersion.contractVersion}"`));
|
|
assert.isTrue(spyLogIncludes(spy, 9, `"contractType":"${contractVersion.contractType}"`));
|
|
});
|
|
it("Correctly rejects unknown contract types", async function () {
|
|
// Should produce an error on a contract type that is unknown. set the financialContract as the finder, for example
|
|
spy = sinon.spy();
|
|
spyLogger = winston.createLogger({
|
|
level: "debug",
|
|
transports: [new SpyTransport({ level: "debug" }, { spy: spy })],
|
|
});
|
|
spyLogger.isFlushed = true; // exit instantly when requested to do so.
|
|
|
|
let didThrowError = false;
|
|
let errorString;
|
|
try {
|
|
await Poll.run({
|
|
logger: spyLogger,
|
|
web3,
|
|
financialContractAddress: finder.options.address,
|
|
pollingDelay,
|
|
errorRetries,
|
|
errorRetriesTimeout,
|
|
startingBlock: fromBlock,
|
|
endingBlock: toBlock,
|
|
monitorConfig: defaultMonitorConfig,
|
|
tokenPriceFeedConfig: defaultTokenPricefeedConfig,
|
|
medianizerPriceFeedConfig: defaultMedianizerPricefeedConfig,
|
|
});
|
|
} catch (error) {
|
|
errorString = error.toString();
|
|
didThrowError = true;
|
|
}
|
|
|
|
assert.isTrue(didThrowError);
|
|
assert.isTrue(errorString.includes("Contract version specified or inferred is not supported by this bot"));
|
|
});
|
|
|
|
it("Correctly re-tries after failed execution loop", async function () {
|
|
// To validate re-try logic this test needs to get the monitor bot to throw within the main while loop. This is
|
|
// not straightforward as the bot is designed to reject invalid configs before getting to the while loop. Once in the
|
|
// while loop it should never throw errors as it gracefully falls over with situations like timed out API calls.
|
|
// One way to induce an error is to give the bot a Financial Contract contract that can get through the initial checks but fails
|
|
// when running any specific calls on the contracts. To do this we can create a Financial Contract that is only the financialContract
|
|
// and excludes any liquidation logic. As a result, calling `getLiquidations` in the Financial Contract contract will error out.
|
|
|
|
// Need to give an unknown identifier to get past the `createReferencePriceFeedForFinancialContract` & `createUniswapPriceFeedForFinancialContract`
|
|
await identifierWhitelist.methods.addSupportedIdentifier(utf8ToHex("UNKNOWN")).send({ from: contractCreator });
|
|
|
|
const PricelessPositionManager = getContract("PricelessPositionManager");
|
|
const invalidFinancialContract = await PricelessPositionManager.new(
|
|
constructorParams.expirationTimestamp,
|
|
constructorParams.withdrawalLiveness,
|
|
constructorParams.collateralAddress,
|
|
constructorParams.tokenAddress,
|
|
constructorParams.finderAddress,
|
|
utf8ToHex("UNKNOWN"),
|
|
constructorParams.minSponsorTokens,
|
|
constructorParams.timerAddress,
|
|
constructorParams.excessTokenBeneficiary
|
|
).send({ from: contractCreator });
|
|
|
|
// Create a spy logger to catch all log messages to validate re-try attempts.
|
|
spyLogger = winston.createLogger({
|
|
level: "debug",
|
|
transports: [new SpyTransport({ level: "debug" }, { spy: spy })],
|
|
});
|
|
spyLogger.isFlushed = true; // exit instantly when requested to do so.
|
|
|
|
errorRetries = 3; // set execution retries to 3 to validate.
|
|
// Note both the token and medanizer price feeds are the same config. This is done so that createReferencePriceFeedForFinancialContract
|
|
// can pass without trying to poll any information on the invalidFinancialContract to ensure that the bot gets into the main while
|
|
// loop without throwing an error in inital set-up. If this left as defaultMedianizerPricefeedConfig (which is blank)
|
|
// The bot will error out in setting up the price feed as the invalidFinancialContract instance cant be queried for `liquidationLiveness`
|
|
// which is required when initalizing the price feed.
|
|
|
|
let errorThrown = false;
|
|
try {
|
|
await Poll.run({
|
|
logger: spyLogger,
|
|
web3,
|
|
financialContractAddress: invalidFinancialContract.options.address,
|
|
pollingDelay,
|
|
errorRetries: errorRetries,
|
|
errorRetriesTimeout,
|
|
startingBlock: fromBlock,
|
|
endingBlock: toBlock,
|
|
monitorConfig: { ...defaultMonitorConfig, contractVersion: "2.0.1", contractType: "ExpiringMultiParty" },
|
|
tokenPriceFeedConfig: defaultTokenPricefeedConfig,
|
|
medianizerPriceFeedConfig: defaultTokenPricefeedConfig,
|
|
});
|
|
} catch (error) {
|
|
errorThrown = true;
|
|
}
|
|
// Iterate over all log events and count the number of
|
|
// execution loop errors.
|
|
let reTryCounts = { executionLoopErrors: 0 };
|
|
for (let i = 0; i < spy.callCount; i++) {
|
|
if (spyLogIncludes(spy, i, "An error was thrown in the execution loop")) reTryCounts.executionLoopErrors += 1;
|
|
}
|
|
|
|
assert.equal(reTryCounts.executionLoopErrors, 3); // Each re-try create a log. These only occur on re-try and so expect 3 logs.
|
|
assert.isTrue(errorThrown); // An error should have been thrown after the 3 execution re-tries.
|
|
});
|
|
}).timeout(10000);
|
|
});
|
|
});
|