UNPKG

@gooddollar/goodprotocol

Version:
898 lines (735 loc) 36.1 kB
import { expect } from "chai"; import { ethers } from "hardhat"; import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; import { createDAO, advanceBlocks } from "../helpers"; import { StakingMockFixedAPY } from "../../types"; import { default as StakingABI } from "../../artifacts/contracts/mocks/StakingMockFixedAPY.sol/StakingMockFixedAPY.json"; const BN = ethers.BigNumber; // APY=5% | Blocks per year = 12*60*24*365 = 6307200 // per block = nroot(1+0.05,numberOfBlocksPerYear) = 1000000007735630000 const BLOCKS_ONE_YEAR = 6307200; const BLOCKS_FOUR_YEARS = 25228800; const BLOCKS_TEN_YEARS = 63072000; 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 // APY = 8% | nroot(1+0.08,numberOfBlocksPerYear) = 1000000012202093100 const INTEREST_RATE_8APY_X64 = BN.from("1000000012202093100"); // x64 representation of same number describe("StakingRewardsFixedAPY - generic staking for fixed APY rewards contract", () => { let signers, setNSAddress, nameService, avatar, genericCall, controller, fixedStaking: StakingMockFixedAPY, goodDollar, founder, staker1, staker2, staker3, staker4; async function stake(_staker, _amount, contract = fixedStaking) { await contract.connect(_staker).stake(_staker.address, _amount); } // on withdraw: _amount / sharePrice = shares redeemed // on stake: _amount / sharePrice = shares added async function getExpectedSharesChange(_amount, _contract = fixedStaking) { return BN.from(_amount) .mul(await _contract.SHARE_PRECISION()) .div(await _contract.sharePrice()); } async function expectSavings(_staker, _amount, _contract = fixedStaking) { const savings = await _contract.getSavings(_staker.address); expect(savings.eq(_amount)).to.be.true; } before(async () => { [founder, staker1, staker2, staker3, staker4, ...signers] = await ethers.getSigners(); let { controller: ctrl, avatar: av, genericCall: gc, gd, nameService: ns, setDAOAddress } = await loadFixture(createDAO); setNSAddress = setDAOAddress; nameService = ns; avatar = av; genericCall = gc; controller = ctrl; goodDollar = await ethers.getContractAt("IGoodDollar", gd); }); const fixture_initOnly = async () => { const staking: StakingMockFixedAPY = (await ethers.deployContract( "StakingMockFixedAPY", [INTEREST_RATE_5APY_X64] )) as StakingMockFixedAPY; return { staking }; }; const fixture_2 = async () => { const staking: StakingMockFixedAPY = (await ethers.deployContract( "StakingMockFixedAPY", [INTEREST_RATE_5APY_X64] )) as StakingMockFixedAPY; return { staking }; }; const fixture_1year = async () => { const staking: StakingMockFixedAPY = (await ethers.deployContract( "StakingMockFixedAPY", [INTEREST_RATE_5APY_X64] )) as StakingMockFixedAPY; await stake(staker1, 10000, staking); await stake(staker2, 10000, staking); await stake(staker3, 10000, staking); await advanceBlocks(BLOCKS_ONE_YEAR); return { staking }; }; const fixture_1year_single = async () => { const staking: StakingMockFixedAPY = (await ethers.deployContract( "StakingMockFixedAPY", [INTEREST_RATE_5APY_X64] )) as StakingMockFixedAPY; await stake(staker3, 10000, staking); await advanceBlocks(BLOCKS_ONE_YEAR); return { staking }; }; it("should set APY successfully", async () => { const { staking } = await loadFixture(fixture_initOnly); const beforeSetInterestRateIn128 = await staking.interestRatePerBlockX64(); const interestRatePerBlockX64 = BN.from(INTEREST_RATE_10APY_X64); // x64 representation of same number const interestRateInt128Format = BN.from(INTEREST_RATE_10APY_128); // 128 representation of same number await staking.setAPY(interestRatePerBlockX64); const afterSetInterestRateIn128 = await staking.interestRatePerBlockX64(); expect(afterSetInterestRateIn128).to.not.equal(beforeSetInterestRateIn128); expect(afterSetInterestRateIn128).to.equal(interestRateInt128Format); }); it("should update staker info after stake operation", async () => { const { staking } = await loadFixture(fixture_initOnly); await stake(staker1, 9000, staking); let info = await staking.stakersInfo(staker1.address); const initialShares = (await staking.SHARE_DECIMALS()).mul(9000); expect(info.lastSharePrice) .to.equal( (await staking.SHARE_PRECISION()).div(await staking.SHARE_DECIMALS()) ) .to.eq(await staking.sharePrice()); // (1g$ with 2 decimals) expect(await staking.sharesSupply()) .to.equal(initialShares) .to.equal(await staking.totalSupply()); expect(await staking.sharesOf(staker1.address)) .eq(initialShares) .eq(await staking.balanceOf(staker1.address)); expect(info.rewardsPaid).to.equal(0); }); it("should handle stake/withdraw the minimal amount of 1", async () => { const { staking } = await loadFixture(fixture_initOnly); await stake(staker4, 1, staking); await advanceBlocks(BLOCKS_TEN_YEARS); await advanceBlocks(BLOCKS_FOUR_YEARS); await expectSavings(staker4, 1, staking); // (1.01^14) = 1.979931 // await expect((await staking.sharePrice()).eq(BN.from(1979931))).to.be.true; await advanceBlocks(BLOCKS_ONE_YEAR); await expectSavings(staker4, 2, staking); // (1.01^15) = 2.078928 await expect((await staking.sharePrice()).eq(207892821613185)).to.be.true; const minimalShares = await staking.amountToShares(1); const stakerInfo = await staking.stakersInfo(staker4.address); console.log({ sharePrice: await staking.sharePrice(), minimalShares, stakerInfo, stakerShares: await staking.balanceOf(staker4.address) }); await staking.withdraw(staker4.address, minimalShares); //this also withdraws the donated rewards let info = await staking.stakersInfo(staker4.address); await expectSavings(staker4, 1, staking); expect(info.rewardsPaid).to.equal(1); expect(await staking.balanceOf(staker4.address)).to.equal( 10000 - minimalShares.toNumber() ); await expect( staking.withdraw( staker4.address, (await staking.amountToShares(1)).sub(1) ) ).revertedWith(/min shares/); }); it("should fail on staking 0", async () => { const { staking } = await loadFixture(fixture_initOnly); await expect(stake(staker4, 0, staking)).to.be.revertedWith(/stake 0/); }); xit("should fail on staking with donationRatio > 100", async () => { const { staking } = await loadFixture(fixture_initOnly); await expect(stake(staker4, 1, 101, staking)).to.be.revertedWith( /donation/ ); }); xit("should fail on staking less than minimal amount of 1", async () => { const { staking } = await loadFixture(fixture_initOnly); await expect(stake(staker4, 1, staking)).to.be.reverted; }); it("Should fail to withdraw exceeding amount", async () => { const { staking } = await loadFixture(fixture_initOnly); await stake(staker1, 1000, staking); const shares = await staking.balanceOf(staker1.address); await expect(staking.withdraw(staker1.address, shares.add(1))).revertedWith( /no balance/ ); }); it("Should fail to withdraw when empty balance", async () => { const { staking } = await loadFixture(fixture_initOnly); await expect(staking.withdraw(staker1.address, 0)).revertedWith( /no balance/ ); }); it("should update global stats after stake operation", async () => { const { staking } = await loadFixture(fixture_initOnly); const statsBefore = await staking.stats(); const PRECISION = await staking.PRECISION(); await stake(staker1, 9000, staking); const statsAfter = await staking.stats(); expect(statsAfter.lastUpdateBlock.gt(statsBefore.lastUpdateBlock)); expect(statsAfter.totalStaked).to.equal(9000); expect(await staking.sharesSupply()).eq( (await staking.SHARE_DECIMALS()).mul(9000) ); expect(statsAfter.totalRewardsPaid).to.equal(0); expect(statsAfter.savings).to.equal(PRECISION.mul(9000)); }); it("should update staker info after withdraw operation", async () => { const { staking } = await loadFixture(fixture_initOnly); await stake(staker1, 9000, staking); await advanceBlocks(BLOCKS_ONE_YEAR); const sharesToWithdraw = await staking.amountToShares(4000); const rewardsBalanceBefore = await staking.earned(staker1.address); expect(rewardsBalanceBefore).eq(450); await staking.withdraw(staker1.address, sharesToWithdraw); let info = await staking.stakersInfo(staker1.address); const initialShares = (await staking.SHARE_DECIMALS()).mul(9000); const shares = await staking.sharesOf(staker1.address); const savings = await staking.getSavings(staker1.address); const rewardsBalance = await staking.earned(staker1.address); const depositShareAfterWithdraw = info.lastSharePrice .mul(shares) .div(await staking.SHARE_PRECISION()); expect(savings).to.eq(5450); // 9000 deposit + 450 rewards - 4000 withdrawn expect(rewardsBalance).to.eq(0); // 449 - 190 expect(shares).to.equal(initialShares.sub(sharesToWithdraw)); expect(info.rewardsPaid).to.equal(450); //relative amount of withdraw from total savings multiplied by rewards earned 4000/9450 * 450 and rounded up await staking.withdraw(staker1.address, shares); //now withdraw everything info = await staking.stakersInfo(staker1.address); expect(await staking.sharesOf(staker1.address)).eq(0); expect(info.rewardsPaid).to.equal(450); }); it("should update global stats after withdraw operation", async () => { const { staking } = await loadFixture(fixture_initOnly); await stake(staker1, 9000, staking); const statsBefore = await staking.stats(); await advanceBlocks(BLOCKS_ONE_YEAR); const sharesToWithdraw = await staking.amountToShares(4000); await staking.withdraw(staker1.address, sharesToWithdraw); const statsAfter = await staking.stats(); const initialShares = (await staking.SHARE_DECIMALS()).mul(9000); expect(statsAfter.lastUpdateBlock.gt(statsBefore.lastUpdateBlock)); expect(statsAfter.totalStaked).to.equal(9000 - 4000 + 450); // 9000 - (4000 - 190 rewards component withdrawn) expect(await staking.sharesSupply()).to.equal( initialShares.sub(sharesToWithdraw) ); expect(statsAfter.totalRewardsPaid).to.equal(450); expect(statsAfter.savings).to.equal(await staking.compoundNextBlock()); }); it("should compound savings over period", async () => { const { staking } = await loadFixture(fixture_1year); console.log( "shares:", await staking.balanceOf(staker1.address), await staking.balanceOf(staker2.address), await staking.balanceOf(staker3.address), "info:", await staking.stakersInfo(staker1.address), await staking.stakersInfo(staker2.address), await staking.stakersInfo(staker3.address) ); let savings = await staking.getSavings(staker1.address); expect(savings).to.equal(10500); savings = await staking.getSavings(staker2.address); expect(savings).to.equal(10500); savings = await staking.getSavings(staker3.address); expect(savings).to.equal(10499); //bought in 2 blocks after let info = await staking.stakersInfo(staker1.address); const initialShares = (await staking.SHARE_DECIMALS()).mul(10000); expect(await staking.principle(staker1.address)).to.equal(10000); expect(await staking.balanceOf(staker1.address)).to.equal(initialShares); info = await staking.stakersInfo(staker2.address); expect(await staking.principle(staker2.address)).to.equal(9999); expect(await staking.balanceOf(staker2.address)).to.equal(99999999); expect(info.rewardsPaid).to.equal(0); info = await staking.stakersInfo(staker3.address); expect(await staking.principle(staker3.address)).to.equal(9999); expect(await staking.balanceOf(staker3.address)).to.equal(99999998); expect(info.rewardsPaid).to.equal(0); }); it("should compound savings over 2 years and new staker after 1 year", async () => { const { staking } = await loadFixture(fixture_1year); //add staker after first year await stake(staker4, 125125, staking); await advanceBlocks(BLOCKS_ONE_YEAR); //check all stakes after 2nd year let savings = await staking.getSavings(staker1.address); expect(savings).to.equal(11025); savings = await staking.getSavings(staker2.address); expect(savings).to.equal(11025); savings = await staking.getSavings(staker3.address); expect(savings).to.equal(11025); savings = await staking.getSavings(staker4.address); expect(savings).to.equal(131381); }); it("should withdraw full amount", async () => { const { staking } = await loadFixture(fixture_1year); const balance = await staking.sharesOf(staker1.address); await staking.withdraw(staker1.address, balance); const info = await staking.stakersInfo(staker1.address); expect(await staking.getSavings(staker1.address)).to.equal(0); expect(await staking.balanceOf(staker1.address)).to.equal(0); expect(info.rewardsPaid).to.equal(500); }); it("should withdraw partial amount and calculate savings correctly after 1 year", async () => { const { staking } = await loadFixture(fixture_1year); const sharesBefore = await staking.sharesOf(staker3.address); //9500 withdraw / sharePrice = shares to reduce const expectedSharesRedeemed = await staking.amountToShares(9500); await staking.withdraw(staker3.address, expectedSharesRedeemed); const balanceAfterWithdraw = await staking.getSavings(staker3.address); expect(balanceAfterWithdraw).to.equal(999); //shares are not exactly 9500 expect((await staking.stakersInfo(staker3.address)).rewardsPaid).to.eq(500); await advanceBlocks(BLOCKS_ONE_YEAR); const info = await staking.stakersInfo(staker3.address); const earnedRewards = await staking.earned(staker3.address); expect(await staking.getSavings(staker3.address)).to.equal(1049); //savings after 999 + 1 year 5% expect(earnedRewards).to.equal(50); //check shares expect(await staking.sharesOf(staker3.address)).to.equal( sharesBefore.sub(expectedSharesRedeemed) ); }); xit("should withdraw partial amount when partially donating and calculate savings correctly after 1 year", async () => { const { staking } = await loadFixture(fixture_1year); const infoBefore = await staking.stakersInfo(staker2.address); const expectedSharesRedeemed = await getExpectedSharesChange( 9500 + 125, staking ); //withdrawing 9500 but 125 donated rewards will be withdrawn also await staking.withdraw(staker2.address, BN.from(9500)); // will withdraw 9500 from savings but also 125 donated rewards, 375 will be withdrawn from the rewards part. const balanceAfterWithdraw = await staking.getSavings(staker2.address); expect(balanceAfterWithdraw).to.equal(875); //10500 - 9500 + 125 donated await advanceBlocks(BLOCKS_ONE_YEAR); const info = await staking.stakersInfo(staker2.address); const [earnedRewards, earnedRewardsAfterDonations] = await staking.earned( staker2.address ); expect(await staking.getSavings(staker2.address)).to.equal( 907 //918 after 1 year. rewards part 43, donated 43*0.25=10.75 = 918-10.75 ); // 875 + 5%APY * 25%donation expect(info.deposit).to.equal(875); expect(info.rewardsPaid).to.equal(375); expect(info.rewardsDonated).to.equal(125); expect(info.avgDonationRatio).to.equal((await staking.PRECISION()).mul(25)); expect(info.shares.toNumber()).to.equal( infoBefore.shares.sub(expectedSharesRedeemed) ); expect( info.shares .mul(await staking.sharePrice()) .div(await staking.SHARE_PRECISION()) ).to.equal(info.deposit.add(earnedRewards)); expect(await staking.getSavings(staker2.address)).to.equal( info.deposit.add(earnedRewardsAfterDonations) ); }); xit("should withdraw partial amount when donating 100% and calculate savings correctly after 1 year", async () => { const { staking } = await loadFixture(fixture_1year); const infoBefore = await staking.stakersInfo(staker1.address); const expectedSharesRedeemed = await getExpectedSharesChange( 9500 + 500, staking ); //withdrawing 9500 but 500 donated rewards will be withdrawn also await staking.withdraw(staker1.address, BN.from(9500)); // this will withdraw 9500 from deposit but also 500 donated rewards const balanceAfterWithdraw = await staking.getSavings(staker1.address); expect(balanceAfterWithdraw).to.equal(500); await advanceBlocks(BLOCKS_ONE_YEAR); const info = await staking.stakersInfo(staker1.address); const [earnedRewards, earnedRewardsAfterDonations] = await staking.earned( staker1.address ); expect(await staking.getSavings(staker1.address)).to.equal(500); expect(info.deposit).to.equal(500); expect(info.rewardsPaid).to.equal(0); expect(info.rewardsDonated).to.equal(500); expect(info.avgDonationRatio).to.equal( (await staking.PRECISION()).mul(100) ); expect(info.shares.toNumber()).to.equal( infoBefore.shares.sub(expectedSharesRedeemed) ); expect( info.shares .mul(await staking.sharePrice()) .div(await staking.SHARE_PRECISION()) ).to.equal(info.deposit.add(earnedRewards)); expect(await staking.getSavings(staker1.address)).to.equal( info.deposit.add(earnedRewardsAfterDonations) ); }); xit("should withdraw rewards from rewards only", async () => { const { staking } = await loadFixture(fixture_1year); const infoBefore = await staking.stakersInfo(staker3.address); const balance = await staking.getSavings(staker3.address); const expectedSharesRedeemed = await getExpectedSharesChange(500, staking); await staking.withdraw(staker3.address, 500); const info = await staking.stakersInfo(staker3.address); expect(balance).to.equal(10500); //initial stake 10000 + 5% expect(await staking.getSavings(staker3.address)).to.equal(10000); expect(info.deposit).to.equal(10000); expect(info.shares.toNumber()).to.equal( infoBefore.shares.sub(expectedSharesRedeemed) ); expect(info.rewardsPaid).to.equal(500); expect(info.rewardsDonated).to.equal(0); expect(info.avgDonationRatio).to.equal(0); }); xit("should update avgDonationRatio after second stake", async () => { const { staking } = await loadFixture(fixture_1year); const infoBefore = await staking.stakersInfo(staker1.address); const statsBefore = await staking.stats(); await staking.stake( staker1.address, BN.from(infoBefore.shares) // staker1 buys same amount of shares he had .mul(await staking.sharePrice()) .div(await staking.SHARE_PRECISION()), 0 ); const infoAfter = await staking.stakersInfo(staker1.address); const statsAfter = await staking.stats(); const PRECISION = await staking.PRECISION(); expect(infoBefore.avgDonationRatio).to.equal(PRECISION.mul(100)); // 1st stake had 100% donation expect(infoAfter.avgDonationRatio).to.equal(PRECISION.mul(50)); // 2nd stake had 0% for same amount of shares => 50% average expect(statsBefore.avgDonationRatio).to.equal(PRECISION.mul(125).div(3)); // total avg of 3 stakers => 0, 25, 100 each had staked 10000 expect(statsAfter.avgDonationRatio).to.equal( BN.from("31249999999999999999") ); // 31.25% = (2 * 0% + 1 * 25% + 1 * 100%) / 4 }); xit("should update avgDonationRatio after partial withdraw", async () => { const { staking } = await loadFixture(fixture_1year); const infoBeforeWithdraw = await staking.stakersInfo(staker1.address); const statsBeforeWithdraw = await staking.stats(); const expectedSharesRedeemed = await getExpectedSharesChange( 2000 + 500, staking ); //2000 + 500 that are donated await staking.withdraw(staker1.address, 2000); //this also withdraws the donated rewards const statsAfterWithdraw = await staking.stats(); const expectedGlobalAvgRatio = statsBeforeWithdraw.avgDonationRatio .mul(statsBeforeWithdraw.totalShares) .sub(expectedSharesRedeemed.mul(infoBeforeWithdraw.avgDonationRatio)) .div(statsAfterWithdraw.totalShares); const infoAfterWithdraw = await staking.stakersInfo(staker1.address); expect(infoBeforeWithdraw.avgDonationRatio).to.equal( infoAfterWithdraw.avgDonationRatio ); expect(expectedGlobalAvgRatio).to.equal( statsAfterWithdraw.avgDonationRatio ); }); it("should calculate correct share price after savings has grown", async () => { const { staking } = await loadFixture(fixture_1year); const SHARE_PRECISION = await staking.SHARE_PRECISION(); const savingsBefore = await staking.compound(); const expectedSharePriceBefore = BN.from(savingsBefore) .mul(SHARE_PRECISION) .div(await staking.PRECISION()) .div(await staking.sharesSupply()); const actualSharePriceBefore = await staking.sharePrice(); expect(actualSharePriceBefore).to.equal(expectedSharePriceBefore); await advanceBlocks(BLOCKS_ONE_YEAR); const savingsAfter = savingsBefore.mul(105).div(100); //estimate const expectedSharePriceAfter = BN.from(savingsAfter) .mul(SHARE_PRECISION) .div(await staking.PRECISION()) .div(await staking.sharesSupply()); const actualSharePriceAfter = await staking.sharePrice(); expect(actualSharePriceAfter.div(1e8)).to.eq( expectedSharePriceAfter.div(1e8) ); //compare rough estimate so we reduce precision by 1e8 }); it("should check compound function compounds savings correctly", async () => { const { staking } = await loadFixture(fixture_1year); const PRECISION = await staking.PRECISION(); const expectedCompoundBefore = 3 * 10000 * 1.05; // 3 stakers of 10000 with 5 APY, after one year const actualCompoundBefore = (await staking.compound()).div(PRECISION); expect(actualCompoundBefore).to.equal(expectedCompoundBefore); await advanceBlocks(BLOCKS_ONE_YEAR); const expectedCompoundAfter = expectedCompoundBefore * 1.05; const actualCompoundAfter = (await staking.compound()).div(PRECISION); expect(actualCompoundAfter).to.equal(expectedCompoundAfter); expect(actualCompoundAfter.gt(actualCompoundBefore)).to.be.true; }); it("should calculate earned rewards in period", async () => { const { staking } = await loadFixture(fixture_1year); let earnedRewards1 = await staking.earned(staker1.address); let earnedRewards2 = await staking.earned(staker2.address); let earnedRewards3 = await staking.earned(staker3.address); expect(earnedRewards1).equal(earnedRewards2).equal(500); expect(earnedRewards3).eq(499); await advanceBlocks(BLOCKS_ONE_YEAR); earnedRewards1 = await staking.earned(staker1.address); earnedRewards2 = await staking.earned(staker2.address); earnedRewards3 = await staking.earned(staker3.address); expect(earnedRewards1) .equal(earnedRewards2) .equal(earnedRewards3) .equal(1025); }); it("Should undo reward part and update staker info", async () => { const { staking } = await loadFixture(fixture_1year); const initialInfo = await staking.stakersInfo(staker3.address); const sharesToWithdraw = await staking.amountToShares(500); const initialRewards = await staking.earned(staker3.address); await staking.withdrawAndUndo(staker3.address, sharesToWithdraw); const infoAfterUndo = await staking.stakersInfo(staker3.address); expect(await staking.getSavings(staker3.address)).to.equal(10499); expect(await staking.earned(staker3.address)).to.eq(500); expect(infoAfterUndo.rewardsPaid).to.equal(initialInfo.rewardsPaid); }); it("Should undo reward and keep global stats the same", async () => { const { staking } = await loadFixture(fixture_1year); const initialStats = await staking.stats(); const initialSavings = await staking.compoundNextBlock(); //withdrawAndUndo will calculate savings of next block const sharesToWithdraw = await staking.amountToShares(500); const initialShares = await staking.sharesSupply(); await staking.withdrawAndUndo(staker3.address, sharesToWithdraw); const statsAfterUndo = await staking.stats(); expect(statsAfterUndo.savings.div(ethers.constants.WeiPerEther)).to.equal( 31500 ); expect(initialStats.totalRewardsPaid).to.equal( statsAfterUndo.totalRewardsPaid ); expect(await staking.sharesSupply()).to.lt(initialShares); expect(initialStats.totalStaked).to.equal(statsAfterUndo.totalStaked); }); xit("Should undo reward when part of them is donated and keep info and global stats the same", async () => { const { staking } = await loadFixture(fixture_1year); const initialStats = await staking.stats(); const initialInfo = await staking.stakersInfo(staker2.address); const initialSavings = await staking.getSavings(staker2.address); const latestSavings = await staking.compoundNextBlock(); //withdrawAndUndo will calculate savings of next block await staking.withdrawAndUndo(staker2.address, 375); //375 rewards 125 donated const infoAfterUndo = await staking.stakersInfo(staker2.address); expect(await staking.getSavings(staker2.address)).to.equal(initialSavings); expect(infoAfterUndo.rewardsPaid).to.equal(0); expect(infoAfterUndo.rewardsDonated).to.equal(0); expect(infoAfterUndo.avgDonationRatio).to.equal( initialInfo.avgDonationRatio ); expect(initialInfo.shares).to.eq(infoAfterUndo.shares); //check global stats const statsAfterUndo = await staking.stats(); expect(statsAfterUndo.savings).to.equal(latestSavings); expect(initialStats.totalRewardsDonated).to.equal( statsAfterUndo.totalRewardsDonated ); expect(initialStats.totalRewardsPaid).to.equal( statsAfterUndo.totalRewardsPaid ); expect(initialStats.totalShares).to.equal(statsAfterUndo.totalShares); expect(initialStats.totalStaked).to.equal(statsAfterUndo.totalStaked); expect(initialStats.avgDonationRatio).to.equal( statsAfterUndo.avgDonationRatio.add(1) //precision loss during withdraw avgDonationRatio calculation ); }); it("Should undo reward when withdrawing partial rewards keep info and global stats the same", async () => { const { staking } = await loadFixture(fixture_1year); const initialInfo = await staking.stakersInfo(staker2.address); const initialSavings = await staking.getSavings(staker2.address); const initialStats = await staking.stats(); const initialTotalSavings = await staking.compoundNextBlock(); //withdrawAndUndo will calculate savings of next block const initialSharesSupply = await staking.sharesSupply(); //current rewards are 500 so 250 is only partial withdraw of rewards const sharesToWithdraw = await staking.amountToShares(250); await staking.withdrawAndUndo(staker2.address, sharesToWithdraw); const infoAfterUndo = await staking.stakersInfo(staker2.address); expect(await staking.getSavings(staker2.address)).to.equal(initialSavings); expect(infoAfterUndo.rewardsPaid).to.equal(initialInfo.rewardsPaid); //check global stats const statsAfterUndo = await staking.stats(); expect(statsAfterUndo.savings).to.equal(initialTotalSavings); expect(initialStats.totalRewardsDonated).to.equal( statsAfterUndo.totalRewardsDonated ); expect(initialStats.totalRewardsPaid).to.equal( statsAfterUndo.totalRewardsPaid ); expect(initialSharesSupply).to.equal((await staking.sharesSupply()).add(1)); //precision loss when converting back from rewards amount to shares expect(initialStats.totalStaked).to.equal(statsAfterUndo.totalStaked); }); //helper test // it.only("Should not suffer from endless precission loss", async () => { // const { staking } = await loadFixture(fixture_1year_single); // const initialShares = await staking.sharesOf(staker3.address); // const maxLoss = await staking.amountToShares(1); // for (let i = 0; i < 500; i++) { // expect(await staking.getSavings(staker3.address)).eq(10500); // expect(await staking.sharesOf(staker3.address)).gte( // initialShares.sub(maxLoss) // ); // expect(await staking.earned(staker3.address)).eq(500); // const sharesToWithdraw = await staking.amountToShares(500); // await staking.withdrawAndUndo(staker3.address, sharesToWithdraw); // } // }); xit("Should undo reward when withdrawing rewards + deposit and update deposit info and stats correctly", async () => {}); it("Should be able to withdraw right after staking", async () => { const { staking } = await loadFixture(fixture_1year); await stake(staker4, 10000, staking); await expect( staking.withdraw( staker4.address, await staking.balanceOf(staker4.address) ) ).not.reverted; const info = await staking.stakersInfo(staker4.address); expect(await staking.getSavings(staker4.address)).to.equal(0); expect(info.rewardsPaid).to.equal(0); expect(info.lastSharePrice.div(1e8)).to.equal( (await staking.sharePrice()).div(1e8) ); }); it("should calculate savings correctly after set APY ", async () => { const { staking } = await loadFixture(fixture_1year); await stake(staker4, 125125, staking); // before set, APY is 5% const beforeSetInterestRateIn128 = await staking.interestRatePerBlockX64(); expect(beforeSetInterestRateIn128).to.equal(INTEREST_RATE_5APY_128); // set APY to 10% await staking.setAPY(INTEREST_RATE_10APY_X64); await advanceBlocks(BLOCKS_ONE_YEAR); await expectSavings(staker1, 11550, staking); await expectSavings(staker2, 11550, staking); await expectSavings(staker3, 11550, staking); // 10000 + 10000((1.05APY1 * 1.10APY2) - 1) await expectSavings(staker4, 137637, staking); // 125125((1.10APY2) - 1) // set APY to 8% await staking.setAPY(INTEREST_RATE_8APY_X64); await advanceBlocks(BLOCKS_ONE_YEAR); await expectSavings(staker1, 12474, staking); await expectSavings(staker2, 12474, staking); await expectSavings(staker3, 12474, staking); await expectSavings(staker4, 148648, staking); // 125125((1.10APY2 * 1.08APY3) - 1) }); it("should handle first stake big, followed by smaller actions", async () => { const { staking } = await loadFixture(fixture_2); await stake(staker4, 10000000, staking); const savingsAfterBigStake = await staking.getSavings(staker4.address); const infoAfterBigStake = await staking.stakersInfo(staker4.address); await stake(signers[0], 5, staking); const savingsAfterSmallStake = await staking.getSavings(staker4.address); const smallStakeSavings = await staking.getSavings(signers[0].address); expect(smallStakeSavings).gt(0); const sharesAfterBigStake = await staking.balanceOf(staker4.address); const sharesAfterSmallStake = await staking.balanceOf(signers[0].address); expect(savingsAfterSmallStake.eq(savingsAfterBigStake)).to.be.true; expect(sharesAfterSmallStake).gt(0); const onegdShares = await staking.amountToShares(1); await expect(staking.withdraw(staker4.address, 1000)).revertedWith(/min/); await expect(staking.withdraw(staker4.address, onegdShares)).not.reverted; await expect( staking.withdraw( signers[0].address, await staking.balanceOf(signers[0].address) ) ).not.reverted; const sharesAfterWithdraw = await staking.balanceOf(staker4.address); const sharesAfterWithdraw2 = await staking.balanceOf(signers[0].address); expect(sharesAfterWithdraw).to.equal(sharesAfterBigStake.sub(onegdShares)); expect(sharesAfterWithdraw2).to.equal(0); }); it("should handle first stake small, followed by 100 Billion stake", async () => { const { staking } = await loadFixture(fixture_2); await stake(signers[0], 5, staking); await stake(staker4, 1e13, staking); const onegdShares = await staking.amountToShares(1); await staking.withdraw(staker4.address, onegdShares); await expect( staking.withdraw( signers[0].address, await staking.balanceOf(signers[0].address) ) ).not.reverted; }); it("should handle first 100 Billion stake, followed by a small", async () => { const { staking } = await loadFixture(fixture_2); await stake(staker4, 1e13, staking); await stake(signers[0], 5, staking); const onegdShares = await staking.amountToShares(1); await expect( staking.withdraw( signers[0].address, await staking.balanceOf(signers[0].address) ) ).not.reverted; await expect(staking.withdraw(staker4.address, onegdShares)).not.reverted; }); xit("should withdraw all when amount=max uint", async () => { const { staking } = await loadFixture(fixture_1year); await expect(staking.withdraw(staker3.address, 0)).revertedWith(/balance/); await staking.withdraw(staker3.address, ethers.constants.MaxUint256); const info = await staking.stakersInfo(staker3.address); expect(info.rewardsPaid).to.equal(499); expect(await staking.sharesOf(staker3.address)).to.equal(0); expect(await staking.getSavings(staker3.address)).to.equal(0); }); it("should be able to get rewards debt (ie savings - deposits - donated rewards)", async () => { const { staking } = await loadFixture(fixture_1year); const debt = (await staking.getRewardsDebt()).div( ethers.utils.parseEther("1") ); //debt is in 1e18 precision expect(debt).to.equal(1500); //30000*1.05 - 300000 }); it("should not be able to stake less than share price", async () => { const { staking } = await loadFixture(fixture_1year); await advanceBlocks(BLOCKS_TEN_YEARS * 20); await expect(stake(staker1, 1, staking)).to.revertedWith(/share/); await expect(stake(staker1, 2, staking)).to.not.reverted; }); it("should not be able to withdraw less than share price", async () => { const { staking } = await loadFixture(fixture_1year); await advanceBlocks(BLOCKS_TEN_YEARS * 10); const sharePrice = await staking.sharePrice(); await expect(staking.withdraw(staker3.address, 1)).to.revertedWith(/share/); }); it("should handle stake/withdraw for 1 Trillion staked for 50 years", async () => { const { staking } = await loadFixture(fixture_1year); await stake(staker3, 100000000000000, staking); await advanceBlocks(BLOCKS_TEN_YEARS * 5); await expect( staking.withdraw( staker3.address, await staking.balanceOf(staker3.address) ) ).to.not.reverted; }); it("should have undo reward handle invalid input", async () => { const staking: StakingMockFixedAPY = (await ( await ethers.getContractFactory("StakingMockFixedAPY") ).deploy(INTEREST_RATE_5APY_X64)) as StakingMockFixedAPY; //undo 0 rewards await stake(staker4, 10000, staking); await staking.withdraw( staker4.address, await staking.balanceOf(staker4.address) ); await expect(staking.undoReward(staker4.address, 0)).to.not.reverted; }); });