import { default as hre, ethers, upgrades } from "hardhat";
import { loadFixture } from "@nomicfoundation/hardhat-network-helpers";
import { BigNumber, Contract, Signer } from "ethers";
import { expect } from "chai";
import { GoodMarketMaker } from "../../types";
import { createDAO, increaseTime, advanceBlocks } from "../helpers";
import ContributionCalculation from "@gooddollar/goodcontracts/stakingModel/build/contracts/ContributionCalculation.json";

const BN = ethers.BigNumber;
export const NULL_ADDRESS = ethers.constants.AddressZero;
export const BLOCK_INTERVAL = 1;

describe("GoodReserve - staking with cDAI mocks", () => {
  let dai: Contract;
  let cDAI;
  let goodReserve: Contract;
  let goodDollar,
    avatar,
    identity,
    marketMaker: GoodMarketMaker,
    contribution,
    controller,
    founder,
    staker,
    schemeMock,
    signers,
    exchangeHelper: Contract,
    setDAOAddress,
    nameService,
    runAsAvatarOnly;

  before(async () => {
    [founder, staker, ...signers] = await ethers.getSigners();
    schemeMock = signers.pop();

    let {
      controller: ctrl,
      avatar: av,
      gd,
      identity,
      daoCreator,
      nameService: ns,
      setDAOAddress: sda,
      setSchemes,
      marketMaker: mm,
      daiAddress,
      cdaiAddress,
      reserve,
      runAsAvatarOnly: raao
    } = await loadFixture(createDAO);

    dai = await ethers.getContractAt("DAIMock", daiAddress);
    cDAI = await ethers.getContractAt("cDAIMock", cdaiAddress);

    avatar = av;
    controller = ctrl;
    setDAOAddress = sda;
    nameService = ns;
    runAsAvatarOnly = raao;
    await setDAOAddress("UNISWAP_ROUTER", signers[0].address);
    const exchangeHelperFactory = await ethers.getContractFactory(
      "ExchangeHelper"
    );
    exchangeHelper = await upgrades.deployProxy(
      exchangeHelperFactory,
      [nameService.address],
      { kind: "uups" }
    );
    await exchangeHelper.setAddresses();
    await setDAOAddress("EXCHANGE_HELPER", exchangeHelper.address);
    console.log("deployed dao", {
      founder: founder.address,
      gd,
      identity,
      controller,
      avatar
    });

    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
    });
    goodReserve = reserve;
  });

  it("should get g$ minting permissions", async () => {
    expect(await goodReserve.dao()).to.be.equal(controller);
    expect(await goodReserve.avatar()).to.be.equal(avatar);
  });

  it("should set marketmaker in the reserve by avatar", async () => {
    await setDAOAddress("MARKET_MAKER", marketMaker.address);
    // const rFactory = await ethers.getContractFactory("GoodReserveCDai");
    // const ctrl = await ethers.getContractAt(
    //   "Controller",
    //   controller,
    //   schemeMock
    // );
    // const encodedCall = rFactory.interface.encodeFunctionData(
    //   "setMarketMaker",
    //   [marketMaker.address]
    // );
    // await ctrl.genericCall(goodReserve.address, encodedCall, avatar, 0);
    // const newMM = await goodReserve.marketMaker();
    // expect(newMM.toString()).to.be.equal(marketMaker.address);
  });

  it("should set fundManager in the reserve by avatar", async () => {
    await setDAOAddress("FUND_MANAGER", founder.address);
  });

  it("should returned fixed 0.0001 market price", async () => {
    const gdPrice = await goodReserve["currentPrice()"]();
    const cdaiWorthInGD = gdPrice.mul(BN.from("100000000"));
    const gdFloatPrice = gdPrice.toNumber() / 10 ** 8; //cdai 8 decimals
    expect(gdFloatPrice).to.be.equal(0.0001);
    expect(cdaiWorthInGD.toString()).to.be.equal("1000000000000"); //in 8 decimals precision
    expect(cdaiWorthInGD.toNumber() / 10 ** 8).to.be.equal(10000);
  });

  it("should returned market price of 1gd in DAI", async () => {
    const gdPrice = await goodReserve["currentPriceDAI()"]();
    const daiWorthInGD = gdPrice.mul(BN.from("500000")); // 1gd = 0.000002 so 1DAI = 50k gd
    const gdFloatPrice = gdPrice.toNumber() / 10 ** 18; //dai 18 decimals
    expect(gdFloatPrice).to.be.equal(0.000002);
    expect(daiWorthInGD.toString()).to.be.equal(ethers.utils.parseEther("1")); //in 18 decimals precision
    //expect(daiWorthInGD.toNumber() / 10 ** 18).to.be.equal(9899.999999999999010000);
  });

  // it("should not be able to buy gd if the minter is not the reserve", async () => {
  //   let amount = 1e8;
  //   await dai["mint(uint256)"](ethers.utils.parseEther("100"));
  //   dai.approve(cDAI.address, ethers.utils.parseEther("100"));
  //   await cDAI["mint(uint256)"](ethers.utils.parseEther("100"));
  //   await cDAI.approve(goodReserve.address, amount);

  //   const gdBalanceBefore = await goodDollar.balanceOf(founder.address);
  //   const cDAIBalanceBefore = await cDAI.balanceOf(founder.address);

  //   let tx = (await goodReserve.buy(cDAI.address, amount, 0)).wait();
  //   const gdBalanceAfter = await goodDollar.balanceOf(founder.address);
  //   const cDAIBalanceAfter = await cDAI.balanceOf(founder.address);
  //   expect(tx).to.be.revertedWith(/not minter/);
  //   expect(gdBalanceAfter.toString()).to.be.equal(gdBalanceBefore.toString());
  //   expect(cDAIBalanceAfter.toString()).to.be.equal(
  //     cDAIBalanceBefore.toString()
  //   );

  //   // //for following tests
  //   // await goodDollar.addMinter(goodReserve.address);
  // });

  it("should mint UBI correctly for 18 decimals precision and no interest", async () => {
    let reserveToken = await marketMaker.reserveTokens(cDAI.address);
    let reserveBalanceBefore = reserveToken.reserveSupply;
    let supplyBefore = reserveToken.gdSupply;
    const gdPriceBefore = await goodReserve["currentPrice()"]();

    await increaseTime(24 * 60 * 60); //required for reserve ratio advance
    const er = await cDAI.exchangeRateStored();
    const daiAmount = ethers.utils
      .parseEther("0.1")
      .mul(BN.from("10").pow(10))
      .mul(er)
      .div(BN.from("10").pow(28));

    await dai["mint(address,uint256)"](goodReserve.address, daiAmount);
    const tx = await (
      await goodReserve.mintUBI(daiAmount, 0, cDAI.address)
    ).wait();
    const gdBalanceFund = await goodDollar.balanceOf(
      await goodReserve.distributionHelper()
    );
    const gdPriceAfter = await goodReserve["currentPrice()"]();
    reserveToken = await marketMaker.reserveTokens(cDAI.address);
    let reserveBalanceAfter = reserveToken.reserveSupply;
    let supplyAfter = reserveToken.gdSupply;
    let rrAfter = reserveToken.reserveRatio;
    // expected that minted token will be added to the previous supply
    const mintEvent = tx.events.find(_ => _.event === "UBIMinted");
    console.log(
      gdPriceBefore.toNumber(),
      supplyAfter.toNumber(),
      supplyBefore.toNumber()
    );
    expect(supplyAfter).to.be.equal(
      mintEvent.args.gdInterestMinted
        .add(mintEvent.args.gdExpansionMinted)
        .add(supplyBefore)
    );
    // expected that the new reserve balance will include
    // the new 1e18 cdai which transferred
    expect(reserveBalanceAfter).to.be.equal(
      reserveBalanceBefore.add(BN.from("10").pow(17))
    );
    // the new reserve ratio should be effected from the mintExpansion by:
    // the daily change that was set up in the constructor (999388834642296)
    // requires the time advance simulation above
    expect(rrAfter.toString()).to.be.equal("999388");
    // the price should be the same
    expect(gdPriceAfter).to.be.equal(gdPriceBefore);
    expect(gdBalanceFund).to.be.equal(
      mintEvent.args.gdInterestMinted
        .add(mintEvent.args.gdExpansionMinted)
        .toString()
    );
  });

  it("should not mint UBI if the reserve is not cDAI", async () => {
    let error = await goodReserve.mintUBI(1, 0, dai.address).catch(e => e);
    expect(error.message).not.to.be.empty;
  });

  it("should not mint UBI if the caller is not the fund manager", async () => {
    let tx = goodReserve.connect(staker).mintUBI(0, 0, cDAI.address);
    await expect(tx).to.be.revertedWith(/GoodReserve: not a minter/);
  });

  it("should be able to buy gd with DAI", async () => {
    let daiAmount = ethers.utils.parseEther("100");
    const cdaiRateStored = await cDAI.exchangeRateStored();
    let amount = daiAmount
      .div(BigNumber.from(10).pow(10))
      .mul(BigNumber.from(10).pow(28))
      .div(cdaiRateStored);
    await dai["mint(uint256)"](daiAmount);
    await dai.approve(exchangeHelper.address, daiAmount);
    expect(await nameService.getAddress("DAI")).to.be.equal(dai.address);
    let reserveToken = await marketMaker.reserveTokens(cDAI.address);
    let reserveBalanceBefore = reserveToken.reserveSupply;
    let supplyBefore = reserveToken.gdSupply;
    let rrBefore = reserveToken.reserveRatio;
    const gdBalanceBefore = await goodDollar.balanceOf(founder.address);
    const daiBalanceBefore = await dai.balanceOf(founder.address);
    const cDAIBalanceReserveBefore = await cDAI.balanceOf(goodReserve.address);
    const priceBefore = await goodReserve["currentPrice()"]();

    let transaction = await (
      await exchangeHelper.buy([dai.address], daiAmount, 0, 0, NULL_ADDRESS)
    ).wait();
    reserveToken = await marketMaker.reserveTokens(cDAI.address);
    let reserveBalanceAfter = reserveToken.reserveSupply;
    let supplyAfter = reserveToken.gdSupply;
    let rrAfter = reserveToken.reserveRatio;
    const gdBalanceAfter = await goodDollar.balanceOf(founder.address);
    const daiBalanceAfter = await dai.balanceOf(founder.address);
    const cDAIBalanceReserveAfter = await cDAI.balanceOf(goodReserve.address);
    const priceAfter = await goodReserve["currentPrice()"]();
    expect(
      (cDAIBalanceReserveAfter - cDAIBalanceReserveBefore).toString()
    ).to.be.equal(amount.toString());
    expect(
      reserveBalanceAfter.sub(reserveBalanceBefore).toString()
    ).to.be.equal(amount.toString());
    expect(supplyAfter.sub(supplyBefore).toString()).to.be.equal(
      gdBalanceAfter.sub(gdBalanceBefore).toString()
    );
    expect(rrAfter.toString()).to.be.equal(rrBefore.toString());
    expect(gdBalanceAfter.gt(gdBalanceBefore)).to.be.true;
    expect(daiBalanceBefore.gt(daiBalanceAfter)).to.be.true;
    expect(priceAfter.toString()).to.be.equal(priceBefore.toString());
    expect(transaction.events.find(_ => _.event === "TokenPurchased")).to.be.not
      .empty;
  });

  it("should be able to buy gd with cDAI through buy", async () => {
    let amount = 1e8;
    await dai["mint(uint256)"](ethers.utils.parseEther("100"));
    await dai.approve(cDAI.address, ethers.utils.parseEther("100"));
    await cDAI["mint(uint256)"](ethers.utils.parseEther("100"));
    let reserveToken = await marketMaker.reserveTokens(cDAI.address);
    let reserveBalanceBefore = reserveToken.reserveSupply;
    let supplyBefore = reserveToken.gdSupply;
    let rrBefore = reserveToken.reserveRatio;
    const gdBalanceBefore = await goodDollar.balanceOf(founder.address);
    const cDAIBalanceBefore = await cDAI.balanceOf(founder.address);
    const cDAIBalanceReserveBefore = await cDAI.balanceOf(goodReserve.address);
    const priceBefore = await goodReserve["currentPrice()"]();
    await cDAI.approve(exchangeHelper.address, amount);
    let transaction = await (
      await exchangeHelper.buy([cDAI.address], amount, 0, 0, NULL_ADDRESS)
    ).wait();
    reserveToken = await marketMaker.reserveTokens(cDAI.address);
    let reserveBalanceAfter = reserveToken.reserveSupply;
    let supplyAfter = reserveToken.gdSupply;
    let rrAfter = reserveToken.reserveRatio;
    const gdBalanceAfter = await goodDollar.balanceOf(founder.address);
    const cDAIBalanceAfter = await cDAI.balanceOf(founder.address);
    const cDAIBalanceReserveAfter = await cDAI.balanceOf(goodReserve.address);
    const priceAfter = await goodReserve["currentPrice()"]();
    expect(
      (cDAIBalanceReserveAfter - cDAIBalanceReserveBefore).toString()
    ).to.be.equal(amount.toString());
    expect(
      reserveBalanceAfter.sub(reserveBalanceBefore).toString()
    ).to.be.equal(amount.toString());
    expect(supplyAfter.sub(supplyBefore).toString()).to.be.equal(
      gdBalanceAfter.sub(gdBalanceBefore).toString()
    );
    expect(rrAfter.toString()).to.be.equal(rrBefore.toString());
    expect(gdBalanceAfter.gt(gdBalanceBefore)).to.be.true;
    expect(cDAIBalanceBefore.gt(cDAIBalanceAfter)).to.be.true;
    expect(priceAfter.toString()).to.be.equal(priceBefore.toString());
    expect(transaction.events.find(_ => _.event === "TokenPurchased")).to.be.not
      .empty;
  });

  it("should be able to buy gd with cDAI", async () => {
    let amount = 1e8;
    await dai["mint(uint256)"](ethers.utils.parseEther("100"));
    await dai.approve(cDAI.address, ethers.utils.parseEther("100"));
    await cDAI["mint(uint256)"](ethers.utils.parseEther("100"));
    let reserveToken = await marketMaker.reserveTokens(cDAI.address);
    let reserveBalanceBefore = reserveToken.reserveSupply;
    let supplyBefore = reserveToken.gdSupply;
    let rrBefore = reserveToken.reserveRatio;
    const gdBalanceBefore = await goodDollar.balanceOf(founder.address);
    const cDAIBalanceBefore = await cDAI.balanceOf(founder.address);
    const cDAIBalanceReserveBefore = await cDAI.balanceOf(goodReserve.address);
    const priceBefore = await goodReserve["currentPrice()"]();
    await cDAI.approve(exchangeHelper.address, amount);
    let transaction = await (
      await exchangeHelper.buy([cDAI.address], amount, 0, 0, NULL_ADDRESS)
    ).wait();
    reserveToken = await marketMaker.reserveTokens(cDAI.address);
    let reserveBalanceAfter = reserveToken.reserveSupply;
    let supplyAfter = reserveToken.gdSupply;
    let rrAfter = reserveToken.reserveRatio;
    const gdBalanceAfter = await goodDollar.balanceOf(founder.address);
    const cDAIBalanceAfter = await cDAI.balanceOf(founder.address);
    const cDAIBalanceReserveAfter = await cDAI.balanceOf(goodReserve.address);
    const priceAfter = await goodReserve["currentPrice()"]();
    expect(
      (cDAIBalanceReserveAfter - cDAIBalanceReserveBefore).toString()
    ).to.be.equal(amount.toString());
    expect(
      reserveBalanceAfter.sub(reserveBalanceBefore).toString()
    ).to.be.equal(amount.toString());
    expect(supplyAfter.sub(supplyBefore).toString()).to.be.equal(
      gdBalanceAfter.sub(gdBalanceBefore).toString()
    );
    expect(rrAfter.toString()).to.be.equal(rrBefore.toString());
    expect(gdBalanceAfter.gt(gdBalanceBefore)).to.be.true;
    expect(cDAIBalanceBefore.gt(cDAIBalanceAfter)).to.be.true;
    expect(priceAfter.toString()).to.be.equal(priceBefore.toString());
    expect(transaction.events.find(_ => _.event === "TokenPurchased")).to.be.not
      .empty;
  });

  it("should be able to buy gd with cDAI with generic amount of cdai tokens", async () => {
    await dai["mint(uint256)"](ethers.utils.parseEther("4895"));
    await dai.approve(cDAI.address, ethers.utils.parseEther("4895"));
    let cdaibefore = await cDAI.balanceOf(founder.address);
    await cDAI["mint(uint256)"](ethers.utils.parseEther("4895"));
    let cdaiafter = await cDAI.balanceOf(founder.address);
    let amount = cdaiafter.sub(cdaibefore).toNumber();
    let reserveToken = await marketMaker.reserveTokens(cDAI.address);
    let reserveBalanceBefore = reserveToken.reserveSupply;
    let supplyBefore = reserveToken.gdSupply;
    const gdBalanceBefore = await goodDollar.balanceOf(founder.address);
    const cDAIBalanceReserveBefore = await cDAI.balanceOf(goodReserve.address);
    await cDAI.approve(exchangeHelper.address, amount);
    let transaction = await (
      await exchangeHelper.buy([cDAI.address], amount, 0, 0, NULL_ADDRESS)
    ).wait();
    reserveToken = await marketMaker.reserveTokens(cDAI.address);
    let reserveBalanceAfter = reserveToken.reserveSupply;
    let supplyAfter = reserveToken.gdSupply;
    const gdBalanceAfter = await goodDollar.balanceOf(founder.address);
    const cDAIBalanceReserveAfter = await cDAI.balanceOf(goodReserve.address);
    // actual cdai balance
    expect(
      cDAIBalanceReserveAfter.sub(cDAIBalanceReserveBefore).toString()
    ).to.be.equal(amount.toString());
    // cdai balance according to the market maker
    expect(
      reserveBalanceAfter.sub(reserveBalanceBefore).toString()
    ).to.be.equal(amount.toString());
    expect(supplyAfter.sub(supplyBefore).toString()).to.be.equal(
      gdBalanceAfter.sub(gdBalanceBefore).toString()
    );
    expect(transaction.events.find(_ => _.event === "TokenPurchased")).to.be.not
      .empty;
  });

  it("should be able to buy gd with other non initialized tokens beside cDAI", async () => {
    let amount = ethers.utils.parseEther("1");
    await dai["mint(uint256)"](amount);
    await dai.approve(exchangeHelper.address, amount);
    let tx = exchangeHelper.buy([dai.address], amount, 0, 0, NULL_ADDRESS);
    await expect(tx).to.not.reverted;
  });

  it("should not be able to buy gd without cDAI allowance", async () => {
    let amount = 1e8;
    await cDAI.approve(exchangeHelper.address, "0");
    let error = await exchangeHelper
      .buy([cDAI.address], amount, 0, 0, NULL_ADDRESS)
      .catch(e => e);
    expect(error.message).to.have.string("ERC20: insufficient allowance");
  });

  it("should not be able to buy gd without enough cDAI funds", async () => {
    let amount = 1e8;
    const cDAIBalanceBeforeTransfer = await cDAI.balanceOf(founder.address);
    await cDAI.transfer(staker.address, cDAIBalanceBeforeTransfer.toString());
    await cDAI.approve(exchangeHelper.address, amount);
    const gdBalanceBefore = await goodDollar.balanceOf(founder.address);
    const cDAIBalanceBefore = await cDAI.balanceOf(founder.address);
    let error = await exchangeHelper
      .buy([cDAI.address], amount, 0, 0, NULL_ADDRESS)
      .catch(e => e);
    const gdBalanceAfter = await goodDollar.balanceOf(founder.address);
    const cDAIBalanceAfter = await cDAI.balanceOf(founder.address);
    expect(error.message).not.to.be.empty;
    expect(gdBalanceAfter.toString()).to.be.equal(gdBalanceBefore.toString());
    expect(cDAIBalanceAfter.toString()).to.be.equal(
      cDAIBalanceBefore.toString()
    );

    await cDAI
      .connect(staker)
      .transfer(founder.address, cDAIBalanceBeforeTransfer.toString());
  });

  it("should not be able to buy gd when the minimum return is higher than the actual return", async () => {
    let amount = 1e8;
    await cDAI.approve(exchangeHelper.address, amount);
    const gdBalanceBefore = await goodDollar.balanceOf(founder.address);
    const cDAIBalanceBefore = await cDAI.balanceOf(founder.address);
    let error = await exchangeHelper
      .buy([cDAI.address], amount, 2000000, 0, NULL_ADDRESS)
      .catch(e => e);
    const gdBalanceAfter = await goodDollar.balanceOf(founder.address);
    const cDAIBalanceAfter = await cDAI.balanceOf(founder.address);
    expect(error.message).to.have.string(
      "GD return must be above the minReturn"
    );
    expect(gdBalanceAfter.toString()).to.be.equal(gdBalanceBefore.toString());
    expect(cDAIBalanceAfter.toString()).to.be.equal(
      cDAIBalanceBefore.toString()
    );
  });

  it("should be able to sell gd to cDAI without contribution", async () => {
    let amount = BN.from("10000");
    let reserveToken = await marketMaker.reserveTokens(cDAI.address);
    let reserveBalanceBefore = reserveToken.reserveSupply;
    let supplyBefore = reserveToken.gdSupply;
    let reserveRatio = reserveToken.reserveRatio;
    const gdBalanceBefore = await goodDollar.balanceOf(founder.address);
    const cDAIBalanceBefore = await cDAI.balanceOf(founder.address);
    const cDAIBalanceReserveBefore = await cDAI.balanceOf(goodReserve.address);
    await goodDollar.approve(exchangeHelper.address, amount);
    let transaction = await (
      await exchangeHelper.sell([cDAI.address], amount, 0, 0, NULL_ADDRESS)
    ).wait();
    reserveToken = await marketMaker.reserveTokens(cDAI.address);
    let reserveBalanceAfter = reserveToken.reserveSupply;
    let supplyAfter = reserveToken.gdSupply;
    const gdBalanceAfter = await goodDollar.balanceOf(founder.address);
    const cDAIBalanceAfter = await cDAI.balanceOf(founder.address);
    const cDAIBalanceReserveAfter = await cDAI.balanceOf(goodReserve.address);
    // according to the initialization settings reserve ratio is 100%. the calculation is:
    // Return = _reserveBalance * (1 - (1 - _sellAmount / _supply) ^ (1000000 / _reserveRatio))
    const bancor = await ethers.getContractAt(
      "BancorFormula",
      await marketMaker.getBancor()
    );

    const expectedReturn = await bancor.calculateSaleReturn(
      supplyBefore.toString(),
      reserveBalanceBefore.toString(),
      reserveRatio.toString(),
      amount
    );
    expect(cDAIBalanceAfter - cDAIBalanceBefore).to.be.equal(expectedReturn);
    expect(cDAIBalanceReserveBefore - cDAIBalanceReserveAfter).to.be.equal(
      expectedReturn
    );
    expect(reserveBalanceBefore.sub(reserveBalanceAfter)).to.be.equal(
      expectedReturn
    );
    // 1e4 gd sold (burn from the supply)
    expect(supplyBefore.sub(supplyAfter)).to.be.equal(amount);
    expect(gdBalanceBefore.gt(gdBalanceAfter)).to.be.true;
    expect(cDAIBalanceAfter.gt(cDAIBalanceBefore)).to.be.true;
    expect(transaction.events.find(_ => _.event === "TokenSold")).to.be.not
      .empty;
  });

  it("should be able to sell gd to cDAI without contribution through sell function", async () => {
    let amount = BN.from("10000");
    let reserveToken = await marketMaker.reserveTokens(cDAI.address);
    let reserveBalanceBefore = reserveToken.reserveSupply;
    let supplyBefore = reserveToken.gdSupply;
    let reserveRatio = reserveToken.reserveRatio;
    const gdBalanceBefore = await goodDollar.balanceOf(founder.address);
    const cDAIBalanceBefore = await cDAI.balanceOf(founder.address);
    const cDAIBalanceReserveBefore = await cDAI.balanceOf(goodReserve.address);
    await goodDollar.approve(exchangeHelper.address, amount);
    let transaction = await (
      await exchangeHelper.sell([cDAI.address], amount, 0, 0, NULL_ADDRESS)
    ).wait();
    reserveToken = await marketMaker.reserveTokens(cDAI.address);
    let reserveBalanceAfter = reserveToken.reserveSupply;
    let supplyAfter = reserveToken.gdSupply;
    const gdBalanceAfter = await goodDollar.balanceOf(founder.address);
    const cDAIBalanceAfter = await cDAI.balanceOf(founder.address);
    const cDAIBalanceReserveAfter = await cDAI.balanceOf(goodReserve.address);
    // according to the initialization settings reserve ratio is 100%. the calculation is:
    // Return = _reserveBalance * (1 - (1 - _sellAmount / _supply) ^ (1000000 / _reserveRatio))
    const bancor = await ethers.getContractAt(
      "BancorFormula",
      await marketMaker.getBancor()
    );
    const expectedReturn = await bancor.calculateSaleReturn(
      supplyBefore.toString(),
      reserveBalanceBefore.toString(),
      reserveRatio.toString(),
      amount
    );
    expect(cDAIBalanceAfter - cDAIBalanceBefore).to.be.equal(expectedReturn);
    expect(cDAIBalanceReserveBefore - cDAIBalanceReserveAfter).to.be.equal(
      expectedReturn
    );
    expect(reserveBalanceBefore.sub(reserveBalanceAfter)).to.be.equal(
      expectedReturn
    );
    // 1e4 gd sold (burn from the supply)
    expect(supplyBefore.sub(supplyAfter)).to.be.equal(amount);
    expect(gdBalanceBefore.gt(gdBalanceAfter)).to.be.true;
    expect(cDAIBalanceAfter.gt(cDAIBalanceBefore)).to.be.true;
    expect(transaction.events.find(_ => _.event === "TokenSold")).to.be.not
      .empty;
  });

  it("should be able to sell gd to DAI without contribution through sell function", async () => {
    let amount = BN.from("10000");
    let reserveToken = await marketMaker.reserveTokens(cDAI.address);
    let reserveBalanceBefore = reserveToken.reserveSupply;
    let supplyBefore = reserveToken.gdSupply;
    let reserveRatio = reserveToken.reserveRatio;
    const gdBalanceBefore = await goodDollar.balanceOf(founder.address);
    const daiBalanceBefore = await dai.balanceOf(founder.address);
    const cDAIBalanceReserveBefore = await cDAI.balanceOf(goodReserve.address);
    await goodDollar.approve(exchangeHelper.address, amount);
    let transaction = await (
      await exchangeHelper.sell([dai.address], amount, 0, 0, NULL_ADDRESS)
    ).wait();
    reserveToken = await marketMaker.reserveTokens(cDAI.address);
    let reserveBalanceAfter = reserveToken.reserveSupply;
    let supplyAfter = reserveToken.gdSupply;
    const gdBalanceAfter = await goodDollar.balanceOf(founder.address);
    const daiBalanceAfter = await dai.balanceOf(founder.address);
    const cDAIBalanceReserveAfter = await cDAI.balanceOf(goodReserve.address);
    // according to the initialization settings reserve ratio is 100%. the calculation is:
    // Return = _reserveBalance * (1 - (1 - _sellAmount / _supply) ^ (1000000 / _reserveRatio))
    const bancor = await ethers.getContractAt(
      "BancorFormula",
      await marketMaker.getBancor()
    );
    const expectedReturn = await bancor.calculateSaleReturn(
      supplyBefore.toString(),
      reserveBalanceBefore.toString(),
      reserveRatio.toString(),
      amount
    );

    //  expect(cDAIBalanceAfter - cDAIBalanceBefore).to.be.equal(expectedReturn);
    // expect(cDAIBalanceReserveBefore - cDAIBalanceReserveAfter).to.be.equal(
    //  expectedReturn
    // );
    //expect(reserveBalanceBefore.sub(reserveBalanceAfter)).to.be.equal(
    //  expectedReturn
    // );
    // 1e4 gd sold (burn from the supply)
    //expect(supplyBefore.sub(supplyAfter)).to.be.equal(amount);
    //expect(gdBalanceBefore.gt(gdBalanceAfter)).to.be.true;
    expect(daiBalanceAfter.gt(daiBalanceBefore)).to.be.true;
    expect(transaction.events.find(_ => _.event === "TokenSold")).to.be.not
      .empty;
  });

  it("should set sell contribution ratio by avatar", async () => {
    let nom = ethers.utils.parseUnits("2", 14);
    let denom = ethers.utils.parseUnits("1", 15);
    let ccFactory = await ethers.getContractFactory(
      ContributionCalculation.abi,
      ContributionCalculation.bytecode
    );
    let encodedCall = ccFactory.interface.encodeFunctionData(
      "setContributionRatio",
      [nom, denom]
    );
    const ctrl = await ethers.getContractAt(
      "Controller",
      controller,
      schemeMock
    );

    await ctrl.genericCall(contribution.address, encodedCall, avatar, 0);
    const newRatio = await contribution.sellContributionRatio();
    expect(newRatio.toString()).to.be.equal("200000000000000000000000000");
  });

  it("should not be able to set the sell contribution ratio if not avatar", async () => {
    let error = await contribution
      .setContributionRatio(2e14, 1e15)
      .catch(e => e);
    expect(error.message).to.have.string("only Avatar can call this method");
  });

  // it("should not be able to set the contribution contract address if not avatar", async () => {
  //   let error = await goodReserve
  //     .setContributionAddress(NULL_ADDRESS)
  //     .catch(e => e);
  //   expect(error.message).to.have.string("only Avatar can call this method");
  // });

  // it("should set contribution contract address by avatar", async () => {
  //   const currentAddress = await goodReserve.contribution();
  //   let encodedCall = web3.eth.abi.encodeFunctionCall(
  //     {
  //       name: "setContributionAddress",
  //       type: "function",
  //       inputs: [
  //         {
  //           type: "address",
  //           name: "_contribution"
  //         }
  //       ]
  //     },
  //     [NULL_ADDRESS]
  //   );
  // const ctrl = await ethers.getContractAt(
  //   "Controller",
  //   controller,
  //   schemeMock
  // );
  //   await ctrl.genericCall(
  //     goodReserve.address,
  //     encodedCall,
  //     avatar,
  //     0
  //   );
  //   let newAddress = await goodReserve.contribution();
  //   expect(newAddress).to.be.equal(NULL_ADDRESS);
  //   encodedCall = web3.eth.abi.encodeFunctionCall(
  //     {
  //       name: "setContributionAddress",
  //       type: "function",
  //       inputs: [
  //         {
  //           type: "address",
  //           name: "_contribution"
  //         }
  //       ]
  //     },
  //     [currentAddress]
  //   );
  // const ctrl = await ethers.getContractAt(
  //   "Controller",
  //   controller,
  //   schemeMock
  // );
  //   await ctrl.genericCall(
  //     goodReserve.address,
  //     encodedCall,
  //     avatar,
  //     0
  //   );
  //   newAddress = await goodReserve.contribution();
  //   expect(newAddress).to.be.equal(currentAddress);
  // });

  it("should calculate the sell contribution", async () => {
    let nom = ethers.utils.parseUnits("2", 14);
    let denom = ethers.utils.parseUnits("1", 15);

    let actual = await contribution.calculateContribution(
      marketMaker.address,
      goodReserve.address,
      founder.address,
      cDAI.address,
      1e4
    );
    expect(actual).to.be.equal(nom.mul(BN.from("10000")).div(denom));
  });

  it("should be able to sell gd to cDAI with contribution of 20%", async () => {
    let amount = 1e4; // 100 gd

    await goodDollar.transfer(staker.address, amount);
    // deduced amount, ie. return minus contribution. reserve ratio is 100%.
    // example without deduction:
    // 1 gd (100) equals to 0.0001 cDai (10000) so 100 gd (10k) equals to 0.01 cDai (1m)
    // since there is 20% contribution the return is 0.008 cDai (800k)
    let expectedReturn = 800000;
    let reserveToken = await marketMaker.reserveTokens(cDAI.address);
    let reserveBalanceBefore = reserveToken.reserveSupply;
    let supplyBefore = reserveToken.gdSupply;
    let rrBefore = reserveToken.reserveRatio;
    const gdBalanceBefore = await goodDollar.balanceOf(staker.address);
    const cDAIBalanceBefore = await cDAI.balanceOf(staker.address);
    const cDAIBalanceReserveBefore = await cDAI.balanceOf(goodReserve.address);
    const priceBefore = await goodReserve["currentPrice()"]();

    await goodDollar.connect(staker).approve(exchangeHelper.address, amount);
    let transaction = await (
      await exchangeHelper
        .connect(staker)
        .sell([cDAI.address], amount, 0, 0, NULL_ADDRESS)
    ).wait();
    reserveToken = await marketMaker.reserveTokens(cDAI.address);
    let reserveBalanceAfter = reserveToken.reserveSupply;
    let supplyAfter = reserveToken.gdSupply;
    let rrAfter = reserveToken.reserveRatio;
    const gdBalanceAfter = await goodDollar.balanceOf(staker.address);
    const cDAIBalanceAfter = await cDAI.balanceOf(staker.address);
    const cDAIBalanceReserveAfter = await cDAI.balanceOf(goodReserve.address);
    const priceAfter = await goodReserve["currentPrice()"]();
    expect(cDAIBalanceAfter.sub(cDAIBalanceBefore)).to.be.equal(
      expectedReturn,
      "seller return mismatch"
    );
    expect(cDAIBalanceReserveBefore.sub(cDAIBalanceReserveAfter)).to.be.equal(
      expectedReturn,
      "reserve balance mismatch"
    );
    expect(reserveBalanceBefore.sub(reserveBalanceAfter)).to.be.equal(
      expectedReturn,
      "reserve token data mismatch"
    );
    expect(supplyBefore.sub(supplyAfter)).to.be.equal(amount);
    expect(rrAfter.toString()).to.be.equal(rrBefore.toString());
    expect(gdBalanceBefore.gt(gdBalanceAfter)).to.be.true;
    expect(cDAIBalanceAfter.gt(cDAIBalanceBefore)).to.be.true;
    expect(priceAfter.toString()).to.be.equal(priceBefore.toString());
    expect(transaction.events.find(_ => _.event === "TokenSold")).to.be.not
      .empty;
  });

  it("should able to sell gd to DAI or cDAI token", async () => {
    let amount = 1e4;
    await goodDollar.approve(exchangeHelper.address, amount);

    let tx = exchangeHelper.sell([dai.address], amount, 0, 0, NULL_ADDRESS);
    await expect(tx).to.not.reverted;
  });

  it("should not be able to sell gd without gd allowance", async () => {
    let amount = 1e4;
    await goodDollar.approve(exchangeHelper.address, "0");
    const gdBalanceBefore = await goodDollar.balanceOf(founder.address);
    const cDAIBalanceBefore = await cDAI.balanceOf(founder.address);
    let error = await exchangeHelper
      .sell([cDAI.address], amount, 0, 0, NULL_ADDRESS)
      .catch(e => e);
    const gdBalanceAfter = await goodDollar.balanceOf(founder.address);
    const cDAIBalanceAfter = await cDAI.balanceOf(founder.address);
    expect(error.message).not.to.be.empty;
    expect(gdBalanceAfter.toString()).to.be.equal(gdBalanceBefore.toString());
    expect(cDAIBalanceAfter.toString()).to.be.equal(
      cDAIBalanceBefore.toString()
    );
  });

  it("should not be able to sell gd without enough gd funds", async () => {
    let amount = 1e4;
    const gdBalanceBeforeTransfer = await goodDollar.balanceOf(founder.address);
    //reset gd holdings
    await goodDollar.transfer(
      staker.address,
      gdBalanceBeforeTransfer.toString()
    );
    await goodDollar.approve(exchangeHelper.address, amount);
    const gdBalanceBefore = await goodDollar.balanceOf(founder.address);
    const cDAIBalanceBefore = await cDAI.balanceOf(founder.address);
    let error = await exchangeHelper
      .sell([cDAI.address], amount, 0, 0, NULL_ADDRESS)
      .catch(e => e);
    const gdBalanceAfter = await goodDollar.balanceOf(founder.address);
    const cDAIBalanceAfter = await cDAI.balanceOf(founder.address);
    expect(error.message).not.to.be.empty;
    expect(gdBalanceAfter.toString()).to.be.equal(gdBalanceBefore.toString());
    expect(cDAIBalanceAfter.toString()).to.be.equal(
      cDAIBalanceBefore.toString()
    );
    //restore gd holdings
    await goodDollar
      .connect(staker)
      .transfer(founder.address, gdBalanceBeforeTransfer.toString());
  });

  it("should not be able to sell gd when the minimum return is higher than the actual return", async () => {
    let amount = 1e4;
    await goodDollar.approve(exchangeHelper.address, amount);
    const gdBalanceBefore = await goodDollar.balanceOf(founder.address);
    const cDAIBalanceBefore = await cDAI.balanceOf(founder.address);
    let error = await exchangeHelper
      .sell([cDAI.address], amount, 2000000, 0, NULL_ADDRESS)
      .catch(e => e);
    const gdBalanceAfter = await goodDollar.balanceOf(founder.address);
    const cDAIBalanceAfter = await cDAI.balanceOf(founder.address);
    expect(error.message).to.have.string(
      "Token return must be above the minReturn"
    );
    expect(gdBalanceAfter.toString()).to.be.equal(gdBalanceBefore.toString());
    expect(cDAIBalanceAfter.toString()).to.be.equal(
      cDAIBalanceBefore.toString()
    );
  });

  it("should return an error if non avatar account is trying to execute recover", async () => {
    let error = await goodReserve.recover(cDAI.address).catch(e => e);
    expect(error.message).to.have.string("only avatar can call this method");
  });

  it("should transfer funds when execute recover of token which the reserve has some balance", async () => {
    await dai["mint(address,uint256)"](
      goodReserve.address,
      ethers.utils.parseEther("100")
    );

    let reserveBalance = await dai.balanceOf(goodReserve.address);
    const reserveFactory = await ethers.getContractFactory("GoodReserveCDai");

    let encodedCall = reserveFactory.interface.encodeFunctionData("recover", [
      dai.address
    ]);

    const ctrl = await ethers.getContractAt(
      "Controller",
      controller,
      schemeMock
    );
    await ctrl.genericCall(goodReserve.address, encodedCall, avatar, 0);
    let recoveredBalance = await dai.balanceOf(avatar);
    expect(recoveredBalance).to.be.equal(reserveBalance);
  });

  it("should not be able to destroy if not avatar", async () => {
    let tx = goodReserve.end();
    await expect(tx).to.revertedWith(/only avatar can call this method/);
  });

  it("should be able to buy gd with cDAI and reserve should be correct", async () => {
    let amount = 1e8;

    await dai["mint(uint256)"](ethers.utils.parseEther("100"));
    await dai.approve(cDAI.address, ethers.utils.parseEther("100"));
    await cDAI["mint(uint256)"](ethers.utils.parseEther("100"));
    let reserveToken = await marketMaker.reserveTokens(cDAI.address);
    let reserveBalanceBefore = reserveToken.reserveSupply;
    const cDAIBalanceReserveBefore = await cDAI.balanceOf(goodReserve.address);
    await cDAI.approve(exchangeHelper.address, amount);
    await exchangeHelper.buy([cDAI.address], amount, 0, 0, NULL_ADDRESS);
    reserveToken = await marketMaker.reserveTokens(cDAI.address);
    let reserveBalanceAfter = reserveToken.reserveSupply;
    const cDAIBalanceReserveAfter = await cDAI.balanceOf(goodReserve.address);
    expect(cDAIBalanceReserveAfter.sub(cDAIBalanceReserveBefore)).to.be.equal(
      amount.toString()
    );
    expect(reserveBalanceAfter.sub(reserveBalanceBefore)).to.be.equal(amount);
  });
  it("should be able to buy gd with cDAI directly through reserve", async () => {
    let amount = 1e8;
    await dai["mint(uint256)"](ethers.utils.parseEther("100"));
    await dai.approve(cDAI.address, ethers.utils.parseEther("100"));
    await cDAI["mint(uint256)"](ethers.utils.parseEther("100"));
    await cDAI.approve(goodReserve.address, ethers.utils.parseEther("10"));
    const founderBalanceBeforeBuy = await goodDollar.balanceOf(founder.address);
    await goodReserve.buy(amount, 0, founder.address);
    const founderBalanceAfterBuy = await goodDollar.balanceOf(founder.address);
    expect(founderBalanceAfterBuy).to.be.gt(founderBalanceBeforeBuy);
  });
  it("should not be able to buy gd through reserve when there is no enough allowance", async () => {
    let amount = 1e8;
    await cDAI.approve(goodReserve.address, 0);
    const tx = await goodReserve.buy(amount, 0, founder.address).catch(e => e);
    expect(tx.message).to.have.string("ERC20: insufficient allowance");
  });
  it("should be able to sell gd directly through reserve", async () => {
    const cdaiBalanceBeforeSell = await cDAI.balanceOf(founder.address);
    await goodDollar.approve(goodReserve.address, "100");
    await goodReserve.sell("100", 0, founder.address, staker.address);
    const cdaiBalanceAfterSell = await cDAI.balanceOf(founder.address);
    expect(cdaiBalanceAfterSell).to.be.gt(cdaiBalanceBeforeSell);
  });
  it("should not be able to sell gd through reserve when there is no enough allowance", async () => {
    await goodDollar.approve(goodReserve.address, "0");
    const tx = await goodReserve
      .sell("100", 0, founder.address, staker.address)
      .catch(e => e);
    expect(tx.message).to.be.not.empty;
  });
  it("seller parameter should not matter if caller is not exchange helper", async () => {
    const stakerGDBalanceBeforeSell = await goodDollar.balanceOf(
      staker.address
    );
    await goodDollar.approve(goodReserve.address, "100");
    await goodReserve.sell("100", 0, founder.address, staker.address);
    const stakerGDBalanceAfterSell = await goodDollar.balanceOf(staker.address);
    expect(stakerGDBalanceAfterSell).to.be.equal(stakerGDBalanceBeforeSell);
  });
  it("should be able to buy gd with cDAI and the total gd should be increased", async () => {
    let amount = 1e8;
    await dai["mint(uint256)"](ethers.utils.parseEther("100"));
    await dai.approve(cDAI.address, ethers.utils.parseEther("100"));
    await cDAI["mint(uint256)"](ethers.utils.parseEther("100"));
    let reserveToken = await marketMaker.reserveTokens(cDAI.address);
    let gdSupplyBefore = reserveToken.gdSupply;
    await cDAI.approve(exchangeHelper.address, amount);
    await exchangeHelper.buy([cDAI.address], amount, 0, 0, NULL_ADDRESS);
    reserveToken = await marketMaker.reserveTokens(cDAI.address);
    let gdSupplyAfter = reserveToken.gdSupply;
    expect(gdSupplyAfter.gt(gdSupplyBefore)).to.be.true;
  });

  it("should be able to retain the precision when buying a low quantity of tokens", async () => {
    let amount = 1e8;
    await dai["mint(uint256)"](ethers.utils.parseEther("100"));
    await dai.approve(cDAI.address, ethers.utils.parseEther("100"));
    await cDAI["mint(uint256)"](ethers.utils.parseEther("100"));
    let reserveToken = await marketMaker.reserveTokens(cDAI.address);
    const priceBefore = await goodReserve["currentPrice()"]();
    await cDAI.approve(exchangeHelper.address, amount);
    await exchangeHelper.buy([cDAI.address], amount, 0, 0, NULL_ADDRESS);
    reserveToken = await marketMaker.reserveTokens(cDAI.address);
    const priceAfter = await goodReserve["currentPrice()"]();
    expect(Math.floor(priceAfter.toNumber() / 100).toString()).to.be.equal(
      Math.floor(priceBefore.toNumber() / 100).toString()
    );
  });

  it("should be able to sell gd to cDAI and reserve should be correct", async () => {
    let reserveToken = await marketMaker.reserveTokens(cDAI.address);
    let reserveBalance = reserveToken.reserveSupply;
    let supply = reserveToken.gdSupply;
    let amount = BN.from("10000");
    await goodDollar.transfer(staker.address, amount);
    const cDAIBalanceBefore = await cDAI.balanceOf(staker.address);
    const cDAIBalanceReserveBefore = await cDAI.balanceOf(goodReserve.address);
    await goodDollar.connect(staker).approve(exchangeHelper.address, amount);
    const transaction = await (
      await exchangeHelper
        .connect(staker)
        .sell([cDAI.address], amount, 0, 0, NULL_ADDRESS)
    ).wait();

    const cDAIBalanceAfter = await cDAI.balanceOf(staker.address);
    const cDAIBalanceReserveAfter = await cDAI.balanceOf(goodReserve.address);

    // return = reserveBalance * (1 - (1 - sellAmount / supply) ^ (1000000 / reserveRatio))
    // if reserve ratio is 100% so:
    // return = reserve balance * (1 - (1 - sellAmount / supply))
    // the contribution ratio is 20%
    let expected =
      parseInt(reserveBalance.toString()) *
      (1 -
        (1 - amount.toNumber() / parseInt(supply.toString())) **
        (1000000 / reserveToken.reserveRatio));

    expected = Math.ceil((0.8 * expected) / 100) * 100; //deduct 20% contribution, allow 5 points precission mismatch (due to bancor pow estimation?), match solidity no floating point
    //expected = Math.floor(0.8 * expected);
    expect(cDAIBalanceAfter.sub(cDAIBalanceBefore)).to.be.equal(expected);
    expect(cDAIBalanceReserveBefore.sub(cDAIBalanceReserveAfter)).to.be.equal(
      expected
    );
  });

  it("should be able to buy gd with cDAI for some other address through buy", async () => {
    let amount = 1e8;
    await dai["mint(uint256)"](ethers.utils.parseEther("100"));
    await dai.approve(cDAI.address, ethers.utils.parseEther("100"));
    await cDAI["mint(uint256)"](ethers.utils.parseEther("100"));
    let reserveToken = await marketMaker.reserveTokens(cDAI.address);
    let reserveBalanceBefore = reserveToken.reserveSupply;
    let supplyBefore = reserveToken.gdSupply;
    let rrBefore = reserveToken.reserveRatio;
    const gdBalanceBefore = await goodDollar.balanceOf(staker.address);
    const cDAIBalanceBefore = await cDAI.balanceOf(founder.address);
    const cDAIBalanceReserveBefore = await cDAI.balanceOf(goodReserve.address);
    const priceBefore = await goodReserve["currentPrice()"]();
    await cDAI.approve(exchangeHelper.address, amount);
    let transaction = await (
      await exchangeHelper.buy([cDAI.address], amount, 0, 0, staker.address)
    ).wait();
    reserveToken = await marketMaker.reserveTokens(cDAI.address);
    let reserveBalanceAfter = reserveToken.reserveSupply;
    let supplyAfter = reserveToken.gdSupply;
    let rrAfter = reserveToken.reserveRatio;
    const gdBalanceAfter = await goodDollar.balanceOf(staker.address);
    const cDAIBalanceAfter = await cDAI.balanceOf(founder.address);
    const cDAIBalanceReserveAfter = await cDAI.balanceOf(goodReserve.address);
    const priceAfter = await goodReserve["currentPrice()"]();
    expect(
      (cDAIBalanceReserveAfter - cDAIBalanceReserveBefore).toString()
    ).to.be.equal(amount.toString());
    expect(
      reserveBalanceAfter.sub(reserveBalanceBefore).toString()
    ).to.be.equal(amount.toString());
    expect(supplyAfter.sub(supplyBefore).toString()).to.be.equal(
      gdBalanceAfter.sub(gdBalanceBefore).toString()
    );
    expect(rrAfter.toString()).to.be.equal(rrBefore.toString());
    expect(gdBalanceAfter.gt(gdBalanceBefore)).to.be.true;
    expect(cDAIBalanceBefore.gt(cDAIBalanceAfter)).to.be.true;
    expect(priceAfter.toString()).to.be.equal(priceBefore.toString());
    expect(transaction.events.find(_ => _.event === "TokenPurchased")).to.be.not
      .empty;
  });

  it("should be able to sell gd to cDAI without contribution through sell function for some other address", async () => {
    let amount = BN.from("10000");
    let reserveToken = await marketMaker.reserveTokens(cDAI.address);
    let reserveBalanceBefore = reserveToken.reserveSupply;
    let supplyBefore = reserveToken.gdSupply;
    let reserveRatio = reserveToken.reserveRatio;
    const gdBalanceBefore = await goodDollar.balanceOf(founder.address);
    const cDAIBalanceBefore = await cDAI.balanceOf(staker.address);
    const cDAIBalanceReserveBefore = await cDAI.balanceOf(goodReserve.address);
    await goodDollar.approve(exchangeHelper.address, amount);
    let transaction = await (
      await exchangeHelper.sell([cDAI.address], amount, 0, 0, staker.address)
    ).wait();
    reserveToken = await marketMaker.reserveTokens(cDAI.address);
    let reserveBalanceAfter = reserveToken.reserveSupply;
    let supplyAfter = reserveToken.gdSupply;
    const gdBalanceAfter = await goodDollar.balanceOf(founder.address);
    const cDAIBalanceAfter = await cDAI.balanceOf(staker.address);
    const cDAIBalanceReserveAfter = await cDAI.balanceOf(goodReserve.address);
    // according to the initialization settings reserve ratio is 100%. the calculation is:
    // Return = _reserveBalance * (1 - (1 - _sellAmount / _supply) ^ (1000000 / _reserveRatio))
    const bancor = await ethers.getContractAt(
      "BancorFormula",
      await marketMaker.getBancor()
    );
    const expectedReturn = await bancor.calculateSaleReturn(
      supplyBefore.toString(),
      reserveBalanceBefore.toString(),
      reserveRatio.toString(),
      amount
    );
    expect(cDAIBalanceAfter - cDAIBalanceBefore).to.be.equal(expectedReturn);
    expect(cDAIBalanceReserveBefore - cDAIBalanceReserveAfter).to.be.equal(
      expectedReturn
    );
    expect(reserveBalanceBefore.sub(reserveBalanceAfter)).to.be.equal(
      expectedReturn
    );
    // 1e4 gd sold (burn from the supply)
    expect(supplyBefore.sub(supplyAfter)).to.be.equal(amount);
    expect(gdBalanceBefore.gt(gdBalanceAfter)).to.be.true;
    expect(cDAIBalanceAfter.gt(cDAIBalanceBefore)).to.be.true;
    expect(transaction.events.find(_ => _.event === "TokenSold")).to.be.not
      .empty;
  });

  it("should be able to retain the precision when selling a low quantity of tokens", async () => {
    let amount = 1e1;
    let reserveToken = await marketMaker.reserveTokens(cDAI.address);
    const priceBefore = await goodReserve["currentPrice()"]();
    await goodDollar.approve(exchangeHelper.address, amount);
    await exchangeHelper.sell([cDAI.address], amount, 0, 0, NULL_ADDRESS);
    reserveToken = await marketMaker.reserveTokens(cDAI.address);
    const priceAfter = await goodReserve["currentPrice()"]();
    expect(Math.floor(priceAfter.toNumber() / 100).toString()).to.be.equal(
      Math.floor(priceBefore.toNumber() / 100).toString()
    );
  });

  //keep this test last as it ends the reserve
  it("should transfer cDAI funds to the given destination and transfer marker maker ownership", async () => {
    expect(await goodReserve.avatar()).to.equal(avatar);

    let avatarBalanceBefore = await cDAI.balanceOf(avatar);
    let reserveBalanceBefore = await cDAI.balanceOf(goodReserve.address);

    const reserveFactory = await ethers.getContractFactory("GoodReserveCDai");

    let encodedCall = reserveFactory.interface.encodeFunctionData("end");

    const ctrl = await ethers.getContractAt(
      "Controller",
      controller,
      schemeMock
    );

    const tx = await (
      await ctrl.genericCall(goodReserve.address, encodedCall, avatar, 0)
    ).wait();

    let avatarBalanceAfter = await cDAI.balanceOf(avatar);
    let reserveBalanceAfter = await cDAI.balanceOf(goodReserve.address);

    expect(avatarBalanceAfter.sub(avatarBalanceBefore)).to.be.equal(
      reserveBalanceBefore
    );
    expect(reserveBalanceAfter.toString()).to.be.equal("0");
    expect(await goodDollar.isMinter(goodReserve.address)).to.be.false;
    expect(await goodDollar.isMinter(avatar)).to.be.true;
  });

  it("should set reserve ratio daily expansion by avatar", async () => {
    let currentReserveRatioDailyExpansion =
      await marketMaker.reserveRatioDailyExpansion();
    console.log(currentReserveRatioDailyExpansion.toString());

    await runAsAvatarOnly(
      goodReserve,
      "setReserveRatioDailyExpansion(uint256,uint256)",
      1,
      1e15
    );

    let newReserveRatioDailyExpansion =
      await marketMaker.reserveRatioDailyExpansion();
    console.log(newReserveRatioDailyExpansion.toString());

    expect(newReserveRatioDailyExpansion).to.not.equal(
      currentReserveRatioDailyExpansion
    );
    expect(newReserveRatioDailyExpansion).to.be.equal(BN.from("1000000000000"));

    const encodedCall = goodReserve.interface.encodeFunctionData(
      "setReserveRatioDailyExpansion",
      [BN.from(currentReserveRatioDailyExpansion).div(1e12), 1e15]
    );
    const ctrl = await ethers.getContractAt(
      "Controller",
      controller,
      schemeMock
    );
    await ctrl.genericCall(goodReserve.address, encodedCall, avatar, 0);
  });
});
