Files
2023-05-02 08:21:52 +01:00

294 lines
11 KiB
TypeScript

import { addGlobalHardhatTestingAddress, ZERO_ADDRESS } from "@uma/common";
import {
AddressWhitelistEthers,
FinderEthers,
IdentifierWhitelistEthers,
OptimisticOracleV2Ethers,
VotingTokenEthers,
} from "@uma/contracts-node";
import { createNewLogger, spyLogIncludes, spyLogLevel, SpyTransport } from "@uma/financial-templates-lib";
import { assert } from "chai";
import sinon from "sinon";
import * as commonModule from "../src/monitor-polymarket/common";
import { MonitoringParams, TradeInformation } from "../src/monitor-polymarket/common";
import { monitorTransactionsProposedOrderBook } from "../src/monitor-polymarket/MonitorProposalsOrderBook";
import { umaEcosystemFixture } from "./fixtures/UmaEcosystem.Fixture";
import { formatBytes32String, getContractFactory, hre, Provider, Signer } from "./utils";
const ethers = hre.ethers;
describe("PolymarketNotifier", function () {
let oov2: OptimisticOracleV2Ethers;
let deployer: Signer;
const indentifier = formatBytes32String("TEST_IDENTIFIER");
const mockData: any[] = [
{
resolvedBy: "",
questionID: "",
createdAt: "2023-01-30 19:06:23.106+00",
question: "Will NATO expand by June 30?",
outcomes: ["Yes", "No"],
outcomePrices: ["1", "0"],
liquidityNum: 2499.29,
volumeNum: 29198.55,
clobTokenIds: [
"41909194383884489857051533149267887932706513238474711698327450766299364965897",
"46296129944740145134028887886488800558338482961897658822042889421944724008114",
],
orderFilledEvents: [[], []],
ancillaryData: "0x123",
txHash: "0xffc94945ccdfced83fb9c4ea3aaecd145130f7d4065f78656b05f4afc33d6e06",
requester: "0x6A9D222616C90FcA5754cd1333cFD9b7fb6a4F74",
proposer: "0xdebA18a1CD3fd1c8FE74640d3959bA03AC7DB5f3",
timestamp: 1675106721,
eventTimestamp: 1680615675,
eventBlockNumber: 41135219,
expirationTimestamp: 1680622875,
proposalTimestamp: 1680615675,
identifier: "0x5945535f4f525f4e4f5f51554552590000000000000000000000000000000000",
eventIndex: 366,
},
];
// Create monitoring params for single block to pass to monitor modules.
const createMonitoringParams = async (): Promise<MonitoringParams> => {
const binaryAdapterAddress = "dummy";
const ctfAdapterAddress = "dummy";
const ctfExchangeAddress = "dummy";
const graphqlEndpoint = "dummy";
const apiEndpoint = "dummy";
return {
binaryAdapterAddress,
ctfAdapterAddress,
ctfExchangeAddress,
maxBlockLookBack: 3499,
graphqlEndpoint,
apiEndpoint,
provider: ethers.provider as Provider,
chainId: (await ethers.provider.getNetwork()).chainId,
pollingDelay: 0,
};
};
beforeEach(async function () {
// Signer from ethers and hardhat-ethers are not version compatible, thus, we cannot use the SignerWithAddress.
[deployer] = (await ethers.getSigners()) as Signer[];
// Get contract instances.
const { finder, votingToken, identifierWhitelist, collateralWhitelist } = (await umaEcosystemFixture()) as {
votingToken: VotingTokenEthers;
finder: FinderEthers;
identifierWhitelist: IdentifierWhitelistEthers;
collateralWhitelist: AddressWhitelistEthers;
};
const defaultLiveness = 7200; // 2 hours
const oo = (await (await getContractFactory("OptimisticOracle", deployer)).deploy(
defaultLiveness,
finder.address,
ZERO_ADDRESS
)) as OptimisticOracleV2Ethers;
oov2 = (await (await getContractFactory("OptimisticOracleV2", deployer)).deploy(
defaultLiveness,
finder.address,
ZERO_ADDRESS
)) as OptimisticOracleV2Ethers;
const multicall = await (await getContractFactory("Multicall3", deployer)).deploy();
addGlobalHardhatTestingAddress("OptimisticOracle", oo.address);
addGlobalHardhatTestingAddress("OptimisticOracleV2", oov2.address);
addGlobalHardhatTestingAddress("Multicall3", multicall.address);
await (await identifierWhitelist.addSupportedIdentifier(indentifier)).wait();
await (await collateralWhitelist.addToWhitelist(votingToken.address)).wait();
// clear all mocks
sinon.restore();
const getNotifiedProposalsMock = sinon.stub();
getNotifiedProposalsMock.returns({});
sinon.stub(commonModule, "getNotifiedProposals").callsFake(getNotifiedProposalsMock);
const storeNotifiedProposalsMock = sinon.stub();
storeNotifiedProposalsMock.returns({});
sinon.stub(commonModule, "storeNotifiedProposals").callsFake(storeNotifiedProposalsMock);
});
it("It should notify if there are orders over the threshold", async function () {
mockData[0].orderBooks = [
{
bids: [],
asks: [],
},
{
bids: [],
asks: [
{
price: 0.9,
size: 100,
},
],
},
];
const mockDataFunction = sinon.stub();
mockDataFunction.returns(mockData);
sinon.stub(commonModule, "getPolymarketMarkets").callsFake(mockDataFunction);
sinon.stub(commonModule, "getMarketsAncillary").callsFake(mockDataFunction);
sinon.stub(commonModule, "getPolymarketOrderBooks").callsFake(mockDataFunction);
sinon.stub(commonModule, "getOrderFilledEvents").callsFake(mockDataFunction);
// Call monitorAssertions directly for the block when the assertion was made.
const spy = sinon.spy();
const spyLogger = createNewLogger([new SpyTransport({}, { spy: spy })]);
await monitorTransactionsProposedOrderBook(spyLogger, await createMonitoringParams());
// The spy should have been called as the order book is not empty.
assert.equal(spy.callCount, 1);
assert.equal(spy.getCall(0).lastArg.at, "PolymarketMonitor");
assert.equal(spy.getCall(0).lastArg.message, "Difference between proposed price and market signal! 🚨");
assert.equal(spyLogLevel(spy, 0), "error");
assert.isTrue(
spyLogIncludes(spy, 0, ` Someone is trying to sell 100 winner outcome tokens at a price of 0.9 on the orderbook.`)
); // price
assert.equal(spy.getCall(0).lastArg.notificationPath, "polymarket-notifier");
});
it("It should not notify if order book is empty", async function () {
mockData[0].proposedPrice = "1.0";
mockData[0].orderBooks = [
{
bids: [],
asks: [],
},
{
bids: [],
asks: [],
},
];
const mockDataFunction = sinon.stub();
mockDataFunction.returns(mockData);
sinon.stub(commonModule, "getPolymarketMarkets").callsFake(mockDataFunction);
sinon.stub(commonModule, "getMarketsAncillary").callsFake(mockDataFunction);
sinon.stub(commonModule, "getPolymarketOrderBooks").callsFake(mockDataFunction);
sinon.stub(commonModule, "getOrderFilledEvents").callsFake(mockDataFunction);
// Call monitorAssertions directly for the block when the assertion was made.
const spy = sinon.spy();
const spyLogger = createNewLogger([new SpyTransport({}, { spy: spy })]);
await monitorTransactionsProposedOrderBook(spyLogger, await createMonitoringParams());
// The spy should not have been called as the order book is empty.
assert.equal(spy.callCount, 0);
});
it("It should notify if there are sell trades over the threshold", async function () {
mockData[0].proposedPrice = "1.0";
mockData[0].orderBooks = [
{
bids: [],
asks: [],
},
{
bids: [],
asks: [],
},
];
mockData[0].orderFilledEvents = [
[
{
price: 0.9,
amount: 100,
timestamp: 123,
type: "sell",
},
] as TradeInformation[],
[],
];
const mockDataFunction = sinon.stub();
mockDataFunction.returns(mockData);
sinon.stub(commonModule, "getPolymarketMarkets").callsFake(mockDataFunction);
sinon.stub(commonModule, "getMarketsAncillary").callsFake(mockDataFunction);
sinon.stub(commonModule, "getPolymarketOrderBooks").callsFake(mockDataFunction);
sinon.stub(commonModule, "getOrderFilledEvents").callsFake(mockDataFunction);
// Call monitorAssertions directly for the block when the assertion was made.
const spy = sinon.spy();
const spyLogger = createNewLogger([new SpyTransport({}, { spy: spy })]);
await monitorTransactionsProposedOrderBook(spyLogger, await createMonitoringParams());
// The spy should have been called as the order book is not empty.
assert.equal(spy.callCount, 1);
assert.equal(spy.getCall(0).lastArg.at, "PolymarketMonitor");
assert.equal(spy.getCall(0).lastArg.message, "Difference between proposed price and market signal! 🚨");
assert.equal(spyLogLevel(spy, 0), "error");
assert.isTrue(
spy
.getCall(0)
.toString()
.includes(
` Someone has already sold winner outcome tokens at a price below the threshold. These are the trades: ${JSON.stringify(
mockData[0].orderFilledEvents[0]
)}`
)
); // price
assert.equal(spy.getCall(0).lastArg.notificationPath, "polymarket-notifier");
});
it("It should notify if there are sell trades over the threshold", async function () {
mockData[0].proposedPrice = "1.0";
mockData[0].orderBooks = [
{
bids: [],
asks: [],
},
{
bids: [],
asks: [],
},
];
mockData[0].orderFilledEvents = [
[],
[
{
price: 0.9,
amount: 100,
timestamp: 123,
type: "buy",
},
] as TradeInformation[],
];
const mockDataFunction = sinon.stub();
mockDataFunction.returns(mockData);
sinon.stub(commonModule, "getPolymarketMarkets").callsFake(mockDataFunction);
sinon.stub(commonModule, "getMarketsAncillary").callsFake(mockDataFunction);
sinon.stub(commonModule, "getPolymarketOrderBooks").callsFake(mockDataFunction);
sinon.stub(commonModule, "getOrderFilledEvents").callsFake(mockDataFunction);
// Call monitorAssertions directly for the block when the assertion was made.
const spy = sinon.spy();
const spyLogger = createNewLogger([new SpyTransport({}, { spy: spy })]);
await monitorTransactionsProposedOrderBook(spyLogger, await createMonitoringParams());
// The spy should have been called as the order book is not empty.
assert.equal(spy.callCount, 1);
assert.equal(spy.getCall(0).lastArg.at, "PolymarketMonitor");
assert.equal(spy.getCall(0).lastArg.message, "Difference between proposed price and market signal! 🚨");
assert.equal(spyLogLevel(spy, 0), "error");
assert.isTrue(
spy
.getCall(0)
.toString()
.includes(
` Someone has already bought loser outcome tokens at a price above the threshold. These are the trades: ${JSON.stringify(
mockData[0].orderFilledEvents[1]
)}`
)
); // price
assert.equal(spy.getCall(0).lastArg.notificationPath, "polymarket-notifier");
});
});