@dydxfoundation/governance
Version:
dYdX governance smart contracts
133 lines (132 loc) • 8.97 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const chai_1 = require("chai");
const utils_1 = require("ethers/lib/utils");
const lodash_1 = __importDefault(require("lodash"));
const util_1 = require("../../src/lib/util");
const impersonate_account_1 = require("../../src/migrations/helpers/impersonate-account");
const types_1 = require("../../src/types");
const StarkProxyV2__factory_1 = require("../../types/factories/StarkProxyV2__factory");
const describe_contract_1 = require("../helpers/describe-contract");
const evm_1 = require("../helpers/evm");
const get_address_with_role_1 = require("../helpers/get-address-with-role");
let deployer;
let shortTimelockSigner;
let merkleTimelockSigner;
// Each borrower is represented by a stark proxy contract.
let liquidityStaking;
let mockStarkPerpetual;
let borrowerStarkProxy;
let forcedTradeWaitingPeriod;
let forcedTradeGracePeriod;
async function init(ctx) {
deployer = ctx.deployer;
shortTimelockSigner = await (0, impersonate_account_1.impersonateAndFundAccount)(ctx.shortTimelock.address);
merkleTimelockSigner = await (0, impersonate_account_1.impersonateAndFundAccount)(ctx.merklePauserTimelock.address);
liquidityStaking = ctx.liquidityStaking;
mockStarkPerpetual = ctx.starkPerpetual;
const ownerAddress = await (0, get_address_with_role_1.findAddressWithRole)(ctx.starkProxies[0], types_1.Role.OWNER_ROLE);
const borrower = await (0, impersonate_account_1.impersonateAndFundAccount)(ownerAddress);
borrowerStarkProxy = new StarkProxyV2__factory_1.StarkProxyV2__factory(borrower).attach(ctx.starkProxies[0].address);
// Grant exchange operator role.
await borrowerStarkProxy.grantRole((0, util_1.getRole)(types_1.Role.EXCHANGE_OPERATOR_ROLE), deployer.address);
forcedTradeWaitingPeriod = await borrowerStarkProxy.FORCED_TRADE_WAITING_PERIOD();
forcedTradeGracePeriod = await borrowerStarkProxy.FORCED_TRADE_GRACE_PERIOD();
}
(0, describe_contract_1.describeContract)('SP2Guardian', init, (ctx) => {
(0, describe_contract_1.describeContractForNetwork)('SP2Guardian Hardhat Tests', ctx, types_1.NetworkName.hardhat, false, () => {
it('No borrowers are initially in default', async () => {
for (const borrower of ctx.starkProxies) {
(0, chai_1.expect)(await liquidityStaking.isBorrowerOverdue(borrower.address)).to.equal(false);
}
});
it('Can veto a forced trade request', async () => {
// Register STARK key and add it to the allowlist.
const mockStarkKey = 0;
await mockStarkPerpetual.registerUser(borrowerStarkProxy.address, mockStarkKey, []);
await borrowerStarkProxy.allowStarkKey(mockStarkKey);
// Owner enqueues a forced trade request.
const args = lodash_1.default.range(12);
const signature = Buffer.from('mock-signature');
const tx = await borrowerStarkProxy.queueForcedTradeRequest(args);
const receipt = await (0, util_1.waitForTx)(tx);
const { argsHash } = borrowerStarkProxy.interface.parseLog(receipt.logs[0]).args;
// Expect to fail during waiting period.
await (0, chai_1.expect)(borrowerStarkProxy.forcedTradeRequest(args, signature)).to.be.revertedWith('SP2Owner: Waiting period has not elapsed for forced trade');
// Allow waiting period to elapse.
let timestamp = await (0, evm_1.latestBlockTimestamp)();
await (0, evm_1.incrementTimeToTimestamp)(forcedTradeWaitingPeriod.add(timestamp).toString());
// Note: 'function selector was not recognized' means it has attempted to call through to the
// mock StarkPerpetual contract, and did not revert on StarkProxy.
await (0, chai_1.expect)(borrowerStarkProxy.forcedTradeRequest(args, signature)).to.be.revertedWith('function selector was not recognized');
// Expect to fail after waiting period, if vetoed.
await borrowerStarkProxy.connect(merkleTimelockSigner).guardianVetoForcedTradeRequests([argsHash]);
await (0, chai_1.expect)(borrowerStarkProxy.forcedTradeRequest(args, signature)).to.be.revertedWith('SP2Owner: Forced trade not queued or was vetoed');
// Queue it again and expect it to fail if outside of the grace period.
await borrowerStarkProxy.queueForcedTradeRequest(args);
timestamp = await (0, evm_1.latestBlockTimestamp)();
await (0, evm_1.incrementTimeToTimestamp)(forcedTradeWaitingPeriod
.add(forcedTradeGracePeriod)
.add(timestamp)
.add(1)
.toString());
await (0, chai_1.expect)(borrowerStarkProxy.forcedTradeRequest(args, signature)).to.be.revertedWith('SP2Owner: Grace period has elapsed for forced trade');
});
it('GUARDIAN_ROLE can cancel and reclaim a deposit', async () => {
// Register STARK key and add it to the allowlist.
const mockStarkKey = 0;
const mockAssetType = 1;
const mockVaultId = 2;
await mockStarkPerpetual.registerUser(borrowerStarkProxy.address, mockStarkKey, []);
await borrowerStarkProxy.allowStarkKey(mockStarkKey);
const guardianStarkProxy = borrowerStarkProxy.connect(shortTimelockSigner);
await (0, chai_1.expect)(guardianStarkProxy.guardianDepositCancel(mockStarkKey, mockAssetType, mockVaultId))
.to.emit(guardianStarkProxy, 'DepositCanceled')
.withArgs(mockStarkKey, mockAssetType, mockVaultId, true);
await (0, chai_1.expect)(guardianStarkProxy.guardianDepositReclaim(mockStarkKey, mockAssetType, mockVaultId))
.to.emit(guardianStarkProxy, 'DepositReclaimed')
.withArgs(mockStarkKey, mockAssetType, mockVaultId, 0, true);
});
it('User without GUARDIAN_ROLE cannot cancel or reclaim a deposit', async () => {
// Register STARK key and add it to the allowlist.
const mockStarkKey = 0;
const mockAssetType = 1;
const mockVaultId = 2;
await mockStarkPerpetual.registerUser(borrowerStarkProxy.address, mockStarkKey, []);
await borrowerStarkProxy.allowStarkKey(mockStarkKey);
const ownerAddress = (await borrowerStarkProxy.signer.getAddress()).toLowerCase();
const accessControlError = `AccessControl: account ${ownerAddress} is missing role ${(0, util_1.getRole)(types_1.Role.GUARDIAN_ROLE)}`;
await (0, chai_1.expect)(borrowerStarkProxy.guardianDepositCancel(mockStarkKey, mockAssetType, mockVaultId))
.to.be.revertedWith(accessControlError);
await (0, chai_1.expect)(borrowerStarkProxy.guardianDepositReclaim(mockStarkKey, mockAssetType, mockVaultId))
.to.be.revertedWith(accessControlError);
});
});
(0, describe_contract_1.describeContractForNetwork)('Stark Proxy deposit cancel and reclaim', ctx, types_1.NetworkName.hardhat, true, () => {
it('GUARDIAN_ROLE can cancel faulty deposit and reclaim funds', async () => {
const wintermuteStarkProxy = borrowerStarkProxy.connect(shortTimelockSigner);
const starkProxyBalanceBefore = await wintermuteStarkProxy.getTokenBalance();
const depositEvents = await wintermuteStarkProxy.queryFilter(wintermuteStarkProxy.filters.DepositedToExchange(null, null, null, null));
const badVaultId = '32';
const faultyDeposits = depositEvents.filter((e) => e.args.starkVaultId.toString() === badVaultId);
(0, chai_1.expect)(faultyDeposits.length).to.equal(1);
const starkKey = faultyDeposits[0].args.starkKey;
const assetType = faultyDeposits[0].args.starkAssetType;
await (0, chai_1.expect)(wintermuteStarkProxy.guardianDepositCancel(starkKey, assetType, badVaultId))
.to.emit(wintermuteStarkProxy, 'DepositCanceled')
.withArgs(starkKey, assetType, badVaultId, true);
const twoDaysSeconds = 2 * 24 * 60 * 60;
await (0, evm_1.increaseTimeAndMine)(twoDaysSeconds);
const lockedFunds = (0, utils_1.parseUnits)('50000000', 6);
await (0, chai_1.expect)(wintermuteStarkProxy.guardianDepositReclaim(starkKey, assetType, badVaultId))
.to.emit(wintermuteStarkProxy, 'DepositReclaimed')
.withArgs(starkKey, assetType, badVaultId, lockedFunds, true);
const starkProxyBalanceAfter = await wintermuteStarkProxy.getTokenBalance();
const diff = starkProxyBalanceAfter.sub(starkProxyBalanceBefore).toString();
(0, chai_1.expect)(diff).to.equal(lockedFunds);
});
});
});