UNPKG

@gooddollar/goodprotocol

Version:
615 lines (515 loc) 22.8 kB
import { default as hre, ethers, upgrades } from "hardhat"; import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; import { Contract, Signer } from "ethers"; import { expect } from "chai"; import { GoodReserveCDai, GReputation, GoodDollarStaking, GovernanceStaking, GoodDollarMintBurnWrapper, IGoodDollar } from "../../types"; import { createDAO, advanceBlocks, increaseTime } from "../helpers"; import { FormatTypes } from "ethers/lib/utils"; const BN = ethers.BigNumber; export const NULL_ADDRESS = ethers.constants.AddressZero; export const BLOCK_INTERVAL = 30; const DONATION_10_PERCENT = 10; const DONATION_30_PERCENT = 30; const STAKE_AMOUNT = 10000; const BLOCKS_ONE_YEAR = 6307200; // APY=5% | per block = nroot(1+0.05,numberOfBlocksPerYear) = 1000000007735630000 const INTEREST_RATE_5APY_X64 = BN.from("1000000007735630000"); // x64 representation of same number const INTEREST_RATE_5APY_128 = BN.from("18446744216406738474"); // 128 representation of same number // APY = 10% | nroot(1+0.10,numberOfBlocksPerYear) = 1000000015111330000 const INTEREST_RATE_10APY_X64 = BN.from("1000000015111330000"); // x64 representation of same number const INTEREST_RATE_10APY_128 = BN.from("18446744352464388739"); // 128 representation of same number const INITIAL_CAP = 100000000000; //1B G$s describe("GoodDollarStaking - check fixed APY G$ rewards", () => { let dai: Contract; let cDAI: Contract; let goodReserve: GoodReserveCDai; let grep: GReputation; let avatar, goodDollar: IGoodDollar, controller, founder, schemeMock, signers, nameService, setDAOAddress, setSchemes, genericCall, runAsAvatarOnly, staker1, staker2; before(async () => { [founder, staker1, staker2, ...signers] = await ethers.getSigners(); schemeMock = signers.pop(); const cdaiFactory = await ethers.getContractFactory("cDAIMock"); let { controller: ctrl, avatar: av, gd, identity, nameService: ns, setDAOAddress: sda, daiAddress, cdaiAddress, reserve, reputation, runAsAvatarOnly: ras, setSchemes: ss, genericCall: gc } = await loadFixture(createDAO); setSchemes = ss; runAsAvatarOnly = ras; dai = await ethers.getContractAt("DAIMock", daiAddress); cDAI = await ethers.getContractAt("cDAIMock", cdaiAddress); avatar = av; controller = ctrl; setDAOAddress = sda; nameService = ns; goodReserve = reserve as GoodReserveCDai; genericCall = gc; console.log("deployed dao", { founder: founder.address, gd, identity, controller, avatar }); grep = (await ethers.getContractAt( "GReputation", reputation )) as GReputation; goodDollar = (await ethers.getContractAt("IGoodDollar", gd)) as IGoodDollar; //This set addresses should be another function because when we put this initialization of addresses in initializer then nameservice is not ready yet so no proper addresses // await goodReserve.setAddresses(); }); async function stake(_staker, _amount, stakingContract) { await goodDollar.mint(_staker.address, _amount); await goodDollar.connect(_staker).approve(stakingContract.address, _amount); await stakingContract.connect(_staker).stake(_amount); } const fixture_staked1year = async (wallets, provider) => { const { staking, goodDollarMintBurnWrapper } = await fixture_ready(); await stake(staker1, STAKE_AMOUNT, staking); await advanceBlocks(BLOCKS_ONE_YEAR); return { staking, goodDollarMintBurnWrapper }; }; const fixture_ready = async () => { const staking = (await ethers.deployContract("GoodDollarStakingMock", [ nameService.address, BN.from("1000000007735630000"), 518400 * 12, 30 ])) as GoodDollarStaking; await staking.upgrade(); await setDAOAddress("GDAO_STAKING", staking.address); const mintBurnWrapperFactory = await ethers.getContractFactory( "GoodDollarMintBurnWrapper" ); let goodDollarMintBurnWrapper = (await upgrades.deployProxy( mintBurnWrapperFactory, [avatar, nameService.address], { kind: "uups" } )) as unknown as GoodDollarMintBurnWrapper; await setSchemes([goodDollarMintBurnWrapper.address]); await setDAOAddress("MINTBURN_WRAPPER", goodDollarMintBurnWrapper.address); await goodDollar.mint(founder.address, "200000000000"); //mint so that 30bps cap can mint some G$ let encodedCall = goodDollarMintBurnWrapper.interface.encodeFunctionData( "addMinter", [staking.address, 0, 0, 30, 0, 0, 30, true] ); const ictrl = await ethers.getContractAt( "Controller", controller, schemeMock ); await ictrl.genericCall( goodDollarMintBurnWrapper.address, encodedCall, avatar, 0 ); return { staking: staking.connect(staker1), goodDollarMintBurnWrapper }; }; const fixture_upgradeTest = async () => { const staking = (await ethers.deployContract("GoodDollarStaking", [ nameService.address, BN.from("1000000007735630000"), 518400 * 12, 30 ])) as GoodDollarStaking; const govStaking = (await ethers.deployContract("GovernanceStaking", [ nameService.address ])) as GovernanceStaking; await setDAOAddress("GDAO_STAKING", govStaking.address); await setSchemes([staking.address]); return { staking, govStaking }; }; it("should update stakingrewardsfixedapy staker info and global stats when staking", async () => { const { staking } = await loadFixture(fixture_ready); const statsBefore = await staking.stats(); const PRECISION = await staking.PRECISION(); await stake(staker1, STAKE_AMOUNT, staking); expect(await goodDollar.balanceOf(staking.address)).equal(STAKE_AMOUNT); const info = await staking.stakersInfo(staker1.address); expect(await staking.getSavings(staker1.address)).to.equal(STAKE_AMOUNT); expect(info.rewardsPaid).to.equal(0); expect(await staking.sharesOf(staker1.address)).to.equal( (await staking.SHARE_DECIMALS()).mul(STAKE_AMOUNT) ); const stats = await staking.stats(); expect(stats.lastUpdateBlock.gt(statsBefore.lastUpdateBlock)); expect(stats.totalStaked).to.equal(STAKE_AMOUNT); expect(await staking.sharesSupply()).eq( (await staking.SHARE_DECIMALS()).mul(STAKE_AMOUNT) ); expect(stats.totalRewardsPaid).to.equal(0); expect(stats.totalStaked).to.equal(STAKE_AMOUNT); expect(stats.savings).to.equal(PRECISION.mul(STAKE_AMOUNT)); }); it("should withdraw only rewards when calling withdrawRewards", async () => { const { staking } = await loadFixture(fixture_ready); // collect 350 earned rewards: 10,000 * 5%APY = 500 total rewards, minus 30% donation await stake(staker1, STAKE_AMOUNT, staking); expect(await goodDollar.balanceOf(staking.address)).equal(STAKE_AMOUNT); await advanceBlocks(BLOCKS_ONE_YEAR); const stakeBefore = await staking.principle(staker1.address); const savingsBefore = await staking.getSavings(staker1.address); await staking.connect(staker1).withdrawRewards(); const savingsAfter = await staking.getSavings(staker1.address); const infoAfter = await staking.stakersInfo(staker1.address); expect(await staking.principle(staker1.address)) .to.equal(stakeBefore) .to.equal(STAKE_AMOUNT); expect(await goodDollar.balanceOf(staker1.address)).equal(500); expect(infoAfter.rewardsPaid).to.equal(500); expect(savingsAfter).to.equal(savingsBefore.sub(500)); expect(await staking.earned(staker1.address)).eq(0); }); it("should withdraw from deposit and undo rewards if unable to mint rewards", async () => { const { staking, goodDollarMintBurnWrapper } = await loadFixture( fixture_ready ); const PAUSE_ALL_ROLE = await goodDollarMintBurnWrapper.PAUSE_ALL_ROLE(); expect(await goodDollarMintBurnWrapper.paused(PAUSE_ALL_ROLE)).to.be.false; // pause goodDollarMintBurnWrapper const encodedCall = goodDollarMintBurnWrapper.interface.encodeFunctionData( "pause", [PAUSE_ALL_ROLE] ); await genericCall(goodDollarMintBurnWrapper.address, encodedCall); expect(await goodDollarMintBurnWrapper.paused(PAUSE_ALL_ROLE)).to.be.true; await stake(staker1, STAKE_AMOUNT, staking); expect(await goodDollar.balanceOf(staking.address)).equal(STAKE_AMOUNT); await advanceBlocks(BLOCKS_ONE_YEAR); const savingsBefore = await staking.getSavings(staker1.address); const infoBefore = await staking.stakersInfo(staker1.address); // withdraw so undo rewards will be called on rewards part await staking.withdrawStake(await staking.sharesOf(staker1.address)); const savingsAfter = await staking.getSavings(staker1.address); const infoAfter = await staking.stakersInfo(staker1.address); expect(await goodDollar.balanceOf(staker1.address)).to.eq(STAKE_AMOUNT); //we expect only the stake to have been withdrawn successfully, no rewards yet expect(savingsBefore).to.equal(STAKE_AMOUNT + 500); expect(savingsAfter).to.equal(500); expect(await staking.earned(staker1.address)).to.equal(500); expect(infoBefore.lastSharePrice).to.gt(0); expect(infoAfter.lastSharePrice).to.eq(0); //we have withdrawn all stake, so all shares are rewards (ie profit) expect(infoAfter.rewardsPaid).to.equal(0); }); it("should withdraw rewards after mint rewards is enabled again", async () => { const { staking, goodDollarMintBurnWrapper } = await loadFixture( fixture_ready ); const PAUSE_ALL_ROLE = await goodDollarMintBurnWrapper.PAUSE_ALL_ROLE(); // pause goodDollarMintBurnWrapper let encodedCall = goodDollarMintBurnWrapper.interface.encodeFunctionData( "pause", [PAUSE_ALL_ROLE] ); await genericCall(goodDollarMintBurnWrapper.address, encodedCall); await stake(staker1, STAKE_AMOUNT, staking); await advanceBlocks(BLOCKS_ONE_YEAR); // withdraw so undo rewards will be called on rewards part await staking.withdrawStake(await staking.sharesOf(staker1.address)); encodedCall = goodDollarMintBurnWrapper.interface.encodeFunctionData( "unpause", [PAUSE_ALL_ROLE] ); await genericCall(goodDollarMintBurnWrapper.address, encodedCall); expect(await goodDollarMintBurnWrapper.paused(PAUSE_ALL_ROLE)).to.be.false; expect(await goodDollar.balanceOf(staker1.address)).to.equal(STAKE_AMOUNT); await staking.withdrawStake(await staking.sharesOf(staker1.address)); const stakerInfo = await staking.stakersInfo(staker1.address); expect(await goodDollar.balanceOf(staker1.address)).to.equal( STAKE_AMOUNT + 500 ); }); it("should have upgrade deadline < 60 days", async () => { const f = await ethers.getContractFactory("GoodDollarStaking"); await expect( f.deploy( nameService.address, BN.from("1000000007735630000"), 518400 * 12, 61 ) ).revertedWith(/max two/); }); it("should not perform upgrade when not deadline", async () => { const { staking } = await loadFixture(fixture_upgradeTest); await expect(staking.upgrade()).to.revertedWith(/deadline/); }); it("should perform upgrade after deadline", async () => { const { staking, govStaking } = await loadFixture(fixture_upgradeTest); const gdaoStakingBefore = await nameService.getAddress("GDAO_STAKING"); await increaseTime(60 * 60 * 24 * 31); //pass > 30 days of await expect(staking.upgrade()).to.not.reverted; const ctrl = await ethers.getContractAt("Controller", controller); await expect(staking.upgrade()).to.reverted; //should not be able to call upgrade again //verify nameService address changed expect(gdaoStakingBefore).to.equal(govStaking.address); expect(await nameService.getAddress("GDAO_STAKING")).to.equal( staking.address ); //verify no longer registered as scheme expect(await ctrl.isSchemeRegistered(staking.address, avatar)).to.be.false; //verify rewards have changed expect((await staking.getRewardsPerBlock())[0]).gt(0); expect(await govStaking.getRewardsPerBlock()).eq(0); }); it("should set APY and change getRewardsPerBlock only by avatar", async () => { const { staking } = await loadFixture(fixture_ready); const [, gdRewardsPerBlockBeforeSet] = await staking.getRewardsPerBlock(); expect(gdRewardsPerBlockBeforeSet.add(1)).to.equal(INTEREST_RATE_5APY_X64); const gdInterestRateIn128BeforeSet = await staking.interestRatePerBlockX64(); expect(gdInterestRateIn128BeforeSet).to.equal(INTEREST_RATE_5APY_128); await runAsAvatarOnly( staking, "setGdApy(uint128)", INTEREST_RATE_10APY_X64 ); const [, gdRewardsPerBlockAfterSet] = await staking.getRewardsPerBlock(); expect(gdRewardsPerBlockAfterSet.add(1)).to.equal(INTEREST_RATE_10APY_X64); const gdInterestRateIn128AfterSet = await staking.interestRatePerBlockX64(); expect(gdInterestRateIn128AfterSet).to.equal(INTEREST_RATE_10APY_128); }); it("should be pausable by avatar", async () => { const { staking } = await loadFixture(fixture_ready); await runAsAvatarOnly(staking, "pause(bool,uint128)", true, "0"); expect(await staking.paused()).to.equal(true); let [, gdRewardsPerBlockAfterSet] = await staking.getRewardsPerBlock(); expect(gdRewardsPerBlockAfterSet).to.equal("0"); await runAsAvatarOnly( staking, "pause(bool,uint128)", false, "1000000029000000000" ); expect(await staking.paused()).to.equal(false); [, gdRewardsPerBlockAfterSet] = await staking.getRewardsPerBlock(); expect(gdRewardsPerBlockAfterSet.add(1)).to.equal("1000000029000000000"); }); it("should not be able to stake when paused", async () => { const { staking } = await loadFixture(fixture_ready); await runAsAvatarOnly(staking, "pause(bool,uint128)", true, "0"); await expect(stake(staker2, "1000", staking)).to.revertedWith(/pause/); }); it("should have max yearly apy of 20%", async () => { const { staking } = await loadFixture(fixture_ready); await runAsAvatarOnly(staking, "setGdApy(uint128)", "1000000029000000000"); let [, gdRewardsPerBlockAfterSet] = await staking.getRewardsPerBlock(); expect(gdRewardsPerBlockAfterSet.add(1)).to.equal("1000000029000000000"); //shout not be set as > 20% apy await runAsAvatarOnly(staking, "setGdApy(uint128)", "1000000030000000000"); [, gdRewardsPerBlockAfterSet] = await staking.getRewardsPerBlock(); expect(gdRewardsPerBlockAfterSet.add(1)).to.equal("1000000029000000000"); }); it("should handle stakingrewardsfixed apy correctly when transfering staking tokens to new staker", async () => { const { staking } = await loadFixture(fixture_staked1year); const RECEIVER_STAKE = 10000; const receiver = staker2; await stake(receiver, RECEIVER_STAKE, staking); const receiverInfo = await staking.stakersInfo(receiver.address); const stakerInfo = await staking.stakersInfo(staker1.address); expect(await staking.getSavings(staker1.address)).to.equal( STAKE_AMOUNT + 500 ); // 500 yearly earned reward expect(await staking.getSavings(receiver.address)).to.equal( RECEIVER_STAKE - 1 //precision loss ); const sharesToTransfer = await staking.amountToShares(200); await staking.transfer(receiver.address, sharesToTransfer); expect( (await staking.stakersInfo(staker1.address)).lastSharePrice ).to.equal(stakerInfo.lastSharePrice); // keep staker relative earnings expect(await staking.getSavings(staker1.address)).to.equal( STAKE_AMOUNT + 500 - 200 ); expect(await staking.earned(staker1.address)).to.equal(490); expect(await staking.earned(receiver.address)).to.equal(9); //the rewards part should have been transfered, there's precision loss expect((await staking.stakersInfo(receiver.address)).lastSharePrice).to.lt( receiverInfo.lastSharePrice ); // increase receiver rewards part = lower lastSharePrice expect(await staking.getSavings(receiver.address)).to.equal( RECEIVER_STAKE + 200 ); const senderInfo = await staking.stakersInfo(staker1.address); expect(senderInfo.rewardsPaid).to.equal(0); //no rewards transfer expect(await goodDollar.balanceOf(staking.address)).to.equal( STAKE_AMOUNT + RECEIVER_STAKE ); // no withdrawals yet //should be able to withdraw everything successfully, ie making sure all calculations add up await staking.withdrawStake(await staking.sharesOf(staker1.address)); expect(await staking.earned(receiver.address)).to.equal(9); //the rewards part should have been transfered, there's precision loss, it is "fixed" when withdrawing await staking .connect(receiver) .withdrawStake(await staking.sharesOf(receiver.address)); expect((await staking.stakersInfo(receiver.address)).rewardsPaid).eq(10); //withdraw transfered 1 GD to the rewards part, to make sure contract balance withdraws are correct expect((await staking.stakersInfo(staker1.address)).rewardsPaid).eq(490); expect(await goodDollar.balanceOf(staking.address)).eq(0); expect(await goodDollar.balanceOf(staker1.address)).eq(10300); expect(await goodDollar.balanceOf(receiver.address)).eq(10200); }); it("should be able to stake using onTokenTransfer", async () => { const { staking, goodDollarMintBurnWrapper } = await loadFixture( fixture_ready ); await goodDollar.mint(staker1.address, "100000000"); await expect( goodDollar .connect(staker1) .transferAndCall( staking.address, "100000000", ethers.constants.HashZero ) ).not.reverted; expect(await staking.getSavings(staker1.address)).to.equal("100000000"); }); it("should asure getStaked returns correct value", async () => { const { staking } = await loadFixture(fixture_ready); // correct after stake await stake(staker1, STAKE_AMOUNT, staking); let [userProductivity, totalProductivity] = await staking[ "getStaked(address)" ](staker1.address); let stakerShares = await staking.sharesOf(staker1.address); let totalStaked = (await staking.stats()).totalStaked; expect(userProductivity).eq(totalProductivity).to.equal(stakerShares); expect(totalStaked).to.equal(STAKE_AMOUNT); await staking.connect(staker1).withdrawStake(stakerShares.div(2)); let [userProductivity2, totalProductivity2] = await staking[ "getStaked(address)" ](staker1.address); expect(userProductivity2).to.equal(stakerShares.div(2)); expect(totalProductivity2).to.equal(stakerShares.div(2)); }); it("it should return getUserPendingReward G$ value equal to earned() rewards after donation", async () => { const { staking } = await loadFixture(fixture_ready); await stake(staker1, STAKE_AMOUNT, staking); await advanceBlocks(BLOCKS_ONE_YEAR); const [, earnedGdRewards] = await staking["getUserPendingReward(address)"]( staker1.address ); const earnedRewards = await staking.earned(staker1.address); expect(earnedGdRewards) .to.equal(earnedRewards) .to.equal(BN.from(STAKE_AMOUNT).mul(5).div(100)); // 5% apy }); it("should return G$ totalRewardsPerShare equal sharePrice()", async () => { const { staking } = await loadFixture(fixture_ready); await stake(staker1, STAKE_AMOUNT, staking); await advanceBlocks(BLOCKS_ONE_YEAR); const stats = await staking.stats(); const sharePrice = await staking.sharePrice(); let [, accumulatedGdRewardsPerShare] = await staking[ "totalRewardsPerShare()" ](); // to be changed //rewards per share = (savings - deposit) / number of shares = 10500 - 10000 / 1000000 expect(accumulatedGdRewardsPerShare.div(1e6)) //div by 1e6 to not compare exact precision due to compounding interest precision .to.equal( BN.from("10500") .sub("10000") .mul(await staking.SHARE_PRECISION()) .div(await staking.sharesSupply()) .div(1e6) ) .to.gt(0); }); it("it should not upgrade if no balance or target is not approved by dao", async () => { const { staking } = await loadFixture(fixture_ready); await expect( staking.connect(staker1).upgradeTo(signers[10].address) ).revertedWith(/no balance/); await stake(staker1, STAKE_AMOUNT, staking); await expect( staking.connect(staker1).upgradeTo(signers[10].address) ).revertedWith(/not DAO approved/); }); it("it should not upgrade if cant mint rewards", async () => { const { staking, goodDollarMintBurnWrapper } = await loadFixture( fixture_ready ); await stake(staker1, STAKE_AMOUNT, staking); await advanceBlocks(BLOCKS_ONE_YEAR); const ictrl = await ethers.getContractAt( "Controller", controller, schemeMock //has scheme permissions set by createDAO() ); await ictrl.registerScheme( signers[10].address, ethers.constants.HashZero, "0x00000001", avatar ); const PAUSE_ALL_ROLE = await goodDollarMintBurnWrapper.PAUSE_ALL_ROLE(); // pause goodDollarMintBurnWrapper let encodedCall = goodDollarMintBurnWrapper.interface.encodeFunctionData( "pause", [PAUSE_ALL_ROLE] ); await genericCall(goodDollarMintBurnWrapper.address, encodedCall); await expect( staking.connect(staker1).upgradeTo(signers[10].address) ).revertedWith(/unable to mint rewards/); }); it("it should upgrade and transfer funds to new staking contract", async () => { const { staking, goodDollarMintBurnWrapper } = await loadFixture( fixture_ready ); await stake(staker1, STAKE_AMOUNT, staking); await advanceBlocks(BLOCKS_ONE_YEAR); const f = await ethers.getContractFactory("GoodDollarStakingMock"); const newStaking = await f.deploy( nameService.address, BN.from("1000000007735630000"), 518400 * 12, 30 ); const ictrl = await ethers.getContractAt( "Controller", controller, schemeMock //has scheme permissions set by createDAO() ); await ictrl.registerScheme( newStaking.address, ethers.constants.HashZero, "0x00000001", avatar ); const balance = await staking.getSavings(staker1.address); console.log("balance:", balance.toNumber()); await staking.connect(staker1).upgradeTo(newStaking.address); expect(await goodDollar.balanceOf(newStaking.address)) .eq(balance) .gt(0); }); });