UNPKG

@gooddollar/goodprotocol

Version:
513 lines (476 loc) 18.6 kB
import { ethers, upgrades } from "hardhat"; import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; import { BigNumber, constants, Contract } from "ethers"; import { expect } from "chai"; import { GoodMarketMaker, CERC20, GoodReserveCDai, SimpleStaking, GoodFundManager, DonationsStaking } from "../../types"; import { createDAO, deployUniswap, getStakingFactory } from "../helpers"; import ContributionCalculation from "@gooddollar/goodcontracts/stakingModel/build/contracts/ContributionCalculation.json"; const BN = ethers.BigNumber; const MaxUint256 = ethers.constants.MaxUint256; export const NULL_ADDRESS = ethers.constants.AddressZero; export const BLOCK_INTERVAL = 30; describe("DonationsStaking - DonationStaking contract that receives funds in ETH/StakingToken and stake them in the SimpleStaking contract", () => { let dai: Contract; let bat: Contract; let pair: Contract, uniswapRouter: Contract, uniswapFactory: Contract; let cDAI, cDAI1, cDAI2, cDAI3, cBat, weth: Contract, comp: Contract; let gasFeeOracle, daiEthOracle: Contract, daiUsdOracle: Contract, batUsdOracle: Contract, ethUsdOracle: Contract, swapHelper, swapHelperTest, compUsdOracle: Contract; let goodReserve: GoodReserveCDai; let donationsStaking: DonationsStaking; let goodCompoundStaking; let goodFundManager: GoodFundManager; let avatar, goodDollar, identity, marketMaker: GoodMarketMaker, contribution, controller, founder, staker, schemeMock, signers, nameService, initializeToken, setDAOAddress, genericCall, goodCompoundStakingFactory; before(async () => { [founder, staker, ...signers] = await ethers.getSigners(); schemeMock = signers.pop(); const cdaiFactory = await ethers.getContractFactory("cDAIMock"); const cBatFactory = await ethers.getContractFactory("cBATMock"); const goodFundManagerFactory = await ethers.getContractFactory( "GoodFundManager" ); goodCompoundStakingFactory = await getStakingFactory( "GoodCompoundStakingV2" ); const daiFactory = await ethers.getContractFactory("DAIMock"); let { controller: ctrl, avatar: av, gd, identity, daoCreator, nameService: ns, setDAOAddress: sda, setSchemes, marketMaker: mm, daiAddress, cdaiAddress, reserve, setReserveToken, genericCall: gc } = await loadFixture(createDAO); comp = await daiFactory.deploy(); genericCall = gc; dai = await ethers.getContractAt("DAIMock", daiAddress); cDAI = await ethers.getContractAt("cDAIMock", cdaiAddress); const swapHelperTestFactory = await ethers.getContractFactory( "SwapHelperTest" ); swapHelperTest = await swapHelperTestFactory.deploy(); avatar = av; controller = ctrl; setDAOAddress = sda; nameService = ns; initializeToken = setReserveToken; goodReserve = reserve as GoodReserveCDai; console.log("deployed dao", { founder: founder.address, gd, identity, controller, avatar }); goodFundManager = (await upgrades.deployProxy( goodFundManagerFactory, [nameService.address], { kind: "uups" } )) as GoodFundManager; const uniswap = await deployUniswap(comp, dai); uniswapFactory = uniswap.factory; await setDAOAddress("UNISWAP_ROUTER", uniswap.router.address); uniswapRouter = uniswap.router; 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 }); bat = await daiFactory.deploy(); // Another erc20 token for uniswap router test cBat = await cBatFactory.deploy(bat.address); weth = uniswap.weth; console.log("setting permissions..."); const tokenUsdOracleFactory = await ethers.getContractFactory( "BatUSDMockOracle" ); compUsdOracle = await ( await ethers.getContractFactory("CompUSDMockOracle") ).deploy(); daiUsdOracle = await tokenUsdOracleFactory.deploy(); const compUsdOracleFactory = await ethers.getContractFactory( "CompUSDMockOracle" ); compUsdOracle = await compUsdOracleFactory.deploy(); await setDAOAddress("UNISWAP_ROUTER", uniswapRouter.address); await setDAOAddress("COMP", comp.address); goodCompoundStaking = await goodCompoundStakingFactory .deploy() .then(async contract => { await contract.init( dai.address, cDAI.address, nameService.address, "Good DAI", "gDAI", "172800", daiUsdOracle.address, compUsdOracle.address, [] ); return contract; }); console.log("staking contract initialized"); batUsdOracle = await tokenUsdOracleFactory.deploy(); await setDAOAddress("MARKET_MAKER", marketMaker.address); swapHelper = await ethers .getContractFactory("UniswapV2SwapHelper") .then(_ => _.deploy()); const donationsStakingFactory = await ethers.getContractFactory( "DonationsStaking", { libraries: { UniswapV2SwapHelper: swapHelper.address } } ); donationsStaking = (await upgrades.deployProxy( donationsStakingFactory, [ nameService.address, goodCompoundStaking.address, [NULL_ADDRESS, dai.address], [dai.address, NULL_ADDRESS] ], { kind: "uups", unsafeAllowLinkedLibraries: true } )) as DonationsStaking; }); it("it should stake donations with ETH", 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 + 500, false ] // set 10 gd per block ); await genericCall(goodFundManager.address, encodedData); let stakeAmount = ethers.utils.parseEther("5"); const totalStakedBeforeStake = await donationsStaking.totalStaked(); let transaction = await ( await donationsStaking.stakeDonations({ value: stakeAmount }) ).wait(); const totalStakedAfterStake = await donationsStaking.totalStaked(); expect(totalStakedBeforeStake).to.be.equal(0); expect(totalStakedAfterStake).to.be.gt(totalStakedBeforeStake); }); it("it should stake donations with DAI", async () => { let stakeAmount = ethers.utils.parseEther("10"); await dai["mint(address,uint256)"](donationsStaking.address, stakeAmount); const totalStakedBeforeStake = await donationsStaking.totalStaked(); let transaction = await (await donationsStaking.stakeDonations()).wait(); const totalStakedAfterStake = await donationsStaking.totalStaked(); expect(totalStakedAfterStake.sub(totalStakedBeforeStake)).to.be.equal( stakeAmount ); }); it("it should reverted when there is no token to stake", async () => { await expect(donationsStaking.stakeDonations()).to.be.revertedWith( /no stakingToken to stake/ ); }); it("it should stake donations with ETH according to 0.3% of pool", async () => { let stakeAmount = ethers.utils.parseEther("20"); const pairContract = await ethers.getContractAt( "UniswapPair", await uniswapFactory.getPair(await uniswapRouter.WETH(), dai.address) ); const beforeDonationReserves = await pairContract.getReserves(); let beforeDonationReserve = beforeDonationReserves[0]; if ((await pairContract.token1()) === (await uniswapRouter.WETH())) { beforeDonationReserve = beforeDonationReserves[1]; } const maxAmount = beforeDonationReserve .mul(await donationsStaking.maxLiquidityPercentageSwap()) .div(100000); let transaction = await ( await donationsStaking.stakeDonations({ value: stakeAmount }) ).wait(); const afterDonationReserves = await pairContract.getReserves(); let afterDonationReserve = afterDonationReserves[0]; const ethBalanceAfterStake = await donationsStaking.provider.getBalance( donationsStaking.address ); if ((await pairContract.token1()) === (await uniswapRouter.WETH())) { afterDonationReserve = afterDonationReserves[1]; } expect(afterDonationReserve).to.be.equal( beforeDonationReserve.add(maxAmount) ); expect(maxAmount).to.be.gt(0); expect(stakeAmount).to.be.gt(maxAmount); expect(stakeAmount.sub(maxAmount)).to.be.equal(ethBalanceAfterStake); // check leftover ETH in contract }); it("withdraw should reverted if caller not avatar", async () => { const tx = await donationsStaking .connect(staker) ["withdraw()"]() .catch(e => e); expect(tx.message).to.have.string("only avatar can call this method"); }); it("it should withdraw donationStaking when caller is avatar and return funds to avatar", async () => { const totalStakedBeforeEnd = await donationsStaking.totalStaked(); const avatarDaiBalanceBeforeEnd = await dai.balanceOf(avatar); let isActive = await donationsStaking.active(); expect(isActive).to.be.equal(true); const avatarETHBalanceBeforeWithdraw = await donationsStaking.provider.getBalance(avatar); const balance = await goodCompoundStaking.balanceOf( donationsStaking.address ); const ethBalanceBeforeWithdraw = await donationsStaking.provider.getBalance( donationsStaking.address ); const encoded = donationsStaking.interface.encodeFunctionData("withdraw"); await genericCall(donationsStaking.address, encoded); const ethBalanceAfterWithdraw = await donationsStaking.provider.getBalance( donationsStaking.address ); isActive = await donationsStaking.active(); const avatarETHBalanceAfterWithdraw = await donationsStaking.provider.getBalance(avatar); const totalStakedAfterEnd = await donationsStaking.totalStaked(); const avatarDaiBalanceAfterEnd = await dai.balanceOf(avatar); expect(avatarDaiBalanceAfterEnd).to.be.gt(avatarDaiBalanceBeforeEnd); expect(avatarDaiBalanceAfterEnd).to.be.equal(totalStakedBeforeEnd); expect(ethBalanceAfterWithdraw).to.be.equal(0); expect(avatarETHBalanceAfterWithdraw).to.be.equal( ethBalanceBeforeWithdraw.add(avatarETHBalanceBeforeWithdraw) ); expect(avatarDaiBalanceAfterEnd).to.be.equal( avatarDaiBalanceBeforeEnd.add(balance) ); expect(totalStakedAfterEnd).to.be.equal(0); }); it("should not allow to stake donations when not active", async () => { let isActive = await donationsStaking.active(); expect(isActive).to.be.equal(true); let stakeAmount = ethers.utils.parseEther("10"); await dai["mint(address,uint256)"](donationsStaking.address, stakeAmount); expect(donationsStaking.stakeDonations()).to.not.be.reverted; let encodedData = donationsStaking.interface.encodeFunctionData( "setActive", [false] ); await genericCall(donationsStaking.address, encodedData); isActive = await donationsStaking.active(); expect(isActive).to.be.equal(false); await dai["mint(address,uint256)"](donationsStaking.address, stakeAmount); await expect(donationsStaking.stakeDonations()).to.be.reverted; // revent to original state encodedData = donationsStaking.interface.encodeFunctionData("setActive", [ true ]); await genericCall(donationsStaking.address, encodedData); }); it("should not allow to set swap path on invalid path", async () => { //Valid scenario check: from ETH to staking token let pathToSet = [NULL_ADDRESS, bat.address, cDAI.address, dai.address]; let encodedData = donationsStaking.interface.encodeFunctionData( "setSwapPaths", [pathToSet] ); await genericCall(donationsStaking.address, encodedData); expect(await isEthToStakingTokenPathEqualTo(pathToSet)).to.be.true; // Invalid scenarios checks const invalidPaths = [ [NULL_ADDRESS], // less than minimum 2 length [bat.address, dai.address], // first is not ETH null address [NULL_ADDRESS, bat.address] // last is not the staking token ]; for (const invalidPath of invalidPaths) { encodedData = donationsStaking.interface.encodeFunctionData( "setSwapPaths", [invalidPath] ); await genericCall(donationsStaking.address, encodedData); expect(await isEthToStakingTokenPathEqualTo(invalidPath)).to.be.false; } encodedData = donationsStaking.interface.encodeFunctionData( "setSwapPaths", [[NULL_ADDRESS, dai.address]] ); await genericCall(donationsStaking.address, encodedData); }); async function isEthToStakingTokenPathEqualTo(path) { for (let index = 0; index < path.length; index++) { let expectedValue = path[index]; let valueAtIndex = await donationsStaking .ethToStakingTokenSwapPath(index) .catch(e => e); if (expectedValue != valueAtIndex) { return false; } } const outOfArray = await donationsStaking .ethToStakingTokenSwapPath(path.length) .catch(e => e); if (!outOfArray.message) { return false; } return true; } it("it should set stakingContract when avatar call it ", async () => { let stakeAmount = ethers.utils.parseEther("6000"); // Max swap amount is around 5964 with current liquidity level so we should set it to higher number in order to test functionality await dai["mint(address,uint256)"](donationsStaking.address, stakeAmount); await donationsStaking.stakeDonations(); const stakingAmountBeforeSet = await goodCompoundStaking.balanceOf( donationsStaking.address ); const donationsStakingETHBalanceBeforeSet = await donationsStaking.provider.getBalance(donationsStaking.address); const stakingContractBeforeSet = await donationsStaking.stakingContract(); const stakingTokenBeforeSet = await donationsStaking.stakingToken(); const avatarDaiBalanceBeforeSet = await dai.balanceOf(avatar); const reserve = await swapHelperTest.getReserves( uniswapFactory.address, dai.address, weth.address ); const safeSwappableAmount = reserve[0] .mul(BN.from(300)) .div(BN.from(100000)); const safeAmount = safeSwappableAmount > stakeAmount ? stakeAmount : safeSwappableAmount; const simpleStaking = await goodCompoundStakingFactory .deploy() .then(async contract => { await contract.init( bat.address, cBat.address, nameService.address, "Good BAT", "gBAT", "172800", daiUsdOracle.address, compUsdOracle.address, [bat.address, dai.address] ); return contract; }); //not avatar await expect( donationsStaking.setStakingContract(simpleStaking.address, [ NULL_ADDRESS, bat.address ]) ).to.be.reverted; let encodedData = donationsStaking.interface.encodeFunctionData( "setStakingContract", [simpleStaking.address, [NULL_ADDRESS, bat.address]] ); await genericCall(donationsStaking.address, encodedData); const avatarDaiBalanceAfterSet = await dai.balanceOf(avatar); const stakingAmountAfterSet = await goodCompoundStaking.balanceOf( donationsStaking.address ); const stakingContractAfterSet = await donationsStaking.stakingContract(); const stakingTokenAfterSet = await donationsStaking.stakingToken(); const donationsStakingETHBalanceAfterSet = await donationsStaking.provider.getBalance(donationsStaking.address); const daiBalanceOfDonationsStaking = await dai.balanceOf( donationsStaking.address ); expect(stakingAmountBeforeSet).to.be.gt(0); expect(stakingAmountAfterSet).to.be.equal(0); expect(stakingContractBeforeSet).to.be.equal(goodCompoundStaking.address); expect(stakingTokenBeforeSet).to.be.equal(dai.address); expect(stakingContractAfterSet).to.be.equal(simpleStaking.address); expect(stakingTokenAfterSet).to.be.equal(bat.address); expect(daiBalanceOfDonationsStaking).to.be.equal(0); // make sure there is no old staking tokens left in the donations staking expect(donationsStakingETHBalanceAfterSet).to.be.gt( // make sure that we sold possible amount of staking tokens that we can sell for ETH donationsStakingETHBalanceBeforeSet ); expect(avatarDaiBalanceAfterSet).to.be.equal( avatarDaiBalanceBeforeSet.add(stakingAmountBeforeSet.sub(safeAmount)) ); // It should send leftover stakingToken to avatar after swap to ETH in safeAmount expect(stakingAmountBeforeSet).to.be.gt(safeAmount); // maxSafeAmount must be smaller than actualstaking amount so we can verify that we hit the limit for transaction amount at once }); it("should set max liquidity percentage swap when avatar", async () => { const originalPercentage = await donationsStaking.maxLiquidityPercentageSwap(); //fail when not avatar const percentageToSet = 21; expect( donationsStaking .connect(staker) ["setMaxLiquidityPercentageSwap(uint24)"](percentageToSet) ).to.be.revertedWith(/only avatar can call this method/); //succeed when avatar let encodedData = donationsStaking.interface.encodeFunctionData( "setMaxLiquidityPercentageSwap", [percentageToSet] ); await genericCall(donationsStaking.address, encodedData); const actualPercentage = await donationsStaking.maxLiquidityPercentageSwap(); expect(actualPercentage).to.be.equal(percentageToSet); //revent to original state encodedData = donationsStaking.interface.encodeFunctionData( "setMaxLiquidityPercentageSwap", [originalPercentage] ); }); it("it should return version of DonationsStaking properly", async () => { const version = await donationsStaking.getVersion(); expect(version).to.be.equal("2.0.0"); }); });