UNPKG

@dydxfoundation/governance

Version:
339 lines (338 loc) 23.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.addStakingTestCases = void 0; const bignumber_js_1 = __importDefault(require("bignumber.js")); const chai_1 = require("chai"); const constants_1 = require("../../../src/lib/constants"); const deploy_upgradeable_1 = require("../../../src/migrations/helpers/deploy-upgradeable"); const types_1 = require("../../../types"); const describe_contract_1 = require("../../helpers/describe-contract"); const evm_1 = require("../../helpers/evm"); const staking_helper_1 = require("../../helpers/staking-helper"); const stakerInitialBalance = 1000000; const stakerInitialBalance2 = 4000000; function addStakingTestCases(getStakers) { // Users. let staker1; let staker2; let fundsRecipient; // Users calling the liquidity staking contract. let stakerSigner1; let stakerSigner2; let distributionEnd; let contract; async function init(ctx) { // Users. [staker1, staker2] = await getStakers(ctx); [, , fundsRecipient] = ctx.users; // Users calling the liquidity staking contract. stakerSigner1 = ctx.safetyModule.connect(staker1); stakerSigner2 = ctx.safetyModule.connect(staker2); distributionEnd = (await ctx.safetyModule.DISTRIBUTION_END()).toString(); // Use helper class to automatically check contract invariants after every update. contract = new staking_helper_1.StakingHelper(ctx, ctx.safetyModule, ctx.dydxToken, ctx.rewardsTreasury.address, ctx.deployer, ctx.deployer, [staker1, staker2], true); // Transfer away any existing DYDX balance. // This is needed when testing on a mainnet fork with the addresses affected by // the Safety Module bug. await ctx.dydxToken.connect(staker1).transfer(ctx.deployer.address, await ctx.dydxToken.balanceOf(staker1.address)); await ctx.dydxToken.connect(staker2).transfer(ctx.deployer.address, await ctx.dydxToken.balanceOf(staker2.address)); // Mint to stakers. await contract.mintAndApprove(staker1, stakerInitialBalance); await contract.mintAndApprove(staker2, stakerInitialBalance2); // Set initial rewards rate to zero. await contract.setRewardsPerSecond(0); } (0, describe_contract_1.describeContractHardhatRevertBeforeEach)('SM1Staking', init, (ctx) => { before(() => { contract.saveSnapshot('main'); }); afterEach(() => { contract.loadSnapshot('main'); }); describe('stake', () => { it('User cannot stake if epoch zero has not started', async () => { const newDistributionStart = await (0, evm_1.latestBlockTimestamp)() + 100; const [smBeforeEpochZero] = await (0, deploy_upgradeable_1.deployUpgradeable)(types_1.SafetyModuleV11__factory, ctx.deployer, [ ctx.dydxToken.address, ctx.dydxToken.address, ctx.rewardsTreasury.address, newDistributionStart, ctx.config.SM_DISTRIBUTION_END, ], [ ctx.config.EPOCH_LENGTH, newDistributionStart, ctx.config.BLACKOUT_WINDOW, ]); await (0, chai_1.expect)(smBeforeEpochZero.stake(stakerInitialBalance)).to.be.revertedWith('SM1EpochSchedule: Epoch zero has not started'); }); it('User can successfully stake if epoch zero has started', async () => { await contract.stake(staker1, stakerInitialBalance); // `dydxToken` should be transferred to SafetyModuleV1 contract, and user should be given an // equivalent amount of `SM1ERC20` tokens (0, chai_1.expect)(await ctx.dydxToken.balanceOf(staker1.address)).to.equal(0); (0, chai_1.expect)(await ctx.dydxToken.balanceOf(ctx.safetyModule.address)).to.equal(stakerInitialBalance); (0, chai_1.expect)(await ctx.safetyModule.balanceOf(staker1.address)).to.equal(stakerInitialBalance); }); }); describe('requestWithdrawal', () => { it('User with nonzero staked balance can request a withdrawal after epoch zero has started', async () => { await contract.stake(staker1, stakerInitialBalance); await contract.requestWithdrawal(staker1, stakerInitialBalance); // `dydxToken` should still be owned by SafetyModuleV1 contract, and user should still own an // equivalent amount of `SM1ERC20` tokens (0, chai_1.expect)(await ctx.dydxToken.balanceOf(staker1.address)).to.equal(0); (0, chai_1.expect)(await ctx.dydxToken.balanceOf(ctx.safetyModule.address)).to.equal(stakerInitialBalance); (0, chai_1.expect)(await ctx.safetyModule.balanceOf(staker1.address)).to.equal(stakerInitialBalance); // Check balances. (Note that the staking helper also does a lot of its own checks.) // // Expect the next active balance to have decreased relative to the current active balance. (0, chai_1.expect)(await ctx.safetyModule.getTotalActiveBalanceCurrentEpoch()).to.equal(stakerInitialBalance); (0, chai_1.expect)(await ctx.safetyModule.getActiveBalanceCurrentEpoch(staker1.address)).to.equal(stakerInitialBalance); (0, chai_1.expect)(await ctx.safetyModule.getActiveBalanceCurrentEpoch(staker2.address)).to.equal(0); (0, chai_1.expect)(await ctx.safetyModule.getTotalActiveBalanceNextEpoch()).to.equal(0); (0, chai_1.expect)(await ctx.safetyModule.getActiveBalanceNextEpoch(staker1.address)).to.equal(0); (0, chai_1.expect)(await ctx.safetyModule.getActiveBalanceNextEpoch(staker2.address)).to.equal(0); // Expect the next inactive balance to have increased relative to the current inactive balance. (0, chai_1.expect)(await ctx.safetyModule.getTotalInactiveBalanceCurrentEpoch()).to.equal(0); (0, chai_1.expect)(await ctx.safetyModule.getInactiveBalanceCurrentEpoch(staker1.address)).to.equal(0); (0, chai_1.expect)(await ctx.safetyModule.getInactiveBalanceCurrentEpoch(staker2.address)).to.equal(0); (0, chai_1.expect)(await ctx.safetyModule.getTotalInactiveBalanceNextEpoch()).to.equal(stakerInitialBalance); (0, chai_1.expect)(await ctx.safetyModule.getInactiveBalanceNextEpoch(staker1.address)).to.equal(stakerInitialBalance); (0, chai_1.expect)(await ctx.safetyModule.getInactiveBalanceNextEpoch(staker2.address)).to.equal(0); }); it('User with nonzero staked balance cannot request a withdrawal during blackout window', async () => { await contract.stake(staker1, stakerInitialBalance); const withinBlackoutWindow = (await (0, evm_1.latestBlockTimestamp)() + Number(await ctx.safetyModule.getTimeRemainingInCurrentEpoch()) + ctx.config.EPOCH_LENGTH - ctx.config.BLACKOUT_WINDOW); await (0, evm_1.incrementTimeToTimestamp)(withinBlackoutWindow); await (0, chai_1.expect)(contract.requestWithdrawal(staker1, stakerInitialBalance)).to.be.revertedWith('SM1Staking: Withdraw requests restricted in the blackout window'); // `dydxToken` should still be owned by SafetyModuleV1 contract, and user should still have an // equivalent amount of `SM1ERC20` tokens (0, chai_1.expect)(await ctx.dydxToken.balanceOf(staker1.address)).to.equal(0); (0, chai_1.expect)(await ctx.dydxToken.balanceOf(ctx.safetyModule.address)).to.equal(stakerInitialBalance); (0, chai_1.expect)(await ctx.safetyModule.balanceOf(staker1.address)).to.equal(stakerInitialBalance); }); it('User with zero staked balance cannot request a withdrawal', async () => { await (0, chai_1.expect)(contract.requestWithdrawal(staker1, 1)).to.be.revertedWith('SM1Staking: Withdraw request exceeds next active balance'); }); }); describe('withdrawStake', () => { it('Staker can request and withdraw full balance', async () => { await contract.stake(staker1, stakerInitialBalance); await contract.requestWithdrawal(staker1, stakerInitialBalance); await contract.elapseEpoch(); // increase time to next epoch, so user can withdraw funds await contract.withdrawStake(staker1, fundsRecipient, stakerInitialBalance); // `dydxToken` should be sent to fundsRecipient, SafetyModuleV1 contract should own nothing // and user should have 0 staked token balance (0, chai_1.expect)(await ctx.dydxToken.balanceOf(fundsRecipient.address)).to.equal(stakerInitialBalance); (0, chai_1.expect)(await ctx.dydxToken.balanceOf(staker1.address)).to.equal(0); (0, chai_1.expect)(await ctx.dydxToken.balanceOf(ctx.safetyModule.address)).to.equal(0); (0, chai_1.expect)(await ctx.safetyModule.balanceOf(staker1.address)).to.equal(0); }); it('Staker can request full balance and make multiple partial withdrawals', async () => { await contract.stake(staker1, stakerInitialBalance); await contract.requestWithdrawal(staker1, stakerInitialBalance); await contract.elapseEpoch(); // increase time to next epoch, so user can withdraw funds const withdrawAmount = 1; await contract.withdrawStake(staker1, fundsRecipient, withdrawAmount); // `dydxToken` should be sent to fundsRecipient, SafetyModuleV1 contract should own remainder (0, chai_1.expect)(await ctx.dydxToken.balanceOf(fundsRecipient.address)).to.equal(withdrawAmount); (0, chai_1.expect)(await ctx.dydxToken.balanceOf(staker1.address)).to.equal(0); (0, chai_1.expect)(await ctx.dydxToken.balanceOf(ctx.safetyModule.address)).to.equal(stakerInitialBalance - withdrawAmount); // Additional withdrawal await contract.withdrawStake(staker1, fundsRecipient, 10); await contract.withdrawStake(staker1, fundsRecipient, 100); await contract.withdrawStake(staker1, fundsRecipient, stakerInitialBalance - 111); }); it('Staker can make multiple partial requests and then a full withdrawal', async () => { await contract.stake(staker1, stakerInitialBalance); await contract.requestWithdrawal(staker1, 100); await contract.requestWithdrawal(staker1, 10); await contract.requestWithdrawal(staker1, 1); await contract.elapseEpoch(); // increase time to next epoch, so user can withdraw funds await contract.withdrawStake(staker1, fundsRecipient, 111); }); it('Staker can make multiple partial requests and then multiple partial withdrawals', async () => { await contract.stake(staker1, stakerInitialBalance); await contract.requestWithdrawal(staker1, 100); await contract.requestWithdrawal(staker1, 10); await contract.requestWithdrawal(staker1, 1); await contract.elapseEpoch(); // increase time to next epoch, so user can withdraw funds await contract.withdrawStake(staker1, fundsRecipient, 50); await contract.withdrawStake(staker1, fundsRecipient, 60); await contract.withdrawStake(staker1, fundsRecipient, 1); }); it('Staker cannot withdraw funds if none are staked', async () => { await (0, chai_1.expect)(stakerSigner1.withdrawStake(staker1.address, stakerInitialBalance)).to.be.revertedWith('SM1Staking: Withdraw amount exceeds staker inactive balance'); }); }); describe('withdrawMaxStake', () => { it('Staker can request and withdraw full balance', async () => { await contract.stake(staker1, stakerInitialBalance); await contract.requestWithdrawal(staker1, stakerInitialBalance); await contract.elapseEpoch(); // increase time to next epoch, so user can withdraw funds await contract.withdrawMaxStake(staker1.address, fundsRecipient.address); // `dydxToken` should be sent to fundsRecipient, SafetyModuleV1 contract should own nothing // and user should have 0 staked token balance (0, chai_1.expect)(await ctx.dydxToken.balanceOf(fundsRecipient.address)).to.equal(stakerInitialBalance); (0, chai_1.expect)(await ctx.dydxToken.balanceOf(staker1.address)).to.equal(0); (0, chai_1.expect)(await ctx.dydxToken.balanceOf(ctx.safetyModule.address)).to.equal(0); (0, chai_1.expect)(await ctx.safetyModule.balanceOf(staker1.address)).to.equal(0); }); it('Staker can try to withdraw max stake even if there is none', async () => { await contract.withdrawMaxStake(staker1, staker1); }); }); describe('Transfer events', () => { it('Emits transfer events as expected', async () => { await (0, chai_1.expect)(ctx.safetyModule.connect(staker1).stake(stakerInitialBalance)) .to.emit(ctx.safetyModule, 'Transfer') .withArgs(constants_1.ZERO_ADDRESS, staker1.address, stakerInitialBalance); await (0, chai_1.expect)(ctx.safetyModule.connect(staker1).transfer(staker2.address, stakerInitialBalance)) .to.emit(ctx.safetyModule, 'Transfer') .withArgs(staker1.address, staker2.address, stakerInitialBalance); await (0, chai_1.expect)(ctx.safetyModule.connect(staker2).requestWithdrawal(stakerInitialBalance)) .not.to.emit(ctx.safetyModule, 'Transfer'); await contract.elapseEpoch(); // increase time to next epoch, so user can withdraw funds await (0, chai_1.expect)(ctx.safetyModule.connect(staker2).withdrawStake(fundsRecipient.address, stakerInitialBalance)) .to.emit(ctx.safetyModule, 'Transfer') .withArgs(staker2.address, constants_1.ZERO_ADDRESS, stakerInitialBalance); }); }); describe('claimRewards', () => { it('User with staked balance can claim rewards', async () => { // Repeat with different rewards rates. await contract.stake(staker1, stakerInitialBalance); let lastTimestamp = await (0, evm_1.latestBlockTimestamp)(); for (const rewardsRate of [1, ctx.config.SM_REWARDS_PER_SECOND]) { await contract.setRewardsPerSecond(rewardsRate); await contract.elapseEpoch(); // Earn one epoch of rewards. await contract.claimRewards(staker1, fundsRecipient, lastTimestamp); lastTimestamp = await (0, evm_1.latestBlockTimestamp)(); await contract.elapseEpoch(); // Earn one epoch of rewards. await contract.claimRewards(staker1, fundsRecipient, lastTimestamp); lastTimestamp = await (0, evm_1.latestBlockTimestamp)(); await contract.elapseEpoch(); // Earn one epoch of rewards. await contract.claimRewards(staker1, fundsRecipient, lastTimestamp); lastTimestamp = await (0, evm_1.latestBlockTimestamp)(); } }); it('User with nonzero staked balance for one epoch but emission rate was zero cannot claim rewards', async () => { await contract.stake(staker1, stakerInitialBalance); // increase time to next epoch, so user can earn rewards await contract.elapseEpoch(); // change EMISSION_RATE to be greater than 0 const emissionRate = 1; await (0, chai_1.expect)(ctx.safetyModule.connect(ctx.deployer).setRewardsPerSecond(emissionRate)) .to.emit(ctx.safetyModule, 'RewardsPerSecondUpdated') .withArgs(emissionRate); (0, chai_1.expect)(await stakerSigner1.callStatic.claimRewards(fundsRecipient.address)).to.equal(0); }); it('Multiple users can stake, requestWithdrawal, withdrawStake, and claimRewards', async () => { // change EMISSION_RATE to be greater than 0 const emissionRate = 1; await contract.setRewardsPerSecond(emissionRate); await contract.stake(staker1, stakerInitialBalance); const stakeTimestamp1 = await (0, evm_1.latestBlockTimestamp)(); await contract.stake(staker2, stakerInitialBalance2); const stakeTimestamp2 = await (0, evm_1.latestBlockTimestamp)(); await contract.requestWithdrawal(staker1, stakerInitialBalance); await contract.requestWithdrawal(staker2, stakerInitialBalance2); await contract.elapseEpoch(); const totalBalance = stakerInitialBalance + stakerInitialBalance2; const beforeClaim1 = await (0, evm_1.latestBlockTimestamp)(); const numTokens1 = new bignumber_js_1.default(beforeClaim1) .minus(stakeTimestamp1) .times(emissionRate) .times(stakerInitialBalance) .div(totalBalance) .toNumber(); (0, chai_1.expect)((await stakerSigner1.callStatic.claimRewards(staker1.address)).toNumber()).to.be.closeTo(numTokens1, 2); const beforeClaim2 = await (0, evm_1.latestBlockTimestamp)(); const numTokens2 = new bignumber_js_1.default(beforeClaim2) .minus(stakeTimestamp2) .times(emissionRate) .times(stakerInitialBalance2) .div(totalBalance) .toNumber(); (0, chai_1.expect)((await stakerSigner2.callStatic.claimRewards(staker2.address)).toNumber()).to.be.closeTo(numTokens2, 2); }); it('User with nonzero staked balance does not earn rewards after distributionEnd', async () => { await (0, evm_1.incrementTimeToTimestamp)(Number(distributionEnd) - ctx.config.EPOCH_LENGTH); // change EMISSION_RATE to be greater than 0 const emissionRate = 1; await (0, chai_1.expect)(ctx.safetyModule.connect(ctx.deployer).setRewardsPerSecond(emissionRate)) .to.emit(ctx.safetyModule, 'RewardsPerSecondUpdated') .withArgs(emissionRate); // expect the `stake` call to succeed, else we can't test `claimRewards` await contract.stake(staker1.address, stakerInitialBalance); const stakedTimestamp = await (0, evm_1.latestBlockTimestamp)(); // move multiple epochs forward so we're after DISTRIBUTION_END // (user should only earn rewards for last epoch) for (let i = 0; i < 5; i++) { await contract.elapseEpoch(); } const numTokens = new bignumber_js_1.default(distributionEnd) .minus(stakedTimestamp) .times(emissionRate) .toNumber(); await (0, chai_1.expect)(stakerSigner1.claimRewards(staker1.address)) .to.emit(ctx.safetyModule, 'ClaimedRewards') .withArgs(staker1.address, staker1.address, numTokens); // verify user can withdraw and doesn't earn additional rewards await contract.requestWithdrawal(staker1.address, stakerInitialBalance); await contract.elapseEpoch(); await contract.withdrawStake(staker1.address, staker1.address, stakerInitialBalance); // user shouldn't have any additional rewards since it's after DISTRIBUTION_END await (0, chai_1.expect)(stakerSigner1.claimRewards(staker1.address)) .to.emit(ctx.safetyModule, 'ClaimedRewards') .withArgs(staker1.address, staker1.address, 0); }); }); describe('transfer', () => { it('User with staked balance can transfer to another user', async () => { await contract.stake(staker1, stakerInitialBalance); await contract.transfer(staker1, staker2, stakerInitialBalance); (0, chai_1.expect)(await ctx.safetyModule.balanceOf(staker1.address)).to.equal(0); (0, chai_1.expect)(await ctx.safetyModule.balanceOf(staker2.address)).to.equal(stakerInitialBalance); }); it('User with staked balance for one epoch can transfer to another user and claim rewards', async () => { // change EMISSION_RATE to be greater than 0 const emissionRate = 1; await (0, chai_1.expect)(ctx.safetyModule.connect(ctx.deployer).setRewardsPerSecond(emissionRate)) .to.emit(ctx.safetyModule, 'RewardsPerSecondUpdated') .withArgs(emissionRate); await contract.stake(staker1, stakerInitialBalance); const stakeTimestamp = await (0, evm_1.latestBlockTimestamp)(); // increase time to next epoch, so user can earn rewards await contract.elapseEpoch(); await contract.transfer(staker1, staker2, stakerInitialBalance); const balanceBeforeClaiming = await ctx.dydxToken.balanceOf(staker1.address); const now = await (0, evm_1.latestBlockTimestamp)(); const numTokens = new bignumber_js_1.default(now) .minus(stakeTimestamp) .times(emissionRate) .toNumber(); await (0, chai_1.expect)(stakerSigner1.claimRewards(staker1.address)) .to.emit(ctx.safetyModule, 'ClaimedRewards') .withArgs(staker1.address, staker1.address, numTokens); (0, chai_1.expect)(await ctx.dydxToken.balanceOf(staker1.address)).to.equal(balanceBeforeClaiming.add(numTokens).toString()); }); }); describe('transferFrom', () => { it('User with staked balance can transfer to another user', async () => { await contract.stake(staker1, stakerInitialBalance); await contract.approve(staker1, staker2, stakerInitialBalance); await contract.transferFrom(staker2, staker1, staker2, stakerInitialBalance); (0, chai_1.expect)(await ctx.safetyModule.balanceOf(staker1.address)).to.equal(0); (0, chai_1.expect)(await ctx.safetyModule.balanceOf(staker2.address)).to.equal(stakerInitialBalance); }); }); }); } exports.addStakingTestCases = addStakingTestCases;