@gooddollar/goodprotocol
Version:
GoodDollar Protocol
513 lines (476 loc) • 18.6 kB
text/typescript
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");
});
});