@dydxfoundation/governance
Version:
dYdX governance smart contracts
154 lines (153 loc) • 8.59 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 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);
});
});
});