diff --git a/contracts/Foundry.sol b/contracts/Foundry.sol index 5b2526e2..2e532255 100644 --- a/contracts/Foundry.sol +++ b/contracts/Foundry.sol @@ -124,7 +124,6 @@ contract Foundry is IFoundry, Ownable, Initializable { ) { meToken_ = meTokenRegistry.finishResubscribe(_meToken); } - // Calculate how many tokens tokens are returned uint256 tokensReturned = calculateBurnReturn(_meToken, _meTokensBurned); diff --git a/contracts/Hub.sol b/contracts/Hub.sol index 292fb423..d683a0b2 100644 --- a/contracts/Hub.sol +++ b/contracts/Hub.sol @@ -75,6 +75,17 @@ contract Hub is Ownable, Initializable { bytes memory _encodedCurveDetails ) external { Details.Hub storage hub_ = _hubs[_id]; + if (hub_.updating && block.timestamp > hub_.endTime) { + Details.Hub memory hubUpdated = finishUpdate(_id); + hub_.refundRatio = hubUpdated.refundRatio; + hub_.targetRefundRatio = hubUpdated.targetRefundRatio; + hub_.curve = hubUpdated.curve; + hub_.targetCurve = hubUpdated.targetCurve; + hub_.reconfigure = hubUpdated.reconfigure; + hub_.updating = hubUpdated.updating; + hub_.startTime = hubUpdated.startTime; + hub_.endTime = hubUpdated.endTime; + } require(!hub_.updating, "already updating"); require(block.timestamp >= hub_.endCooldown, "Still cooling down"); if (_targetRefundRatio != 0) { @@ -113,31 +124,6 @@ contract Hub is Ownable, Initializable { hub_.endCooldown = block.timestamp + _warmup + _duration + _cooldown; } - function finishUpdate(uint256 id) external returns (Details.Hub memory) { - Details.Hub storage hub_ = _hubs[id]; - require(block.timestamp > hub_.endTime, "Still updating"); - - if (hub_.targetRefundRatio != 0) { - hub_.refundRatio = hub_.targetRefundRatio; - hub_.targetRefundRatio = 0; - } - - if (hub_.reconfigure) { - if (hub_.targetCurve == address(0)) { - ICurve(hub_.curve).finishReconfigure(id); - } else { - hub_.curve = hub_.targetCurve; - hub_.targetCurve = address(0); - } - hub_.reconfigure = false; - } - - hub_.updating = false; - hub_.startTime = 0; - hub_.endTime = 0; - return hub_; - } - function setWarmup(uint256 warmup_) external onlyOwner { require(warmup_ != _warmup, "warmup_ == _warmup"); _warmup = warmup_; @@ -176,4 +162,29 @@ contract Hub is Ownable, Initializable { function getCooldown() external view returns (uint256) { return _cooldown; } + + function finishUpdate(uint256 id) public returns (Details.Hub memory) { + Details.Hub storage hub_ = _hubs[id]; + require(block.timestamp > hub_.endTime, "Still updating"); + + if (hub_.targetRefundRatio != 0) { + hub_.refundRatio = hub_.targetRefundRatio; + hub_.targetRefundRatio = 0; + } + + if (hub_.reconfigure) { + if (hub_.targetCurve == address(0)) { + ICurve(hub_.curve).finishReconfigure(id); + } else { + hub_.curve = hub_.targetCurve; + hub_.targetCurve = address(0); + } + hub_.reconfigure = false; + } + + hub_.updating = false; + hub_.startTime = 0; + hub_.endTime = 0; + return hub_; + } } diff --git a/test/contracts/libs/WeightedAverage.ts b/test/contracts/libs/WeightedAverage.ts new file mode 100644 index 00000000..1f256544 --- /dev/null +++ b/test/contracts/libs/WeightedAverage.ts @@ -0,0 +1,125 @@ +import { BigNumber } from "@ethersproject/bignumber"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { WeightedAverage } from "../../../artifacts/types/WeightedAverage"; +import { deploy, weightedAverageSimulation } from "../../utils/helpers"; + +describe("WeightedAverage.sol", () => { + let wa: WeightedAverage; + let account0: SignerWithAddress; + let account1: SignerWithAddress; + before(async () => { + [account0, account1] = await ethers.getSigners(); + wa = await deploy("WeightedAverage"); + + await wa.deployed(); + }); + + describe("calculate()", () => { + it("Returns amount if block.timestamp < startTime", async () => { + const block = await ethers.provider.getBlock("latest"); + + const cur = await wa.calculate(42, 245646, block.timestamp + 1, 1); + expect(cur).to.equal(42); + }); + it("Returns targetAmount if block.timestamp > endTime", async () => { + const block = await ethers.provider.getBlock("latest"); + const cur = await wa.calculate( + 42, + 245646, + block.timestamp - 2, + block.timestamp - 1 + ); + expect(cur).to.equal(245646); + }); + it("works with different amount and target amount", async () => { + const block = await ethers.provider.getBlock("latest"); + const amount = "100"; + const targetAmount = "1000"; + const startTime = block.timestamp - 50; + const endTime = block.timestamp + 50; + const cur = await wa.calculate( + ethers.utils.parseEther(amount), + ethers.utils.parseEther(targetAmount), + startTime, + endTime + ); + const calcRes = weightedAverageSimulation( + Number(amount), + Number(targetAmount), + startTime, + endTime, + block.timestamp + ); + expect(calcRes).to.equal(550); + expect(Number(ethers.utils.formatEther(cur))).to.equal(calcRes); + }); + it("works at the begining of migration", async () => { + const block = await ethers.provider.getBlock("latest"); + const amount = "0"; + const targetAmount = "10"; + const startTime = block.timestamp - 1; + const endTime = block.timestamp + 9; + const cur = await wa.calculate( + ethers.utils.parseEther(amount), + ethers.utils.parseEther(targetAmount), + startTime, + endTime + ); + const calcRes = weightedAverageSimulation( + Number(amount), + Number(targetAmount), + startTime, + endTime, + block.timestamp + ); + expect(calcRes).to.equal(1); + expect(Number(ethers.utils.formatEther(cur))).to.equal(calcRes); + }); + it("works in the middle of migration", async () => { + const block = await ethers.provider.getBlock("latest"); + const amount = "0"; + const targetAmount = "10"; + const startTime = block.timestamp - 5; + const endTime = block.timestamp + 5; + const cur = await wa.calculate( + ethers.utils.parseEther(amount), + ethers.utils.parseEther(targetAmount), + startTime, + endTime + ); + const calcRes = weightedAverageSimulation( + Number(amount), + Number(targetAmount), + startTime, + endTime, + block.timestamp + ); + expect(calcRes).to.equal(5); + expect(Number(ethers.utils.formatEther(cur))).to.equal(calcRes); + }); + it("works at the end of migration", async () => { + const block = await ethers.provider.getBlock("latest"); + const amount = "0"; + const targetAmount = "10"; + const startTime = block.timestamp - 9; + const endTime = block.timestamp + 1; + const cur = await wa.calculate( + ethers.utils.parseEther(amount), + ethers.utils.parseEther(targetAmount), + startTime, + endTime + ); + const calcRes = weightedAverageSimulation( + Number(amount), + Number(targetAmount), + startTime, + endTime, + block.timestamp + ); + expect(calcRes).to.equal(9); + expect(Number(ethers.utils.formatEther(cur))).to.equal(calcRes); + }); + }); +}); diff --git a/test/contracts/migrations/UniswapSingleTransfer.ts b/test/contracts/migrations/UniswapSingleTransfer.ts index 50c2f06b..5ef3231c 100644 --- a/test/contracts/migrations/UniswapSingleTransfer.ts +++ b/test/contracts/migrations/UniswapSingleTransfer.ts @@ -12,7 +12,7 @@ import { CurveRegistry } from "../../../artifacts/types/CurveRegistry"; import { MigrationRegistry } from "../../../artifacts/types/MigrationRegistry"; import { SingleAssetVault } from "../../../artifacts/types/SingleAssetVault"; import { MeToken } from "../../../artifacts/types/MeToken"; -import { impersonate, mineBlock, passOneHour } from "../../utils/hardhatNode"; +import { impersonate, mineBlock, passHours } from "../../utils/hardhatNode"; import { UniswapSingleTransfer } from "../../../artifacts/types/UniswapSingleTransfer"; import { hubSetup } from "../../utils/hubSetup"; import { expect } from "chai"; diff --git a/test/integration/Hub/UpdateRefundRatio.ts b/test/integration/Hub/UpdateRefundRatio.ts index 77804a9d..6cdef031 100644 --- a/test/integration/Hub/UpdateRefundRatio.ts +++ b/test/integration/Hub/UpdateRefundRatio.ts @@ -1,5 +1,10 @@ import { ethers, getNamedAccounts } from "hardhat"; -import { deploy, getContractAt } from "../../utils/helpers"; +import { + deploy, + getContractAt, + toETHNum, + weightedAverageSimulation, +} from "../../utils/helpers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; import { BigNumber, Signer } from "ethers"; import { ERC20 } from "../../../artifacts/types/ERC20"; @@ -15,8 +20,9 @@ import { VaultRegistry } from "../../../artifacts/types/VaultRegistry"; import { expect } from "chai"; import { SingleAssetVault } from "../../../artifacts/types/SingleAssetVault"; import { UniswapSingleTransfer } from "../../../artifacts/types/UniswapSingleTransfer"; -import { passOneDay } from "../../utils/hardhatNode"; +import { passDays, passHours, passSeconds } from "../../utils/hardhatNode"; import { beforeEach } from "mocha"; +import { BlockList } from "net"; describe("Hub - update RefundRatio", () => { let meTokenRegistry: MeTokenRegistry; @@ -37,9 +43,10 @@ describe("Hub - update RefundRatio", () => { let baseY: BigNumber; const MAX_WEIGHT = 1000000; let encodedCurveDetails: string; + let encodedVaultArgs: string; const firstHubId = 1; const firstRefundRatio = 5000; - const targetRefundRatio = 500000; // 50% + const targetedRefundRatio = 500000; // 50% before(async () => { // TODO: pre-load contracts // NOTE: hub.register() should have already been called @@ -52,13 +59,9 @@ describe("Hub - update RefundRatio", () => { ["uint256", "uint32"], [baseY, reserveWeight] ); - const encodedVaultArgs = ethers.utils.defaultAbiCoder.encode( - ["address"], - [DAI] - ); + encodedVaultArgs = ethers.utils.defaultAbiCoder.encode(["address"], [DAI]); bancorZeroCurve = await deploy("BancorZeroCurve"); - console.log(`before hubsetup`); ({ token, hub, @@ -78,7 +81,6 @@ describe("Hub - update RefundRatio", () => { firstRefundRatio, bancorZeroCurve )); - console.log(`after hubsetup`); // Deploy uniswap migration and approve it to the registry const migration = await deploy( @@ -104,62 +106,61 @@ describe("Hub - update RefundRatio", () => { const name = "Carl0 meToken"; const symbol = "CARL"; - console.log(`before subscribe`); const tx = await meTokenRegistry .connect(account0) .subscribe(name, "CARL", firstHubId, 0); - console.log(`after subscribe`); const meTokenAddr = await meTokenRegistry.getOwnerMeToken(account0.address); - console.log(`meTokenAddr :${meTokenAddr}`); meToken = await getContractAt("MeToken", meTokenAddr); // Register Hub2 w/ same args but different refund Ratio const tokenDeposited = ethers.utils.parseEther("100"); await token.connect(account2).approve(foundry.address, tokenDeposited); - console.log(`account2 :${account2.address}`); const balBefore = await meToken.balanceOf(account2.address); - console.log(`meTokssss`); - console.log(`balBefore :${ethers.utils.formatEther(balBefore)}`); const vaultBalBefore = await token.balanceOf(singleAssetVault.address); - console.log(`vaultBalBefore :${ethers.utils.formatEther(vaultBalBefore)}`); await foundry .connect(account2) .mint(meTokenAddr, tokenDeposited, account2.address); const balAfter = await meToken.balanceOf(account2.address); const vaultBalAfter = await token.balanceOf(singleAssetVault.address); - console.log(`vaultBalAfter :${ethers.utils.formatEther(vaultBalAfter)}`); expect(vaultBalAfter.sub(vaultBalBefore)).to.equal(tokenDeposited); - console.log(`balAfter :${ethers.utils.formatEther(balAfter)}`); - // Initialize Hub1 update to Hub2 param + //setWarmup for 2 days + let warmup = await hub.getWarmup(); + expect(warmup).to.equal(0); + await hub.setWarmup(172800); + + warmup = await hub.getWarmup(); + expect(warmup).to.equal(172800); + let cooldown = await hub.getCooldown(); + expect(cooldown).to.equal(0); + //setCooldown for 1 day + await hub.setCooldown(86400); + cooldown = await hub.getCooldown(); + expect(cooldown).to.equal(86400); + + let duration = await hub.getDuration(); + expect(duration).to.equal(0); + //setDuration for 1 week + await hub.setDuration(604800); + duration = await hub.getDuration(); + expect(duration).to.equal(604800); }); describe("During warmup", () => { before(async () => { - //setWarmup for 2 days - let warmup = await hub.getWarmup(); - expect(warmup).to.equal(0); - await hub.setWarmup(172800); - - warmup = await hub.getWarmup(); - expect(warmup).to.equal(172800); await hub.initUpdate( firstHubId, bancorZeroCurve.address, - targetRefundRatio, + targetedRefundRatio, encodedCurveDetails ); }); it("initUpdate() cannot be called", async () => { // TODO: fast fwd a little bit let lastBlock = await ethers.provider.getBlock("latest"); - console.log(`lastBlock.timestamp:${lastBlock.timestamp}`); - - await passOneDay(); + await passDays(1); lastBlock = await ethers.provider.getBlock("latest"); - console.log(`lastBlock.timestamp:${lastBlock.timestamp}`); //await hub.setWarmup(172801); lastBlock = await ethers.provider.getBlock("latest"); - console.log(`lastBlock.timestamp:${lastBlock.timestamp}`); await expect( hub.initUpdate(1, bancorZeroCurve.address, 1000, encodedCurveDetails) ).to.be.revertedWith("already updating"); @@ -172,32 +173,19 @@ describe("Hub - update RefundRatio", () => { ); await token.connect(account2).approve(foundry.address, tokenDeposited); - - console.log(`account2 :${account2.address}`); const balBefore = await meToken.balanceOf(account0.address); const balDaiBefore = await token.balanceOf(account0.address); - console.log(`meTokssss`); - console.log(`balBefore :${ethers.utils.formatEther(balBefore)}`); - console.log(`balDaiBefore :${ethers.utils.formatEther(balDaiBefore)}`); const vaultBalBefore = await token.balanceOf(singleAssetVault.address); - console.log( - `**vault**BalBefore :${ethers.utils.formatEther(vaultBalBefore)}` - ); + await foundry .connect(account2) .mint(meToken.address, tokenDeposited, account0.address); const balAfter = await meToken.balanceOf(account0.address); - console.log(`balAfter :${ethers.utils.formatEther(balAfter)}`); const vaultBalAfter = await token.balanceOf(singleAssetVault.address); - console.log( - `**vault**BalAfter :${ethers.utils.formatEther(vaultBalAfter)}` - ); + expect(vaultBalAfter.sub(vaultBalBefore)).to.equal(tokenDeposited); const balDaiAcc1Before = await token.balanceOf(account1.address); - console.log( - `balDaiAcc1Before :${ethers.utils.formatEther(balDaiAcc1Before)}` - ); //send half burnt by owner @@ -205,34 +193,25 @@ describe("Hub - update RefundRatio", () => { .connect(account0) .burn(meToken.address, balAfter, account0.address); const balDaiAfter = await token.balanceOf(account0.address); - console.log(`balDaiAfter :${ethers.utils.formatEther(balDaiAfter)}`); const vaultBalAfterBurn = await token.balanceOf(singleAssetVault.address); - console.log( - `**vault**BalAfterBurn :${ethers.utils.formatEther(vaultBalAfterBurn)}` - ); + // we have less DAI in the vault cos they have been sent to the burner expect(vaultBalAfter.sub(vaultBalAfterBurn)).to.equal( balDaiAfter.sub(balDaiBefore) ); // buyer const balAcc1Before = await meToken.balanceOf(account1.address); - console.log(`balAcc1Before :${ethers.utils.formatEther(balAcc1Before)}`); await token.connect(account1).approve(foundry.address, tokenDeposited); await foundry .connect(account1) .mint(meToken.address, tokenDeposited, account1.address); const vaultBalAfterMint = await token.balanceOf(singleAssetVault.address); - console.log( - `**vault**BalAfterMint :${ethers.utils.formatEther(vaultBalAfterMint)}` - ); + expect(vaultBalAfterMint.sub(vaultBalAfterBurn)).to.equal(tokenDeposited); const balDaiAcc1AfterMint = await token.balanceOf(account1.address); - console.log( - `balDaiAcc1AfterMint :${ethers.utils.formatEther(balDaiAcc1AfterMint)}` - ); + const balAcc1After = await meToken.balanceOf(account1.address); - console.log(`balAcc1After :${ethers.utils.formatEther(balAcc1After)}`); expect(balAcc1After.sub(balAcc1Before)).to.equal( balAfter.sub(balBefore).sub(ethers.utils.parseUnits("1", "wei")) ); @@ -241,17 +220,10 @@ describe("Hub - update RefundRatio", () => { .connect(account1) .burn(meToken.address, balAcc1After, account1.address); const balDaiAcc1After = await token.balanceOf(account1.address); - console.log( - `balDaiAcc1After :${ethers.utils.formatEther(balDaiAcc1After)}` - ); + const vaultBalAfterBuyerBurn = await token.balanceOf( singleAssetVault.address ); - console.log( - `**vault**BalAfterBuyerBurn :${ethers.utils.formatEther( - vaultBalAfterBuyerBurn - )}` - ); // we have less DAI in the vault cos they have been sent to the burner expect(vaultBalAfterMint.sub(vaultBalAfterBuyerBurn)).to.equal( balDaiAcc1After.sub(balDaiAcc1Before.sub(tokenDeposited)) @@ -268,7 +240,7 @@ describe("Hub - update RefundRatio", () => { describe("During duration", () => { before(async () => { - await passOneDay(); + await passHours(1); }); it("initUpdate() cannot be called", async () => { // TODO: fast to active duration @@ -277,8 +249,9 @@ describe("Hub - update RefundRatio", () => { ).to.be.revertedWith("already updating"); }); - it("Assets received for owner based on weighted average", async () => { - // TODO: calculate weighted refundRatio based on current time relative to duration + it("Assets received for owner are not based on weighted average refund ratio only applies to buyer", async () => { + //move forward 2 Days + await passDays(2); const tokenDepositedInETH = 100; const tokenDeposited = ethers.utils.parseEther( tokenDepositedInETH.toString() @@ -286,68 +259,408 @@ describe("Hub - update RefundRatio", () => { await token.connect(account2).approve(foundry.address, tokenDeposited); - console.log(`account2 :${account2.address}`); const balBefore = await meToken.balanceOf(account0.address); const balDaiBefore = await token.balanceOf(account0.address); - console.log(`meTokssss`); - console.log(`balBefore :${ethers.utils.formatEther(balBefore)}`); - console.log(`balDaiBefore :${ethers.utils.formatEther(balDaiBefore)}`); const vaultBalBefore = await token.balanceOf(singleAssetVault.address); - console.log( - `**vault**BalBefore :${ethers.utils.formatEther(vaultBalBefore)}` - ); + // send token to owner await foundry .connect(account2) .mint(meToken.address, tokenDeposited, account0.address); const balAfter = await meToken.balanceOf(account0.address); - console.log(`balAfter :${ethers.utils.formatEther(balAfter)}`); const vaultBalAfterMint = await token.balanceOf(singleAssetVault.address); - console.log( - `**vault**BalAfterMint :${ethers.utils.formatEther(vaultBalAfterMint)}` - ); + expect(vaultBalAfterMint.sub(vaultBalBefore)).to.equal(tokenDeposited); // burnt by owner await meToken.connect(account0).approve(foundry.address, balAfter); - const allowance = await token.allowance( - singleAssetVault.address, - foundry.address + + const meTotSupply = await meToken.totalSupply(); + + const meDetails = await meTokenRegistry.getDetails(meToken.address); + + const tokensReturned = await foundry.calculateBurnReturn( + meToken.address, + balAfter ); - console.log(`//allowance :${ethers.utils.formatEther(allowance)}`); + + const rewardFromLockedPool = one + .mul(balAfter) + .mul(meDetails.balanceLocked) + .div(meTotSupply) + .div(one); + await foundry .connect(account0) .burn(meToken.address, balAfter, account0.address); + const balAfterBurn = await meToken.balanceOf(account0.address); + expect(balBefore).to.equal(balAfterBurn); const balDaiAfter = await token.balanceOf(account0.address); - console.log(`balDaiAfter :${ethers.utils.formatEther(balDaiAfter)}`); - /* expect( - Number( - ethers.utils.formatEther( - tokenDeposited.sub(balDaiBefore.sub(balDaiAfter)) - ) - ) - ).to.equal((tokenDepositedInETH * firstRefundRatio) / MAX_WEIGHT); */ + const { active, updating } = await hub.getDetails(1); + expect(active).to.be.true; + expect(updating).to.be.true; + + expect(toETHNum(balDaiAfter.sub(balDaiBefore))).to.equal( + toETHNum(tokensReturned.add(rewardFromLockedPool)) + ); }); - it("Assets received for buyer based on weighted average", async () => {}); + it("Assets received for buyer based on weighted average", async () => { + //move forward 3 Days + await passDays(3); + // TODO: calculate weighted refundRatio based on current time relative to duration + const tokenDepositedInETH = 100; + const tokenDeposited = ethers.utils.parseEther( + tokenDepositedInETH.toString() + ); + await token.connect(account2).approve(foundry.address, tokenDeposited); + const vaultBalBefore = await token.balanceOf(singleAssetVault.address); + + // send token to owner + await foundry + .connect(account2) + .mint(meToken.address, tokenDeposited, account2.address); + const balDaiAfterMint = await token.balanceOf(account2.address); + const balAfter = await meToken.balanceOf(account2.address); + + const vaultBalAfterMint = await token.balanceOf(singleAssetVault.address); + expect(vaultBalAfterMint.sub(vaultBalBefore)).to.equal(tokenDeposited); + // burnt by owner + await meToken.connect(account2).approve(foundry.address, balAfter); + + const tokensReturned = await foundry.calculateBurnReturn( + meToken.address, + balAfter + ); + await foundry + .connect(account2) + .burn(meToken.address, balAfter, account2.address); + const balDaiAfterBurn = await token.balanceOf(account2.address); + + const { + active, + refundRatio, + updating, + startTime, + endTime, + endCooldown, + reconfigure, + targetRefundRatio, + } = await hub.getDetails(1); + expect(active).to.be.true; + expect(updating).to.be.true; + const block = await ethers.provider.getBlock("latest"); + + const calcWAvrgRes = weightedAverageSimulation( + refundRatio.toNumber(), + targetRefundRatio.toNumber(), + startTime.toNumber(), + endTime.toNumber(), + block.timestamp + ); + const calculatedReturn = tokensReturned + .mul(BigNumber.from(Math.floor(calcWAvrgRes))) + .div(BigNumber.from(10 ** 6)); + + // we get the calcWAvrgRes percentage of the tokens returned by the Metokens burn + expect(balDaiAfterBurn.sub(balDaiAfterMint)).to.equal(calculatedReturn); + }); }); describe("During cooldown", () => { it("initUpdate() cannot be called", async () => { - // TODO: fast fwd to active cooldown + const { + active, + refundRatio, + updating, + startTime, + endTime, + endCooldown, + reconfigure, + targetRefundRatio, + } = await hub.getDetails(1); + expect(active).to.be.true; + expect(updating).to.be.true; + const block = await ethers.provider.getBlock("latest"); + + //Block.timestamp should be between endtime and endCooldown + // move forward to cooldown + await passSeconds(endTime.sub(block.timestamp).toNumber() + 1); + await expect( + hub.initUpdate(1, bancorZeroCurve.address, 1000, encodedCurveDetails) + ).to.be.revertedWith("Still cooling down"); + }); + + it("Before refundRatio set, burn() for owner should not use the targetRefundRatio", async () => { + const tokenDepositedInETH = 100; + const tokenDeposited = ethers.utils.parseEther( + tokenDepositedInETH.toString() + ); + + await token.connect(account2).approve(foundry.address, tokenDeposited); + + const balBefore = await meToken.balanceOf(account0.address); + const balDaiBefore = await token.balanceOf(account0.address); + const vaultBalBefore = await token.balanceOf(singleAssetVault.address); + + // send token to owner + await foundry + .connect(account2) + .mint(meToken.address, tokenDeposited, account0.address); + const { + active, + updating, + refundRatio, + startTime, + endTime, + endCooldown, + reconfigure, + targetRefundRatio, + } = await hub.getDetails(1); + // update has been finished by calling mint function as we passed the end time + expect(targetRefundRatio).to.equal(0); + expect(refundRatio).to.equal(targetedRefundRatio); + const balAfter = await meToken.balanceOf(account0.address); + const vaultBalAfterMint = await token.balanceOf(singleAssetVault.address); + + expect(vaultBalAfterMint.sub(vaultBalBefore)).to.equal(tokenDeposited); + // burnt by owner + await meToken.connect(account0).approve(foundry.address, balAfter); + + const meTotSupply = await meToken.totalSupply(); + + const meDetails = await meTokenRegistry.getDetails(meToken.address); + + const tokensReturned = await foundry.calculateBurnReturn( + meToken.address, + balAfter + ); + + const rewardFromLockedPool = one + .mul(balAfter) + .mul(meDetails.balanceLocked) + .div(meTotSupply) + .div(one); + + await foundry + .connect(account0) + .burn(meToken.address, balAfter, account0.address); + const balAfterBurn = await meToken.balanceOf(account0.address); + expect(balBefore).to.equal(balAfterBurn); + const balDaiAfter = await token.balanceOf(account0.address); + //Block.timestamp should be between endtime and endCooldown + const block = await ethers.provider.getBlock("latest"); + expect(block.timestamp).to.be.gt(endTime); + expect(block.timestamp).to.be.lt(endCooldown); + + expect(active).to.be.true; + expect(updating).to.be.false; + + expect(toETHNum(balDaiAfter.sub(balDaiBefore))).to.equal( + toETHNum(tokensReturned.add(rewardFromLockedPool)) + ); + }); + + it("Before refundRatio set, burn() for buyers should use the targetRefundRatio", async () => { + await passHours(4); + // TODO: calculate weighted refundRatio based on current time relative to duration + const tokenDepositedInETH = 100; + const tokenDeposited = ethers.utils.parseEther( + tokenDepositedInETH.toString() + ); + + await token.connect(account2).approve(foundry.address, tokenDeposited); + const vaultBalBefore = await token.balanceOf(singleAssetVault.address); + // send token to owner + await foundry + .connect(account2) + .mint(meToken.address, tokenDeposited, account2.address); + const balDaiAfterMint = await token.balanceOf(account2.address); + const balAfter = await meToken.balanceOf(account2.address); + const vaultBalAfterMint = await token.balanceOf(singleAssetVault.address); + + expect(vaultBalAfterMint.sub(vaultBalBefore)).to.equal(tokenDeposited); + // burnt by owner + await meToken.connect(account2).approve(foundry.address, balAfter); + + const tokensReturned = await foundry.calculateBurnReturn( + meToken.address, + balAfter + ); + await foundry + .connect(account2) + .burn(meToken.address, balAfter, account2.address); + const balDaiAfterBurn = await token.balanceOf(account2.address); + + const { + active, + refundRatio, + updating, + startTime, + endTime, + endCooldown, + reconfigure, + targetRefundRatio, + } = await hub.getDetails(1); + + expect(active).to.be.true; + expect(updating).to.be.false; + expect(targetRefundRatio).to.equal(0); + expect(refundRatio).to.equal(targetedRefundRatio); + //Block.timestamp should be between endtime and endCooldown + const block = await ethers.provider.getBlock("latest"); + + expect(block.timestamp).to.be.gt(endTime); + expect(block.timestamp).to.be.lt(endCooldown); + const calculatedReturn = tokensReturned + .mul(BigNumber.from(targetedRefundRatio)) + .div(BigNumber.from(10 ** 6)); + + // we get the calcWAvrgRes percentage of the tokens returned by the Metokens burn + expect(balDaiAfterBurn.sub(balDaiAfterMint)).to.equal(calculatedReturn); }); - it("Before refundRatio set, burn() should use the targetRefundRatio", async () => {}); + it("Call finishUpdate() and update refundRatio to targetRefundRatio", async () => { + await hub.register( + token.address, + singleAssetVault.address, + bancorZeroCurve.address, + targetedRefundRatio / 2, //refund ratio + encodedCurveDetails, + encodedVaultArgs + ); + const hubId = (await hub.count()).toNumber(); + expect(hubId).to.be.equal(firstHubId + 1); + await hub.setWarmup(0); + await hub.setCooldown(0); + await hub.setDuration(0); + + let warmup = await hub.getWarmup(); + expect(warmup).to.equal(0); + + let cooldown = await hub.getCooldown(); + expect(cooldown).to.equal(0); + + let duration = await hub.getDuration(); + expect(duration).to.equal(0); + const detBefore = await hub.getDetails(hubId); + + expect(detBefore.active).to.be.true; + expect(detBefore.updating).to.be.false; + expect(detBefore.targetRefundRatio).to.equal(0); + await hub.initUpdate( + hubId, + bancorZeroCurve.address, + targetedRefundRatio, + encodedCurveDetails + ); + const detAfterInit = await hub.getDetails(hubId); + + expect(detAfterInit.active).to.be.true; + expect(detAfterInit.updating).to.be.true; + expect(detAfterInit.refundRatio).to.equal(targetedRefundRatio / 2); + expect(detAfterInit.targetRefundRatio).to.equal(targetedRefundRatio); - it("Call finishUpdate() and update refundRatio to targetRefundRatio", async () => {}); + await hub.finishUpdate(hubId); + const detAfterUpdate = await hub.getDetails(hubId); + + expect(detAfterUpdate.active).to.be.true; + expect(detAfterUpdate.updating).to.be.false; + expect(detAfterUpdate.refundRatio).to.equal(targetedRefundRatio); + expect(detAfterUpdate.targetRefundRatio).to.equal(0); + }); }); describe("After cooldown", () => { it("initUpdate() can be called again", async () => { // TODO: fast fwd to after cooldown + const { + active, + refundRatio, + updating, + startTime, + endTime, + endCooldown, + reconfigure, + targetRefundRatio, + } = await hub.getDetails(1); + + expect(active).to.be.true; + expect(updating).to.be.false; + expect(targetRefundRatio).to.equal(0); + expect(refundRatio).to.equal(targetedRefundRatio); + //Block.timestamp should be between endtime and endCooldown + const block = await ethers.provider.getBlock("latest"); + expect(block.timestamp).to.be.gt(endTime); + expect(block.timestamp).to.be.lt(endCooldown); + + await passSeconds(endCooldown.sub(block.timestamp).toNumber() + 1); + await hub.initUpdate( + 1, + bancorZeroCurve.address, + 1000, + encodedCurveDetails + ); + + const detAfterInit = await hub.getDetails(1); + expect(detAfterInit.active).to.be.true; + expect(detAfterInit.updating).to.be.true; + expect(detAfterInit.refundRatio).to.equal(targetedRefundRatio); + expect(detAfterInit.targetRefundRatio).to.equal(1000); }); - it("If no burns during cooldown, initUpdate() first calls finishUpdate()", async () => {}); + it("If no burns during cooldown, initUpdate() first calls finishUpdate()", async () => { + await hub.register( + token.address, + singleAssetVault.address, + bancorZeroCurve.address, + targetedRefundRatio / 2, //refund ratio + encodedCurveDetails, + encodedVaultArgs + ); + const hubId = (await hub.count()).toNumber(); + expect(hubId).to.be.equal(firstHubId + 2); + + let warmup = await hub.getWarmup(); + expect(warmup).to.equal(0); + + let cooldown = await hub.getCooldown(); + expect(cooldown).to.equal(0); + + let duration = await hub.getDuration(); + expect(duration).to.equal(0); + const detBefore = await hub.getDetails(hubId); + expect(detBefore.active).to.be.true; + expect(detBefore.updating).to.be.false; + expect(detBefore.targetRefundRatio).to.equal(0); + await hub.initUpdate( + hubId, + bancorZeroCurve.address, + targetedRefundRatio, + encodedCurveDetails + ); + const detAfterInit = await hub.getDetails(hubId); + + expect(detAfterInit.active).to.be.true; + expect(detAfterInit.updating).to.be.true; + expect(detAfterInit.refundRatio).to.equal(targetedRefundRatio / 2); + expect(detAfterInit.targetRefundRatio).to.equal(targetedRefundRatio); + + const block = await ethers.provider.getBlock("latest"); + expect(detAfterInit.endCooldown.sub(block.timestamp)).to.equal(0); + await hub.initUpdate( + hubId, + bancorZeroCurve.address, + 1000, + encodedCurveDetails + ); + + const detAfterUpdate = await hub.getDetails(hubId); + expect(detAfterUpdate.active).to.be.true; + expect(detAfterUpdate.updating).to.be.true; + expect(detAfterUpdate.refundRatio).to.equal(targetedRefundRatio); + expect(detAfterUpdate.targetRefundRatio).to.equal(1000); + }); it("If no burns during cooldown, initUpdate() args are compared to new vals set from on finishUpdate()", async () => {}); }); diff --git a/test/utils/hardhatNode.ts b/test/utils/hardhatNode.ts index 54494f30..0b5e31ae 100644 --- a/test/utils/hardhatNode.ts +++ b/test/utils/hardhatNode.ts @@ -2,24 +2,31 @@ import { ContractTransaction, Signer } from "ethers"; import { network, getNamedAccounts, ethers } from "hardhat"; import { TransactionReceipt } from "@ethersproject/abstract-provider"; -export async function passOneHour(): Promise { +export async function passSeconds(sec: Number): Promise { await network.provider.request({ method: "evm_increaseTime", - params: [3600], + params: [sec], }); } -export async function passOneDay(): Promise { +export async function passHours(amount: number): Promise { await network.provider.request({ method: "evm_increaseTime", - params: [86400], + params: [3600 * amount], }); } -export async function passOneWeek(): Promise { +export async function passDays(amount: number): Promise { await network.provider.request({ method: "evm_increaseTime", - params: [604800], + params: [86400 * amount], + }); +} + +export async function passWeeks(amount: number): Promise { + await network.provider.request({ + method: "evm_increaseTime", + params: [604800 * amount], }); } diff --git a/test/utils/helpers.ts b/test/utils/helpers.ts index 6d75bb9b..d92bb564 100644 --- a/test/utils/helpers.ts +++ b/test/utils/helpers.ts @@ -1,3 +1,4 @@ +import { BigNumber } from "@ethersproject/bignumber"; import { BaseContract, Contract } from "@ethersproject/contracts"; import { Libraries } from "@nomiclabs/hardhat-ethers/types"; import { ethers } from "hardhat"; @@ -24,7 +25,40 @@ export async function getContractAt( )) as unknown as Type; return ctr; } +export const toETHNum = (amount: BigNumber): number => { + return Number(ethers.utils.formatEther(amount)); +}; +export const weightedAverageSimulation = ( + amount: number, + targetAmount: number, + startTime: number, + endTime: number, + blockTimestamp: number +) => { + // Currently in an update, return weighted average + if (blockTimestamp < startTime) { + // Update hasn't started, apply no weighting + return amount; + } else if (blockTimestamp > endTime) { + // Update is over, return target amount + return targetAmount; + } else { + if (targetAmount > amount) { + return ( + amount + + ((targetAmount - amount) * (blockTimestamp - startTime)) / + (endTime - startTime) + ); + } else { + return ( + amount - + ((amount - targetAmount) * (blockTimestamp - startTime)) / + (endTime - startTime) + ); + } + } +}; export const maxExpArray = [ /* 0 */ "0xd7", /* 1 */ "0x19f",