@gooddollar/goodprotocol
Version:
GoodDollar Protocol
1,346 lines (1,226 loc) • 80.2 kB
text/typescript
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