UNPKG

@gooddollar/goodprotocol

Version:
1,346 lines (1,226 loc) 80.2 kB
import { ethers, upgrades } from "hardhat"; import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; import { BigNumber, Contract } from "ethers"; import { expect } from "chai"; import { GoodMarketMaker } from "../../types"; import { createDAO, increaseTime, advanceBlocks, deployUniswap, getStakingFactory } from "../helpers"; import ContributionCalculation from "@gooddollar/goodcontracts/stakingModel/build/contracts/ContributionCalculation.json"; import IUniswapV2Pair from "@uniswap/v2-core/build/IUniswapV2Pair.json"; const BN = ethers.BigNumber; export const NULL_ADDRESS = ethers.constants.AddressZero; export const BLOCK_INTERVAL = 30; describe("StakingRewards - staking with cDAI mocks and get Rewards in GoodDollar", () => { let dai: Contract; let bat: Contract; let comp: Contract; let pair: Contract, uniswapRouter: Contract; let cDAI, cDAI1, cDAI2, cDAI3, cBat: Contract; let gasFeeOracle: Contract, daiEthOracle: Contract, daiUsdOracle: Contract, batUsdOracle: Contract, ethUsdOracle: Contract, compUsdOracle: Contract; let goodReserve: Contract; let goodCompoundStaking; let goodFundManager: Contract; let avatar, goodDollar, identity, marketMaker: GoodMarketMaker, contribution, controller, founder, staker, schemeMock, signers, nameService, initializeToken, setDAOAddress, genericCall, goodCompoundStakingFactory, goodCompoundStakingTestFactory, runAsAvatarOnly, deployStaking; before(async () => { [founder, staker, ...signers] = await ethers.getSigners(); schemeMock = signers.pop(); let { controller: ctrl, avatar: av, gd, identity, nameService: ns, setDAOAddress: sda, setSchemes, marketMaker: mm, daiAddress, cdaiAddress, reserve, setReserveToken, genericCall: gc, COMP, runAsAvatarOnly: raao } = await loadFixture(createDAO); runAsAvatarOnly = raao const cdaiFactory = await ethers.getContractFactory("cDAIMock"); const cBatFactory = await ethers.getContractFactory("cBATMock"); const goodFundManagerFactory = await ethers.getContractFactory( "GoodFundManager" ); goodCompoundStakingFactory = await getStakingFactory( "GoodCompoundStakingV2" ); goodCompoundStakingTestFactory = await getStakingFactory( "GoodCompoundStakingTest" ); const daiFactory = await ethers.getContractFactory("DAIMock"); genericCall = gc; dai = await ethers.getContractAt("DAIMock", daiAddress); cDAI = await ethers.getContractAt("cDAIMock", cdaiAddress); comp = COMP; const uniswap = await deployUniswap(comp, dai); uniswapRouter = uniswap.router; const { factory, weth, compPairContract, daiPairContract } = uniswap; avatar = av; controller = ctrl; setDAOAddress = sda; nameService = ns; initializeToken = setReserveToken; goodReserve = reserve; console.log("deployed dao", { founder: founder.address, gd, identity, controller, avatar }); goodFundManager = await upgrades.deployProxy( goodFundManagerFactory, [nameService.address], { kind: "uups" } ); await setDAOAddress("FUND_MANAGER", goodFundManager.address); console.log("Deployed goodfund manager", { manager: goodFundManager.address }); goodDollar = await ethers.getContractAt("IGoodDollar", gd); contribution = await ethers.getContractAt( ContributionCalculation.abi, await nameService.getAddress("CONTRIBUTION_CALCULATION") ); marketMaker = mm; console.log("deployed contribution, deploying reserve...", { founder: founder.address }); console.log("setting permissions..."); const tokenUsdOracleFactory = await ethers.getContractFactory( "BatUSDMockOracle" ); await setDAOAddress("UNISWAP_ROUTER", uniswapRouter.address); daiUsdOracle = await tokenUsdOracleFactory.deploy(); const compUsdOracleFactory = await ethers.getContractFactory( "CompUSDMockOracle" ); compUsdOracle = await compUsdOracleFactory.deploy(); deployStaking = (token, itoken, blocksThreashold = "50") => goodCompoundStakingFactory.deploy().then(async contract => { await contract.init( token || dai.address, itoken || cDAI.address, nameService.address, "Good DAI", "gDAI", blocksThreashold, daiUsdOracle.address, compUsdOracle.address, [] ); return contract; }); goodCompoundStaking = await deployStaking(null, null, "172800"); console.log("initializing marketmaker..."); cDAI1 = await cdaiFactory.deploy(dai.address); const cdaiLowWorthFactory = await ethers.getContractFactory( "cDAILowWorthMock" ); cDAI2 = await cdaiLowWorthFactory.deploy(dai.address); const cdaiNonMintableFactory = await ethers.getContractFactory( "cDAINonMintableMock" ); cDAI3 = await cdaiNonMintableFactory.deploy(dai.address); bat = await daiFactory.deploy(); // Another erc20 token for uniswap router test cBat = await cBatFactory.deploy(bat.address); await initializeToken( cDAI1.address, "100", //1gd "10000", //0.0001 cDai "1000000" //100% rr ); await initializeToken( cDAI2.address, "100", //1gd "10000", //0.0001 cDai "1000000" //100% rr ); await initializeToken( cDAI3.address, "100", //1gd "10000", //0.0001 cDai "1000000" //100% rr ); await factory.createPair(bat.address, dai.address); // Create tokenA and dai pair const pairAddress = factory.getPair(bat.address, dai.address); pair = new Contract( pairAddress, JSON.stringify(IUniswapV2Pair.abi), staker ).connect(founder); await setDAOAddress("CDAI", cDAI.address); await setDAOAddress("DAI", dai.address); batUsdOracle = await tokenUsdOracleFactory.deploy(); await dai["mint(address,uint256)"]( founder.address, ethers.utils.parseEther("2000000") ); await bat["mint(address,uint256)"]( founder.address, ethers.utils.parseEther("2000000") ); await addLiquidity( ethers.utils.parseEther("2000000"), ethers.utils.parseEther("2000000") ); gasFeeOracle = await ethers.getContractAt( "GasPriceMockOracle", await nameService.getAddress("GAS_PRICE_ORACLE") ); daiEthOracle = await ethers.getContractAt( "DaiEthPriceMockOracle", await nameService.getAddress("DAI_ETH_ORACLE") ); await setDAOAddress("MARKET_MAKER", marketMaker.address); }); it("should be set rewards per block for particular stacking contract", async () => { const goodFundManagerFactory = await ethers.getContractFactory( "GoodFundManager" ); const currentBlockNumber = await ethers.provider.getBlockNumber(); const encodedData = goodFundManagerFactory.interface.encodeFunctionData( "setStakingReward", [ "1000", goodCompoundStaking.address, currentBlockNumber - 5, currentBlockNumber + 10, false ] // set 10 gd per block ); await genericCall(goodFundManager.address, encodedData, avatar, 0); let rewardPerBlock = await goodFundManager.rewardsForStakingContract( goodCompoundStaking.address ); expect(rewardPerBlock[0].toString()).to.be.equal("1000"); expect(rewardPerBlock[1].toString()).to.be.equal( (currentBlockNumber - 5).toString() ); expect(rewardPerBlock[2].toString()).to.be.equal( (currentBlockNumber + 10).toString() ); expect(rewardPerBlock[3]).to.be.equal(false); }); it("should be able to earn rewards after some block passed", async () => { let stakingAmount = ethers.utils.parseEther("100"); await dai["mint(address,uint256)"](staker.address, stakingAmount); await dai .connect(staker) .approve(goodCompoundStaking.address, stakingAmount); await goodCompoundStaking.connect(staker).stake(stakingAmount, 0, false); let gdBalanceBeforeWithdraw = await goodDollar.balanceOf(staker.address); await advanceBlocks(4); await goodCompoundStaking .connect(staker) .withdrawStake(stakingAmount, false); let gdBalancerAfterWithdraw = await goodDollar.balanceOf(staker.address); expect(gdBalancerAfterWithdraw.toString()).to.be.equal("2500"); }); it("it should withdraw effective stakes and donated stakes proportionally", async () => { const stakingAmount = ethers.utils.parseEther("100"); await dai["mint(address,uint256)"](staker.address, stakingAmount); await dai .connect(staker) .approve(goodCompoundStaking.address, stakingAmount.mul(BN.from("2"))); await goodCompoundStaking.connect(staker).stake(stakingAmount, 0, false); await goodCompoundStaking.connect(staker).stake(stakingAmount, 100, false); const userStakeInfoBeforeWithdraw = await goodCompoundStaking.users( staker.address ); expect(userStakeInfoBeforeWithdraw.effectiveStakes).to.be.equal( stakingAmount ); expect(userStakeInfoBeforeWithdraw.amount).to.be.equal( stakingAmount.mul(BN.from("2")) ); await goodCompoundStaking .connect(staker) .withdrawStake(stakingAmount, false); const userStakeInfoAfterWithdraw = await goodCompoundStaking.users( staker.address ); expect(userStakeInfoAfterWithdraw.effectiveStakes).to.be.equal( stakingAmount.div(BN.from("2")) ); // should be half of the stakes that staked initially expect(userStakeInfoAfterWithdraw.amount).to.be.equal(stakingAmount); await goodCompoundStaking .connect(staker) .withdrawStake(stakingAmount, false); }); it("it should not increase totalEffectiveStakes when staked amount's rewards are donated", async () => { const stakingAmount = ethers.utils.parseEther("100"); await dai["mint(address,uint256)"](staker.address, stakingAmount); await dai .connect(staker) .approve(goodCompoundStaking.address, stakingAmount); const totalEffectiveStakesBeforeStake = await goodCompoundStaking .getStats() .then(_ => _[3]); await goodCompoundStaking.connect(staker).stake(stakingAmount, 100, false); const totalEffectiveStakesAfterStake = await goodCompoundStaking .getStats() .then(_ => _[3]); await goodCompoundStaking .connect(staker) .withdrawStake(stakingAmount, false); expect(totalEffectiveStakesAfterStake).to.be.equal( totalEffectiveStakesBeforeStake ); }); it("it should increase totalEffectiveStakes when staked without donation", async () => { const stakingAmount = ethers.utils.parseEther("100"); await dai["mint(address,uint256)"](staker.address, stakingAmount); await dai .connect(staker) .approve(goodCompoundStaking.address, stakingAmount); const totalEffectiveStakesBeforeStake = await goodCompoundStaking .getStats() .then(_ => _[3]); await goodCompoundStaking.connect(staker).stake(stakingAmount, 0, false); const totalEffectiveStakesAfterStake = await goodCompoundStaking .getStats() .then(_ => _[3]); await goodCompoundStaking .connect(staker) .withdrawStake(stakingAmount, false); expect(totalEffectiveStakesAfterStake).to.be.gt( totalEffectiveStakesBeforeStake ); }); it("it should withdraw effective and stakes according to ratio", async () => { const stakingAmount = ethers.utils.parseEther("100"); await dai["mint(address,uint256)"](staker.address, stakingAmount); await dai .connect(staker) .approve(goodCompoundStaking.address, stakingAmount); await goodCompoundStaking .connect(staker) .stake(stakingAmount.mul(80).div(100), 0, false); await goodCompoundStaking .connect(staker) .stake(stakingAmount.mul(20).div(100), 100, false); const userStakeInfoBeforeWithdraw = await goodCompoundStaking.users( staker.address ); const withdrawAmount = stakingAmount.mul(20).div(100); const withdrawFromEffectiveStake = withdrawAmount .mul(userStakeInfoBeforeWithdraw.effectiveStakes) .div(userStakeInfoBeforeWithdraw.amount); const withdrawFromDonation = withdrawAmount.sub(withdrawFromEffectiveStake); await goodCompoundStaking .connect(staker) .withdrawStake(withdrawAmount, false); const userStakeInfoAfterWithdraw = await goodCompoundStaking.users( staker.address ); expect(userStakeInfoBeforeWithdraw.effectiveStakes).to.be.gt(0); expect(userStakeInfoAfterWithdraw.effectiveStakes).to.be.equal( userStakeInfoBeforeWithdraw.effectiveStakes.sub( withdrawFromEffectiveStake ) ); expect( userStakeInfoAfterWithdraw.amount.sub( userStakeInfoAfterWithdraw.effectiveStakes ) ).to.be.equal( userStakeInfoBeforeWithdraw.amount .sub(userStakeInfoBeforeWithdraw.effectiveStakes) .sub(withdrawFromDonation) ); await goodCompoundStaking .connect(staker) .withdrawStake(stakingAmount.sub(withdrawAmount), false); // withdraw left amount }); it("shouldn't be able to earn rewards after rewards blockend passed", async () => { let stakingAmount = ethers.utils.parseEther("100"); await dai["mint(address,uint256)"](staker.address, stakingAmount); await dai .connect(staker) .approve(goodCompoundStaking.address, stakingAmount); await goodCompoundStaking.connect(staker).stake(stakingAmount, 0, false); let gdBalanceBeforeWithdraw = await goodDollar.balanceOf(staker.address); advanceBlocks(5); await goodCompoundStaking .connect(staker) .withdrawStake(stakingAmount, false); let gdBalancerAfterWithdraw = await goodDollar.balanceOf(staker.address); expect(gdBalancerAfterWithdraw.toString()).to.be.equal( gdBalanceBeforeWithdraw.toString() ); }); it("shouldn't be able to mint reward when staking contract is blacklisted", async () => { const goodCompoundStaking2 = await deployStaking(null, null, "1728000"); let encodedDataTwo = goodFundManager.interface.encodeFunctionData( "setStakingReward", ["1000", goodCompoundStaking2.address, "55", "1000", false] // set 10 gd per block ); await genericCall(goodFundManager.address, encodedDataTwo); let stakingAmount = ethers.utils.parseEther("100"); await dai["mint(address,uint256)"](staker.address, stakingAmount); await dai .connect(staker) .approve(goodCompoundStaking2.address, stakingAmount); await goodCompoundStaking2.connect(staker).stake(stakingAmount, 0, false); let gdBalanceBeforeWithdraw = await goodDollar.balanceOf(staker.address); advanceBlocks(5); encodedDataTwo = goodFundManager.interface.encodeFunctionData( "setStakingReward", ["1000", goodCompoundStaking2.address, "55", "1000", true] // set 10 gd per block ); await genericCall(goodFundManager.address, encodedDataTwo); await goodCompoundStaking2 .connect(staker) .withdrawStake(stakingAmount, false); let gdBalancerAfterWithdraw = await goodDollar.balanceOf(staker.address); expect(gdBalancerAfterWithdraw).to.be.equal(gdBalanceBeforeWithdraw); }); it("shouldn't be able to collect interest when contract is not active", async () => { const goodCompoundStaking2 = await deployStaking(null, null, "1728000"); await expect( goodFundManager .connect(staker) .collectInterest([goodCompoundStaking2.address], true) ).revertedWith(/not a dao contract/); }); // it("should set blacklisted false and mint rewards", async () => { // const goodFundManagerFactory = await ethers.getContractFactory( // "GoodFundManager" // ); // const ictrl = await ethers.getContractAt( // "Controller", // controller, // schemeMock // ); // const currentBlockNumber = await ethers.provider.getBlockNumber(); // const encodedDataTwo = goodFundManagerFactory.interface.encodeFunctionData( // "setStakingReward", // [ // "1000", // goodCompoundStaking.address, // currentBlockNumber, // currentBlockNumber + 500, // false // ] // set 10 gd per block // ); // await ictrl.genericCall(goodFundManager.address, encodedDataTwo, avatar, 0); // let stakingAmount = ethers.utils.parseEther("100"); // await dai["mint(address,uint256)"](staker.address, stakingAmount); // await dai // .connect(staker) // .approve(goodCompoundStaking.address, stakingAmount); // await goodCompoundStaking.connect(staker).stake(stakingAmount, 0); // let gdBalanceBeforeWithdraw = await goodDollar.balanceOf(staker.address); // advanceBlocks(5); // await goodCompoundStaking.connect(staker).withdrawStake(stakingAmount); // let gdBalancerAfterWithdraw = await goodDollar.balanceOf(staker.address); // let gCDAIbalanceAfter = await goodCompoundStaking.balanceOf(staker.address); // let gCDAITotalSupply = await goodCompoundStaking.totalSupply(); // expect(gCDAIbalanceAfter).to.be.equal(gCDAITotalSupply); // staker should own whole staking tokens // expect(gdBalancerAfterWithdraw.toString()).to.be.equal("5500"); // should mint previous rewards as well // }); it("it should send staker's productivity to some other user", async () => { let stakingAmount = ethers.utils.parseEther("100"); await dai["mint(address,uint256)"](staker.address, stakingAmount); await dai .connect(staker) .approve(goodCompoundStaking.address, stakingAmount); await goodCompoundStaking.connect(staker).stake(stakingAmount, 100, false); let stakersProductivityBefore = await goodCompoundStaking.getProductivity( staker.address ); await goodCompoundStaking .connect(staker) .transfer(founder.address, stakingAmount); let stakersProductivityAfter = await goodCompoundStaking .connect(staker) .getProductivity(staker.address); let foundersProductivity = await goodCompoundStaking.getProductivity( founder.address ); expect(stakersProductivityAfter[0].toString()).to.be.equal("0"); expect(foundersProductivity[0].toString()).to.be.equals( stakingAmount.toString() ); }); it("it shouldn't be able to withdraw stake when staker sent it to another user", async () => { const stakingAmount = ethers.utils.parseEther("100"); await expect( goodCompoundStaking.connect(staker).withdrawStake(stakingAmount, false) ).to.be.reverted; }); it("it should be able to withdraw their stake when got staking tokens from somebody else", async () => { const stakingAmount = ethers.utils.parseEther("100"); await goodCompoundStaking.withdrawStake(stakingAmount, false); const foundersProductivity = await goodCompoundStaking.getProductivity( founder.address ); expect(foundersProductivity[0].toString()).to.be.equal("0"); expect(foundersProductivity[1].toString()).to.be.equal("0"); // Total productivity also should equal 0 }); it("stake should generate some interest and shoul be used to generate UBI", async () => { const stakingAmount = ethers.utils.parseEther("100"); await dai["mint(address,uint256)"](staker.address, stakingAmount); await dai .connect(staker) .approve(goodCompoundStaking.address, stakingAmount); await goodCompoundStaking.connect(staker).stake(stakingAmount, 100, false); await dai["mint(address,uint256)"]( staker.address, ethers.utils.parseEther("1000000") ); await dai .connect(staker) .transfer(cDAI.address, ethers.utils.parseEther("1000000")); // We should put extra DAI to mock cDAI contract in order to provide interest await cDAI.increasePriceWithMultiplier("1500"); // increase interest by calling exchangeRateCurrent const currentUBIInterestBeforeWithdraw = await goodCompoundStaking.currentGains(false, true); await goodCompoundStaking .connect(staker) .withdrawStake(stakingAmount, false); const gdBalanceBeforeCollectInterest = await goodDollar.balanceOf( staker.address ); const contractAddressesToBeCollected = await goodFundManager.calcSortedContracts(); const addressesToCollect = contractAddressesToBeCollected.map(x => x[0]); await goodFundManager .connect(staker) .collectInterest(addressesToCollect, false); const gdBalanceAfterCollectInterest = await goodDollar.balanceOf( staker.address ); const currentUBIInterestAfterWithdraw = await goodCompoundStaking.currentGains(false, true); expect(currentUBIInterestBeforeWithdraw[0].toString()).to.not.be.equal("0"); expect(currentUBIInterestAfterWithdraw[0].toString()).to.be.equal("0"); expect(gdBalanceAfterCollectInterest.gt(gdBalanceBeforeCollectInterest)); }); it("it should get rewards with updated values", async () => { const goodFundManagerFactory = await ethers.getContractFactory( "GoodFundManager" ); const currentBlockNumber = await ethers.provider.getBlockNumber(); const encodedDataTwo = goodFundManagerFactory.interface.encodeFunctionData( "setStakingReward", [ "1000", goodCompoundStaking.address, currentBlockNumber, currentBlockNumber + 5000, false ] // set 10 gd per block ); await genericCall(goodFundManager.address, encodedDataTwo, avatar, 0); const stakingAmount = ethers.utils.parseEther("100"); await dai["mint(address,uint256)"](staker.address, stakingAmount); await dai .connect(staker) .approve(goodCompoundStaking.address, stakingAmount); await goodCompoundStaking.connect(staker).stake(stakingAmount, 0, false); await advanceBlocks(4); const stakingContractVals = await goodFundManager.rewardsForStakingContract( goodCompoundStaking.address ); let rewardsEarned = await goodCompoundStaking.getUserPendingReward( staker.address, stakingContractVals[0], stakingContractVals[1], stakingContractVals[2] ); //baseshare rewards is in 18 decimals expect(rewardsEarned.toString()).to.be.equal( ethers.utils.parseUnits("20", 18) ); // Each block reward is 10gd so total reward 40gd but since multiplier is 0.5 for first month should get 20gd await goodCompoundStaking .connect(staker) .withdrawStake(stakingAmount, false); }); it("should get user minted and pending rewards", async () => { const goodFundManagerFactory = await ethers.getContractFactory( "GoodFundManager" ); const currentBlockNumber = await ethers.provider.getBlockNumber(); const encodedData = goodFundManagerFactory.interface.encodeFunctionData( "setStakingReward", [ "1000", goodCompoundStaking.address, currentBlockNumber, currentBlockNumber + 5000, false ] // set 10 gd per block ); await genericCall(goodFundManager.address, encodedData, avatar, 0); const stakingAmount = ethers.utils.parseEther("100"); await dai["mint(address,uint256)"](staker.address, stakingAmount); await dai .connect(staker) .approve(goodCompoundStaking.address, stakingAmount); await goodCompoundStaking.connect(staker).stake(stakingAmount, 0, false); await advanceBlocks(4); const userMintedAndPending = await goodCompoundStaking.getUserMintedAndPending(staker.address); const userMintedReward = userMintedAndPending[0].toString(); const userPendingReward = userMintedAndPending[1].toString(); expect(userMintedReward).to.equal("5000"); expect(userPendingReward).to.equal("2000"); await goodCompoundStaking .connect(staker) .withdrawStake(stakingAmount, false); }); it("it should get rewards with 1x multiplier for after threshold pass", async () => { const goodFundManagerFactory = await ethers.getContractFactory( "GoodFundManager" ); const simpleStaking = await await deployStaking(); const currentBlockNumber = await ethers.provider.getBlockNumber(); let encodedDataTwo = goodFundManagerFactory.interface.encodeFunctionData( "setStakingReward", [ "1000", simpleStaking.address, currentBlockNumber, currentBlockNumber + 100, false ] // set 10 gd per block ); await genericCall(goodFundManager.address, encodedDataTwo, avatar, 0); const stakingAmount = ethers.utils.parseEther("100"); await dai["mint(address,uint256)"](staker.address, stakingAmount); await dai.connect(staker).approve(simpleStaking.address, stakingAmount); let gdBalanceStakerBeforeWithdraw = await goodDollar.balanceOf( staker.address ); await simpleStaking.connect(staker).stake(stakingAmount, 0, false); await advanceBlocks(54); await simpleStaking.connect(staker).withdrawStake(stakingAmount, false); let gdBalanceStakerAfterWithdraw = await goodDollar.balanceOf( staker.address ); expect( gdBalanceStakerAfterWithdraw.sub(gdBalanceStakerBeforeWithdraw).toString() ).to.be.equal("30000"); // 50 blocks reward worth 500gd but since it's with the 0.5x multiplier so 250gd then there is 5 blocks which gets full reward so total reward is 300gd encodedDataTwo = goodFundManagerFactory.interface.encodeFunctionData( "setStakingReward", [ "1000", simpleStaking.address, currentBlockNumber, currentBlockNumber + 100, true ] // set 10 gd per block ); await genericCall(goodFundManager.address, encodedDataTwo, avatar, 0); }); it("Should transfer somebody's staking token's when they approve", async () => { const stakingAmount = ethers.utils.parseEther("100"); await dai["mint(address,uint256)"](staker.address, stakingAmount); await dai .connect(staker) .approve(goodCompoundStaking.address, stakingAmount); await goodCompoundStaking.connect(staker).stake(stakingAmount, 100, false); await goodCompoundStaking .connect(staker) .approve(founder.address, stakingAmount); const stakingTokenBalanceBeforeTransfer = await goodCompoundStaking.balanceOf(founder.address); await goodCompoundStaking.transferFrom( staker.address, founder.address, stakingAmount ); const stakingTokenBalanceAfterTransfer = await goodCompoundStaking.balanceOf(founder.address); expect( stakingTokenBalanceAfterTransfer.gt(stakingTokenBalanceBeforeTransfer) ).to.be.true; await goodCompoundStaking.withdrawStake(stakingAmount, false); }); it("Should be able to withdraw rewards without withdraw stake", async () => { const stakingAmount = ethers.utils.parseEther("100"); await dai["mint(address,uint256)"](staker.address, stakingAmount); await dai .connect(staker) .approve(goodCompoundStaking.address, stakingAmount); await goodCompoundStaking.connect(staker).stake(stakingAmount, 0, false); await advanceBlocks(5); const stakingContractVals = await goodFundManager.rewardsForStakingContract( goodCompoundStaking.address ); const earnedRewardBeforeWithdrawReward = await goodCompoundStaking.getUserPendingReward( staker.address, stakingContractVals[0], stakingContractVals[1], stakingContractVals[2] ); await goodCompoundStaking.connect(staker).withdrawRewards(); const earnedRewardAfterWithdrawReward = await goodCompoundStaking.getUserPendingReward( staker.address, stakingContractVals[0], stakingContractVals[1], stakingContractVals[2] ); expect(earnedRewardAfterWithdrawReward.lt(earnedRewardBeforeWithdrawReward)) .to.be.true; expect(earnedRewardAfterWithdrawReward.toString()).to.be.equal("0"); await goodCompoundStaking .connect(staker) .withdrawStake(stakingAmount, false); }); it("it should not mint reward when staking contract is not registered", async () => { const simpleStaking = await deployStaking(); const tx = await simpleStaking.withdrawRewards().catch(e => e); expect(tx.message).to.not.be.empty; }); it("it should be able to distribute rewards when blockEnd passed but last Reward block was before blockend", async () => { const goodFundManagerFactory = await ethers.getContractFactory( "GoodFundManager" ); const currentBlockNumber = await ethers.provider.getBlockNumber(); let encodedData = goodFundManagerFactory.interface.encodeFunctionData( "setStakingReward", [ "1000", goodCompoundStaking.address, currentBlockNumber - 10, currentBlockNumber + 50, false ] // set 10 gd per block ); await genericCall(goodFundManager.address, encodedData); const stakingAmount = ethers.utils.parseEther("100"); const initialGdBalance = await goodDollar.balanceOf(staker.address); await dai["mint(address,uint256)"](staker.address, stakingAmount); await dai .connect(staker) .approve(goodCompoundStaking.address, stakingAmount); const stakeBlockNumber = (await ethers.provider.getBlockNumber()) + 1; await goodCompoundStaking.connect(staker).stake(stakingAmount, 0, false); await advanceBlocks(5); await goodCompoundStaking .connect(staker) .withdrawStake(stakingAmount.div(2), false); const gdBalanceAfterFirstWithdraw = await goodDollar.balanceOf( staker.address ); const firstWithdrawBlockNumber = await ethers.provider.getBlockNumber(); await advanceBlocks(60); await goodCompoundStaking .connect(staker) .withdrawStake(stakingAmount.div(2), false); const gdBalanceAfterSecondWithdraw = await goodDollar.balanceOf( staker.address ); expect(gdBalanceAfterFirstWithdraw).to.be.gt(initialGdBalance); expect(gdBalanceAfterFirstWithdraw.sub(initialGdBalance)).to.be.equal( BN.from("1000") .mul(firstWithdrawBlockNumber - stakeBlockNumber) .div(2) ); expect(gdBalanceAfterSecondWithdraw).to.be.gt(gdBalanceAfterFirstWithdraw); expect( gdBalanceAfterSecondWithdraw.sub(gdBalanceAfterFirstWithdraw) ).to.be.equal( BN.from("1000") .mul(currentBlockNumber + 50 - firstWithdrawBlockNumber) .div(2) .sub(1) ); // sub 1 due to precision loss }); it("it should not earn rewards when currentBlock < blockStart", async () => { const goodFundManagerFactory = await ethers.getContractFactory( "GoodFundManager" ); const currentBlockNumber = await ethers.provider.getBlockNumber(); let encodedData = goodFundManagerFactory.interface.encodeFunctionData( "setStakingReward", [ "1000", goodCompoundStaking.address, currentBlockNumber + 100, currentBlockNumber + 500, false ] // set 10 gd per block ); await genericCall(goodFundManager.address, encodedData); const stakingAmount = ethers.utils.parseEther("100"); const initialGdBalance = await goodDollar.balanceOf(staker.address); await dai["mint(address,uint256)"](staker.address, stakingAmount); await dai .connect(staker) .approve(goodCompoundStaking.address, stakingAmount); await goodCompoundStaking.connect(staker).stake(stakingAmount, 0, false); await advanceBlocks(50); await goodCompoundStaking .connect(staker) .withdrawStake(stakingAmount, false); const gdBalanceAfterWithdraw = await goodDollar.balanceOf(staker.address); expect(initialGdBalance).to.be.gt(0); expect(gdBalanceAfterWithdraw).to.be.equal(initialGdBalance); }); it("it should earn rewards when they stake before blockStart but keep their stake until after blockStart", async () => { const goodFundManagerFactory = await ethers.getContractFactory( "GoodFundManager" ); const currentBlockNumber = await ethers.provider.getBlockNumber(); let encodedData = goodFundManagerFactory.interface.encodeFunctionData( "setStakingReward", [ "1000", goodCompoundStaking.address, currentBlockNumber + 30, currentBlockNumber + 500, false ] // set 10 gd per block ); await genericCall(goodFundManager.address, encodedData); const stakingAmount = ethers.utils.parseEther("100"); const initialGdBalance = await goodDollar.balanceOf(staker.address); await dai["mint(address,uint256)"](staker.address, stakingAmount); await dai .connect(staker) .approve(goodCompoundStaking.address, stakingAmount); await goodCompoundStaking.connect(staker).stake(stakingAmount, 0, false); await advanceBlocks(50); await goodCompoundStaking .connect(staker) .withdrawStake(stakingAmount, false); const withdrawBlockNumber = await ethers.provider.getBlockNumber(); const gdBalanceAfterWithdraw = await goodDollar.balanceOf(staker.address); expect(initialGdBalance).to.be.gt(0); expect(gdBalanceAfterWithdraw).to.be.gt(initialGdBalance); expect(gdBalanceAfterWithdraw.sub(initialGdBalance)).to.be.equal( BN.from("1000") .mul(withdrawBlockNumber - (currentBlockNumber + 30)) .div(2) .sub(1) ); }); it("should be able earn to 50% of rewards when owns 50% of total productivity", async () => { const goodFundManagerFactory = await ethers.getContractFactory( "GoodFundManager" ); const ictrl = await ethers.getContractAt( "Controller", controller, schemeMock ); const currentBlockNumber = await ethers.provider.getBlockNumber(); let encodedDataTwo = goodFundManagerFactory.interface.encodeFunctionData( "setStakingReward", [ "1000", goodCompoundStaking.address, currentBlockNumber - 10, currentBlockNumber + 100, false ] // set 10 gd per block ); const stakingAmount = ethers.utils.parseEther("100"); await dai["mint(address,uint256)"](staker.address, stakingAmount); await dai["mint(address,uint256)"](signers[0].address, stakingAmount); // We use some different signer than founder since founder also UBI INTEREST collector await dai .connect(signers[0]) .approve(goodCompoundStaking.address, stakingAmount); await dai .connect(staker) .approve(goodCompoundStaking.address, stakingAmount); let stakerTwoGDAmountBeforeStake = await goodDollar.balanceOf( signers[0].address ); let stakerGDAmountBeforeStake = await goodDollar.balanceOf(staker.address); await goodCompoundStaking.connect(staker).stake(stakingAmount, 100, false); await goodCompoundStaking .connect(signers[0]) .stake(stakingAmount, 100, false); await advanceBlocks(5); await goodCompoundStaking .connect(staker) .withdrawStake(stakingAmount, false); await goodCompoundStaking .connect(signers[0]) .withdrawStake(stakingAmount, false); let stakerTwoGDAmountAfterStake = await goodDollar.balanceOf( signers[0].address ); let stakerGDAmountAfterStake = await goodDollar.balanceOf(staker.address); expect( stakerTwoGDAmountAfterStake.sub(stakerTwoGDAmountBeforeStake).toString() ).to.be.equal( stakerGDAmountAfterStake.sub(stakerGDAmountBeforeStake).toString() ); }); it("Accumulated per share has enough precision when reward << totalproductivity", async () => { const goodFundManagerFactory = await ethers.getContractFactory( "GoodFundManager" ); const currentBlockNumber = await ethers.provider.getBlockNumber(); let encodedDataTwo = goodFundManagerFactory.interface.encodeFunctionData( "setStakingReward", [ "1000", goodCompoundStaking.address, currentBlockNumber - 10, currentBlockNumber + 100, false ] // set 10 gd per block ); await genericCall(goodFundManager.address, encodedDataTwo, avatar, 0); const stakingAmount = ethers.utils.parseEther("1000000000"); await dai["mint(address,uint256)"](founder.address, stakingAmount); // 1 billion dai to stake await dai.approve(goodCompoundStaking.address, stakingAmount); await goodCompoundStaking.stake(stakingAmount, 0, false); await advanceBlocks(4); const gdBalanceBeforeWithdraw = await goodDollar.balanceOf(founder.address); await goodCompoundStaking.withdrawStake(stakingAmount, false); const gdBalanceAfterWithdraw = await goodDollar.balanceOf(founder.address); expect( gdBalanceAfterWithdraw.sub(gdBalanceBeforeWithdraw).toString() ).to.be.equal("2500"); }); it("it should not get any reward when donationPer set to 100", async () => { const stakingAmount = ethers.utils.parseEther("100"); await dai["mint(address,uint256)"](staker.address, stakingAmount); let stakerGDAmountBeforeStake = await goodDollar.balanceOf(staker.address); await dai .connect(staker) .approve(goodCompoundStaking.address, stakingAmount); await goodCompoundStaking.connect(staker).stake(stakingAmount, 100, false); await advanceBlocks(4); await goodCompoundStaking .connect(staker) .withdrawStake(stakingAmount, false); let stakerGDAmountAfterStake = await goodDollar.balanceOf(staker.address); expect(stakerGDAmountAfterStake).to.be.equal(stakerGDAmountBeforeStake); }); it("it should be reverted when donation per set to different than 0 or 100", async () => { const stakingAmount = ethers.utils.parseEther("100"); await dai["mint(address,uint256)"](staker.address, stakingAmount); await dai .connect(staker) .approve(goodCompoundStaking.address, stakingAmount); const tx = await goodCompoundStaking .connect(staker) .stake(stakingAmount, 55, false) .catch(e => e); expect(tx.message).to.be.not.empty; }); it("should be able to sort staking contracts and collect interests from lowest to highest[ @skip-on-coverage ]", async () => { const stakingAmount = ethers.utils.parseEther("100"); await dai["mint(address,uint256)"](staker.address, stakingAmount); await dai .connect(staker) .approve(goodCompoundStaking.address, stakingAmount); await goodCompoundStaking.connect(staker).stake(stakingAmount, 100, false); await cDAI.increasePriceWithMultiplier("6000"); // increase interest by calling exchangeRateCurrent const simpleStaking = await deployStaking(); const simpleStaking1 = await deployStaking(); const goodFundManagerFactory = await ethers.getContractFactory( "GoodFundManager" ); let encodedData = goodFundManagerFactory.interface.encodeFunctionData( "setStakingReward", ["100", simpleStaking.address, 0, 10, false] ); await genericCall(goodFundManager.address, encodedData, avatar, 0); await dai["mint(address,uint256)"](staker.address, stakingAmount); await dai.connect(staker).approve(simpleStaking.address, stakingAmount); await simpleStaking.connect(staker).stake(stakingAmount, 100, false); await dai["mint(address,uint256)"](staker.address, stakingAmount); await dai.connect(staker).approve(simpleStaking1.address, stakingAmount); await simpleStaking1.connect(staker).stake(stakingAmount, 100, false); await cDAI.increasePriceWithMultiplier("200"); // increase interest by calling increasePriceWithMultiplier const contractsToBeCollected = await goodFundManager.calcSortedContracts(); const addressesToCollect = contractsToBeCollected.map(x => x[0]); expect(addressesToCollect[0]).to.be.equal(simpleStaking.address); expect(addressesToCollect[1]).to.be.equal(goodCompoundStaking.address); await goodCompoundStaking .connect(staker) .withdrawStake(stakingAmount, false); encodedData = goodFundManagerFactory.interface.encodeFunctionData( "setStakingReward", ["100", simpleStaking.address, 0, 10, true] ); await genericCall(goodFundManager.address, encodedData, avatar, 0); }); it("It should not collect interest when interest is lower than gas cost [ @skip-on-coverage ]", async () => { await gasFeeOracle.setPrice(ethers.constants.WeiPerEther); //increase gas price so costs is more than ubi minted await expect( goodFundManager.collectInterest([], false, { gasLimit: 770000 }) ).revertedWith(/X*gas costs/); await gasFeeOracle.setPrice(25e8); //undo gas price }); it("It should always collect interest without rewards when forced", async () => { await goodFundManager .collectInterest([goodCompoundStaking.address], false) .catch(e => e); // make sure there is no interest left const stakingAmount = ethers.utils.parseEther("100"); await dai["mint(address,uint256)"](staker.address, stakingAmount); await dai .connect(staker) .approve(goodCompoundStaking.address, stakingAmount); await goodCompoundStaking.connect(staker).stake(stakingAmount, 100, false); const transaction = goodFundManager.collectInterest( [goodCompoundStaking.address], true, { gasLimit: 770000 } ); await expect(transaction).to.emit(goodFundManager, "FundsTransferred"); const tx = await (await transaction).wait(); const event = tx.events.find(e => e.event === "FundsTransferred"); expect(event.args.gdReward).to.equal(0); await goodCompoundStaking .connect(staker) .withdrawStake(stakingAmount, false); }); it("It should sort array from lowest to highest ", async () => { const goodFundManagerTestFactory = await ethers.getContractFactory( "GoodFundManagerTest" ); const goodFundManagerTest = await goodFundManagerTestFactory.deploy( nameService.address ); const addresses = [ founder.address, staker.address, cDAI.address, cDAI1.address ]; const balances = [ ethers.utils.parseEther("100"), ethers.utils.parseEther("85"), ethers.utils.parseEther("90"), ethers.utils.parseEther("30") ]; const sortedArrays = await goodFundManagerTest.testSorting( balances, addresses ); expect(sortedArrays[0][0]).to.be.equal(ethers.utils.parseEther("30")); expect(sortedArrays[0][3]).to.be.equal(ethers.utils.parseEther("100")); expect(sortedArrays[1][3]).to.be.equal(founder.address); expect(sortedArrays[1][0]).to.be.equal(cDAI1.address); }); // needs to come before next test where we blacklist the goodcompoundstaking contract it("should revert when colleced interest is not greater than gas cost when 2 months passed [ @skip-on-coverage ]", async () => { // make sure expansion is very low await initializeToken( cDAI.address, "129966743722", //1gd "499870173594", //0.0001 cDai "1000000" //100% rr ); await runAsAvatarOnly( goodReserve, "setReserveRatioDailyExpansion(uint256,uint256)", ethers.utils.parseEther("999999999"), ethers.utils.parseEther("1000000000") ); const currentBlockNumber = await ethers.provider.getBlockNumber(); const currentBlock = await ethers.provider.getBlock(currentBlockNumber); let encodedData = goodFundManager.interface.encodeFunctionData( "setStakingReward", [ "1000", goodCompoundStaking.address, 0, currentBlockNumber + 1000000, false ] // set 10 gd per block ); await genericCall(goodFundManager.address, encodedData); await ethers.provider.send("evm_setNextBlockTimestamp", [ currentBlock.timestamp + 5185020 ]); await ethers.provider.send("evm_mine", []); console.log(await goodFundManager.activeContracts(0)); await expect( goodFundManager.collectInterest([goodCompoundStaking.address], false) ).revertedWith(/< gas costs/); }); it("should be able to mint ubi when 0 interest but ubi value > interestMultiplier*gas cost when 2 months passed [ @skip-on-coverage ]", async () => { await initializeToken( cDAI.address, "129966743722", //1gd "499870173594", //0.0001 cDai "400000" //100% rr ); await runAsAvatarOnly( goodReserve, "setReserveRatioDailyExpansion(uint256,uint256)", ethers.utils.parseEther("999999999"), ethers.utils.parseEther("1000000000") ); const currentBlockNumber = await ethers.provider.getBlockNumber(); const currentBlock = await ethers.provider.getBlock(currentBlockNumber); let encodedData = goodFundManager.interface.encodeFunctionData( "setStakingReward", [ "1000", goodCompoundStaking.address, 0, currentBlockNumber + 1000000, false ] // set 10 gd per block ); await genericCall(goodFundManager.address, encodedData); await ethers.provider.send("evm_setNextBlockTimestamp", [ currentBlock.timestamp + 5185020 ]); await ethers.provider.send("evm_mine", []); console.log(await goodFundManager.activeContracts(0)); await expect( goodFundManager.collectInterest([goodCompoundStaking.address], false) ).revertedWith(/< gas costs/); }); it("It should not be able to calc and sort array when there is no active staking contract", async () => { const goodFundManagerFactory = await ethers.getContractFactory( "GoodFundManager" ); let encodedData = goodFundManagerFactory.interface.encodeFunctionData( "setStakingReward", ["100", goodCompoundStaking.address, 0, 10, true] ); await genericCall(goodFundManager.address, encodedData, avatar, 0); const contractsToInterestCollected = await goodFundManager.calcSortedContracts(); expect(contractsToInterestCollected.length).to.be.equal(0); encodedData = goodFundManagerFactory.interface.encodeFunctionData( "setStakingReward", ["100", goodCompoundStaking.address, 100, 1000, false] ); await genericCall(goodFundManager.address, encodedData, avatar, 0); }); it("Avatar should be able to set gd minting gas amount", async () => { const goodFundManagerFactory = await ethers.getContractFactory( "GoodFundManager" ); let encodedData = goodFundManagerFactory.interface.encodeFunctionData( "setGasCost", ["140000"] ); await genericCall(goodFundManager.address, encodedData, avatar, 0); }); it("Avatar should be able to set collectInterestTimeThreshold", async () => { const goodFundManagerFactory = await ethers.getContractFactory( "GoodFundManager" ); let encodedData = goodFundManagerFactory.interface.encodeFunctionData( "setCollectInterestTimeThreshold", ["5184000"] ); await genericCall(goodFundManager.address, encodedData, avatar, 0); }); it("Avatar should be able set interestMultiplier", async () => { const goodFundManagerFactory = await ethers.getContractFactory( "GoodFundManager" ); let encodedData = goodFundManagerFactory.interface.encodeFunctionData( "setInterestMultiplier", ["4"] ); await genericCall(goodFundManager.address, encodedData, avatar, 0); }); it("Avatar should be able set gasCostExceptInterestCollect", async () => { const goodFundManagerFactory = await ethers.getContractFactory( "GoodFundManager" ); let encodedData = goodFundManagerFactory.interface.encodeFunctionData( "setGasCostExceptInterestCollect", ["650000"] ); await genericCall(goodFundManager.address, encodedData, avatar, 0); }); it("It should be able to collect Interest from non DAI or cDAI staking contract [ @skip-on-coverage ]", async () => { const goodFundManagerFactory = await ethers.getContractFactory( "GoodFundManager" ); const simpleStaking = await goodCompoundStakingFactory .deploy() .then(async contract => { await contract.init( bat.address, cBat.address, nameService.address, "Good BaT", "gBAT", "50", batUsdOracle.address, compUsdOracle.address, [bat.address, dai.address] ); return contract; }); let encodedData = goodFundManagerFactory.interface.encodeFunctionData( "setStakingReward", ["100", simpleStaking.address, 10, 10000, false] ); await genericCall(goodFundManager.address, encodedData, avatar, 0); encodedData = goodCompoundStakingFactory.interface.encodeFunctionData( "setcollectInterestGasCostParams", ["250000", "150000"] ); await genericCall(simpleStaking.address, encodedData, avatar, 0); await bat["mint(address,uint256)"]( founder.address, ethers.utils.parseEther("1001000") ); await bat.transfer(cBat.address, ethers.utils.parseEther("1000000")); // We should put extra BAT to mock cBAT contract in order to provide interest await dai["mint(address,uint256)"]( founder.address, ethers.utils.parseEther("1000000") ); await dai.approve( goodCompoundStaking.address, ethers.utils.parseEther("100") ); await goodCompoundStaking.stake(ethers.utils.parseEther("100"), 100, false); await bat.approve(simpleStaking.address, ethers.utils.parseEther("100")); await simpleStaking.stake(ethers.utils.parseEther("100"), 100, false); await cDAI.increasePriceWithMultiplier("2000"); await cBat.increasePriceWithMultiplier("500"); const collectableContracts = await goodFundManager.calcSortedContracts(); const addr