Files
2023-03-06 14:47:02 +02:00

442 lines
21 KiB
TypeScript

import {
EmergencyProposerEthers,
GovernorV2Ethers,
IdentifierWhitelistEthers,
ProposerV2Ethers,
RegistryEthers,
VotingTokenEthers,
VotingV2Ethers,
} from "@uma/contracts-node";
import { createNewLogger, spyLogIncludes, spyLogLevel, SpyTransport } from "@uma/financial-templates-lib";
import { assert } from "chai";
import sinon from "sinon";
import { emergencyQuorum, governanceProposalBond, maxRolls, minimumWaitTime, phaseLength } from "./constants";
import {
formatBytes32String,
getBlockNumberFromTx,
hardhatTime,
hre,
parseBytes32String,
parseUnits,
Provider,
Signer,
toUtf8Bytes,
toUtf8String,
} from "./utils";
import { dvm2Fixture } from "./fixtures/DVM2.Fixture";
import { umaEcosystemFixture } from "./fixtures/UmaEcosystem.Fixture";
import { MonitoringParams, BotModes } from "../src/monitor-dvm/common";
import { monitorDeletion } from "../src/monitor-dvm/MonitorDeletion";
import { monitorEmergency } from "../src/monitor-dvm/MonitorEmergency";
import { monitorGovernance } from "../src/monitor-dvm/MonitorGovernance";
import { monitorGovernorTransfers } from "../src/monitor-dvm/MonitorGovernorTransfers";
import { monitorMints } from "../src/monitor-dvm/MonitorMints";
import { monitorRolled } from "../src/monitor-dvm/MonitorRolled";
import { monitorStakes } from "../src/monitor-dvm/MonitorStakes";
import { monitorUnstakes } from "../src/monitor-dvm/MonitorUnstakes";
const ethers = hre.ethers;
// Create default monitoring params for single block to pass to monitor modules.
const createMonitoringParams = async (blockNumber: number): Promise<MonitoringParams> => {
// Bot modes are not used as we are calling monitor modules directly.
const botModes: BotModes = {
unstakesEnabled: false,
stakesEnabled: false,
governanceEnabled: false,
deletionEnabled: false,
emergencyEnabled: false,
rolledEnabled: false,
governorTransfersEnabled: false,
mintsEnabled: false,
};
return {
provider: ethers.provider as Provider,
chainId: (await ethers.provider.getNetwork()).chainId,
blockRange: { start: blockNumber, end: blockNumber },
pollingDelay: 0,
botModes,
unstakeThreshold: parseUnits("0"),
stakeThreshold: parseUnits("0"),
governorTransfersThreshold: parseUnits("0"),
mintsThreshold: parseUnits("0"),
};
};
describe("DVMMonitor", function () {
let votingToken: VotingTokenEthers;
let votingV2: VotingV2Ethers;
let governorV2: GovernorV2Ethers;
let proposerV2: ProposerV2Ethers;
let emergencyProposer: EmergencyProposerEthers;
let registry: RegistryEthers;
let identifierWhitelist: IdentifierWhitelistEthers;
let deployer: Signer;
let staker: Signer;
let proposer: Signer;
let requester: Signer;
let deployerAddress: string;
let stakerAddress: string;
let proposerAddress: string;
let requesterAddress: string;
beforeEach(async function () {
// Signer from ethers and hardhat-ethers are not version compatible, thus, we cannot use the SignerWithAddress.
[deployer, staker, proposer, requester] = (await ethers.getSigners()) as Signer[];
deployerAddress = await deployer.getAddress();
stakerAddress = await staker.getAddress();
proposerAddress = await proposer.getAddress();
requesterAddress = await requester.getAddress();
// Get contract instances.
const umaEcosystemContracts = await umaEcosystemFixture();
votingToken = umaEcosystemContracts.votingToken;
registry = umaEcosystemContracts.registry;
identifierWhitelist = umaEcosystemContracts.identifierWhitelist;
const dvm2Contracts = await dvm2Fixture();
votingV2 = dvm2Contracts.votingV2;
governorV2 = dvm2Contracts.governorV2;
proposerV2 = dvm2Contracts.proposerV2;
emergencyProposer = dvm2Contracts.emergencyProposer;
});
it("Monitor unstake", async function () {
const stakeAmount = parseUnits("100");
// Fund staker with voting tokens to stake.
await votingToken.transfer(await stakerAddress, stakeAmount);
await votingToken.connect(staker).approve(votingV2.address, stakeAmount);
await votingV2.connect(staker).stake(stakeAmount);
// Request unstake.
const unstakeTx = await votingV2.connect(staker).requestUnstake(stakeAmount);
const unstakeBlockNumber = await getBlockNumberFromTx(unstakeTx);
// Call monitorUnstakes directly for the block when the unstake request was made.
const spy = sinon.spy();
const spyLogger = createNewLogger([new SpyTransport({}, { spy: spy })]);
await monitorUnstakes(spyLogger, await createMonitoringParams(unstakeBlockNumber));
// When calling monitoring module directly there should be only one log (index 0) with the unstake caught by spy.
assert.equal(spy.getCall(0).lastArg.at, "DVMMonitor");
assert.equal(spy.getCall(0).lastArg.message, "Large unstake requested 😟");
assert.equal(spyLogLevel(spy, 0), "warn");
assert.isTrue(spyLogIncludes(spy, 0, stakerAddress));
assert.isTrue(spyLogIncludes(spy, 0, unstakeTx.hash));
});
it("Monitor unstake below threshold", async function () {
const stakeAmount = parseUnits("100");
// Fund staker with voting tokens to stake.
await votingToken.transfer(await stakerAddress, stakeAmount);
await votingToken.connect(staker).approve(votingV2.address, stakeAmount);
await votingV2.connect(staker).stake(stakeAmount);
// Request unstake.
const unstakeTx = await votingV2.connect(staker).requestUnstake(stakeAmount);
const unstakeBlockNumber = await getBlockNumberFromTx(unstakeTx);
// Call monitorUnstakes directly for the block when the unstake request was made.
const spy = sinon.spy();
const spyLogger = createNewLogger([new SpyTransport({}, { spy: spy })]);
await monitorUnstakes(spyLogger, {
...(await createMonitoringParams(unstakeBlockNumber)),
unstakeThreshold: stakeAmount.add("1"),
});
// When calling monitoring module directly there should be no logs.
assert.equal(spy.callCount, 0);
});
it("Monitor stake", async function () {
const stakeAmount = parseUnits("100");
// Fund staker with voting tokens to stake.
await votingToken.transfer(await stakerAddress, stakeAmount);
await votingToken.connect(staker).approve(votingV2.address, stakeAmount);
const stakeTx = await votingV2.connect(staker).stake(stakeAmount);
const stakeBlockNumber = await getBlockNumberFromTx(stakeTx);
// Call monitorStakes directly for the block when the stake was made.
const spy = sinon.spy();
const spyLogger = createNewLogger([new SpyTransport({}, { spy: spy })]);
await monitorStakes(spyLogger, await createMonitoringParams(stakeBlockNumber));
// When calling monitoring module directly there should be only one log (index 0) with the stake caught by spy.
assert.equal(spy.getCall(0).lastArg.at, "DVMMonitor");
assert.equal(spy.getCall(0).lastArg.message, "Large amount staked 🍖");
assert.equal(spyLogLevel(spy, 0), "warn");
assert.isTrue(spyLogIncludes(spy, 0, stakerAddress));
assert.isTrue(spyLogIncludes(spy, 0, stakeTx.hash));
});
it("Monitor stake below threshold", async function () {
const stakeAmount = parseUnits("100");
// Fund staker with voting tokens to stake.
await votingToken.transfer(await stakerAddress, stakeAmount);
await votingToken.connect(staker).approve(votingV2.address, stakeAmount);
const stakeTx = await votingV2.connect(staker).stake(stakeAmount);
const stakeBlockNumber = await getBlockNumberFromTx(stakeTx);
// Call monitorStakes directly for the block when the stake was made.
const spy = sinon.spy();
const spyLogger = createNewLogger([new SpyTransport({}, { spy: spy })]);
await monitorStakes(spyLogger, {
...(await createMonitoringParams(stakeBlockNumber)),
stakeThreshold: stakeAmount.add("1"),
});
// When calling monitoring module directly there should be no logs.
assert.equal(spy.callCount, 0);
});
it("Monitor governance proposal", async function () {
// Fund and approve proposal bond.
await votingToken.transfer(await proposerAddress, governanceProposalBond);
await votingToken.connect(proposer).approve(proposerV2.address, governanceProposalBond);
// Create a proposal with one empty transaction.
const transaction = { to: proposerAddress, value: 0, data: toUtf8Bytes("") };
const proposalTx = await proposerV2.connect(proposer).propose([transaction], toUtf8Bytes(""));
const proposalBlockNumber = await getBlockNumberFromTx(proposalTx);
// Get admin identifier from the first RequestAdded event in the governance request transaction.
const adminIdentifier = parseBytes32String(
(await votingV2.queryFilter(votingV2.filters.RequestAdded(), proposalBlockNumber, proposalBlockNumber))[0].args
.identifier
);
// Call monitorGovernance directly for the block when the proposal was made.
const spy = sinon.spy();
const spyLogger = createNewLogger([new SpyTransport({}, { spy: spy })]);
await monitorGovernance(spyLogger, await createMonitoringParams(proposalBlockNumber));
// When calling monitoring module directly there should be only one log (index 0) with the proposal caught by spy.
assert.equal(spy.getCall(0).lastArg.at, "DVMMonitor");
assert.equal(spy.getCall(0).lastArg.message, "New governance proposal created 📜");
assert.equal(spyLogLevel(spy, 0), "warn");
assert.isTrue(spyLogIncludes(spy, 0, adminIdentifier));
assert.isTrue(spyLogIncludes(spy, 0, proposalTx.hash));
});
it("Monitor emergency proposal", async function () {
// Fund and approve emergency proposal bond.
await votingToken.transfer(await proposerAddress, emergencyQuorum);
await votingToken.connect(proposer).approve(emergencyProposer.address, emergencyQuorum);
// Create a emergency proposal with one empty transaction.
const transaction = { to: proposerAddress, value: 0, data: toUtf8Bytes("") };
const emergencyProposalTx = await emergencyProposer.connect(proposer).emergencyPropose([transaction]);
const emergencyProposalBlockNumber = await getBlockNumberFromTx(emergencyProposalTx);
// Get proposal id and proposer from the first EmergencyTransactionsProposed event in the proposal transaction.
const { id, caller } = (
await emergencyProposer.queryFilter(
emergencyProposer.filters.EmergencyTransactionsProposed(),
emergencyProposalBlockNumber,
emergencyProposalBlockNumber
)
)[0].args;
// Call monitorEmergency directly for the block when the emergency proposal was made.
const spy = sinon.spy();
const spyLogger = createNewLogger([new SpyTransport({}, { spy: spy })]);
await monitorEmergency(spyLogger, await createMonitoringParams(emergencyProposalBlockNumber));
// When calling monitoring module directly there should be only one log (index 0) with the proposal caught by spy.
assert.equal(spy.getCall(0).lastArg.at, "DVMMonitor");
assert.equal(spy.getCall(0).lastArg.message, "New emergency proposal created 🚨");
assert.equal(spyLogLevel(spy, 0), "error");
assert.isTrue(spyLogIncludes(spy, 0, caller));
assert.isTrue(spyLogIncludes(spy, 0, "emergency proposal #" + id.toString()));
assert.isTrue(spyLogIncludes(spy, 0, emergencyProposalTx.hash));
});
it("Monitor deleted request", async function () {
const identifier = formatBytes32String("TEST_IDENTIFIER");
const time = await votingV2.getCurrentTime();
const ancillaryData = toUtf8Bytes("Test ancillary data");
// Register requester and approve price identifier.
await registry.addMember(1, deployerAddress);
await registry.registerContract([], requesterAddress);
await registry.removeMember(1, deployerAddress);
await identifierWhitelist.addSupportedIdentifier(identifier);
// Initiate request.
await votingV2.connect(requester)["requestPrice(bytes32,uint256,bytes)"](identifier, time, ancillaryData);
// Advance time past maxRolls.
const endOfCurrentRound = await votingV2.getRoundEndTime(await votingV2.getCurrentRoundId());
await hardhatTime.setNextBlockTimestamp(endOfCurrentRound.toNumber() + phaseLength * 2 * (maxRolls + 1));
// Process resolvable price requests to triger deletion.
const deletionTx = await votingV2.processResolvablePriceRequests();
const deletionBlockNumber = await getBlockNumberFromTx(deletionTx);
// Call monitorDeletion directly for the block when the deletion was triggered.
const spy = sinon.spy();
const spyLogger = createNewLogger([new SpyTransport({}, { spy: spy })]);
await monitorDeletion(spyLogger, await createMonitoringParams(deletionBlockNumber));
// When calling monitoring module directly there should be only one log (index 0) with the deletion caught by spy.
assert.equal(spy.getCall(0).lastArg.at, "DVMMonitor");
assert.equal(spy.getCall(0).lastArg.message, "Request deleted as spam 🔇");
assert.equal(spyLogLevel(spy, 0), "error");
assert.isTrue(spyLogIncludes(spy, 0, parseBytes32String(identifier)));
assert.isTrue(spyLogIncludes(spy, 0, deletionTx.hash));
assert.isTrue(spyLogIncludes(spy, 0, toUtf8String(ancillaryData)));
});
it("Monitor rolled request", async function () {
const identifier = formatBytes32String("TEST_IDENTIFIER");
const time = await votingV2.getCurrentTime();
const ancillaryData = toUtf8Bytes("Test ancillary data");
// Register requester and approve price identifier.
await registry.addMember(1, deployerAddress);
await registry.registerContract([], requesterAddress);
await registry.removeMember(1, deployerAddress);
await identifierWhitelist.addSupportedIdentifier(identifier);
// Initiate request.
await votingV2.connect(requester)["requestPrice(bytes32,uint256,bytes)"](identifier, time, ancillaryData);
// Advance time one round past when the request was initially scheduled to be voted on.
const endOfCurrentRound = await votingV2.getRoundEndTime(await votingV2.getCurrentRoundId());
await hardhatTime.setNextBlockTimestamp(endOfCurrentRound.toNumber() + phaseLength * 2);
// Process resolvable price requests to triger request rolling.
const rolledTx = await votingV2.processResolvablePriceRequests();
const rolledBlockNumber = await getBlockNumberFromTx(rolledTx);
// Call monitorRolled directly for the block when the deletion was triggered.
const spy = sinon.spy();
const spyLogger = createNewLogger([new SpyTransport({}, { spy: spy })]);
await monitorRolled(spyLogger, await createMonitoringParams(rolledBlockNumber));
// When calling monitoring module directly there should be only one log (index 0) with the roll caught by spy.
assert.equal(spy.getCall(0).lastArg.at, "DVMMonitor");
assert.equal(spy.getCall(0).lastArg.message, "Rolled request 🎲");
assert.equal(spyLogLevel(spy, 0), "error");
assert.isTrue(spyLogIncludes(spy, 0, parseBytes32String(identifier)));
assert.isTrue(spyLogIncludes(spy, 0, rolledTx.hash));
assert.isTrue(spyLogIncludes(spy, 0, toUtf8String(ancillaryData)));
});
it("Monitor transfers from governor", async function () {
// Fund the governor first.
const transferAmount = parseUnits("100");
await votingToken.transfer(governorV2.address, transferAmount);
// Fund and approve emergency proposal bond.
await votingToken.transfer(await proposerAddress, emergencyQuorum);
await votingToken.connect(proposer).approve(emergencyProposer.address, emergencyQuorum);
// Create emergency proposal to transfer funds from governor to proposer.
const transaction = {
to: votingToken.address,
value: 0,
data: votingToken.interface.encodeFunctionData("transfer", [proposerAddress, transferAmount]),
};
const emergencyProposalTx = await emergencyProposer.connect(proposer).emergencyPropose([transaction]);
const emergencyProposalBlockNumber = await getBlockNumberFromTx(emergencyProposalTx);
// Get proposal id from the first EmergencyTransactionsProposed event in the proposal transaction.
const id = (
await emergencyProposer.queryFilter(
emergencyProposer.filters.EmergencyTransactionsProposed(),
emergencyProposalBlockNumber,
emergencyProposalBlockNumber
)
)[0].args.id;
// Advance time past minimumWaitTime
await hardhatTime.increase(minimumWaitTime);
// Execute emergency proposal.
const transferTx = await emergencyProposer.executeEmergencyProposal(id);
const transferBlockNumber = await getBlockNumberFromTx(transferTx);
// Call monitorGovernorTransfers directly for the block when the transfer was executed.
const spy = sinon.spy();
const spyLogger = createNewLogger([new SpyTransport({}, { spy: spy })]);
await monitorGovernorTransfers(spyLogger, await createMonitoringParams(transferBlockNumber));
// When calling monitoring module directly there should be only one log (index 0) with the transfer caught by spy.
assert.equal(spy.getCall(0).lastArg.at, "DVMMonitor");
assert.equal(spy.getCall(0).lastArg.message, "Large governor transfer 📤");
assert.equal(spyLogLevel(spy, 0), "error");
assert.isTrue(spyLogIncludes(spy, 0, proposerAddress));
assert.isTrue(spyLogIncludes(spy, 0, transferTx.hash));
});
it("Monitor transfers from governor below threshold", async function () {
// Fund the governor first.
const transferAmount = parseUnits("100");
await votingToken.transfer(governorV2.address, transferAmount);
// Fund and approve emergency proposal bond.
await votingToken.transfer(await proposerAddress, emergencyQuorum);
await votingToken.connect(proposer).approve(emergencyProposer.address, emergencyQuorum);
// Create emergency proposal to transfer funds from governor to proposer.
const transaction = {
to: votingToken.address,
value: 0,
data: votingToken.interface.encodeFunctionData("transfer", [proposerAddress, transferAmount]),
};
const emergencyProposalTx = await emergencyProposer.connect(proposer).emergencyPropose([transaction]);
const emergencyProposalBlockNumber = await getBlockNumberFromTx(emergencyProposalTx);
// Get proposal id from the first EmergencyTransactionsProposed event in the proposal transaction.
const id = (
await emergencyProposer.queryFilter(
emergencyProposer.filters.EmergencyTransactionsProposed(),
emergencyProposalBlockNumber,
emergencyProposalBlockNumber
)
)[0].args.id;
// Advance time past minimumWaitTime
await hardhatTime.increase(minimumWaitTime);
// Execute emergency proposal.
const transferTx = await emergencyProposer.executeEmergencyProposal(id);
const transferBlockNumber = await getBlockNumberFromTx(transferTx);
// Call monitorGovernorTransfers directly for the block when the transfer was executed.
const spy = sinon.spy();
const spyLogger = createNewLogger([new SpyTransport({}, { spy: spy })]);
await monitorGovernorTransfers(spyLogger, {
...(await createMonitoringParams(transferBlockNumber)),
governorTransfersThreshold: transferAmount.add("1"),
});
// When calling monitoring module directly there should be no logs.
assert.equal(spy.callCount, 0);
});
it("Monitor mint", async function () {
// Mint tokens to deployer.
const mintAmount = parseUnits("100");
const mintTx = await votingToken.mint(deployerAddress, mintAmount);
const mintBlockNumber = await getBlockNumberFromTx(mintTx);
// Call monitorMints directly for the block when the mint was made.
const spy = sinon.spy();
const spyLogger = createNewLogger([new SpyTransport({}, { spy: spy })]);
await monitorMints(spyLogger, await createMonitoringParams(mintBlockNumber));
// When calling monitoring module directly there should be only one log (index 0) with the mint caught by spy.
assert.equal(spy.getCall(0).lastArg.at, "DVMMonitor");
assert.equal(spy.getCall(0).lastArg.message, "Large UMA minting 💸");
assert.equal(spyLogLevel(spy, 0), "error");
assert.isTrue(spyLogIncludes(spy, 0, deployerAddress));
assert.isTrue(spyLogIncludes(spy, 0, mintTx.hash));
});
it("Monitor mint below threshold", async function () {
// Mint tokens to deployer.
const mintAmount = parseUnits("100");
const mintTx = await votingToken.mint(deployerAddress, mintAmount);
const mintBlockNumber = await getBlockNumberFromTx(mintTx);
// Call monitorMints directly for the block when the mint was made.
const spy = sinon.spy();
const spyLogger = createNewLogger([new SpyTransport({}, { spy: spy })]);
await monitorMints(spyLogger, {
...(await createMonitoringParams(mintBlockNumber)),
mintsThreshold: mintAmount.add("1"),
});
// When calling monitoring module directly there should be no logs.
assert.equal(spy.callCount, 0);
});
});