From 05a324171b2ca2708689b05d2cf5984c11259b2b Mon Sep 17 00:00:00 2001 From: Nick Addison Date: Tue, 30 Mar 2021 21:24:04 +1100 Subject: [PATCH] Feeder pools boost (#158) * Bumped solidity-coverage dep so it can compile unchecked Solidity statements * Bumped Solidity linter to handle unchecked statements * Added more protections to whitelistPools Added poolId to Whitelisted event Added tests for BoostDirector * BoostDirector.getBalance no longer fails if calling vault not whitelisted. Returns 0 instead. * Renamed pool to vault in BoostDirector --- contracts/interfaces/IBoostDirector.sol | 2 +- contracts/savings/BoostDirector.sol | 80 +++-- .../savings/MockBoostedSavingsVault.sol | 26 ++ package.json | 4 +- test/savings/savings-vault.spec.ts | 330 ++++++++++++++---- yarn.lock | 35 +- 6 files changed, 348 insertions(+), 129 deletions(-) create mode 100644 contracts/z_mocks/savings/MockBoostedSavingsVault.sol diff --git a/contracts/interfaces/IBoostDirector.sol b/contracts/interfaces/IBoostDirector.sol index f6cce38e..138376c2 100644 --- a/contracts/interfaces/IBoostDirector.sol +++ b/contracts/interfaces/IBoostDirector.sol @@ -10,5 +10,5 @@ interface IBoostDirector { bool _pokeNew ) external; - function whitelistPools(address[] calldata _pools) external; + function whitelistVaults(address[] calldata _vaults) external; } \ No newline at end of file diff --git a/contracts/savings/BoostDirector.sol b/contracts/savings/BoostDirector.sol index 5055bd44..008e9155 100644 --- a/contracts/savings/BoostDirector.sol +++ b/contracts/savings/BoostDirector.sol @@ -12,22 +12,22 @@ import { ImmutableModule } from "../shared/ImmutableModule.sol"; * @title BoostDirector * @author mStable * @notice Supports the directing of vMTA balance from Staking up to X accounts - * @dev Uses a bitmap to store the id's of a given users chosen pools in a gas efficient manner. + * @dev Uses a bitmap to store the id's of a given users chosen vaults in a gas efficient manner. */ contract BoostDirector is IBoostDirector, ImmutableModule { event Directed(address user, address boosted); event RedirectedBoost(address user, address boosted, address replaced); - event Whitelisted(address pool); + event Whitelisted(address vaultAddress, uint8 vaultId); // Read the vMTA balance from here IIncentivisedVotingLockup public immutable stakingContract; - // Whitelisted pools set by governance (only these pools can read balances) - uint8 private poolCount; - // Pool address -> internal id for tracking - mapping(address => uint8) public _pools; - // uint128 packed with up to 16 uint8's. Each uint is a pool ID + // Whitelisted vaults set by governance (only these vaults can read balances) + uint8 private vaultCount; + // Vault address -> internal id for tracking + mapping(address => uint8) public _vaults; + // uint128 packed with up to 16 uint8's. Each uint is a vault ID mapping(address => uint128) public _directedBitmap; @@ -41,58 +41,56 @@ contract BoostDirector is IBoostDirector, ImmutableModule { } /** - * @dev Initialize function - simply sets the initial array of whitelisted pools + * @dev Initialize function - simply sets the initial array of whitelisted vaults */ - function initialize(address[] calldata _newPools) external { - require(poolCount == 0, "Already initialized"); - _whitelistPools(_newPools); + function initialize(address[] calldata _newVaults) external { + require(vaultCount == 0, "Already initialized"); + _whitelistVaults(_newVaults); } /** - * @dev Whitelist pools - only callable by governance. Whitelists pools, unless they + * @dev Whitelist vaults - only callable by governance. Whitelists vaults, unless they * have already been whitelisted */ - function whitelistPools(address[] calldata _newPools) external override onlyGovernor { - _whitelistPools(_newPools); + function whitelistVaults(address[] calldata _newVaults) external override onlyGovernor { + _whitelistVaults(_newVaults); } /** - * @dev Takes an array of newPools. For each, determines if it is already whitelisted. - * If not, then increment poolCount and same the pool with new ID + * @dev Takes an array of newVaults. For each, determines if it is already whitelisted. + * If not, then increment vaultCount and same the vault with new ID */ - function _whitelistPools(address[] calldata _newPools) internal { - uint256 len = _newPools.length; + function _whitelistVaults(address[] calldata _newVaults) internal { + uint256 len = _newVaults.length; + require(len > 0, "Must be at least one vault"); for (uint256 i = 0; i < len; i++) { - uint8 id = _pools[_newPools[i]]; - require(id == 0, "Pool already whitelisted"); + uint8 id = _vaults[_newVaults[i]]; + require(id == 0, "Vault already whitelisted"); - poolCount += 1; - _pools[_newPools[i]] = poolCount; + vaultCount += 1; + _vaults[_newVaults[i]] = vaultCount; - emit Whitelisted(_newPools[i]); + emit Whitelisted(_newVaults[i], vaultCount); } } /*************************************** - POOL + Vault ****************************************/ - // A view methods for getBalance.. necessary? - // function readBalance(address _pool, address _user) external view returns (uint256) { - // } - /** - * @dev Gets the balance of a user that has been directed to the caller (a pool). - * If the user has not directed to this pool, or there are less than 3 directed, + * @dev Gets the balance of a user that has been directed to the caller (a vault). + * If the user has not directed to this vault, or there are less than 3 directed, * then add this to the list * @param _user Address of the user for which to get balance * @return Directed balance */ function getBalance(address _user) external override returns (uint256) { - // Get pool details - uint8 id = _pools[msg.sender]; - require(id > 0, "Pool not whitelisted"); + // Get vault details + uint8 id = _vaults[msg.sender]; + // If vault has not been whitelisted, just return zero + if(id == 0) return 0; // Get existing bitmap and balance uint128 bitmap = _directedBitmap[_user]; @@ -112,21 +110,21 @@ contract BoostDirector is IBoostDirector, ImmutableModule { } /** - * @dev Directs rewards to a pool, and removes them from the old pool. Provided - * that old is active and the new pool is whitelisted. - * @param _old Address of the old pool that will no longer get boosted - * @param _new Address of the new pool that will get boosted - * @param _pokeNew Bool to say if we should poke the boost on the new pool + * @dev Directs rewards to a vault, and removes them from the old vault. Provided + * that old is active and the new vault is whitelisted. + * @param _old Address of the old vault that will no longer get boosted + * @param _new Address of the new vault that will get boosted + * @param _pokeNew Bool to say if we should poke the boost on the new vault */ function setDirection( address _old, address _new, bool _pokeNew ) external override { - uint8 idOld = _pools[_old]; - uint8 idNew = _pools[_new]; + uint8 idOld = _vaults[_old]; + uint8 idNew = _vaults[_new]; - require(idOld > 0 && idNew > 0, "Pools not whitelisted"); + require(idOld > 0 && idNew > 0, "Vaults not whitelisted"); uint128 bitmap = _directedBitmap[msg.sender]; (bool isWhitelisted, uint8 count, uint8 pos) = _indexExists(bitmap, idOld); diff --git a/contracts/z_mocks/savings/MockBoostedSavingsVault.sol b/contracts/z_mocks/savings/MockBoostedSavingsVault.sol new file mode 100644 index 00000000..bb508426 --- /dev/null +++ b/contracts/z_mocks/savings/MockBoostedSavingsVault.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity 0.8.2; + +import { IBoostDirector } from "../../interfaces/IBoostDirector.sol"; + +contract MockBoostedSavingsVault { + + IBoostDirector public immutable boostDirector; + + event Poked(address indexed user); + event TestGetBalance(uint256 balance); + + constructor(address _boostDirector) { + boostDirector = IBoostDirector(_boostDirector); + } + + function pokeBoost(address _account) external { + emit Poked(_account); + } + + function testGetBalance(address _user) external returns (uint256 balance) { + balance = boostDirector.getBalance(_user); + + emit TestGetBalance(balance); + } +} \ No newline at end of file diff --git a/package.json b/package.json index b8e4a3f2..e4fa9990 100644 --- a/package.json +++ b/package.json @@ -65,8 +65,8 @@ "prettier-plugin-solidity": "^1.0.0-beta.3", "sol-merger": "^3.0.1", "solc": "0.8.2", - "solhint": "^3.3.2", - "solidity-coverage": "0.7.12", + "solhint": "^3.3.4", + "solidity-coverage": "0.7.16", "ts-generator": "^0.1.1", "ts-node": "^9.1.1", "tsconfig-paths": "^3.9.0", diff --git a/test/savings/savings-vault.spec.ts b/test/savings/savings-vault.spec.ts index 513d4c0c..0d775760 100644 --- a/test/savings/savings-vault.spec.ts +++ b/test/savings/savings-vault.spec.ts @@ -1,3 +1,5 @@ +/* eslint-disable prefer-destructuring */ +/* eslint-disable no-underscore-dangle */ /* eslint-disable no-await-in-loop */ import { ethers } from "hardhat" @@ -22,6 +24,8 @@ import { AssetProxy__factory, BoostDirector__factory, BoostDirector, + MockBoostedSavingsVault, + MockBoostedSavingsVault__factory, } from "types/generated" import { shouldBehaveLikeDistributionRecipient, @@ -76,7 +80,7 @@ describe("SavingsVault", async () => { let rewardsDistributor: Account let rewardToken: MockERC20 - let iMasset: MockERC20 + let imAsset: MockERC20 let nexus: MockNexus let savingsVault: BoostedSavingsVault let stakingContract: MockStakingContract @@ -101,13 +105,7 @@ describe("SavingsVault", async () => { let denom = parseFloat(utils.formatUnits(scaledBalance)) denom **= 0.875 const flooredMTA = vMTA.gt(maxVMTA) ? maxVMTA : vMTA - let rhs = floor.add( - flooredMTA - .mul(coeff) - .div(10) - .mul(fullScale) - .div(simpleToExactAmount(denom)), - ) + let rhs = floor.add(flooredMTA.mul(coeff).div(10).mul(fullScale).div(simpleToExactAmount(denom))) rhs = rhs.gt(minBoost) ? rhs : minBoost return rhs.gt(maxBoost) ? maxBoost : rhs } @@ -117,38 +115,26 @@ describe("SavingsVault", async () => { const lockedRewards = (total: BN): BN => total.div(100).mul(67) const redeployRewards = async (priceCoefficient = priceCoeff): Promise => { - nexus = await (await new MockNexus__factory(sa.default.signer)).deploy(sa.governor.address, DEAD_ADDRESS, DEAD_ADDRESS) - rewardToken = await (await new MockERC20__factory(sa.default.signer)).deploy( - "Reward", - "RWD", - 18, - rewardsDistributor.address, - 10000000, - ) - iMasset = await (await new MockERC20__factory(sa.default.signer)).deploy( - "Interest bearing mUSD", - "imUSD", - 18, - sa.default.address, - 1000000, - ) - stakingContract = await (await new MockStakingContract__factory(sa.default.signer)).deploy() + nexus = await new MockNexus__factory(sa.default.signer).deploy(sa.governor.address, DEAD_ADDRESS, DEAD_ADDRESS) + rewardToken = await new MockERC20__factory(sa.default.signer).deploy("Reward", "RWD", 18, rewardsDistributor.address, 10000000) + imAsset = await new MockERC20__factory(sa.default.signer).deploy("Interest bearing mUSD", "imUSD", 18, sa.default.address, 1000000) + stakingContract = await new MockStakingContract__factory(sa.default.signer).deploy() - boostDirector = await (await new BoostDirector__factory(sa.default.signer)).deploy(nexus.address, stakingContract.address) + boostDirector = await new BoostDirector__factory(sa.default.signer).deploy(nexus.address, stakingContract.address) const vaultFactory = await new BoostedSavingsVault__factory(sa.default.signer) - const impl = await vaultFactory.deploy( + const vaultImpl = await vaultFactory.deploy( nexus.address, - iMasset.address, + imAsset.address, boostDirector.address, priceCoefficient, coeff, rewardToken.address, ) - const data = impl.interface.encodeFunctionData("initialize", [rewardsDistributor.address, "Vault A", "vA"]) - const proxy = await (await new AssetProxy__factory(sa.default.signer)).deploy(impl.address, sa.dummy4.address, data) - await boostDirector.initialize([proxy.address]) - return vaultFactory.attach(proxy.address) + const data = vaultImpl.interface.encodeFunctionData("initialize", [rewardsDistributor.address, "Vault A", "vA"]) + const vaultProxy = await new AssetProxy__factory(sa.default.signer).deploy(vaultImpl.address, sa.dummy4.address, data) + await boostDirector.initialize([vaultProxy.address]) + return vaultFactory.attach(vaultProxy.address) } const snapshotStakingData = async (sender = sa.default, beneficiary = sa.default): Promise => { @@ -169,8 +155,8 @@ describe("SavingsVault", async () => { totalSupply: await savingsVault.totalSupply(), }, tokenBalance: { - sender: await iMasset.balanceOf(sender.address), - contract: await iMasset.balanceOf(savingsVault.address), + sender: await imAsset.balanceOf(sender.address), + contract: await imAsset.balanceOf(savingsVault.address), }, vMTABalance: await stakingContract.balanceOf(beneficiary.address), userData: { @@ -211,7 +197,7 @@ describe("SavingsVault", async () => { it("should set all initial state", async () => { // Set in constructor expect(await savingsVault.nexus()).to.eq(nexus.address) - expect(await savingsVault.stakingToken()).to.eq(iMasset.address) + expect(await savingsVault.stakingToken()).to.eq(imAsset.address) expect(await savingsVault.boostDirector()).to.eq(boostDirector.address) expect(await savingsVault.rewardsToken()).to.eq(rewardToken.address) expect(await savingsVault.rewardsDistributor()).to.eq(rewardsDistributor.address) @@ -261,10 +247,7 @@ describe("SavingsVault", async () => { : timeAfter.sub(beforeData.contractData.lastUpdateTime) const increaseInRewardPerToken = beforeData.boostBalance.totalSupply.eq(BN.from(0)) ? BN.from(0) - : beforeData.contractData.rewardRate - .mul(timeApplicableToRewards) - .mul(fullScale) - .div(beforeData.boostBalance.totalSupply) + : beforeData.contractData.rewardRate.mul(timeApplicableToRewards).mul(fullScale).div(beforeData.boostBalance.totalSupply) expect(beforeData.contractData.rewardPerTokenStored.add(increaseInRewardPerToken)).to.be.eq( afterData.contractData.rewardPerTokenStored, ) @@ -325,13 +308,11 @@ describe("SavingsVault", async () => { expect(isExistingStaker).eq(true) } // 2. Approve staking token spending and send the TX - await iMasset.connect(sender.signer).approve(savingsVault.address, stakeAmount) + await imAsset.connect(sender.signer).approve(savingsVault.address, stakeAmount) const tx = senderIsBeneficiary ? savingsVault.connect(sender.signer)["stake(uint256)"](stakeAmount) : savingsVault.connect(sender.signer)["stake(address,uint256)"](beneficiary.address, stakeAmount) - await expect(tx) - .to.emit(savingsVault, "Staked") - .withArgs(beneficiary.address, stakeAmount, sender.address) + await expect(tx).to.emit(savingsVault, "Staked").withArgs(beneficiary.address, stakeAmount, sender.address) // 3. Ensure rewards are accrued to the beneficiary const afterData = await snapshotStakingData(sender, beneficiary) @@ -357,9 +338,7 @@ describe("SavingsVault", async () => { const expectSuccesfulFunding = async (rewardUnits: BN): Promise => { const beforeData = await snapshotStakingData() const tx = savingsVault.connect(rewardsDistributor.signer).notifyRewardAmount(rewardUnits) - await expect(tx) - .to.emit(savingsVault, "RewardAdded") - .withArgs(rewardUnits) + await expect(tx).to.emit(savingsVault, "RewardAdded").withArgs(rewardUnits) const cur = BN.from(await getTimestamp()) const leftOverRewards = beforeData.contractData.rewardRate.mul( @@ -401,9 +380,7 @@ describe("SavingsVault", async () => { // 2. Send withdrawal tx const tx = savingsVault.connect(sender.signer).withdraw(withdrawAmount) - await expect(tx) - .to.emit(savingsVault, "Withdrawn") - .withArgs(sender.address, withdrawAmount) + await expect(tx).to.emit(savingsVault, "Withdrawn").withArgs(sender.address, withdrawAmount) // 3. Expect Rewards to accrue to the beneficiary // StakingToken balance of sender @@ -468,7 +445,7 @@ describe("SavingsVault", async () => { }) it("should fail if staker has insufficient balance", async () => { - await iMasset.connect(sa.dummy2.signer).approve(savingsVault.address, 1) + await imAsset.connect(sa.dummy2.signer).approve(savingsVault.address, 1) await expect(savingsVault.connect(sa.dummy2.signer)["stake(uint256)"](1)).to.be.revertedWith("VM Exception") }) }) @@ -799,8 +776,8 @@ describe("SavingsVault", async () => { savingsVault = await redeployRewards() staker2 = sa.dummy1 staker3 = sa.dummy2 - await iMasset.transfer(staker2.address, staker2Stake) - await iMasset.transfer(staker3.address, staker3Stake) + await imAsset.transfer(staker2.address, staker2Stake) + await imAsset.transfer(staker3.address, staker3Stake) }) it("should accrue rewards on a pro rata basis", async () => { /* @@ -906,35 +883,29 @@ describe("SavingsVault", async () => { context("using staking / reward tokens with diff decimals", () => { before(async () => { - rewardToken = await (await new MockERC20__factory(sa.default.signer)).deploy( - "Reward", - "RWD", - 12, - rewardsDistributor.address, - 10000000, - ) - iMasset = await (await new MockERC20__factory(sa.default.signer)).deploy( + rewardToken = await new MockERC20__factory(sa.default.signer).deploy("Reward", "RWD", 12, rewardsDistributor.address, 10000000) + imAsset = await new MockERC20__factory(sa.default.signer).deploy( "Interest bearing mUSD", "imUSD", 16, sa.default.address, 1000000, ) - stakingContract = await (await new MockStakingContract__factory(sa.default.signer)).deploy() + stakingContract = await new MockStakingContract__factory(sa.default.signer).deploy() - boostDirector = await (await new BoostDirector__factory(sa.default.signer)).deploy(nexus.address, stakingContract.address) + boostDirector = await new BoostDirector__factory(sa.default.signer).deploy(nexus.address, stakingContract.address) - const vaultFactory = await new BoostedSavingsVault__factory(sa.default.signer) + const vaultFactory = new BoostedSavingsVault__factory(sa.default.signer) const impl = await vaultFactory.deploy( nexus.address, - iMasset.address, + imAsset.address, boostDirector.address, priceCoeff, coeff, rewardToken.address, ) const data = impl.interface.encodeFunctionData("initialize", [rewardsDistributor.address, "Vault A", "vA"]) - const proxy = await (await new AssetProxy__factory(sa.default.signer)).deploy(impl.address, sa.dummy4.address, data) + const proxy = await new AssetProxy__factory(sa.default.signer).deploy(impl.address, sa.dummy4.address, data) savingsVault = vaultFactory.attach(proxy.address) await boostDirector.initialize([proxy.address]) }) @@ -954,13 +925,8 @@ describe("SavingsVault", async () => { const rewardPerToken = await savingsVault.rewardPerToken() assertBNClose( rewardPerToken, - ONE_WEEK.mul(rewardRate) - .mul(fullScale) - .div(boosted), - BN.from(1) - .mul(rewardRate) - .mul(fullScale) - .div(boosted), + ONE_WEEK.mul(rewardRate).mul(fullScale).div(boosted), + BN.from(1).mul(rewardRate).mul(fullScale).div(boosted), ) // Calc estimated unclaimed reward for the user @@ -1414,9 +1380,7 @@ describe("SavingsVault", async () => { // claims all immediate unlocks const tx = savingsVault["exit(uint256,uint256)"](first, last) await expect(tx).to.emit(savingsVault, "RewardPaid") - await expect(tx) - .to.emit(savingsVault, "Withdrawn") - .withArgs(sa.default.address, hunnit) + await expect(tx).to.emit(savingsVault, "Withdrawn").withArgs(sa.default.address, hunnit) }) }) }) @@ -1554,4 +1518,222 @@ describe("SavingsVault", async () => { }) }) }) + context("Govern boost director", () => { + let vaultA: Account + let vaultB: Account + let vaultC: Account + let vaultD: Account + let vaultUnlisted: Account + let user1NoStake: Account + let user2Staked: Account + let user3Staked: Account + before(async () => { + vaultA = sa.dummy1 + vaultB = sa.dummy2 + vaultC = sa.dummy3 + vaultD = sa.dummy4 + vaultUnlisted = sa.all[10] + user1NoStake = sa.all[11] + user2Staked = sa.all[12] + user3Staked = sa.all[13] + + rewardToken = await new MockERC20__factory(sa.default.signer).deploy("Reward", "RWD", 18, rewardsDistributor.address, 10000000) + stakingContract = await new MockStakingContract__factory(sa.default.signer).deploy() + await stakingContract.setBalanceOf(user2Staked.address, 20000) + await stakingContract.setBalanceOf(user3Staked.address, 30000) + }) + context("Whitelisting boost savings vaults", () => { + before(async () => { + boostDirector = await new BoostDirector__factory(sa.default.signer).deploy(nexus.address, stakingContract.address) + await boostDirector.initialize([vaultA.address]) + }) + it("should get first vault A", async () => { + expect(await boostDirector._vaults(vaultA.address)).to.eq(1) + }) + it("should fail if not governor", async () => { + let tx = boostDirector.connect(sa.default.signer).whitelistVaults([vaultB.address]) + await expect(tx).to.revertedWith("Only governor can execute") + tx = boostDirector.connect(sa.fundManager.signer).whitelistVaults([vaultB.address]) + await expect(tx).to.revertedWith("Only governor can execute") + }) + it("should succeed in whitelisting no boost savings vault", async () => { + const tx = boostDirector.connect(sa.governor.signer).whitelistVaults([]) + await expect(tx).to.revertedWith("Must be at least one vault") + }) + it("should succeed in whitelisting one boost savings vault", async () => { + const tx = boostDirector.connect(sa.governor.signer).whitelistVaults([vaultB.address]) + await expect(tx).to.emit(boostDirector, "Whitelisted").withArgs(vaultB.address, 2) + expect(await boostDirector._vaults(vaultB.address)).to.eq(2) + }) + it("should fail if already whitelisted", async () => { + const tx = boostDirector.connect(sa.governor.signer).whitelistVaults([vaultB.address]) + await expect(tx).to.revertedWith("Vault already whitelisted") + }) + it("should succeed in whitelisting two boost savings vault", async () => { + const tx = boostDirector.connect(sa.governor.signer).whitelistVaults([vaultC.address, vaultD.address]) + await expect(tx).to.emit(boostDirector, "Whitelisted").withArgs(vaultC.address, 3) + await expect(tx).to.emit(boostDirector, "Whitelisted").withArgs(vaultD.address, 4) + expect(await boostDirector._vaults(vaultC.address)).to.eq(3) + expect(await boostDirector._vaults(vaultD.address)).to.eq(4) + }) + }) + context("get boost balance", () => { + let boostDirectorVaultA: BoostDirector + let boostDirectorVaultB: BoostDirector + let boostDirectorVaultC: BoostDirector + let boostDirectorVaultD: BoostDirector + before(async () => { + boostDirector = await new BoostDirector__factory(sa.default.signer).deploy(nexus.address, stakingContract.address) + await boostDirector.initialize([vaultA.address, vaultB.address, vaultC.address, vaultD.address]) + boostDirectorVaultA = boostDirector.connect(vaultA.signer) + boostDirectorVaultB = boostDirector.connect(vaultB.signer) + boostDirectorVaultC = boostDirector.connect(vaultC.signer) + boostDirectorVaultD = boostDirector.connect(vaultD.signer) + }) + context("called from vault A", () => { + context("for user 1 with nothing staked", () => { + it("should get zero balance", async () => { + const bal = await boostDirectorVaultA.callStatic.getBalance(user1NoStake.address) + expect(bal).to.eq(0) + }) + it("should add user to boost director", async () => { + const tx = boostDirectorVaultA.getBalance(user1NoStake.address) + await expect(tx).to.emit(boostDirector, "Directed").withArgs(user1NoStake.address, vaultA.address) + }) + it("should fail to add user to boost director again", async () => { + const tx = boostDirectorVaultA.getBalance(user1NoStake.address) + await expect(tx).to.not.emit(boostDirector, "Directed") + }) + it("should get user zero balance after being added", async () => { + const bal = await boostDirectorVaultA.callStatic.getBalance(user1NoStake.address) + expect(bal).to.eq(0) + }) + }) + context("for user 2 with 20,000 staked", () => { + it("should get user 2 balance", async () => { + const bal = await boostDirectorVaultA.callStatic.getBalance(user2Staked.address) + expect(bal).to.eq(20000) + }) + it("should add user 2 to boost director", async () => { + const tx = boostDirectorVaultA.getBalance(user2Staked.address) + await expect(tx).to.emit(boostDirector, "Directed").withArgs(user2Staked.address, vaultA.address) + }) + it("should fail to add user to boost director again", async () => { + const tx = boostDirectorVaultA.getBalance(user2Staked.address) + await expect(tx).to.not.emit(boostDirector, "Directed") + }) + it("should get user 2 balance after being added", async () => { + const bal = await boostDirectorVaultA.callStatic.getBalance(user2Staked.address) + expect(bal).to.eq(20000) + }) + }) + }) + context("user 3 with 30,000 staked added to vaults A, B and C but not D", () => { + const userStakedBalance = 30000 + it("vault A should get user balance before being added to any vaults", async () => { + const bal = await boostDirectorVaultA.callStatic.getBalance(user3Staked.address) + expect(bal).to.eq(userStakedBalance) + }) + it("vault A should add user to boost director", async () => { + const tx = boostDirectorVaultA.getBalance(user3Staked.address) + await expect(tx).to.emit(boostDirector, "Directed").withArgs(user3Staked.address, vaultA.address) + }) + it("vault B should add user to boost director", async () => { + const tx = boostDirectorVaultB.getBalance(user3Staked.address) + await expect(tx).to.emit(boostDirector, "Directed").withArgs(user3Staked.address, vaultB.address) + }) + it("vault C should add user to boost director", async () => { + const tx = boostDirectorVaultC.getBalance(user3Staked.address) + await expect(tx).to.emit(boostDirector, "Directed").withArgs(user3Staked.address, vaultC.address) + }) + it("vault C should get user balance after user added", async () => { + const bal = await boostDirectorVaultC.callStatic.getBalance(user3Staked.address) + expect(bal).to.eq(userStakedBalance) + }) + it("vault D should fail to add user as its the fourth", async () => { + const tx = boostDirectorVaultD.getBalance(user3Staked.address) + await expect(tx).to.not.emit(boostDirector, "Directed") + }) + it("vault D should get zero balance for the user", async () => { + const bal = await boostDirectorVaultD.callStatic.getBalance(user3Staked.address) + expect(bal).to.eq(0) + }) + it("vault A should still user balance", async () => { + const bal = await boostDirectorVaultA.callStatic.getBalance(user3Staked.address) + expect(bal).to.eq(userStakedBalance) + }) + it("vault B should still fer user balance", async () => { + const bal = await boostDirectorVaultB.callStatic.getBalance(user3Staked.address) + expect(bal).to.eq(userStakedBalance) + }) + it("vault C should still user balance", async () => { + const bal = await boostDirectorVaultC.callStatic.getBalance(user3Staked.address) + expect(bal).to.eq(userStakedBalance) + }) + }) + context("adding non whitelisted vaults", () => { + it("should fail to add user from unlisted vault", async () => { + const tx = boostDirector.connect(vaultUnlisted.signer).getBalance(user2Staked.address) + await expect(tx).to.not.emit(boostDirector, "Directed") + }) + it("should get zero balance for unlisted vault", async () => { + const bal = await boostDirector.connect(vaultUnlisted.signer).callStatic.getBalance(user3Staked.address) + expect(bal).to.eq(0) + }) + it("should fail for user to add themselves as a vault", async () => { + const tx = boostDirector.connect(user2Staked.signer).getBalance(user2Staked.address) + await expect(tx).to.not.emit(boostDirector, "Directed") + }) + }) + }) + context("redirect staked rewards to new boost savings vault", () => { + let mockedVaults: MockBoostedSavingsVault[] + before(async () => { + boostDirector = await new BoostDirector__factory(sa.default.signer).deploy(nexus.address, stakingContract.address) + + const mockedVaultsPromises = [1, 2, 3, 4, 5].map(() => + new MockBoostedSavingsVault__factory(sa.default.signer).deploy(boostDirector.address), + ) + mockedVaults = await Promise.all(mockedVaultsPromises) + const mockedVaultAddresses = mockedVaults.map((vault) => vault.address) + await boostDirector.initialize(mockedVaultAddresses) + + // For user 1, add the first three vaults to the Boost Director. + await mockedVaults[0].testGetBalance(user1NoStake.address) + await mockedVaults[1].testGetBalance(user1NoStake.address) + await mockedVaults[2].testGetBalance(user1NoStake.address) + // For user 2, add the first two vaults to the Boost Director. + await mockedVaults[0].testGetBalance(user2Staked.address) + await mockedVaults[1].testGetBalance(user2Staked.address) + // For user 3, just add the first vault + await mockedVaults[0].testGetBalance(user3Staked.address) + }) + it("should fail as old vault is not whitelisted", async () => { + const tx = boostDirector.connect(user1NoStake.signer).setDirection(sa.dummy1.address, mockedVaults[3].address, false) + await expect(tx).to.revertedWith("Vaults not whitelisted") + }) + it("should fail as user 1 has not been added to the old vault 4", async () => { + const tx = boostDirector.connect(user1NoStake.signer).setDirection(mockedVaults[4].address, mockedVaults[3].address, false) + await expect(tx).to.revertedWith("No need to replace old") + }) + it("should fail as new vault is not whitelisted", async () => { + const tx = boostDirector.connect(user1NoStake.signer).setDirection(mockedVaults[0].address, sa.dummy1.address, false) + await expect(tx).to.revertedWith("Vaults not whitelisted") + }) + it("user 1 should succeed in replacing vault 1 with vault 4 that is not poked", async () => { + await boostDirector.connect(user1NoStake.signer).setDirection(mockedVaults[0].address, mockedVaults[3].address, false) + }) + it("user 1 should succeed in replacing vault 2 vault 5 that is poked", async () => { + await boostDirector.connect(user1NoStake.signer).setDirection(mockedVaults[1].address, mockedVaults[4].address, true) + }) + it("should fail as user 2 only has 2 vault", async () => { + const tx = boostDirector.connect(user2Staked.signer).setDirection(mockedVaults[0].address, mockedVaults[3].address, false) + await expect(tx).to.revertedWith("No need to replace old") + }) + it("should fail as user 3 only has 1 vault", async () => { + const tx = boostDirector.connect(user3Staked.signer).setDirection(mockedVaults[0].address, mockedVaults[3].address, false) + await expect(tx).to.revertedWith("No need to replace old") + }) + }) + }) }) diff --git a/yarn.lock b/yarn.lock index 2182d94c..f2e70cf8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -646,7 +646,12 @@ resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.11.0.tgz#28bc1972e1620f7b388b485bca76a78ac2cb5c59" integrity sha512-IaC4IaW8uoOB8lmEkw6c19y1vJBK/+7SzAbGQ+LmBYRPXSLNB+UgpORvmcAJEXhB04kWKyz/Os1U8onqm6U/+w== -"@solidity-parser/parser@^0.8.1", "@solidity-parser/parser@^0.8.2": +"@solidity-parser/parser@^0.12.0": + version "0.12.0" + resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.12.0.tgz#18a0fb2a9d2484b23176f63b16093c64794fc323" + integrity sha512-DT3f/Aa4tQysZwUsuqBwvr8YRJzKkvPUKV/9o2/o5EVw3xqlbzmtx4O60lTUcZdCawL+N8bBLNUyOGpHjGlJVQ== + +"@solidity-parser/parser@^0.8.2": version "0.8.2" resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.8.2.tgz#a6a5e93ac8dca6884a99a532f133beba59b87b69" integrity sha512-8LySx3qrNXPgB5JiULfG10O3V7QTxI/TLzSw5hFQhXWSkVxZBAv4rZQ0sYgLEbc8g3L2lmnujj1hKul38Eu5NQ== @@ -8099,6 +8104,13 @@ semver@^7.2.1, semver@^7.3.2: dependencies: lru-cache "^6.0.0" +semver@^7.3.4: + version "7.3.5" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" + integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== + dependencies: + lru-cache "^6.0.0" + semver@~5.4.1: version "5.4.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" @@ -8394,12 +8406,12 @@ solc@^0.6.3: semver "^5.5.0" tmp "0.0.33" -solhint@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/solhint/-/solhint-3.3.2.tgz#ebd7270bb50fd378b427d7a6fc9f2a7fd00216c0" - integrity sha512-8tHCkIAk1axLLG6Qu2WIH3GgNABonj9eAWejJbov3o3ujkZQRNHeHU1cC4/Dmjsh3Om7UzFFeADUHu2i7ZJeiw== +solhint@^3.3.4: + version "3.3.4" + resolved "https://registry.yarnpkg.com/solhint/-/solhint-3.3.4.tgz#81770c60eeb027e6e447cb91ed599baf5e888e09" + integrity sha512-AEyjshF/PC6kox1c1l79Pji+DK9WVuk5u2WEh6bBKt188gWa63NBOAgYg0fBRr5CTUmsuGc1sGH7dgUVs83mKw== dependencies: - "@solidity-parser/parser" "^0.8.2" + "@solidity-parser/parser" "^0.12.0" ajv "^6.6.1" antlr4 "4.7.1" ast-parents "0.0.1" @@ -8421,12 +8433,12 @@ solidity-comments-extractor@^0.0.4: resolved "https://registry.yarnpkg.com/solidity-comments-extractor/-/solidity-comments-extractor-0.0.4.tgz#ce420aef23641ffd0131c7d80ba85b6e1e42147e" integrity sha512-58glBODwXIKMaQ7rfcJOrWtFQMMOK28tJ0/LcB5Xhu7WtAxk4UX2fpgKPuaL41XjMp/y0gAa1MTLqk018wuSzA== -solidity-coverage@0.7.12: - version "0.7.12" - resolved "https://registry.yarnpkg.com/solidity-coverage/-/solidity-coverage-0.7.12.tgz#0c6ca866cc6e15bfebecd71725666b3bc121f263" - integrity sha512-9iCiZU1rppeZEaprN9j7QSNWzOyMqipQ1VYMPbeipVr2HI0qdxkmg/QtizyJyHz35eClmuxyBNEzXTMaFnldTA== +solidity-coverage@0.7.16: + version "0.7.16" + resolved "https://registry.yarnpkg.com/solidity-coverage/-/solidity-coverage-0.7.16.tgz#c8c8c46baa361e2817bbf275116ddd2ec90a55fb" + integrity sha512-ttBOStywE6ZOTJmmABSg4b8pwwZfYKG8zxu40Nz+sRF5bQX7JULXWj/XbX0KXps3Fsp8CJXg8P29rH3W54ipxw== dependencies: - "@solidity-parser/parser" "^0.8.1" + "@solidity-parser/parser" "^0.12.0" "@truffle/provider" "^0.2.24" chalk "^2.4.2" death "^1.1.0" @@ -8442,6 +8454,7 @@ solidity-coverage@0.7.12: pify "^4.0.1" recursive-readdir "^2.2.2" sc-istanbul "^0.4.5" + semver "^7.3.4" shelljs "^0.8.3" web3-utils "^1.3.0"