UNPKG

@dydxfoundation/governance

Version:
261 lines (260 loc) 16.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const chai_1 = require("chai"); const util_1 = require("../../src/lib/util"); const impersonate_account_1 = require("../../src/migrations/helpers/impersonate-account"); const types_1 = require("../../src/types"); 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"); const staking_helper_1 = require("../helpers/staking-helper"); // Snapshots const snapshots = new Map(); const fundsStakedSnapshot = 'FundsStaked'; const borrowerAllocationsSettledSnapshot = 'BorrowerAllocationsSettled'; const borrowerAmountDue = 'BorrowerAmountDue'; const borrowerRestrictedSnapshot = 'BorrowerRestrictedSnapshot'; const stakerInitialBalance = 1000000; // Contracts. let liquidityStaking; let mockStakedToken; let mockStarkPerpetual; let shortTimelockSigner; // Users. let deployer; let stakers; let borrowerStarkProxies; let expectedAllocations; let contract; let epochLength; let blackoutWindow; async function init(ctx) { ({ liquidityStaking, deployer, } = ctx); mockStakedToken = ctx.dydxCollateralToken; mockStarkPerpetual = ctx.starkPerpetual; blackoutWindow = await liquidityStaking.getBlackoutWindow(); epochLength = (await liquidityStaking.getEpochParameters())[0]; // Users. stakers = ctx.users.slice(1, 3); // 2 stakers const borrowers = await Promise.all(ctx.starkProxies.map(async (b) => { const ownerAddress = await (0, get_address_with_role_1.findAddressWithRole)(b, types_1.Role.OWNER_ROLE); return (0, impersonate_account_1.impersonateAndFundAccount)(ownerAddress); })); borrowerStarkProxies = borrowers.map((b, i) => ctx.starkProxies[i].connect(b)); // Grant roles. await Promise.all(borrowerStarkProxies.map(async (b) => { await b.grantRole((0, util_1.getRole)(types_1.Role.EXCHANGE_OPERATOR_ROLE), deployer.address); await b.grantRole((0, util_1.getRole)(types_1.Role.BORROWER_ROLE), deployer.address); })); shortTimelockSigner = await (0, impersonate_account_1.impersonateAndFundAccount)(ctx.shortTimelock.address); // Use helper class to automatically check contract invariants after every update. contract = new staking_helper_1.StakingHelper(ctx, liquidityStaking, mockStakedToken, ctx.rewardsTreasury.address, deployer, shortTimelockSigner, stakers.concat(borrowers), false); // Mint staked tokens and set allowances. await Promise.all(stakers.map((s) => contract.mintAndApprove(s, stakerInitialBalance))); // Initial stake of 1M. await contract.stake(stakers[0], stakerInitialBalance / 4); await contract.stake(stakers[1], (stakerInitialBalance / 4) * 3); await (0, evm_1.saveSnapshot)(snapshots, fundsStakedSnapshot, contract); } (0, describe_contract_1.describeContractHardhatRevertBefore)('SP1Borrowing', init, () => { describe('After stake is deposited and allocations are set', () => { before(async () => { await (0, evm_1.loadSnapshot)(snapshots, fundsStakedSnapshot, contract); // Allocations: [40%, 60%] await contract.setBorrowerAllocations({ [borrowerStarkProxies[0].address]: 0.4, [borrowerStarkProxies[1].address]: 0.6, [borrowerStarkProxies[2].address]: 0.0, [borrowerStarkProxies[3].address]: 0.0, [borrowerStarkProxies[4].address]: 0.0, }); expectedAllocations = [stakerInitialBalance * 0.4, stakerInitialBalance * 0.6]; // Enter the blackout window. await contract.elapseEpoch(); await advanceToBlackoutWindow(); await (0, evm_1.saveSnapshot)(snapshots, borrowerAllocationsSettledSnapshot, contract); }); describe('Before borrowing', () => { beforeEach(async () => { await (0, evm_1.loadSnapshot)(snapshots, borrowerAllocationsSettledSnapshot, contract); }); it('Auto-pay cannot be called outside the blackout window', async () => { await contract.elapseEpoch(); await (0, chai_1.expect)(borrowerStarkProxies[0].autoPayOrBorrow()).to.be.revertedWith('SP1Borrowing: Auto-pay may only be used during the blackout window'); }); it('Auto-pay will borrow full borrowable amount', async () => { const results = await contract.autoPay(borrowerStarkProxies[0]); expectEqs(results, [expectedAllocations[0], 0, 0]); // After a non-reverting call to autoPay(), should never be overdue in the next epoch. await contract.elapseEpoch(); (0, chai_1.expect)(await liquidityStaking.isBorrowerOverdue(borrowerStarkProxies[0].address)).to.equal(false); }); }); describe('When borrower has an amount due', () => { before(async () => { await (0, evm_1.loadSnapshot)(snapshots, borrowerAllocationsSettledSnapshot, contract); // Borrow full amount of 0.4M await contract.fullBorrowViaProxy(borrowerStarkProxies[0], stakerInitialBalance * 0.4); // Staker request withdrawal of 0.5M (half the funds in the contract). await contract.elapseEpoch(); await contract.requestWithdrawal(stakers[1], stakerInitialBalance / 2); // 0.5M // Enter the blackout window. await advanceToBlackoutWindow(); await (0, evm_1.saveSnapshot)(snapshots, borrowerAmountDue, contract); }); beforeEach(async () => { await (0, evm_1.loadSnapshot)(snapshots, borrowerAmountDue, contract); }); it('Auto-pay will repay the borrow amount due', async () => { // Expect repay 0.2M let results = await contract.autoPay(borrowerStarkProxies[0]); expectEqs(results, [0, expectedAllocations[0] / 2, 0]); // While still in the blackout period, staker stakes more, and borrower auto-borrows. // Stake another 1M. await contract.mintAndApprove(stakers[0], stakerInitialBalance); await contract.stake(stakers[0], stakerInitialBalance); // Just a quick overview of the current state. (0, chai_1.expect)(await liquidityStaking.getTotalActiveBalanceCurrentEpoch()).to.equal(2000000); (0, chai_1.expect)(await liquidityStaking.getTotalActiveBalanceNextEpoch()).to.equal(1500000); (0, chai_1.expect)(await liquidityStaking.getTotalInactiveBalanceCurrentEpoch()).to.equal(0); (0, chai_1.expect)(await liquidityStaking.getTotalInactiveBalanceNextEpoch()).to.equal(500000); (0, chai_1.expect)(await liquidityStaking.getAllocatedBalanceCurrentEpoch(borrowerStarkProxies[0].address)).to.equal(800000); (0, chai_1.expect)(await liquidityStaking.getAllocatedBalanceNextEpoch(borrowerStarkProxies[0].address)).to.equal(600000); (0, chai_1.expect)(await liquidityStaking.getContractBalanceAvailableToWithdraw()).to.equal(1800000); (0, chai_1.expect)(await liquidityStaking.getBorrowedBalance(borrowerStarkProxies[0].address)).to.equal(200000); (0, chai_1.expect)(await liquidityStaking.getBorrowableAmount(borrowerStarkProxies[0].address)).to.equal(400000); // Current active is 2M and next active is 1.5M. // We have an outstanding borrow of 0.2M. // Expect borrow to be limited by next active, so should be another 0.4M. results = await contract.autoPay(borrowerStarkProxies[0]); expectEqs(results, [stakerInitialBalance * 0.4, 0, 0]); // Next epoch: request withdraw everything. await contract.elapseEpoch(); await contract.requestWithdrawal(stakers[0], (stakerInitialBalance * 5) / 4); // 0.25M await contract.requestWithdrawal(stakers[1], (stakerInitialBalance * 1) / 4); // 1.25M await advanceToBlackoutWindow(); results = await contract.autoPay(borrowerStarkProxies[0]); expectEqs(results, [0, stakerInitialBalance * 0.6, 0]); // Repay 0.6M // After a non-reverting call to autoPay(), should never be overdue in the next epoch. await contract.elapseEpoch(); (0, chai_1.expect)(await liquidityStaking.isBorrowerOverdue(borrowerStarkProxies[0].address)).to.equal(false); }); it('Auto-pay will revert if borrower does not have funds to pay amount due by the next epoch', async () => { // Need repayment of 0.2M. Borrower currently has 0.4M. // Deposit 0.2M + 1 to the exchange. const starkKey = 123; await mockStarkPerpetual.registerUser(borrowerStarkProxies[0].address, starkKey, []); await borrowerStarkProxies[0].allowStarkKey(starkKey); await borrowerStarkProxies[0].depositToExchange(starkKey, 456, 789, stakerInitialBalance * 0.2 + 1); // Auto-pay will revert to indicate there are not enough funds to avoid a shortfall. await (0, chai_1.expect)(contract.autoPay(borrowerStarkProxies[0])).to.be.revertedWith('SP1Borrowing: Insufficient funds to avoid falling short on repayment'); // Withdraw (all) from the exchange, then deposit 0.2M again. await borrowerStarkProxies[0].withdrawFromExchange(starkKey, 456); await borrowerStarkProxies[0].depositToExchange(starkKey, 456, 789, stakerInitialBalance * 0.2); // Expect repay 0.2M const results = await contract.autoPay(borrowerStarkProxies[0]); expectEqs(results, [0, expectedAllocations[0] / 2, 0]); // After a non-reverting call to autoPay(), should never be overdue in the next epoch. await contract.elapseEpoch(); (0, chai_1.expect)(await liquidityStaking.isBorrowerOverdue(borrowerStarkProxies[0].address)).to.equal(false); }); }); describe('When borrower has a debt due', () => { before(async () => { await (0, evm_1.loadSnapshot)(snapshots, borrowerAllocationsSettledSnapshot, contract); // Borrow full amount of 0.4M await contract.fullBorrowViaProxy(borrowerStarkProxies[0], stakerInitialBalance * 0.4); // Staker request withdrawal of 0.75M (3/4 the funds in the contract). await contract.elapseEpoch(); await contract.requestWithdrawal(stakers[1], (stakerInitialBalance * 3) / 4); // Advance to the next epoch and mark debt. // Overdue amount is 0.15M against a requested amount of 0.75M. await contract.elapseEpoch(); await contract.markDebt({ [borrowerStarkProxies[0].address]: stakerInitialBalance * 0.15, }, [borrowerStarkProxies[0].address], 0.8); await (0, evm_1.saveSnapshot)(snapshots, borrowerAmountDue, contract); }); beforeEach(async () => { await (0, evm_1.loadSnapshot)(snapshots, borrowerAmountDue, contract); }); it('Auto-pay will pay outstanding debt if there are available funds to do so', async () => { // Borrowed, 0.4M and have not repaid any, but 0.15M was converted to debt. // The total active staked funds are 0.25M, and borrower's allocation is 40%, or 0.1M. // // So expect to owe 0.15M in debt and another 0.15M in borrow due by the next epoch. // // Make a partial repayment directly, first, and then call auto-pay. const earlyRepaymentAmount = 129787; await contract.repayBorrowViaProxy(borrowerStarkProxies[0], earlyRepaymentAmount); await advanceToBlackoutWindow(); const results = await contract.autoPay(borrowerStarkProxies[0]); expectEqs(results, [ 0, stakerInitialBalance * 0.15 - earlyRepaymentAmount, stakerInitialBalance * 0.15, ]); // After a non-reverting call to autoPay(), should never be overdue in the next epoch. await contract.elapseEpoch(); (0, chai_1.expect)(await liquidityStaking.isBorrowerOverdue(borrowerStarkProxies[0].address)).to.equal(false); }); }); describe('After restricted by guardian', () => { before(async () => { await (0, evm_1.loadSnapshot)(snapshots, borrowerAllocationsSettledSnapshot, contract); await (0, chai_1.expect)(borrowerStarkProxies[0].connect(shortTimelockSigner).guardianSetBorrowingRestriction(true)) .to.emit(borrowerStarkProxies[0], 'BorrowingRestrictionChanged') .withArgs(true); await (0, evm_1.saveSnapshot)(snapshots, borrowerRestrictedSnapshot, contract); }); beforeEach(async () => { await (0, evm_1.loadSnapshot)(snapshots, borrowerRestrictedSnapshot, contract); }); it('Auto-pay will not borrow (and will not revert) if borrower is restricted by guardian', async () => { // Expect borrowable amount according to staking contract to be unchanged. (0, chai_1.expect)(await liquidityStaking.getBorrowableAmount(borrowerStarkProxies[0].address)).to.equal(stakerInitialBalance * 0.4); // Expect borrowable amount according to proxy contract to be zero. (0, chai_1.expect)(await borrowerStarkProxies[0].getBorrowableAmount()).to.equal(0); // Auto-pay results in zero borrow. const results = await contract.autoPay(borrowerStarkProxies[0]); expectEqs(results, [0, 0, 0]); // After a non-reverting call to autoPay(), should never be overdue in the next epoch. await contract.elapseEpoch(); (0, chai_1.expect)(await liquidityStaking.isBorrowerOverdue(borrowerStarkProxies[0].address)).to.equal(false); }); it('Borrower can borrow again if restriction is released', async () => { await (0, chai_1.expect)(borrowerStarkProxies[0].connect(shortTimelockSigner).guardianSetBorrowingRestriction(false)) .to.emit(borrowerStarkProxies[0], 'BorrowingRestrictionChanged') .withArgs(false); // Borrow full amount of 0.4M. const results = await contract.autoPay(borrowerStarkProxies[0]); expectEqs(results, [expectedAllocations[0], 0, 0]); // After a non-reverting call to autoPay(), should never be overdue in the next epoch. await contract.elapseEpoch(); (0, chai_1.expect)(await liquidityStaking.isBorrowerOverdue(borrowerStarkProxies[0].address)).to.equal(false); }); }); }); /** * Progress to the blackout window of the current epoch. */ async function advanceToBlackoutWindow(mineBlock = true) { const remaining = (await liquidityStaking.getTimeRemainingInCurrentEpoch()).toNumber() || epochLength.toNumber(); const timeUntilBlackoutWindow = remaining - blackoutWindow.toNumber(); if (mineBlock) { await (0, evm_1.increaseTimeAndMine)(timeUntilBlackoutWindow); } else { await (0, evm_1.increaseTime)(timeUntilBlackoutWindow); } } }); function expectEqs(actual, expected) { (0, chai_1.expect)(actual).to.have.length(expected.length); for (let i = 0; i < expected.length; i++) { (0, chai_1.expect)(actual[i], `expectEqs[${i}]: ${actual}`).to.be.equal(expected[i]); } }