UNPKG

@dydxfoundation/governance

Version:
154 lines (153 loc) 8.59 kB
"use strict"; 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 constants_1 = require("../../src/lib/constants"); const util_1 = require("../../src/lib/util"); const balance_tree_1 = __importDefault(require("../../src/merkle-tree-helpers/balance-tree")); const impersonate_account_1 = require("../../src/migrations/helpers/impersonate-account"); const types_1 = require("../../src/types"); const types_2 = require("../../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 borrowerHasBorrowed = 'BorrowerHasBorrowed'; const stakerInitialBalance = 1000000; // Contracts. let deployer; let liquidityStaking; let mockStakedToken; let mockStarkPerpetual; // Users. let staker; let exchangeOperator; let withdrawalOperator; let shortTimelockSigner; let borrower; let asExchangeOperator; let asWithdrawalOperator; let contract; async function init(ctx) { ({ liquidityStaking, deployer, } = ctx); mockStakedToken = ctx.dydxCollateralToken; mockStarkPerpetual = ctx.starkPerpetual; // Users. [staker, exchangeOperator, withdrawalOperator] = ctx.users; [borrower] = ctx.starkProxies; const ownerAddress = await (0, get_address_with_role_1.findAddressWithRole)(borrower, types_1.Role.OWNER_ROLE); const ownerSigner = await (0, impersonate_account_1.impersonateAndFundAccount)(ownerAddress); borrower = borrower.connect(ownerSigner); // Grant roles. await borrower.grantRole((0, util_1.getRole)(types_1.Role.BORROWER_ROLE), deployer.address); await borrower.grantRole((0, util_1.getRole)(types_1.Role.EXCHANGE_OPERATOR_ROLE), deployer.address); await borrower.grantRole((0, util_1.getRole)(types_1.Role.EXCHANGE_OPERATOR_ROLE), exchangeOperator.address); await borrower.grantRole((0, util_1.getRole)(types_1.Role.WITHDRAWAL_OPERATOR_ROLE), withdrawalOperator.address); asExchangeOperator = borrower.connect(exchangeOperator); asWithdrawalOperator = borrower.connect(withdrawalOperator); 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, [staker, withdrawalOperator, exchangeOperator], false); // Mint staked tokens and set allowances. await contract.mintAndApprove(staker, stakerInitialBalance); // Initial stake of 1M. await contract.stake(staker, stakerInitialBalance); await (0, evm_1.saveSnapshot)(snapshots, fundsStakedSnapshot, contract); } (0, describe_contract_1.describeContractHardhatRevertBefore)('SP2Withdrawals', init, (ctx) => { describe('Borrower, after borrowing funds', () => { before(async () => { await (0, evm_1.loadSnapshot)(snapshots, fundsStakedSnapshot, contract); // Allocations: 40% for first borrower, 0% for rest await contract.setBorrowerAllocations({ [borrower.address]: 0.4, [ctx.starkProxies[1].address]: 0.0, [ctx.starkProxies[2].address]: 0.0, [ctx.starkProxies[3].address]: 0.0, [ctx.starkProxies[4].address]: 0.0, [constants_1.ZERO_ADDRESS]: 0.6, }); // Borrow full amount. await contract.elapseEpoch(); await contract.fullBorrowViaProxy(borrower, stakerInitialBalance * 0.4); (0, chai_1.expect)(await mockStakedToken.balanceOf(borrower.address)).to.equal(stakerInitialBalance * 0.4); await (0, evm_1.saveSnapshot)(snapshots, borrowerHasBorrowed, contract); }); it('Can use non-borrowed funds on the exchange, and withdrawal operator can withdraw those funds', async () => { const borrowedAmount = stakerInitialBalance * 0.4; const [starkKey, assetType, vaultId] = [123, 456, 789]; await mockStarkPerpetual.registerUser(borrower.address, starkKey, []); await borrower.allowStarkKey(starkKey); // Deposit borrowed funds to the exchange. await asExchangeOperator.depositToExchange(starkKey, assetType, vaultId, borrowedAmount); // Deposit own, un-borrowed funds to the exchange. const ownFundsAmount = 1200000; await mockStakedToken.transfer(borrower.address, ownFundsAmount); await asExchangeOperator.depositToExchange(starkKey, assetType, vaultId, ownFundsAmount); // Expect contract to have no funds (0, chai_1.expect)(await mockStakedToken.balanceOf(borrower.address)).to.equal(0); // Withdraw from the exchange. await asExchangeOperator.withdrawFromExchange(starkKey, assetType); // Non withdrawal operator cannot withdraw from the proxy. await (0, chai_1.expect)(asExchangeOperator.externalWithdrawToken(exchangeOperator.address, 1)) .to.be.revertedWith('AccessControl'); // Cannot withdraw if recipient not on the allowlist. await (0, chai_1.expect)(asWithdrawalOperator.externalWithdrawToken(withdrawalOperator.address, ownFundsAmount)) .to.be.revertedWith('SP1Storage: Recipient is not on the allowlist'); // Cannot withdraw if contract would end up without enough funds to cover the borrowed balance. await borrower.allowExternalRecipient(withdrawalOperator.address); await (0, chai_1.expect)(asWithdrawalOperator.externalWithdrawToken(withdrawalOperator.address, ownFundsAmount + 1)) .to.be.revertedWith('SP2Withdrawals: Amount exceeds withdrawable balance'); // Can withdraw if enough funds are left to cover the borrowed balance. await asWithdrawalOperator.externalWithdrawToken(withdrawalOperator.address, ownFundsAmount); // Expected borrower to have all their funds back. (0, chai_1.expect)(await mockStakedToken.balanceOf(withdrawalOperator.address)).to.equal(ownFundsAmount); // Return borrowed funds. await contract.repayBorrowViaProxy(borrower, borrowedAmount); (0, chai_1.expect)(await borrower.getBorrowedBalance()).to.equal(0); }); }); describe('claimRewardsFromMerkleDistributor', () => { let dydxToken; let merkleDistributor; let mockRewardsOracle; let treasurySupply; let simpleTree; let waitingPeriod; before(async () => { ({ dydxToken, merkleDistributor, } = ctx); // Deploy and use mock rewards oracle. mockRewardsOracle = await new types_2.MockRewardsOracle__factory(deployer).deploy(); await merkleDistributor.connect(shortTimelockSigner).setRewardsOracle(mockRewardsOracle.address); treasurySupply = await dydxToken.balanceOf(ctx.rewardsTreasury.address); // Simple tree example that gives all tokens to a single address. simpleTree = new balance_tree_1.default({ [borrower.address]: treasurySupply, }); // Get the waiting period. waitingPeriod = (await merkleDistributor.WAITING_PERIOD()).toNumber(); const mockIpfsCid = Buffer.from('0'.repeat(64), 'hex'); await mockRewardsOracle.setMockValue(simpleTree.getHexRoot(), 0, mockIpfsCid); await merkleDistributor.proposeRoot(); await (0, evm_1.incrementTimeToTimestamp)(await (0, evm_1.latestBlockTimestamp)() + waitingPeriod); await merkleDistributor.updateRoot(); }); it('can claim rewards', async () => { const proof = simpleTree.getProof(borrower.address, treasurySupply); await (0, chai_1.expect)(asWithdrawalOperator.claimRewardsFromMerkleDistributor(treasurySupply, proof)) .to.emit(merkleDistributor, 'RewardsClaimed') .withArgs(borrower.address, treasurySupply); }); }); });